diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 98869cf8d..d646ddc8d 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -35,6 +35,36 @@ - Sped up fuzzy filtering in selectors (model, settings, file/tree, hook, OAuth) by preparing the query once per filter instead of once per candidate, and memoizing the per-text search index across keystrokes. Incremental typing over a 400-item list drops ~57% (6.7ms → 2.9ms for an 8-keystroke session) with no change to match ranking and no first-keystroke regression; long texts (pasted prompts, transcripts) bypass the cache so memory stays bounded. +## [16.2.13] - 2026-07-01 + +### Fixed + +- Fixed fuzzy-search filtering for CJK and other non-ASCII queries by preserving Unicode letters and numbers during query normalization ([#4114](https://github.com/can1357/oh-my-pi/issues/4114)). + +## [16.2.12] - 2026-07-01 + +### Fixed + +- Optimized streaming markdown rendering to reuse already-rendered prefix lines and only render new content deltas, improving performance and reducing redraw flicker. + +## [16.2.10] - 2026-06-30 + +### Fixed + +- Fixed mid-prompt `/skill:` autocomplete acceptance wiping the user's draft. The autocomplete now inserts the `/skill: ` token at the cursor (replacing only the partial `/sk` slash token) and preserves prose typed before and after it, so a user can compose a prompt and reach for a skill without losing their train of thought ([#3913](https://github.com/can1357/oh-my-pi/issues/3913)). + +## [16.2.9] - 2026-06-30 + +### Added + +- Added `Editor.submit()` to allow programmatic composer submission, enabling integration with speech input and other automated flows. + +## [16.2.7] - 2026-06-30 + +### Fixed + +- Fixed an issue where a fast double-Escape keypress was swallowed and ignored, preventing double-escape gestures and subsequent Escape key handlers from firing. + ## [16.2.3] - 2026-06-28 ### Added diff --git a/packages/tui/test/fuzzy-cache.test.ts b/packages/tui/test/fuzzy-cache.test.ts new file mode 100644 index 000000000..0fbbcdd29 --- /dev/null +++ b/packages/tui/test/fuzzy-cache.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, it } from "bun:test"; +import { fuzzyMatch, fuzzyRank, resetFuzzyIndexCache } from "@oh-my-pi/pi-tui/fuzzy"; + +describe("fuzzy index cache", () => { + it("produces identical ordering whether the cache is cold or warm", () => { + const items = [ + "openai/gpt-4o", + "openai/gpt-4o-mini", + "openai/gpt-4-turbo", + "openai/o3", + "anthropic/claude-3.5-sonnet", + "anthropic/claude-4-opus", + "google/gemini-2.5-pro", + ]; + resetFuzzyIndexCache(); + const cold = fuzzyRank(items, "gpt 4o", item => item).map(result => result.item); + // Second pass reuses the now-cached per-text indices; the result must be byte-for-byte identical. + const warm = fuzzyRank(items, "gpt 4o", item => item).map(result => result.item); + expect(warm).toEqual(cold); + }); + + it("matches long candidate texts (cache bypass) deterministically", () => { + const longText = `openai/gpt-4o ${"x".repeat(5000)}`; + resetFuzzyIndexCache(); + const first = fuzzyMatch("gpt4", longText); + const second = fuzzyMatch("gpt4", longText); + expect(first).toEqual(second); + expect(first.matches).toBe(true); + }); +}); + +describe("fuzzyRank empty-normalized query", () => { + it("still calls getText for every item when the query normalizes to empty", () => { + const seen: string[] = []; + const items = ["alpha", "beta", "gamma"]; + const out = fuzzyRank(items, "!!!", item => { + seen.push(item); + return item; + }); + // A non-blank query that normalizes to empty matches everything with score 0, + // and must still invoke getText per item (preserving callback side effects). + expect(seen).toEqual(items); + expect(out.map(result => result.item)).toEqual(items); + expect(out.every(result => result.score === 0)).toBe(true); + }); +}); diff --git a/packages/tui/test/fuzzy.test.ts b/packages/tui/test/fuzzy.test.ts index 208947cf6..51ce95611 100644 --- a/packages/tui/test/fuzzy.test.ts +++ b/packages/tui/test/fuzzy.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "bun:test"; -import { fuzzyFilter, fuzzyMatch, fuzzyRank, resetFuzzyIndexCache } from "@oh-my-pi/pi-tui/fuzzy"; +import { fuzzyFilter, fuzzyMatch } from "@oh-my-pi/pi-tui/fuzzy"; describe("fuzzyFilter", () => { it("does not satisfy long tokens by scattering letters across unrelated words", () => { @@ -44,46 +44,3 @@ describe("fuzzyFilter", () => { }); }); -describe("fuzzy index cache", () => { - it("produces identical ordering whether the cache is cold or warm", () => { - const items = [ - "openai/gpt-4o", - "openai/gpt-4o-mini", - "openai/gpt-4-turbo", - "openai/o3", - "anthropic/claude-3.5-sonnet", - "anthropic/claude-4-opus", - "google/gemini-2.5-pro", - ]; - resetFuzzyIndexCache(); - const cold = fuzzyRank(items, "gpt 4o", item => item).map(result => result.item); - // Second pass reuses the now-cached per-text indices; the result must be byte-for-byte identical. - const warm = fuzzyRank(items, "gpt 4o", item => item).map(result => result.item); - expect(warm).toEqual(cold); - }); - - it("matches long candidate texts (cache bypass) deterministically", () => { - const longText = `openai/gpt-4o ${"x".repeat(5000)}`; - resetFuzzyIndexCache(); - const first = fuzzyMatch("gpt4", longText); - const second = fuzzyMatch("gpt4", longText); - expect(first).toEqual(second); - expect(first.matches).toBe(true); - }); -}); - -describe("fuzzyRank empty-normalized query", () => { - it("still calls getText for every item when the query normalizes to empty", () => { - const seen: string[] = []; - const items = ["alpha", "beta", "gamma"]; - const out = fuzzyRank(items, "!!!", item => { - seen.push(item); - return item; - }); - // A non-blank query that normalizes to empty matches everything with score 0, - // and must still invoke getText per item (preserving callback side effects). - expect(seen).toEqual(items); - expect(out.map(result => result.item)).toEqual(items); - expect(out.every(result => result.score === 0)).toBe(true); - }); -});