From b589d45e6ef4e40f570fb70ada7e6a4d86f6152d Mon Sep 17 00:00:00 2001 From: metaphorics <152830360+metaphorics@users.noreply.github.com> Date: Thu, 2 Jul 2026 17:25:49 +0900 Subject: [PATCH] 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 --- packages/coding-agent/CHANGELOG.md | 5 +++++ .../src/modes/components/copy-selector.ts | 20 +++++++++++++++---- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 11098023c..ebb7b6182 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,11 @@ ## [Unreleased] +### Changed + +- Memoize copy-selector preview highlight so keyboard navigation does not re-highlight the whole preview every render ([#4238](https://github.com/can1357/oh-my-pi/issues/4238)) + + ## [16.3.0] - 2026-07-02 ### Added diff --git a/packages/coding-agent/src/modes/components/copy-selector.ts b/packages/coding-agent/src/modes/components/copy-selector.ts index 02fe40e0b..70eab4e1d 100644 --- a/packages/coding-agent/src/modes/components/copy-selector.ts +++ b/packages/coding-agent/src/modes/components/copy-selector.ts @@ -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));