From a50c7b1da149170185f087951d9ae1a4b74009f5 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 7 Aug 2026 22:43:36 -0400 Subject: [PATCH] fix(frame): render continuous box borders --- .changeset/clean-box-drawing.md | 5 +++ bun.lock | 10 ++--- packages/drive/package.json | 2 +- packages/drive/src/frame/index.ts | 41 +++++++++++++++++++ packages/drive/test/frame.test.ts | 28 +++++++++++++ .../multi-tool-interleavings.ts | 8 ++-- 6 files changed, 84 insertions(+), 10 deletions(-) create mode 100644 .changeset/clean-box-drawing.md diff --git a/.changeset/clean-box-drawing.md b/.changeset/clean-box-drawing.md new file mode 100644 index 0000000..4e2c635 --- /dev/null +++ b/.changeset/clean-box-drawing.md @@ -0,0 +1,5 @@ +--- +"opencode-drive": patch +--- + +Render light box-drawing borders as continuous geometric primitives. diff --git a/bun.lock b/bun.lock index beed928..6ecb38c 100644 --- a/bun.lock +++ b/bun.lock @@ -31,7 +31,7 @@ }, "packages/drive": { "name": "opencode-drive", - "version": "1.4.0", + "version": "1.4.1", "bin": { "opencode-drive": "bin/opencode-drive", }, @@ -39,7 +39,7 @@ "@effect/platform-node": "4.0.0-beta.101", "@effect/platform-node-shared": "4.0.0-beta.101", "@napi-rs/canvas": "1.0.2", - "@opencode-ai/client": "0.0.0-next-16543", + "@opencode-ai/client": "0.0.0-next-17010", "@opentui/core": "0.4.5", "@types/bun": "1.3.13", "@typescript/native-preview": "7.0.0-dev.20251207.1", @@ -168,11 +168,11 @@ "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], - "@opencode-ai/client": ["@opencode-ai/client@0.0.0-next-16543", "", { "dependencies": { "@opencode-ai/protocol": "0.0.0-next-16543", "@opencode-ai/schema": "0.0.0-next-16543" }, "peerDependencies": { "effect": "4.0.0-beta.101" }, "optionalPeers": ["effect"] }, "sha512-TjVOLtl2P5/63mTxTRb0WF9IYrPEJwKvUKEMkeII/AHqkH/QmSMmIZjRJyHQMgc5QBC8qtw+hm8WRiCiudu6qw=="], + "@opencode-ai/client": ["@opencode-ai/client@0.0.0-next-17010", "", { "dependencies": { "@opencode-ai/protocol": "0.0.0-next-17010", "@opencode-ai/schema": "0.0.0-next-17010" }, "peerDependencies": { "effect": "4.0.0-beta.101" }, "optionalPeers": ["effect"] }, "sha512-NrmgWgWyf86J+WS9C1XYHyv7pg3Dc73+0tCEeft+UNEV5j4oeEycaWyQ9bi9qKBeSwe9UrMgA7pN9ogkyA3iRQ=="], - "@opencode-ai/protocol": ["@opencode-ai/protocol@0.0.0-next-16543", "", { "dependencies": { "@opencode-ai/schema": "0.0.0-next-16543", "effect": "4.0.0-beta.101" } }, "sha512-SIYM15csEIf2wtrVCM3Eg3+SCvG66gyjrckqEeTtrfntqAVH5+M9Yce5oefQyZr5vKL+akGzjX5ywsgMqb4DlA=="], + "@opencode-ai/protocol": ["@opencode-ai/protocol@0.0.0-next-17010", "", { "dependencies": { "@opencode-ai/schema": "0.0.0-next-17010", "effect": "4.0.0-beta.101" } }, "sha512-Tg0hKjO3xi0IWNb5nECzqanfuOCx/bEls9gbJ1fw1JdIHGYY4Ug6kMpr+6pTrte+Mo/8yOOOrrpDek1VHPl4Nw=="], - "@opencode-ai/schema": ["@opencode-ai/schema@0.0.0-next-16543", "", { "dependencies": { "@standard-schema/spec": "1.1.0", "effect": "4.0.0-beta.101" } }, "sha512-X29NyL9urulLSRKjVFukqR6zFdW7uPcdnaup1L3JH46RL0NQIfnXPTpqC/tFBhbu6S4LlhP/SsfKj56ncvPVKQ=="], + "@opencode-ai/schema": ["@opencode-ai/schema@0.0.0-next-17010", "", { "dependencies": { "@standard-schema/spec": "1.1.0", "effect": "4.0.0-beta.101" } }, "sha512-K0ZDy1o2NgyBTHsE6JjGuZdRMcvb8tgJstBwh5xh0gawohcKPT7mYo1XNe7p2okPCVBQMduG41Dq5rV46ZdHJw=="], "@opencode-drive/catalog": ["@opencode-drive/catalog@workspace:apps/catalog"], diff --git a/packages/drive/package.json b/packages/drive/package.json index f8d4c09..e8d022d 100644 --- a/packages/drive/package.json +++ b/packages/drive/package.json @@ -61,7 +61,7 @@ "@effect/platform-node": "4.0.0-beta.101", "@effect/platform-node-shared": "4.0.0-beta.101", "@napi-rs/canvas": "1.0.2", - "@opencode-ai/client": "0.0.0-next-16543", + "@opencode-ai/client": "0.0.0-next-17010", "@opentui/core": "0.4.5", "@types/bun": "1.3.13", "@typescript/native-preview": "7.0.0-dev.20251207.1", diff --git a/packages/drive/src/frame/index.ts b/packages/drive/src/frame/index.ts index 1c86b71..3e9aac7 100644 --- a/packages/drive/src/frame/index.ts +++ b/packages/drive/src/frame/index.ts @@ -62,6 +62,27 @@ export const BlockGlyphs: Record = { "╹": { x: CellWidth / 2 - 1, y: 0, width: 2, height: CellHeight / 2, stretch: false }, } +const lightBoxGlyphs: Record< + string, + readonly [up: boolean, right: boolean, down: boolean, left: boolean] +> = { + "─": [false, true, false, true], + "│": [true, false, true, false], + "┌": [false, true, true, false], + "┐": [false, false, true, true], + "└": [true, true, false, false], + "┘": [true, false, false, true], + "├": [true, true, true, false], + "┤": [true, false, true, true], + "┬": [false, true, true, true], + "┴": [true, true, false, true], + "┼": [true, true, true, true], + "╭": [false, true, true, false], + "╮": [false, false, true, true], + "╰": [true, true, false, false], + "╯": [true, false, false, true], +} + const diagonalBlockGlyphs: Record>> = { "▚": [ { x: 0, y: 0, width: CellWidth / 2, height: CellHeight / 2 }, @@ -94,6 +115,26 @@ export const drawBlockGlyph = ( context.fillRect(x + glyph.x, y + glyph.y, width, glyph.height) return true } + const box = lightBoxGlyphs[char] + if (box !== undefined) { + const lineX = CellWidth / 2 - 1 + const lineY = CellHeight / 2 - 1 + const vertical = box[0] || box[2] + if (vertical) { + const top = box[0] ? 0 : lineY + const bottom = box[2] ? CellHeight : lineY + 1 + context.fillRect(x + lineX, y + top, 1, bottom - top) + } + if (!vertical && (box[1] || box[3])) { + const left = box[3] ? 0 : lineX + const right = box[1] ? CellWidth : lineX + 1 + context.fillRect(x + left, y + lineY, right - left, 1) + } else { + if (box[3]) context.fillRect(x, y + lineY, lineX, 1) + if (box[1]) context.fillRect(x + lineX + 1, y + lineY, CellWidth - lineX - 1, 1) + } + return true + } const quadrants = diagonalBlockGlyphs[char] if (quadrants === undefined) return false for (const quadrant of quadrants) diff --git a/packages/drive/test/frame.test.ts b/packages/drive/test/frame.test.ts index dd923cf..faaeece 100644 --- a/packages/drive/test/frame.test.ts +++ b/packages/drive/test/frame.test.ts @@ -35,6 +35,34 @@ describe("frame geometry", () => { ]) }) + it("draws light box borders as connected pixel geometry", () => { + const rects: Array = [] + const context = { + fillRect: (x: number, y: number, width: number, height: number) => + void rects.push([x, y, width, height]), + } + expect(drawBlockGlyph(context, "│", 0, 0)).toBe(true) + expect(drawBlockGlyph(context, "─", CellWidth, 0)).toBe(true) + expect(drawBlockGlyph(context, "╭", CellWidth * 2, 0)).toBe(true) + expect(drawBlockGlyph(context, "┼", CellWidth * 3, 0)).toBe(true) + expect(rects.length).toBe(7) + expect( + rects.every( + ([x, y, width, height]) => + Number.isInteger(x) && + Number.isInteger(y) && + Number.isInteger(width) && + Number.isInteger(height), + ), + ).toBe(true) + const pixels = rects.flatMap(([x, y, width, height]) => + Array.from({ length: width * height }, (_, index) => + `${x + (index % width)},${y + Math.floor(index / width)}`, + ), + ) + expect(new Set(pixels).size).toBe(pixels.length) + }) + it("draws diagonal quadrant blocks edge-to-edge", () => { const rects: Array = [] const context = { diff --git a/packages/drive/test/manual/tui-regressions/multi-tool-interleavings.ts b/packages/drive/test/manual/tui-regressions/multi-tool-interleavings.ts index e1da35d..80d75e2 100644 --- a/packages/drive/test/manual/tui-regressions/multi-tool-interleavings.ts +++ b/packages/drive/test/manual/tui-regressions/multi-tool-interleavings.ts @@ -30,7 +30,7 @@ export default defineScript({ yield* opencode.event.subscribe().pipe( Stream.runForEach((event) => { if (event.type === "session.tool.success" || event.type === "session.tool.failed") - toolSettlements.push(`${event.type}:${event.data.callID}`) + toolSettlements.push(`${event.type}:${event.data.id}`) if (event.type === "session.execution.interrupted") return Deferred.succeed(executionInterrupted, undefined).pipe(Effect.asVoid) return Effect.void @@ -92,13 +92,13 @@ export default defineScript({ opencode.permission.list({ sessionID }).pipe( Effect.map((items) => { const questionPermission = items.find( - (item) => item.source?.type === "tool" && item.source.callID === questionCallID, + (item) => item.source?.type === "tool" && item.source.id === questionCallID, ) const readPermission = items.find( - (item) => item.source?.type === "tool" && item.source.callID === readCallID, + (item) => item.source?.type === "tool" && item.source.id === readCallID, ) const globPermission = items.find( - (item) => item.source?.type === "tool" && item.source.callID === globCallID, + (item) => item.source?.type === "tool" && item.source.id === globCallID, ) return questionPermission && readPermission && globPermission ? { question: questionPermission, read: readPermission, glob: globPermission }