diff --git a/.planning/phases/06-session-chat-research-design/CHAT_COMPONENT_DESIGN.md b/.planning/phases/06-session-chat-research-design/CHAT_COMPONENT_DESIGN.md
new file mode 100644
index 000000000..21204c7a7
--- /dev/null
+++ b/.planning/phases/06-session-chat-research-design/CHAT_COMPONENT_DESIGN.md
@@ -0,0 +1,954 @@
+# Session Chat Component Architecture Design
+
+**Purpose:** Design React component hierarchy for session chat, following established jam-ui patterns from Backing Track and JamTrack.
+
+**Date:** 2026-01-26
+**Phase:** 06-session-chat-research-design
+**Plan:** 02
+
+---
+
+## Overview
+
+This document designs the component architecture for session chat in jam-ui, reusing existing patterns from Phase 3 (Backing Track) and Phase 5 (JamTrack), with WindowPortal for modeless dialog support.
+
+**Key Design Principles:**
+- Follow JamTrack player pattern: Main component with specialized sub-components
+- Use WindowPortal for modeless chat window (already proven for Metronome, JamTrack)
+- Heavy Redux integration for state management (minimal props)
+- Clear separation: UI components vs data/behavior (Redux)
+
+---
+
+## Component Hierarchy
+
+```
+JKSessionChatButton (in session screen top nav)
+├── Badge (unread count indicator)
+└── onClick → dispatch(openChatWindow)
+
+JKSessionChatWindow (WindowPortal wrapper)
+├── WindowPortal (from jam-ui/src/components/common/WindowPortal.js)
+│ └── Chat content (rendered inside popup window)
+│ ├── JKChatHeader
+│ │ ├── Title ("Session Chat")
+│ │ └── Close button
+│ ├── JKChatMessageList (scrollable container)
+│ │ ├── JKChatLoadingSpinner (conditional: fetchStatus === 'loading')
+│ │ ├── JKChatEmptyState (conditional: messages.length === 0)
+│ │ └── JKChatMessage[] (repeated for each message)
+│ │ ├── Avatar (user photo)
+│ │ ├── Sender name
+│ │ ├── Message text
+│ │ └── Timestamp
+│ └── JKChatComposer
+│ ├── Textarea (message input)
+│ ├── Character count (255 max)
+│ └── Send button
+```
+
+---
+
+## Component Specifications
+
+### 1. JKSessionChatButton
+
+**File:** `jam-ui/src/components/client/JKSessionChatButton.js`
+
+**Purpose:** Chat button in session screen top nav, shows unread badge.
+
+**Props:**
+```javascript
+{
+ // No props - all state from Redux
+}
+```
+
+**Redux Selectors:**
+```javascript
+const unreadCount = useSelector(selectUnreadCount(sessionId));
+const isWindowOpen = useSelector(state => state.sessionChat.isWindowOpen);
+```
+
+**Redux Actions:**
+```javascript
+dispatch(openChatWindow());
+dispatch(closeChatWindow());
+```
+
+**Render:**
+```jsx
+
+```
+
+**State Ownership:**
+- ✅ Redux: unreadCount, isWindowOpen
+- ❌ Props: None
+- ✅ Local: None (purely presentational)
+
+**Integration Point:**
+- Add to `JKSessionScreen` top navigation area (near mixer, metronome buttons)
+- Position: Absolute or flex layout (TBD in implementation)
+
+---
+
+### 2. JKSessionChatWindow
+
+**File:** `jam-ui/src/components/client/JKSessionChatWindow.js`
+
+**Purpose:** Main chat window component, wraps WindowPortal for modeless dialog.
+
+**Props:**
+```javascript
+{
+ // No props - all state from Redux
+}
+```
+
+**Redux Selectors:**
+```javascript
+const isWindowOpen = useSelector(state => state.sessionChat.isWindowOpen);
+const sessionId = useSelector(state => state.activeSession.sessionId);
+const messages = useSelector(selectChatMessages(sessionId));
+const fetchStatus = useSelector(state => state.sessionChat.fetchStatus[sessionId]);
+const currentUser = useSelector(state => state.auth.user);
+```
+
+**Redux Actions:**
+```javascript
+dispatch(closeChatWindow());
+dispatch(fetchChatHistory({ channel: sessionId, channelType: 'session', sessionId }));
+dispatch(markAsRead({ channel: sessionId }));
+```
+
+**Lifecycle:**
+```javascript
+useEffect(() => {
+ if (isWindowOpen && sessionId) {
+ // Fetch message history on window open
+ dispatch(fetchChatHistory({ channel: sessionId, channelType: 'session', sessionId }));
+
+ // Mark messages as read
+ dispatch(markAsRead({ channel: sessionId }));
+ }
+}, [isWindowOpen, sessionId, dispatch]);
+```
+
+**Render:**
+```jsx
+{isWindowOpen && (
+
+
+
+
+
+
+
+)}
+```
+
+**State Ownership:**
+- ✅ Redux: isWindowOpen, messages, fetchStatus
+- ❌ Props: None
+- ✅ Local: None (delegates to sub-components)
+
+**Window Management:**
+- Uses WindowPortal (no modifications needed)
+- Dimensions: 400x600px (similar to Metronome popup)
+- Position: User-configurable (WindowPortal handles)
+- Auto-cleanup: WindowPortal handles on close
+
+---
+
+### 3. JKChatHeader
+
+**File:** `jam-ui/src/components/client/chat/JKChatHeader.js`
+
+**Purpose:** Chat window title bar with close button.
+
+**Props:**
+```javascript
+{
+ onClose: () => void // Callback to close window
+}
+```
+
+**Redux Selectors:**
+```javascript
+// None - purely presentational
+```
+
+**Render:**
+```jsx
+
+
Session Chat
+
+
+```
+
+**State Ownership:**
+- ❌ Redux: None
+- ✅ Props: onClose callback
+- ✅ Local: None
+
+**Styling:**
+- Fixed height (e.g., 48px)
+- Flex layout: title left, close button right
+- Border-bottom separator
+
+---
+
+### 4. JKChatMessageList
+
+**File:** `jam-ui/src/components/client/chat/JKChatMessageList.js`
+
+**Purpose:** Scrollable message container with auto-scroll to bottom.
+
+**Props:**
+```javascript
+{
+ // No props - all state from Redux
+}
+```
+
+**Redux Selectors:**
+```javascript
+const sessionId = useSelector(state => state.activeSession.sessionId);
+const messages = useSelector(selectChatMessages(sessionId)); // Sorted by created_at ASC
+const fetchStatus = useSelector(state => state.sessionChat.fetchStatus[sessionId]);
+const currentUser = useSelector(state => state.auth.user);
+```
+
+**Local State:**
+```javascript
+const [isUserScrolling, setIsUserScrolling] = useState(false);
+const scrollContainerRef = useRef(null);
+```
+
+**Auto-Scroll Logic:**
+```javascript
+useEffect(() => {
+ if (!isUserScrolling && scrollContainerRef.current) {
+ // Auto-scroll to bottom on new messages
+ scrollContainerRef.current.scrollTo({
+ top: scrollContainerRef.current.scrollHeight,
+ behavior: 'smooth'
+ });
+ }
+}, [messages.length, isUserScrolling]);
+
+const handleScroll = (e) => {
+ const { scrollTop, scrollHeight, clientHeight } = e.target;
+ const isAtBottom = scrollHeight - scrollTop - clientHeight < 50;
+ setIsUserScrolling(!isAtBottom);
+};
+```
+
+**Render:**
+```jsx
+
+ {fetchStatus === 'loading' && }
+
+ {fetchStatus === 'succeeded' && messages.length === 0 && (
+
+ )}
+
+ {messages.map(message => (
+
+ ))}
+
+```
+
+**State Ownership:**
+- ✅ Redux: messages, fetchStatus, currentUser
+- ❌ Props: None
+- ✅ Local: isUserScrolling, scrollContainerRef
+
+**Styling:**
+- Flex-grow: 1 (fills space between header and composer)
+- Overflow-y: auto (scrollable)
+- Padding: 16px
+- Background: Light gray (#f5f5f5)
+
+---
+
+### 5. JKChatMessage
+
+**File:** `jam-ui/src/components/client/chat/JKChatMessage.js`
+
+**Purpose:** Single message item (avatar, name, text, timestamp).
+
+**Props:**
+```javascript
+{
+ message: {
+ id: string,
+ senderId: string,
+ senderName: string,
+ message: string,
+ createdAt: string, // ISO 8601 timestamp
+ channel: string
+ },
+ isCurrentUser: boolean // True if message from current user
+}
+```
+
+**Redux Selectors:**
+```javascript
+// None - all data passed via props
+```
+
+**Helper Functions:**
+```javascript
+import dayjs from 'dayjs';
+import relativeTime from 'dayjs/plugin/relativeTime';
+
+dayjs.extend(relativeTime);
+
+const formatTimestamp = (isoTimestamp) => {
+ return dayjs(isoTimestamp).fromNow(); // "5 minutes ago"
+};
+```
+
+**Render:**
+```jsx
+
+ {!isCurrentUser && (
+
+ {/* Placeholder: Use first letter of name */}
+
+ {message.senderName.charAt(0).toUpperCase()}
+
+
+ )}
+
+
+
+
+ {isCurrentUser ? 'You' : message.senderName}
+
+
+ {formatTimestamp(message.createdAt)}
+
+
+
+
+ {message.message}
+
+
+
+ {isCurrentUser && (
+
+
+ {message.senderName.charAt(0).toUpperCase()}
+
+
+ )}
+
+```
+
+**State Ownership:**
+- ❌ Redux: None
+- ✅ Props: message, isCurrentUser
+- ✅ Local: None (purely presentational)
+
+**Styling:**
+- Layout: Flex row
+- Current user messages: Avatar on right, text bubble right-aligned (blue)
+- Other user messages: Avatar on left, text bubble left-aligned (gray)
+- Text bubble: Rounded corners, padding, max-width 70%
+- Timestamp: Small, gray, relative time format
+
+**Future Enhancement (Phase 11):**
+- Replace avatar placeholder with actual user photo from API
+- Add attachment support (music_notation, claimed_recording)
+
+---
+
+### 6. JKChatComposer
+
+**File:** `jam-ui/src/components/client/chat/JKChatComposer.js`
+
+**Purpose:** Message composition area (textarea + send button).
+
+**Props:**
+```javascript
+{
+ // No props - all state from Redux
+}
+```
+
+**Redux Selectors:**
+```javascript
+const sessionId = useSelector(state => state.activeSession.sessionId);
+const sendStatus = useSelector(state => state.sessionChat.sendStatus);
+const sendError = useSelector(state => state.sessionChat.sendError);
+const clientId = useSelector(state => state.auth.clientId); // For WebSocket routing
+const isConnected = useSelector(state => state.jamServer.isConnected);
+```
+
+**Redux Actions:**
+```javascript
+dispatch(sendMessage({
+ message,
+ channel: sessionId,
+ channelType: 'session',
+ sessionId,
+ clientId
+}));
+```
+
+**Local State:**
+```javascript
+const [message, setMessage] = useState('');
+const textareaRef = useRef(null);
+```
+
+**Keyboard Handling:**
+```javascript
+const handleKeyDown = (e) => {
+ if (e.key === 'Enter' && !e.shiftKey) {
+ e.preventDefault(); // Prevent newline
+ handleSend();
+ }
+ // Shift+Enter allows newline
+};
+```
+
+**Send Handler:**
+```javascript
+const handleSend = () => {
+ const trimmed = message.trim();
+
+ // Validation
+ if (!trimmed) return;
+ if (trimmed.length > 255) {
+ alert('Message too long (max 255 characters)');
+ return;
+ }
+ if (!isConnected) {
+ alert('Cannot send message: disconnected from server');
+ return;
+ }
+
+ // Send message
+ dispatch(sendMessage({
+ message: trimmed,
+ channel: sessionId,
+ channelType: 'session',
+ sessionId,
+ clientId
+ }));
+
+ // Clear input
+ setMessage('');
+ textareaRef.current?.focus();
+};
+```
+
+**Render:**
+```jsx
+
+ {sendError && (
+
+ Error: {sendError}
+
+ )}
+
+
+
+```
+
+**State Ownership:**
+- ✅ Redux: sendStatus, sendError, sessionId, clientId, isConnected
+- ❌ Props: None
+- ✅ Local: message, textareaRef
+
+**Styling:**
+- Fixed height textarea (e.g., 80px)
+- Auto-resize (optional, Phase 11)
+- Send button: Orange (primary action)
+- Character count: Right-aligned, small font
+- Error message: Red background, above input
+
+---
+
+### 7. JKChatLoadingSpinner
+
+**File:** `jam-ui/src/components/client/chat/JKChatLoadingSpinner.js`
+
+**Purpose:** Loading indicator while fetching message history.
+
+**Props:**
+```javascript
+{
+ // No props
+}
+```
+
+**Render:**
+```jsx
+
+
+
Loading messages...
+
+```
+
+**State Ownership:**
+- ❌ Redux: None
+- ❌ Props: None
+- ✅ Local: None (purely presentational)
+
+**Styling:**
+- Centered in message list
+- CSS spinner animation (existing jam-ui spinner class?)
+- Gray text
+
+---
+
+### 8. JKChatEmptyState
+
+**File:** `jam-ui/src/components/client/chat/JKChatEmptyState.js`
+
+**Purpose:** Empty state when no messages in session.
+
+**Props:**
+```javascript
+{
+ // No props
+}
+```
+
+**Render:**
+```jsx
+
+
💬
+
No messages yet
+
+ Start the conversation by sending a message below
+
+
+```
+
+**State Ownership:**
+- ❌ Redux: None
+- ❌ Props: None
+- ✅ Local: None (purely presentational)
+
+**Styling:**
+- Centered in message list
+- Large icon
+- Gray text
+- Friendly copy
+
+---
+
+## Props vs Redux Decision Matrix
+
+| Data/State | Props | Redux | Rationale |
+|------------|-------|-------|-----------|
+| **Messages** | ❌ | ✅ | Shared across WebSocket handler, needs persistence |
+| **Unread count** | ❌ | ✅ | Displayed in button, updated by WebSocket |
+| **Window open/closed** | ❌ | ✅ | Persisted across re-renders |
+| **Fetch status** | ❌ | ✅ | Shared loading state |
+| **Send status** | ❌ | ✅ | Shared send state |
+| **Current user** | ❌ | ✅ | Available from auth slice |
+| **Session ID** | ❌ | ✅ | Available from activeSession slice |
+| **Client ID** | ❌ | ✅ | Available from auth slice |
+| **Connection status** | ❌ | ✅ | Available from jamServer context |
+| **Message input text** | ❌ | ❌ | ✅ Local (ephemeral, no need to persist) |
+| **Scroll position** | ❌ | ❌ | ✅ Local (managed by ref) |
+| **User scrolling flag** | ❌ | ❌ | ✅ Local (auto-scroll logic) |
+| **onClose callback** | ✅ | ❌ | Passed to sub-components |
+
+**Summary:**
+- **Heavy Redux usage** for data and state management
+- **Minimal props** (only callbacks like onClose)
+- **Local state** only for ephemeral UI state (input text, scroll position)
+
+---
+
+## Reusable Components
+
+### Available from jam-ui
+
+**1. WindowPortal**
+- ✅ **File:** `jam-ui/src/components/common/WindowPortal.js`
+- ✅ **Usage:** Wrap chat content for modeless dialog
+- ✅ **No modifications needed**
+
+**2. Button Classes**
+- ✅ **CSS Classes:** `.button-orange` (primary), `.button-grey` (secondary)
+- ✅ **Usage:** Apply to `