diff --git a/src/web/public/app.js b/src/web/public/app.js index b5a824e0..30960fbe 100644 --- a/src/web/public/app.js +++ b/src/web/public/app.js @@ -2206,7 +2206,6 @@ class CodemanApp { document.body.classList.toggle('cjk-input-visible', !!showCjk); cjkEl.style.display = showCjk ? 'block' : 'none'; cjkEl.setAttribute('aria-hidden', showCjk ? 'false' : 'true'); - if (showCjk && cjkEl.value === '\u200B') cjkEl.value = ''; if (!showCjk) window.cjkActive = false; if (typeof KeyboardHandler !== 'undefined') KeyboardHandler.updateLayoutForKeyboard(); } diff --git a/src/web/public/input-cjk.js b/src/web/public/input-cjk.js index 4a3b6a8d..72305c82 100644 --- a/src/web/public/input-cjk.js +++ b/src/web/public/input-cjk.js @@ -41,8 +41,6 @@ // eslint-disable-next-line no-unused-vars const CjkInput = (() => { let _textarea = null; - let _terminalContainer = null; - let _xtermTextarea = null; let _send = null; let _initialized = false; let _composing = false; @@ -77,23 +75,6 @@ const CjkInput = (() => { _textarea.setSelectionRange(1, 1); } - function _isMobileComposer() { - return !!( - _textarea && - typeof MobileDetection !== 'undefined' && - MobileDetection.isTouchDevice() && - _textarea.classList.contains('cjk-input-visible') - ); - } - - function _resetInput() { - if (_isMobileComposer()) { - _textarea.value = ''; - } else { - _resetToPhantom(); - } - } - /** Check if textarea contains only phantom(s) or is empty — no real user text */ function _isEffectivelyEmpty() { return !_strip(_textarea.value); @@ -116,44 +97,22 @@ const CjkInput = (() => { _composing = false; _textarea = document.getElementById('cjkInput'); if (!_textarea) return this; - _terminalContainer = document.getElementById('terminalContainer'); - // Seed the phantom character for the hidden/immediate CJK path. - _resetInput(); + _resetToPhantom(); _listeners.mousedown = (e) => { e.stopPropagation(); }; _listeners.focus = () => { window.cjkActive = true; - if (_isMobileComposer() && _textarea.value === PHANTOM) { - _textarea.value = ''; - return; - } - // Restore phantom if textarea was emptied while blurred - if (!_textarea.value && !_isMobileComposer()) _resetToPhantom(); + if (!_textarea.value) _resetToPhantom(); }; _listeners.blur = () => { window.cjkActive = false; }; _textarea.addEventListener('mousedown', _listeners.mousedown); _textarea.addEventListener('focus', _listeners.focus); _textarea.addEventListener('blur', _listeners.blur); - _listeners.xtermFocusRedirect = () => { - if (!_isMobileComposer()) return; - _textarea.focus(); - }; - if (_terminalContainer) { - _xtermTextarea = _terminalContainer.querySelector('.xterm-helper-textarea'); - if (_xtermTextarea) { - _xtermTextarea.addEventListener('focus', _listeners.xtermFocusRedirect, { capture: true }); - } - } - // ── Composition tracking ── _listeners.compositionstart = () => { _composing = true; - if (_isMobileComposer()) { - if (_textarea.value === PHANTOM) _textarea.value = ''; - return; - } // Clear phantom so IME sees a clean textarea — some IMEs include // existing text in the composition region which would corrupt input. if (_textarea.value === PHANTOM) { @@ -162,7 +121,6 @@ const CjkInput = (() => { }; _listeners.compositionend = () => { _composing = false; - if (_isMobileComposer()) return; // Defer flush: some Android IMEs haven't committed text to textarea // when compositionend fires. setTimeout(0) ensures we read the final value. setTimeout(_flush, 0); @@ -185,7 +143,7 @@ const CjkInput = (() => { } else { _send('\r'); } - _resetInput(); + _resetToPhantom(); return; } @@ -193,7 +151,7 @@ const CjkInput = (() => { if (e.key === 'Escape') { e.preventDefault(); _composing = false; - _resetInput(); + _resetToPhantom(); return; } @@ -207,19 +165,6 @@ const CjkInput = (() => { // Below: only when NOT composing (composing keystrokes belong to IME) if (_composing) return; - if (_isMobileComposer()) { - if (e.key === 'Backspace' && _isEffectivelyEmpty()) { - e.preventDefault(); - _send('\x7f'); - return; - } - if (PASSTHROUGH_KEYS[e.key] && _isEffectivelyEmpty()) { - e.preventDefault(); - _send(PASSTHROUGH_KEYS[e.key]); - } - return; - } - // Backspace: forward to PTY when no real text in textarea // (Desktop path — Android uses the input event + phantom approach) if (e.key === 'Backspace' && _isEffectivelyEmpty()) { @@ -251,13 +196,6 @@ const CjkInput = (() => { // making keydown unreliable. input fires AFTER character insertion and // carries inputType which tells us whether the text is final or tentative. _listeners.input = (e) => { - if (_isMobileComposer()) { - if (_textarea.value.includes(PHANTOM)) { - _textarea.value = _strip(_textarea.value); - } - return; - } - // ── Backspace / delete detection ── // Android long-press backspace generates rapid deleteContentBackward events. // The phantom character ensures the textarea is never truly empty, so each @@ -305,13 +243,8 @@ const CjkInput = (() => { if (handler) _textarea.removeEventListener(event, handler); } } - if (_xtermTextarea && _listeners.xtermFocusRedirect) { - _xtermTextarea.removeEventListener('focus', _listeners.xtermFocusRedirect, { capture: true }); - } window.cjkActive = false; _composing = false; - _terminalContainer = null; - _xtermTextarea = null; for (const key of Object.keys(_listeners)) delete _listeners[key]; _initialized = false; }, diff --git a/src/web/public/keyboard-accessory.js b/src/web/public/keyboard-accessory.js index d638aef5..5ac900d6 100644 --- a/src/web/public/keyboard-accessory.js +++ b/src/web/public/keyboard-accessory.js @@ -300,7 +300,10 @@ const KeyboardAccessoryBar = { const sendText = () => { const text = textarea.value; close(); - if (text) app.sendInput(text); + if (text) { + app.sendInput(text); + setTimeout(() => app.sendInput('\r'), 80); + } }; // Filter to images, close the dialog, and hand off to the shared diff --git a/src/web/public/styles.css b/src/web/public/styles.css index 960f4974..7a397d95 100644 --- a/src/web/public/styles.css +++ b/src/web/public/styles.css @@ -9307,9 +9307,9 @@ kbd { bottom: calc(var(--safe-area-bottom) + 40px); z-index: 52; display: block; - min-height: 44px; - max-height: 96px; - padding: 12px 10px; + min-height: 34px; + max-height: 68px; + padding: 6px 10px; border: 1px solid rgba(80, 120, 190, 0.55); border-left: none; border-right: none; diff --git a/src/web/public/terminal-ui.js b/src/web/public/terminal-ui.js index f7b10bda..a26813ce 100644 --- a/src/web/public/terminal-ui.js +++ b/src/web/public/terminal-ui.js @@ -1577,71 +1577,11 @@ Object.assign(CodemanApp.prototype, { } }, + // CJK textarea already provides visual feedback — bypass local echo + // buffering so each composed word reaches the PTY immediately. _handleCjkInput(text) { if (!this.activeSessionId) return; - const sessionId = this.activeSessionId; - const session = this.sessions.get(sessionId); - const useLocalEcho = !!(this._localEchoEnabled && this._localEchoOverlay && session?.mode !== 'shell'); - if (!useLocalEcho) { - this._sendInputAsync(sessionId, text); - return; - } - - if (text === '\x7f') { - const source = this._localEchoOverlay.removeChar(); - if (source === 'flushed') { - // Sync app-level flushed Maps (per-session state for tab switching), - // mirroring the onData backspace path — otherwise switching tabs away - // and back restores a stale, too-long flushed overlay. - const { count, text: flushedText } = this._localEchoOverlay.getFlushed(); - if (this._flushedOffsets?.has(sessionId)) { - if (count === 0) { - this._flushedOffsets.delete(sessionId); - this._flushedTexts?.delete(sessionId); - } else { - this._flushedOffsets.set(sessionId, count); - this._flushedTexts?.set(sessionId, flushedText); - } - } - this._sendInputAsync(sessionId, text); - } - return; - } - - if (/[\r\n]+$/.test(text)) { - const committed = text.replace(/[\r\n]+$/g, ''); - if (committed) this._localEchoOverlay.appendText(committed); - const pending = this._localEchoOverlay.pendingText || ''; - this._localEchoOverlay.clear(); - this._localEchoOverlay.suppressBufferDetection(); - this._flushedOffsets?.delete(sessionId); - this._flushedTexts?.delete(sessionId); - if (pending) this._sendInputAsync(sessionId, pending); - setTimeout(() => this._sendInputAsync(sessionId, '\r'), pending ? 80 : 0); - return; - } - - // Multi-byte escape sequence (arrow/Home/End from a hardware keyboard on - // the composer) — forward to the PTY without touching overlay state, - // mirroring the onData path. Appending it to pending text would type raw - // ESC bytes into the prompt on the next Enter. - if (text.length > 1 && text.charCodeAt(0) === 27) { - this._sendInputAsync(sessionId, text); - return; - } - - if (text.length === 1 && text.charCodeAt(0) < 32) { - const pending = this._localEchoOverlay.pendingText || ''; - this._localEchoOverlay.clear(); - this._localEchoOverlay.suppressBufferDetection(); - this._flushedOffsets?.delete(sessionId); - this._flushedTexts?.delete(sessionId); - if (pending) this._sendInputAsync(sessionId, pending); - this._sendInputAsync(sessionId, text); - return; - } - - this._localEchoOverlay.appendText(text); + this._sendInputAsync(this.activeSessionId, text); }, /**