feat(coding-agent): add treeFilterMode setting for /tree default filter (#1852)

Add configurable initial filter mode for the session tree navigator.
Users who always switch to a specific filter (e.g. no-tools via Ctrl+T)
can now set it as default in settings.

Same pattern as doubleEscapeAction (#404). Filter infra from #747.
This commit is contained in:
lajarre 2026-03-05 21:25:58 +00:00 committed by GitHub
parent a0d839ce84
commit 1f39cc776a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 39 additions and 2 deletions

View file

@ -42,6 +42,7 @@ Edit directly or use `/settings` for common options.
| `quietStartup` | boolean | `false` | Hide startup header | | `quietStartup` | boolean | `false` | Hide startup header |
| `collapseChangelog` | boolean | `false` | Show condensed changelog after updates | | `collapseChangelog` | boolean | `false` | Show condensed changelog after updates |
| `doubleEscapeAction` | string | `"tree"` | Action for double-escape: `"tree"`, `"fork"`, or `"none"` | | `doubleEscapeAction` | string | `"tree"` | Action for double-escape: `"tree"`, `"fork"`, or `"none"` |
| `treeFilterMode` | string | `"default"` | Default filter for `/tree`: `"default"`, `"no-tools"`, `"user-only"`, `"labeled-only"`, `"all"` |
| `editorPaddingX` | number | `0` | Horizontal padding for input editor (0-3) | | `editorPaddingX` | number | `0` | Horizontal padding for input editor (0-3) |
| `autocompleteMaxVisible` | number | `5` | Max visible items in autocomplete dropdown (3-20) | | `autocompleteMaxVisible` | number | `5` | Max visible items in autocomplete dropdown (3-20) |
| `showHardwareCursor` | boolean | `false` | Show terminal cursor | | `showHardwareCursor` | boolean | `false` | Show terminal cursor |

View file

@ -87,6 +87,7 @@ export interface Settings {
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" | "none"; // Action for double-escape with empty editor (default: "tree") 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 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)
@ -866,6 +867,18 @@ export class SettingsManager {
this.save(); 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 { getShowHardwareCursor(): boolean {
return this.settings.showHardwareCursor ?? process.env.PI_HARDWARE_CURSOR === "1"; return this.settings.showHardwareCursor ?? process.env.PI_HARDWARE_CURSOR === "1";
} }

View file

@ -38,6 +38,7 @@ export interface SettingsConfig {
hideThinkingBlock: boolean; hideThinkingBlock: boolean;
collapseChangelog: boolean; collapseChangelog: boolean;
doubleEscapeAction: "fork" | "tree" | "none"; doubleEscapeAction: "fork" | "tree" | "none";
treeFilterMode: "default" | "no-tools" | "user-only" | "labeled-only" | "all";
showHardwareCursor: boolean; showHardwareCursor: boolean;
editorPaddingX: number; editorPaddingX: number;
autocompleteMaxVisible: number; autocompleteMaxVisible: number;
@ -60,6 +61,7 @@ export interface SettingsCallbacks {
onHideThinkingBlockChange: (hidden: boolean) => void; onHideThinkingBlockChange: (hidden: boolean) => void;
onCollapseChangelogChange: (collapsed: boolean) => void; onCollapseChangelogChange: (collapsed: boolean) => void;
onDoubleEscapeActionChange: (action: "fork" | "tree" | "none") => void; onDoubleEscapeActionChange: (action: "fork" | "tree" | "none") => void;
onTreeFilterModeChange: (mode: "default" | "no-tools" | "user-only" | "labeled-only" | "all") => 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;
@ -200,6 +202,13 @@ export class SettingsSelectorComponent extends Container {
currentValue: config.doubleEscapeAction, currentValue: config.doubleEscapeAction,
values: ["tree", "fork", "none"], values: ["tree", "fork", "none"],
}, },
{
id: "tree-filter-mode",
label: "Tree filter mode",
description: "Default filter when opening /tree",
currentValue: config.treeFilterMode,
values: ["default", "no-tools", "user-only", "labeled-only", "all"],
},
{ {
id: "thinking", id: "thinking",
label: "Thinking level", label: "Thinking level",
@ -379,6 +388,11 @@ export class SettingsSelectorComponent extends Container {
case "double-escape-action": case "double-escape-action":
callbacks.onDoubleEscapeActionChange(newValue as "fork" | "tree"); callbacks.onDoubleEscapeActionChange(newValue as "fork" | "tree");
break; break;
case "tree-filter-mode":
callbacks.onTreeFilterModeChange(
newValue as "default" | "no-tools" | "user-only" | "labeled-only" | "all",
);
break;
case "show-hardware-cursor": case "show-hardware-cursor":
callbacks.onShowHardwareCursorChange(newValue === "true"); callbacks.onShowHardwareCursorChange(newValue === "true");
break; break;

View file

@ -37,7 +37,7 @@ interface FlatNode {
} }
/** Filter mode for tree display */ /** Filter mode for tree display */
type FilterMode = "default" | "no-tools" | "user-only" | "labeled-only" | "all"; export type FilterMode = "default" | "no-tools" | "user-only" | "labeled-only" | "all";
/** /**
* Tree list component with selection and ASCII art visualization * Tree list component with selection and ASCII art visualization
@ -70,9 +70,11 @@ class TreeList implements Component {
currentLeafId: string | null, currentLeafId: string | null,
maxVisibleLines: number, maxVisibleLines: number,
initialSelectedId?: string, initialSelectedId?: string,
initialFilterMode?: FilterMode,
) { ) {
this.currentLeafId = currentLeafId; this.currentLeafId = currentLeafId;
this.maxVisibleLines = maxVisibleLines; this.maxVisibleLines = maxVisibleLines;
this.filterMode = initialFilterMode ?? "default";
this.multipleRoots = tree.length > 1; this.multipleRoots = tree.length > 1;
this.flatNodes = this.flattenTree(tree); this.flatNodes = this.flattenTree(tree);
this.buildActivePath(); this.buildActivePath();
@ -1001,13 +1003,14 @@ export class TreeSelectorComponent extends Container implements Focusable {
onCancel: () => void, onCancel: () => void,
onLabelChange?: (entryId: string, label: string | undefined) => void, onLabelChange?: (entryId: string, label: string | undefined) => void,
initialSelectedId?: string, initialSelectedId?: string,
initialFilterMode?: FilterMode,
) { ) {
super(); super();
this.onLabelChangeCallback = onLabelChange; this.onLabelChangeCallback = onLabelChange;
const maxVisibleLines = Math.max(5, Math.floor(terminalHeight / 2)); const maxVisibleLines = Math.max(5, Math.floor(terminalHeight / 2));
this.treeList = new TreeList(tree, currentLeafId, maxVisibleLines, initialSelectedId); this.treeList = new TreeList(tree, currentLeafId, maxVisibleLines, initialSelectedId, initialFilterMode);
this.treeList.onSelect = onSelect; this.treeList.onSelect = onSelect;
this.treeList.onCancel = onCancel; this.treeList.onCancel = onCancel;
this.treeList.onLabelEdit = (entryId, currentLabel) => this.showLabelInput(entryId, currentLabel); this.treeList.onLabelEdit = (entryId, currentLabel) => this.showLabelInput(entryId, currentLabel);

View file

@ -3045,6 +3045,7 @@ export class InteractiveMode {
hideThinkingBlock: this.hideThinkingBlock, hideThinkingBlock: this.hideThinkingBlock,
collapseChangelog: this.settingsManager.getCollapseChangelog(), collapseChangelog: this.settingsManager.getCollapseChangelog(),
doubleEscapeAction: this.settingsManager.getDoubleEscapeAction(), doubleEscapeAction: this.settingsManager.getDoubleEscapeAction(),
treeFilterMode: this.settingsManager.getTreeFilterMode(),
showHardwareCursor: this.settingsManager.getShowHardwareCursor(), showHardwareCursor: this.settingsManager.getShowHardwareCursor(),
editorPaddingX: this.settingsManager.getEditorPaddingX(), editorPaddingX: this.settingsManager.getEditorPaddingX(),
autocompleteMaxVisible: this.settingsManager.getAutocompleteMaxVisible(), autocompleteMaxVisible: this.settingsManager.getAutocompleteMaxVisible(),
@ -3124,6 +3125,9 @@ export class InteractiveMode {
onDoubleEscapeActionChange: (action) => { onDoubleEscapeActionChange: (action) => {
this.settingsManager.setDoubleEscapeAction(action); this.settingsManager.setDoubleEscapeAction(action);
}, },
onTreeFilterModeChange: (mode) => {
this.settingsManager.setTreeFilterMode(mode);
},
onShowHardwareCursorChange: (enabled) => { onShowHardwareCursorChange: (enabled) => {
this.settingsManager.setShowHardwareCursor(enabled); this.settingsManager.setShowHardwareCursor(enabled);
this.ui.setShowHardwareCursor(enabled); this.ui.setShowHardwareCursor(enabled);
@ -3413,6 +3417,7 @@ export class InteractiveMode {
private showTreeSelector(initialSelectedId?: string): void { private showTreeSelector(initialSelectedId?: string): void {
const tree = this.sessionManager.getTree(); const tree = this.sessionManager.getTree();
const realLeafId = this.sessionManager.getLeafId(); const realLeafId = this.sessionManager.getLeafId();
const initialFilterMode = this.settingsManager.getTreeFilterMode();
if (tree.length === 0) { if (tree.length === 0) {
this.showStatus("No entries in session"); this.showStatus("No entries in session");
@ -3531,6 +3536,7 @@ export class InteractiveMode {
this.ui.requestRender(); this.ui.requestRender();
}, },
initialSelectedId, initialSelectedId,
initialFilterMode,
); );
return { component: selector, focus: selector }; return { component: selector, focus: selector };
}); });