diff --git a/packages/ui/src/components/chat/SessionGoalButton.tsx b/packages/ui/src/components/chat/SessionGoalButton.tsx index 8450729f..f8e382df 100644 --- a/packages/ui/src/components/chat/SessionGoalButton.tsx +++ b/packages/ui/src/components/chat/SessionGoalButton.tsx @@ -76,6 +76,20 @@ export const SessionGoalButton: React.FC = React.memo(({ type="button" className={cn(footerIconButtonClass, colorClass)} onClick={handleClick} + // Same guard as PermissionAutoAcceptButton, but only for the ARM + // toggle: arming happens mid-typing (the next message IS the + // objective), so that tap must not dismiss the soft keyboard or + // trigger the Android keyboard-close relayout that moves the button + // before the click lands. Opening the manage sheet (goal exists) is a + // context switch — there the keyboard should close as usual. + onMouseDown={(event) => { + if (!goal) event.preventDefault(); + }} + onPointerDownCapture={(event) => { + if (!goal && event.pointerType === 'touch') { + event.preventDefault(); + } + }} aria-label={label} aria-pressed={isEngaged} {...(withTooltip ? {} : { title: label })} diff --git a/packages/ui/src/components/chat/SessionGoalDialog.tsx b/packages/ui/src/components/chat/SessionGoalDialog.tsx index 532aaeb5..5192db36 100644 --- a/packages/ui/src/components/chat/SessionGoalDialog.tsx +++ b/packages/ui/src/components/chat/SessionGoalDialog.tsx @@ -18,6 +18,8 @@ import { import { sessionGoalStatusColor, sessionGoalStatusLabelKey } from '@/lib/sessionGoalPresentation'; import { clearSessionGoal, setSessionGoal } from '@/lib/sessionGoalActions'; import { useI18n } from '@/lib/i18n'; +import { MobileOverlayPanel } from '@/components/ui/MobileOverlayPanel'; +import { useUIStore } from '@/stores/useUIStore'; interface SessionGoalDialogProps { open: boolean; @@ -31,6 +33,7 @@ interface SessionGoalDialogProps { // (pause/resume/complete/clear) once a goal exists. export function SessionGoalDialog({ open, onOpenChange, sessionId, directory }: SessionGoalDialogProps) { const { t } = useI18n(); + const isMobile = useUIStore((state) => state.isMobile); const { goal } = useSessionGoal(sessionId, directory); const objectiveContent = useGoalObjectiveContent(sessionId, goal); @@ -86,13 +89,9 @@ export function SessionGoalDialog({ open, onOpenChange, sessionId, directory }: true, ); - return ( - - - - {goal ? t('chat.goal.dialog.titleManage') : t('chat.goal.dialog.titleCreate')} - + const title = goal ? t('chat.goal.dialog.titleManage') : t('chat.goal.dialog.titleCreate'); + const body = (
{goal && (
@@ -193,6 +192,25 @@ export function SessionGoalDialog({ open, onOpenChange, sessionId, directory }:
+ ); + + // Mobile renders the shared bottom-sheet overlay instead of a centered + // dialog — same pattern as model controls and the session status panel. + if (isMobile) { + return ( + onOpenChange(false)}> + {body} + + ); + } + + return ( + + + + {title} + + {body} ); diff --git a/packages/ui/src/styles/mobile.css b/packages/ui/src/styles/mobile.css index 6a444ba8..ee885a43 100644 --- a/packages/ui/src/styles/mobile.css +++ b/packages/ui/src/styles/mobile.css @@ -615,6 +615,20 @@ transition: opacity 0.2s ease-out; } +/* Bottom-sheet overlays live inside a keyboard-inset surface whose bottom is + lifted by the keyboard height, but their max-height is expressed in 100dvh — + which does NOT shrink in the Capacitor app (native keyboard resize is off). + Without this cap a sheet with the keyboard raised (typing inside it) can + grow past the top safe area and slide under the notch/status bar. Subtract + both the keyboard inset and the top safe area from the available height. */ +:root.oc-capacitor-app .pwa-overlay-panel { + max-height: calc( + 100dvh + - var(--oc-keyboard-inset, 0px) + - max(0.75rem, env(safe-area-inset-top)) + ); +} + /* Full-screen scroll views (e.g. the connect/login screen) live outside the app shell, so shrink them by the keyboard height the same way the shell does. Capping the height (instead of min-height: 100dvh) is what makes overflow-y-auto actually