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 |
| `collapseChangelog` | boolean | `false` | Show condensed changelog after updates |
| `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) |
| `autocompleteMaxVisible` | number | `5` | Max visible items in autocomplete dropdown (3-20) |
| `showHardwareCursor` | boolean | `false` | Show terminal cursor |

View file

@ -87,6 +87,7 @@ export interface Settings {
images?: ImageSettings;
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")
treeFilterMode?: "default" | "no-tools" | "user-only" | "labeled-only" | "all"; // Default filter when opening /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)
@ -866,6 +867,18 @@ export class SettingsManager {
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 {
return this.settings.showHardwareCursor ?? process.env.PI_HARDWARE_CURSOR === "1";
}

View file

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

View file

@ -37,7 +37,7 @@ interface FlatNode {
}
/** 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
@ -70,9 +70,11 @@ class TreeList implements Component {
currentLeafId: string | null,
maxVisibleLines: number,
initialSelectedId?: string,
initialFilterMode?: FilterMode,
) {
this.currentLeafId = currentLeafId;
this.maxVisibleLines = maxVisibleLines;
this.filterMode = initialFilterMode ?? "default";
this.multipleRoots = tree.length > 1;
this.flatNodes = this.flattenTree(tree);
this.buildActivePath();
@ -1001,13 +1003,14 @@ export class TreeSelectorComponent extends Container implements Focusable {
onCancel: () => void,
onLabelChange?: (entryId: string, label: string | undefined) => void,
initialSelectedId?: string,
initialFilterMode?: FilterMode,
) {
super();
this.onLabelChangeCallback = onLabelChange;
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.onCancel = onCancel;
this.treeList.onLabelEdit = (entryId, currentLabel) => this.showLabelInput(entryId, currentLabel);

View file

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