diff --git a/.planning/phases/09-message-composition/09-01-PLAN.md b/.planning/phases/09-message-composition/09-01-PLAN.md new file mode 100644 index 000000000..308a7379d --- /dev/null +++ b/.planning/phases/09-message-composition/09-01-PLAN.md @@ -0,0 +1,418 @@ +--- +phase: 09-message-composition +plan: 01 +type: standard +--- + + +Create message composer component with textarea, send button, validation, and keyboard handling. + +Purpose: Enable users to type and submit chat messages with proper validation and UX patterns (Enter to send, character limits). +Output: JKChatComposer component with validation, keyboard handling, and character count display. + + + +@./.claude/get-shit-done/workflows/execute-phase.md +@./.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md + +# Phase 6 design documents +@.planning/phases/06-session-chat-research-design/CHAT_COMPONENT_DESIGN.md +@.planning/phases/06-session-chat-research-design/IMPLEMENTATION_ROADMAP.md + +# Phase 8 summaries (window and message list) +@.planning/phases/08-chat-window-ui/08-01-SUMMARY.md +@.planning/phases/08-chat-window-ui/08-02-SUMMARY.md +@.planning/phases/08-chat-window-ui/08-03-SUMMARY.md + +# Redux state management (has sendMessage thunk) +@jam-ui/src/store/features/sessionChatSlice.js + +# Integration point +@jam-ui/src/components/client/JKSessionChatWindow.js + +# Existing chat components for reference +@jam-ui/src/components/client/chat/JKChatMessage.js + +# Codebase conventions +@.planning/codebase/CONVENTIONS.md +@.planning/codebase/CHAT_REACT_PATTERNS.md +@CLAUDE.md + +**Key context from Phase 6 design:** +- Composer component: Textarea for message input + Send button +- Keyboard handling: Enter to send, Shift+Enter for newline +- Validation: 1-255 characters, trim whitespace, non-empty after trim +- Character count: Display "X/255" below textarea +- Disable send when: empty message, over 255 chars, or disconnected +- Connection check: Use isConnected from JamServerContext (useJamServerContext hook) + +**Key context from Phase 7:** +- sendMessage thunk available: accepts { channel, sessionId, message } +- optimistic updates: message added immediately with pending flag +- Redux selectors: selectSendStatus, selectSendError + +**Key context from Phase 8:** +- JKSessionChatWindow exists with WindowPortal +- Message list already integrated +- Window dimensions: 450×600px +- Inline styles used for MVP (SCSS deferred) + +**Established patterns:** +- React.memo for performance optimization +- useCallback for event handlers +- Controlled components for form inputs +- Redux for state, local state for ephemeral UI (input text) + + + + + + Task 1: Create JKChatComposer component with textarea and send button + jam-ui/src/components/client/chat/JKChatComposer.js + +Create message composer component in chat subdirectory: + +**Component structure:** +```javascript +import React, { useState, useCallback, useMemo } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { sendMessage, selectSendStatus, selectSendError } from '../../../store/features/sessionChatSlice'; +import { selectSessionId } from '../../../store/features/activeSessionSlice'; +import { useJamServerContext } from '../../../context/JamServerContext'; + +const JKChatComposer = () => { + const dispatch = useDispatch(); + const [inputText, setInputText] = useState(''); + + const sessionId = useSelector(selectSessionId); + const sendStatus = useSelector(selectSendStatus); + const sendError = useSelector(selectSendError); + const { server } = useJamServerContext(); + const isConnected = server?.isConnected ?? false; + + // Character count and validation + const trimmedText = inputText.trim(); + const charCount = inputText.length; + const isValid = trimmedText.length > 0 && trimmedText.length <= 255; + const isSending = sendStatus === 'loading'; + const canSend = isValid && isConnected && !isSending; + + const handleInputChange = useCallback((e) => { + setInputText(e.target.value); + }, []); + + const handleSend = useCallback(async () => { + if (!canSend) return; + + // Send message + await dispatch(sendMessage({ + channel: sessionId, + sessionId, + message: trimmedText + })); + + // Clear input on success + setInputText(''); + }, [canSend, dispatch, sessionId, trimmedText]); + + return ( +
+