mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 10:05:14 +00:00
940 lines
28 KiB
TypeScript
940 lines
28 KiB
TypeScript
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<string, unknown>)[key] = { ...baseValue, ...overrideValue };
|
|
} else {
|
|
// For primitives and arrays, override value wins
|
|
(result as Record<string, unknown>)[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<keyof Settings>(); // Track global fields modified during session
|
|
private modifiedNestedFields = new Map<keyof Settings, Set<string>>(); // Track global nested field modifications
|
|
private modifiedProjectFields = new Set<keyof Settings>(); // Track project fields modified during session
|
|
private modifiedProjectNestedFields = new Map<keyof Settings, Set<string>>(); // 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<void> = 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<Settings> = {}): 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<string, unknown>): 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<Settings>): 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<keyof Settings, Set<string>>): Map<keyof Settings, Set<string>> {
|
|
const snapshot = new Map<keyof Settings, Set<string>>();
|
|
for (const [key, value] of source.entries()) {
|
|
snapshot.set(key, new Set(value));
|
|
}
|
|
return snapshot;
|
|
}
|
|
|
|
private persistScopedSettings(
|
|
scope: SettingsScope,
|
|
snapshotSettings: Settings,
|
|
modifiedFields: Set<keyof Settings>,
|
|
modifiedNestedFields: Map<keyof Settings, Set<string>>,
|
|
): void {
|
|
this.storage.withLock(scope, (current) => {
|
|
const currentFileSettings = current
|
|
? SettingsManager.migrateSettings(JSON.parse(current) as Record<string, unknown>)
|
|
: {};
|
|
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<string, unknown>) ?? {};
|
|
const inMemoryNested = value as Record<string, unknown>;
|
|
const mergedNested = { ...baseNested };
|
|
for (const nestedKey of nestedModified) {
|
|
mergedNested[nestedKey] = inMemoryNested[nestedKey];
|
|
}
|
|
(mergedSettings as Record<string, unknown>)[field] = mergedNested;
|
|
} else {
|
|
(mergedSettings as Record<string, unknown>)[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<void> {
|
|
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 ?? {});
|
|
}
|
|
}
|