--- phase: 21-chat-foundation plan: 04 type: execute wave: 2 depends_on: ["21-00", "21-02"] files_modified: - ui/src/context/ChatPanelContext.tsx - ui/src/components/ChatPanel.tsx - ui/src/components/ChatInput.tsx - ui/src/components/ChatMessage.tsx - ui/src/main.tsx - ui/src/components/Layout.tsx autonomous: true requirements: [INPUT-01, INPUT-07, THEME-01] must_haves: truths: - "A chat icon button in the Layout toggles the chat drawer open/closed" - "Chat panel open state persists to localStorage under nexus:chat-panel-open" - "Opening chat panel closes the PropertiesPanel" - "Chat input auto-resizes as user types, up to max-height 160px" - "Enter sends message, Shift+Enter inserts newline, Escape clears input" - "Chat panel uses theme CSS variables (bg-background, bg-card, border-border)" artifacts: - path: "ui/src/context/ChatPanelContext.tsx" provides: "ChatPanelProvider and useChatPanel hook" exports: ["ChatPanelProvider", "useChatPanel"] - path: "ui/src/components/ChatPanel.tsx" provides: "Right-side chat drawer shell" exports: ["ChatPanel"] - path: "ui/src/components/ChatInput.tsx" provides: "Auto-resize textarea with keyboard shortcuts" exports: ["ChatInput"] - path: "ui/src/components/ChatMessage.tsx" provides: "Message wrapper for user vs assistant alignment" exports: ["ChatMessage"] key_links: - from: "ui/src/components/Layout.tsx" to: "ui/src/components/ChatPanel.tsx" via: "ChatPanel rendered as sibling before PropertiesPanel" pattern: " Create the chat panel shell, context provider, input component, and Layout integration. Purpose: Wire the chat UI skeleton into the app -- a toggle button in the Layout, a right-side drawer with open/close animation, and an auto-resizing input with keyboard shortcuts. This gives users a visible, interactive chat panel. Output: Functional chat drawer that opens/closes, with working text input. @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/phases/21-chat-foundation/21-RESEARCH.md @.planning/phases/21-chat-foundation/21-UI-SPEC.md @.planning/phases/21-chat-foundation/21-02-SUMMARY.md From ui/src/context/PanelContext.tsx: ```typescript const STORAGE_KEY = "paperclip:panel-visible"; interface PanelContextValue { panelContent: ReactNode | null; panelVisible: boolean; openPanel: (content: ReactNode) => void; closePanel: () => void; setPanelVisible: (visible: boolean) => void; togglePanelVisible: () => void; } export function PanelProvider({ children }: { children: ReactNode }): JSX.Element; export function usePanel(): PanelContextValue; ``` From ui/src/components/Layout.tsx (line 416-435): ```tsx
``` From ui/src/main.tsx (line 50-51): ```tsx ``` From ui/src/components/ChatMarkdownMessage.tsx (created in Plan 02): ```typescript export function ChatMarkdownMessage({ content, className }: { content: string; className?: string }): JSX.Element; ```
Task 1: Create ChatPanelContext and ChatInput components ui/src/context/ChatPanelContext.tsx, ui/src/components/ChatInput.tsx, ui/src/components/ChatMessage.tsx, ui/src/components/ChatInput.test.tsx - ChatInput calls onSend when Enter is pressed without Shift - ChatInput inserts newline when Shift+Enter is pressed - ChatInput clears content when Escape is pressed - ChatInput does not call onSend when input is empty - ChatInput disables send button when isSubmitting is true - ui/src/context/PanelContext.tsx (mirror this pattern for localStorage persistence) - ui/src/context/ThemeContext.tsx (context + hook export pattern) - ui/src/components/MarkdownBody.tsx (understand existing markdown rendering approach) - ui/src/components/ChatInput.test.tsx (test stub from Plan 00 -- fill in test implementations) First, update `ui/src/components/ChatInput.test.tsx` to replace `.todo` stubs with real test implementations. Use `@testing-library/react` (if available) or `renderToStaticMarkup` to verify: - Enter key triggers onSend callback with the textarea content - Shift+Enter does NOT trigger onSend - Escape clears the textarea value - Empty textarea does not trigger onSend on Enter - Send button has disabled state when isSubmitting=true Then create the components: **ChatPanelContext.tsx:** Create `ui/src/context/ChatPanelContext.tsx` mirroring the PanelContext pattern: ```typescript import { createContext, useCallback, useContext, useState, type ReactNode } from "react"; const STORAGE_KEY = "nexus:chat-panel-open"; interface ChatPanelContextValue { chatOpen: boolean; activeConversationId: string | null; setChatOpen: (open: boolean) => void; toggleChat: () => void; setActiveConversationId: (id: string | null) => void; } const ChatPanelContext = createContext(null); function readPreference(): boolean { try { const raw = localStorage.getItem(STORAGE_KEY); return raw === "true"; } catch { return false; } } function writePreference(open: boolean) { try { localStorage.setItem(STORAGE_KEY, String(open)); } catch { /* ignore */ } } export function ChatPanelProvider({ children }: { children: ReactNode }) { const [chatOpen, setChatOpenState] = useState(readPreference); const [activeConversationId, setActiveConversationId] = useState(null); const setChatOpen = useCallback((open: boolean) => { setChatOpenState(open); writePreference(open); }, []); const toggleChat = useCallback(() => { setChatOpenState((prev) => { const next = !prev; writePreference(next); return next; }); }, []); return ( {children} ); } export function useChatPanel() { const ctx = useContext(ChatPanelContext); if (!ctx) throw new Error("useChatPanel must be used within ChatPanelProvider"); return ctx; } ``` Key difference from PanelContext: default is `false` (chat starts closed), and uses `nexus:` prefix not `paperclip:`. **ChatInput.tsx:** Create `ui/src/components/ChatInput.tsx`: - A `
` wrapper with `onSubmit` that calls the provided `onSend(text)` callback - `