--- phase: 08-mobile-pwa plan: 02 type: execute wave: 1 depends_on: [] files_modified: - packages/portal/app/(dashboard)/chat/page.tsx - packages/portal/components/chat-window.tsx - packages/portal/components/chat-sidebar.tsx - packages/portal/components/mobile-chat-header.tsx - packages/portal/lib/use-visual-viewport.ts - packages/portal/lib/use-chat-socket.ts - packages/portal/messages/en.json - packages/portal/messages/es.json - packages/portal/messages/pt.json autonomous: true requirements: - MOB-03 - MOB-06 must_haves: truths: - "On mobile, tapping a conversation shows full-screen chat with back arrow header" - "Back arrow returns to conversation list on mobile" - "Desktop two-column chat layout is unchanged" - "Chat input stays visible when iOS virtual keyboard opens" - "Message input is fixed at bottom, does not scroll away" - "Streaming responses (word-by-word tokens) work on mobile" - "No hover-dependent interactions break on touch devices" artifacts: - path: "packages/portal/components/mobile-chat-header.tsx" provides: "Back arrow + agent name header for mobile full-screen chat" exports: ["MobileChatHeader"] - path: "packages/portal/lib/use-visual-viewport.ts" provides: "Visual Viewport API hook for iOS keyboard offset" exports: ["useVisualViewport"] key_links: - from: "packages/portal/app/(dashboard)/chat/page.tsx" to: "packages/portal/components/mobile-chat-header.tsx" via: "rendered when mobileShowChat is true on < md screens" pattern: "MobileChatHeader" - from: "packages/portal/components/chat-window.tsx" to: "packages/portal/lib/use-visual-viewport.ts" via: "keyboard offset applied to input container" pattern: "useVisualViewport" - from: "packages/portal/app/(dashboard)/chat/page.tsx" to: "mobileShowChat state" via: "toggles between conversation list and full-screen chat on mobile" pattern: "mobileShowChat" --- Mobile-optimized chat experience with WhatsApp-style full-screen conversation flow, iOS keyboard handling, and touch-safe interactions. Purpose: Chat is the primary user interaction on mobile. The two-column desktop layout doesn't work on small screens. This plan implements the conversation list -> full-screen chat pattern (like WhatsApp/iMessage) and handles the iOS virtual keyboard problem that breaks fixed inputs. Output: Full-screen mobile chat with back navigation, Visual Viewport keyboard handling, touch-safe interaction patterns. @/home/adelorenzo/.claude/get-shit-done/workflows/execute-plan.md @/home/adelorenzo/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/08-mobile-pwa/08-CONTEXT.md @.planning/phases/08-mobile-pwa/08-RESEARCH.md From packages/portal/app/(dashboard)/chat/page.tsx: ```typescript // ChatPageInner renders: // -
with // -
with // State: activeConversationId, showAgentPicker // handleSelectConversation sets activeConversationId + updates URL // Container:
``` From packages/portal/components/chat-window.tsx: ```typescript export interface ChatWindowProps { conversationId: string | null; authHeaders: ChatSocketAuthHeaders; } // ActiveConversation renders: // - Connection status banner // - Message list:
// - Input area:
// Container:
``` From packages/portal/components/chat-sidebar.tsx: ```typescript export interface ChatSidebarProps { conversations: Conversation[]; activeId: string | null; onSelect: (id: string) => void; onNewChat: () => void; } ``` From packages/portal/lib/use-chat-socket.ts: ```typescript export type ChatSocketAuthHeaders = { userId: string; role: string; tenantId: string | null; } ``` Task 1: Mobile full-screen chat toggle and Visual Viewport keyboard hook packages/portal/app/(dashboard)/chat/page.tsx, packages/portal/components/chat-window.tsx, packages/portal/components/chat-sidebar.tsx, packages/portal/components/mobile-chat-header.tsx, packages/portal/lib/use-visual-viewport.ts 1. Create `lib/use-visual-viewport.ts` — hook to handle iOS keyboard offset: ```typescript import { useState, useEffect } from 'react' export function useVisualViewport() { const [offset, setOffset] = useState(0) useEffect(() => { const vv = window.visualViewport if (!vv) return const handler = () => { const diff = window.innerHeight - vv.height - vv.offsetTop setOffset(Math.max(0, diff)) } vv.addEventListener('resize', handler) vv.addEventListener('scroll', handler) return () => { vv.removeEventListener('resize', handler); vv.removeEventListener('scroll', handler) } }, []) return offset } ``` 2. Create `components/mobile-chat-header.tsx`: - "use client" component - Props: `agentName: string, onBack: () => void` - Renders: `md:hidden` — only visible on mobile - Layout: flex row with ArrowLeft icon button (onBack), agent avatar circle (first letter of agentName), agent name text - Style: sticky top-0 z-10 bg-background border-b, h-14, items centered - Back arrow: large tap target (min 44x44px), uses lucide ArrowLeft 3. Update `app/(dashboard)/chat/page.tsx` — add mobile full-screen toggle: - Add state: `const [mobileShowChat, setMobileShowChat] = useState(false)` - Modify `handleSelectConversation` to also call `setMobileShowChat(true)` (always, not just on mobile — CSS handles visibility) - Update container: change `h-[calc(100vh-4rem)]` to `h-[calc(100dvh-4rem)] md:h-[calc(100vh-4rem)]` (dvh for mobile to handle iOS browser chrome) - Chat sidebar panel: wrap with conditional classes: ```tsx
``` - Chat window panel: wrap with conditional classes: ```tsx
{mobileShowChat && ( setMobileShowChat(false)} /> )}
``` - Extract agent name from conversations array for the active conversation: `const activeConversationAgentName = conversations.find(c => c.id === activeConversationId)?.agent_name ?? 'AI Employee'` - When URL has `?id=xxx` on mount and on mobile, set mobileShowChat to true: ```tsx useEffect(() => { if (urlConversationId) setMobileShowChat(true) }, [urlConversationId]) ``` - On mobile, the "New Chat" agent picker should also set mobileShowChat true after conversation creation (already handled by handleSelectConversation calling setMobileShowChat(true)) 4. Update `components/chat-window.tsx` — keyboard-safe input on mobile: - Import and use `useVisualViewport` in ActiveConversation - Apply keyboard offset to the input container: ```tsx const keyboardOffset = useVisualViewport() // On the input area div:
``` - When keyboardOffset > 0 (keyboard is open), auto-scroll to bottom of message list - Change the EmptyState container from `h-full` to responsive: works both when full-screen and when sharing space 5. Update `components/chat-sidebar.tsx` — touch-optimized tap targets: - Ensure conversation buttons have minimum 44px height (current py-3 likely sufficient, verify) - The "New Conversation" button should have at least 44x44 tap target on mobile - Replace any `hover:bg-accent` with `hover:bg-accent active:bg-accent` so touch devices get immediate feedback via the active pseudo-class (Tailwind v4 wraps hover in @media(hover:hover) already, but active provides touch feedback) 6. Add i18n key: `chat.backToConversations` in en/es/pt.json for the back button aria-label cd /home/adelorenzo/repos/konstruct/packages/portal && npm run build On mobile (< 768px): chat page shows conversation list full-width. Tapping a conversation shows full-screen chat with back arrow header. Back arrow returns to list. iOS keyboard pushes the input up instead of hiding it. Desktop two-column layout unchanged. Build passes. All chat functionality (send, streaming, typing indicator) works in both layouts. - `npm run build` passes in packages/portal - Chat page renders conversation list on mobile by default - Selecting a conversation shows full-screen chat with MobileChatHeader on mobile - Back button returns to conversation list - Desktop layout unchanged (two columns) - Chat input stays visible when keyboard opens (Visual Viewport hook active) Mobile chat follows the WhatsApp-style pattern: conversation list full-screen, then full-screen chat with back arrow. Input is keyboard-safe on iOS. Touch interactions have immediate feedback. Desktop layout is unmodified. After completion, create `.planning/phases/08-mobile-pwa/08-02-SUMMARY.md`