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 {
>