fix(omp): honor tree navigation order
This commit is contained in:
@@ -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", () => {
|
||||
|
||||
Reference in New Issue
Block a user