diff --git a/packages/xterm-zerolag-input/src/cell-dimensions.ts b/packages/xterm-zerolag-input/src/cell-dimensions.ts index 3c5f54ae..6410dab2 100644 --- a/packages/xterm-zerolag-input/src/cell-dimensions.ts +++ b/packages/xterm-zerolag-input/src/cell-dimensions.ts @@ -4,18 +4,26 @@ import type { XtermTerminal, CellDimensions } from './types.js'; * Get cell dimensions from the terminal, handling xterm.js v5 (private API) * and v7+ (public API). * + * Returns CSS-pixel values. xterm's `device.char` is in device pixels, so + * we divide by `devicePixelRatio` to stay consistent with `css.cell`. + * * Returns `null` if the terminal is not yet rendered or dimensions are * unavailable. */ export function getCellDimensions(terminal: XtermTerminal): CellDimensions | null { // eslint-disable-next-line @typescript-eslint/no-explicit-any const t = terminal as any; + const dpr = typeof devicePixelRatio === 'number' && devicePixelRatio > 0 + ? devicePixelRatio : 1; // Try v7+ public API first if (t.dimensions?.css?.cell) { + const cellH = t.dimensions.css.cell.height; return { width: t.dimensions.css.cell.width, - height: t.dimensions.css.cell.height, + height: cellH, + charTop: (t.dimensions?.device?.char?.top ?? 0) / dpr, + charHeight: (t.dimensions?.device?.char?.height ?? (cellH * dpr)) / dpr, }; } @@ -23,9 +31,12 @@ export function getCellDimensions(terminal: XtermTerminal): CellDimensions | nul try { const dims = t._core?._renderService?.dimensions; if (dims?.css?.cell) { + const cellH = dims.css.cell.height; return { width: dims.css.cell.width, - height: dims.css.cell.height, + height: cellH, + charTop: (dims.device?.char?.top ?? 0) / dpr, + charHeight: (dims.device?.char?.height ?? (cellH * dpr)) / dpr, }; } } catch { diff --git a/packages/xterm-zerolag-input/src/overlay-renderer.ts b/packages/xterm-zerolag-input/src/overlay-renderer.ts index 1b6b637b..6dffc852 100644 --- a/packages/xterm-zerolag-input/src/overlay-renderer.ts +++ b/packages/xterm-zerolag-input/src/overlay-renderer.ts @@ -8,9 +8,9 @@ import type { RenderParams, FontStyle } from './types.js'; * occurs with normal DOM text flow. */ export function renderOverlay(container: HTMLDivElement, params: RenderParams): void { - const { lines, startCol, totalCols, cellW, cellH, promptRow, font, showCursor, cursorColor } = params; + const { lines, startCol, totalCols, cellW, cellH, charTop, charHeight, promptRow, font, showCursor, cursorColor } = params; - // Position container at prompt row + // Position container at prompt row. container.style.left = '0px'; container.style.top = (promptRow * cellH) + 'px'; @@ -22,7 +22,7 @@ export function renderOverlay(container: HTMLDivElement, params: RenderParams): const leftPx = i === 0 ? startCol * cellW : 0; const widthPx = i === 0 ? (fullWidthPx - leftPx) : fullWidthPx; const topPx = i * cellH; - const lineEl = makeLine(lines[i], leftPx, topPx, widthPx, cellH, cellW, font); + const lineEl = makeLine(lines[i], leftPx, topPx, widthPx, cellH, cellW, charTop, charHeight, font); container.appendChild(lineEl); } @@ -60,6 +60,8 @@ function makeLine( widthPx: number, cellH: number, cellW: number, + charTop: number, + charHeight: number, font: FontStyle, ): HTMLDivElement { const el = document.createElement('div'); @@ -68,21 +70,28 @@ function makeLine( el.style.left = leftPx + 'px'; el.style.top = topPx + 'px'; el.style.width = widthPx + 'px'; + // Extend background 1px past cell boundary to cover the compositing + // seam between the overlay layer (z-index:7) and the canvas layer below. + // The extra 1px lands in the next row's charTop gap (empty area before + // text rendering starts), so no canvas content is obscured. el.style.height = (cellH + 1) + 'px'; - el.style.lineHeight = cellH + 'px'; + + // Spans fill the full cell height with matching lineHeight for natural + // CSS vertical centering. No transform — any sub-pixel overhang past + // the line div causes visible anti-aliasing artifacts at the boundary. + // The ≤0.5px difference from canvas ceil() rounding is imperceptible. for (let i = 0; i < text.length; i++) { const span = document.createElement('span'); - // Match xterm.js canvas text rendering: - // - antialiased smoothing (canvas uses grayscale, not LCD subpixel) - // - geometricPrecision for consistent glyph sizing - // - no ligatures (canvas renders each glyph independently) + // No ligatures — canvas renders each glyph independently. 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"; + "font-feature-settings:'liga' 0,'calt' 0"; span.style.left = (i * cellW) + 'px'; + span.style.top = '0px'; span.style.width = cellW + 'px'; + span.style.height = cellH + 'px'; + span.style.lineHeight = cellH + 'px'; span.style.fontFamily = font.fontFamily; span.style.fontSize = font.fontSize; span.style.fontWeight = font.fontWeight; diff --git a/packages/xterm-zerolag-input/src/types.ts b/packages/xterm-zerolag-input/src/types.ts index 294bd2d2..c023a6a9 100644 --- a/packages/xterm-zerolag-input/src/types.ts +++ b/packages/xterm-zerolag-input/src/types.ts @@ -137,6 +137,10 @@ export interface ZerolagInputState { export interface CellDimensions { width: number; height: number; + /** Vertical offset (px) from cell top to where characters render. */ + charTop: number; + /** Height of the character rendering area (px). */ + charHeight: number; } /** Parameters for the overlay renderer. */ @@ -146,6 +150,10 @@ export interface RenderParams { totalCols: number; cellW: number; cellH: number; + /** Vertical offset (px) from cell top to character rendering area. */ + charTop: number; + /** Height of the character rendering area (px). */ + charHeight: number; promptRow: number; font: FontStyle; showCursor: boolean; diff --git a/packages/xterm-zerolag-input/src/zerolag-input-addon.ts b/packages/xterm-zerolag-input/src/zerolag-input-addon.ts index 983a5e0c..66eae400 100644 --- a/packages/xterm-zerolag-input/src/zerolag-input-addon.ts +++ b/packages/xterm-zerolag-input/src/zerolag-input-addon.ts @@ -380,6 +380,20 @@ export class ZerolagInputAddon implements XtermAddon { this._bufferDetectDone = true; } + // ─── Prompt configuration ────────────────────────────────────────── + + /** + * Change the prompt detection strategy at runtime. + * Call this when switching between CLI modes (e.g., Claude Code vs OpenCode) + * that use different prompt characters. + */ + setPrompt(finder: PromptFinder): void { + this._options.prompt = finder; + this._lastPromptPos = null; + this._lastRenderKey = ''; + if (this._pendingText || this._flushedOffset > 0) this._render(); + } + // ─── Prompt utilities ───────────────────────────────────────────── /** @@ -531,7 +545,7 @@ export class ZerolagInputAddon implements XtermAddon { return; } - const { width: cellW, height: cellH } = dims; + const { width: cellW, height: cellH, charTop, charHeight } = dims; const totalCols = this._terminal.cols; const offset = this._getPromptOffset(); const startCol = activePrompt.col + offset; @@ -581,6 +595,8 @@ export class ZerolagInputAddon implements XtermAddon { totalCols, cellW, cellH, + charTop, + charHeight, promptRow: activePrompt.row, font: this._font, showCursor: this._options.showCursor, diff --git a/src/web/public/app.js b/src/web/public/app.js index f689207c..eaaac438 100644 --- a/src/web/public/app.js +++ b/src/web/public/app.js @@ -5587,11 +5587,6 @@ class CodemanApp { } } - formatTokens(tokens) { - if (tokens >= 1000000) return `${(tokens / 1000000).toFixed(1)}M`; - if (tokens >= 1000) return `${(tokens / 1000).toFixed(1)}k`; - return String(tokens || 0); - } formatDuration(ms) { if (!ms || ms < 0) return '0s'; @@ -8091,14 +8086,6 @@ class CodemanApp { cbContainer.onclick = () => this.resetCircuitBreaker(); } - async resetCircuitBreaker() { - if (!this.activeSessionId) return; - try { - await this._apiPost(`/api/sessions/${this.activeSessionId}/ralph-circuit-breaker/reset`, {}); - } catch (err) { - console.error('Failed to reset circuit breaker:', err); - } - } updateRalphStats(loop, completed, total) { // Time stat @@ -10703,7 +10690,7 @@ class CodemanApp { } } - killAllSessions() { + killAllMuxSessions() { const count = this.muxSessions?.length || 0; if (count === 0) { alert('No sessions to kill'); diff --git a/src/web/public/index.html b/src/web/public/index.html index 274e5722..1714c2c7 100644 --- a/src/web/public/index.html +++ b/src/web/public/index.html @@ -436,7 +436,7 @@