diff --git a/web/src/client/components/modal-wrapper.ts b/web/src/client/components/modal-wrapper.ts index 3d6bde2f..6b9b6a3e 100644 --- a/web/src/client/components/modal-wrapper.ts +++ b/web/src/client/components/modal-wrapper.ts @@ -48,8 +48,8 @@ export class ModalWrapper extends LitElement { } } - // Focus management - if (changedProperties.has('visible') && this.visible) { + // Focus management - but not if we have a special attribute to prevent it + if (changedProperties.has('visible') && this.visible && !this.hasAttribute('no-autofocus')) { requestAnimationFrame(() => { const focusable = this.querySelector( 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' diff --git a/web/src/client/components/session-view.ts b/web/src/client/components/session-view.ts index 9ac8211d..10fca5c4 100644 --- a/web/src/client/components/session-view.ts +++ b/web/src/client/components/session-view.ts @@ -119,6 +119,9 @@ export class SessionView extends LitElement { setShowQuickKeys: (value: boolean) => this.directKeyboardManager.setShowQuickKeys(value), ensureHiddenInputVisible: () => this.directKeyboardManager.ensureHiddenInputVisible(), cleanup: () => this.directKeyboardManager.cleanup(), + getKeyboardMode: () => this.directKeyboardManager.getKeyboardMode(), + isRecentlyEnteredKeyboardMode: () => + this.directKeyboardManager.isRecentlyEnteredKeyboardMode(), }), setShowQuickKeys: (value: boolean) => { this.uiStateManager.setShowQuickKeys(value); @@ -778,9 +781,13 @@ export class SessionView extends LitElement { this.uiStateManager.clearCtrlSequence(); this.uiStateManager.setShowCtrlAlpha(false); - // Refocus the hidden input + // Refocus the hidden input and restart focus retention if (this.directKeyboardManager.shouldRefocusHiddenInput()) { - this.directKeyboardManager.refocusHiddenInput(); + // Use a small delay to ensure the modal is fully closed first + setTimeout(() => { + this.directKeyboardManager.refocusHiddenInput(); + this.directKeyboardManager.startFocusRetentionPublic(); + }, 50); } } @@ -792,9 +799,13 @@ export class SessionView extends LitElement { this.uiStateManager.setShowCtrlAlpha(false); this.uiStateManager.clearCtrlSequence(); - // Refocus the hidden input + // Refocus the hidden input and restart focus retention if (this.directKeyboardManager.shouldRefocusHiddenInput()) { - this.directKeyboardManager.refocusHiddenInput(); + // Use a small delay to ensure the modal is fully closed first + setTimeout(() => { + this.directKeyboardManager.refocusHiddenInput(); + this.directKeyboardManager.startFocusRetentionPublic(); + }, 50); } } @@ -889,6 +900,12 @@ export class SessionView extends LitElement { if ('scrollToBottom' in terminal) { terminal.scrollToBottom(); } + + // Also ensure the terminal content is scrolled within its container + const terminalArea = this.querySelector('.terminal-area'); + if (terminalArea) { + terminalArea.scrollTop = terminalArea.scrollHeight; + } }, 50); } } @@ -1019,10 +1036,23 @@ export class SessionView extends LitElement { contain: layout style paint; /* Isolate terminal updates */ } - /* Add padding to terminal when quick keys are visible */ + /* Make terminal content 50px larger to prevent clipping */ + .terminal-area vibe-terminal, + .terminal-area vibe-terminal-binary { + height: calc(100% + 50px) !important; + margin-bottom: -50px !important; + } + + /* Transform terminal up when quick keys are visible */ + .terminal-area[data-quickkeys-visible="true"] { + transform: translateY(-110px); + transition: transform 0.2s ease-out; + } + + /* Add padding to terminal content when keyboard is visible */ .terminal-area[data-quickkeys-visible="true"] vibe-terminal, .terminal-area[data-quickkeys-visible="true"] vibe-terminal-binary { - padding-bottom: 120px !important; + padding-bottom: 70px !important; box-sizing: border-box; } @@ -1123,7 +1153,7 @@ export class SessionView extends LitElement { // Add safe area padding for landscape mode on mobile to handle notch uiState.isMobile && uiState.isLandscape ? 'safe-area-left safe-area-right' : '' }" - data-quickkeys-visible="${uiState.showQuickKeys && uiState.keyboardHeight > 0}" + data-quickkeys-visible="${uiState.showQuickKeys}" > ${ this.loadingAnimationManager.isLoading() diff --git a/web/src/client/components/session-view/ctrl-alpha-overlay.ts b/web/src/client/components/session-view/ctrl-alpha-overlay.ts index ecb06813..6ad8ae1a 100644 --- a/web/src/client/components/session-view/ctrl-alpha-overlay.ts +++ b/web/src/client/components/session-view/ctrl-alpha-overlay.ts @@ -6,7 +6,6 @@ */ import { html, LitElement } from 'lit'; import { customElement, property } from 'lit/decorators.js'; -import '../modal-wrapper.js'; @customElement('ctrl-alpha-overlay') export class CtrlAlphaOverlay extends LitElement { @@ -28,24 +27,26 @@ export class CtrlAlphaOverlay extends LitElement { } render() { + console.log('[CtrlAlphaOverlay] render called, visible:', this.visible); if (!this.visible) return null; + // Render directly without modal-wrapper to debug the issue return html` - this.onCancel?.()} - .closeOnBackdrop=${true} - .closeOnEscape=${false} + +
{ + if (e.target === e.currentTarget) { + this.onCancel?.(); + } + }} > - -
- +
e.stopPropagation()} >
Ctrl + Key
@@ -142,7 +143,7 @@ export class CtrlAlphaOverlay extends LitElement { }
-
+ `; } } diff --git a/web/src/client/components/session-view/direct-keyboard-manager.ts b/web/src/client/components/session-view/direct-keyboard-manager.ts index 0e434a09..9bbd4a2d 100644 --- a/web/src/client/components/session-view/direct-keyboard-manager.ts +++ b/web/src/client/components/session-view/direct-keyboard-manager.ts @@ -30,6 +30,7 @@ import { Z_INDEX } from '../../utils/constants.js'; import { createLogger } from '../../utils/logger.js'; import type { InputManager } from './input-manager.js'; +import { ManagerEventEmitter } from './interfaces.js'; const logger = createLogger('direct-keyboard-manager'); @@ -47,7 +48,7 @@ export interface DirectKeyboardCallbacks { clearCtrlSequence(): void; } -export class DirectKeyboardManager { +export class DirectKeyboardManager extends ManagerEventEmitter { private hiddenInput: HTMLInputElement | null = null; private focusRetentionInterval: number | null = null; private inputManager: InputManager | null = null; @@ -67,16 +68,17 @@ export class DirectKeyboardManager { private instanceId: string; // biome-ignore lint/correctness/noUnusedPrivateClassMembers: Used for focus state management private hiddenInputFocused = false; - // biome-ignore lint/correctness/noUnusedPrivateClassMembers: Used for keyboard mode timing private keyboardModeTimestamp = 0; // biome-ignore lint/correctness/noUnusedPrivateClassMembers: Used for IME composition private compositionBuffer = ''; constructor(instanceId: string) { + super(); this.instanceId = instanceId; // Add global paste listener for environments where Clipboard API doesn't work this.setupGlobalPasteListener(); + this.ensureHiddenInputVisible(); } setInputManager(inputManager: InputManager): void { @@ -119,10 +121,10 @@ export class DirectKeyboardManager { focusHiddenInput(): void { logger.log('Entering keyboard mode'); + // Enter keyboard mode this.keyboardMode = true; this.keyboardModeTimestamp = Date.now(); - this.updateHiddenInputPosition(); // Add capture phase click handler to prevent any clicks from stealing focus if (!this.captureClickHandler) { @@ -183,6 +185,11 @@ export class DirectKeyboardManager { ensureHiddenInputVisible(): void { if (!this.hiddenInput) { this.createHiddenInput(); + } else { + // Make sure it's in the DOM + if (!this.hiddenInput.parentNode) { + document.body.appendChild(this.hiddenInput); + } } // Show quick keys immediately when entering keyboard mode @@ -224,6 +231,8 @@ export class DirectKeyboardManager { this.hiddenInput = document.createElement('input'); this.hiddenInput.type = 'text'; this.hiddenInput.style.position = 'absolute'; + + // Hidden input that receives keyboard focus this.hiddenInput.style.opacity = '0.01'; // iOS needs non-zero opacity this.hiddenInput.style.fontSize = '16px'; // Prevent zoom on iOS this.hiddenInput.style.border = 'none'; @@ -233,6 +242,7 @@ export class DirectKeyboardManager { this.hiddenInput.style.caretColor = 'transparent'; this.hiddenInput.style.cursor = 'default'; this.hiddenInput.style.pointerEvents = 'none'; // Start with pointer events disabled + this.hiddenInput.placeholder = ''; this.hiddenInput.style.webkitUserSelect = 'text'; // iOS specific this.hiddenInput.autocapitalize = 'none'; // More explicit than 'off' this.hiddenInput.autocomplete = 'off'; @@ -391,8 +401,9 @@ export class DirectKeyboardManager { if (this.keyboardMode) { logger.log('In keyboard mode - maintaining focus'); - // Immediately try to refocus + // Add a small delay to allow Done button to exit keyboard mode first setTimeout(() => { + // Re-check keyboard mode after delay - Done button might have exited it if ( this.keyboardMode && this.hiddenInput && @@ -401,7 +412,7 @@ export class DirectKeyboardManager { logger.log('Refocusing hidden input to maintain keyboard'); this.hiddenInput.focus(); } - }, 0); + }, 50); // 50ms delay to allow Done button processing // Don't exit keyboard mode or hide quick keys return; @@ -437,11 +448,8 @@ export class DirectKeyboardManager { } }); - // Add to the terminal container - const terminalContainer = this.sessionViewElement?.querySelector('#terminal-container'); - if (terminalContainer) { - terminalContainer.appendChild(this.hiddenInput); - } + // Add to the body for debugging (so it's always visible) + document.body.appendChild(this.hiddenInput); } handleQuickKeyPress = async ( @@ -458,6 +466,7 @@ export class DirectKeyboardManager { if (isSpecial && key === 'Done') { // Dismiss the keyboard logger.log('Done button pressed - dismissing keyboard'); + // Set a flag to prevent refocus attempts this.dismissKeyboard(); return; } else if (isModifier && key === 'Control') { @@ -466,22 +475,17 @@ export class DirectKeyboardManager { return; } else if (key === 'CtrlFull') { // Toggle the full Ctrl+Alpha overlay + console.log('[DirectKeyboardManager] CtrlFull pressed, toggling Ctrl+Alpha overlay'); if (this.callbacks) { this.callbacks.toggleCtrlAlpha(); } const showCtrlAlpha = this.callbacks?.getShowCtrlAlpha() ?? false; + console.log('[DirectKeyboardManager] showCtrlAlpha after toggle:', showCtrlAlpha); if (showCtrlAlpha) { - // Stop focus retention when showing Ctrl overlay - if (this.focusRetentionInterval) { - clearInterval(this.focusRetentionInterval); - this.focusRetentionInterval = null; - } - - // Blur the hidden input to prevent it from capturing input - if (this.hiddenInput) { - this.hiddenInput.blur(); - } + // Keep focus retention running - we want the keyboard to stay visible + // The Ctrl+Alpha overlay should show above the keyboard + // Don't stop focus retention or blur the input } else { // Clear the Ctrl sequence when closing if (this.callbacks) { @@ -581,6 +585,10 @@ export class DirectKeyboardManager { } else if (key === 'Delete') { // Send delete key this.inputManager.sendInput('delete'); + } else if (key === 'Done') { + // Safety check - Done should have been handled earlier + this.dismissKeyboard(); + return; } else if (key.startsWith('F')) { // Handle function keys F1-F12 const fNum = Number.parseInt(key.substring(1)); @@ -613,7 +621,13 @@ export class DirectKeyboardManager { } // Send the key to terminal - this.inputManager.sendInput(keyToSend.toLowerCase()); + // For single character keys, send as text + if (keyToSend.length === 1) { + this.inputManager.sendInputText(keyToSend); + } else { + // For special keys, send as input command + this.inputManager.sendInput(keyToSend.toLowerCase()); + } } // Always keep focus on hidden input after any key press (except Done) @@ -689,14 +703,15 @@ export class DirectKeyboardManager { if (!this.hiddenInput) return; if (this.keyboardMode) { - // In keyboard mode: cover the terminal to receive input - this.hiddenInput.style.position = 'absolute'; - this.hiddenInput.style.top = '0'; - this.hiddenInput.style.left = '0'; - this.hiddenInput.style.width = '100%'; + // In keyboard mode: position at bottom center but invisible + this.hiddenInput.style.position = 'fixed'; + this.hiddenInput.style.bottom = '50px'; // Above quick keys + this.hiddenInput.style.left = '50%'; + this.hiddenInput.style.transform = 'translateX(-50%)'; + this.hiddenInput.style.width = '1px'; this.hiddenInput.style.height = '1px'; - this.hiddenInput.style.zIndex = String(Z_INDEX.TERMINAL_OVERLAY); - this.hiddenInput.style.pointerEvents = 'none'; + this.hiddenInput.style.zIndex = String(Z_INDEX.TERMINAL_OVERLAY + 100); + this.hiddenInput.style.pointerEvents = 'auto'; // Allow focus } else { // In scroll mode: position off-screen this.hiddenInput.style.position = 'fixed'; @@ -897,4 +912,61 @@ export class DirectKeyboardManager { this.hiddenInput = null; } } + + getKeyboardMode(): boolean { + return this.keyboardMode; + } + + isRecentlyEnteredKeyboardMode(): boolean { + // Check if we entered keyboard mode within the last 2 seconds + // This helps prevent iOS keyboard animation from being interrupted + if (!this.keyboardMode) return false; + + const timeSinceEntry = Date.now() - this.keyboardModeTimestamp; + return timeSinceEntry < 2000; // 2 seconds + } + + showVisibleInputForKeyboard(): void { + // Prevent multiple inputs + if (document.getElementById('vibe-visible-keyboard-input')) return; + + const input = document.createElement('input'); + input.type = 'text'; + input.id = 'vibe-visible-keyboard-input'; + input.placeholder = 'Type here...'; + input.style.position = 'fixed'; + input.style.bottom = '80px'; // Just above your "Show Keyboard" button + input.style.left = '50%'; + input.style.transform = 'translateX(-50%)'; + input.style.zIndex = '9999'; + input.style.fontSize = '18px'; + input.style.padding = '0.5em'; + input.style.background = '#fff'; + input.style.color = '#000'; + input.style.border = '1px solid #ccc'; + input.style.borderRadius = '6px'; + + document.body.appendChild(input); + + // Add a slight delay before focusing + setTimeout(() => { + input.focus(); + console.log('Input focused:', document.activeElement === input); + }, 50); + + // On blur or enter, remove input and send text + const cleanup = () => { + if (input.value && this.inputManager) { + this.inputManager.sendInputText(input.value); + } + input.remove(); + }; + + input.addEventListener('blur', cleanup); + input.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + cleanup(); + } + }); + } } diff --git a/web/src/client/components/session-view/interfaces.ts b/web/src/client/components/session-view/interfaces.ts index a183d540..d0a39848 100644 --- a/web/src/client/components/session-view/interfaces.ts +++ b/web/src/client/components/session-view/interfaces.ts @@ -68,6 +68,8 @@ export interface ManagerAccessCallbacks { setShowQuickKeys?(value: boolean): void; ensureHiddenInputVisible(): void; cleanup(): void; + getKeyboardMode(): boolean; + isRecentlyEnteredKeyboardMode(): boolean; }; getInputManager(): { isKeyboardShortcut(e: KeyboardEvent): boolean } | null; getTerminalLifecycleManager(): { diff --git a/web/src/client/components/session-view/lifecycle-event-manager.ts b/web/src/client/components/session-view/lifecycle-event-manager.ts index ad6c2cc2..46c5bf60 100644 --- a/web/src/client/components/session-view/lifecycle-event-manager.ts +++ b/web/src/client/components/session-view/lifecycle-event-manager.ts @@ -413,6 +413,18 @@ export class LifecycleEventManager extends ManagerEventEmitter { directKeyboardManager && directKeyboardManager.getShowQuickKeys() ) { + // Check if we recently entered keyboard mode (within last 2 seconds) + // This prevents iOS keyboard animation from being interrupted + const isRecentlyEntered = + directKeyboardManager.isRecentlyEnteredKeyboardMode?.() ?? false; + + if (isRecentlyEntered) { + logger.log( + 'Ignoring keyboard dismissal - recently entered keyboard mode, likely iOS animation' + ); + return; // Don't hide quick keys during iOS keyboard animation + } + // Force hide quick keys when keyboard dismisses this.callbacks.setShowQuickKeys(false); diff --git a/web/src/client/components/session-view/overlays-container.ts b/web/src/client/components/session-view/overlays-container.ts index 15193b5b..6c91c1a7 100644 --- a/web/src/client/components/session-view/overlays-container.ts +++ b/web/src/client/components/session-view/overlays-container.ts @@ -107,15 +107,30 @@ export class OverlaysContainer extends LitElement { > - + ${(() => { + const visible = this.uiState.isMobile && this.uiState.showCtrlAlpha; + console.log( + '[OverlaysContainer] Ctrl+Alpha visible:', + visible, + 'isMobile:', + this.uiState.isMobile, + 'showCtrlAlpha:', + this.uiState.showCtrlAlpha, + 'z-index should be above', + Z_INDEX.TERMINAL_QUICK_KEYS + ); + return html` + + `; + })()} ${ @@ -126,10 +141,6 @@ export class OverlaysContainer extends LitElement { @pointerdown=${(e: PointerEvent) => { e.preventDefault(); e.stopPropagation(); - }} - @click=${(e: MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); this.callbacks?.onKeyboardButtonClick(); }} title="Show keyboard" diff --git a/web/src/client/components/terminal-quick-keys.ts b/web/src/client/components/terminal-quick-keys.ts index 3c8b6e50..cfbd0073 100644 --- a/web/src/client/components/terminal-quick-keys.ts +++ b/web/src/client/components/terminal-quick-keys.ts @@ -1,5 +1,6 @@ import { html, LitElement, type PropertyValues } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; +import { Z_INDEX } from '../utils/constants.js'; // Terminal-specific quick keys for mobile use const TERMINAL_QUICK_KEYS = [ @@ -27,7 +28,6 @@ const TERMINAL_QUICK_KEYS = [ { key: '/', label: '/', row: 2 }, { key: '\\', label: '\\', row: 2 }, { key: '-', label: '-', row: 2 }, - { key: 'Done', label: 'Done', special: true, row: 2 }, // Third row - additional special characters { key: 'Option', label: '⌥', modifier: true, row: 3 }, { key: 'Command', label: '⌘', modifier: true, row: 3 }, @@ -63,6 +63,9 @@ const FUNCTION_KEYS = Array.from({ length: 12 }, (_, i) => ({ func: true, })); +// Done button - always visible +const DONE_BUTTON = { key: 'Done', label: 'Done', special: true }; + @customElement('terminal-quick-keys') export class TerminalQuickKeys extends LitElement { createRenderRoot() { @@ -110,8 +113,8 @@ export class TerminalQuickKeys extends LitElement { } private getButtonSizeClass(_label: string): string { - // Use flexible sizing without constraining width - return this.isLandscape ? 'px-1 py-1' : 'px-1.5 py-1.5'; + // Use minimal padding to fit more buttons + return this.isLandscape ? 'px-0.5 py-1' : 'px-1 py-1.5'; } private getButtonFontClass(label: string): string { @@ -201,6 +204,7 @@ export class TerminalQuickKeys extends LitElement { this.requestUpdate(); } + // Always pass the key press to the handler - let it decide what to do with special keys if (this.onKeyPress) { this.onKeyPress(key, isModifier, isSpecial, isToggle); } @@ -272,21 +276,35 @@ export class TerminalQuickKeys extends LitElement { left: 0; right: 0; bottom: 0; - z-index: 999999; - background-color: rgb(var(--color-bg-secondary)); - width: 100%; - /* Properly handle safe areas */ - padding-left: env(safe-area-inset-left); - padding-right: env(safe-area-inset-right); + z-index: ${Z_INDEX.TERMINAL_QUICK_KEYS}; + background-color: rgb(var(--color-bg-secondary) / 0.98); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + width: 100vw; + max-width: 100vw; + /* No safe areas needed when above keyboard */ + padding-left: 0; + padding-right: 0; + margin-left: 0; + margin-right: 0; + box-sizing: border-box; } /* The actual bar with buttons */ .quick-keys-bar { - background: rgb(var(--color-bg-secondary)); - border-top: 1px solid rgb(var(--color-border-base)); + background: transparent; + border-top: 1px solid rgb(var(--color-border-base) / 0.5); padding: 0.25rem 0; width: 100%; box-sizing: border-box; + overflow: hidden; + } + + /* Button rows - ensure full width */ + .quick-keys-bar > div { + width: 100%; + padding-left: 0.125rem; + padding-right: 0.125rem; } /* Quick key buttons */ @@ -295,6 +313,8 @@ export class TerminalQuickKeys extends LitElement { -webkit-tap-highlight-color: transparent; user-select: none; -webkit-user-select: none; + flex: 1 1 0; + min-width: 0; } /* Modifier key styling */ @@ -366,6 +386,25 @@ export class TerminalQuickKeys extends LitElement { -webkit-tap-highlight-color: transparent; user-select: none; -webkit-user-select: none; + flex: 1 1 0; + min-width: 0; + } + + /* Scrollable row styling */ + .scrollable-row { + overflow-x: auto; + -webkit-overflow-scrolling: touch; + scroll-behavior: smooth; + } + + /* Hide scrollbar but keep functionality */ + .scrollable-row::-webkit-scrollbar { + display: none; + } + + .scrollable-row { + -ms-overflow-style: none; + scrollbar-width: none; } /* Toggle button styling */ @@ -394,6 +433,8 @@ export class TerminalQuickKeys extends LitElement { -webkit-tap-highlight-color: transparent; user-select: none; -webkit-user-select: none; + flex: 1 1 0; + min-width: 0; } @@ -412,7 +453,7 @@ export class TerminalQuickKeys extends LitElement { >
-
+
${TERMINAL_QUICK_KEYS.filter((k) => k.row === 1).map( ({ key, label, modifier, arrow, toggle }) => html`
` : this.showFunctionKeys ? html` - -
+ +
${FUNCTION_KEYS.map( ({ key, label }) => html`
` : html` -
+
${TERMINAL_QUICK_KEYS.filter((k) => k.row === 2).map( ({ key, label, modifier, combo, special, toggle }) => html`
` } -
+
${TERMINAL_QUICK_KEYS.filter((k) => k.row === 3).map( ({ key, label, modifier, combo, special }) => html`