Files
konstruct/.planning/phases/08-mobile-pwa/08-02-PLAN.md

10 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
08-mobile-pwa 02 execute 1
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
true
MOB-03
MOB-06
truths artifacts key_links
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
path provides exports
packages/portal/components/mobile-chat-header.tsx Back arrow + agent name header for mobile full-screen chat
MobileChatHeader
path provides exports
packages/portal/lib/use-visual-viewport.ts Visual Viewport API hook for iOS keyboard offset
useVisualViewport
from to via pattern
packages/portal/app/(dashboard)/chat/page.tsx packages/portal/components/mobile-chat-header.tsx rendered when mobileShowChat is true on < md screens MobileChatHeader
from to via pattern
packages/portal/components/chat-window.tsx packages/portal/lib/use-visual-viewport.ts keyboard offset applied to input container useVisualViewport
from to via pattern
packages/portal/app/(dashboard)/chat/page.tsx mobileShowChat state toggles between conversation list and full-screen chat on mobile 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.

<execution_context> @/home/adelorenzo/.claude/get-shit-done/workflows/execute-plan.md @/home/adelorenzo/.claude/get-shit-done/templates/summary.md </execution_context>

@.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:

// ChatPageInner renders:
// - <div className="w-72 shrink-0"> with <ChatSidebar ... />
// - <div className="flex-1"> with <ChatWindow ... />
// State: activeConversationId, showAgentPicker
// handleSelectConversation sets activeConversationId + updates URL
// Container: <div className="flex h-[calc(100vh-4rem)] overflow-hidden">

From packages/portal/components/chat-window.tsx:

export interface ChatWindowProps {
  conversationId: string | null;
  authHeaders: ChatSocketAuthHeaders;
}
// ActiveConversation renders:
// - Connection status banner
// - Message list: <div className="flex-1 overflow-y-auto px-4 py-4">
// - Input area: <div className="shrink-0 border-t px-4 py-3">
// Container: <div className="flex flex-col h-full">

From packages/portal/components/chat-sidebar.tsx:

export interface ChatSidebarProps {
  conversations: Conversation[];
  activeId: string | null;
  onSelect: (id: string) => void;
  onNewChat: () => void;
}

From packages/portal/lib/use-chat-socket.ts:

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
     <div className={cn(
       "md:w-72 md:shrink-0 md:block",
       mobileShowChat ? "hidden" : "flex flex-col w-full"
     )}>
     ```
   - Chat window panel: wrap with conditional classes:
     ```tsx
     <div className={cn(
       "flex-1 md:block",
       !mobileShowChat ? "hidden" : "flex flex-col w-full"
     )}>
       {mobileShowChat && (
         <MobileChatHeader
           agentName={activeConversationAgentName}
           onBack={() => setMobileShowChat(false)}
         />
       )}
       <ChatWindow ... />
     </div>
     ```
   - 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:
     <div className="shrink-0 border-t px-4 py-3"
          style={{ paddingBottom: `calc(${keyboardOffset}px + env(safe-area-inset-bottom, 0px))` }}>
     ```
   - 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)

<success_criteria> 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. </success_criteria>

After completion, create `.planning/phases/08-mobile-pwa/08-02-SUMMARY.md`