Add Project Roman session fix analysis and design documentation
This commit includes comprehensive analysis and design documentation for fixing critical session management issues in manually created projects. Phase 1 Complete: - Identified 4 critical errors (SSE null reference, array access, race conditions, virtual workingDir mismatch) - Created detailed root cause analysis - Designed comprehensive solution with 5 components - Complete implementation plan with testing strategy Files added: - ROMAN_SESSION_ISSUE_ANALYSIS.md - Detailed root cause analysis - ROMAN_SESSION_FIX_DESIGN.md - Complete solution design - ROMAN_IMPLEMENTATION_SUMMARY.md - Quick reference guide - PHASE_1_COMPLETE_REPORT.md - Executive summary Next: Awaiting AI Engineer review before implementation Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
840
ROMAN_SESSION_FIX_DESIGN.md
Normal file
840
ROMAN_SESSION_FIX_DESIGN.md
Normal file
@@ -0,0 +1,840 @@
|
||||
# Project Roman Session Fix - Phase 2 Design Document
|
||||
|
||||
## Executive Summary
|
||||
This document outlines the comprehensive fix for the Project Roman session issues. The solution addresses root causes: race conditions, multiple sources of truth, and insufficient error handling.
|
||||
|
||||
## Design Principles
|
||||
|
||||
### 1. Single Direction Data Flow
|
||||
```
|
||||
API (Source of Truth)
|
||||
↓
|
||||
Frontend State (Derived)
|
||||
↓
|
||||
UI (Computed)
|
||||
```
|
||||
|
||||
### 2. Optimistic Updates with Rollback
|
||||
- Update UI immediately on user action
|
||||
- Verify with API in background
|
||||
- Rollback on failure
|
||||
|
||||
### 3. Event-Driven Architecture
|
||||
- All state changes emit events
|
||||
- Components subscribe to relevant events
|
||||
- No direct state manipulation
|
||||
|
||||
## Component 1: Enhanced State Management
|
||||
|
||||
### New Class: SessionStateManager
|
||||
```javascript
|
||||
class SessionStateManager extends EventEmitter {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
projects: new Map(), // projectId -> Project
|
||||
sessions: new Map(), // sessionId -> Session
|
||||
activeProjectId: null,
|
||||
activeSessionId: null,
|
||||
loading: false,
|
||||
error: null
|
||||
};
|
||||
this.initialized = false;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// CORE STATE OPERATIONS - All transactions go through here
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Atomic state update with event emission
|
||||
* @param {string} operation - Operation name
|
||||
* @param {Function} updater - Function that updates state
|
||||
* @returns {Object} Result of operation
|
||||
*/
|
||||
async transaction(operation, updater) {
|
||||
const transactionId = `${operation}-${Date.now()}`;
|
||||
console.log(`[State] Transaction START: ${transactionId}`);
|
||||
|
||||
try {
|
||||
// Emit transaction start
|
||||
this.emit('transaction:start', { transactionId, operation });
|
||||
|
||||
// Execute update
|
||||
const result = await updater(this.state);
|
||||
|
||||
// Emit transaction success
|
||||
this.emit('transaction:success', { transactionId, operation, result });
|
||||
|
||||
console.log(`[State] Transaction SUCCESS: ${transactionId}`);
|
||||
return result;
|
||||
} catch (error) {
|
||||
// Emit transaction failure
|
||||
this.emit('transaction:error', { transactionId, operation, error });
|
||||
console.error(`[State] Transaction ERROR: ${transactionId}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize state from API
|
||||
*/
|
||||
async initialize() {
|
||||
if (this.initialized) {
|
||||
console.log('[State] Already initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
return this.transaction('initialize', async (state) => {
|
||||
state.loading = true;
|
||||
this.emit('state:loading', { loading: true });
|
||||
|
||||
// Fetch from API
|
||||
const response = await fetch('/claude/api/claude/sessions');
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Build state from API response
|
||||
const allSessions = [
|
||||
...(data.active || []).map(s => ({...s, status: 'active'})),
|
||||
...(data.historical || []).map(s => ({...s, status: 'historical'}))
|
||||
];
|
||||
|
||||
// Group sessions by project
|
||||
const grouped = this.groupSessionsByProject(allSessions);
|
||||
|
||||
// Load manually created projects from localStorage (as metadata only)
|
||||
const manualProjects = this.loadManualProjectMetadata();
|
||||
|
||||
// Merge manual projects with sessions
|
||||
this.mergeManualProjects(grouped, manualProjects);
|
||||
|
||||
// Update state
|
||||
state.projects = grouped;
|
||||
allSessions.forEach(session => {
|
||||
state.sessions.set(session.id, session);
|
||||
});
|
||||
|
||||
state.loading = false;
|
||||
this.initialized = true;
|
||||
|
||||
this.emit('state:initialized', {
|
||||
projectCount: state.projects.size,
|
||||
sessionCount: state.sessions.size
|
||||
});
|
||||
|
||||
return { projectCount: state.projects.size, sessionCount: state.sessions.size };
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create session with optimistic update
|
||||
*/
|
||||
async createSession(workingDir, projectId, projectName) {
|
||||
// Optimistic: Add session to state immediately
|
||||
const tempSessionId = `temp-${Date.now()}`;
|
||||
const tempSession = {
|
||||
id: tempSessionId,
|
||||
workingDir,
|
||||
status: 'creating',
|
||||
metadata: { projectId, project: projectName },
|
||||
createdAt: new Date().toISOString(),
|
||||
_optimistic: true
|
||||
};
|
||||
|
||||
this.transaction('createSession:optimistic', (state) => {
|
||||
// Add to sessions
|
||||
state.sessions.set(tempSessionId, tempSession);
|
||||
|
||||
// Add to project
|
||||
const project = state.projects.get(projectId.replace('project-', ''));
|
||||
if (project) {
|
||||
project.sessions.unshift(tempSession);
|
||||
project.activeSessionId = tempSessionId;
|
||||
}
|
||||
|
||||
this.emit('session:added:optimistic', { session: tempSession, projectId });
|
||||
});
|
||||
|
||||
try {
|
||||
// Actual API call
|
||||
const response = await fetch('/claude/api/claude/sessions', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
workingDir,
|
||||
metadata: { projectId, project: projectName }
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
|
||||
const data = await response.json();
|
||||
const realSession = data.session || data;
|
||||
|
||||
// Replace optimistic session with real session
|
||||
return this.transaction('createSession:commit', (state) => {
|
||||
// Remove temp session
|
||||
state.sessions.delete(tempSessionId);
|
||||
|
||||
// Add real session
|
||||
state.sessions.set(realSession.id, realSession);
|
||||
|
||||
// Update project
|
||||
const project = state.projects.get(projectId.replace('project-', ''));
|
||||
if (project) {
|
||||
const tempIndex = project.sessions.findIndex(s => s.id === tempSessionId);
|
||||
if (tempIndex !== -1) {
|
||||
project.sessions[tempIndex] = realSession;
|
||||
} else {
|
||||
project.sessions.unshift(realSession);
|
||||
}
|
||||
project.activeSessionId = realSession.id;
|
||||
}
|
||||
|
||||
this.emit('session:added:confirmed', { session: realSession, projectId });
|
||||
|
||||
return realSession;
|
||||
});
|
||||
} catch (error) {
|
||||
// Rollback: Remove optimistic session
|
||||
this.transaction('createSession:rollback', (state) => {
|
||||
state.sessions.delete(tempSessionId);
|
||||
|
||||
const project = state.projects.get(projectId.replace('project-', ''));
|
||||
if (project) {
|
||||
project.sessions = project.sessions.filter(s => s.id !== tempSessionId);
|
||||
}
|
||||
|
||||
this.emit('session:added:rollback', { tempSessionId, error });
|
||||
});
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Group sessions by working directory (project)
|
||||
*/
|
||||
groupSessionsByProject(sessions) {
|
||||
const grouped = new Map();
|
||||
|
||||
sessions.forEach(session => {
|
||||
const dir = session.workingDir || 'default';
|
||||
let projectKey;
|
||||
|
||||
// Handle virtual working directories
|
||||
if (dir.startsWith('/virtual/projects/')) {
|
||||
projectKey = dir.replace('/virtual/projects/', '');
|
||||
} else {
|
||||
projectKey = dir.replace(/\//g, '-').replace(/^-/, '') || 'default';
|
||||
}
|
||||
|
||||
if (!grouped.has(projectKey)) {
|
||||
const projectName = dir.split('/').pop() || 'Default';
|
||||
grouped.set(projectKey, {
|
||||
id: `project-${projectKey}`,
|
||||
name: this.deduplicateProjectName(projectName, grouped),
|
||||
workingDir: dir,
|
||||
sessions: [],
|
||||
activeSessionId: null,
|
||||
manuallyCreated: dir.startsWith('/virtual/projects/'),
|
||||
isVirtual: dir.startsWith('/virtual/projects/')
|
||||
});
|
||||
}
|
||||
|
||||
const project = grouped.get(projectKey);
|
||||
project.sessions.push(session);
|
||||
});
|
||||
|
||||
// Sort sessions and set active
|
||||
grouped.forEach(project => {
|
||||
project.sessions.sort((a, b) => {
|
||||
const dateA = new Date(a.lastActivity || a.createdAt || 0);
|
||||
const dateB = new Date(b.lastActivity || b.createdAt || 0);
|
||||
return dateB - dateA;
|
||||
});
|
||||
if (project.sessions.length > 0) {
|
||||
project.activeSessionId = project.sessions[0].id;
|
||||
}
|
||||
});
|
||||
|
||||
return grouped;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load manual project metadata from localStorage
|
||||
*/
|
||||
loadManualProjectMetadata() {
|
||||
try {
|
||||
const stored = localStorage.getItem('claude_ide_projects');
|
||||
if (!stored) return new Map();
|
||||
|
||||
const projectsData = JSON.parse(stored);
|
||||
const metadata = new Map();
|
||||
|
||||
projectsData.forEach(projectData => {
|
||||
const key = projectData.id.replace('project-', '');
|
||||
// Only store metadata, not sessions (sessions come from API)
|
||||
metadata.set(key, {
|
||||
id: projectData.id,
|
||||
name: projectData.name,
|
||||
workingDir: projectData.workingDir,
|
||||
manuallyCreated: true,
|
||||
createdAt: projectData.createdAt
|
||||
});
|
||||
});
|
||||
|
||||
return metadata;
|
||||
} catch (error) {
|
||||
console.error('[State] Error loading manual project metadata:', error);
|
||||
return new Map();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge manual projects with API-derived projects
|
||||
*/
|
||||
mergeManualProjects(grouped, manualMetadata) {
|
||||
manualMetadata.forEach((meta, key) => {
|
||||
if (!grouped.has(key)) {
|
||||
// Project exists in metadata but not in API (no sessions yet)
|
||||
grouped.set(key, {
|
||||
...meta,
|
||||
sessions: [],
|
||||
activeSessionId: null
|
||||
});
|
||||
} else {
|
||||
// Project exists in both - prefer manual metadata for name/description
|
||||
const existing = grouped.get(key);
|
||||
existing.name = meta.name;
|
||||
existing.manuallyCreated = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
deduplicateProjectName(name, existingProjects) {
|
||||
const names = Array.from(existingProjects.values()).map(p => p.name);
|
||||
let finalName = name;
|
||||
let counter = 2;
|
||||
|
||||
while (names.includes(finalName)) {
|
||||
finalName = `${name} (${counter})`;
|
||||
counter++;
|
||||
}
|
||||
|
||||
return finalName;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Component 2: Enhanced Real-Time Logger
|
||||
|
||||
### New File: real-time-logger.js
|
||||
```javascript
|
||||
/**
|
||||
* Real-Time Logger - Centralized logging with server integration
|
||||
* Provides debugging, monitoring, and alerting capabilities
|
||||
*/
|
||||
|
||||
class RealTimeLogger {
|
||||
constructor() {
|
||||
this.logs = [];
|
||||
this.maxLogs = 1000;
|
||||
this.endpoint = '/claude/api/log-event';
|
||||
this.errorCounts = new Map();
|
||||
this.alertThreshold = 5; // Alert after 5 similar errors
|
||||
}
|
||||
|
||||
/**
|
||||
* Log an event with context
|
||||
*/
|
||||
log(category, level, message, data = {}) {
|
||||
const entry = {
|
||||
timestamp: Date.now(),
|
||||
isoTime: new Date().toISOString(),
|
||||
category,
|
||||
level,
|
||||
message,
|
||||
data,
|
||||
url: window.location.href,
|
||||
sessionId: window.attachedSessionId || null,
|
||||
projectId: window.projectManager?.activeProjectId || null
|
||||
};
|
||||
|
||||
// Add to in-memory log
|
||||
this.logs.push(entry);
|
||||
if (this.logs.length > this.maxLogs) {
|
||||
this.logs.shift();
|
||||
}
|
||||
|
||||
// Track error patterns
|
||||
if (level === 'error') {
|
||||
this.trackError(entry);
|
||||
}
|
||||
|
||||
// Send to server
|
||||
this.sendToServer(entry);
|
||||
|
||||
// Console output with color coding
|
||||
this.consoleOutput(entry);
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
trackError(entry) {
|
||||
const key = `${entry.category}:${entry.message}`;
|
||||
const count = (this.errorCounts.get(key) || 0) + 1;
|
||||
this.errorCounts.set(key, count);
|
||||
|
||||
if (count >= this.alertThreshold) {
|
||||
this.alert(`Recurring error: ${entry.message}`, { count, entry });
|
||||
}
|
||||
}
|
||||
|
||||
async sendToServer(entry) {
|
||||
try {
|
||||
// Batch multiple logs if possible
|
||||
if (this.pendingSend) {
|
||||
this.pendingBatch.push(entry);
|
||||
return;
|
||||
}
|
||||
|
||||
this.pendingBatch = [entry];
|
||||
this.pendingSend = true;
|
||||
|
||||
// Small delay to batch more logs
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
await fetch(this.endpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
events: this.pendingBatch,
|
||||
pageInfo: {
|
||||
pathname: window.location.pathname,
|
||||
search: window.location.search,
|
||||
hash: window.location.hash
|
||||
}
|
||||
})
|
||||
});
|
||||
} catch (error) {
|
||||
// Silent fail - don't log to avoid infinite loop
|
||||
} finally {
|
||||
this.pendingSend = false;
|
||||
this.pendingBatch = [];
|
||||
}
|
||||
}
|
||||
|
||||
consoleOutput(entry) {
|
||||
const prefix = `[${entry.category}] ${entry.level.toUpperCase()}`;
|
||||
const styles = {
|
||||
info: 'color: #0066cc',
|
||||
warn: 'color: #ff9900',
|
||||
error: 'color: #cc0000',
|
||||
success: 'color: #00cc66',
|
||||
debug: 'color: #888888'
|
||||
};
|
||||
|
||||
const style = styles[entry.level] || styles.info;
|
||||
|
||||
if (entry.level === 'error') {
|
||||
console.error(`%c${prefix}`, style, entry.message, entry.data);
|
||||
} else if (entry.level === 'warn') {
|
||||
console.warn(`%c${prefix}`, style, entry.message, entry.data);
|
||||
} else {
|
||||
console.log(`%c${prefix}`, style, entry.message, entry.data);
|
||||
}
|
||||
}
|
||||
|
||||
alert(message, data) {
|
||||
// Show visual alert
|
||||
this.showAlertIndicator(message);
|
||||
|
||||
// Log critical
|
||||
this.log('alert', 'error', message, data);
|
||||
}
|
||||
|
||||
showAlertIndicator(message) {
|
||||
const existing = document.getElementById('rt-logger-alert');
|
||||
if (existing) existing.remove();
|
||||
|
||||
const alert = document.createElement('div');
|
||||
alert.id = 'rt-logger-alert';
|
||||
alert.innerHTML = `
|
||||
<div style="position:fixed; bottom:20px; right:20px; z-index:99999;
|
||||
background:linear-gradient(135deg,#ff6b6b,#ee5a6f);
|
||||
color:white; padding:16px 20px; border-radius:8px;
|
||||
box-shadow:0 4px 12px rgba(0,0,0,0.3);
|
||||
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
|
||||
max-width:300px;">
|
||||
<div style="font-weight:600; margin-bottom:4px;">Recurring Error Detected</div>
|
||||
<div style="font-size:13px; opacity:0.9;">${message}</div>
|
||||
<button onclick="this.parentElement.remove()" style="margin-top:8px;
|
||||
background:rgba(255,255,255,0.2); border:none;
|
||||
color:white; padding:4px 12px; border-radius:4px;
|
||||
cursor:pointer; font-size:12px;">Dismiss</button>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(alert);
|
||||
}
|
||||
|
||||
// Convenience methods
|
||||
info(category, message, data) { return this.log(category, 'info', message, data); }
|
||||
warn(category, message, data) { return this.log(category, 'warn', message, data); }
|
||||
error(category, message, data) { return this.log(category, 'error', message, data); }
|
||||
success(category, message, data) { return this.log(category, 'success', message, data); }
|
||||
debug(category, message, data) { return this.log(category, 'debug', message, data); }
|
||||
|
||||
/**
|
||||
* Get recent logs
|
||||
*/
|
||||
getLogs(filter = {}) {
|
||||
let filtered = this.logs;
|
||||
|
||||
if (filter.category) {
|
||||
filtered = filtered.filter(l => l.category === filter.category);
|
||||
}
|
||||
if (filter.level) {
|
||||
filtered = filtered.filter(l => l.level === filter.level);
|
||||
}
|
||||
if (filter.since) {
|
||||
filtered = filtered.filter(l => l.timestamp >= filter.since);
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
/**
|
||||
* Export logs for analysis
|
||||
*/
|
||||
exportLogs() {
|
||||
return JSON.stringify({
|
||||
exportTime: new Date().toISOString(),
|
||||
url: window.location.href,
|
||||
totalLogs: this.logs.length,
|
||||
logs: this.logs,
|
||||
errorPatterns: Object.fromEntries(this.errorCounts)
|
||||
}, null, 2);
|
||||
}
|
||||
}
|
||||
|
||||
// Global instance
|
||||
window.rtLogger = new RealTimeLogger();
|
||||
|
||||
// Auto-export logs on error
|
||||
window.addEventListener('error', () => {
|
||||
sessionStorage.setItem('rtLogger_logs', window.rtLogger.exportLogs());
|
||||
});
|
||||
|
||||
// Export critical logs on unload
|
||||
window.addEventListener('beforeunload', () => {
|
||||
const errorLogs = window.rtLogger.getLogs({ level: 'error' });
|
||||
if (errorLogs.length > 0) {
|
||||
sessionStorage.setItem('rtLogger_errors', JSON.stringify(errorLogs));
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Component 3: Fixed SSE Client
|
||||
|
||||
### Modified: sse-client.js
|
||||
```javascript
|
||||
connect(sessionId) {
|
||||
if (this.eventSource) {
|
||||
this.disconnect();
|
||||
}
|
||||
|
||||
this.currentSessionId = sessionId;
|
||||
const url = `/claude/api/session/${encodeURIComponent(sessionId)}/events`;
|
||||
|
||||
window.rtLogger?.info('SSE', 'Connecting to SSE', { sessionId, url });
|
||||
|
||||
try {
|
||||
this.eventSource = new EventSource(url);
|
||||
|
||||
// CRITICAL FIX: Check if EventSource was successfully created
|
||||
if (!this.eventSource) {
|
||||
throw new Error('EventSource construction returned null');
|
||||
}
|
||||
|
||||
// CRITICAL FIX: Check readyState before setting handlers
|
||||
if (this.eventSource.readyState === EventSource.CLOSED) {
|
||||
throw new Error('EventSource closed immediately');
|
||||
}
|
||||
|
||||
// Now safe to set handlers
|
||||
this.eventSource.onopen = () => {
|
||||
window.rtLogger?.success('SSE', 'Connected', { sessionId });
|
||||
this.reconnectAttempts = 0;
|
||||
this.emit('connected', { sessionId });
|
||||
};
|
||||
|
||||
this.eventSource.onerror = (error) => {
|
||||
window.rtLogger?.error('SSE', 'Connection error', {
|
||||
sessionId,
|
||||
readyState: this.eventSource?.readyState,
|
||||
error: error?.toString()
|
||||
});
|
||||
// ... rest of error handling
|
||||
};
|
||||
|
||||
this.registerEventListeners();
|
||||
|
||||
} catch (error) {
|
||||
window.rtLogger?.error('SSE', 'Failed to create EventSource', {
|
||||
sessionId,
|
||||
error: error.message
|
||||
});
|
||||
this.emit('error', { sessionId, error: error.message });
|
||||
this.handleReconnect();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Component 4: Defensive Array Operations
|
||||
|
||||
### Modified: chat-enhanced.js
|
||||
```javascript
|
||||
async function loadChatHistory(sessionsToRender = null) {
|
||||
try {
|
||||
const historyList = document.getElementById('chat-history-list');
|
||||
if (!historyList) {
|
||||
window.rtLogger?.warn('ChatHistory', 'Element not found', { element: 'chat-history-list' });
|
||||
return;
|
||||
}
|
||||
|
||||
// CRITICAL FIX: Always ensure we have an array
|
||||
let allSessions = [];
|
||||
|
||||
if (Array.isArray(sessionsToRender)) {
|
||||
// Use provided sessions
|
||||
allSessions = sessionsToRender;
|
||||
window.rtLogger?.info('ChatHistory', 'Rendering provided sessions', { count: allSessions.length });
|
||||
} else if (window.projectManager?.activeProjectId) {
|
||||
// Use active project sessions
|
||||
const activeProject = window.projectManager.projects.get(
|
||||
window.projectManager.activeProjectId.replace('project-', '')
|
||||
);
|
||||
|
||||
if (activeProject?.sessions && Array.isArray(activeProject.sessions)) {
|
||||
allSessions = activeProject.sessions;
|
||||
window.rtLogger?.info('ChatHistory', 'Using project sessions', {
|
||||
project: activeProject.name,
|
||||
count: allSessions.length
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// CRITICAL FIX: Validate array before sorting
|
||||
if (!Array.isArray(allSessions)) {
|
||||
window.rtLogger?.warn('ChatHistory', 'Sessions is not an array', { type: typeof allSessions });
|
||||
allSessions = [];
|
||||
}
|
||||
|
||||
// Safe sort with validation
|
||||
allSessions.sort((a, b) => {
|
||||
const dateA = new Date(a?.createdAt || a?.created_at || 0);
|
||||
const dateB = new Date(b?.createdAt || b?.created_at || 0);
|
||||
return dateB - dateA;
|
||||
});
|
||||
|
||||
// Safe render with validation
|
||||
if (allSessions.length === 0) {
|
||||
historyList.innerHTML = '<div class="chat-history-empty">No sessions in this project</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
historyList.innerHTML = allSessions.map(session => {
|
||||
// Validate session object
|
||||
if (!session?.id) {
|
||||
window.rtLogger?.warn('ChatHistory', 'Invalid session object', { session });
|
||||
return '';
|
||||
}
|
||||
|
||||
// ... render code
|
||||
}).join('');
|
||||
|
||||
window.rtLogger?.success('ChatHistory', 'Rendered sessions', { count: allSessions.length });
|
||||
|
||||
} catch (error) {
|
||||
window.rtLogger?.error('ChatHistory', 'Failed to load history', { error: error.message });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Component 5: Virtual Working Directory Validator
|
||||
|
||||
### New: project-validator.js
|
||||
```javascript
|
||||
/**
|
||||
* Project Validator - Ensures project consistency
|
||||
*/
|
||||
|
||||
class ProjectValidator {
|
||||
/**
|
||||
* Validate and fix project working directory
|
||||
*/
|
||||
static validateProject(project) {
|
||||
const issues = [];
|
||||
const fixes = [];
|
||||
|
||||
// Check if project is virtual but has wrong workingDir
|
||||
if (project.manuallyCreated && project.isVirtual) {
|
||||
const expectedWorkingDir = `/virtual/projects/${project.id.replace('project-', '')}`;
|
||||
|
||||
if (project.workingDir !== expectedWorkingDir) {
|
||||
issues.push({
|
||||
type: 'working_dir_mismatch',
|
||||
project: project.name,
|
||||
expected: expectedWorkingDir,
|
||||
actual: project.workingDir
|
||||
});
|
||||
|
||||
fixes.push({
|
||||
type: 'fix_working_dir',
|
||||
projectId: project.id,
|
||||
workingDir: expectedWorkingDir
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { issues, fixes };
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix all project issues
|
||||
*/
|
||||
static fixProjects(projects) {
|
||||
const results = [];
|
||||
|
||||
projects.forEach((project, key) => {
|
||||
const validation = this.validateProject(project);
|
||||
|
||||
if (validation.issues.length > 0) {
|
||||
window.rtLogger?.warn('ProjectValidator', 'Found project issues', {
|
||||
project: project.name,
|
||||
issues: validation.issues
|
||||
});
|
||||
|
||||
validation.fixes.forEach(fix => {
|
||||
if (fix.type === 'fix_working_dir') {
|
||||
project.workingDir = fix.workingDir;
|
||||
results.push({ project: project.name, fixed: fix.workingDir });
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (results.length > 0) {
|
||||
window.rtLogger?.success('ProjectValidator', 'Fixed projects', { results });
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate existing localStorage projects
|
||||
*/
|
||||
static migrateLocalStorage() {
|
||||
try {
|
||||
const stored = localStorage.getItem('claude_ide_projects');
|
||||
if (!stored) return { migrated: 0 };
|
||||
|
||||
const projectsData = JSON.parse(stored);
|
||||
let migrated = 0;
|
||||
|
||||
const fixed = projectsData.map(project => {
|
||||
if (project.manuallyCreated && !project.workingDir?.startsWith('/virtual/projects/')) {
|
||||
const projectKey = project.id.replace('project-', '');
|
||||
project.workingDir = `/virtual/projects/${projectKey}`;
|
||||
project.isVirtual = true;
|
||||
migrated++;
|
||||
|
||||
window.rtLogger?.info('ProjectValidator', 'Migrated project', {
|
||||
name: project.name,
|
||||
newWorkingDir: project.workingDir
|
||||
});
|
||||
}
|
||||
return project;
|
||||
});
|
||||
|
||||
localStorage.setItem('claude_ide_projects', JSON.stringify(fixed));
|
||||
|
||||
return { migrated, projects: fixed };
|
||||
} catch (error) {
|
||||
window.rtLogger?.error('ProjectValidator', 'Migration failed', { error: error.message });
|
||||
return { migrated: 0, error: error.message };
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Order
|
||||
|
||||
### Phase 3: Implementation (Pending AI Engineer Approval)
|
||||
|
||||
1. **Step 1: Install Real-Time Logger** (Low Risk)
|
||||
- Add real-time-logger.js to index.html
|
||||
- Test logging functionality
|
||||
- Verify server endpoint exists
|
||||
|
||||
2. **Step 2: Fix SSE Client** (Low Risk)
|
||||
- Add null checks to sse-client.js
|
||||
- Test connection with valid session
|
||||
- Test connection with invalid session
|
||||
|
||||
3. **Step 3: Add Array Validation** (Low Risk)
|
||||
- Add guards to chat-enhanced.js
|
||||
- Add guards to project-manager.js
|
||||
- Test with empty sessions array
|
||||
- Test with undefined sessions
|
||||
|
||||
4. **Step 4: Add Project Validator** (Medium Risk)
|
||||
- Create project-validator.js
|
||||
- Run migration once
|
||||
- Validate existing projects
|
||||
|
||||
5. **Step 5: Implement State Manager** (High Risk)
|
||||
- Create session-state-manager.js
|
||||
- Migrate ProjectManager to use it
|
||||
- Test all state transitions
|
||||
- Verify optimistic updates work
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Unit Tests
|
||||
- SessionStateManager transactions
|
||||
- ProjectValidator migration
|
||||
- Array validation functions
|
||||
|
||||
### Integration Tests
|
||||
- Session creation flow
|
||||
- Project switching flow
|
||||
- Session persistence flow
|
||||
|
||||
### Manual Tests
|
||||
1. Create new project
|
||||
2. Add session to project
|
||||
3. Refresh page
|
||||
4. Verify sessions persist
|
||||
5. Check console for errors
|
||||
|
||||
## Rollback Plan
|
||||
|
||||
If any step fails:
|
||||
1. Revert specific file changes
|
||||
2. Keep existing logger for debugging
|
||||
3. Document what failed and why
|
||||
4. Adjust approach based on findings
|
||||
|
||||
---
|
||||
|
||||
**Status:** Phase 2 Complete - Awaiting AI Engineer Review
|
||||
**Required Approval:** AI Engineer must review and approve before Phase 3 implementation
|
||||
**Review Focus Areas:**
|
||||
1. State management architecture
|
||||
2. Optimistic update pattern
|
||||
3. Error handling strategy
|
||||
4. Migration safety
|
||||
Reference in New Issue
Block a user