mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
fix(coding-agent): render session tree search cursor (#9157)
This PR adds the cursor block to the **Type to search** input in **Session Tree**. This is similar to the search input in **Resume Session**. Before: <img width="1120" height="778" alt="Screenshot 2026-09-04 at 19 25 30" src="https://github.com/user-attachments/assets/8f2e0ba9-8476-452b-a6be-1ad9b8d7d6c8" /> After: <img width="1120" height="778" alt="Screenshot 2026-09-04 at 19 25 50" src="https://github.com/user-attachments/assets/7f6ffcbf-db5a-4732-84fe-6ee0544116fa" />
This commit is contained in:
parent
c3b938e3e9
commit
2cfab7c269
2 changed files with 88 additions and 11 deletions
|
|
@ -1,6 +1,7 @@
|
|||
import {
|
||||
type Component,
|
||||
Container,
|
||||
CURSOR_MARKER,
|
||||
type Focusable,
|
||||
getKeybindings,
|
||||
Input,
|
||||
|
|
@ -1152,7 +1153,8 @@ class TreeList implements Component {
|
|||
}
|
||||
|
||||
/** Component that displays the current search query */
|
||||
class SearchLine implements Component {
|
||||
class SearchLine implements Component, Focusable {
|
||||
focused = false;
|
||||
private treeList: TreeList;
|
||||
|
||||
constructor(treeList: TreeList) {
|
||||
|
|
@ -1162,11 +1164,16 @@ class SearchLine implements Component {
|
|||
invalidate(): void {}
|
||||
|
||||
render(width: number): string[] {
|
||||
if (width <= 0) return [""];
|
||||
|
||||
const query = this.treeList.getSearchQuery();
|
||||
if (query) {
|
||||
return [truncateToWidth(` ${theme.fg("muted", "Type to search:")} ${theme.fg("accent", query)}`, width)];
|
||||
}
|
||||
return [truncateToWidth(` ${theme.fg("muted", "Type to search:")}`, width)];
|
||||
const line = query
|
||||
? ` ${theme.fg("muted", "Type to search:")} ${theme.fg("accent", query)}`
|
||||
: ` ${theme.fg("muted", "Type to search:")} `;
|
||||
if (!this.focused) return [truncateToWidth(line, width)];
|
||||
|
||||
const content = truncateToWidth(line, Math.max(0, width - 1));
|
||||
return [`${content}${CURSOR_MARKER}\x1b[7m \x1b[27m`];
|
||||
}
|
||||
|
||||
handleInput(_keyData: string): void {}
|
||||
|
|
@ -1325,20 +1332,21 @@ class LabelInput implements Component, Focusable {
|
|||
*/
|
||||
export class TreeSelectorComponent extends Container implements Focusable {
|
||||
private treeList: TreeList;
|
||||
private searchLine: SearchLine;
|
||||
private labelInput: LabelInput | null = null;
|
||||
private labelInputContainer: Container;
|
||||
private treeContainer: Container;
|
||||
private onLabelChangeCallback?: (entryId: string, label: string | undefined) => void;
|
||||
public onCopy?: (text: string | undefined) => void;
|
||||
|
||||
// Focusable implementation - propagate to labelInput when active for IME cursor positioning
|
||||
// Focusable implementation - propagate to the active input for IME cursor positioning
|
||||
private _focused = false;
|
||||
get focused(): boolean {
|
||||
return this._focused;
|
||||
}
|
||||
set focused(value: boolean) {
|
||||
this._focused = value;
|
||||
// Propagate to labelInput when it's active
|
||||
this.searchLine.focused = value && this.labelInput === null;
|
||||
if (this.labelInput) {
|
||||
this.labelInput.focused = value;
|
||||
}
|
||||
|
|
@ -1364,6 +1372,7 @@ export class TreeSelectorComponent extends Container implements Focusable {
|
|||
this.treeList.onCancel = onCancel;
|
||||
this.treeList.onCopy = (text) => this.onCopy?.(text);
|
||||
this.treeList.onLabelEdit = (entryId, currentLabel) => this.showLabelInput(entryId, currentLabel);
|
||||
this.searchLine = new SearchLine(this.treeList);
|
||||
|
||||
this.treeContainer = new Container();
|
||||
this.treeContainer.addChild(this.treeList);
|
||||
|
|
@ -1374,7 +1383,7 @@ export class TreeSelectorComponent extends Container implements Focusable {
|
|||
this.addChild(new DynamicBorder());
|
||||
this.addChild(new Text(theme.bold(" Session Tree"), 1, 0));
|
||||
this.addChild(new TreeHelp());
|
||||
this.addChild(new SearchLine(this.treeList));
|
||||
this.addChild(this.searchLine);
|
||||
this.addChild(new DynamicBorder());
|
||||
this.addChild(new Spacer(1));
|
||||
this.addChild(this.treeContainer);
|
||||
|
|
@ -1396,8 +1405,8 @@ export class TreeSelectorComponent extends Container implements Focusable {
|
|||
};
|
||||
this.labelInput.onCancel = () => this.hideLabelInput();
|
||||
|
||||
// Propagate current focused state to the new labelInput
|
||||
this.labelInput.focused = this._focused;
|
||||
// Propagate current focused state
|
||||
this.focused = this._focused;
|
||||
|
||||
this.treeContainer.clear();
|
||||
this.labelInputContainer.clear();
|
||||
|
|
@ -1406,6 +1415,7 @@ export class TreeSelectorComponent extends Container implements Focusable {
|
|||
|
||||
private hideLabelInput(): void {
|
||||
this.labelInput = null;
|
||||
this.focused = this._focused;
|
||||
this.labelInputContainer.clear();
|
||||
this.treeContainer.clear();
|
||||
this.treeContainer.addChild(this.treeList);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { stripVTControlCharacters } from "node:util";
|
||||
import { setKeybindings, visibleWidth } from "@earendil-works/pi-tui";
|
||||
import { CURSOR_MARKER, setKeybindings, visibleWidth } from "@earendil-works/pi-tui";
|
||||
import { beforeAll, beforeEach, describe, expect, test } from "vitest";
|
||||
import { KeybindingsManager } from "../src/core/keybindings.ts";
|
||||
import type {
|
||||
|
|
@ -310,6 +310,73 @@ describe("TreeSelectorComponent", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("search cursor", () => {
|
||||
test("renders the cursor only while the selector is focused", () => {
|
||||
const tree = buildTree([userMessage("user-1", null, "hello")]);
|
||||
const selector = new TreeSelectorComponent(
|
||||
tree,
|
||||
"user-1",
|
||||
24,
|
||||
() => {},
|
||||
() => {},
|
||||
);
|
||||
|
||||
expect(selector.render(80).join("\n")).not.toContain(CURSOR_MARKER);
|
||||
|
||||
selector.focused = true;
|
||||
const focusedLines = selector.render(80);
|
||||
const cursorLines = focusedLines.filter((line) => line.includes(CURSOR_MARKER));
|
||||
expect(cursorLines).toHaveLength(1);
|
||||
expect(stripVTControlCharacters(cursorLines[0]!.replace(CURSOR_MARKER, ""))).toBe(" Type to search: ");
|
||||
expect(cursorLines[0]).toContain(`${CURSOR_MARKER}\x1b[7m \x1b[27m`);
|
||||
|
||||
selector.focused = false;
|
||||
expect(selector.render(80).join("\n")).not.toContain(CURSOR_MARKER);
|
||||
});
|
||||
|
||||
test("moves focus to the label input and restores it after cancellation", () => {
|
||||
const tree = buildTree([userMessage("user-1", null, "hello")]);
|
||||
const selector = new TreeSelectorComponent(
|
||||
tree,
|
||||
"user-1",
|
||||
24,
|
||||
() => {},
|
||||
() => {},
|
||||
);
|
||||
selector.focused = true;
|
||||
|
||||
selector.handleInput("L");
|
||||
let rendered = selector.render(80);
|
||||
let cursorLineIndex = rendered.findIndex((line) => line.includes(CURSOR_MARKER));
|
||||
expect(cursorLineIndex).toBeGreaterThan(0);
|
||||
expect(stripVTControlCharacters(rendered[cursorLineIndex - 1]!)).toContain("Label (empty to remove):");
|
||||
expect(rendered.filter((line) => line.includes(CURSOR_MARKER))).toHaveLength(1);
|
||||
|
||||
selector.handleInput("\x1b");
|
||||
rendered = selector.render(80);
|
||||
cursorLineIndex = rendered.findIndex((line) => line.includes(CURSOR_MARKER));
|
||||
expect(stripVTControlCharacters(rendered[cursorLineIndex]!)).toContain("Type to search:");
|
||||
expect(rendered.filter((line) => line.includes(CURSOR_MARKER))).toHaveLength(1);
|
||||
});
|
||||
|
||||
test("keeps the cursor within narrow terminal widths", () => {
|
||||
const tree = buildTree([userMessage("user-1", null, "hello")]);
|
||||
const selector = new TreeSelectorComponent(
|
||||
tree,
|
||||
"user-1",
|
||||
24,
|
||||
() => {},
|
||||
() => {},
|
||||
);
|
||||
selector.focused = true;
|
||||
selector.handleInput("a long search query");
|
||||
|
||||
const cursorLine = selector.render(10).find((line) => line.includes(CURSOR_MARKER));
|
||||
expect(cursorLine).toBeDefined();
|
||||
expect(visibleWidth(cursorLine!)).toBeLessThanOrEqual(10);
|
||||
});
|
||||
});
|
||||
|
||||
describe("copy", () => {
|
||||
test("copies the full selected message with ctrl+x", () => {
|
||||
const message = `${"long message ".repeat(30)}\nsecond line`;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue