perf(tui): memoize copy-selector preview highlight across renders

Every keystroke or arrow-key navigation in the copy selector re-rendered the preview by syntax-highlighting the entire `target.preview`, replacing tabs, and wrapping it to width. `CopySelectorComponent` now keeps instance-private memo fields for the last target and its generated source; `invalidate()` clears them, and rendering reuses the cached source while the selected target is unchanged. Verified with `bun test packages/coding-agent/test/modes/components/copy-selector.test.ts` (5 pass, 0 fail).

Closes #4238
This commit is contained in:
metaphorics
2026-07-02 17:28:55 +09:00
parent 8a8e9aebe5
commit b589d45e6e
2 changed files with 21 additions and 4 deletions
@@ -53,6 +53,8 @@ function gutterCells(hasNext: boolean): string {
export class CopySelectorComponent implements Component {
#roots: CopyTarget[];
#cursorId: string;
#lastSourceTarget?: CopyTarget;
#lastSource?: string;
#treeRows = MIN_TREE_ROWS;
// Reused across renders to wrap preview content to the pane width.
#previewText = new Text("", 0, 0);
@@ -65,7 +67,10 @@ export class CopySelectorComponent implements Component {
this.#cursorId = roots[0]?.id ?? "";
}
invalidate(): void {}
invalidate(): void {
this.#lastSourceTarget = undefined;
this.#lastSource = undefined;
}
#flatten(): FlatNode[] {
const out: FlatNode[] = [];
@@ -153,9 +158,16 @@ export class CopySelectorComponent implements Component {
// Code/command previews are syntax-highlighted; everything else is shown
// as plain text. Both are wrapped (not hard-truncated) to the pane width.
const isCode = target.language !== undefined;
const source = isCode
? highlightCode(replaceTabs(target.preview), target.language).join("\n")
: replaceTabs(target.preview);
let source: string;
if (target === this.#lastSourceTarget && this.#lastSource !== undefined) {
source = this.#lastSource;
} else {
source = isCode
? highlightCode(replaceTabs(target.preview), target.language).join("\n")
: replaceTabs(target.preview);
this.#lastSourceTarget = target;
this.#lastSource = source;
}
this.#previewText.setText(source);
const wrapped = this.#previewText.render(Math.max(1, width - 4));