---
phase: 07-chat-infrastructure
plan: 03
type: tdd
---

<objective>
Implement WebSocket message handling and memoized selectors using TDD methodology.

Purpose: Integrate chat with real-time WebSocket messaging and create efficient Redux selectors. All WebSocket routing and selector logic must be tested following CLAUDE.md TDD requirements.

Output: CHAT_MESSAGE WebSocket handler, 8 memoized selectors, and localStorage persistence with comprehensive tests.
</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_API.md
@.planning/phases/06-session-chat-research-design/CHAT_REDUX_DESIGN.md

# Phase 7 Plan 1 & 2 outputs
@.planning/phases/07-chat-infrastructure/07-01-SUMMARY.md
@.planning/phases/07-chat-infrastructure/07-02-SUMMARY.md

# Existing WebSocket patterns
@jam-ui/src/hooks/useSessionWebSocket.js
@jam-ui/src/store/features/mediaSlice.js

# Codebase conventions
@.planning/codebase/ARCHITECTURE.md
@CLAUDE.md

**Key context from Phase 6:**
- WebSocket message type: CHAT_MESSAGE (Protocol Buffer)
- Message format: { msg_id, user_id, user_name, message, channel, session_id, created_at }
- Unread increment: only if window closed AND channel not active
- localStorage persistence: lastReadAt timestamps for unread tracking

**Existing WebSocket patterns:**
- useSessionWebSocket.js handles message routing
- Message types: MIXER_CHANGES, JAM_TRACK_CHANGES, etc.
- Pattern: Switch on message type → dispatch Redux action

**Selector patterns from existing slices:**
- Use Reselect createSelector for memoization
- Chain selectors for derived data
- Input selectors → output selector pattern

**TDD requirements from CLAUDE.md:**
- Write test FIRST (RED phase)
- Mock JamServer for WebSocket tests
- Test all selector scenarios
- Implement minimum code to pass (GREEN phase)
- Refactor while keeping tests green
</context>

<tasks>

<task type="tdd">
  <name>Task 1: Add CHAT_MESSAGE WebSocket handler (TDD)</name>
  <files>jam-ui/src/hooks/useSessionWebSocket.js, jam-ui/src/hooks/__tests__/useSessionWebSocket.test.js</files>
  <action>
Using TDD methodology, add CHAT_MESSAGE handler to WebSocket hook:

**RED Phase - Write failing tests first:**

1. Create or extend test file `jam-ui/src/hooks/__tests__/useSessionWebSocket.test.js`
2. Write tests for CHAT_MESSAGE routing:
   ```javascript
   import { renderHook } from '@testing-library/react-hooks';
   import { useSessionWebSocket } from '../useSessionWebSocket';
   import { addMessageFromWebSocket, incrementUnreadCount } from '../../store/features/sessionChatSlice';

   describe('useSessionWebSocket - CHAT_MESSAGE', () => {
     let mockDispatch;
     let mockJamServer;

     beforeEach(() => {
       mockDispatch = jest.fn();
       mockJamServer = {
         on: jest.fn(),
         off: jest.fn()
       };
     });

     test('dispatches addMessageFromWebSocket on CHAT_MESSAGE', () => {
       renderHook(() => useSessionWebSocket(mockJamServer, mockDispatch));

       // Simulate CHAT_MESSAGE from server
       const messageHandler = mockJamServer.on.mock.calls.find(
         call => call[0] === 'message'
       )[1];

       const chatMessage = {
         type: 'CHAT_MESSAGE',
         msg_id: 'msg-1',
         user_id: 'user-1',
         user_name: 'John Doe',
         message: 'Hello world',
         channel: 'session',
         session_id: 'session-abc',
         created_at: '2026-01-26T12:00:00Z'
       };

       messageHandler(chatMessage);

       expect(mockDispatch).toHaveBeenCalledWith(
         addMessageFromWebSocket({
           id: 'msg-1',
           senderId: 'user-1',
           senderName: 'John Doe',
           message: 'Hello world',
           channel: 'session',
           sessionId: 'session-abc',
           createdAt: '2026-01-26T12:00:00Z'
         })
       );
     });

     test('increments unread count if window closed', () => {
       // Mock Redux state where chat window is closed
       const mockGetState = jest.fn(() => ({
         sessionChat: {
           isWindowOpen: false,
           activeChannel: 'global'
         }
       }));

       renderHook(() => useSessionWebSocket(mockJamServer, mockDispatch, mockGetState));

       const messageHandler = mockJamServer.on.mock.calls.find(
         call => call[0] === 'message'
       )[1];

       const chatMessage = {
         type: 'CHAT_MESSAGE',
         channel: 'session',
         session_id: 'session-abc',
         // ... other fields
       };

       messageHandler(chatMessage);

       expect(mockDispatch).toHaveBeenCalledWith(addMessageFromWebSocket(expect.any(Object)));
       expect(mockDispatch).toHaveBeenCalledWith(incrementUnreadCount({ channel: 'session-abc' }));
     });

     test('does NOT increment unread if window open and active channel', () => {
       const mockGetState = jest.fn(() => ({
         sessionChat: {
           isWindowOpen: true,
           activeChannel: 'session-abc'
         }
       }));

       renderHook(() => useSessionWebSocket(mockJamServer, mockDispatch, mockGetState));

       const messageHandler = mockJamServer.on.mock.calls.find(
         call => call[0] === 'message'
       )[1];

       const chatMessage = {
         type: 'CHAT_MESSAGE',
         channel: 'session',
         session_id: 'session-abc',
         // ... other fields
       };

       messageHandler(chatMessage);

       expect(mockDispatch).toHaveBeenCalledWith(addMessageFromWebSocket(expect.any(Object)));
       expect(mockDispatch).not.toHaveBeenCalledWith(incrementUnreadCount(expect.any(Object)));
     });

     test('handles global channel messages', () => {
       // Test without session_id
     });

     test('handles lesson channel messages', () => {
       // Test with lesson_session_id
     });
   });
   ```

3. Run tests - should FAIL (handler doesn't exist yet)

**GREEN Phase - Implement WebSocket handler:**

4. Add CHAT_MESSAGE case to `useSessionWebSocket.js`:
   ```javascript
   import { addMessageFromWebSocket, incrementUnreadCount } from '../store/features/sessionChatSlice';

   export const useSessionWebSocket = (jamServer, dispatch, getState) => {
     useEffect(() => {
       if (!jamServer) return;

       const handleMessage = (message) => {
         switch (message.type) {
           case 'CHAT_MESSAGE': {
             // Transform Protocol Buffer format to Redux format
             const chatMessage = {
               id: message.msg_id,
               senderId: message.user_id,
               senderName: message.user_name,
               message: message.message,
               channel: message.channel,
               sessionId: message.session_id || null,
               lessonSessionId: message.lesson_session_id || null,
               createdAt: message.created_at,
               purpose: message.purpose || null,
               musicNotation: message.music_notation || null,
               claimedRecording: message.claimed_recording || null
             };

             // Add message to Redux
             dispatch(addMessageFromWebSocket(chatMessage));

             // Increment unread count if window closed or different channel active
             const state = getState();
             const { isWindowOpen, activeChannel } = state.sessionChat;
             const messageChannel = message.channel === 'session'
               ? `session-${message.session_id}`
               : message.channel === 'lesson'
               ? `lesson-${message.lesson_session_id}`
               : 'global';

             if (!isWindowOpen || activeChannel !== messageChannel) {
               dispatch(incrementUnreadCount({ channel: messageChannel }));
             }

             break;
           }

           // ... existing cases (MIXER_CHANGES, JAM_TRACK_CHANGES, etc.)
         }
       };

       jamServer.on('message', handleMessage);
       return () => jamServer.off('message', handleMessage);
     }, [jamServer, dispatch, getState]);
   };
   ```

5. Run tests - should PASS

**REFACTOR Phase:**

6. Extract channel key construction to helper function (shared with sessionChatSlice)
7. Add JSDoc comments
8. Ensure tests still pass

**Verification:**
- All WebSocket handler tests pass
- Message routing works correctly
- Unread increment logic validated
  </action>
  <verify>
CHAT_MESSAGE WebSocket handler implemented with passing unit tests. Message routing validated. Unread increment logic working correctly.
  </verify>
  <done>
Real-time chat integration complete with TDD validation.
  </done>
</task>

<task type="tdd">
  <name>Task 2: Implement memoized selectors (TDD)</name>
  <files>jam-ui/src/store/features/sessionChatSlice.js, jam-ui/src/store/features/__tests__/sessionChatSlice.test.js</files>
  <action>
Using TDD methodology, implement 8 memoized selectors using Reselect:

**RED Phase - Write failing tests first:**

1. Add tests to `sessionChatSlice.test.js`:
   ```javascript
   import {
     selectChatMessages,
     selectUnreadCount,
     selectTotalUnreadCount,
     selectIsChatWindowOpen,
     selectActiveChannel,
     selectFetchStatus,
     selectSendStatus,
     selectSendError
   } from '../sessionChatSlice';

   describe('sessionChat selectors', () => {
     const mockState = {
       sessionChat: {
         messagesByChannel: {
           'session-abc': [
             { id: 'msg-1', message: 'Hello' },
             { id: 'msg-2', message: 'World' }
           ],
           'global': [
             { id: 'msg-3', message: 'Global msg' }
           ]
         },
         activeChannel: 'session-abc',
         unreadCounts: {
           'session-abc': 0,
           'global': 5,
           'session-xyz': 3
         },
         isWindowOpen: true,
         fetchStatus: {
           'session-abc': 'succeeded',
           'global': 'loading'
         },
         sendStatus: 'idle',
         sendError: null
       }
     };

     describe('selectChatMessages', () => {
       test('returns messages for specified channel', () => {
         const messages = selectChatMessages(mockState, 'session-abc');
         expect(messages).toHaveLength(2);
         expect(messages[0].id).toBe('msg-1');
       });

       test('returns empty array for non-existent channel', () => {
         const messages = selectChatMessages(mockState, 'session-nonexistent');
         expect(messages).toEqual([]);
       });

       test('memoizes result for same channel', () => {
         const result1 = selectChatMessages(mockState, 'session-abc');
         const result2 = selectChatMessages(mockState, 'session-abc');
         expect(result1).toBe(result2); // Same reference
       });
     });

     describe('selectUnreadCount', () => {
       test('returns unread count for channel', () => {
         const count = selectUnreadCount(mockState, 'global');
         expect(count).toBe(5);
       });

       test('returns 0 for channel with no unread', () => {
         const count = selectUnreadCount(mockState, 'session-abc');
         expect(count).toBe(0);
       });

       test('returns 0 for non-existent channel', () => {
         const count = selectUnreadCount(mockState, 'nonexistent');
         expect(count).toBe(0);
       });
     });

     describe('selectTotalUnreadCount', () => {
       test('sums all unread counts across channels', () => {
         const total = selectTotalUnreadCount(mockState);
         expect(total).toBe(8); // 0 + 5 + 3
       });

       test('returns 0 when no unread messages', () => {
         const emptyState = {
           sessionChat: { unreadCounts: {} }
         };
         const total = selectTotalUnreadCount(emptyState);
         expect(total).toBe(0);
       });

       test('memoizes result', () => {
         const result1 = selectTotalUnreadCount(mockState);
         const result2 = selectTotalUnreadCount(mockState);
         expect(result1).toBe(result2);
       });
     });

     describe('selectIsChatWindowOpen', () => {
       test('returns window open state', () => {
         const isOpen = selectIsChatWindowOpen(mockState);
         expect(isOpen).toBe(true);
       });
     });

     describe('selectActiveChannel', () => {
       test('returns active channel', () => {
         const channel = selectActiveChannel(mockState);
         expect(channel).toBe('session-abc');
       });
     });

     describe('selectFetchStatus', () => {
       test('returns fetch status for channel', () => {
         const status = selectFetchStatus(mockState, 'session-abc');
         expect(status).toBe('succeeded');
       });

       test('returns idle for non-existent channel', () => {
         const status = selectFetchStatus(mockState, 'nonexistent');
         expect(status).toBe('idle');
       });
     });

     describe('selectSendStatus', () => {
       test('returns send status', () => {
         const status = selectSendStatus(mockState);
         expect(status).toBe('idle');
       });
     });

     describe('selectSendError', () => {
       test('returns send error', () => {
         const error = selectSendError(mockState);
         expect(error).toBeNull();
       });
     });
   });
   ```

2. Run tests - should FAIL (selectors don't exist yet)

**GREEN Phase - Implement selectors:**

3. Add selectors to sessionChatSlice.js:
   ```javascript
   import { createSelector } from '@reduxjs/toolkit';

   // Input selectors (direct state access)
   const selectSessionChatState = (state) => state.sessionChat;
   const selectMessagesByChannel = (state) => state.sessionChat.messagesByChannel;
   const selectUnreadCounts = (state) => state.sessionChat.unreadCounts;

   // Output selectors (memoized)
   export const selectChatMessages = createSelector(
     [selectMessagesByChannel, (state, channel) => channel],
     (messagesByChannel, channel) => messagesByChannel[channel] || []
   );

   export const selectUnreadCount = createSelector(
     [selectUnreadCounts, (state, channel) => channel],
     (unreadCounts, channel) => unreadCounts[channel] || 0
   );

   export const selectTotalUnreadCount = createSelector(
     [selectUnreadCounts],
     (unreadCounts) => Object.values(unreadCounts).reduce((sum, count) => sum + count, 0)
   );

   export const selectIsChatWindowOpen = createSelector(
     [selectSessionChatState],
     (chatState) => chatState.isWindowOpen
   );

   export const selectActiveChannel = createSelector(
     [selectSessionChatState],
     (chatState) => chatState.activeChannel
   );

   export const selectFetchStatus = createSelector(
     [selectSessionChatState, (state, channel) => channel],
     (chatState, channel) => chatState.fetchStatus[channel] || 'idle'
   );

   export const selectSendStatus = createSelector(
     [selectSessionChatState],
     (chatState) => chatState.sendStatus
   );

   export const selectSendError = createSelector(
     [selectSessionChatState],
     (chatState) => chatState.sendError
   );
   ```

4. Run tests - should PASS

**REFACTOR Phase:**

5. Review selector memoization (ensure proper input selectors)
6. Add JSDoc comments with @param and @returns
7. Ensure tests still pass

**Performance tests:**

8. Add memoization tests:
   ```javascript
   describe('selector memoization', () => {
     test('selectChatMessages returns same reference for unchanged state', () => {
       const result1 = selectChatMessages(mockState, 'session-abc');
       const result2 = selectChatMessages(mockState, 'session-abc');
       expect(result1).toBe(result2);
     });

     test('selectChatMessages returns new reference when messages change', () => {
       const result1 = selectChatMessages(mockState, 'session-abc');
       const newState = {
         ...mockState,
         sessionChat: {
           ...mockState.sessionChat,
           messagesByChannel: {
             ...mockState.sessionChat.messagesByChannel,
             'session-abc': [...mockState.sessionChat.messagesByChannel['session-abc'], { id: 'msg-new' }]
           }
         }
       };
       const result2 = selectChatMessages(newState, 'session-abc');
       expect(result1).not.toBe(result2);
     });
   });
   ```

**Verification:**
- All selector tests pass
- Memoization validated
- Edge cases handled (empty, null, non-existent)
  </action>
  <verify>
8 memoized selectors implemented with passing unit tests. Memoization validated. All edge cases handled correctly.
  </verify>
  <done>
Efficient Redux selectors complete with TDD validation.
  </done>
</task>

<task type="tdd">
  <name>Task 3: Add localStorage persistence for lastReadAt (TDD)</name>
  <files>jam-ui/src/store/features/sessionChatSlice.js, jam-ui/src/store/features/__tests__/sessionChatSlice.test.js, jam-ui/src/utils/chatStorage.js, jam-ui/src/utils/__tests__/chatStorage.test.js</files>
  <action>
Using TDD methodology, implement localStorage persistence for unread tracking:

**RED Phase - Write failing tests first:**

1. Create test file `jam-ui/src/utils/__tests__/chatStorage.test.js`:
   ```javascript
   import { saveLastReadAt, loadLastReadAt, clearLastReadAt } from '../chatStorage';

   describe('chatStorage', () => {
     beforeEach(() => {
       localStorage.clear();
     });

     describe('saveLastReadAt', () => {
       test('saves lastReadAt timestamp to localStorage', () => {
         saveLastReadAt('session-abc', '2026-01-26T12:00:00Z');
         const stored = localStorage.getItem('jk_chat_lastReadAt');
         expect(stored).toBeTruthy();
         const parsed = JSON.parse(stored);
         expect(parsed['session-abc']).toBe('2026-01-26T12:00:00Z');
       });

       test('merges with existing data', () => {
         saveLastReadAt('session-abc', '2026-01-26T12:00:00Z');
         saveLastReadAt('global', '2026-01-26T13:00:00Z');
         const stored = localStorage.getItem('jk_chat_lastReadAt');
         const parsed = JSON.parse(stored);
         expect(parsed['session-abc']).toBe('2026-01-26T12:00:00Z');
         expect(parsed['global']).toBe('2026-01-26T13:00:00Z');
       });

       test('handles localStorage errors gracefully', () => {
         // Mock localStorage.setItem to throw error
         jest.spyOn(Storage.prototype, 'setItem').mockImplementation(() => {
           throw new Error('QuotaExceededError');
         });
         expect(() => saveLastReadAt('session-abc', '2026-01-26T12:00:00Z')).not.toThrow();
       });
     });

     describe('loadLastReadAt', () => {
       test('loads lastReadAt timestamps from localStorage', () => {
         localStorage.setItem('jk_chat_lastReadAt', JSON.stringify({
           'session-abc': '2026-01-26T12:00:00Z',
           'global': '2026-01-26T13:00:00Z'
         }));
         const data = loadLastReadAt();
         expect(data['session-abc']).toBe('2026-01-26T12:00:00Z');
         expect(data['global']).toBe('2026-01-26T13:00:00Z');
       });

       test('returns empty object if no data', () => {
         const data = loadLastReadAt();
         expect(data).toEqual({});
       });

       test('handles JSON parse errors gracefully', () => {
         localStorage.setItem('jk_chat_lastReadAt', 'invalid json');
         const data = loadLastReadAt();
         expect(data).toEqual({});
       });
     });

     describe('clearLastReadAt', () => {
       test('clears specific channel', () => {
         localStorage.setItem('jk_chat_lastReadAt', JSON.stringify({
           'session-abc': '2026-01-26T12:00:00Z',
           'global': '2026-01-26T13:00:00Z'
         }));
         clearLastReadAt('session-abc');
         const stored = localStorage.getItem('jk_chat_lastReadAt');
         const parsed = JSON.parse(stored);
         expect(parsed['session-abc']).toBeUndefined();
         expect(parsed['global']).toBe('2026-01-26T13:00:00Z');
       });

       test('clears all data if no channel specified', () => {
         localStorage.setItem('jk_chat_lastReadAt', JSON.stringify({
           'session-abc': '2026-01-26T12:00:00Z'
         }));
         clearLastReadAt();
         const stored = localStorage.getItem('jk_chat_lastReadAt');
         expect(stored).toBeNull();
       });
     });
   });
   ```

2. Add integration tests to `sessionChatSlice.test.js`:
   ```javascript
   import { loadLastReadAt } from '../../utils/chatStorage';

   describe('sessionChat localStorage integration', () => {
     test('loads lastReadAt on init', () => {
       localStorage.setItem('jk_chat_lastReadAt', JSON.stringify({
         'session-abc': '2026-01-26T12:00:00Z'
       }));

       // Initialize Redux store (would call initializeChatStorage)
       const state = sessionChatReducer(undefined, { type: '@@INIT' });
       // After initialization, lastReadAt should be loaded
       expect(state.lastReadAt['session-abc']).toBe('2026-01-26T12:00:00Z');
     });

     test('saves lastReadAt when marking as read', () => {
       localStorage.clear();
       const state = {
         activeChannel: 'session-abc',
         unreadCounts: { 'session-abc': 5 },
         lastReadAt: {}
       };
       const action = { type: 'sessionChat/openChatWindow' };
       sessionChatReducer(state, action);

       // Check localStorage was updated
       const stored = localStorage.getItem('jk_chat_lastReadAt');
       expect(stored).toBeTruthy();
     });
   });
   ```

3. Run tests - should FAIL (utility doesn't exist yet)

**GREEN Phase - Implement localStorage utilities:**

4. Create `jam-ui/src/utils/chatStorage.js`:
   ```javascript
   const STORAGE_KEY = 'jk_chat_lastReadAt';

   export const saveLastReadAt = (channel, timestamp) => {
     try {
       const existing = loadLastReadAt();
       existing[channel] = timestamp;
       localStorage.setItem(STORAGE_KEY, JSON.stringify(existing));
     } catch (error) {
       console.error('Failed to save lastReadAt:', error);
     }
   };

   export const loadLastReadAt = () => {
     try {
       const stored = localStorage.getItem(STORAGE_KEY);
       if (!stored) return {};
       return JSON.parse(stored);
     } catch (error) {
       console.error('Failed to load lastReadAt:', error);
       return {};
     }
   };

   export const clearLastReadAt = (channel) => {
     try {
       if (channel) {
         const existing = loadLastReadAt();
         delete existing[channel];
         if (Object.keys(existing).length === 0) {
           localStorage.removeItem(STORAGE_KEY);
         } else {
           localStorage.setItem(STORAGE_KEY, JSON.stringify(existing));
         }
       } else {
         localStorage.removeItem(STORAGE_KEY);
       }
     } catch (error) {
       console.error('Failed to clear lastReadAt:', error);
     }
   };
   ```

5. Integrate with Redux slice:
   ```javascript
   import { saveLastReadAt, loadLastReadAt } from '../../utils/chatStorage';

   // In initialState:
   const initialState = {
     // ... other fields
     lastReadAt: loadLastReadAt() // Load from localStorage on init
   };

   // In reducers that update lastReadAt:
   openChatWindow: (state) => {
     state.isWindowOpen = true;
     if (state.activeChannel) {
       const now = new Date().toISOString();
       state.lastReadAt[state.activeChannel] = now;
       state.unreadCounts[state.activeChannel] = 0;
       saveLastReadAt(state.activeChannel, now); // Save to localStorage
     }
   },

   markAsRead: (state, action) => {
     const { channel } = action.payload;
     const now = new Date().toISOString();
     state.lastReadAt[channel] = now;
     state.unreadCounts[channel] = 0;
     saveLastReadAt(channel, now); // Save to localStorage
   }
   ```

6. Run tests - should PASS

**REFACTOR Phase:**

7. Review error handling in chatStorage utilities
8. Add JSDoc comments
9. Ensure tests still pass

**Edge case tests:**

10. Add tests for edge cases:
    ```javascript
    describe('chatStorage edge cases', () => {
      test('handles very old timestamps', () => {
        // Test with timestamps from months ago
      });

      test('handles invalid timestamp formats', () => {
        // Test with malformed timestamps
      });

      test('handles large data sets', () => {
        // Test with many channels
      });
    });
    ```

**Verification:**
- All localStorage tests pass
- Error handling validated
- Integration with Redux slice works
- Edge cases handled
  </action>
  <verify>
localStorage utilities implemented with passing unit tests. Integration with Redux validated. Error handling working correctly. Edge cases handled.
  </verify>
  <done>
Complete chat infrastructure with localStorage persistence. Phase 7 complete with comprehensive TDD coverage. Ready for Phase 8 (Chat Window UI & Message Display).
  </done>
</task>

</tasks>

<verification>
Before declaring plan complete:
- [ ] CHAT_MESSAGE WebSocket handler added to useSessionWebSocket.js
- [ ] Unit tests for WebSocket message routing
- [ ] 8 memoized selectors implemented: selectChatMessages, selectUnreadCount, selectTotalUnreadCount, selectIsChatWindowOpen, selectActiveChannel, selectFetchStatus, selectSendStatus, selectSendError
- [ ] Unit tests for all selectors with edge cases
- [ ] Memoization validated with performance tests
- [ ] localStorage utilities created: saveLastReadAt, loadLastReadAt, clearLastReadAt
- [ ] Unit tests for localStorage with error handling
- [ ] Integration with Redux slice validated
- [ ] All tests passing (TDD methodology followed)
- [ ] Phase 7 complete - ready for Phase 8
</verification>

<success_criteria>

- WebSocket CHAT_MESSAGE handler implemented and tested
- 8 memoized selectors complete with 100% test coverage
- localStorage persistence working with error handling
- All WebSocket routing validated
- Selector memoization confirmed
- Integration tests show complete data flow
- TDD methodology strictly followed (test-first approach)
- Phase 7 complete - Redux infrastructure ready for UI components
- Ready for Phase 8 (Chat Window UI & Message Display)
</success_criteria>

<output>
After completion, create `.planning/phases/07-chat-infrastructure/07-03-SUMMARY.md`:

# Phase 7 Plan 3: WebSocket Integration & Selectors Summary

**Implemented WebSocket message handling and memoized selectors using TDD methodology**

## Accomplishments

- Added CHAT_MESSAGE handler to useSessionWebSocket.js
- Implemented 8 memoized selectors using Reselect
- Created localStorage utilities for lastReadAt persistence
- Comprehensive unit test suite with 100% coverage
- WebSocket message routing validated
- Selector memoization performance confirmed
- localStorage error handling validated

## Files Created/Modified

- `jam-ui/src/hooks/useSessionWebSocket.js` - Added CHAT_MESSAGE handler
- `jam-ui/src/hooks/__tests__/useSessionWebSocket.test.js` - WebSocket handler tests
- `jam-ui/src/store/features/sessionChatSlice.js` - Added 8 selectors and localStorage integration
- `jam-ui/src/store/features/__tests__/sessionChatSlice.test.js` - Selector and localStorage tests
- `jam-ui/src/utils/chatStorage.js` - localStorage utilities
- `jam-ui/src/utils/__tests__/chatStorage.test.js` - localStorage utility tests

## Decisions Made

[Document any implementation decisions: Channel key construction? Unread increment logic? localStorage error strategy?]

## Issues Encountered

[Any challenges during WebSocket integration, or "None"]

## Next Phase Readiness

**Phase 7 complete!** Redux infrastructure (state, thunks, selectors, WebSocket) fully implemented and tested.

Ready for Phase 8 (Chat Window UI & Message Display).

Use `/gsd:plan-phase 8` to break down Phase 8 into executable plans.
</output>
