This skill provides unified expert-level guidance for terminal implementation in VS Code extensions...
This skill provides comprehensive knowledge for implementing terminal features in VS Code extensions. It unifies xterm.js implementation details with VS Code's terminal architecture patterns, ensuring consistency with the official VS Code terminal implementation.
| Addon | Package | Purpose | Loading |
|---|---|---|---|
| FitAddon | @xterm/addon-fit |
Auto-resize to container | Always |
| WebglAddon | @xterm/addon-webgl |
GPU acceleration (30%+ perf) | Lazy |
| SerializeAddon | @xterm/addon-serialize |
Session persistence | Lazy |
| SearchAddon | @xterm/addon-search |
Find in terminal | Lazy |
| Unicode11Addon | @xterm/addon-unicode11 |
Extended Unicode | Config |
| WebLinksAddon | @xterm/addon-web-links |
Clickable URLs | Config |
| ImageAddon | @xterm/addon-image |
Inline images | Config |
| Operation | Target | Notes |
|---|---|---|
| Terminal creation | <500ms | Including PTY spawn |
| Session restore | <3s | 1000 lines scrollback |
| Terminal disposal | <100ms | Full cleanup |
| Resize handling | 100ms debounce | Prevents excessive calls |
| Output buffering | 16ms flush | ~60fps rendering |
import { Terminal } from '@xterm/xterm';
import { FitAddon } from '@xterm/addon-fit';
import { WebglAddon } from '@xterm/addon-webgl';
class TerminalManager {
private terminal: Terminal;
private fitAddon: FitAddon;
private container: HTMLElement;
constructor(container: HTMLElement) {
this.container = container;
this.terminal = this.createTerminal();
this.fitAddon = new FitAddon();
}
private createTerminal(): Terminal {
return new Terminal({
// Appearance
fontFamily: '"Cascadia Code", "Fira Code", monospace',
fontSize: 14,
lineHeight: 1.2,
cursorStyle: 'block',
cursorBlink: true,
// Behavior
scrollback: 5000,
altClickMovesCursor: true,
// Performance
fastScrollModifier: 'alt',
smoothScrollDuration: 0,
// Theme
theme: {
background: '#1e1e1e',
foreground: '#d4d4d4',
cursor: '#d4d4d4',
selectionBackground: '#264f78'
}
});
}
initialize(): void {
this.terminal.loadAddon(this.fitAddon);
this.terminal.open(this.container);
this.fitAddon.fit();
this.setupResizeObserver();
}
private setupResizeObserver(): void {
const resizeObserver = new ResizeObserver(() => {
requestAnimationFrame(() => this.fitAddon.fit());
});
resizeObserver.observe(this.container);
}
}
class RenderingManager {
private webglAddon: WebglAddon | undefined;
private terminal: Terminal;
enableWebGL(): boolean {
try {
this.webglAddon = new WebglAddon();
this.webglAddon.onContextLoss(() => {
console.warn('WebGL context lost, falling back to canvas');
this.disableWebGL();
});
this.terminal.loadAddon(this.webglAddon);
return true;
} catch (error) {
console.warn('WebGL not available:', error);
return false;
}
}
disableWebGL(): void {
this.webglAddon?.dispose();
this.webglAddon = undefined;
}
}
import { SerializeAddon } from '@xterm/addon-serialize';
class ScrollbackManager {
private serializeAddon: SerializeAddon;
private terminal: Terminal;
constructor(terminal: Terminal) {
this.terminal = terminal;
this.serializeAddon = new SerializeAddon();
this.terminal.loadAddon(this.serializeAddon);
}
saveScrollback(options?: { scrollback?: number }): string {
return this.serializeAddon.serialize({
scrollback: options?.scrollback ?? 1000,
excludeModes: true,
excludeAltBuffer: true
});
}
restoreScrollback(content: string): void {
this.terminal.write(content);
}
}
class OutputBuffer {
private terminal: Terminal;
private buffer: string = '';
private flushScheduled = false;
private flushInterval: number;
constructor(terminal: Terminal, flushInterval: number = 16) {
this.terminal = terminal;
this.flushInterval = flushInterval;
}
write(data: string): void {
this.buffer += data;
// Detect high-frequency output
if (this.buffer.length > 10000) {
this.flushInterval = 4; // 250fps for AI agents
}
this.scheduleFlush();
}
private scheduleFlush(): void {
if (!this.flushScheduled) {
this.flushScheduled = true;
setTimeout(() => this.flush(), this.flushInterval);
}
}
private flush(): void {
if (this.buffer.length > 0) {
this.terminal.write(this.buffer);
this.buffer = '';
}
this.flushScheduled = false;
// Reset to normal frequency after idle
if (this.buffer.length === 0) {
this.flushInterval = 16;
}
}
}
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā Main Process (Electron) ā
āāāāāāāāāāāāāāāāāāāāā¬āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
āāāāāāāāāāāāā¼āāāāāāāāāāāā¬āāāāāāāāāāāāāāāāāāā
ā¼ ā¼ ā¼ ā¼
āāāāāāāāāāāāāāāā āāāāāāāāāāāāāāāā āāāāāāāāāāāāāāāā āāāāāāāāāāāāāāāā
ā Renderer ā ā Extension ā ā PTY Host ā ā Shared ā
ā Process ā ā Host Process ā ā Process ā ā Process ā
āāāāāāāāāāāāāāā⤠āāāāāāāāāāāāāāā⤠āāāāāāāāāāāāāāā⤠āāāāāāāāāāāāāāāā¤
ā - Workbench ā ā - Extension ā ā - PtyService ā ā - Extension ā
ā - XtermTerm ā ā execution ā ā - node-pty ā ā mgmt ā
ā - UI ā ā - ExtHost ā ā - Shell ā ā ā
āāāāāāāāāāāāāāāā ā Terminal ā ā processes ā āāāāāāāāāāāāāāāā
āāāāāāāāāāāāāāāā āāāāāāāāāāāāāāāā
import * as pty from 'node-pty';
class TerminalProcess {
private _ptyProcess: pty.IPty;
constructor(
shellPath: string,
args: string[],
cwd: string,
cols: number,
rows: number,
env: { [key: string]: string }
) {
this._ptyProcess = pty.spawn(shellPath, args, {
name: 'xterm-256color',
cols,
rows,
cwd,
env
});
this._ptyProcess.onData(data => this._onData.fire(data));
this._ptyProcess.onExit(({ exitCode }) => this._onExit.fire(exitCode));
}
write(data: string): void {
this._ptyProcess.write(data);
}
resize(cols: number, rows: number): void {
if (cols < 1 || rows < 1) return; // Prevent native exceptions
this._ptyProcess.resize(cols, rows);
}
shutdown(): void {
this._ptyProcess.kill();
}
}
class FlowControlledProcess {
private _unacknowledgedCharCount = 0;
private _isPaused = false;
private readonly HIGH_WATERMARK = 100000;
private readonly LOW_WATERMARK = 5000;
handleData(data: string): void {
this._unacknowledgedCharCount += data.length;
if (!this._isPaused && this._unacknowledgedCharCount > this.HIGH_WATERMARK) {
this._ptyProcess.pause();
this._isPaused = true;
}
this._onData.fire(data);
}
acknowledge(charCount: number): void {
this._unacknowledgedCharCount -= charCount;
if (this._isPaused && this._unacknowledgedCharCount <= this.LOW_WATERMARK) {
this._ptyProcess.resume();
this._isPaused = false;
}
}
}
User Keypress ā Browser KeyboardEvent ā Custom key event handler
ā Check VS Code keybindings
āāā [Match] ā Execute command, preventDefault
āāā [No Match] ā Pass to xterm.js ā Emits onData ā Write to PTY
terminal.attachCustomKeyEventHandler((event: KeyboardEvent) => {
// Ctrl+C for copy (when selection exists)
if (event.ctrlKey && event.key === 'c' && terminal.hasSelection()) {
navigator.clipboard.writeText(terminal.getSelection());
return false;
}
// Ctrl+V for paste
if (event.ctrlKey && event.key === 'v') {
navigator.clipboard.readText().then(text => terminal.paste(text));
return false;
}
// Custom shortcuts
if (event.ctrlKey && event.key === 'l') {
terminal.clear();
return false;
}
return true; // Let xterm.js handle
});
class IMEHandler {
private _composing = false;
handleCompositionStart(): void {
this._composing = true;
}
handleCompositionEnd(text: string): void {
this._composing = false;
this._pty.write(text);
}
handleKeyDown(event: KeyboardEvent): boolean {
if (this._composing) return false; // Don't process during composition
return true;
}
}
OSC 633 ; A ST ā Prompt start
OSC 633 ; B ST ā Command line start
OSC 633 ; C ST ā Command execution start
OSC 633 ; D [; <ExitCode>] ST ā Command finished
OSC 633 ; P ; <Property>=<Value> ST ā Set property
class ShellIntegrationAddon implements ITerminalAddon {
activate(terminal: Terminal): void {
terminal.parser.registerOscHandler(633, data => {
const [code, ...params] = data.split(';');
switch (code) {
case 'A': this._handlePromptStart(); return true;
case 'B': this._handleCommandStart(); return true;
case 'C': this._handleExecutionStart(); return true;
case 'D':
const exitCode = params[0] ? parseInt(params[0]) : undefined;
this._handleCommandFinished(exitCode);
return true;
}
return false;
});
}
}
abstract class Disposable implements IDisposable {
private readonly _store = new DisposableStore();
private _isDisposed = false;
protected _register<T extends IDisposable>(disposable: T): T {
if (this._isDisposed) {
disposable.dispose();
return disposable;
}
return this._store.add(disposable);
}
dispose(): void {
if (this._isDisposed) return;
this._isDisposed = true;
this._store.dispose();
}
}
class DisposableStore implements IDisposable {
private readonly _toDispose = new Set<IDisposable>();
add<T extends IDisposable>(disposable: T): T {
this._toDispose.add(disposable);
return disposable;
}
dispose(): void {
// LIFO order for safety
const items = Array.from(this._toDispose).reverse();
this._toDispose.clear();
items.forEach(item => item.dispose());
}
}
class XtermVSCodeIntegration {
private terminal: Terminal;
private vscode: any;
private fitAddon: FitAddon;
constructor(container: HTMLElement) {
this.vscode = acquireVsCodeApi();
this.terminal = new Terminal({
fontFamily: 'var(--vscode-editor-font-family)',
fontSize: 14,
theme: this.getVSCodeTheme()
});
this.fitAddon = new FitAddon();
this.terminal.loadAddon(this.fitAddon);
this.terminal.open(container);
this.fitAddon.fit();
this.setupCommunication();
}
private getVSCodeTheme(): ITheme {
const style = getComputedStyle(document.body);
return {
background: style.getPropertyValue('--vscode-terminal-background').trim() ||
style.getPropertyValue('--vscode-editor-background').trim(),
foreground: style.getPropertyValue('--vscode-terminal-foreground').trim(),
cursor: style.getPropertyValue('--vscode-terminalCursor-foreground').trim(),
selectionBackground: style.getPropertyValue('--vscode-terminal-selectionBackground').trim()
};
}
private setupCommunication(): void {
// Send input to extension
this.terminal.onData(data => {
this.vscode.postMessage({ type: 'input', data });
});
// Receive output from extension
window.addEventListener('message', event => {
const message = event.data;
switch (message.type) {
case 'output': this.terminal.write(message.data); break;
case 'resize': this.terminal.resize(message.cols, message.rows); break;
case 'clear': this.terminal.clear(); break;
case 'focus': this.terminal.focus(); break;
}
});
this.vscode.postMessage({ type: 'ready' });
}
}
const ANSI = {
// Cursor movement
cursorUp: (n: number) => `\x1b[${n}A`,
cursorDown: (n: number) => `\x1b[${n}B`,
cursorPosition: (row: number, col: number) => `\x1b[${row};${col}H`,
// Erase
eraseLine: '\x1b[2K',
eraseScreen: '\x1b[2J',
// Text formatting
reset: '\x1b[0m',
bold: '\x1b[1m',
italic: '\x1b[3m',
underline: '\x1b[4m',
// Colors
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
// 256 colors
fg256: (n: number) => `\x1b[38;5;${n}m`,
bg256: (n: number) => `\x1b[48;5;${n}m`,
// True color (24-bit)
fgRGB: (r: number, g: number, b: number) => `\x1b[38;2;${r};${g};${b}m`,
bgRGB: (r: number, g: number, b: number) => `\x1b[48;2;${r};${g};${b}m`,
// Screen modes
alternateScreen: '\x1b[?1049h',
normalScreen: '\x1b[?1049l',
hideCursor: '\x1b[?25l',
showCursor: '\x1b[?25h'
};
For detailed reference documentation:
references/xterm-api.md - Complete xterm.js API referencereferences/vscode-terminal.md - VS Code terminal source referencereferences/escape-sequences.md - ANSI escape sequence referenceFor implementation reference:
src/vs/workbench/contrib/terminal/