From eba2fa32a3ffb82360831ae57a3b25bf4b0e1ab5 Mon Sep 17 00:00:00 2001 From: arkon Date: Thu, 26 Feb 2026 10:53:26 +0100 Subject: [PATCH] feat: enable local echo overlay for OpenCode sessions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use a cursor-based prompt finder for OpenCode's Bubble Tea TUI: - Custom finder locates ┃ (U+2503) border on the cursor's row - Offset 3 skips "┃ " to reach the text input start position - Prompt finder is swapped dynamically when switching between Claude (❯ character) and OpenCode (┃ border) sessions - Added setPrompt() method to ZerolagInputAddon for runtime updates Co-Authored-By: Claude Opus 4.6 (1M context) --- src/web/public/app.js | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/src/web/public/app.js b/src/web/public/app.js index b999efdc..e9c25e01 100644 --- a/src/web/public/app.js +++ b/src/web/public/app.js @@ -2005,6 +2005,8 @@ class ZerolagInputAddon { resetBufferDetection() { this._bufferDetectDone = false; } undoDetection() { this._flushedOffset = 0; this._flushedText = ''; this._bufferDetectDone = false; } suppressBufferDetection() { this._bufferDetectDone = true; } + /** Update the prompt finder at runtime (e.g., when switching between Claude and OpenCode sessions) */ + setPrompt(finder) { this._options.prompt = finder; this._lastPromptPos = null; } findPrompt() { if (!this._terminal) return null; return _zl_findPrompt(this._terminal, this._options.prompt ?? _ZL_DEFAULT_PROMPT); @@ -3622,14 +3624,39 @@ class ClaudemanApp { const settings = this.loadAppSettingsFromStorage(); const session = this.activeSessionId ? this.sessions.get(this.activeSessionId) : null; const echoEnabled = settings.localEchoEnabled ?? MobileDetection.isTouchDevice(); - // Disable local echo for OpenCode sessions — Bubble Tea TUI has no '>' prompt - // for the overlay to anchor to, so keystrokes buffer invisibly - const isOpenCode = session?.mode === 'opencode'; - const shouldEnable = !!(echoEnabled && session && !isOpenCode); + const shouldEnable = !!(echoEnabled && session); if (this._localEchoEnabled && !shouldEnable) { this._localEchoOverlay?.clear(); } this._localEchoEnabled = shouldEnable; + + // Swap prompt finder based on session mode + if (this._localEchoOverlay && session) { + if (session.mode === 'opencode') { + // OpenCode (Bubble Tea TUI): find the ┃ border on the cursor's row. + // The input area is "┃ " — the ┃ is the anchor, offset 3 skips "┃ ". + // We use the cursor row (cursorY) to find the right line, then scan for ┃. + this._localEchoOverlay.setPrompt({ + type: 'custom', + offset: 3, + find: (terminal) => { + try { + const buf = terminal.buffer.active; + const row = buf.cursorY; + const line = buf.getLine(buf.viewportY + row); + if (!line) return null; + const text = line.translateToString(true); + const idx = text.indexOf('\u2503'); // ┃ (BOX DRAWINGS HEAVY VERTICAL) + if (idx >= 0) return { row, col: idx }; + return null; + } catch { return null; } + } + }); + } else { + // Claude Code: scan for ❯ prompt character + this._localEchoOverlay.setPrompt({ type: 'character', char: '\u276f', offset: 2 }); + } + } } /**