fix(cli): persist /settings general toggles (mode, auto-approve, compaction) across restarts (#12614)

* feat(core): persist plan/act mode, tool auto-approve, and compaction mode in global settings

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(cli): restore /settings general toggles across restarts

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* fix(core): make global settings updates cross-process safe

Targeted setters previously did unlocked read-modify-write cycles over the
shared global-settings.json, so concurrent hosts (two CLIs, or CLI + VS Code)
could silently discard each other's changes. Route all setters through a new
updateGlobalSettings(mutate) helper that re-reads the latest on-disk state
under a short-lived lock file (with stale-lock reclaim and a bounded wait)
and replaces the file atomically via temp-file rename so readers never see
torn writes.

* Revert "fix(core): make global settings updates cross-process safe"

This reverts commit 198c1c831b.

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
This commit is contained in:
Saoud Rizwan
2026-07-28 08:56:55 -07:00
committed by GitHub
parent dc175c73a8
commit e3ff875e09
12 changed files with 589 additions and 13 deletions
+1
View File
@@ -136,6 +136,7 @@ export function commanderToParsedArgs(program: Command): ParsedArgs {
interactive: !!opts.tui,
outputMode: opts.json ? "json" : "text",
mode: opts.plan ? "plan" : opts.yolo ? "yolo" : opts.zen ? "zen" : "act",
modeExplicitlySet: !!(opts.plan || opts.act || opts.yolo || opts.zen),
sandbox: !!opts.dataDir,
acpMode: !!opts.acp,
thinking: false,
+189 -1
View File
@@ -1,4 +1,6 @@
import { fstatSync } from "node:fs";
import { fstatSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type {
CliMigrationNotice,
@@ -18,6 +20,7 @@ vi.mock("node:fs", async () => {
const originalArgv = [...process.argv];
const originalStdinIsTTY = process.stdin.isTTY;
const originalStdoutIsTTY = process.stdout.isTTY;
const originalGlobalSettingsPath = process.env.CLINE_GLOBAL_SETTINGS_PATH;
const mockState = vi.hoisted(() => ({
runAgentImports: 0,
runInteractiveImports: 0,
@@ -216,8 +219,17 @@ vi.mock("./utils/telemetry", () => telemetryMocks);
vi.mock("./utils/worktree", () => worktreeMocks);
describe("runCli lightweight command dispatch", () => {
let globalSettingsRoot: string | undefined;
beforeEach(() => {
process.exitCode = undefined;
// Startup now reads persisted general settings; point the resolver at a
// fresh temp file so the developer's real settings cannot leak in.
globalSettingsRoot = mkdtempSync(join(tmpdir(), "cline-cli-main-test-"));
process.env.CLINE_GLOBAL_SETTINGS_PATH = join(
globalSettingsRoot,
"global-settings.json",
);
mockState.runAgentImports = 0;
mockState.runInteractiveImports = 0;
mockState.runAgentCalls = 0;
@@ -317,6 +329,16 @@ describe("runCli lightweight command dispatch", () => {
afterEach(() => {
process.exitCode = undefined;
if (originalGlobalSettingsPath === undefined) {
delete process.env.CLINE_GLOBAL_SETTINGS_PATH;
} else {
process.env.CLINE_GLOBAL_SETTINGS_PATH = originalGlobalSettingsPath;
}
if (globalSettingsRoot) {
rmSync(globalSettingsRoot, { recursive: true, force: true });
globalSettingsRoot = undefined;
}
process.argv = [...originalArgv];
Object.defineProperty(process.stdin, "isTTY", {
value: originalStdinIsTTY,
@@ -915,6 +937,172 @@ describe("runCli lightweight command dispatch", () => {
);
});
describe("persisted general settings at startup", () => {
function writePersistedSettings(settings: Record<string, unknown>) {
const path = process.env.CLINE_GLOBAL_SETTINGS_PATH;
if (!path) {
throw new Error("CLINE_GLOBAL_SETTINGS_PATH is not set");
}
writeFileSync(path, JSON.stringify(settings));
}
it("restores the persisted plan mode when no mode flag is provided", async () => {
writePersistedSettings({ planActMode: "plan" });
promptMocks.resolveSystemPrompt.mockClear();
process.argv = ["bun", "src/index.ts"];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(runtimeMocks.runInteractive).toHaveBeenCalledWith(
expect.objectContaining({ mode: "plan" }),
expect.anything(),
undefined,
expect.any(Object),
);
expect(promptMocks.resolveSystemPrompt).toHaveBeenCalledWith(
expect.objectContaining({ mode: "plan" }),
);
});
it("prefers an explicit --act flag over the persisted plan mode", async () => {
writePersistedSettings({ planActMode: "plan" });
promptMocks.resolveSystemPrompt.mockClear();
process.argv = ["bun", "src/index.ts", "--act"];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(runtimeMocks.runInteractive).toHaveBeenCalledWith(
expect.objectContaining({ mode: "act" }),
expect.anything(),
undefined,
expect.any(Object),
);
expect(promptMocks.resolveSystemPrompt).toHaveBeenCalledWith(
expect.objectContaining({ mode: "act" }),
);
});
it("restores the persisted auto-approve setting as a runtime policy", async () => {
writePersistedSettings({ toolAutoApprove: false });
process.argv = ["bun", "src/index.ts"];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(runtimeMocks.runInteractive).toHaveBeenCalledWith(
expect.objectContaining({
defaultToolAutoApprove: true,
toolPolicies: {
"*": { autoApprove: false },
},
}),
expect.anything(),
undefined,
expect.any(Object),
);
});
it("prefers an explicit --auto-approve flag over the persisted setting", async () => {
writePersistedSettings({ toolAutoApprove: false });
process.argv = ["bun", "src/index.ts", "--auto-approve", "true"];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(runtimeMocks.runInteractive).toHaveBeenCalledWith(
expect.objectContaining({
toolPolicies: {
"*": { autoApprove: true },
},
}),
expect.anything(),
undefined,
expect.any(Object),
);
});
it("restores disabled compaction across restarts", async () => {
writePersistedSettings({
compactionEnabled: false,
compactionStrategy: "basic",
});
process.argv = ["bun", "src/index.ts"];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(runtimeMocks.runInteractive).toHaveBeenCalledWith(
expect.objectContaining({
compaction: { enabled: false },
}),
expect.anything(),
undefined,
expect.any(Object),
);
});
it("restores the persisted compaction strategy across restarts", async () => {
writePersistedSettings({
compactionEnabled: true,
compactionStrategy: "basic",
});
process.argv = ["bun", "src/index.ts"];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(runtimeMocks.runInteractive).toHaveBeenCalledWith(
expect.objectContaining({
compaction: { enabled: true, strategy: "basic" },
}),
expect.anything(),
undefined,
expect.any(Object),
);
});
it("prefers an explicit --compaction flag over the persisted mode", async () => {
writePersistedSettings({ compactionEnabled: false });
process.argv = ["bun", "src/index.ts", "--compaction", "agentic"];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(runtimeMocks.runInteractive).toHaveBeenCalledWith(
expect.objectContaining({
compaction: { enabled: true, strategy: "agentic" },
}),
expect.anything(),
undefined,
expect.any(Object),
);
});
it("applies persisted settings to single-prompt runs as well", async () => {
writePersistedSettings({
compactionEnabled: true,
compactionStrategy: "basic",
planActMode: "plan",
});
forcePromptModeInput();
process.argv = ["bun", "src/index.ts", "say hello"];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(runtimeMocks.runAgent).toHaveBeenCalledWith(
"say hello",
expect.objectContaining({
compaction: { enabled: true, strategy: "basic" },
mode: "plan",
}),
expect.anything(),
);
});
});
it("forces chat view when resuming a session", async () => {
process.argv = ["bun", "src/index.ts", "--id", "sess_123"];
+29 -11
View File
@@ -43,6 +43,11 @@ import {
normalizeProviderId,
} from "./utils/provider-auth";
import { resolveCliReasoning } from "./utils/reasoning";
import {
resolveStartupCompactionMode,
resolveStartupMode,
resolveStartupToolAutoApprove,
} from "./utils/startup-settings";
import { rewriteTeamPrompt, TEAM_COMMAND_USAGE } from "./utils/team-command";
import {
captureCliExtensionActivated,
@@ -865,14 +870,6 @@ export async function runCli(): Promise<void> {
}
}
setCurrentOutputMode(args.outputMode);
const defaultToolAutoApprove = true;
const effectiveToolAutoApprove =
args.autoApproveOverride ?? defaultToolAutoApprove;
const toolPolicies: Record<string, ToolPolicy> = {
"*": {
autoApprove: effectiveToolAutoApprove,
},
};
if (args.outputMode === "json" && (args.interactive || !args.prompt)) {
writeErr(
@@ -949,6 +946,27 @@ export async function runCli(): Promise<void> {
runAgent,
} = await loadCliRuntimeModules();
// General settings toggled in the TUI /settings panel persist to the
// global settings file; explicit CLI flags take precedence over the
// persisted values, which in turn override the built-in defaults.
const persistedGlobalSettings = coreServer.readGlobalSettings();
const defaultToolAutoApprove = true;
const effectiveToolAutoApprove = resolveStartupToolAutoApprove(
args,
persistedGlobalSettings,
defaultToolAutoApprove,
);
const toolPolicies: Record<string, ToolPolicy> = {
"*": {
autoApprove: effectiveToolAutoApprove,
},
};
const effectiveMode = resolveStartupMode(args, persistedGlobalSettings);
const effectiveCompactionMode = resolveStartupCompactionMode(
args,
persistedGlobalSettings,
);
// Register the SDK early logger as early as possible — before any
// provider settings reads — so the full startup sequence is captured.
// These components operate before/outside ClineCore sessions, so the
@@ -1107,13 +1125,13 @@ export async function runCli(): Promise<void> {
cwd,
explicitSystemPrompt: args.systemPrompt,
providerId: provider,
mode: args.mode ?? "act",
mode: effectiveMode,
}),
execution: {
maxConsecutiveMistakes: args.retries ?? 3,
},
checkpoint: CLI_DEFAULT_CHECKPOINT_CONFIG,
compaction: buildCliCompactionConfig(args.compactionMode),
compaction: buildCliCompactionConfig(effectiveCompactionMode),
timeoutSeconds: args.timeoutSeconds,
sandbox: sandboxEnabled,
sandboxDataDir,
@@ -1121,7 +1139,7 @@ export async function runCli(): Promise<void> {
thinking: resolvedReasoning.thinking,
reasoningEffort: resolvedReasoning.reasoningEffort,
outputMode: args.outputMode,
mode: args.mode,
mode: effectiveMode,
logger: loggerAdapter.core,
loggerConfig: loggerAdapter.runtimeConfig,
telemetry: getCliTelemetryService(loggerAdapter.core),
+8
View File
@@ -2,6 +2,9 @@ import {
getCurrentContextSize,
type ProviderSettings,
ProviderSettingsManager,
setCompactionModeGlobally,
setPlanActModeGlobally,
setToolAutoApproveGlobally,
type UserInstructionConfigService,
} from "@cline/core";
import { formatModeSwitchNotice } from "@cline/shared";
@@ -712,15 +715,20 @@ export async function runInteractive(
onTurnErrorReported: () => {},
onAutoApproveChange: (enabled) => {
setInteractiveAutoApprove(enabled);
setToolAutoApproveGlobally(enabled);
void refreshInteractiveSessionPolicies();
},
onCompactionModeChange: async (mode) => {
await sessionRuntime.ensureReady();
applyCliCompactionMode(config, mode);
setCompactionModeGlobally(mode);
await sessionRuntime.restartWithCurrentMessages();
},
onModeChange: async (mode) => {
if (!isInteractiveMode(mode)) return;
// Persist the user's choice immediately, even when the switch is
// deferred until the current turn aborts, so it survives restarts.
setPlanActModeGlobally(mode);
if (isRunning) {
pendingModeChange.current = mode;
pendingModeChange.source = "ui";
+10
View File
@@ -46,6 +46,7 @@ describe("parseArgs", () => {
interactive: false,
outputMode: "text",
mode: "act",
modeExplicitlySet: false,
sandbox: false,
acpMode: false,
thinking: false,
@@ -220,6 +221,15 @@ describe("parseArgs", () => {
expect(parsedYolo.autoApproveOverride).toBe(true);
});
it("marks explicit mode flags so persisted settings do not override them", () => {
expect(parseArgs([]).modeExplicitlySet).toBe(false);
expect(parseArgs(["Audit the repo"]).modeExplicitlySet).toBe(false);
expect(parseArgs(["--plan"]).modeExplicitlySet).toBe(true);
expect(parseArgs(["--act"]).modeExplicitlySet).toBe(true);
expect(parseArgs(["--yolo"]).modeExplicitlySet).toBe(true);
expect(parseArgs(["--zen", "do it"]).modeExplicitlySet).toBe(true);
});
it("parses --zen flag for background hub dispatch", () => {
const parsedLong = parseArgs(["--zen", "do it"]);
expect(parsedLong.mode).toBe("zen");
+113
View File
@@ -0,0 +1,113 @@
import type { GlobalSettings } from "@cline/core";
import { describe, expect, it } from "vitest";
import {
resolveStartupCompactionMode,
resolveStartupMode,
resolveStartupToolAutoApprove,
} from "./startup-settings";
function makeSettings(overrides: Partial<GlobalSettings> = {}): GlobalSettings {
return {
autoUpdateEnabled: true,
telemetryOptOut: false,
...overrides,
};
}
describe("resolveStartupMode", () => {
it("uses the parsed default when nothing is persisted", () => {
expect(
resolveStartupMode({ mode: "act", modeExplicitlySet: false }, makeSettings()),
).toBe("act");
});
it("restores the persisted plan/act mode when no mode flag is provided", () => {
expect(
resolveStartupMode(
{ mode: "act", modeExplicitlySet: false },
makeSettings({ planActMode: "plan" }),
),
).toBe("plan");
});
it("prefers an explicit mode flag over the persisted mode", () => {
expect(
resolveStartupMode(
{ mode: "act", modeExplicitlySet: true },
makeSettings({ planActMode: "plan" }),
),
).toBe("act");
expect(
resolveStartupMode(
{ mode: "yolo", modeExplicitlySet: true },
makeSettings({ planActMode: "plan" }),
),
).toBe("yolo");
});
});
describe("resolveStartupToolAutoApprove", () => {
it("falls back to the built-in default when nothing is persisted", () => {
expect(resolveStartupToolAutoApprove({}, makeSettings(), true)).toBe(true);
});
it("restores the persisted auto-approve setting", () => {
expect(
resolveStartupToolAutoApprove(
{},
makeSettings({ toolAutoApprove: false }),
true,
),
).toBe(false);
});
it("prefers an explicit --auto-approve flag over the persisted setting", () => {
expect(
resolveStartupToolAutoApprove(
{ autoApproveOverride: true },
makeSettings({ toolAutoApprove: false }),
true,
),
).toBe(true);
expect(
resolveStartupToolAutoApprove(
{ autoApproveOverride: false },
makeSettings({ toolAutoApprove: true }),
true,
),
).toBe(false);
});
});
describe("resolveStartupCompactionMode", () => {
it("returns undefined so Core's default applies when nothing is persisted", () => {
expect(resolveStartupCompactionMode({}, makeSettings())).toBeUndefined();
});
it("restores the persisted strategy", () => {
expect(
resolveStartupCompactionMode(
{},
makeSettings({ compactionEnabled: true, compactionStrategy: "basic" }),
),
).toBe("basic");
});
it("restores the off state even when a strategy is retained on disk", () => {
expect(
resolveStartupCompactionMode(
{},
makeSettings({ compactionEnabled: false, compactionStrategy: "basic" }),
),
).toBe("off");
});
it("prefers an explicit --compaction flag over the persisted mode", () => {
expect(
resolveStartupCompactionMode(
{ compactionMode: "agentic" },
makeSettings({ compactionEnabled: false }),
),
).toBe("agentic");
});
});
+46
View File
@@ -0,0 +1,46 @@
import type { GlobalSettings } from "@cline/core";
import type { CliAgentMode, CliCompactionMode, ParsedArgs } from "./types";
/**
* Resolves general settings at CLI startup with the precedence
* explicit CLI flag -> persisted global setting -> built-in default,
* so choices made in the TUI /settings panel survive restarts (see
* https://github.com/cline/cline/issues/12158).
*/
export function resolveStartupMode(
args: Pick<ParsedArgs, "mode" | "modeExplicitlySet">,
settings: GlobalSettings,
): CliAgentMode {
if (args.modeExplicitlySet) {
return args.mode;
}
return settings.planActMode ?? args.mode;
}
export function resolveStartupToolAutoApprove(
args: Pick<ParsedArgs, "autoApproveOverride">,
settings: GlobalSettings,
defaultToolAutoApprove: boolean,
): boolean {
return (
args.autoApproveOverride ?? settings.toolAutoApprove ?? defaultToolAutoApprove
);
}
/**
* Returns undefined when neither a flag nor a persisted value exists, so
* callers fall through to Core's compaction default.
*/
export function resolveStartupCompactionMode(
args: Pick<ParsedArgs, "compactionMode">,
settings: GlobalSettings,
): CliCompactionMode | undefined {
if (args.compactionMode) {
return args.compactionMode;
}
if (settings.compactionEnabled === false) {
return "off";
}
return settings.compactionStrategy;
}
+2
View File
@@ -71,6 +71,8 @@ export interface ParsedArgs {
interactive: boolean;
outputMode: CliOutputMode;
mode: CliAgentMode;
/** Whether a mode flag (--plan/--act/--yolo/--zen) was explicitly provided */
modeExplicitlySet?: boolean;
timeoutSeconds?: number;
invalidTimeoutSeconds?: string;
thinking: boolean;
+8
View File
@@ -509,7 +509,9 @@ export {
NoOpFeatureFlagsProvider,
} from "./services/feature-flags";
export type {
GlobalCompactionMode,
GlobalCompactionStrategy,
GlobalPlanActMode,
GlobalSettings,
} from "./services/global-settings";
export {
@@ -521,15 +523,21 @@ export {
isPluginDisabledGlobally,
isTelemetryOptedOutGlobally,
isToolDisabledGlobally,
readCompactionModeGlobally,
readCompactionStrategyGlobally,
readGlobalSettings,
readPlanActModeGlobally,
readToolAutoApproveGlobally,
resolveDisabledPluginPaths,
resolveDisabledToolNames,
setAutoUpdateEnabledGlobally,
setCompactionModeGlobally,
setCompactionStrategyGlobally,
setDisabledPlugin,
setDisabledTools,
setPlanActModeGlobally,
setTelemetryOptOutGlobally,
setToolAutoApproveGlobally,
toggleDisabledTool,
writeGlobalSettings,
} from "./services/global-settings";
@@ -5,13 +5,19 @@ import type { ITelemetryService } from "@cline/shared";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
GlobalSettingsSchema,
readCompactionModeGlobally,
readCompactionStrategyGlobally,
readGlobalSettings,
readPlanActModeGlobally,
readToolAutoApproveGlobally,
setAutoUpdateEnabledGlobally,
setCompactionModeGlobally,
setCompactionStrategyGlobally,
setDisabledPlugin,
setDisabledTools,
setPlanActModeGlobally,
setTelemetryOptOutGlobally,
setToolAutoApproveGlobally,
writeGlobalSettings,
} from "./global-settings";
@@ -217,6 +223,105 @@ describe("global-settings", () => {
}
});
it("normalizes the persisted general settings fields", () => {
expect(
GlobalSettingsSchema.parse({
compactionEnabled: false,
planActMode: "plan",
toolAutoApprove: false,
}),
).toEqual({
autoUpdateEnabled: true,
compactionEnabled: false,
planActMode: "plan",
telemetryOptOut: false,
toolAutoApprove: false,
});
// Invalid values fall back to unset instead of failing the whole parse.
expect(
GlobalSettingsSchema.parse({
compactionEnabled: "yes",
planActMode: "chaos",
toolAutoApprove: 42,
}),
).toEqual({
autoUpdateEnabled: true,
telemetryOptOut: false,
});
});
it("reads and writes the plan/act mode globally", async () => {
const root = await mkdtemp(join(tmpdir(), "core-global-settings-"));
try {
const settingsPath = join(root, "global-settings.json");
process.env.CLINE_GLOBAL_SETTINGS_PATH = settingsPath;
expect(readPlanActModeGlobally()).toBeUndefined();
setPlanActModeGlobally("plan");
expect(readPlanActModeGlobally()).toBe("plan");
setPlanActModeGlobally("act");
expect(readPlanActModeGlobally()).toBe("act");
expect(JSON.parse(await readFile(settingsPath, "utf8"))).toEqual({
autoUpdateEnabled: true,
planActMode: "act",
telemetryOptOut: false,
});
} finally {
await rm(root, { recursive: true, force: true });
}
});
it("reads and writes the tool auto-approve setting globally", async () => {
const root = await mkdtemp(join(tmpdir(), "core-global-settings-"));
try {
const settingsPath = join(root, "global-settings.json");
process.env.CLINE_GLOBAL_SETTINGS_PATH = settingsPath;
expect(readToolAutoApproveGlobally()).toBeUndefined();
setToolAutoApproveGlobally(false);
expect(readToolAutoApproveGlobally()).toBe(false);
setToolAutoApproveGlobally(true);
expect(readToolAutoApproveGlobally()).toBe(true);
} finally {
await rm(root, { recursive: true, force: true });
}
});
it("round-trips the compaction mode including the off state", async () => {
const root = await mkdtemp(join(tmpdir(), "core-global-settings-"));
try {
const settingsPath = join(root, "global-settings.json");
process.env.CLINE_GLOBAL_SETTINGS_PATH = settingsPath;
expect(readCompactionModeGlobally()).toBeUndefined();
setCompactionModeGlobally("basic");
expect(readCompactionModeGlobally()).toBe("basic");
// Turning compaction off retains the previous strategy on disk so
// re-enabling restores it.
setCompactionModeGlobally("off");
expect(readCompactionModeGlobally()).toBe("off");
expect(readGlobalSettings()).toEqual({
autoUpdateEnabled: true,
compactionEnabled: false,
compactionStrategy: "basic",
telemetryOptOut: false,
});
setCompactionModeGlobally("agentic");
expect(readCompactionModeGlobally()).toBe("agentic");
expect(readGlobalSettings()).toEqual({
autoUpdateEnabled: true,
compactionEnabled: true,
compactionStrategy: "agentic",
telemetryOptOut: false,
});
} finally {
await rm(root, { recursive: true, force: true });
}
});
describe("caching", () => {
it("invalidates the cache when writeGlobalSettings is called", async () => {
const root = await mkdtemp(join(tmpdir(), "core-global-settings-"));
@@ -37,11 +37,21 @@ export type GlobalCompactionStrategy = z.infer<
typeof GlobalCompactionStrategySchema
>;
/** Compaction strategy plus the "off" state surfaced by the CLI settings UI. */
export type GlobalCompactionMode = GlobalCompactionStrategy | "off";
const GlobalPlanActModeSchema = z.enum(["plan", "act"]);
export type GlobalPlanActMode = z.infer<typeof GlobalPlanActModeSchema>;
export const GlobalSettingsSchema = z
.object({
telemetryOptOut: z.boolean().default(false).catch(false),
autoUpdateEnabled: z.boolean().default(true).catch(true),
compactionStrategy: GlobalCompactionStrategySchema.optional(),
compactionEnabled: z.boolean().optional().catch(undefined),
planActMode: GlobalPlanActModeSchema.optional().catch(undefined),
toolAutoApprove: z.boolean().optional().catch(undefined),
disabledTools: GlobalSettingsStringListSchema.optional(),
disabledPlugins: GlobalSettingsStringListSchema.optional(),
})
@@ -51,6 +61,9 @@ export const GlobalSettingsSchema = z
telemetryOptOut: boolean;
autoUpdateEnabled: boolean;
compactionStrategy?: GlobalCompactionStrategy;
compactionEnabled?: boolean;
planActMode?: GlobalPlanActMode;
toolAutoApprove?: boolean;
disabledTools?: string[];
disabledPlugins?: string[];
} = {
@@ -60,6 +73,15 @@ export const GlobalSettingsSchema = z
if (settings.compactionStrategy) {
normalized.compactionStrategy = settings.compactionStrategy;
}
if (settings.compactionEnabled !== undefined) {
normalized.compactionEnabled = settings.compactionEnabled;
}
if (settings.planActMode) {
normalized.planActMode = settings.planActMode;
}
if (settings.toolAutoApprove !== undefined) {
normalized.toolAutoApprove = settings.toolAutoApprove;
}
if (settings.disabledTools?.length) {
normalized.disabledTools = settings.disabledTools;
}
@@ -203,6 +225,51 @@ export function setCompactionStrategyGlobally(
writeGlobalSettings({ ...readGlobalSettings(), compactionStrategy });
}
/**
* Returns the persisted compaction mode including the disabled state, or
* undefined when the user never chose one (callers apply their own default).
*/
export function readCompactionModeGlobally(): GlobalCompactionMode | undefined {
const settings = readGlobalSettings();
if (settings.compactionEnabled === false) {
return "off";
}
return settings.compactionStrategy;
}
/**
* Persists the full compaction mode. Selecting "off" keeps the previously
* chosen strategy so re-enabling compaction restores it.
*/
export function setCompactionModeGlobally(mode: GlobalCompactionMode): void {
const settings = readGlobalSettings();
if (mode === "off") {
writeGlobalSettings({ ...settings, compactionEnabled: false });
return;
}
writeGlobalSettings({
...settings,
compactionEnabled: true,
compactionStrategy: mode,
});
}
export function readPlanActModeGlobally(): GlobalPlanActMode | undefined {
return readGlobalSettings().planActMode;
}
export function setPlanActModeGlobally(planActMode: GlobalPlanActMode): void {
writeGlobalSettings({ ...readGlobalSettings(), planActMode });
}
export function readToolAutoApproveGlobally(): boolean | undefined {
return readGlobalSettings().toolAutoApprove;
}
export function setToolAutoApproveGlobally(toolAutoApprove: boolean): void {
writeGlobalSettings({ ...readGlobalSettings(), toolAutoApprove });
}
export function resolveDisabledToolNames(
disabledToolNames?: ReadonlyArray<string>,
): Set<string> {
+11 -1
View File
@@ -128,19 +128,29 @@ export {
isPluginDisabledGlobally,
isTelemetryOptedOutGlobally,
isToolDisabledGlobally,
readCompactionModeGlobally,
readCompactionStrategyGlobally,
readGlobalSettings,
readPlanActModeGlobally,
readToolAutoApproveGlobally,
resolveDisabledPluginPaths,
resolveDisabledToolNames,
setAutoUpdateEnabledGlobally,
setCompactionModeGlobally,
setCompactionStrategyGlobally,
setDisabledPlugin,
setDisabledTools,
setPlanActModeGlobally,
setTelemetryOptOutGlobally,
setToolAutoApproveGlobally,
toggleDisabledTool,
writeGlobalSettings,
} from "./services/global-settings";
export type { GlobalCompactionStrategy } from "./services/global-settings";
export type {
GlobalCompactionMode,
GlobalCompactionStrategy,
GlobalPlanActMode,
} from "./services/global-settings";
export type {
ListPluginToolsResult,
PluginToolSummary,