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

<objective>
Integrate message composer into chat window, wire up send functionality, and test end-to-end message flows.

Purpose: Complete message composition feature by connecting composer to Redux state, integrating into window, and validating send/receive behavior with tests.
Output: Working message composition with integration tests covering send, receive, and error scenarios.
</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 9 Plan 1 summary
@.planning/phases/09-message-composition/09-01-SUMMARY.md

# Phase 7 summary (WebSocket integration)
@.planning/phases/07-chat-infrastructure/07-03-SUMMARY.md

# Phase 8 summaries (window structure)
@.planning/phases/08-chat-window-ui/08-01-SUMMARY.md
@.planning/phases/08-chat-window-ui/08-02-SUMMARY.md

# Components to integrate
@jam-ui/src/components/client/chat/JKChatComposer.js
@jam-ui/src/components/client/JKSessionChatWindow.js
@jam-ui/src/components/client/chat/JKChatMessageList.js

# Redux state management
@jam-ui/src/store/features/sessionChatSlice.js

# Test utilities and examples
@jam-ui/test/chat/chat-button.spec.ts
@jam-ui/test/chat/chat-window.spec.ts
@jam-ui/test/utils/test-helpers.ts

# Codebase conventions
@.planning/codebase/CONVENTIONS.md
@.planning/codebase/TESTING.md
@CLAUDE.md

**Key context from Plan 09-01:**
- JKChatComposer component created with validation and keyboard handling
- Component uses local state for inputText
- sendMessage thunk already wired to Send button
- Validation prevents empty/long/disconnected sends

**Key context from Phase 7:**
- sendMessage thunk: pending → optimistic message added, fulfilled → replace with server response, rejected → remove optimistic
- CHAT_MESSAGE WebSocket handler: adds messages via addMessageFromWebSocket
- Message deduplication: checks msg_id to prevent duplicates

**Key context from Phase 8:**
- JKSessionChatWindow uses WindowPortal (450×600px)
- JKChatMessageList displays messages with auto-scroll
- Window layout: Header → MessageList → (empty space for composer)

**Established test patterns:**
- Playwright integration tests with Redux store access
- window.__REDUX_STORE__ for testing (dev/test environments)
- loginToJamUI and createAndJoinSession test helpers
- Mock WebSocket for real-time testing

**Integration requirements:**
- Composer should be at bottom of chat window
- Messages should auto-scroll when new message sent
- Optimistic update should show message immediately
- Error handling should display in composer
</context>

<tasks>

<task type="auto">
  <name>Task 1: Integrate JKChatComposer into JKSessionChatWindow</name>
  <files>jam-ui/src/components/client/JKSessionChatWindow.js</files>
  <action>
Add composer to chat window below message list:

**Import composer:**
```javascript
import JKChatComposer from './chat/JKChatComposer';
```

**Update window layout:**
Current structure has Header → MessageList → (placeholder). Replace placeholder with composer.

Find the content area (likely after JKChatMessageList) and add:
```jsx
{/* Message List */}
<JKChatMessageList />

{/* Composer - always at bottom */}
<JKChatComposer />
```

**Layout considerations:**
- Composer should be fixed at bottom of window
- Message list should fill remaining space above composer
- Window dimensions: 450×600px total
- Suggested layout:
  - Header: ~50px
  - Message list: flex-grow (fills available space)
  - Composer: ~120px (auto-sized based on content)

**Update container styles if needed:**
```jsx
<div style={{
  display: 'flex',
  flexDirection: 'column',
  height: '100%',
  width: '100%'
}}>
  <JKChatHeader onClose={handleClose} />
  <div style={{ flex: 1, overflowY: 'auto' }}>
    <JKChatMessageList />
  </div>
  <JKChatComposer />
</div>
```

**Why this layout:**
- Header fixed at top
- Message list scrollable, fills middle space
- Composer fixed at bottom (standard chat pattern)
- Flex layout ensures proper space distribution
  </action>
  <verify>
JKChatComposer integrated into window. Composer visible at bottom of chat window. Layout correct (header/list/composer).
  </verify>
  <done>
Composer integrated into chat window. Window layout complete with all three sections.
  </done>
</task>

<task type="auto">
  <name>Task 2: Write integration tests for message sending flow</name>
  <files>jam-ui/test/chat/send-message.spec.ts</files>
  <action>
Create Playwright integration tests for send message behavior:

```typescript
import { test, expect } from '@playwright/test';
import { loginToJamUI, createAndJoinSession } from '../utils/test-helpers';

test.describe('Send Message', () => {
  test.beforeEach(async ({ page }) => {
    await loginToJamUI(page);
    await createAndJoinSession(page);

    // Open chat window
    const chatButton = page.locator('img[alt="Chat"]');
    await chatButton.click();
  });

  test('sends message when Send button clicked', async ({ page, context }) => {
    // Wait for popup
    const popupPromise = context.waitForEvent('page');
    const popup = await popupPromise;
    await popup.waitForLoadState('domcontentloaded');

    // Type message
    const textarea = popup.locator('textarea[placeholder*="Type a message"]');
    await textarea.fill('Hello from test!');

    // Click send button
    const sendButton = popup.locator('button:has-text("Send")');
    await sendButton.click();

    // Wait for message to appear in list
    await popup.waitForTimeout(500);
    const message = popup.locator('text=Hello from test!');
    await expect(message).toBeVisible();

    // Verify textarea cleared
    await expect(textarea).toHaveValue('');
  });

  test('sends message when Enter key pressed', async ({ page, context }) => {
    const popupPromise = context.waitForEvent('page');
    const popup = await popupPromise;
    await popup.waitForLoadState('domcontentloaded');

    const textarea = popup.locator('textarea[placeholder*="Type a message"]');
    await textarea.fill('Hello via Enter!');

    // Press Enter (not Shift+Enter)
    await textarea.press('Enter');

    // Message should appear
    await popup.waitForTimeout(500);
    const message = popup.locator('text=Hello via Enter!');
    await expect(message).toBeVisible();

    // Textarea cleared
    await expect(textarea).toHaveValue('');
  });

  test('inserts newline when Shift+Enter pressed', async ({ page, context }) => {
    const popupPromise = context.waitForEvent('page');
    const popup = await popupPromise;
    await popup.waitForLoadState('domcontentloaded');

    const textarea = popup.locator('textarea[placeholder*="Type a message"]');
    await textarea.fill('Line 1');

    // Press Shift+Enter
    await textarea.press('Shift+Enter');

    // Type second line
    await textarea.type('Line 2');

    // Verify multiline content
    const value = await textarea.inputValue();
    expect(value).toContain('\n');
    expect(value).toContain('Line 1');
    expect(value).toContain('Line 2');

    // Message NOT sent yet
    const messages = popup.locator('[data-testid="chat-message"]');
    expect(await messages.count()).toBe(0);
  });

  test('disables send button when message empty', async ({ page, context }) => {
    const popupPromise = context.waitForEvent('page');
    const popup = await popupPromise;
    await popup.waitForLoadState('domcontentloaded');

    const sendButton = popup.locator('button:has-text("Send")');

    // Initially disabled (empty)
    await expect(sendButton).toBeDisabled();

    // Type message - becomes enabled
    const textarea = popup.locator('textarea[placeholder*="Type a message"]');
    await textarea.fill('Test message');
    await expect(sendButton).toBeEnabled();

    // Clear - becomes disabled again
    await textarea.fill('');
    await expect(sendButton).toBeDisabled();
  });

  test('disables send button when message over 255 characters', async ({ page, context }) => {
    const popupPromise = context.waitForEvent('page');
    const popup = await popupPromise;
    await popup.waitForLoadState('domcontentloaded');

    const textarea = popup.locator('textarea[placeholder*="Type a message"]');
    const sendButton = popup.locator('button:has-text("Send")');

    // Type 256 characters
    const longMessage = 'a'.repeat(256);
    await textarea.fill(longMessage);

    // Send button disabled
    await expect(sendButton).toBeDisabled();

    // Character count shows red and over-limit
    const charCount = popup.locator('text=/256\\/255/');
    await expect(charCount).toBeVisible();

    // Validation message appears
    const validationMsg = popup.locator('text=/characters too long/i');
    await expect(validationMsg).toBeVisible();
  });

  test('shows error when send fails', async ({ page, context }) => {
    const popupPromise = context.waitForEvent('page');
    const popup = await popupPromise;
    await popup.waitForLoadState('domcontentloaded');

    // Simulate send failure by rejecting the API call
    await popup.evaluate(() => {
      const store = (window as any).__REDUX_STORE__;
      // Mock sendMessage to fail
      // Note: This requires setting up API mocking in test environment
    });

    const textarea = popup.locator('textarea[placeholder*="Type a message"]');
    await textarea.fill('Test failure');

    const sendButton = popup.locator('button:has-text("Send")');
    await sendButton.click();

    // Error message should appear
    await popup.waitForTimeout(500);
    const errorMsg = popup.locator('text=/Failed to send message/i');
    await expect(errorMsg).toBeVisible();
  });

  test('shows optimistic update immediately', async ({ page, context }) => {
    const popupPromise = context.waitForEvent('page');
    const popup = await popupPromise;
    await popup.waitForLoadState('domcontentloaded');

    const textarea = popup.locator('textarea[placeholder*="Type a message"]');
    await textarea.fill('Optimistic test');

    const sendButton = popup.locator('button:has-text("Send")');
    await sendButton.click();

    // Message should appear immediately (optimistic)
    // Don't wait for server - should show right away
    const message = popup.locator('text=Optimistic test');
    await expect(message).toBeVisible({ timeout: 1000 });
  });
});
```

**Test coverage:**
- Send via button click
- Send via Enter key
- Newline via Shift+Enter
- Disabled when empty
- Disabled when over 255 chars
- Error display on send failure
- Optimistic update behavior

**Testing notes:**
- Uses Playwright's keyboard simulation for Enter/Shift+Enter
- Verifies Redux state updates via optimistic pattern
- Tests validation UI feedback
- Checks character count display
  </action>
  <verify>
Integration tests created with 7+ test cases. Tests cover send flows, keyboard handling, validation, and error states.
  </verify>
  <done>
Integration tests complete for send message feature. All send scenarios covered.
  </done>
</task>

<task type="auto">
  <name>Task 3: Write integration tests for real-time message receive</name>
  <files>jam-ui/test/chat/receive-message.spec.ts</files>
  <action>
Create tests for receiving messages via WebSocket:

```typescript
import { test, expect } from '@playwright/test';
import { loginToJamUI, createAndJoinSession } from '../utils/test-helpers';

test.describe('Receive Message', () => {
  test.beforeEach(async ({ page }) => {
    await loginToJamUI(page);
    await createAndJoinSession(page);

    // Open chat window
    const chatButton = page.locator('img[alt="Chat"]');
    await chatButton.click();
  });

  test('displays message received via WebSocket', async ({ page, context }) => {
    const popupPromise = context.waitForEvent('page');
    const popup = await popupPromise;
    await popup.waitForLoadState('domcontentloaded');

    // Simulate receiving message via WebSocket (dispatch Redux action)
    await page.evaluate(() => {
      const store = (window as any).__REDUX_STORE__;

      // Simulate WebSocket message
      store.dispatch({
        type: 'sessionChat/addMessageFromWebSocket',
        payload: {
          msg_id: 'test-msg-123',
          user_id: 'other-user-id',
          user_name: 'Test User',
          message: 'Hello from WebSocket!',
          created_at: new Date().toISOString(),
          channel: 'test-session-id'
        }
      });
    });

    // Wait for message to appear in popup
    await popup.waitForTimeout(500);
    const message = popup.locator('text=Hello from WebSocket!');
    await expect(message).toBeVisible();

    // Verify sender name displayed
    const sender = popup.locator('text=Test User');
    await expect(sender).toBeVisible();
  });

  test('does not duplicate sent message when received via WebSocket', async ({ page, context }) => {
    const popupPromise = context.waitForEvent('page');
    const popup = await popupPromise;
    await popup.waitForLoadState('domcontentloaded');

    // Send message
    const textarea = popup.locator('textarea[placeholder*="Type a message"]');
    await textarea.fill('No duplicate test');

    const sendButton = popup.locator('button:has-text("Send")');
    await sendButton.click();

    // Wait for optimistic update
    await popup.waitForTimeout(300);

    // Simulate receiving same message via WebSocket (with same msg_id)
    await page.evaluate(() => {
      const store = (window as any).__REDUX_STORE__;
      const state = store.getState();

      // Get the message that was just sent (will have msg_id from server)
      const messages = state.sessionChat.messagesByChannel['test-session-id'] || [];
      const lastMessage = messages[messages.length - 1];

      // Try to add again via WebSocket (should be deduplicated)
      if (lastMessage) {
        store.dispatch({
          type: 'sessionChat/addMessageFromWebSocket',
          payload: lastMessage
        });
      }
    });

    await popup.waitForTimeout(300);

    // Should only have ONE instance of the message
    const messages = popup.locator('text=No duplicate test');
    expect(await messages.count()).toBe(1);
  });

  test('auto-scrolls to bottom when new message received', async ({ page, context }) => {
    const popupPromise = context.waitForEvent('page');
    const popup = await popupPromise;
    await popup.waitForLoadState('domcontentloaded');

    // Add multiple messages to enable scrolling
    for (let i = 1; i <= 10; i++) {
      await page.evaluate((num) => {
        const store = (window as any).__REDUX_STORE__;
        store.dispatch({
          type: 'sessionChat/addMessageFromWebSocket',
          payload: {
            msg_id: `msg-${num}`,
            user_id: 'user-id',
            user_name: 'Test User',
            message: `Message ${num}`,
            created_at: new Date().toISOString(),
            channel: 'test-session-id'
          }
        });
      }, i);

      await popup.waitForTimeout(100);
    }

    // Last message should be visible (auto-scrolled)
    const lastMessage = popup.locator('text=Message 10');
    await expect(lastMessage).toBeVisible();
  });

  test('increments unread badge when message received and window closed', async ({ page, context }) => {
    // Open then close chat window
    const chatButton = page.locator('img[alt="Chat"]');
    await chatButton.click();

    const popupPromise = context.waitForEvent('page');
    const popup = await popupPromise;
    await popup.waitForLoadState('domcontentloaded');

    // Close window
    const closeButton = popup.locator('button[aria-label="Close chat"]');
    await closeButton.click();
    await page.waitForTimeout(500);

    // Simulate receiving message while window closed
    await page.evaluate(() => {
      const store = (window as any).__REDUX_STORE__;
      store.dispatch({
        type: 'sessionChat/addMessageFromWebSocket',
        payload: {
          msg_id: 'msg-closed-window',
          user_id: 'other-user',
          user_name: 'Test User',
          message: 'Message while closed',
          created_at: new Date().toISOString(),
          channel: 'test-session-id'
        }
      });
    });

    await page.waitForTimeout(300);

    // Unread badge should show "1"
    const badge = page.locator('div:has-text("1")').filter({ hasText: /^1$/ });
    await expect(badge).toBeVisible();
  });
});
```

**Test coverage:**
- Receive message via WebSocket dispatch
- Message deduplication (sent message not duplicated on WebSocket receive)
- Auto-scroll on new message
- Unread count increment when window closed

**WebSocket simulation:**
Tests dispatch Redux actions directly to simulate WebSocket messages. This is valid because:
- Phase 7 tested WebSocket handler separately
- These tests focus on UI response to Redux state changes
- Full WebSocket mocking would be more complex without clear benefit
  </action>
  <verify>
Integration tests for receive flow created. Tests cover WebSocket message display, deduplication, auto-scroll, and unread tracking.
  </verify>
  <done>
Receive message tests complete. All real-time scenarios covered. Phase 9 complete.
  </done>
</task>

</tasks>

<verification>
Before declaring plan complete:
- [ ] JKChatComposer integrated into JKSessionChatWindow
- [ ] Composer visible at bottom of chat window
- [ ] Window layout correct (header/list/composer)
- [ ] Send message integration tests pass (7 tests)
- [ ] Receive message integration tests pass (4 tests)
- [ ] Message sending works end-to-end
- [ ] Real-time message receive works
- [ ] No console errors or warnings
- [ ] All tests executable (may need CI/test environment to run)
</verification>

<success_criteria>

- JKChatComposer integrated into chat window
- Window layout complete (header/list/composer)
- Integration tests written for send flow (7+ tests)
- Integration tests written for receive flow (4+ tests)
- Tests cover: send via button/Enter, keyboard handling, validation, errors, WebSocket receive, deduplication, auto-scroll
- Phase 9 complete - message composition and sending fully functional
- Ready for Phase 10 (Read/Unread Status Management)
</success_criteria>

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

# Phase 9 Plan 2: Send Message Integration & Real-Time Delivery Summary

**Integrated composer into chat window, completed send functionality, and validated with comprehensive integration tests (11 tests)**

## Accomplishments

- Integrated JKChatComposer into JKSessionChatWindow
- Window layout complete: Header → Message List → Composer
- Send message flow works end-to-end
- Real-time message receive via WebSocket
- Message deduplication prevents duplicates
- Optimistic updates show messages immediately
- Integration tests: 11 total (7 send, 4 receive)

## Files Created/Modified

- `jam-ui/src/components/client/JKSessionChatWindow.js` - Added composer (MODIFIED)
- `jam-ui/test/chat/send-message.spec.ts` - Send flow tests (NEW)
- `jam-ui/test/chat/receive-message.spec.ts` - Receive flow tests (NEW)

## Decisions Made

[Document any implementation decisions]

## Issues Encountered

[Problems and resolutions, or "None"]

## Phase 9 Complete!

**Message Composition & Sending fully functional:**
- ✅ Composer UI with validation (Plan 9.1)
- ✅ Send integration and real-time delivery (Plan 9.2)

Ready for Phase 10 (Read/Unread Status Management).

Next phase will add:
- Unread message tracking per channel
- Badge updates on new messages
- Mark-as-read on window open
- localStorage persistence for unread counts

---
*Phase: 09-message-composition*
*Completed: [date]*
</output>
