diff --git a/bun.lock b/bun.lock index 38c97e4832d..7ccc658e81a 100644 --- a/bun.lock +++ b/bun.lock @@ -571,6 +571,20 @@ "@typescript/native-preview": "catalog:", }, }, + "packages/merman": { + "name": "@opencode-ai/merman", + "version": "0.0.0", + "dependencies": { + "@opencode-ai/plugin": "workspace:*", + "@opentui/core": "catalog:", + "string-width": "catalog:", + }, + "devDependencies": { + "@tsconfig/bun": "catalog:", + "@types/bun": "catalog:", + "@typescript/native-preview": "catalog:", + }, + }, "packages/plugin": { "name": "@opencode-ai/plugin", "version": "1.18.8", @@ -882,6 +896,7 @@ "dependencies": { "@opencode-ai/client": "workspace:*", "@opencode-ai/core": "workspace:*", + "@opencode-ai/merman": "workspace:*", "@opencode-ai/plugin": "workspace:*", "@opencode-ai/schema": "workspace:*", "@opencode-ai/simulation": "workspace:*", @@ -2083,6 +2098,8 @@ "@opencode-ai/httpapi-codegen": ["@opencode-ai/httpapi-codegen@workspace:packages/httpapi-codegen"], + "@opencode-ai/merman": ["@opencode-ai/merman@workspace:packages/merman"], + "@opencode-ai/plugin": ["@opencode-ai/plugin@workspace:packages/plugin"], "@opencode-ai/protocol": ["@opencode-ai/protocol@workspace:packages/protocol"], diff --git a/packages/merman/package.json b/packages/merman/package.json new file mode 100644 index 00000000000..7bc5439debe --- /dev/null +++ b/packages/merman/package.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://json.schemastore.org/package.json", + "name": "@opencode-ai/merman", + "version": "0.0.0", + "private": true, + "type": "module", + "exports": { + "./plugin": "./src/plugin.ts" + }, + "scripts": { + "test": "bun test --timeout 30000 --only-failures", + "typecheck": "tsgo --noEmit" + }, + "dependencies": { + "@opencode-ai/plugin": "workspace:*", + "@opentui/core": "catalog:", + "string-width": "catalog:" + }, + "devDependencies": { + "@tsconfig/bun": "catalog:", + "@types/bun": "catalog:", + "@typescript/native-preview": "catalog:" + } +} diff --git a/packages/merman/src/core/canvas.test.ts b/packages/merman/src/core/canvas.test.ts new file mode 100644 index 00000000000..a33aa4c791c --- /dev/null +++ b/packages/merman/src/core/canvas.test.ts @@ -0,0 +1,96 @@ +import { describe, expect, test } from "bun:test" +import stringWidth from "string-width" +import { DiagramCanvas, DiagramCanvasSizeError, type DiagramCanvasCell } from "./canvas.js" + +describe("DiagramCanvas", () => { + test("rejects canvases that exceed the rendering budget", () => { + expect(() => new DiagramCanvas(2_000, 1_000)).toThrow(DiagramCanvasSizeError) + }) + + test("writes cells and text while clipping out-of-bounds positions", () => { + const canvas = new DiagramCanvas<"label">(5, 2) + + canvas.setCell(0, 0, "A", "label") + canvas.setCell(9, 0, "X", "label") + canvas.setText(2, 1, "hey", "label") + + expect(canvas.toString()).toBe("A\n hey") + }) + + test("uses measured character widths for text placement", () => { + const canvas = new DiagramCanvas<"label">(5, 1) + + canvas.setText(0, 0, "a界b", "label") + + expect(canvas.toString()).toBe("a界b") + expect(stringWidth(canvas.toString())).toBe(4) + }) + + test("preserves combined graphemes while placing later text", () => { + const canvas = new DiagramCanvas<"label">(4, 1) + + canvas.setText(0, 0, "e\u0301x", "label") + + expect(canvas.toString()).toBe("e\u0301x") + expect(stringWidth(canvas.toString())).toBe(2) + }) + + test("merges cells through the adapter-provided merge function", () => { + type Style = "line" + const canvas = new DiagramCanvas