diff --git a/packages/utils/CHANGELOG.md b/packages/utils/CHANGELOG.md index 4f9c0e3fa..0c2c322bd 100644 --- a/packages/utils/CHANGELOG.md +++ b/packages/utils/CHANGELOG.md @@ -21,6 +21,9 @@ ### Fixed - Reduced partial JSON parsing work for long streaming tool-call arguments by scaling the parse throttle geometrically with buffer size. +### Fixed + +- Fixed Mermaid ASCII multi-word edge labels allowing routed lines to show through spaces ([#8098](https://github.com/can1357/oh-my-pi/issues/8098)). ## [17.2.11] - 2026-08-07 diff --git a/packages/utils/src/vendor/mermaid-ascii/ascii/canvas.ts b/packages/utils/src/vendor/mermaid-ascii/ascii/canvas.ts index 49cf5e168..229934953 100644 --- a/packages/utils/src/vendor/mermaid-ascii/ascii/canvas.ts +++ b/packages/utils/src/vendor/mermaid-ascii/ascii/canvas.ts @@ -8,7 +8,7 @@ import type { Canvas, DrawingCoord, RoleCanvas, CharRole, AsciiTheme, ColorMode } from './types' import { colorizeLine, DEFAULT_ASCII_THEME } from './ansi' -import { displayWidth, toCells, WIDE_PAD } from '../text-metrics' +import { displayWidth, LABEL_SPACE, toCells, WIDE_PAD } from '../text-metrics' /** * Create a blank canvas filled with spaces. @@ -189,7 +189,7 @@ export function isJunctionChar(c: string): boolean { * letter/digit test misses. */ function isLabelChar(c: string): boolean { - return c === WIDE_PAD || displayWidth(c) === 2 || /[\p{L}\p{N}]/u.test(c) + return c === LABEL_SPACE || c === WIDE_PAD || displayWidth(c) === 2 || /[\p{L}\p{N}]/u.test(c) } /** @@ -268,7 +268,7 @@ export function mergeCanvases( for (let x = 0; x < overlay.length; x++) { for (let y = 0; y < overlay[0]!.length; y++) { const c = overlay[x]![y]! - // WIDE_PAD cells are written atomically with their lead below + // Spaces are transparent; WIDE_PAD cells are written atomically with their lead below if (c === ' ' || c === WIDE_PAD) continue const mx = x + offset.x const my = y + offset.y @@ -327,8 +327,8 @@ export function canvasToString(canvas: Canvas, options?: CanvasToStringOptions): let line = '' for (let x = 0; x <= maxX; x++) { const c = canvas[x]![y]! - // Skip wide-glyph continuation cells: the glyph itself spans 2 columns - if (c !== WIDE_PAD) line += c + // Skip wide-glyph continuation cells and restore opaque label spaces. + if (c !== WIDE_PAD) line += c === LABEL_SPACE ? ' ' : c } lines.push(line) } else { @@ -338,7 +338,7 @@ export function canvasToString(canvas: Canvas, options?: CanvasToStringOptions): for (let x = 0; x <= maxX; x++) { const c = canvas[x]![y]! if (c === WIDE_PAD) continue - chars.push(c) + chars.push(c === LABEL_SPACE ? ' ' : c) roles.push(roleCanvas[x]?.[y] ?? null) } lines.push(colorizeLine(chars, roles, theme, colorMode)) diff --git a/packages/utils/src/vendor/mermaid-ascii/ascii/draw.ts b/packages/utils/src/vendor/mermaid-ascii/ascii/draw.ts index 1cefc6c49..2e1e86189 100644 --- a/packages/utils/src/vendor/mermaid-ascii/ascii/draw.ts +++ b/packages/utils/src/vendor/mermaid-ascii/ascii/draw.ts @@ -21,7 +21,7 @@ import { gridToDrawingCoord, lineToDrawing } from './grid' import { splitLines } from './multiline-utils' import { getCorners } from './shapes/corners' import { getShapeAttachmentPoint } from './shapes/index' -import { displayWidth, toCells, WIDE_PAD } from '../text-metrics' +import { displayWidth, LABEL_SPACE, toCells, WIDE_PAD } from '../text-metrics' // ============================================================================ // Node drawing — renders a node using shape-aware rendering @@ -679,7 +679,7 @@ function drawTextOnLine(canvas: Canvas, line: DrawingCoord[], label: string, isU for (let i = 0; i < lines.length; i++) { const lineText = lines[i]! const startX = middleX - Math.floor(displayWidth(lineText) / 2) - drawText(canvas, { x: startX, y: startY + i }, lineText) + drawText(canvas, { x: startX, y: startY + i }, lineText.replaceAll(' ', LABEL_SPACE)) } } diff --git a/packages/utils/src/vendor/mermaid-ascii/text-metrics.ts b/packages/utils/src/vendor/mermaid-ascii/text-metrics.ts index 77c89e7b0..ebe805f3d 100644 --- a/packages/utils/src/vendor/mermaid-ascii/text-metrics.ts +++ b/packages/utils/src/vendor/mermaid-ascii/text-metrics.ts @@ -29,6 +29,9 @@ */ export const WIDE_PAD = '\u0000' +/** Opaque label-space placeholder that serializes back to a regular space. */ +export const LABEL_SPACE = '\u0001' + const graphemeSegmenter = new Intl.Segmenter() /** diff --git a/packages/utils/test/mermaid-ascii.test.ts b/packages/utils/test/mermaid-ascii.test.ts index 1486b3e17..5f0100862 100644 --- a/packages/utils/test/mermaid-ascii.test.ts +++ b/packages/utils/test/mermaid-ascii.test.ts @@ -37,6 +37,24 @@ describe("renderMermaidAscii", () => { expect(rows[markerRow + 1]).toMatch(/╰─+╯/); }); + it("masks routed lines behind spaces in edge labels", () => { + const horizontal = renderMermaidAscii( + ["graph LR", " A[Agent] -->|on Mac| B[Server]", " A -->|on Linux| C[Cluster]"].join("\n"), + { colorMode: "none", useAscii: false }, + ); + const vertical = renderMermaidAscii("graph TD\n A[Agent] -->|to c| C[Cluster]", { + colorMode: "none", + useAscii: false, + }); + + expect(horizontal).toContain("on Mac"); + expect(horizontal).toContain("on Linux"); + expect(horizontal).not.toContain("on─Mac"); + expect(horizontal).not.toContain("on─Linux"); + expect(vertical).toContain("to c"); + expect(vertical).not.toContain("to│c"); + }); + it("returns a bounded fallback for declaration orders that make a clean route unreachable", () => { const rendered = renderMermaidAsciiSafe( [