---
phase: 09-message-composition
plan: 01
type: standard
---

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

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

<context>
@.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)
</context>

<tasks>

<task type="auto">
  <name>Task 1: Create JKChatComposer component with textarea and send button</name>
  <files>jam-ui/src/components/client/chat/JKChatComposer.js</files>
  <action>
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 (
    <div style={{ padding: '12px', borderTop: '1px solid #ddd' }}>
      <textarea
        value={inputText}
        onChange={handleInputChange}
        placeholder="Type a message..."
        rows={3}
        style={{
          width: '100%',
          padding: '8px',
          border: '1px solid #ccc',
          borderRadius: '4px',
          resize: 'none',
          fontFamily: 'inherit',
          fontSize: '14px'
        }}
        disabled={!isConnected || isSending}
      />
      <div style={{
        display: 'flex',
        justifyContent: 'space-between',
        alignItems: 'center',
        marginTop: '8px'
      }}>
        <span style={{
          fontSize: '12px',
          color: charCount > 255 ? '#dc3545' : '#6c757d'
        }}>
          {charCount}/255
        </span>
        <button
          onClick={handleSend}
          disabled={!canSend}
          style={{
            padding: '6px 16px',
            backgroundColor: canSend ? '#007bff' : '#ccc',
            color: 'white',
            border: 'none',
            borderRadius: '4px',
            cursor: canSend ? 'pointer' : 'not-allowed',
            fontSize: '14px',
            fontWeight: '500'
          }}
        >
          {isSending ? 'Sending...' : 'Send'}
        </button>
      </div>
      {sendError && (
        <div style={{
          marginTop: '8px',
          padding: '8px',
          backgroundColor: '#f8d7da',
          color: '#721c24',
          borderRadius: '4px',
          fontSize: '12px'
        }}>
          Failed to send message. Please try again.
        </div>
      )}
      {!isConnected && (
        <div style={{
          marginTop: '8px',
          padding: '8px',
          backgroundColor: '#fff3cd',
          color: '#856404',
          borderRadius: '4px',
          fontSize: '12px'
        }}>
          Disconnected. Reconnecting...
        </div>
      )}
    </div>
  );
};

export default React.memo(JKChatComposer);
```

**Key features:**
- Local state for inputText (ephemeral, no need for Redux)
- Character count display with red color when > 255
- Disabled states: not connected, sending, invalid input
- Error display for send failures
- Disconnected warning
- React.memo for performance
- useCallback for handlers to prevent re-renders

**Validation logic:**
- Trim whitespace before checking length
- Must be 1-255 characters after trim
- Button disabled when invalid or disconnected or sending

**Why local state for inputText:**
Per Phase 6 design, input text is ephemeral UI state that doesn't need persistence across re-renders or sharing with other components.
  </action>
  <verify>
JKChatComposer component created. Component renders textarea, send button, character count. Validation logic prevents sending empty/long messages.
  </verify>
  <done>
Composer component exists with all UI elements and validation logic.
  </done>
</task>

<task type="auto">
  <name>Task 2: Add keyboard handling for Enter and Shift+Enter</name>
  <files>jam-ui/src/components/client/chat/JKChatComposer.js</files>
  <action>
Add keyboard event handler to textarea for Enter-to-send pattern:

**Add to component:**
```javascript
const handleKeyDown = useCallback((e) => {
  // Enter without Shift: send message
  if (e.key === 'Enter' && !e.shiftKey) {
    e.preventDefault(); // Prevent newline insertion
    handleSend();
  }
  // Shift+Enter: allow default (insert newline)
}, [handleSend]);
```

**Update textarea:**
```jsx
<textarea
  value={inputText}
  onChange={handleInputChange}
  onKeyDown={handleKeyDown}  // Add this
  placeholder="Type a message..."
  // ... rest of props
/>
```

**Behavior:**
- Enter key: Calls handleSend() if canSend is true, prevents default newline
- Shift+Enter: Allows default behavior (inserts newline)
- If send disabled (invalid/disconnected/sending): Enter does nothing

**Why preventDefault on Enter:**
Without preventDefault, pressing Enter would insert a newline AND send the message, leaving a blank line in the textarea.

**Testing consideration:**
This keyboard behavior should be tested in integration tests (Plan 09-02) with Playwright's keyboard simulation.
  </action>
  <verify>
Keyboard handling added. Press Enter sends message (if valid). Press Shift+Enter inserts newline. No PropTypes warnings.
  </verify>
  <done>
Enter key sends message, Shift+Enter creates new line. Keyboard UX complete.
  </done>
</task>

<task type="auto">
  <name>Task 3: Add character count validation feedback</name>
  <files>jam-ui/src/components/client/chat/JKChatComposer.js</files>
  <action>
Enhance character count display with visual feedback and validation messages:

**Update character count section:**
```javascript
// Calculate validation state
const charCount = inputText.length;
const isOverLimit = charCount > 255;
const isNearLimit = charCount > 230 && charCount <= 255; // 90% threshold

// Character count color
const countColor = isOverLimit ? '#dc3545' : isNearLimit ? '#ffc107' : '#6c757d';

// Validation message
const getValidationMessage = () => {
  if (isOverLimit) {
    return `Message is ${charCount - 255} characters too long`;
  }
  if (!isConnected) {
    return 'Waiting for connection...';
  }
  if (trimmedText.length === 0 && charCount > 0) {
    return 'Message cannot be empty';
  }
  return null;
};

const validationMessage = getValidationMessage();
```

**Update character count display:**
```jsx
<div style={{
  display: 'flex',
  justifyContent: 'space-between',
  alignItems: 'center',
  marginTop: '8px'
}}>
  <div style={{ display: 'flex', flexDirection: 'column', flex: 1 }}>
    <span style={{
      fontSize: '12px',
      color: countColor,
      fontWeight: isOverLimit || isNearLimit ? 'bold' : 'normal'
    }}>
      {charCount}/255
    </span>
    {validationMessage && (
      <span style={{
        fontSize: '11px',
        color: isOverLimit ? '#dc3545' : '#ffc107',
        marginTop: '2px'
      }}>
        {validationMessage}
      </span>
    )}
  </div>
  <button onClick={handleSend} disabled={!canSend}>
    {/* button content */}
  </button>
</div>
```

**Validation feedback levels:**
- Gray (default): 0-230 characters
- Yellow/warning: 231-255 characters (approaching limit)
- Red/error: 256+ characters (over limit)

**Validation messages:**
- Over limit: "Message is X characters too long"
- Empty after trim: "Message cannot be empty"
- Disconnected: "Waiting for connection..."

**Why near-limit warning at 230:**
Gives user advance notice before hitting hard limit, better UX than sudden red at 256.
  </action>
  <verify>
Character count shows color-coded feedback. Validation messages appear for over-limit, empty, and disconnected states. Visual feedback enhances UX.
  </verify>
  <done>
Character count with visual feedback complete. Validation messages guide user input.
  </done>
</task>

</tasks>

<verification>
Before declaring plan complete:
- [ ] JKChatComposer component renders with textarea and send button
- [ ] Character count displays correctly (X/255)
- [ ] Send button disabled when message invalid/disconnected/sending
- [ ] Enter key sends message, Shift+Enter creates newline
- [ ] Character count color changes based on length (gray/yellow/red)
- [ ] Validation messages appear for error states
- [ ] No console errors or warnings
- [ ] Component exports properly
</verification>

<success_criteria>

- JKChatComposer component created with full validation
- Keyboard handling works (Enter/Shift+Enter)
- Character count display with color-coded feedback
- Validation messages for all error states
- Disabled states work correctly (disconnected, sending, invalid)
- Component ready for integration in Plan 09-02
- No PropTypes warnings or console errors
</success_criteria>

<output>
After completion, create `.planning/phases/09-message-composition/09-01-SUMMARY.md`:

# Phase 9 Plan 1: Message Composer UI & Validation Summary

**Created message composer with textarea, keyboard handling (Enter/Shift+Enter), character validation (1-255), and visual feedback**

## Accomplishments

- JKChatComposer component with controlled textarea
- Enter to send, Shift+Enter for newline
- Character count display (X/255) with color-coded feedback
- Validation: empty check, 255-char limit, trim whitespace
- Disabled states: disconnected, sending, invalid input
- Error display for send failures
- Visual feedback for validation states

## Files Created/Modified

- `jam-ui/src/components/client/chat/JKChatComposer.js` - Composer component (NEW)

## Decisions Made

[Document any implementation decisions]

## Issues Encountered

[Problems and resolutions, or "None"]

## Next Step

Ready for 09-02-PLAN.md (Send Message Integration & Real-Time Delivery)
</output>
