Skip to content
Draft
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
31 changes: 28 additions & 3 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,10 @@ html {
/* Fix 100vh bug on mobile - use fill-available as fallback */
height: 100%;
height: -webkit-fill-available;
/* Prevent horizontal scroll on mobile */
overflow-x: hidden;
/* Improve touch scrolling */
-webkit-overflow-scrolling: touch;
}

body {
Expand All @@ -160,6 +164,12 @@ body {
/* Fix 100vh bug on mobile */
min-height: 100%;
min-height: -webkit-fill-available;
/* Prevent horizontal scroll on mobile */
overflow-x: hidden;
/* Improve touch scrolling */
-webkit-overflow-scrolling: touch;
/* Prevent pull-to-refresh on mobile (can interfere with scrolling) */
overscroll-behavior-y: contain;
}

/* Smooth animations */
Expand Down Expand Up @@ -360,13 +370,28 @@ a:active {


/* ========================================
iOS PWA Scroll & Viewport Optimizations
Mobile Scroll & Viewport Optimizations
======================================== */

/* iOS PWA scroll improvements for conversation container */
/* Mobile scroll improvements for all scroll containers */
[data-scroll-container] {
/* Smooth momentum scrolling on iOS - enables natural bounce */
/* Smooth momentum scrolling on iOS/Android - enables natural bounce */
-webkit-overflow-scrolling: touch;
/* Prevent scroll chaining on mobile */
overscroll-behavior: contain;
/* Improve scroll performance */
will-change: scroll-position;
/* Ensure proper scrolling when keyboard is open */
scroll-behavior: smooth;
}

/* Optimize scrolling performance on mobile */
@media (max-width: 768px) {
[data-scroll-container] {
/* Use GPU acceleration for smoother scrolling */
transform: translateZ(0);
-webkit-transform: translateZ(0);
}
}


Expand Down
49 changes: 48 additions & 1 deletion src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,46 @@ import { CursorLoader } from '@/components/CursorLoader';
import { HomeActivityList } from '@/components/HomeActivityList';
import { RepoPicker } from '@/components/RepoPicker';
import { theme } from '@/lib/theme';

// Hook to detect mobile keyboard state for layout adjustments
function useMobileKeyboardState() {
const [keyboardHeight, setKeyboardHeight] = useState(0);
const [isOpen, setIsOpen] = useState(false);

useEffect(() => {
// Detect mobile devices
const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent) ||
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1) ||
window.innerWidth < 768;

if (!isMobile) return;

const vv = window.visualViewport;
if (!vv) return;

const handleViewportChange = () => {
const currentHeight = vv.height;
const windowHeight = window.innerHeight;
const heightDiff = windowHeight - currentHeight;

// Keyboard is open if viewport shrunk significantly
if (heightDiff > 100) {
setKeyboardHeight(heightDiff);
setIsOpen(true);
} else {
setKeyboardHeight(0);
setIsOpen(false);
}
};

vv.addEventListener('resize', handleViewportChange);
return () => {
vv.removeEventListener('resize', handleViewportChange);
};
}, []);

return { keyboardHeight, isOpen };
}
import {
validateApiKey,
listRepositories,
Expand Down Expand Up @@ -127,6 +167,9 @@ export default function Home() {
// Track whether composer has input (for hiding empty state)
const [hasComposerInput, setHasComposerInput] = useState(false);

// Track mobile keyboard state for layout adjustments
const { keyboardHeight: mobileKeyboardHeight, isOpen: isMobileKeyboardOpen } = useMobileKeyboardState();

// Trigger to restart polling in ConversationView (for follow-ups to finished agents)
const [refreshTrigger, setRefreshTrigger] = useState(0);

Expand Down Expand Up @@ -999,7 +1042,11 @@ export default function Home() {
paddingTop: isInChatView
? 'calc(env(safe-area-inset-top, 0px) + 3.5rem)'
: 'calc(env(safe-area-inset-top, 0px) + 7rem)',
paddingBottom: 'calc(env(safe-area-inset-bottom, 0px) + 5.5rem)',
// Adjust bottom padding when keyboard is open on mobile
// Reduce padding since composer moves up with keyboard
paddingBottom: isMobileKeyboardOpen && mobileKeyboardHeight > 100
? `calc(env(safe-area-inset-bottom, 0px) + ${Math.max(1.5, 5.5 - (mobileKeyboardHeight / 100))}rem)`
: 'calc(env(safe-area-inset-bottom, 0px) + 5.5rem)',
}}
>
{isInChatView ? (
Expand Down
130 changes: 103 additions & 27 deletions src/components/Composer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,45 +4,99 @@ import { useState, useRef, useEffect } from 'react';
import { CursorLoader } from './CursorLoader';
import { trackComposerSubmit } from '@/lib/analytics';

// Hook to handle iOS keyboard viewport issues
// Returns keyboard height so parent can adjust layout
function useIOSKeyboard(): number {
// Hook to handle mobile keyboard viewport issues (iOS, Android, etc.)
// Returns keyboard height and isOpen state so parent can adjust layout
function useMobileKeyboard(): { keyboardHeight: number; isOpen: boolean } {
const [keyboardHeight, setKeyboardHeight] = useState(0);
const [isOpen, setIsOpen] = useState(false);
const initialViewportHeight = useRef(0);
const lastHeight = useRef(0);

useEffect(() => {
// Check if we're on iOS/mobile Safari
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) ||
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
// Detect mobile devices (iOS, Android, etc.)
const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent) ||
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1) ||
window.innerWidth < 768; // Also treat small screens as mobile

if (!isIOS) return;
if (!isMobile) return;

const vv = window.visualViewport;
if (!vv) return;
if (!vv) {
// Fallback for browsers without visualViewport API
const handleResize = () => {
const currentHeight = window.innerHeight;
if (initialViewportHeight.current === 0) {
initialViewportHeight.current = currentHeight;
}

const heightDiff = initialViewportHeight.current - currentHeight;
// Keyboard is likely open if viewport shrunk significantly (>150px)
if (heightDiff > 150) {
setKeyboardHeight(heightDiff);
setIsOpen(true);
} else {
setKeyboardHeight(0);
setIsOpen(false);
// Reset initial height if keyboard closed (viewport returned to normal)
if (currentHeight > initialViewportHeight.current * 0.9) {
initialViewportHeight.current = currentHeight;
}
}
};

window.addEventListener('resize', handleResize);
initialViewportHeight.current = window.innerHeight;

return () => {
window.removeEventListener('resize', handleResize);
};
}

// Store initial viewport height (full screen without keyboard)
initialViewportHeight.current = window.innerHeight;
lastHeight.current = vv.height;

const handleViewportChange = () => {
// Calculate keyboard height from difference between layout and visual viewport
const currentKeyboardHeight = window.innerHeight - vv.height;
const currentHeight = vv.height;
const windowHeight = window.innerHeight;

// Only set if keyboard is actually visible (threshold to avoid false positives)
if (currentKeyboardHeight > 100) {
// Calculate keyboard height from difference between window and visual viewport
const currentKeyboardHeight = windowHeight - currentHeight;

// Detect keyboard state: significant height difference indicates keyboard is open
// Use a threshold to avoid false positives from small viewport changes
const threshold = 100;

if (currentKeyboardHeight > threshold) {
setKeyboardHeight(currentKeyboardHeight);
setIsOpen(true);
} else {
// Keyboard closed - reset if viewport returned close to initial height
if (currentHeight > initialViewportHeight.current * 0.9) {
initialViewportHeight.current = windowHeight;
}
setKeyboardHeight(0);
setIsOpen(false);
}

lastHeight.current = currentHeight;
};

// Listen to viewport changes
vv.addEventListener('resize', handleViewportChange);
vv.addEventListener('scroll', handleViewportChange);

// Also listen to window resize as fallback
window.addEventListener('resize', handleViewportChange);

return () => {
vv.removeEventListener('resize', handleViewportChange);
vv.removeEventListener('scroll', handleViewportChange);
window.removeEventListener('resize', handleViewportChange);
};
}, []);

return keyboardHeight;
return { keyboardHeight, isOpen };
}

// Default model to use
Expand All @@ -69,21 +123,37 @@ export function Composer({
const textareaRef = useRef<HTMLTextAreaElement>(null);
const lastSubmitRef = useRef<number>(0);

// Get iOS keyboard height to adjust composer position
const keyboardHeight = useIOSKeyboard();
// Get mobile keyboard state to adjust composer position and trigger scroll
const { keyboardHeight, isOpen: isKeyboardOpen } = useMobileKeyboard();

// Scroll conversation to bottom when keyboard opens
// Scroll conversation to bottom when keyboard opens (with delay for smooth animation)
useEffect(() => {
if (keyboardHeight > 0) {
const scrollContainer = document.querySelector('[data-scroll-container]');
if (scrollContainer) {
scrollContainer.scrollTo({
top: scrollContainer.scrollHeight,
behavior: 'smooth'
});
}
if (isKeyboardOpen && keyboardHeight > 0) {
// Use requestAnimationFrame to ensure DOM is updated before scrolling
const scrollToBottom = () => {
const scrollContainer = document.querySelector('[data-scroll-container]');
if (scrollContainer) {
// Use scrollIntoView on bottom anchor if available, otherwise scroll container
const bottomAnchor = scrollContainer.querySelector('[data-bottom-anchor]');
if (bottomAnchor) {
bottomAnchor.scrollIntoView({ behavior: 'smooth', block: 'end' });
} else {
scrollContainer.scrollTo({
top: scrollContainer.scrollHeight,
behavior: 'smooth'
});
}
}
};

// Small delay to allow keyboard animation to start
const timer = setTimeout(scrollToBottom, 100);
// Also try immediately in case keyboard opens instantly
requestAnimationFrame(scrollToBottom);

return () => clearTimeout(timer);
}
}, [keyboardHeight]);
}, [isKeyboardOpen, keyboardHeight]);

// Auto-resize textarea - grows with content
useEffect(() => {
Expand Down Expand Up @@ -146,12 +216,18 @@ export function Composer({
// Check if we have text content for styling changes
const hasContent = value.trim().length > 0;

// Calculate safe transform value (avoid accessing window during SSR)
const transformValue = typeof window !== 'undefined' && keyboardHeight > 100
? `translateY(-${Math.min(keyboardHeight, window.innerHeight * 0.5)}px)`
: undefined;

return (
<div
className="relative transition-transform duration-200 ease-out"
style={{
// Move composer up when iOS keyboard is visible
transform: keyboardHeight > 0 ? `translateY(-${keyboardHeight}px)` : undefined
// Move composer up when mobile keyboard is visible
// Only apply transform if keyboard is significantly open to avoid jitter
transform: transformValue,
}}
>
<div
Expand Down
Loading