mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 08:03:39 +00:00
feat(coding-agent): add "none" option to doubleEscapeAction setting
Allows disabling double-escape behavior entirely for users who accidentally trigger the tree/fork selector. Fixes #973
This commit is contained in:
parent
0587f045d9
commit
cb08758696
4 changed files with 51 additions and 25 deletions
|
|
@ -2,11 +2,16 @@
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- Added "none" option to `doubleEscapeAction` setting to disable double-escape behavior entirely ([#973](https://github.com/badlogic/pi-mono/issues/973) by [@juanibiapina](https://github.com/juanibiapina))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Read tool now handles macOS filenames with curly quotes (U+2019) and NFD Unicode normalization ([#1078](https://github.com/badlogic/pi-mono/issues/1078))
|
||||
- Respect .gitignore, .ignore, and .fdignore files when scanning package resources for skills, prompts, themes, and extensions ([#1072](https://github.com/badlogic/pi-mono/issues/1072))
|
||||
- Fixed tool call argument defaults when providers omit inputs ([#1065](https://github.com/badlogic/pi-mono/issues/1065))
|
||||
- Invalid JSON in settings.json no longer causes the file to be overwritten with empty settings ([#1054](https://github.com/badlogic/pi-mono/issues/1054))
|
||||
|
||||
## [0.50.3] - 2026-01-29
|
||||
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ export interface Settings {
|
|||
terminal?: TerminalSettings;
|
||||
images?: ImageSettings;
|
||||
enabledModels?: string[]; // Model patterns for cycling (same format as --models CLI flag)
|
||||
doubleEscapeAction?: "fork" | "tree"; // Action for double-escape with empty editor (default: "tree")
|
||||
doubleEscapeAction?: "fork" | "tree" | "none"; // Action for double-escape with empty editor (default: "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)
|
||||
|
|
@ -126,18 +126,21 @@ export class SettingsManager {
|
|||
private persist: boolean;
|
||||
private modifiedFields = new Set<keyof Settings>(); // Track fields modified during session
|
||||
private modifiedNestedFields = new Map<keyof Settings, Set<string>>(); // Track nested field modifications
|
||||
private globalSettingsLoadError: Error | null = null; // Track if settings file had parse errors
|
||||
|
||||
private constructor(
|
||||
settingsPath: string | null,
|
||||
projectSettingsPath: string | null,
|
||||
initialSettings: Settings,
|
||||
persist: boolean,
|
||||
loadError: Error | null = null,
|
||||
) {
|
||||
this.settingsPath = settingsPath;
|
||||
this.projectSettingsPath = projectSettingsPath;
|
||||
this.persist = persist;
|
||||
this.globalSettings = initialSettings;
|
||||
this.inMemoryProjectSettings = {};
|
||||
this.globalSettingsLoadError = loadError;
|
||||
const projectSettings = this.loadProjectSettings();
|
||||
this.settings = deepMergeSettings(this.globalSettings, projectSettings);
|
||||
}
|
||||
|
|
@ -146,8 +149,19 @@ export class SettingsManager {
|
|||
static create(cwd: string = process.cwd(), agentDir: string = getAgentDir()): SettingsManager {
|
||||
const settingsPath = join(agentDir, "settings.json");
|
||||
const projectSettingsPath = join(cwd, CONFIG_DIR_NAME, "settings.json");
|
||||
const globalSettings = SettingsManager.loadFromFile(settingsPath);
|
||||
return new SettingsManager(settingsPath, projectSettingsPath, globalSettings, true);
|
||||
|
||||
let globalSettings: Settings = {};
|
||||
let loadError: Error | null = null;
|
||||
|
||||
try {
|
||||
globalSettings = SettingsManager.loadFromFile(settingsPath);
|
||||
} catch (error) {
|
||||
loadError = error as Error;
|
||||
console.error(`Warning: Invalid JSON in ${settingsPath}: ${error}`);
|
||||
console.error(`Fix the syntax error to enable settings persistence.`);
|
||||
}
|
||||
|
||||
return new SettingsManager(settingsPath, projectSettingsPath, globalSettings, true, loadError);
|
||||
}
|
||||
|
||||
/** Create an in-memory SettingsManager (no file I/O) */
|
||||
|
|
@ -159,14 +173,9 @@ export class SettingsManager {
|
|||
if (!existsSync(path)) {
|
||||
return {};
|
||||
}
|
||||
try {
|
||||
const content = readFileSync(path, "utf-8");
|
||||
const settings = JSON.parse(content);
|
||||
return SettingsManager.migrateSettings(settings);
|
||||
} catch (error) {
|
||||
console.error(`Warning: Could not read settings file ${path}: ${error}`);
|
||||
return {};
|
||||
}
|
||||
const content = readFileSync(path, "utf-8");
|
||||
const settings = JSON.parse(content);
|
||||
return SettingsManager.migrateSettings(settings);
|
||||
}
|
||||
|
||||
/** Migrate old settings format to new format */
|
||||
|
|
@ -247,6 +256,14 @@ export class SettingsManager {
|
|||
|
||||
private save(): void {
|
||||
if (this.persist && this.settingsPath) {
|
||||
// Don't overwrite if the file had parse errors on initial load
|
||||
if (this.globalSettingsLoadError) {
|
||||
// Re-merge to update active settings even though we can't persist
|
||||
const projectSettings = this.loadProjectSettings();
|
||||
this.settings = deepMergeSettings(this.globalSettings, projectSettings);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const dir = dirname(this.settingsPath);
|
||||
if (!existsSync(dir)) {
|
||||
|
|
@ -282,6 +299,7 @@ export class SettingsManager {
|
|||
this.globalSettings = mergedSettings;
|
||||
writeFileSync(this.settingsPath, JSON.stringify(this.globalSettings, null, 2), "utf-8");
|
||||
} catch (error) {
|
||||
// File may have been externally modified with invalid JSON - don't overwrite
|
||||
console.error(`Warning: Could not save settings file: ${error}`);
|
||||
}
|
||||
}
|
||||
|
|
@ -644,11 +662,11 @@ export class SettingsManager {
|
|||
this.save();
|
||||
}
|
||||
|
||||
getDoubleEscapeAction(): "fork" | "tree" {
|
||||
getDoubleEscapeAction(): "fork" | "tree" | "none" {
|
||||
return this.settings.doubleEscapeAction ?? "tree";
|
||||
}
|
||||
|
||||
setDoubleEscapeAction(action: "fork" | "tree"): void {
|
||||
setDoubleEscapeAction(action: "fork" | "tree" | "none"): void {
|
||||
this.globalSettings.doubleEscapeAction = action;
|
||||
this.markModified("doubleEscapeAction");
|
||||
this.save();
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ export interface SettingsConfig {
|
|||
availableThemes: string[];
|
||||
hideThinkingBlock: boolean;
|
||||
collapseChangelog: boolean;
|
||||
doubleEscapeAction: "fork" | "tree";
|
||||
doubleEscapeAction: "fork" | "tree" | "none";
|
||||
showHardwareCursor: boolean;
|
||||
editorPaddingX: number;
|
||||
autocompleteMaxVisible: number;
|
||||
|
|
@ -55,7 +55,7 @@ export interface SettingsCallbacks {
|
|||
onThemePreview?: (theme: string) => void;
|
||||
onHideThinkingBlockChange: (hidden: boolean) => void;
|
||||
onCollapseChangelogChange: (collapsed: boolean) => void;
|
||||
onDoubleEscapeActionChange: (action: "fork" | "tree") => void;
|
||||
onDoubleEscapeActionChange: (action: "fork" | "tree" | "none") => void;
|
||||
onShowHardwareCursorChange: (enabled: boolean) => void;
|
||||
onEditorPaddingXChange: (padding: number) => void;
|
||||
onAutocompleteMaxVisibleChange: (maxVisible: number) => void;
|
||||
|
|
@ -186,7 +186,7 @@ export class SettingsSelectorComponent extends Container {
|
|||
label: "Double-escape action",
|
||||
description: "Action when pressing Escape twice with empty editor",
|
||||
currentValue: config.doubleEscapeAction,
|
||||
values: ["tree", "fork"],
|
||||
values: ["tree", "fork", "none"],
|
||||
},
|
||||
{
|
||||
id: "thinking",
|
||||
|
|
|
|||
|
|
@ -1740,17 +1740,20 @@ export class InteractiveMode {
|
|||
this.isBashMode = false;
|
||||
this.updateEditorBorderColor();
|
||||
} else if (!this.editor.getText().trim()) {
|
||||
// Double-escape with empty editor triggers /tree or /fork based on setting
|
||||
const now = Date.now();
|
||||
if (now - this.lastEscapeTime < 500) {
|
||||
if (this.settingsManager.getDoubleEscapeAction() === "tree") {
|
||||
this.showTreeSelector();
|
||||
// Double-escape with empty editor triggers /tree, /fork, or nothing based on setting
|
||||
const action = this.settingsManager.getDoubleEscapeAction();
|
||||
if (action !== "none") {
|
||||
const now = Date.now();
|
||||
if (now - this.lastEscapeTime < 500) {
|
||||
if (action === "tree") {
|
||||
this.showTreeSelector();
|
||||
} else {
|
||||
this.showUserMessageSelector();
|
||||
}
|
||||
this.lastEscapeTime = 0;
|
||||
} else {
|
||||
this.showUserMessageSelector();
|
||||
this.lastEscapeTime = now;
|
||||
}
|
||||
this.lastEscapeTime = 0;
|
||||
} else {
|
||||
this.lastEscapeTime = now;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue