fix(omp): honor tree navigation order

This commit is contained in:
Alexander Kirilin
2026-07-30 19:11:27 -04:00
parent 40a3c256b7
commit bb76178f55
3 changed files with 78 additions and 25 deletions
+1 -1
View File
@@ -4,7 +4,7 @@
### Added
- Added turn-aware `/tree` navigation: Alt+Up/Alt+Down traverses previous/next user or assistant turns while skipping tool and bookkeeping entries, Home/End jumps to the first/latest turn, and PageUp/PageDown moves by a visible page.
- Added turn-aware `/tree` navigation: Alt+Up/Alt+Down traverses previous/next user or assistant turns while skipping tool and bookkeeping entries, Home/End jumps to the first/last visible item, and PageUp/PageDown moves by a visible page.
## [17.2.1] - 2026-07-30
@@ -801,33 +801,19 @@ class TreeList implements Component {
}
}
#jumpToTurnBoundary(boundary: "first" | "last"): void {
const start = boundary === "first" ? 0 : this.#filteredNodes.length - 1;
const step = boundary === "first" ? 1 : -1;
for (let index = start; index >= 0 && index < this.#filteredNodes.length; index += step) {
const entry = this.#filteredNodes[index]?.node.entry;
if (entry?.type === "message" && (entry.message.role === "user" || entry.message.role === "assistant")) {
this.#selectedIndex = index;
return;
}
}
}
handleInput(keyData: string): void {
if (matchesKey(keyData, "alt+up")) {
this.#moveToAdjacentTurn(-1);
} else if (matchesKey(keyData, "alt+down")) {
this.#moveToAdjacentTurn(1);
} else if (matchesSelectUp(keyData)) {
if (matchesSelectUp(keyData)) {
this.#selectedIndex = this.#selectedIndex === 0 ? this.#filteredNodes.length - 1 : this.#selectedIndex - 1;
} else if (matchesSelectDown(keyData)) {
this.#selectedIndex = this.#selectedIndex === this.#filteredNodes.length - 1 ? 0 : this.#selectedIndex + 1;
} else if (matchesKey(keyData, "alt+up")) {
this.#moveToAdjacentTurn(-1);
} else if (matchesKey(keyData, "alt+down")) {
this.#moveToAdjacentTurn(1);
} else if (matchesKey(keyData, "home")) {
// Jump to the earliest actual conversation turn, skipping tool and bookkeeping entries.
this.#jumpToTurnBoundary("first");
this.#selectedIndex = 0;
} else if (matchesKey(keyData, "end")) {
// Jump to the latest actual conversation turn, skipping tool and bookkeeping entries.
this.#jumpToTurnBoundary("last");
this.#selectedIndex = Math.max(0, this.#filteredNodes.length - 1);
} else if (matchesSelectPageUp(keyData) || matchesKey(keyData, "left")) {
this.#selectedIndex = Math.max(0, this.#selectedIndex - this.maxVisibleLines);
} else if (matchesSelectPageDown(keyData) || matchesKey(keyData, "right")) {
@@ -994,7 +980,7 @@ export class TreeSelectorComponent extends Container {
new TruncatedText(
theme.fg(
"muted",
"Enter: switch. Alt+↑/↓: previous/next turn. PgUp/PgDn (←/→): page. Home/End: first/latest turn. Shift+Enter: summarize & switch. Shift+L: label. Ctrl+O: filter. Alt+D/T/U/L/A: filter. Type to search",
"Enter: switch. Alt+↑/↓: previous/next turn. PgUp/PgDn (←/→): page. Home/End: first/last item. Shift+Enter: summarize & switch. Shift+L: label. Ctrl+O: filter. Alt+D/T/U/L/A: filter. Type to search",
),
0,
0,
@@ -146,7 +146,7 @@ describe("selector navigation keybindings", () => {
expect(selected).toEqual(["child"]);
});
it("traverses actual turns while skipping tool results in the session tree", () => {
it("traverses actual turns and jumps to first/last visible tree items", () => {
const firstUser = createMessageNode("first-user", null, "First question");
const assistant = createAgentMessageNode("assistant", "first-user", {
role: "assistant",
@@ -209,7 +209,74 @@ describe("selector navigation keybindings", () => {
selector.handleInput("\x1b[H");
selector.handleInput("\n");
expect(selected).toEqual(["second-user", "assistant", "first-user", "assistant", "second-user", "first-user"]);
expect(selected).toEqual([
"second-user",
"assistant",
"first-user",
"assistant",
"trailing-tool-result",
"first-user",
]);
});
it("honors configured row bindings before Alt+Up and Alt+Down turn traversal", () => {
setKeybindings(
KeybindingsManager.inMemory({
"tui.select.up": "alt+up",
"tui.select.down": "alt+down",
}),
);
const firstUser = createMessageNode("first-user", null, "First question");
const toolResult = createAgentMessageNode("tool-result", "first-user", {
role: "toolResult",
toolCallId: "call-1",
toolName: "read",
content: [{ type: "text", text: "file contents" }],
isError: false,
timestamp: 2,
});
const secondUser = createMessageNode("second-user", "tool-result", "Second question");
firstUser.children.push(toolResult);
toolResult.children.push(secondUser);
const selected: string[] = [];
const selector = new TreeSelectorComponent(
[firstUser],
"first-user",
40,
id => selected.push(id),
() => {},
);
selector.handleInput("\x1b[1;3B");
selector.handleInput("\n");
selector.handleInput("\x1b[1;3A");
selector.handleInput("\n");
expect(selected).toEqual(["tool-result", "first-user"]);
});
it("uses rendered tree order for Home and End across branches", () => {
const root = createMessageNode("root", null, "Root");
const activeBranch = createMessageNode("active-branch", "root", "Active branch");
const inactiveBranch = createMessageNode("inactive-branch", "root", "Inactive branch");
root.children.push(activeBranch, inactiveBranch);
const selected: string[] = [];
const selector = new TreeSelectorComponent(
[root],
"active-branch",
40,
id => selected.push(id),
() => {},
);
selector.handleInput("\x1b[F");
selector.handleInput("\n");
selector.handleInput("\x1b[H");
selector.handleInput("\n");
expect(selected).toEqual(["inactive-branch", "root"]);
});
it("uses PageUp and PageDown to move by a visible page in the session tree", () => {