Tree selector improvements: active line highlight and tool filter

- Add inverse background highlight for selected/active line
- Add 'no-tools' filter mode to hide tool results
- Add isShiftCtrlO to cycle filters backwards
- Filter order: default → no-tools → user-only → labeled-only → all
This commit is contained in:
Mario Zechner 2025-12-30 01:40:13 +01:00
parent 04ce66951e
commit e4df5d14b5
3 changed files with 28 additions and 4 deletions

View file

@ -11,6 +11,7 @@ import {
isCtrlO,
isEnter,
isEscape,
isShiftCtrlO,
Spacer,
Text,
TruncatedText,
@ -42,7 +43,7 @@ interface FlatNode {
}
/** Filter mode for tree display */
type FilterMode = "default" | "user-only" | "labeled-only" | "all";
type FilterMode = "default" | "no-tools" | "user-only" | "labeled-only" | "all";
/**
* Tree list component with selection and ASCII art visualization
@ -265,6 +266,9 @@ class TreeList implements Component {
passesFilter =
(entry.type === "message" && entry.message.role === "user") ||
(entry.type === "custom_message" && entry.display);
} else if (this.filterMode === "no-tools") {
// Hide tool results
passesFilter = !(entry.type === "message" && entry.message.role === "toolResult");
} else if (this.filterMode === "labeled-only") {
passesFilter = flatNode.node.label !== undefined;
} else if (this.filterMode !== "all") {
@ -373,6 +377,8 @@ class TreeList implements Component {
private getFilterLabel(): string {
switch (this.filterMode) {
case "no-tools":
return " [no-tools]";
case "user-only":
return " [user]";
case "labeled-only":
@ -456,7 +462,10 @@ class TreeList implements Component {
const label = flatNode.node.label ? theme.fg("warning", `[${flatNode.node.label}] `) : "";
const content = this.getEntryDisplayText(flatNode.node, isSelected);
const line = cursor + theme.fg("dim", prefix) + pathMarker + label + content;
let line = cursor + theme.fg("dim", prefix) + pathMarker + label + content;
if (isSelected) {
line = theme.inverse(line);
}
lines.push(truncateToWidth(line, width));
}
@ -663,9 +672,15 @@ class TreeList implements Component {
}
} else if (isCtrlC(keyData)) {
this.onCancel?.();
} else if (isShiftCtrlO(keyData)) {
// Cycle filter backwards
const modes: FilterMode[] = ["default", "no-tools", "user-only", "labeled-only", "all"];
const currentIndex = modes.indexOf(this.filterMode);
this.filterMode = modes[(currentIndex - 1 + modes.length) % modes.length];
this.applyFilter();
} else if (isCtrlO(keyData)) {
// Cycle filter: default → user-only → labeled-only → all → default
const modes: FilterMode[] = ["default", "user-only", "labeled-only", "all"];
// Cycle filter forwards: default → no-tools → user-only → labeled-only → all → default
const modes: FilterMode[] = ["default", "no-tools", "user-only", "labeled-only", "all"];
const currentIndex = modes.indexOf(this.filterMode);
this.filterMode = modes[(currentIndex + 1) % modes.length];
this.applyFilter();

View file

@ -51,6 +51,7 @@ export {
isEscape,
isHome,
isShiftCtrlD,
isShiftCtrlO,
isShiftCtrlP,
isShiftEnter,
isShiftTab,

View file

@ -304,6 +304,14 @@ export function isCtrlO(data: string): boolean {
return data === RAW.CTRL_O || data === Keys.CTRL_O || matchesKittySequence(data, CODEPOINTS.o, MODIFIERS.ctrl);
}
/**
* Check if input matches Shift+Ctrl+O (Kitty protocol only).
* Ignores lock key bits.
*/
export function isShiftCtrlO(data: string): boolean {
return matchesKittySequence(data, CODEPOINTS.o, MODIFIERS.shift + MODIFIERS.ctrl);
}
/**
* Check if input matches Ctrl+P (raw byte or Kitty protocol).
* Ignores lock key bits.