mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-20 14:05:08 +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]
|
## [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
|
### 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))
|
- 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))
|
- 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))
|
- 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
|
## [0.50.3] - 2026-01-29
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@ export interface Settings {
|
||||||
terminal?: TerminalSettings;
|
terminal?: TerminalSettings;
|
||||||
images?: ImageSettings;
|
images?: ImageSettings;
|
||||||
enabledModels?: string[]; // Model patterns for cycling (same format as --models CLI flag)
|
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
|
thinkingBudgets?: ThinkingBudgetsSettings; // Custom token budgets for thinking levels
|
||||||
editorPaddingX?: number; // Horizontal padding for input editor (default: 0)
|
editorPaddingX?: number; // Horizontal padding for input editor (default: 0)
|
||||||
autocompleteMaxVisible?: number; // Max visible items in autocomplete dropdown (default: 5)
|
autocompleteMaxVisible?: number; // Max visible items in autocomplete dropdown (default: 5)
|
||||||
|
|
@ -126,18 +126,21 @@ export class SettingsManager {
|
||||||
private persist: boolean;
|
private persist: boolean;
|
||||||
private modifiedFields = new Set<keyof Settings>(); // Track fields modified during session
|
private modifiedFields = new Set<keyof Settings>(); // Track fields modified during session
|
||||||
private modifiedNestedFields = new Map<keyof Settings, Set<string>>(); // Track nested field modifications
|
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(
|
private constructor(
|
||||||
settingsPath: string | null,
|
settingsPath: string | null,
|
||||||
projectSettingsPath: string | null,
|
projectSettingsPath: string | null,
|
||||||
initialSettings: Settings,
|
initialSettings: Settings,
|
||||||
persist: boolean,
|
persist: boolean,
|
||||||
|
loadError: Error | null = null,
|
||||||
) {
|
) {
|
||||||
this.settingsPath = settingsPath;
|
this.settingsPath = settingsPath;
|
||||||
this.projectSettingsPath = projectSettingsPath;
|
this.projectSettingsPath = projectSettingsPath;
|
||||||
this.persist = persist;
|
this.persist = persist;
|
||||||
this.globalSettings = initialSettings;
|
this.globalSettings = initialSettings;
|
||||||
this.inMemoryProjectSettings = {};
|
this.inMemoryProjectSettings = {};
|
||||||
|
this.globalSettingsLoadError = loadError;
|
||||||
const projectSettings = this.loadProjectSettings();
|
const projectSettings = this.loadProjectSettings();
|
||||||
this.settings = deepMergeSettings(this.globalSettings, projectSettings);
|
this.settings = deepMergeSettings(this.globalSettings, projectSettings);
|
||||||
}
|
}
|
||||||
|
|
@ -146,8 +149,19 @@ export class SettingsManager {
|
||||||
static create(cwd: string = process.cwd(), agentDir: string = getAgentDir()): SettingsManager {
|
static create(cwd: string = process.cwd(), agentDir: string = getAgentDir()): SettingsManager {
|
||||||
const settingsPath = join(agentDir, "settings.json");
|
const settingsPath = join(agentDir, "settings.json");
|
||||||
const projectSettingsPath = join(cwd, CONFIG_DIR_NAME, "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) */
|
/** Create an in-memory SettingsManager (no file I/O) */
|
||||||
|
|
@ -159,14 +173,9 @@ export class SettingsManager {
|
||||||
if (!existsSync(path)) {
|
if (!existsSync(path)) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
const content = readFileSync(path, "utf-8");
|
const content = readFileSync(path, "utf-8");
|
||||||
const settings = JSON.parse(content);
|
const settings = JSON.parse(content);
|
||||||
return SettingsManager.migrateSettings(settings);
|
return SettingsManager.migrateSettings(settings);
|
||||||
} catch (error) {
|
|
||||||
console.error(`Warning: Could not read settings file ${path}: ${error}`);
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Migrate old settings format to new format */
|
/** Migrate old settings format to new format */
|
||||||
|
|
@ -247,6 +256,14 @@ export class SettingsManager {
|
||||||
|
|
||||||
private save(): void {
|
private save(): void {
|
||||||
if (this.persist && this.settingsPath) {
|
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 {
|
try {
|
||||||
const dir = dirname(this.settingsPath);
|
const dir = dirname(this.settingsPath);
|
||||||
if (!existsSync(dir)) {
|
if (!existsSync(dir)) {
|
||||||
|
|
@ -282,6 +299,7 @@ export class SettingsManager {
|
||||||
this.globalSettings = mergedSettings;
|
this.globalSettings = mergedSettings;
|
||||||
writeFileSync(this.settingsPath, JSON.stringify(this.globalSettings, null, 2), "utf-8");
|
writeFileSync(this.settingsPath, JSON.stringify(this.globalSettings, null, 2), "utf-8");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// File may have been externally modified with invalid JSON - don't overwrite
|
||||||
console.error(`Warning: Could not save settings file: ${error}`);
|
console.error(`Warning: Could not save settings file: ${error}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -644,11 +662,11 @@ export class SettingsManager {
|
||||||
this.save();
|
this.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
getDoubleEscapeAction(): "fork" | "tree" {
|
getDoubleEscapeAction(): "fork" | "tree" | "none" {
|
||||||
return this.settings.doubleEscapeAction ?? "tree";
|
return this.settings.doubleEscapeAction ?? "tree";
|
||||||
}
|
}
|
||||||
|
|
||||||
setDoubleEscapeAction(action: "fork" | "tree"): void {
|
setDoubleEscapeAction(action: "fork" | "tree" | "none"): void {
|
||||||
this.globalSettings.doubleEscapeAction = action;
|
this.globalSettings.doubleEscapeAction = action;
|
||||||
this.markModified("doubleEscapeAction");
|
this.markModified("doubleEscapeAction");
|
||||||
this.save();
|
this.save();
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ export interface SettingsConfig {
|
||||||
availableThemes: string[];
|
availableThemes: string[];
|
||||||
hideThinkingBlock: boolean;
|
hideThinkingBlock: boolean;
|
||||||
collapseChangelog: boolean;
|
collapseChangelog: boolean;
|
||||||
doubleEscapeAction: "fork" | "tree";
|
doubleEscapeAction: "fork" | "tree" | "none";
|
||||||
showHardwareCursor: boolean;
|
showHardwareCursor: boolean;
|
||||||
editorPaddingX: number;
|
editorPaddingX: number;
|
||||||
autocompleteMaxVisible: number;
|
autocompleteMaxVisible: number;
|
||||||
|
|
@ -55,7 +55,7 @@ export interface SettingsCallbacks {
|
||||||
onThemePreview?: (theme: string) => void;
|
onThemePreview?: (theme: string) => void;
|
||||||
onHideThinkingBlockChange: (hidden: boolean) => void;
|
onHideThinkingBlockChange: (hidden: boolean) => void;
|
||||||
onCollapseChangelogChange: (collapsed: boolean) => void;
|
onCollapseChangelogChange: (collapsed: boolean) => void;
|
||||||
onDoubleEscapeActionChange: (action: "fork" | "tree") => void;
|
onDoubleEscapeActionChange: (action: "fork" | "tree" | "none") => void;
|
||||||
onShowHardwareCursorChange: (enabled: boolean) => void;
|
onShowHardwareCursorChange: (enabled: boolean) => void;
|
||||||
onEditorPaddingXChange: (padding: number) => void;
|
onEditorPaddingXChange: (padding: number) => void;
|
||||||
onAutocompleteMaxVisibleChange: (maxVisible: number) => void;
|
onAutocompleteMaxVisibleChange: (maxVisible: number) => void;
|
||||||
|
|
@ -186,7 +186,7 @@ export class SettingsSelectorComponent extends Container {
|
||||||
label: "Double-escape action",
|
label: "Double-escape action",
|
||||||
description: "Action when pressing Escape twice with empty editor",
|
description: "Action when pressing Escape twice with empty editor",
|
||||||
currentValue: config.doubleEscapeAction,
|
currentValue: config.doubleEscapeAction,
|
||||||
values: ["tree", "fork"],
|
values: ["tree", "fork", "none"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "thinking",
|
id: "thinking",
|
||||||
|
|
|
||||||
|
|
@ -1740,10 +1740,12 @@ export class InteractiveMode {
|
||||||
this.isBashMode = false;
|
this.isBashMode = false;
|
||||||
this.updateEditorBorderColor();
|
this.updateEditorBorderColor();
|
||||||
} else if (!this.editor.getText().trim()) {
|
} else if (!this.editor.getText().trim()) {
|
||||||
// Double-escape with empty editor triggers /tree or /fork based on setting
|
// 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();
|
const now = Date.now();
|
||||||
if (now - this.lastEscapeTime < 500) {
|
if (now - this.lastEscapeTime < 500) {
|
||||||
if (this.settingsManager.getDoubleEscapeAction() === "tree") {
|
if (action === "tree") {
|
||||||
this.showTreeSelector();
|
this.showTreeSelector();
|
||||||
} else {
|
} else {
|
||||||
this.showUserMessageSelector();
|
this.showUserMessageSelector();
|
||||||
|
|
@ -1753,6 +1755,7 @@ export class InteractiveMode {
|
||||||
this.lastEscapeTime = now;
|
this.lastEscapeTime = now;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Register app action handlers
|
// Register app action handlers
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue