diff --git a/package-lock.json b/package-lock.json
index 9493ab33..197904e1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "claudeman",
- "version": "0.1600",
+ "version": "0.1618",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "claudeman",
- "version": "0.1600",
+ "version": "0.1618",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
@@ -20,6 +20,7 @@
"uuid": "^10.0.0",
"xterm": "^5.3.0",
"xterm-addon-fit": "^0.8.0",
+ "xterm-addon-unicode11": "^0.6.0",
"xterm-addon-webgl": "^0.16.0",
"zod": "^4.3.6"
},
@@ -4669,6 +4670,16 @@
"xterm": "^5.0.0"
}
},
+ "node_modules/xterm-addon-unicode11": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/xterm-addon-unicode11/-/xterm-addon-unicode11-0.6.0.tgz",
+ "integrity": "sha512-5pkb8YoS/deRtNqQRw8t640mu+Ga8B2MG3RXGQu0bwgcfr8XiXIRI880TWM49ICAHhTmnOLPzIIBIjEnCq7k2A==",
+ "deprecated": "This package is now deprecated. Move to @xterm/addon-unicode11 instead.",
+ "license": "MIT",
+ "peerDependencies": {
+ "xterm": "^5.0.0"
+ }
+ },
"node_modules/xterm-addon-webgl": {
"version": "0.16.0",
"resolved": "https://registry.npmjs.org/xterm-addon-webgl/-/xterm-addon-webgl-0.16.0.tgz",
diff --git a/package.json b/package.json
index 52cee7d8..cb518489 100644
--- a/package.json
+++ b/package.json
@@ -10,7 +10,7 @@
},
"scripts": {
"postinstall": "node scripts/postinstall.js",
- "build": "tsc && chmod +x dist/index.js && mkdir -p dist/web dist/templates dist/web/public/vendor && cp -r src/web/public dist/web/ && cp src/templates/case-template.md dist/templates/ && cp node_modules/xterm/css/xterm.css dist/web/public/vendor/ && npx esbuild node_modules/xterm/lib/xterm.js --minify --outfile=dist/web/public/vendor/xterm.min.js && npx esbuild node_modules/xterm-addon-fit/lib/xterm-addon-fit.js --minify --outfile=dist/web/public/vendor/xterm-addon-fit.min.js && cp node_modules/xterm-addon-webgl/lib/xterm-addon-webgl.js dist/web/public/vendor/xterm-addon-webgl.min.js && npx esbuild dist/web/public/app.js --minify --drop:console --outfile=dist/web/public/app.js --allow-overwrite && npx esbuild dist/web/public/styles.css --minify --outfile=dist/web/public/styles.css --allow-overwrite && npx esbuild dist/web/public/mobile.css --minify --outfile=dist/web/public/mobile.css --allow-overwrite && for f in dist/web/public/*.js dist/web/public/*.css dist/web/public/*.html dist/web/public/vendor/*.js dist/web/public/vendor/*.css; do [ -f \"$f\" ] && gzip -9 -k -f \"$f\" && { brotli -9 -k -f \"$f\" 2>/dev/null || true; }; done",
+ "build": "tsc && chmod +x dist/index.js && mkdir -p dist/web dist/templates dist/web/public/vendor && cp -r src/web/public dist/web/ && cp src/templates/case-template.md dist/templates/ && cp node_modules/xterm/css/xterm.css dist/web/public/vendor/ && npx esbuild node_modules/xterm/lib/xterm.js --minify --outfile=dist/web/public/vendor/xterm.min.js && npx esbuild node_modules/xterm-addon-fit/lib/xterm-addon-fit.js --minify --outfile=dist/web/public/vendor/xterm-addon-fit.min.js && cp node_modules/xterm-addon-webgl/lib/xterm-addon-webgl.js dist/web/public/vendor/xterm-addon-webgl.min.js && npx esbuild node_modules/xterm-addon-unicode11/lib/xterm-addon-unicode11.js --minify --outfile=dist/web/public/vendor/xterm-addon-unicode11.min.js && npx esbuild dist/web/public/app.js --minify --drop:console --outfile=dist/web/public/app.js --allow-overwrite && npx esbuild dist/web/public/styles.css --minify --outfile=dist/web/public/styles.css --allow-overwrite && npx esbuild dist/web/public/mobile.css --minify --outfile=dist/web/public/mobile.css --allow-overwrite && for f in dist/web/public/*.js dist/web/public/*.css dist/web/public/*.html dist/web/public/vendor/*.js dist/web/public/vendor/*.css; do [ -f \"$f\" ] && gzip -9 -k -f \"$f\" && { brotli -9 -k -f \"$f\" 2>/dev/null || true; }; done",
"start": "node dist/index.js",
"dev": "tsx src/index.ts",
"web": "node dist/index.js web",
@@ -47,6 +47,7 @@
"uuid": "^10.0.0",
"xterm": "^5.3.0",
"xterm-addon-fit": "^0.8.0",
+ "xterm-addon-unicode11": "^0.6.0",
"xterm-addon-webgl": "^0.16.0",
"zod": "^4.3.6"
},
diff --git a/src/session.ts b/src/session.ts
index d27db4ac..015a1a86 100644
--- a/src/session.ts
+++ b/src/session.ts
@@ -949,7 +949,7 @@ export class Session extends EventEmitter {
cols: 120,
rows: 40,
cwd: this.workingDir,
- env: { ...process.env, TERM: 'xterm-256color', COLORTERM: undefined, CLAUDECODE: undefined },
+ env: { ...process.env, LANG: 'en_US.UTF-8', LC_ALL: 'en_US.UTF-8', TERM: 'xterm-256color', COLORTERM: undefined, CLAUDECODE: undefined },
});
// Set claudeSessionId immediately since we passed --session-id to Claude
@@ -1016,6 +1016,8 @@ export class Session extends EventEmitter {
cwd: this.workingDir,
env: {
...process.env,
+ LANG: 'en_US.UTF-8',
+ LC_ALL: 'en_US.UTF-8',
PATH: getAugmentedPath(),
TERM: 'xterm-256color',
COLORTERM: undefined,
@@ -1295,7 +1297,7 @@ export class Session extends EventEmitter {
cols: 120,
rows: 40,
cwd: this.workingDir,
- env: { ...process.env, TERM: 'xterm-256color', COLORTERM: undefined, CLAUDECODE: undefined },
+ env: { ...process.env, LANG: 'en_US.UTF-8', LC_ALL: 'en_US.UTF-8', TERM: 'xterm-256color', COLORTERM: undefined, CLAUDECODE: undefined },
});
} catch (spawnErr) {
console.error('[Session] Failed to spawn PTY for shell mux attachment:', spawnErr);
@@ -1330,6 +1332,8 @@ export class Session extends EventEmitter {
cwd: this.workingDir,
env: {
...process.env,
+ LANG: 'en_US.UTF-8',
+ LC_ALL: 'en_US.UTF-8',
TERM: 'xterm-256color',
CLAUDEMAN_MUX: '1',
CLAUDEMAN_SESSION_ID: this.id,
@@ -1458,6 +1462,8 @@ export class Session extends EventEmitter {
cwd: this.workingDir,
env: {
...process.env,
+ LANG: 'en_US.UTF-8',
+ LC_ALL: 'en_US.UTF-8',
PATH: getAugmentedPath(),
TERM: 'xterm-256color',
COLORTERM: undefined,
diff --git a/src/tmux-manager.ts b/src/tmux-manager.ts
index 253e8dde..bcd6470a 100644
--- a/src/tmux-manager.ts
+++ b/src/tmux-manager.ts
@@ -265,6 +265,8 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
const pathExport = claudeDir ? `export PATH="${claudeDir}:$PATH" && ` : '';
const envExports = [
+ 'export LANG=en_US.UTF-8',
+ 'export LC_ALL=en_US.UTF-8',
'unset CLAUDECODE',
'unset COLORTERM',
'export CLAUDEMAN_MUX=1',
@@ -431,6 +433,8 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
const claudeDir = findClaudeDir();
const pathExport = claudeDir ? `export PATH="${claudeDir}:$PATH" && ` : '';
const envExports = [
+ 'export LANG=en_US.UTF-8',
+ 'export LC_ALL=en_US.UTF-8',
'unset CLAUDECODE',
'unset COLORTERM',
'export CLAUDEMAN_MUX=1',
diff --git a/src/web/public/app.js b/src/web/public/app.js
index 66ef485e..95a5dc01 100644
--- a/src/web/public/app.js
+++ b/src/web/public/app.js
@@ -933,8 +933,39 @@ function _zl_readTextAfterPrompt(terminal, prompt, offset) {
} catch { return ''; }
}
+// Get visual cell width of a character (CJK wide chars = 2, others = 1)
+function _zl_charCellWidth(ch, terminal) {
+ if (terminal?.unicode?.getStringCellWidth) {
+ return terminal.unicode.getStringCellWidth(ch);
+ }
+ const code = ch.codePointAt(0);
+ if (code >= 0x1100 && (
+ (code <= 0x115F) || // Hangul Jamo
+ (code >= 0x2E80 && code <= 0x303E) || // CJK Radicals, Kangxi, Ideographic
+ (code >= 0x3040 && code <= 0x33BF) || // Hiragana, Katakana, Bopomofo, CJK Compat
+ (code >= 0x3400 && code <= 0x4DBF) || // CJK Unified Ext A
+ (code >= 0x4E00 && code <= 0xA4CF) || // CJK Unified, Yi
+ (code >= 0xA960 && code <= 0xA97C) || // Hangul Jamo Extended-A
+ (code >= 0xAC00 && code <= 0xD7A3) || // Hangul Syllables
+ (code >= 0xF900 && code <= 0xFAFF) || // CJK Compat Ideographs
+ (code >= 0xFE30 && code <= 0xFE6F) || // CJK Compat Forms
+ (code >= 0xFF01 && code <= 0xFF60) || // Fullwidth Forms
+ (code >= 0xFFE0 && code <= 0xFFE6) || // Fullwidth Signs
+ (code >= 0x1F000 && code <= 0x1FBFF) || // Mahjong, Domino, Emoji
+ (code >= 0x20000 && code <= 0x2FFFF) || // CJK Unified Ext B-F
+ (code >= 0x30000 && code <= 0x3FFFF) // CJK Unified Ext G+
+ )) return 2;
+ return 1;
+}
+
+function _zl_stringCellWidth(str, terminal) {
+ let w = 0;
+ for (const ch of str) w += _zl_charCellWidth(ch, terminal);
+ return w;
+}
+
function _zl_renderOverlay(container, params) {
- const { lines, startCol, totalCols, cellW, cellH, promptRow, font, showCursor, cursorColor } = params;
+ const { lines, startCol, totalCols, cellW, cellH, promptRow, font, showCursor, cursorColor, terminal } = params;
container.style.left = '0px';
container.style.top = (promptRow * cellH) + 'px';
container.innerHTML = '';
@@ -943,13 +974,13 @@ function _zl_renderOverlay(container, params) {
const leftPx = i === 0 ? startCol * cellW : 0;
const widthPx = i === 0 ? (fullWidthPx - leftPx) : fullWidthPx;
const topPx = i * cellH;
- const lineEl = _zl_makeLine(lines[i], leftPx, topPx, widthPx, cellH, cellW, font);
+ const lineEl = _zl_makeLine(lines[i], leftPx, topPx, widthPx, cellH, cellW, font, terminal);
container.appendChild(lineEl);
}
if (showCursor) {
const lastLine = lines[lines.length - 1];
const lastLineLeft = lines.length === 1 ? startCol : 0;
- const cursorCol = lastLineLeft + lastLine.length;
+ const cursorCol = lastLineLeft + _zl_stringCellWidth(lastLine, terminal);
if (cursorCol < totalCols) {
const cursor = document.createElement('span');
cursor.style.cssText = 'position:absolute;display:inline-block';
@@ -964,7 +995,7 @@ function _zl_renderOverlay(container, params) {
container.style.display = '';
}
-function _zl_makeLine(text, leftPx, topPx, widthPx, cellH, cellW, font) {
+function _zl_makeLine(text, leftPx, topPx, widthPx, cellH, cellW, font, terminal) {
const el = document.createElement('div');
el.style.cssText = 'position:absolute;pointer-events:none';
el.style.backgroundColor = font.backgroundColor;
@@ -973,18 +1004,21 @@ function _zl_makeLine(text, leftPx, topPx, widthPx, cellH, cellW, font) {
el.style.width = widthPx + 'px';
el.style.height = (cellH + 1) + 'px';
el.style.lineHeight = cellH + 'px';
- for (let i = 0; i < text.length; i++) {
+ let colOffset = 0;
+ for (const ch of text) {
+ const charWidth = _zl_charCellWidth(ch, terminal);
const span = document.createElement('span');
span.style.cssText = "position:absolute;display:inline-block;text-align:center;pointer-events:none;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;text-rendering:geometricPrecision;font-feature-settings:'liga' 0,'calt' 0";
- span.style.left = (i * cellW) + 'px';
- span.style.width = cellW + 'px';
+ span.style.left = (colOffset * cellW) + 'px';
+ span.style.width = (charWidth * cellW) + 'px';
span.style.fontFamily = font.fontFamily;
span.style.fontSize = font.fontSize;
span.style.fontWeight = font.fontWeight;
span.style.color = font.color;
if (font.letterSpacing) span.style.letterSpacing = font.letterSpacing;
- span.textContent = text[i];
+ span.textContent = ch;
el.appendChild(span);
+ colOffset += charWidth;
}
return el;
}
@@ -1192,11 +1226,15 @@ class ZerolagInputAddon {
if (renderKey === this._lastRenderKey && this._overlay.style.display !== 'none') return;
this._lastRenderKey = renderKey;
const firstLineCols = Math.max(1, totalCols - startCol);
- const lines = []; let remaining = displayText;
- lines.push(remaining.slice(0, firstLineCols)); remaining = remaining.slice(firstLineCols);
- while (remaining.length > 0) { lines.push(remaining.slice(0, totalCols)); remaining = remaining.slice(totalCols); }
+ const lines = []; const chars = [...displayText]; let ci = 0;
+ { let lineStr = '', lineCols = 0;
+ while (ci < chars.length) { const cw = _zl_charCellWidth(chars[ci], this._terminal); if (lineCols + cw > firstLineCols) break; lineStr += chars[ci]; lineCols += cw; ci++; }
+ lines.push(lineStr); }
+ while (ci < chars.length) { let lineStr = '', lineCols = 0;
+ while (ci < chars.length) { const cw = _zl_charCellWidth(chars[ci], this._terminal); if (lineCols + cw > totalCols) break; lineStr += chars[ci]; lineCols += cw; ci++; }
+ lines.push(lineStr); }
const cursorColor = this._options.cursorColor ?? this._terminal.options.theme?.cursor ?? _ZL_DEFAULT_CURSOR;
- _zl_renderOverlay(this._overlay, { lines, startCol, totalCols, cellW, cellH, promptRow: activePrompt.row, font: this._font, showCursor: this._options.showCursor, cursorColor });
+ _zl_renderOverlay(this._overlay, { lines, startCol, totalCols, cellW, cellH, promptRow: activePrompt.row, font: this._font, showCursor: this._options.showCursor, cursorColor, terminal: this._terminal });
} catch {
if (this._overlay) { this._overlay.innerHTML = ''; this._overlay.style.display = 'none'; }
}
@@ -2010,6 +2048,14 @@ class ClaudemanApp {
this.fitAddon = new FitAddon.FitAddon();
this.terminal.loadAddon(this.fitAddon);
+ if (typeof Unicode11Addon !== 'undefined') {
+ try {
+ const unicode11Addon = new Unicode11Addon.Unicode11Addon();
+ this.terminal.loadAddon(unicode11Addon);
+ this.terminal.unicode.activeVersion = '11';
+ } catch (_e) { /* Unicode11 addon failed — default Unicode handling used */ }
+ }
+
const container = document.getElementById('terminalContainer');
this.terminal.open(container);
@@ -14365,6 +14411,14 @@ class ClaudemanApp {
const fitAddon = new FitAddon.FitAddon();
terminal.loadAddon(fitAddon);
+ if (typeof Unicode11Addon !== 'undefined') {
+ try {
+ const unicode11Addon = new Unicode11Addon.Unicode11Addon();
+ terminal.loadAddon(unicode11Addon);
+ terminal.unicode.activeVersion = '11';
+ } catch (_e) { /* Unicode11 addon failed */ }
+ }
+
try {
terminal.open(body);
} catch (err) {
diff --git a/src/web/public/index.html b/src/web/public/index.html
index a5f6d031..4f5f210d 100644
--- a/src/web/public/index.html
+++ b/src/web/public/index.html
@@ -18,6 +18,7 @@
+