Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/ios/ios/App/App.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,7 @@
CODE_SIGN_ENTITLEMENTS = App/App.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = M96WTV3CKX;
INFOPLIST_FILE = App/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = (
Expand Down
19 changes: 19 additions & 0 deletions apps/web/src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -732,6 +732,25 @@
-webkit-text-size-adjust: 100%;
}

/* Prevent iOS input zoom - font-size < 16px causes auto-zoom on focus */
.capacitor-ios input,
.capacitor-ios textarea,
.capacitor-ios select,
.capacitor-ios [contenteditable] {
font-size: 16px !important;
}

/* Smooth keyboard padding transition for elements that adapt to keyboard */
.capacitor-ios .keyboard-animate {
transition: padding-bottom 0.25s ease-out;
}

/* Utility for elements that need keyboard-aware bottom positioning */
.capacitor-ios .keyboard-aware-bottom {
padding-bottom: var(--keyboard-height, 0px);
transition: padding-bottom 0.25s ease-out;
}

@media print {
/* Hide ALL UI chrome - comprehensive selectors */
header,
Expand Down
14 changes: 14 additions & 0 deletions apps/web/src/components/ai/chat/input/ChatInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { InputActions } from './InputActions';
import { InputFooter } from '@/components/ui/floating-input';
import { useAssistantSettingsStore } from '@/stores/useAssistantSettingsStore';
import { useSpeechRecognition } from '@/hooks/useSpeechRecognition';
import { useMobileKeyboard } from '@/hooks/useMobileKeyboard';

export interface ChatInputProps {
/** Current input value */
Expand Down Expand Up @@ -135,13 +136,26 @@ export const ChatInput = forwardRef<ChatInputRef, ChatInputProps>(
},
});

// Mobile keyboard management
const keyboard = useMobileKeyboard();
const prevStreamingRef = useRef(isStreaming);

// Dismiss keyboard when streaming starts
useEffect(() => {
if (isStreaming && !prevStreamingRef.current) {
keyboard.dismiss();
}
prevStreamingRef.current = isStreaming;
}, [isStreaming, keyboard]);

useImperativeHandle(ref, () => ({
focus: () => textareaRef.current?.focus(),
clear: () => textareaRef.current?.clear(),
}));

const handleSend = () => {
if (value.trim() && !disabled) {
keyboard.dismiss();
onSend();
}
};
Expand Down
11 changes: 10 additions & 1 deletion apps/web/src/components/ai/chat/input/ChatTextarea.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';

import React, { forwardRef, useImperativeHandle, useRef } from 'react';
import React, { forwardRef, useImperativeHandle, useRef, useState } from 'react';
import { useSuggestion } from '@/hooks/useSuggestion';
import { Textarea } from '@/components/ui/textarea';
import SuggestionPopup from '@/components/mentions/SuggestionPopup';
Expand Down Expand Up @@ -59,6 +59,8 @@ const ChatTextareaInner = forwardRef<ChatTextareaRef, ChatTextareaProps>(
) => {
const textareaRef = useRef<HTMLTextAreaElement>(null);
const context = useSuggestionContext();
// Track IME composition state to prevent accidental sends during predictive text
const [isComposing, setIsComposing] = useState(false);

const suggestion = useSuggestion({
inputRef: textareaRef as React.RefObject<HTMLTextAreaElement>,
Expand All @@ -81,7 +83,12 @@ const ChatTextareaInner = forwardRef<ChatTextareaRef, ChatTextareaProps>(
suggestion.handleKeyDown(e);

// Send on Enter (without Shift) when suggestions are closed
// Also check for IME composition to prevent sends during predictive text selection
if (!context.isOpen && e.key === 'Enter' && !e.shiftKey) {
// Don't send during IME composition (predictive text, etc.)
if (isComposing || e.nativeEvent.isComposing) {
return;
}
e.preventDefault();
if (value.trim() && !disabled) {
onSend();
Expand All @@ -96,6 +103,8 @@ const ChatTextareaInner = forwardRef<ChatTextareaRef, ChatTextareaProps>(
value={value}
onChange={(e) => suggestion.handleValueChange(e.target.value)}
onKeyDown={handleKeyDown}
onCompositionStart={() => setIsComposing(true)}
onCompositionEnd={() => setIsComposing(false)}
placeholder={placeholder}
disabled={disabled}
className={cn(
Expand Down
16 changes: 15 additions & 1 deletion apps/web/src/components/ai/shared/chat/ChatInputArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
* Used by both Agent engine and Global Assistant engine
*/

import React, { useRef, forwardRef, useImperativeHandle } from 'react';
import React, { useRef, forwardRef, useImperativeHandle, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import { Send, StopCircle } from 'lucide-react';
import AiInput from './AiInput';
import { ChatInputRef } from '@/components/messages/ChatInput';
import { getAIErrorMessage } from '@/lib/ai/shared/error-messages';
import { useMobileKeyboard } from '@/hooks/useMobileKeyboard';

interface ChatInputAreaProps {
/** Current input value */
Expand Down Expand Up @@ -76,6 +77,18 @@ export const ChatInputArea = forwardRef<ChatInputAreaRef, ChatInputAreaProps>(
) => {
const chatInputRef = useRef<ChatInputRef>(null);

// Mobile keyboard management
const keyboard = useMobileKeyboard();
const prevStreamingRef = useRef(isStreaming);

// Dismiss keyboard when streaming starts
useEffect(() => {
if (isStreaming && !prevStreamingRef.current) {
keyboard.dismiss();
}
prevStreamingRef.current = isStreaming;
}, [isStreaming, keyboard]);

// Expose methods to parent
useImperativeHandle(ref, () => ({
focus: () => chatInputRef.current?.focus?.(),
Expand All @@ -85,6 +98,7 @@ export const ChatInputArea = forwardRef<ChatInputAreaRef, ChatInputAreaProps>(
// Handle send
const handleSend = () => {
if (value.trim() && !disabled && !isLoading && !isReadOnly) {
keyboard.dismiss();
onSend();
}
};
Expand Down
13 changes: 12 additions & 1 deletion apps/web/src/components/layout/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import { DebugPanel } from "./DebugPanel";
import { useLayoutStore } from "@/stores/useLayoutStore";
import { useHasHydrated } from "@/hooks/useHasHydrated";
import { usePerformanceMonitor } from "@/hooks/usePerformanceMonitor";
import { useRouter } from "next/navigation";
import { useRouter, usePathname } from "next/navigation";
import { isCapacitorApp } from "@/lib/capacitor-bridge";
import { useCallback, useEffect, useState } from "react";
import {
Sheet,
Expand All @@ -31,6 +32,7 @@ interface LayoutProps {
function Layout({ children }: LayoutProps) {
const { isLoading, isAuthenticated } = useAuth();
const router = useRouter();
const pathname = usePathname();
const isSheetBreakpoint = useBreakpoint("(max-width: 1023px)");

// Use selective Zustand subscriptions to prevent re-renders when unrelated store values change
Expand Down Expand Up @@ -60,6 +62,15 @@ function Layout({ children }: LayoutProps) {
}
}, [isSheetBreakpoint]);

// Auto-close sheets on navigation (Capacitor only)
// This fixes the issue where tapping a sidebar item navigates but leaves the sheet open
useEffect(() => {
if (isCapacitorApp() && isSheetBreakpoint) {
setLeftSheetOpen(false);
setRightSheetOpen(false);
}
}, [pathname, isSheetBreakpoint]);

// Handle authentication redirect with Next.js router for faster navigation
useEffect(() => {
if (hasHydrated && !isLoading && !isAuthenticated) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,8 @@ export function PageTreeItem({
<Link
href={linkHref}
onPointerDown={(e) => e.stopPropagation()}
className="flex-1 min-w-0 ml-1.5 truncate text-sm font-medium text-gray-900 dark:text-gray-100 hover:underline cursor-pointer"
onTouchEnd={(e) => e.stopPropagation()}
className="flex-1 min-w-0 ml-1.5 truncate text-sm font-medium text-gray-900 dark:text-gray-100 hover:underline cursor-pointer touch-manipulation"
>
{item.title}
</Link>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { cn } from '@/lib/utils';
import { useSuggestion } from '@/hooks/useSuggestion';
import { useSuggestionContext } from '@/components/providers/SuggestionProvider';
import SuggestionPopup from '@/components/mentions/SuggestionPopup';
import { useMobileKeyboard } from '@/hooks/useMobileKeyboard';

interface FloatingCellEditorProps {
value: string;
Expand Down Expand Up @@ -32,6 +33,9 @@ export const FloatingCellEditor: React.FC<FloatingCellEditorProps> = ({
const inputRef = useRef<HTMLInputElement>(null);
const [mounted, setMounted] = useState(false);
const suggestionContext = useSuggestionContext();
const { height: keyboardHeight } = useMobileKeyboard();
// Track IME composition state to prevent accidental commits during predictive text
const [isComposing, setIsComposing] = useState(false);

// Sheet-specific trigger pattern: allows @ after formula operators and whitespace
// Allows: ( = + - * / , < > ! and whitespace characters, or at start of string
Expand Down Expand Up @@ -63,6 +67,10 @@ export const FloatingCellEditor: React.FC<FloatingCellEditorProps> = ({

switch (event.key) {
case 'Enter':
// Don't commit during IME composition (predictive text, etc.)
if (isComposing || event.nativeEvent.isComposing) {
return;
}
if (!event.shiftKey) {
event.preventDefault();
onCommit(value);
Expand All @@ -87,7 +95,7 @@ export const FloatingCellEditor: React.FC<FloatingCellEditorProps> = ({
break;
}
},
[value, onCommit, onCancel, suggestion, suggestionContext.isOpen]
[value, onCommit, onCancel, suggestion, suggestionContext.isOpen, isComposing]
);

const handleBlur = useCallback(
Expand Down Expand Up @@ -149,10 +157,26 @@ export const FloatingCellEditor: React.FC<FloatingCellEditorProps> = ({
const minWidth = isMobile ? 100 : 120;
const minHeight = isMobile ? 36 : cellRect.height;

// Calculate position, accounting for keyboard on iOS
const viewportHeight = typeof window !== 'undefined'
? (window.visualViewport?.height ?? window.innerHeight)
: 0;
const availableHeight = viewportHeight - keyboardHeight;
Comment on lines +160 to +164

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid double-subtracting iOS keyboard height

On iOS/WKWebView, visualViewport.height already reflects the visible area when the keyboard is open. Subtracting the CSS --keyboard-height again makes availableHeight too small, so the floating editor can be pushed far above the cell (or even off‑screen) when the keyboard appears. Consider using visualViewport.height directly when available and only subtracting the CSS height when you fall back to window.innerHeight.

Useful? React with 👍 / 👎.

const cellBottom = cellRect.top + Math.max(cellRect.height, minHeight);

// If cell would be hidden behind keyboard, move it up
let adjustedTop = cellRect.top;
if (keyboardHeight > 0 && cellBottom > availableHeight - 20) {
// Move editor to be visible above keyboard with some padding
adjustedTop = availableHeight - Math.max(cellRect.height, minHeight) - 20;
// Don't go above the viewport
adjustedTop = Math.max(20, adjustedTop);
}

const style: React.CSSProperties = {
position: 'fixed',
left: cellRect.left,
top: cellRect.top,
top: adjustedTop,
width: Math.max(cellRect.width, minWidth),
height: Math.max(cellRect.height, minHeight),
zIndex: 1000,
Expand All @@ -172,6 +196,8 @@ export const FloatingCellEditor: React.FC<FloatingCellEditorProps> = ({
onChange={(e) => suggestion.handleValueChange(e.target.value)}
onKeyDown={handleKeyDown}
onBlur={handleBlur}
onCompositionStart={() => setIsComposing(true)}
onCompositionEnd={() => setIsComposing(false)}
className={cn(
'h-full w-full rounded-none border-2 border-primary bg-background px-2 py-1.5 text-sm',
'sm:px-3 sm:py-2',
Expand Down
12 changes: 10 additions & 2 deletions apps/web/src/components/messages/ChatInput.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import React, { forwardRef, useImperativeHandle, useRef } from 'react';
import React, { forwardRef, useImperativeHandle, useRef, useState } from 'react';
import { useSuggestion } from '@/hooks/useSuggestion';
import { Textarea } from '@/components/ui/textarea';
import SuggestionPopup from '@/components/mentions/SuggestionPopup';
Expand Down Expand Up @@ -30,6 +30,8 @@ const ChatInputWithProvider = forwardRef<ChatInputRef, ChatInputProps>(({
}, ref) => {
const textareaRef = useRef<HTMLTextAreaElement>(null);
const context = useSuggestionContext();
// Track IME composition state to prevent accidental sends during predictive text
const [isComposing, setIsComposing] = useState(false);

const suggestion = useSuggestion({
inputRef: textareaRef as React.RefObject<HTMLTextAreaElement>,
Expand All @@ -53,8 +55,12 @@ const ChatInputWithProvider = forwardRef<ChatInputRef, ChatInputProps>(({

const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
suggestion.handleKeyDown(e);

if (!context.isOpen && e.key === 'Enter' && !e.shiftKey) {
// Don't send during IME composition (predictive text, etc.)
if (isComposing || e.nativeEvent.isComposing) {
return;
}
e.preventDefault();
if (value.trim()) {
onSendMessage();
Expand All @@ -69,6 +75,8 @@ const ChatInputWithProvider = forwardRef<ChatInputRef, ChatInputProps>(({
value={value}
onChange={(e) => suggestion.handleValueChange(e.target.value)}
onKeyDown={handleKeyDown}
onCompositionStart={() => setIsComposing(true)}
onCompositionEnd={() => setIsComposing(false)}
placeholder={placeholder}
className="min-h-[40px] max-h-[120px] w-full"
/>
Expand Down
3 changes: 3 additions & 0 deletions apps/web/src/components/ui/floating-input/InputPositioner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ export function InputPositioner({
)}
style={{
maxWidth: isCentered ? centeredMaxWidth : undefined,
// Add keyboard-aware padding when docked (uses CSS variable set by iOS Capacitor)
// Using paddingBottom instead of animating bottom to avoid Framer Motion conflicts
paddingBottom: !isCentered ? 'calc(1rem + var(--keyboard-height, 0px))' : undefined,
}}
initial={false}
animate={position}
Expand Down
Loading