diff --git a/.planning/phases/12-attachment-research-&-backend-validation/docs/REACT_INTEGRATION_DESIGN.md b/.planning/phases/12-attachment-research-&-backend-validation/docs/REACT_INTEGRATION_DESIGN.md
new file mode 100644
index 000000000..b6338ead1
--- /dev/null
+++ b/.planning/phases/12-attachment-research-&-backend-validation/docs/REACT_INTEGRATION_DESIGN.md
@@ -0,0 +1,1319 @@
+# React Integration Design for Session Attachments
+
+**Phase:** 12 (Attachment Research & Backend Validation)
+**Plan:** 02
+**Date:** 2026-02-02
+**Integration Strategy:** Extend existing jam-ui chat components with attachment upload/display
+
+---
+
+## Executive Summary
+
+This document provides the complete React integration strategy for adding file attachment capabilities to the existing jam-ui session chat. The design extends existing components (`JKChatComposer`, `JKChatMessage`, `JKSessionScreen`) and Redux state (`sessionChatSlice`) to support file selection, validation, upload, progress tracking, and attachment display.
+
+**Design Principles:**
+1. **Minimal changes:** Extend, don't replace existing components
+2. **Pattern reuse:** Follow established jam-ui patterns (WindowPortal, Redux thunks, WebSocket handlers)
+3. **TDD implementation:** All new functionality requires tests before code
+4. **Progressive enhancement:** Attachment features are additive to existing chat
+
+---
+
+## 1. Component Modifications Overview
+
+| Component | Current State | Modifications Needed | Phase | Lines Est. |
+|-----------|---------------|----------------------|-------|------------|
+| `JKChatComposer.js` | Text input + Send button | Add attach button, file validation, upload trigger | 13 | +80 |
+| `JKChatMessage.js` | Text display with avatar | Handle attachment messages, render download link | 14 | +40 |
+| `JKSessionScreen.js` | WebSocket CHAT_MESSAGE handler | Extract attachment fields from WebSocket payload | 14-15 | +10 |
+| `sessionChatSlice.js` | Chat state management | Add upload state, uploadAttachment thunk | 13 | +120 |
+| `rest.js` | API helper functions | Add uploadMusicNotation, getMusicNotationUrl | 13 | +40 |
+| **New:** `JKChatAttachButton.js` | N/A | Hidden file input + visible button trigger | 13 | +60 |
+| **New:** `attachmentValidation.js` | N/A | Client-side file validation utilities | 13 | +80 |
+
+**Total estimated additions:** ~430 lines of code + ~200 lines of tests = 630 lines total
+
+---
+
+## 2. New Component: JKChatAttachButton
+
+### Purpose
+Provide a user-friendly file selection interface with hidden `` triggered by visible button.
+
+### Component Skeleton
+
+```javascript
+// jam-ui/src/components/client/chat/JKChatAttachButton.js
+import React, { useRef, useCallback } from 'react';
+import PropTypes from 'prop-types';
+
+/**
+ * JKChatAttachButton - File selection button for chat attachments
+ *
+ * Pattern: Hidden file input + visible button trigger
+ * - User clicks visible button
+ * - Button triggers hidden file input click
+ * - File input onChange calls onFileSelect callback
+ * - Input value reset after selection (allows re-selecting same file)
+ *
+ * @param {Object} props
+ * @param {function} props.onFileSelect - Callback when file selected: (file) => void
+ * @param {boolean} props.disabled - Disable button (during upload or when disconnected)
+ * @param {boolean} props.isUploading - Show uploading state
+ */
+const JKChatAttachButton = ({ onFileSelect, disabled, isUploading }) => {
+ const fileInputRef = useRef(null);
+
+ const handleButtonClick = useCallback(() => {
+ fileInputRef.current?.click();
+ }, []);
+
+ const handleFileChange = useCallback((e) => {
+ const file = e.target.files?.[0];
+ if (file) {
+ onFileSelect(file);
+ // Reset input value to allow re-selecting same file
+ e.target.value = '';
+ }
+ }, [onFileSelect]);
+
+ return (
+ <>
+ {/* Visible button */}
+
+
+ {/* Hidden file input */}
+
+ >
+ );
+};
+
+JKChatAttachButton.propTypes = {
+ onFileSelect: PropTypes.func.isRequired,
+ disabled: PropTypes.bool,
+ isUploading: PropTypes.bool
+};
+
+JKChatAttachButton.defaultProps = {
+ disabled: false,
+ isUploading: false
+};
+
+export default React.memo(JKChatAttachButton);
+```
+
+### Accept Attribute Value
+
+**From requirements (pending mp3 decision):**
+```javascript
+accept=".pdf,.xml,.mxl,.txt,.png,.jpg,.jpeg,.gif,.mp3,.wav"
+```
+
+**If mp3 is NOT supported (after backend decision):**
+```javascript
+accept=".pdf,.xml,.mxl,.txt,.png,.jpg,.jpeg,.gif,.wav"
+```
+
+### Integration in JKChatComposer
+
+```javascript
+// Add to JKChatComposer.js imports
+import JKChatAttachButton from './JKChatAttachButton';
+
+// Add file upload handler
+const handleFileSelect = useCallback(async (file) => {
+ // Validate file before upload
+ const validation = validateFile(file);
+ if (!validation.valid) {
+ // Show error (could use toast or inline error display)
+ console.error('File validation failed:', validation.error);
+ return;
+ }
+
+ // Dispatch upload action
+ await dispatch(uploadAttachment({
+ file,
+ sessionId,
+ clientId: server?.clientId
+ }));
+}, [dispatch, sessionId, server?.clientId]);
+
+// Add button before Send button in render
+
+
+
+
+```
+
+---
+
+## 3. Redux State Extensions (sessionChatSlice.js)
+
+### New State Shape
+
+```javascript
+const initialState = {
+ // ... existing state
+ messagesByChannel: {},
+ activeChannel: null,
+ // ... etc
+
+ // NEW: Upload state tracking
+ uploadState: {
+ status: 'idle', // 'idle' | 'uploading' | 'error'
+ progress: 0, // 0-100 (future: if progress tracking implemented)
+ error: null, // Error message string or null
+ fileName: null // Currently uploading file name (for UI display)
+ }
+};
+```
+
+### New Reducers
+
+```javascript
+const sessionChatSlice = createSlice({
+ name: 'sessionChat',
+ initialState,
+ reducers: {
+ // ... existing reducers
+
+ // NEW: Set upload status
+ setUploadStatus: (state, action) => {
+ const { status, progress, error, fileName } = action.payload;
+ state.uploadState.status = status;
+ if (progress !== undefined) state.uploadState.progress = progress;
+ if (error !== undefined) state.uploadState.error = error;
+ if (fileName !== undefined) state.uploadState.fileName = fileName;
+ },
+
+ // NEW: Clear upload error
+ clearUploadError: (state) => {
+ state.uploadState.error = null;
+ state.uploadState.status = 'idle';
+ }
+ },
+ extraReducers: (builder) => {
+ // ... existing thunk handlers
+
+ // NEW: Upload attachment thunk handlers
+ builder
+ .addCase(uploadAttachment.pending, (state, action) => {
+ state.uploadState.status = 'uploading';
+ state.uploadState.error = null;
+ state.uploadState.fileName = action.meta.arg.file.name;
+ state.uploadState.progress = 0;
+ })
+ .addCase(uploadAttachment.fulfilled, (state, action) => {
+ state.uploadState.status = 'idle';
+ state.uploadState.fileName = null;
+ state.uploadState.progress = 0;
+ // Note: Actual chat message arrives via WebSocket, not from upload response
+ })
+ .addCase(uploadAttachment.rejected, (state, action) => {
+ state.uploadState.status = 'error';
+ state.uploadState.error = action.error.message || 'Upload failed';
+ state.uploadState.progress = 0;
+ });
+ }
+});
+
+export const { setUploadStatus, clearUploadError } = sessionChatSlice.actions;
+```
+
+### New Async Thunk: uploadAttachment
+
+```javascript
+/**
+ * Upload file attachment to session
+ * Creates MusicNotation record, which triggers ChatMessage creation
+ * ChatMessage broadcast via WebSocket (handled separately)
+ *
+ * @param {Object} params
+ * @param {File} params.file - File object from input
+ * @param {string} params.sessionId - Music session ID
+ * @param {string} [params.clientId] - WebSocket client ID (for deduplication)
+ */
+export const uploadAttachment = createAsyncThunk(
+ 'sessionChat/uploadAttachment',
+ async ({ file, sessionId, clientId }, { rejectWithValue }) => {
+ try {
+ // Build FormData
+ const formData = new FormData();
+ formData.append('files[]', file);
+ formData.append('session_id', sessionId);
+
+ // Determine attachment type based on file extension
+ const ext = file.name.split('.').pop().toLowerCase();
+ const audioExts = ['mp3', 'wav', 'flac', 'ogg', 'aiff', 'aifc', 'au'];
+ const attachmentType = audioExts.includes(ext) ? 'audio' : 'notation';
+ formData.append('attachment_type', attachmentType);
+
+ // Upload via REST API
+ const response = await uploadMusicNotation(formData);
+
+ // Response contains file metadata, but ChatMessage arrives via WebSocket
+ return response;
+ } catch (error) {
+ // Check for specific error types
+ if (error.status === 413) {
+ return rejectWithValue('File too large - maximum 10 MB');
+ }
+ if (error.status === 422) {
+ return rejectWithValue('Invalid file type or format');
+ }
+ return rejectWithValue(error.message || 'Upload failed');
+ }
+ }
+);
+```
+
+### New Selectors
+
+```javascript
+// Select upload state
+export const selectUploadStatus = (state) => state.sessionChat.uploadState.status;
+export const selectUploadError = (state) => state.sessionChat.uploadState.error;
+export const selectUploadProgress = (state) => state.sessionChat.uploadState.progress;
+export const selectUploadFileName = (state) => state.sessionChat.uploadState.fileName;
+export const selectIsUploading = (state) => state.sessionChat.uploadState.status === 'uploading';
+```
+
+---
+
+## 4. REST Helper Additions (rest.js)
+
+### New Function: uploadMusicNotation
+
+```javascript
+/**
+ * Upload file attachment to S3 via backend
+ *
+ * IMPORTANT: Uses native fetch, NOT apiFetch wrapper
+ * Reason: FormData requires browser to set Content-Type with boundary
+ *
+ * @param {FormData} formData - FormData with files[], session_id, attachment_type
+ * @returns {Promise