Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@ A full-featured terminal plugin for [CloudCLI UI](https://github.com/cloudcli-ai
- **Unicode 11** — full emoji and wide-character support
- **Auto-resize** — terminal reflows when the panel is resized

## Rendering Fallback

The terminal uses WebGL by default. If a browser/driver combination renders
box drawing, emoji, or CJK text as black squares, disable WebGL for that browser
profile and reopen the terminal:

```js
localStorage.setItem('web-terminal-disable-webgl', 'true')
```

## Installation

**From CloudCLI UI (recommended):**
Expand Down
17 changes: 11 additions & 6 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,9 @@ const THEMES: Record<string, TerminalTheme> = {

// ── Persistent prefs ──────────────────────────────────────────────────────────
const PREFS_KEY = 'web-terminal-prefs';
const WEBGL_DISABLED_KEY = 'web-terminal-disable-webgl';
const DEFAULT_FONT_FAMILY = '"Cascadia Mono", Consolas, "DejaVu Sans Mono", "Liberation Mono", "Noto Sans Mono", "Noto Sans Mono CJK JP", "Noto Sans CJK JP", "Microsoft YaHei", "MS Gothic", Meiryo, "PingFang SC", "Hiragino Sans GB", "Noto Color Emoji", Menlo, Monaco, "Courier New", monospace';
function isWebglDisabled(): boolean { try { return localStorage.getItem(WEBGL_DISABLED_KEY) === 'true'; } catch { return false; } }
function loadPrefs(): Partial<Prefs> { try { return JSON.parse(localStorage.getItem(PREFS_KEY) || '{}'); } catch { return {}; } }
function savePrefs(p: Prefs): void { try { localStorage.setItem(PREFS_KEY, JSON.stringify(p)); } catch { /* ignore */ } }

Expand Down Expand Up @@ -353,7 +356,7 @@ class TerminalSession {
this.terminal = new opts.Terminal({
cursorBlink: true,
fontSize: opts.prefs.fontSize || 14,
fontFamily: opts.prefs.fontFamily || "Menlo, Monaco, 'Courier New', monospace",
fontFamily: opts.prefs.fontFamily || DEFAULT_FONT_FAMILY,
allowProposedApi: true, convertEol: true, scrollback: 10000,
tabStopWidth: 4, macOptionIsMeta: true, macOptionClickForcesSelection: true,
theme: THEMES[opts.prefs.theme || 'VS Dark'],
Expand All @@ -368,11 +371,13 @@ class TerminalSession {

this.terminal.open(this.el);

try {
const webgl = new opts.WebglAddon();
webgl.onContextLoss(() => { try { webgl.dispose(); } catch { /* ignore */ } });
this.terminal.loadAddon(webgl);
} catch { /* ignore */ }
if (!isWebglDisabled()) {
try {
const webgl = new opts.WebglAddon();
webgl.onContextLoss(() => { try { webgl.dispose(); } catch { /* ignore */ } });
this.terminal.loadAddon(webgl);
} catch { /* ignore */ }
}

this.terminal.attachCustomKeyEventHandler((e: KeyboardEvent) => {
if (e.type !== 'keydown') return true;
Expand Down
17 changes: 14 additions & 3 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ interface PtyProcess {
kill(): void;
pause(): void;
resume(): void;
onData(callback: (data: string) => void): void;
onData(callback: (data: string | Buffer) => void): void;
onExit(callback: (event: { exitCode: number; signal?: number }) => void): void;
spawn(shell: string, args: string[], opts: any): PtyProcess;
}
Expand Down Expand Up @@ -128,6 +128,7 @@ wss.on('connection', (ws: any) => {
rows: 24,
cwd,
env: { ...process.env, TERM: 'xterm-256color', COLORTERM: 'truecolor', TERM_PROGRAM: 'web-terminal' },
encoding: null,
});
} catch (err) {
safeSend(ws, { type: 'error', message: `Failed to spawn shell: ${(err as Error).message}` });
Expand All @@ -138,10 +139,20 @@ wss.on('connection', (ws: any) => {
sessions.set(sessionId, { pty: ptyProc, ws });
safeSend(ws, { type: 'ready', sessionId, shell, cwd });

ptyProc.onData((chunk: string) => {
const decoder = new TextDecoder('utf-8', { fatal: false });

ptyProc.onData((chunk: string | Buffer) => {
const text = typeof chunk === 'string'
? chunk
: decoder.decode(chunk, { stream: true });

if (!text) {
return;
}

ptyProc.pause();
if (ws.readyState === WebSocket.OPEN) {
ws.send(chunk, () => ptyProc.resume());
ws.send(text, () => ptyProc.resume());
} else {
ptyProc.resume();
}
Expand Down