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") + }) + }) +})