diff --git a/src/web/public/input-cjk.js b/src/web/public/input-cjk.js index 9390a039..db4f406c 100644 --- a/src/web/public/input-cjk.js +++ b/src/web/public/input-cjk.js @@ -162,6 +162,10 @@ const CjkInput = (() => { if (!_textarea.classList.contains('cjk-input-visible')) { window.cjkActive = false; } + // Reset composing state — some IMEs fire compositionstart without a + // matching compositionend, leaving _composing stuck true and blocking + // all subsequent input events. + _composing = false; }; _textarea.addEventListener('mousedown', _listeners.mousedown); _textarea.addEventListener('focus', _listeners.focus); @@ -214,8 +218,10 @@ const CjkInput = (() => { return; } - // Below: only when NOT composing (composing keystrokes belong to IME) - if (_composing) return; + // Below: only when NOT composing (composing keystrokes belong to IME). + // Also check isComposing/keyCode 229 — the first keydown of a CJK + // sequence arrives BEFORE compositionstart, so _composing is still false. + if (_composing || e.isComposing || e.keyCode === 229) return; // Backspace: forward to PTY when no real text in textarea if (e.key === 'Backspace' && _isEffectivelyEmpty()) { diff --git a/src/web/public/styles.css b/src/web/public/styles.css index cd0a3e78..18091558 100644 --- a/src/web/public/styles.css +++ b/src/web/public/styles.css @@ -996,6 +996,12 @@ body { display: none !important; } +/* Redraw terminal header button: hidden by default (opt-in via App Settings → + Display → "Header Displays"). Pure client-side toggle. */ +.btn-redraw-terminal--hidden { + display: none !important; +} + .btn-icon-header.btn-settings { width: 30px; height: 30px; @@ -2229,8 +2235,10 @@ body.solo-mode .btn-lifecycle-log { background: transparent !important; } -/* Touch devices: JS touch handler owns scroll/tap — prevent browser - from consuming touch events before touchmove preventDefault fires. */ +/* Touch devices: prevent browser from claiming the touch gesture before + our JS touchmove handler fires. Without this, the browser starts native + scrolling during the first few px of finger travel and ignores our + subsequent preventDefault() — breaking custom terminal scroll. */ .touch-device .terminal-container, .touch-device .terminal-container .xterm, .touch-device .terminal-container .xterm-viewport, diff --git a/src/web/public/terminal-ui.js b/src/web/public/terminal-ui.js index a26813ce..64a8a5a5 100644 --- a/src/web/public/terminal-ui.js +++ b/src/web/public/terminal-ui.js @@ -365,11 +365,14 @@ Object.assign(CodemanApp.prototype, { let pixelAccum = 0; let didScroll = false; // track whether touchmove fired (tap vs scroll) + let touchStartY = 0; + const TAP_THRESHOLD = 8; // px — ignore micro-drift to distinguish tap from scroll container.addEventListener( 'touchstart', (ev) => { if (ev.touches.length === 1) { touchLastY = ev.touches[0].clientY; + touchStartY = touchLastY; velocity = 0; pixelAccum = 0; isTouching = true; @@ -388,9 +391,15 @@ Object.assign(CodemanApp.prototype, { 'touchmove', (ev) => { if (ev.touches.length === 1 && isTouching) { - ev.preventDefault(); - didScroll = true; const touchY = ev.touches[0].clientY; + if (!didScroll && Math.abs(touchY - touchStartY) >= TAP_THRESHOLD) { + didScroll = true; + } + // Only preventDefault once it's a real scroll — preventing micro-drift + // touchmove kills click synthesis, which iOS needs to show the keyboard. + if (didScroll) { + ev.preventDefault(); + } const delta = touchLastY - touchY; // positive = scroll down pixelAccum += delta; velocity = delta * 1.2; @@ -410,19 +419,21 @@ Object.assign(CodemanApp.prototype, { container.addEventListener( 'touchend', - () => { + (ev) => { isTouching = false; if (!scrollFrame && Math.abs(velocity) > 0.3) { scrollFrame = requestAnimationFrame(scrollLoop); } - // Tap (no scroll): refocus xterm's hidden textarea so keyboard input - // routes back to the terminal. Without this, a tap on the terminal area - // consumes the touch event but xterm's textarea never regains focus. if (!didScroll && this.terminal) { const cjkInput = document.getElementById('cjkInput'); if (cjkInput?.classList.contains('cjk-input-visible')) { cjkInput.focus(); } else { + // Don't synthesize mousedown — the browser does it naturally + // because we don't preventDefault() for taps (< TAP_THRESHOLD). + // touch-action:none blocks native scroll but NOT click synthesis. + // The browser's mousedown carries detail:1 + correct coordinates, + // so xterm's SelectionService._handleSingleClick picks it up. this._syncMobileHelperTextareaToCursor(); this.terminal.focus(); }