From 27b9698cf336515d748eb3f856618fffb250b9a2 Mon Sep 17 00:00:00 2001 From: Tamsi Date: Thu, 2 Jul 2026 08:20:05 +0200 Subject: [PATCH] fix(cli): prevent truncate helpers from overflowing at small widths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `truncateLeft` and `truncateMiddle` build their tail with `str.slice(-keep)`. When `keep` is 0 — which happens at `maxLength` 1 (both) and 2 (middle) — `str.slice(-0)` is `str.slice(0)` and returns the *entire* string, so the "truncated" result is longer than the input instead of shorter. These helpers are called from TUI components with dynamic widths that reach 1 (e.g. `dialog-select.tsx` uses `Math.max(1, …)`), so a narrow pane could emit a value far wider than the available space and blow out the layout. Drop the tail explicitly when `keep <= 0`. The shared-file edits are wrapped in `kilocode_change` markers; the regression test lives under `test/kilocode/`. --- packages/opencode/src/util/locale.ts | 14 ++++- .../opencode/test/kilocode/locale.test.ts | 56 +++++++++++++++++++ 2 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 packages/opencode/test/kilocode/locale.test.ts diff --git a/packages/opencode/src/util/locale.ts b/packages/opencode/src/util/locale.ts index ec900b44167..afd40f6df02 100644 --- a/packages/opencode/src/util/locale.ts +++ b/packages/opencode/src/util/locale.ts @@ -65,7 +65,12 @@ export function truncate(str: string, len: number): string { export function truncateLeft(str: string, len: number): string { if (str.length <= len) return str - return "…" + str.slice(-(len - 1)) + // kilocode_change start + const keep = len - 1 + // `slice(-0)` is `slice(0)` and returns the whole string, so at len <= 1 the + // tail must be dropped explicitly, otherwise it leaks the entire input. + return "…" + (keep > 0 ? str.slice(-keep) : "") + // kilocode_change end } export function truncateMiddle(str: string, maxLength: number = 35): string { @@ -75,7 +80,12 @@ export function truncateMiddle(str: string, maxLength: number = 35): string { const keepStart = Math.ceil((maxLength - ellipsis.length) / 2) const keepEnd = Math.floor((maxLength - ellipsis.length) / 2) - return str.slice(0, keepStart) + ellipsis + str.slice(-keepEnd) + // kilocode_change start + // `slice(-0)` is `slice(0)` and returns the whole string, so when keepEnd is 0 + // (maxLength 1 or 2) the tail must be dropped explicitly, otherwise the result + // is longer than the input instead of truncated. + return str.slice(0, keepStart) + ellipsis + (keepEnd > 0 ? str.slice(-keepEnd) : "") + // kilocode_change end } export function pluralize(count: number, singular: string, plural: string): string { diff --git a/packages/opencode/test/kilocode/locale.test.ts b/packages/opencode/test/kilocode/locale.test.ts new file mode 100644 index 00000000000..2b909d7e0e7 --- /dev/null +++ b/packages/opencode/test/kilocode/locale.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, test } from "bun:test" +import { Locale } from "../../src/util/locale" + +describe("util.locale", () => { + const long = "src/components/very-long-file-name.tsx" + + describe("truncateLeft", () => { + test("never returns more than maxLength characters", () => { + for (const len of [1, 2, 3, 10, 20]) { + expect(Locale.truncateLeft(long, len).length).toBeLessThanOrEqual(len) + } + }) + + test("collapses to a single ellipsis at length 1", () => { + // `slice(-0)` is `slice(0)`, which used to leak the whole string here. + expect(Locale.truncateLeft(long, 1)).toBe("…") + }) + + test("keeps the tail characters", () => { + expect(Locale.truncateLeft(long, 5)).toBe("….tsx") + }) + + test("returns short strings unchanged", () => { + expect(Locale.truncateLeft("abc", 10)).toBe("abc") + }) + }) + + describe("truncateMiddle", () => { + test("never returns more than maxLength characters", () => { + for (const len of [1, 2, 3, 10, 20]) { + expect(Locale.truncateMiddle(long, len).length).toBeLessThanOrEqual(len) + } + }) + + test("collapses to a single ellipsis at length 1", () => { + // keepEnd is 0 here, so `slice(-0)` used to append the whole string. + expect(Locale.truncateMiddle(long, 1)).toBe("…") + }) + + test("drops the tail at length 2", () => { + expect(Locale.truncateMiddle(long, 2)).toBe("s…") + }) + + test("keeps head and tail around the ellipsis", () => { + const result = Locale.truncateMiddle(long, 11) + expect(result.length).toBe(11) + expect(result).toContain("…") + expect(result.startsWith("src")).toBe(true) + expect(result.endsWith("tsx")).toBe(true) + }) + + test("returns short strings unchanged", () => { + expect(Locale.truncateMiddle("abc", 10)).toBe("abc") + }) + }) +})