Skip to content

Commit 429240f

Browse files
authored
ignore: add truncation funcs (anomalyco#7178)
1 parent a0dc90b commit 429240f

3 files changed

Lines changed: 140 additions & 0 deletions

File tree

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
export namespace Truncate {
2+
export const MAX_LINES = 2000
3+
export const MAX_BYTES = 50 * 1024
4+
5+
export interface Result {
6+
content: string
7+
truncated: boolean
8+
}
9+
10+
export interface Options {
11+
maxLines?: number
12+
maxBytes?: number
13+
direction?: "head" | "tail"
14+
}
15+
16+
export function output(text: string, options: Options = {}): Result {
17+
const maxLines = options.maxLines ?? MAX_LINES
18+
const maxBytes = options.maxBytes ?? MAX_BYTES
19+
const direction = options.direction ?? "head"
20+
const lines = text.split("\n")
21+
const totalBytes = Buffer.byteLength(text, "utf-8")
22+
23+
if (lines.length <= maxLines && totalBytes <= maxBytes) {
24+
return { content: text, truncated: false }
25+
}
26+
27+
const out: string[] = []
28+
var i = 0
29+
var bytes = 0
30+
var hitBytes = false
31+
32+
if (direction === "head") {
33+
for (i = 0; i < lines.length && i < maxLines; i++) {
34+
const size = Buffer.byteLength(lines[i], "utf-8") + (i > 0 ? 1 : 0)
35+
if (bytes + size > maxBytes) {
36+
hitBytes = true
37+
break
38+
}
39+
out.push(lines[i])
40+
bytes += size
41+
}
42+
const removed = hitBytes ? totalBytes - bytes : lines.length - out.length
43+
const unit = hitBytes ? "chars" : "lines"
44+
return { content: `${out.join("\n")}\n\n...${removed} ${unit} truncated...`, truncated: true }
45+
}
46+
47+
for (i = lines.length - 1; i >= 0 && out.length < maxLines; i--) {
48+
const size = Buffer.byteLength(lines[i], "utf-8") + (out.length > 0 ? 1 : 0)
49+
if (bytes + size > maxBytes) {
50+
hitBytes = true
51+
break
52+
}
53+
out.unshift(lines[i])
54+
bytes += size
55+
}
56+
const removed = hitBytes ? totalBytes - bytes : lines.length - out.length
57+
const unit = hitBytes ? "chars" : "lines"
58+
return { content: `...${removed} ${unit} truncated...\n\n${out.join("\n")}`, truncated: true }
59+
}
60+
}

packages/opencode/test/session/fixtures/models-api.json

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { describe, test, expect } from "bun:test"
2+
import { Truncate } from "../../src/session/truncation"
3+
import path from "path"
4+
5+
const FIXTURES_DIR = path.join(import.meta.dir, "fixtures")
6+
7+
describe("Truncate", () => {
8+
describe("output", () => {
9+
test("truncates large json file by bytes", async () => {
10+
const content = await Bun.file(path.join(FIXTURES_DIR, "models-api.json")).text()
11+
const result = Truncate.output(content)
12+
13+
expect(result.truncated).toBe(true)
14+
expect(Buffer.byteLength(result.content, "utf-8")).toBeLessThanOrEqual(Truncate.MAX_BYTES + 100)
15+
expect(result.content).toContain("truncated...")
16+
})
17+
18+
test("returns content unchanged when under limits", () => {
19+
const content = "line1\nline2\nline3"
20+
const result = Truncate.output(content)
21+
22+
expect(result.truncated).toBe(false)
23+
expect(result.content).toBe(content)
24+
})
25+
26+
test("truncates by line count", () => {
27+
const lines = Array.from({ length: 100 }, (_, i) => `line${i}`).join("\n")
28+
const result = Truncate.output(lines, { maxLines: 10 })
29+
30+
expect(result.truncated).toBe(true)
31+
expect(result.content.split("\n").length).toBeLessThanOrEqual(12)
32+
expect(result.content).toContain("...90 lines truncated...")
33+
})
34+
35+
test("truncates by byte count", () => {
36+
const content = "a".repeat(1000)
37+
const result = Truncate.output(content, { maxBytes: 100 })
38+
39+
expect(result.truncated).toBe(true)
40+
expect(result.content).toContain("truncated...")
41+
})
42+
43+
test("truncates from head by default", () => {
44+
const lines = Array.from({ length: 10 }, (_, i) => `line${i}`).join("\n")
45+
const result = Truncate.output(lines, { maxLines: 3 })
46+
47+
expect(result.truncated).toBe(true)
48+
expect(result.content).toContain("line0")
49+
expect(result.content).toContain("line1")
50+
expect(result.content).toContain("line2")
51+
expect(result.content).not.toContain("line9")
52+
})
53+
54+
test("truncates from tail when direction is tail", () => {
55+
const lines = Array.from({ length: 10 }, (_, i) => `line${i}`).join("\n")
56+
const result = Truncate.output(lines, { maxLines: 3, direction: "tail" })
57+
58+
expect(result.truncated).toBe(true)
59+
expect(result.content).toContain("line7")
60+
expect(result.content).toContain("line8")
61+
expect(result.content).toContain("line9")
62+
expect(result.content).not.toContain("line0")
63+
})
64+
65+
test("uses default MAX_LINES and MAX_BYTES", () => {
66+
expect(Truncate.MAX_LINES).toBe(2000)
67+
expect(Truncate.MAX_BYTES).toBe(50 * 1024)
68+
})
69+
70+
test("large single-line file truncates with byte message", async () => {
71+
const content = await Bun.file(path.join(FIXTURES_DIR, "models-api.json")).text()
72+
const result = Truncate.output(content)
73+
74+
expect(result.truncated).toBe(true)
75+
expect(result.content).toContain("chars truncated...")
76+
expect(Buffer.byteLength(content, "utf-8")).toBeGreaterThan(Truncate.MAX_BYTES)
77+
})
78+
})
79+
})

0 commit comments

Comments
 (0)