From cca63892f6b87651833698fbebbcf2a526f0567e Mon Sep 17 00:00:00 2001 From: maxx-masa Date: Tue, 2 Jun 2026 22:48:21 +0900 Subject: [PATCH] fix(ui): render workspace terminal with Canvas + CJK fonts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Switch the xterm renderer from WebGL to Canvas (@xterm/addon-canvas) and add "Noto Sans Mono CJK JP" / "Noto Sans CJK JP" to the terminal font stack so Japanese text renders correctly in workspace sessions. The WebGL glyph atlas drew any styled CJK cell it could not rasterize (notably italic Japanese — Noto Sans Mono CJK JP has no italic face) as a solid black box. The Canvas renderer rasterizes each cell via 2D fillText, falling back through the browser font stack and synthesizing obliques, so bold/italic Japanese renders correctly — while staying much faster than the DOM renderer. Co-Authored-By: Claude Opus 4.8 --- pnpm-lock.yaml | 12 ++++++++++++ ui/package.json | 1 + ui/src/components/workspace/Terminal.tsx | 23 +++++++++++++++-------- 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8829598c0..245c09a0c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -284,6 +284,9 @@ importers: '@vercel/analytics': specifier: ^2.0.1 version: 2.0.1(react@19.2.4) + '@xterm/addon-canvas': + specifier: ^0.7.0 + version: 0.7.0(@xterm/xterm@5.5.0) '@xterm/addon-fit': specifier: ^0.10.0 version: 0.10.0(@xterm/xterm@5.5.0) @@ -1769,6 +1772,11 @@ packages: resolution: {integrity: sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==} engines: {node: '>=14.6'} + '@xterm/addon-canvas@0.7.0': + resolution: {integrity: sha512-LF5LYcfvefJuJ7QotNRdRSPc9YASAVDeoT5uyXS/nZshZXjYplGXRECBGiznwvhNL2I8bq1Lf5MzRwstsYQ2Iw==} + peerDependencies: + '@xterm/xterm': ^5.0.0 + '@xterm/addon-fit@0.10.0': resolution: {integrity: sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==} peerDependencies: @@ -6018,6 +6026,10 @@ snapshots: '@xmldom/xmldom@0.9.10': {} + '@xterm/addon-canvas@0.7.0(@xterm/xterm@5.5.0)': + dependencies: + '@xterm/xterm': 5.5.0 + '@xterm/addon-fit@0.10.0(@xterm/xterm@5.5.0)': dependencies: '@xterm/xterm': 5.5.0 diff --git a/ui/package.json b/ui/package.json index 582ac5a99..132d225c1 100644 --- a/ui/package.json +++ b/ui/package.json @@ -12,6 +12,7 @@ }, "dependencies": { "@vercel/analytics": "^2.0.1", + "@xterm/addon-canvas": "^0.7.0", "@xterm/addon-fit": "^0.10.0", "@xterm/addon-web-links": "^0.11.0", "@xterm/addon-webgl": "^0.18.0", diff --git a/ui/src/components/workspace/Terminal.tsx b/ui/src/components/workspace/Terminal.tsx index 6204e32d1..41434232b 100644 --- a/ui/src/components/workspace/Terminal.tsx +++ b/ui/src/components/workspace/Terminal.tsx @@ -1,9 +1,9 @@ import { lazy, Suspense, useEffect, useRef, useState } from 'react'; import type { ReactElement } from 'react'; +import { CanvasAddon } from '@xterm/addon-canvas'; import { FitAddon } from '@xterm/addon-fit'; import { WebLinksAddon } from '@xterm/addon-web-links'; -import { WebglAddon } from '@xterm/addon-webgl'; import { Terminal as Xterm } from '@xterm/xterm'; import '@xterm/xterm/css/xterm.css'; @@ -112,7 +112,7 @@ export function TerminalView(props: TerminalViewProps): ReactElement { const term = new Xterm({ theme: darkTheme, fontFamily: - 'ui-monospace, "SF Mono", Menlo, Monaco, "Cascadia Mono", "DejaVu Sans Mono", monospace', + 'ui-monospace, "SF Mono", Menlo, Monaco, "Cascadia Mono", "DejaVu Sans Mono", "Noto Sans Mono CJK JP", "Noto Sans CJK JP", monospace', fontSize: 13, lineHeight: 1.2, cursorBlink: true, @@ -127,13 +127,20 @@ export function TerminalView(props: TerminalViewProps): ReactElement { term.loadAddon(new WebLinksAddon()); term.open(container); - let webgl: WebglAddon | null = null; + // Canvas renderer (not WebGL). The WebGL glyph atlas renders any styled + // CJK cell it can't rasterize — notably italic Japanese, since Noto Sans + // Mono CJK JP has no italic face — as a solid black box. Canvas rasterizes + // each cell via 2D fillText, which falls back through the browser font + // stack and synthesizes obliques, so bold/italic Japanese renders + // correctly. It's far faster than the DOM renderer (which made input echo + // sluggish in heavy TUIs like the agent CLIs) while avoiding WebGL's + // missing-glyph boxes. + let canvas: CanvasAddon | null = null; try { - webgl = new WebglAddon(); - webgl.onContextLoss(() => webgl?.dispose()); - term.loadAddon(webgl); + canvas = new CanvasAddon(); + term.loadAddon(canvas); } catch { - webgl = null; + canvas = null; } safeFit(fit); @@ -257,7 +264,7 @@ export function TerminalView(props: TerminalViewProps): ReactElement { } catch { // ignore } - webgl?.dispose(); + canvas?.dispose(); term.dispose(); }; }, [wsId, sessionId, wsUrl]);