mirror of
https://github.com/cline/cline.git
synced 2026-08-28 19:48:08 +08:00
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:
@@ -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
@@ -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
@@ -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),
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user