@appoly/multiagent-chat 1.0.2 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/renderer.js CHANGED
@@ -1,261 +1,269 @@
1
+ // ═══════════════════════════════════════════════════════════
1
2
  // State
3
+ // ═══════════════════════════════════════════════════════════
4
+
5
+ let sessionState = 'empty'; // 'empty' | 'loaded' | 'active' | 'stopped'
6
+ let currentSessionId = null;
7
+ let currentWorkspace = null; // Project root path
8
+ let sessionsList = []; // Array of session metadata
9
+
2
10
  let currentConfig = null;
3
11
  let agentData = {};
4
12
  let currentAgentTab = null;
5
13
  let terminals = {};
6
- let agentColors = {}; // Map of agent name -> color
7
- let chatMessages = []; // Array of chat messages
8
- let inputLocked = {}; // Map of agent name -> boolean (default true)
9
- let planHasContent = false; // Track if PLAN_FINAL has content
10
- let implementationStarted = false; // Track if implementation has started
14
+ let agentColors = {};
15
+ let chatMessages = [];
16
+ let inputLocked = {};
17
+ let planHasContent = false;
18
+ let implementationStarted = false;
11
19
  let autoScrollEnabled = true;
12
- let currentMainTab = 'chat'; // Track which main tab is active ('chat', 'plan', or 'diff')
13
- let lastDiffData = null; // Cache last diff data
14
- let parsedDiffFiles = []; // Parsed diff data per file
15
- let selectedDiffFile = null; // Currently selected file in diff view (null = all)
16
- let pollingIntervals = []; // Store interval IDs to clear on reset
17
-
18
- // Workspace selection state
19
- let selectedWorkspace = null; // Currently selected workspace path
20
- let recentWorkspaces = []; // Array of recent workspaces
21
- let cliWorkspace = null; // Workspace passed via CLI
22
- let cwdInfo = null; // Current working directory info
20
+ let currentRightTab = 'terminal';
21
+ let lastDiffData = null;
22
+ let parsedDiffFiles = [];
23
+ let selectedDiffFile = null;
24
+ let pollingIntervals = [];
23
25
 
24
26
  const CHAT_SCROLL_THRESHOLD = 40;
25
- const TAB_STORAGE_KEY = 'activeMainTab';
27
+ const LAYOUT_STORAGE_KEY = 'multiagent-layout';
26
28
 
29
+ // ═══════════════════════════════════════════════════════════
27
30
  // DOM Elements
28
- const challengeScreen = document.getElementById('challenge-screen');
29
- const sessionScreen = document.getElementById('session-screen');
30
- const challengeInput = document.getElementById('challenge-input');
31
- const startButton = document.getElementById('start-button');
32
- const configDetails = document.getElementById('config-details');
31
+ // ═══════════════════════════════════════════════════════════
32
+
33
+ const sidebarWorkspaceName = document.getElementById('sidebar-workspace-name');
34
+ const sidebarWorkspacePath = document.getElementById('sidebar-workspace-path');
33
35
  const newSessionButton = document.getElementById('new-session-button');
34
- const workspacePath = document.getElementById('workspace-path');
35
- const agentTabsContainer = document.getElementById('agent-tabs');
36
- const agentOutputsContainer = document.getElementById('agent-outputs');
36
+ const sessionListEl = document.getElementById('session-list');
37
+ const changeWorkspaceButton = document.getElementById('change-workspace-button');
38
+ const settingsButton = document.getElementById('settings-button');
39
+
40
+ const welcomeView = document.getElementById('welcome-view');
41
+ const sessionView = document.getElementById('session-view');
42
+ const rightPanel = document.getElementById('right-panel');
43
+
44
+ const promptInput = document.getElementById('prompt-input');
45
+ const promptSendBtn = document.getElementById('prompt-send-btn');
46
+ const configDetails = document.getElementById('config-details');
47
+
37
48
  const chatViewer = document.getElementById('chat-viewer');
38
49
  const chatNewMessages = document.getElementById('chat-new-messages');
39
50
  const chatNewMessagesButton = document.getElementById('chat-new-messages-button');
40
51
  const userMessageInput = document.getElementById('user-message-input');
41
52
  const sendMessageButton = document.getElementById('send-message-button');
42
- const planViewer = document.getElementById('plan-viewer');
43
- const startImplementingButton = document.getElementById('start-implementing-button');
44
53
 
45
- // Workspace picker elements
46
- const recentWorkspacesEl = document.getElementById('recent-workspaces');
47
- const browseWorkspaceButton = document.getElementById('browse-workspace-button');
48
- const useCwdButton = document.getElementById('use-cwd-button');
49
- const editConfigButton = document.getElementById('edit-config-button');
50
- const selectedWorkspaceInfo = document.getElementById('selected-workspace-info');
51
- const selectedWorkspacePathEl = document.getElementById('selected-workspace-path');
52
- const cliBadge = document.getElementById('cli-badge');
53
-
54
- // Main tabs elements
55
- const mainTabChat = document.getElementById('main-tab-chat');
54
+ const agentTabsContainer = document.getElementById('agent-tabs');
55
+ const agentOutputsContainer = document.getElementById('agent-outputs');
56
+
57
+ const mainTabTerminal = document.getElementById('main-tab-terminal');
56
58
  const mainTabPlan = document.getElementById('main-tab-plan');
57
59
  const mainTabDiff = document.getElementById('main-tab-diff');
58
- const chatTabContent = document.getElementById('chat-tab-content');
60
+ const terminalTabContent = document.getElementById('terminal-tab-content');
59
61
  const planTabContent = document.getElementById('plan-tab-content');
60
62
  const diffTabContent = document.getElementById('diff-tab-content');
61
63
 
62
- // Diff elements
64
+ const planViewer = document.getElementById('plan-viewer');
65
+ const startImplementingButton = document.getElementById('start-implementing-button');
66
+ const refreshPlanButton = document.getElementById('refresh-plan-button');
67
+
63
68
  const diffBadge = document.getElementById('diff-badge');
64
69
  const diffStats = document.getElementById('diff-stats');
65
70
  const diffContent = document.getElementById('diff-content');
66
71
  const diffUntracked = document.getElementById('diff-untracked');
67
72
  const diffFileListItems = document.getElementById('diff-file-list-items');
68
73
  const refreshDiffButton = document.getElementById('refresh-diff-button');
69
- const refreshPlanButton = document.getElementById('refresh-plan-button');
70
74
 
71
- // Modal elements
72
75
  const implementationModal = document.getElementById('implementation-modal');
73
76
  const agentSelectionContainer = document.getElementById('agent-selection');
74
77
  const modalCancelButton = document.getElementById('modal-cancel');
75
78
  const modalStartButton = document.getElementById('modal-start');
76
79
 
77
- // New session modal elements
78
- const newSessionModal = document.getElementById('new-session-modal');
79
- const newSessionCancelButton = document.getElementById('new-session-cancel');
80
- const newSessionConfirmButton = document.getElementById('new-session-confirm');
80
+ // ═══════════════════════════════════════════════════════════
81
+ // Helpers
82
+ // ═══════════════════════════════════════════════════════════
83
+
84
+ function getTerminalTheme() {
85
+ const style = getComputedStyle(document.documentElement);
86
+ return {
87
+ background: style.getPropertyValue('--terminal-bg').trim() || '#0b0e11',
88
+ foreground: style.getPropertyValue('--terminal-fg').trim() || '#e0e0e0',
89
+ cursor: style.getPropertyValue('--terminal-cursor').trim() || '#e0e0e0',
90
+ selectionBackground: style.getPropertyValue('--terminal-selection').trim() || '#264f78',
91
+ };
92
+ }
81
93
 
94
+ // ═══════════════════════════════════════════════════════════
82
95
  // Initialize
83
- async function initialize() {
84
- console.log('Initializing app...');
96
+ // ═══════════════════════════════════════════════════════════
97
+
98
+ async function initializeApp() {
99
+ // Set platform class for platform-specific CSS (e.g., macOS hidden title bar)
100
+ if (window.electronAPI?.platform) {
101
+ document.body.classList.add(`platform-${window.electronAPI.platform}`);
102
+ }
85
103
 
86
- // Check if xterm is available
104
+ console.log('Initializing app...');
87
105
  console.log('Terminal available?', typeof Terminal !== 'undefined');
88
106
  console.log('FitAddon available?', typeof FitAddon !== 'undefined');
89
- console.log('FitAddon object:', FitAddon);
90
107
  console.log('marked available?', typeof marked !== 'undefined');
91
108
 
92
- // Check if electronAPI is available
93
109
  if (!window.electronAPI) {
94
110
  console.error('electronAPI not available!');
95
111
  configDetails.innerHTML = '<span style="color: #dc3545;">Error: Electron API not available</span>';
96
112
  return;
97
113
  }
98
114
 
99
- console.log('electronAPI available:', Object.keys(window.electronAPI));
100
-
101
115
  try {
102
- console.log('Loading config...');
116
+ // Load config
103
117
  currentConfig = await window.electronAPI.loadConfig();
104
- console.log('Config loaded:', currentConfig);
105
118
  displayConfig();
106
119
 
107
- // Load workspace info
108
- await loadWorkspaceInfo();
120
+ // Get CWD as workspace
121
+ const wsInfo = await window.electronAPI.getCurrentWorkspace();
122
+ currentWorkspace = wsInfo.path;
123
+
124
+ // Update sidebar
125
+ sidebarWorkspaceName.textContent = wsInfo.name;
126
+ sidebarWorkspacePath.textContent = wsInfo.path;
127
+ sidebarWorkspacePath.title = wsInfo.path;
128
+
129
+ // Load sessions for this workspace
130
+ await loadSessionsList();
131
+
132
+ // Show welcome view
133
+ showWelcomeView();
109
134
  } catch (error) {
110
- console.error('Error loading config:', error);
111
- configDetails.innerHTML = `<span style="color: #dc3545;">Error loading configuration: ${error.message}</span>`;
135
+ console.error('Error initializing:', error);
136
+ configDetails.innerHTML = `<span style="color: #dc3545;">Error: ${error.message}</span>`;
112
137
  }
113
138
  }
114
139
 
115
- // Load workspace info: CLI workspace, recent workspaces, current directory
116
- async function loadWorkspaceInfo() {
117
- try {
118
- // Check for CLI workspace
119
- cliWorkspace = await window.electronAPI.getCliWorkspace();
140
+ // ═══════════════════════════════════════════════════════════
141
+ // View Management
142
+ // ═══════════════════════════════════════════════════════════
143
+
144
+ function showWelcomeView() {
145
+ welcomeView.style.display = 'flex';
146
+ sessionView.style.display = 'none';
147
+ rightPanel.style.display = 'none';
148
+ sessionState = 'empty';
149
+ currentSessionId = null;
150
+ promptInput.value = '';
151
+ promptInput.focus();
152
+
153
+ // Deselect all session items
154
+ sessionListEl.querySelectorAll('.session-item').forEach(el => {
155
+ el.classList.remove('active');
156
+ });
157
+ }
120
158
 
121
- // Load recent workspaces
122
- recentWorkspaces = await window.electronAPI.getRecentWorkspaces();
159
+ function showSessionView() {
160
+ welcomeView.style.display = 'none';
161
+ sessionView.style.display = 'flex';
162
+ }
163
+
164
+ function showRightPanel() {
165
+ rightPanel.style.display = 'flex';
166
+ // Restore layout after showing
167
+ requestAnimationFrame(() => {
168
+ restoreLayout();
169
+ refitTerminals();
170
+ });
171
+ }
123
172
 
124
- // Get current directory info
125
- cwdInfo = await window.electronAPI.getCurrentDirectory();
173
+ function hideRightPanel() {
174
+ rightPanel.style.display = 'none';
175
+ }
126
176
 
127
- // If CLI workspace provided, auto-select it
128
- if (cliWorkspace) {
129
- selectWorkspace(cliWorkspace, true);
130
- } else if (recentWorkspaces.length > 0 && recentWorkspaces[0].exists) {
131
- // Auto-select most recent valid workspace
132
- selectWorkspace(recentWorkspaces[0].path, false);
133
- }
177
+ // ═══════════════════════════════════════════════════════════
178
+ // Session List (Sidebar)
179
+ // ═══════════════════════════════════════════════════════════
134
180
 
135
- // Render workspace list
136
- renderWorkspaceList();
181
+ async function loadSessionsList() {
182
+ if (!currentWorkspace) return;
137
183
 
138
- // Show/hide "Use current directory" button
139
- if (cwdInfo && cwdInfo.isUsable) {
140
- useCwdButton.style.display = 'flex';
141
- } else {
142
- useCwdButton.style.display = 'none';
143
- }
184
+ try {
185
+ sessionsList = await window.electronAPI.getSessionsForWorkspace(currentWorkspace);
186
+ renderSessionsList();
144
187
  } catch (error) {
145
- console.error('Error loading workspace info:', error);
146
- recentWorkspacesEl.innerHTML = '<li class="workspace-empty">Error loading workspaces</li>';
188
+ console.error('Error loading sessions:', error);
189
+ sessionsList = [];
190
+ renderSessionsList();
147
191
  }
148
192
  }
149
193
 
150
- // Render the workspace list
151
- function renderWorkspaceList() {
152
- if (recentWorkspaces.length === 0) {
153
- recentWorkspacesEl.innerHTML = `
154
- <li class="workspace-empty">
155
- No recent workspaces.<br>
156
- Browse for a folder to get started.
157
- </li>
158
- `;
194
+ function renderSessionsList() {
195
+ if (sessionsList.length === 0) {
196
+ sessionListEl.innerHTML = '<div class="session-list-empty">No sessions yet</div>';
159
197
  return;
160
198
  }
161
199
 
162
- recentWorkspacesEl.innerHTML = recentWorkspaces.map(ws => {
163
- const isSelected = selectedWorkspace === ws.path;
164
- const isMissing = !ws.exists;
165
- const classes = ['workspace-item'];
166
- if (isSelected) classes.push('selected');
167
- if (isMissing) classes.push('missing');
168
-
169
- const timeAgo = formatRelativeTime(ws.lastUsed);
170
-
171
- let actionsHtml = '';
172
- if (isMissing) {
173
- actionsHtml = `
174
- <div class="workspace-missing-actions">
175
- <button class="remove-btn" data-path="${escapeHtml(ws.path)}">Remove</button>
176
- <button class="locate-btn" data-path="${escapeHtml(ws.path)}">Locate</button>
177
- </div>
178
- `;
200
+ // Group by time
201
+ const now = new Date();
202
+ const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
203
+ const yesterday = new Date(today - 86400000);
204
+ const weekAgo = new Date(today - 7 * 86400000);
205
+
206
+ const groups = {
207
+ today: [],
208
+ yesterday: [],
209
+ thisWeek: [],
210
+ older: []
211
+ };
212
+
213
+ for (const session of sessionsList) {
214
+ const date = new Date(session.lastActiveAt || session.createdAt);
215
+ if (date >= today) {
216
+ groups.today.push(session);
217
+ } else if (date >= yesterday) {
218
+ groups.yesterday.push(session);
219
+ } else if (date >= weekAgo) {
220
+ groups.thisWeek.push(session);
221
+ } else {
222
+ groups.older.push(session);
179
223
  }
224
+ }
225
+
226
+ let html = '';
180
227
 
181
- return `
182
- <li class="${classes.join(' ')}" data-path="${escapeHtml(ws.path)}" data-exists="${ws.exists}">
183
- <div class="workspace-item-header">
184
- <span class="workspace-name">${escapeHtml(ws.name)}</span>
185
- ${isMissing ? '<span class="workspace-badge missing">Missing</span>' : ''}
228
+ const renderGroup = (label, sessions) => {
229
+ if (sessions.length === 0) return '';
230
+ let groupHtml = `<div class="session-group-label">${label}</div>`;
231
+ for (const session of sessions) {
232
+ const isActive = session.id === currentSessionId;
233
+ const statusClass = session.status === 'active' ? 'active' : '';
234
+ const statusTitle = session.status === 'active' ? 'Session active' : 'Session completed';
235
+ const timeStr = formatRelativeTime(session.lastActiveAt || session.createdAt);
236
+
237
+ groupHtml += `
238
+ <div class="session-item ${isActive ? 'active' : ''}" data-session-id="${escapeHtml(session.id)}">
239
+ <div class="session-item-title">${escapeHtml(session.title)}</div>
240
+ <div class="session-item-meta">
241
+ <span class="session-item-status ${statusClass}" title="${statusTitle}"></span>
242
+ <span>${timeStr}</span>
243
+ <span>${session.messageCount || 0} msgs</span>
244
+ </div>
186
245
  </div>
187
- <span class="workspace-path" title="${escapeHtml(ws.path)}">${escapeHtml(ws.path)}</span>
188
- <span class="workspace-time">${timeAgo}</span>
189
- ${actionsHtml}
190
- </li>
191
- `;
192
- }).join('');
246
+ `;
247
+ }
248
+ return groupHtml;
249
+ };
193
250
 
194
- // Add click handlers
195
- recentWorkspacesEl.querySelectorAll('.workspace-item').forEach(item => {
196
- const path = item.dataset.path;
197
- const exists = item.dataset.exists === 'true';
198
-
199
- // Main item click (only if exists)
200
- item.addEventListener('click', (e) => {
201
- // Don't select if clicking on action buttons
202
- if (e.target.classList.contains('remove-btn') || e.target.classList.contains('locate-btn')) {
203
- return;
204
- }
205
- if (exists) {
206
- selectWorkspace(path, false);
207
- renderWorkspaceList();
208
- }
209
- });
210
- });
251
+ html += renderGroup('Today', groups.today);
252
+ html += renderGroup('Yesterday', groups.yesterday);
253
+ html += renderGroup('This Week', groups.thisWeek);
254
+ html += renderGroup('Older', groups.older);
211
255
 
212
- // Add action button handlers
213
- recentWorkspacesEl.querySelectorAll('.remove-btn').forEach(btn => {
214
- btn.addEventListener('click', async (e) => {
215
- e.stopPropagation();
216
- const path = btn.dataset.path;
217
- await window.electronAPI.removeRecentWorkspace(path);
218
- recentWorkspaces = await window.electronAPI.getRecentWorkspaces();
219
- renderWorkspaceList();
220
- });
221
- });
256
+ sessionListEl.innerHTML = html;
222
257
 
223
- recentWorkspacesEl.querySelectorAll('.locate-btn').forEach(btn => {
224
- btn.addEventListener('click', async (e) => {
225
- e.stopPropagation();
226
- const oldPath = btn.dataset.path;
227
- const result = await window.electronAPI.browseForWorkspace();
228
- if (!result.canceled) {
229
- await window.electronAPI.updateRecentWorkspacePath(oldPath, result.path);
230
- recentWorkspaces = await window.electronAPI.getRecentWorkspaces();
231
- selectWorkspace(result.path, false);
232
- renderWorkspaceList();
233
- }
258
+ // Add click handlers
259
+ sessionListEl.querySelectorAll('.session-item').forEach(item => {
260
+ item.addEventListener('click', () => {
261
+ const sessionId = item.dataset.sessionId;
262
+ handleLoadSession(sessionId);
234
263
  });
235
264
  });
236
265
  }
237
266
 
238
- // Select a workspace
239
- function selectWorkspace(path, fromCli = false) {
240
- selectedWorkspace = path;
241
-
242
- // Update UI
243
- selectedWorkspaceInfo.style.display = 'flex';
244
- selectedWorkspacePathEl.textContent = path;
245
- selectedWorkspacePathEl.title = path;
246
-
247
- if (fromCli) {
248
- cliBadge.style.display = 'inline';
249
- } else {
250
- cliBadge.style.display = 'none';
251
- }
252
-
253
- // Enable start button
254
- startButton.disabled = false;
255
- startButton.textContent = 'Start Session';
256
- }
257
-
258
- // Format relative time (e.g., "2 hours ago", "Yesterday")
259
267
  function formatRelativeTime(isoString) {
260
268
  const date = new Date(isoString);
261
269
  const now = new Date();
@@ -265,121 +273,238 @@ function formatRelativeTime(isoString) {
265
273
  const diffDays = Math.floor(diffMs / 86400000);
266
274
 
267
275
  if (diffMins < 1) return 'Just now';
268
- if (diffMins < 60) return `${diffMins} min${diffMins > 1 ? 's' : ''} ago`;
269
- if (diffHours < 24) return `${diffHours} hour${diffHours > 1 ? 's' : ''} ago`;
276
+ if (diffMins < 60) return `${diffMins}m ago`;
277
+ if (diffHours < 24) return `${diffHours}h ago`;
270
278
  if (diffDays === 1) return 'Yesterday';
271
- if (diffDays < 7) return `${diffDays} days ago`;
279
+ if (diffDays < 7) return `${diffDays}d ago`;
272
280
  return date.toLocaleDateString();
273
281
  }
274
282
 
275
- // Browse for workspace
276
- async function browseForWorkspace() {
277
- const result = await window.electronAPI.browseForWorkspace();
278
- if (!result.canceled) {
279
- selectWorkspace(result.path, false);
280
- // Add to recents immediately
281
- await window.electronAPI.addRecentWorkspace(result.path);
282
- recentWorkspaces = await window.electronAPI.getRecentWorkspaces();
283
- renderWorkspaceList();
283
+ // ═══════════════════════════════════════════════════════════
284
+ // Session Flows
285
+ // ═══════════════════════════════════════════════════════════
286
+
287
+ // Handle sending a message - dispatches based on sessionState
288
+ async function handleSendMessage(text) {
289
+ if (!text.trim()) return;
290
+
291
+ if (sessionState === 'empty') {
292
+ await handleNewSession(text.trim());
293
+ } else if (sessionState === 'loaded') {
294
+ await handleResumeSession(text.trim());
295
+ } else if (sessionState === 'active' || sessionState === 'stopped') {
296
+ await handleSendUserMessage(text.trim());
284
297
  }
285
298
  }
286
299
 
287
- // Use current working directory
288
- async function useCwd() {
289
- if (cwdInfo && cwdInfo.isUsable) {
290
- selectWorkspace(cwdInfo.path, false);
291
- // Add to recents like Browse does
292
- await window.electronAPI.addRecentWorkspace(cwdInfo.path);
293
- recentWorkspaces = await window.electronAPI.getRecentWorkspaces();
294
- renderWorkspaceList();
300
+ // New session: user typed in welcome prompt
301
+ async function handleNewSession(challenge) {
302
+ promptSendBtn.disabled = true;
303
+ promptSendBtn.classList.add('loading');
304
+
305
+ try {
306
+ const result = await window.electronAPI.startSession({
307
+ challenge,
308
+ workspace: currentWorkspace
309
+ });
310
+
311
+ if (result.success) {
312
+ currentSessionId = result.sessionId;
313
+ agentColors = result.colors || {};
314
+ chatMessages = [];
315
+ sessionState = 'active';
316
+
317
+ result.agents.forEach(agent => {
318
+ agentData[agent.name] = {
319
+ ...agentData[agent.name],
320
+ name: agent.name,
321
+ status: agentData[agent.name]?.status || 'starting',
322
+ output: agentData[agent.name]?.output || [],
323
+ use_pty: agent.use_pty
324
+ };
325
+ });
326
+
327
+ // Switch to session view
328
+ showSessionView();
329
+ showRightPanel();
330
+ createAgentTabs(result.agents);
331
+ renderChatMessages();
332
+ startChatPolling();
333
+
334
+ // Refresh sessions list
335
+ await loadSessionsList();
336
+ } else {
337
+ alert(`Failed to start session: ${result.error}`);
338
+ }
339
+ } catch (error) {
340
+ console.error('Error starting session:', error);
341
+ alert('Error starting session. Check console for details.');
342
+ } finally {
343
+ promptSendBtn.disabled = false;
344
+ promptSendBtn.classList.remove('loading');
295
345
  }
296
346
  }
297
347
 
298
- // Open config folder
299
- async function openConfigFolder() {
300
- await window.electronAPI.openConfigFolder();
301
- }
348
+ // Load past session (click sidebar) - dormant, no agents
349
+ async function handleLoadSession(sessionId) {
350
+ try {
351
+ // Stop any running agents first
352
+ if (sessionState === 'active') {
353
+ stopChatPolling();
354
+ await window.electronAPI.resetSession();
355
+ cleanupTerminals();
356
+ }
302
357
 
303
- // Display configuration info
304
- function displayConfig() {
305
- if (!currentConfig) {
306
- configDetails.innerHTML = '<span style="color: #dc3545;">No configuration loaded</span>';
307
- return;
308
- }
358
+ const result = await window.electronAPI.loadSession(currentWorkspace, sessionId);
309
359
 
310
- const agentList = currentConfig.agents.map(a => {
311
- const color = a.color || '#667eea';
312
- return `<span style="color: ${color}">• ${a.name}</span> (${a.command})`;
313
- }).join('<br>');
360
+ if (result.success) {
361
+ currentSessionId = sessionId;
362
+ agentColors = result.colors || {};
363
+ chatMessages = result.messages || [];
364
+ sessionState = 'loaded';
314
365
 
315
- configDetails.innerHTML = `
316
- <strong>Agents:</strong><br>${agentList}
317
- `;
318
- }
366
+ // Show session view, hide right panel (dormant - no agents)
367
+ showSessionView();
368
+ hideRightPanel();
319
369
 
320
- // Start session
321
- async function startSession() {
322
- const challenge = challengeInput.value.trim();
370
+ // Render chat history
371
+ renderChatMessages();
323
372
 
324
- if (!challenge) {
325
- alert('Please enter a challenge for the agents to work on.');
326
- return;
327
- }
373
+ // Update sidebar highlighting
374
+ renderSessionsList();
328
375
 
329
- if (!selectedWorkspace) {
330
- alert('Please select a workspace first.');
331
- return;
376
+ // Set placeholder to indicate resume behavior
377
+ userMessageInput.placeholder = 'Type a message to resume this session with agents...';
378
+ } else {
379
+ console.error('Failed to load session:', result.error);
380
+ }
381
+ } catch (error) {
382
+ console.error('Error loading session:', error);
332
383
  }
384
+ }
333
385
 
334
- startButton.disabled = true;
335
- startButton.textContent = 'Starting...';
386
+ // Resume session: user typed while dormant (loaded state)
387
+ async function handleResumeSession(newMessage) {
388
+ sendMessageButton.disabled = true;
389
+ sendMessageButton.classList.add('loading');
336
390
 
337
391
  try {
338
- const result = await window.electronAPI.startSession({
339
- challenge,
340
- workspace: selectedWorkspace
341
- });
392
+ const result = await window.electronAPI.resumeSession(currentWorkspace, currentSessionId, newMessage);
342
393
 
343
394
  if (result.success) {
344
- // Initialize agent data and colors
345
395
  agentColors = result.colors || {};
346
- chatMessages = []; // Reset chat messages
396
+ sessionState = 'active';
347
397
 
348
398
  result.agents.forEach(agent => {
349
399
  agentData[agent.name] = {
400
+ ...agentData[agent.name],
350
401
  name: agent.name,
351
- status: 'starting',
352
- output: [],
402
+ status: agentData[agent.name]?.status || 'starting',
403
+ output: agentData[agent.name]?.output || [],
353
404
  use_pty: agent.use_pty
354
405
  };
355
406
  });
356
407
 
357
- // Switch to session screen
358
- challengeScreen.classList.remove('active');
359
- sessionScreen.classList.add('active');
360
-
361
- // Setup UI
362
- workspacePath.textContent = result.workspace;
363
- if (result.fromCli) {
364
- workspacePath.innerHTML = result.workspace + ' <span class="cli-badge" style="font-size: 10px; padding: 2px 6px; background: rgba(20, 241, 149, 0.15); color: var(--accent-primary); border-radius: 4px; margin-left: 8px;">(via CLI)</span>';
365
- }
408
+ // Show right panel, create terminals
409
+ showRightPanel();
366
410
  createAgentTabs(result.agents);
367
- renderChatMessages(); // Initial render (empty)
368
411
  startChatPolling();
412
+
413
+ // Reset placeholder
414
+ userMessageInput.placeholder = 'Send a message to all agents... (Enter to send)';
415
+ userMessageInput.value = '';
416
+
417
+ // Refresh sessions list
418
+ await loadSessionsList();
369
419
  } else {
370
- alert(`Failed to start session: ${result.error}`);
371
- startButton.disabled = false;
372
- startButton.textContent = 'Start Session';
420
+ alert(`Failed to resume session: ${result.error}`);
373
421
  }
374
422
  } catch (error) {
375
- console.error('Error starting session:', error);
376
- alert('Error starting session. Check console for details.');
377
- startButton.disabled = false;
378
- startButton.textContent = 'Start Session';
423
+ console.error('Error resuming session:', error);
424
+ alert('Error resuming session. Check console for details.');
425
+ } finally {
426
+ sendMessageButton.disabled = false;
427
+ sendMessageButton.classList.remove('loading');
428
+ }
429
+ }
430
+
431
+ // Send user message to active session
432
+ async function handleSendUserMessage(message) {
433
+ sendMessageButton.disabled = true;
434
+ sendMessageButton.classList.add('loading');
435
+
436
+ try {
437
+ const result = await window.electronAPI.sendUserMessage(message);
438
+ if (result.success) {
439
+ userMessageInput.value = '';
440
+ } else {
441
+ alert(`Failed to send message: ${result.error}`);
442
+ }
443
+ } catch (error) {
444
+ console.error('Error sending message:', error);
445
+ alert('Error sending message. Check console for details.');
446
+ } finally {
447
+ sendMessageButton.disabled = false;
448
+ sendMessageButton.classList.remove('loading');
449
+ }
450
+ }
451
+
452
+ // New session button - return to welcome view
453
+ async function handleNewSessionButton() {
454
+ if (sessionState === 'active') {
455
+ stopChatPolling();
456
+ await window.electronAPI.resetSession();
457
+ cleanupTerminals();
458
+ }
459
+
460
+ // Reset UI state
461
+ chatMessages = [];
462
+ planHasContent = false;
463
+ implementationStarted = false;
464
+ agentData = {};
465
+ currentAgentTab = null;
466
+ autoScrollEnabled = true;
467
+ lastDiffData = null;
468
+ parsedDiffFiles = [];
469
+ selectedDiffFile = null;
470
+ agentTabsContainer.innerHTML = '';
471
+ agentOutputsContainer.innerHTML = '';
472
+
473
+ showWelcomeView();
474
+
475
+ // Refresh sessions list
476
+ await loadSessionsList();
477
+ }
478
+
479
+ function cleanupTerminals() {
480
+ Object.values(terminals).forEach(({ terminal }) => {
481
+ try { terminal.dispose(); } catch (e) { /* ignore */ }
482
+ });
483
+ terminals = {};
484
+ }
485
+
486
+ // ═══════════════════════════════════════════════════════════
487
+ // Display Config
488
+ // ═══════════════════════════════════════════════════════════
489
+
490
+ function displayConfig() {
491
+ if (!currentConfig) {
492
+ configDetails.innerHTML = '<span style="color: #dc3545;">No configuration loaded</span>';
493
+ return;
379
494
  }
495
+
496
+ const agentList = currentConfig.agents.map(a => {
497
+ const color = a.color || '#667eea';
498
+ return `<span style="color: ${color}">&#8226; ${a.name}</span> (${a.command})`;
499
+ }).join('<br>');
500
+
501
+ configDetails.innerHTML = `<strong>Agents:</strong><br>${agentList}`;
380
502
  }
381
503
 
382
- // Create agent tabs
504
+ // ═══════════════════════════════════════════════════════════
505
+ // Agent Tabs & Terminal
506
+ // ═══════════════════════════════════════════════════════════
507
+
383
508
  function createAgentTabs(agents) {
384
509
  agentTabsContainer.innerHTML = '';
385
510
  agentOutputsContainer.innerHTML = '';
@@ -387,7 +512,6 @@ function createAgentTabs(agents) {
387
512
  agents.forEach((agent, index) => {
388
513
  const agentInfo = agentData[agent.name];
389
514
 
390
- // Create tab
391
515
  const tab = document.createElement('button');
392
516
  tab.className = 'tab';
393
517
  if (index === 0) {
@@ -398,37 +522,37 @@ function createAgentTabs(agents) {
398
522
  tab.onclick = () => switchAgentTab(agent.name);
399
523
  agentTabsContainer.appendChild(tab);
400
524
 
401
- // Create output container
402
525
  const outputDiv = document.createElement('div');
403
526
  outputDiv.className = 'agent-output';
404
527
  outputDiv.id = `output-${agent.name}`;
405
- if (index === 0) {
406
- outputDiv.classList.add('active');
407
- }
528
+ if (index === 0) outputDiv.classList.add('active');
408
529
 
409
- // Add status indicator
410
530
  const statusDiv = document.createElement('div');
411
- statusDiv.className = 'agent-status starting';
412
- statusDiv.textContent = 'Starting...';
531
+ const knownStatus = agentInfo?.status || 'starting';
532
+ statusDiv.className = `agent-status ${knownStatus}`;
413
533
  statusDiv.id = `status-${agent.name}`;
534
+ if (knownStatus === 'running') {
535
+ statusDiv.textContent = 'Running';
536
+ } else if (knownStatus === 'stopped') {
537
+ statusDiv.textContent = 'Stopped';
538
+ } else if (knownStatus === 'error') {
539
+ statusDiv.textContent = 'Error';
540
+ } else {
541
+ statusDiv.textContent = 'Starting...';
542
+ }
414
543
  outputDiv.appendChild(statusDiv);
415
544
 
416
545
  if (agentInfo && agentInfo.use_pty) {
417
- // Create xterm terminal for PTY agents
418
546
  const terminalDiv = document.createElement('div');
419
547
  terminalDiv.id = `terminal-${agent.name}`;
420
548
  terminalDiv.className = 'terminal-container';
421
549
  outputDiv.appendChild(terminalDiv);
422
550
 
423
- // Create and initialize terminal
424
551
  const terminal = new Terminal({
425
552
  cursorBlink: true,
426
553
  fontSize: 13,
427
554
  fontFamily: 'Courier New, monospace',
428
- theme: {
429
- background: '#1e1e1e',
430
- foreground: '#e0e0e0'
431
- },
555
+ theme: getTerminalTheme(),
432
556
  rows: 40,
433
557
  cols: 120
434
558
  });
@@ -439,35 +563,27 @@ function createAgentTabs(agents) {
439
563
  fitAddon.fit();
440
564
 
441
565
  terminals[agent.name] = { terminal, fitAddon };
442
-
443
- // Initialize input lock state (default: locked)
444
566
  inputLocked[agent.name] = true;
445
-
446
- // Disable stdin by default (prevents cursor/typing when locked)
447
567
  terminal.options.disableStdin = true;
448
568
 
449
- // Add lock toggle button (inside terminal container so it stays anchored)
450
569
  const lockToggle = document.createElement('button');
451
570
  lockToggle.className = 'input-lock-toggle';
452
571
  lockToggle.innerHTML = '🔒 Input locked';
453
572
  lockToggle.onclick = () => toggleInputLock(agent.name);
454
573
  terminalDiv.appendChild(lockToggle);
455
574
 
456
- // Wire terminal input to PTY (only when unlocked)
457
575
  terminal.onData((data) => {
458
576
  if (!inputLocked[agent.name]) {
459
577
  window.electronAPI.sendPtyInput(agent.name, data);
460
578
  }
461
579
  });
462
580
 
463
- // Fit terminal on window resize
464
581
  window.addEventListener('resize', () => {
465
582
  if (terminals[agent.name]) {
466
583
  terminals[agent.name].fitAddon.fit();
467
584
  }
468
585
  });
469
586
  } else {
470
- // Create regular text output for non-PTY agents
471
587
  const contentPre = document.createElement('pre');
472
588
  contentPre.id = `content-${agent.name}`;
473
589
  outputDiv.appendChild(contentPre);
@@ -477,37 +593,22 @@ function createAgentTabs(agents) {
477
593
  });
478
594
  }
479
595
 
480
- // Switch agent tab
481
596
  function switchAgentTab(agentName) {
482
597
  currentAgentTab = agentName;
483
598
 
484
- // Update tabs
485
- document.querySelectorAll('.tab').forEach(tab => {
486
- if (tab.textContent === agentName) {
487
- tab.classList.add('active');
488
- } else {
489
- tab.classList.remove('active');
490
- }
599
+ document.querySelectorAll('#agent-tabs .tab').forEach(tab => {
600
+ tab.classList.toggle('active', tab.textContent === agentName);
491
601
  });
492
602
 
493
- // Update outputs
494
603
  document.querySelectorAll('.agent-output').forEach(output => {
495
- if (output.id === `output-${agentName}`) {
496
- output.classList.add('active');
497
- } else {
498
- output.classList.remove('active');
499
- }
604
+ output.classList.toggle('active', output.id === `output-${agentName}`);
500
605
  });
501
606
 
502
- // Fit terminal if this agent uses PTY
503
607
  if (terminals[agentName]) {
504
- setTimeout(() => {
505
- terminals[agentName].fitAddon.fit();
506
- }, 100);
608
+ setTimeout(() => terminals[agentName].fitAddon.fit(), 100);
507
609
  }
508
610
  }
509
611
 
510
- // Toggle input lock for a terminal
511
612
  function toggleInputLock(agentName) {
512
613
  inputLocked[agentName] = !inputLocked[agentName];
513
614
  const toggle = document.querySelector(`#terminal-${agentName} .input-lock-toggle`);
@@ -516,17 +617,13 @@ function toggleInputLock(agentName) {
516
617
  if (inputLocked[agentName]) {
517
618
  toggle.innerHTML = '🔒 Input locked';
518
619
  toggle.classList.remove('unlocked');
519
- // Disable stdin and blur terminal when locked
520
620
  if (terminal) {
521
621
  terminal.options.disableStdin = true;
522
- if (terminal.textarea) {
523
- terminal.textarea.blur();
524
- }
622
+ if (terminal.textarea) terminal.textarea.blur();
525
623
  }
526
624
  } else {
527
625
  toggle.innerHTML = '🔓 Input unlocked';
528
626
  toggle.classList.add('unlocked');
529
- // Enable stdin and focus terminal when unlocked
530
627
  if (terminal) {
531
628
  terminal.options.disableStdin = false;
532
629
  terminal.focus();
@@ -534,47 +631,40 @@ function toggleInputLock(agentName) {
534
631
  }
535
632
  }
536
633
 
537
- // Update agent output
538
634
  function updateAgentOutput(agentName, output, isPty) {
539
635
  if (!agentData[agentName]) {
540
636
  agentData[agentName] = { name: agentName, output: [] };
541
637
  }
542
-
543
638
  agentData[agentName].output.push(output);
544
639
 
545
- // Check if this agent uses PTY/terminal
546
640
  if (isPty && terminals[agentName]) {
547
- // Write directly to xterm terminal
548
641
  terminals[agentName].terminal.write(output);
549
642
  } else {
550
- // Use regular text output
551
643
  const contentElement = document.getElementById(`content-${agentName}`);
552
644
  if (contentElement) {
553
645
  contentElement.textContent = agentData[agentName].output.join('');
554
-
555
- // Auto-scroll if this is the active tab
556
646
  if (currentAgentTab === agentName) {
557
647
  const outputContainer = document.getElementById(`output-${agentName}`);
558
- if (outputContainer) {
559
- outputContainer.scrollTop = outputContainer.scrollHeight;
560
- }
648
+ if (outputContainer) outputContainer.scrollTop = outputContainer.scrollHeight;
561
649
  }
562
650
  }
563
651
  }
564
652
  }
565
653
 
566
- // Update agent status
567
654
  function updateAgentStatus(agentName, status, exitCode = null, error = null) {
568
- if (agentData[agentName]) {
655
+ if (!agentData[agentName]) {
656
+ agentData[agentName] = { name: agentName, status, output: [] };
657
+ } else {
569
658
  agentData[agentName].status = status;
570
659
  }
571
660
 
572
661
  const statusElement = document.getElementById(`status-${agentName}`);
573
662
  if (statusElement) {
574
663
  statusElement.className = `agent-status ${status}`;
575
-
576
664
  if (status === 'running') {
577
665
  statusElement.textContent = 'Running';
666
+ } else if (status === 'restarting') {
667
+ statusElement.textContent = 'Restarting...';
578
668
  } else if (status === 'stopped') {
579
669
  statusElement.textContent = `Stopped (exit code: ${exitCode})`;
580
670
  } else if (status === 'error') {
@@ -583,19 +673,19 @@ function updateAgentStatus(agentName, status, exitCode = null, error = null) {
583
673
  }
584
674
  }
585
675
 
586
- // Format timestamp for display
676
+ // ═══════════════════════════════════════════════════════════
677
+ // Chat
678
+ // ═══════════════════════════════════════════════════════════
679
+
587
680
  function formatTimestamp(isoString) {
588
681
  const date = new Date(isoString);
589
682
  return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
590
683
  }
591
684
 
592
- // Render a single chat message as HTML
593
685
  function renderChatMessage(message) {
594
686
  const isUser = message.type === 'user';
595
687
  const alignClass = isUser ? 'chat-message-right' : 'chat-message-left';
596
688
  const color = message.color || agentColors[message.agent?.toLowerCase()] || '#667eea';
597
-
598
- // Parse markdown content
599
689
  const htmlContent = marked.parse(message.content || '');
600
690
 
601
691
  return `
@@ -611,88 +701,110 @@ function renderChatMessage(message) {
611
701
  `;
612
702
  }
613
703
 
614
- // Render all chat messages
615
704
  function renderChatMessages() {
616
705
  if (chatMessages.length === 0) {
617
- chatViewer.innerHTML = '<div class="chat-empty">No messages yet. Agents are starting...</div>';
706
+ if (sessionState === 'active') {
707
+ chatViewer.innerHTML = '<div class="chat-empty">No messages yet. Agents are starting...</div>';
708
+ } else if (sessionState === 'loaded') {
709
+ chatViewer.innerHTML = '<div class="chat-empty">Empty session. Type a message to start.</div>';
710
+ } else {
711
+ chatViewer.innerHTML = '';
712
+ }
618
713
  setNewMessagesBanner(false);
619
714
  return;
620
715
  }
621
716
 
622
717
  chatViewer.innerHTML = chatMessages.map(renderChatMessage).join('');
623
- if (autoScrollEnabled) {
624
- scrollChatToBottom();
625
- }
718
+ if (autoScrollEnabled) scrollChatToBottom();
626
719
  }
627
720
 
628
- // Add a new message to chat
629
721
  function addChatMessage(message) {
630
- // Check if message already exists (by sequence number)
631
722
  const exists = chatMessages.some(m => m.seq === message.seq);
632
723
  if (!exists) {
633
724
  const shouldScroll = autoScrollEnabled;
634
725
  chatMessages.push(message);
635
- chatMessages.sort((a, b) => a.seq - b.seq); // Ensure order
726
+ chatMessages.sort((a, b) => a.seq - b.seq);
636
727
  renderChatMessages();
637
- if (!shouldScroll) {
638
- setNewMessagesBanner(true);
639
- }
728
+ if (!shouldScroll) setNewMessagesBanner(true);
729
+ updateSidebarMessageCount();
640
730
  }
641
731
  }
642
732
 
643
- // Update chat from full message array (for refresh/sync)
644
733
  function updateChatFromMessages(messages) {
645
734
  if (Array.isArray(messages)) {
646
735
  const prevLastSeq = chatMessages.length > 0 ? chatMessages[chatMessages.length - 1].seq : null;
647
736
  const nextLastSeq = messages.length > 0 ? messages[messages.length - 1].seq : null;
648
737
  const hasChanges = messages.length !== chatMessages.length || prevLastSeq !== nextLastSeq;
649
- if (!hasChanges) {
650
- return;
651
- }
738
+ if (!hasChanges) return;
652
739
 
653
740
  const shouldScroll = autoScrollEnabled;
654
741
  chatMessages = messages;
655
742
  renderChatMessages();
656
- if (!shouldScroll) {
657
- setNewMessagesBanner(true);
658
- }
743
+ if (!shouldScroll) setNewMessagesBanner(true);
744
+ updateSidebarMessageCount();
659
745
  }
660
746
  }
661
747
 
662
- // Send user message
663
- async function sendUserMessage() {
664
- const message = userMessageInput.value.trim();
665
-
666
- if (!message) {
667
- return;
748
+ function updateSidebarMessageCount() {
749
+ if (!currentSessionId) return;
750
+ const currentSession = sessionsList.find(s => s.id === currentSessionId);
751
+ if (currentSession && currentSession.messageCount !== chatMessages.length) {
752
+ currentSession.messageCount = chatMessages.length;
753
+ currentSession.lastActiveAt = new Date().toISOString();
754
+ renderSessionsList();
668
755
  }
756
+ }
669
757
 
670
- sendMessageButton.disabled = true;
671
- sendMessageButton.textContent = 'Sending...';
758
+ function isChatNearBottom() {
759
+ return chatViewer.scrollHeight - chatViewer.scrollTop - chatViewer.clientHeight <= CHAT_SCROLL_THRESHOLD;
760
+ }
672
761
 
673
- try {
674
- const result = await window.electronAPI.sendUserMessage(message);
762
+ function scrollChatToBottom() {
763
+ chatViewer.scrollTop = chatViewer.scrollHeight;
764
+ }
675
765
 
676
- if (result.success) {
677
- userMessageInput.value = '';
678
- // Chat will be updated via file watcher
679
- } else {
680
- alert(`Failed to send message: ${result.error}`);
681
- }
682
- } catch (error) {
683
- console.error('Error sending message:', error);
684
- alert('Error sending message. Check console for details.');
685
- } finally {
686
- sendMessageButton.disabled = false;
687
- sendMessageButton.textContent = 'Send Message';
766
+ function setNewMessagesBanner(visible) {
767
+ if (!chatNewMessages) return;
768
+ chatNewMessages.classList.toggle('visible', visible);
769
+ chatNewMessages.setAttribute('aria-hidden', visible ? 'false' : 'true');
770
+ }
771
+
772
+ // ═══════════════════════════════════════════════════════════
773
+ // Right Panel Tabs (Terminal / Plan / Diff)
774
+ // ═══════════════════════════════════════════════════════════
775
+
776
+ function switchRightTab(tabName) {
777
+ currentRightTab = tabName;
778
+
779
+ mainTabTerminal.classList.toggle('active', tabName === 'terminal');
780
+ mainTabPlan.classList.toggle('active', tabName === 'plan');
781
+ mainTabDiff.classList.toggle('active', tabName === 'diff');
782
+
783
+ terminalTabContent.classList.toggle('active', tabName === 'terminal');
784
+ planTabContent.classList.toggle('active', tabName === 'plan');
785
+ diffTabContent.classList.toggle('active', tabName === 'diff');
786
+
787
+ if (tabName === 'terminal') {
788
+ requestAnimationFrame(() => refitTerminals());
789
+ }
790
+
791
+ if (tabName === 'diff') {
792
+ refreshGitDiff();
793
+ requestAnimationFrame(() => restoreDiffLayout());
794
+ }
795
+
796
+ if (tabName === 'plan') {
797
+ refreshPlan();
688
798
  }
689
799
  }
690
800
 
691
- // Refresh plan and update button visibility
801
+ // ═══════════════════════════════════════════════════════════
802
+ // Plan
803
+ // ═══════════════════════════════════════════════════════════
804
+
692
805
  async function refreshPlan() {
693
806
  try {
694
807
  const content = await window.electronAPI.getPlanContent();
695
-
696
808
  if (content.trim()) {
697
809
  const htmlContent = marked.parse(content);
698
810
  planViewer.innerHTML = `<div class="markdown-content">${htmlContent}</div>`;
@@ -701,15 +813,12 @@ async function refreshPlan() {
701
813
  planViewer.innerHTML = '<em>No plan yet...</em>';
702
814
  planHasContent = false;
703
815
  }
704
-
705
- // Update button visibility
706
816
  updateImplementButtonState();
707
817
  } catch (error) {
708
818
  console.error('Error refreshing plan:', error);
709
819
  }
710
820
  }
711
821
 
712
- // Update the Start Implementing button state
713
822
  function updateImplementButtonState() {
714
823
  if (implementationStarted) {
715
824
  startImplementingButton.textContent = 'Implementation in progress';
@@ -724,56 +833,10 @@ function updateImplementButtonState() {
724
833
  }
725
834
  }
726
835
 
727
- // Switch between main tabs (Chat, Plan, Diff)
728
- function switchMainTab(tabName) {
729
- currentMainTab = tabName;
836
+ // ═══════════════════════════════════════════════════════════
837
+ // Diff
838
+ // ═══════════════════════════════════════════════════════════
730
839
 
731
- // Update tab active states
732
- mainTabChat.classList.toggle('active', tabName === 'chat');
733
- mainTabPlan.classList.toggle('active', tabName === 'plan');
734
- mainTabDiff.classList.toggle('active', tabName === 'diff');
735
-
736
- // Update content visibility
737
- chatTabContent.classList.toggle('active', tabName === 'chat');
738
- planTabContent.classList.toggle('active', tabName === 'plan');
739
- diffTabContent.classList.toggle('active', tabName === 'diff');
740
-
741
- // Save tab state to localStorage
742
- try {
743
- localStorage.setItem(TAB_STORAGE_KEY, tabName);
744
- } catch (e) {
745
- // Ignore localStorage errors
746
- }
747
-
748
- // Refresh main panels layout on any tab switch
749
- requestAnimationFrame(() => handleContainerResize());
750
-
751
- // Fetch diff if switching to diff tab
752
- if (tabName === 'diff') {
753
- refreshGitDiff();
754
- // Restore diff layout after tab is visible (deferred to avoid zero-width issue)
755
- requestAnimationFrame(() => restoreDiffLayout());
756
- }
757
-
758
- // Refresh plan if switching to plan tab
759
- if (tabName === 'plan') {
760
- refreshPlan();
761
- }
762
- }
763
-
764
- // Restore saved tab state
765
- function restoreTabState() {
766
- try {
767
- const savedTab = localStorage.getItem(TAB_STORAGE_KEY);
768
- if (savedTab && ['chat', 'plan', 'diff'].includes(savedTab)) {
769
- switchMainTab(savedTab);
770
- }
771
- } catch (e) {
772
- // Ignore localStorage errors
773
- }
774
- }
775
-
776
- // Fetch and render git diff
777
840
  async function refreshGitDiff() {
778
841
  try {
779
842
  const data = await window.electronAPI.getGitDiff();
@@ -786,7 +849,6 @@ async function refreshGitDiff() {
786
849
  }
787
850
  }
788
851
 
789
- // Parse diff into per-file blocks
790
852
  function parseDiffIntoFiles(diffText) {
791
853
  if (!diffText || !diffText.trim()) return [];
792
854
 
@@ -797,35 +859,21 @@ function parseDiffIntoFiles(diffText) {
797
859
 
798
860
  for (const line of lines) {
799
861
  if (line.startsWith('diff --git')) {
800
- // Save previous file if exists
801
862
  if (currentFile) {
802
863
  currentFile.content = currentContent.join('\n');
803
864
  files.push(currentFile);
804
865
  }
805
-
806
- // Extract filename from diff header
807
866
  const match = line.match(/diff --git a\/(.+?) b\/(.+)/);
808
867
  const filename = match ? match[2] : 'unknown';
809
-
810
- currentFile = {
811
- filename,
812
- status: 'modified',
813
- content: ''
814
- };
868
+ currentFile = { filename, status: 'modified', content: '' };
815
869
  currentContent = [line];
816
870
  } else if (currentFile) {
817
871
  currentContent.push(line);
818
-
819
- // Detect file status
820
- if (line.startsWith('new file mode')) {
821
- currentFile.status = 'added';
822
- } else if (line.startsWith('deleted file mode')) {
823
- currentFile.status = 'deleted';
824
- }
872
+ if (line.startsWith('new file mode')) currentFile.status = 'added';
873
+ else if (line.startsWith('deleted file mode')) currentFile.status = 'deleted';
825
874
  }
826
875
  }
827
876
 
828
- // Don't forget the last file
829
877
  if (currentFile) {
830
878
  currentFile.content = currentContent.join('\n');
831
879
  files.push(currentFile);
@@ -834,28 +882,22 @@ function parseDiffIntoFiles(diffText) {
834
882
  return files;
835
883
  }
836
884
 
837
- // Render file list in diff view
838
885
  function renderDiffFileList(files, untracked) {
839
886
  if (!diffFileListItems) return;
840
887
 
841
888
  let html = '';
842
-
843
- // Add "All Files" option
844
889
  const allActive = selectedDiffFile === null ? 'active' : '';
845
890
  html += `<li class="file-list-item all-files ${allActive}" data-file="__all__">All Files</li>`;
846
891
 
847
- // Add changed files
848
892
  for (const file of files) {
849
893
  const isActive = selectedDiffFile === file.filename ? 'active' : '';
850
- const statusClass = file.status;
851
894
  const statusLabel = file.status === 'added' ? 'A' : file.status === 'deleted' ? 'D' : 'M';
852
895
  html += `<li class="file-list-item ${isActive}" data-file="${escapeHtml(file.filename)}">
853
- <span class="file-status ${statusClass}">${statusLabel}</span>
896
+ <span class="file-status ${file.status}">${statusLabel}</span>
854
897
  <span class="file-name">${escapeHtml(file.filename)}</span>
855
898
  </li>`;
856
899
  }
857
900
 
858
- // Add untracked files
859
901
  for (const file of (untracked || [])) {
860
902
  const isActive = selectedDiffFile === file ? 'active' : '';
861
903
  html += `<li class="file-list-item ${isActive}" data-file="${escapeHtml(file)}" data-untracked="true">
@@ -870,22 +912,16 @@ function renderDiffFileList(files, untracked) {
870
912
 
871
913
  diffFileListItems.innerHTML = html;
872
914
 
873
- // Add click handlers (skip items without data-file attribute)
874
915
  diffFileListItems.querySelectorAll('.file-list-item[data-file]').forEach(item => {
875
916
  item.addEventListener('click', () => {
876
917
  const file = item.dataset.file;
877
- if (file === '__all__') {
878
- selectedDiffFile = null;
879
- } else {
880
- selectedDiffFile = file;
881
- }
918
+ selectedDiffFile = file === '__all__' ? null : file;
882
919
  renderDiffFileList(parsedDiffFiles, lastDiffData?.untracked);
883
920
  renderDiffContent();
884
921
  });
885
922
  });
886
923
  }
887
924
 
888
- // Render diff content based on selected file
889
925
  function renderDiffContent() {
890
926
  if (!lastDiffData) {
891
927
  diffContent.innerHTML = '<em class="diff-empty">No diff data available</em>';
@@ -893,25 +929,21 @@ function renderDiffContent() {
893
929
  }
894
930
 
895
931
  if (selectedDiffFile === null) {
896
- // Show all files
897
932
  if (lastDiffData.diff && lastDiffData.diff.trim()) {
898
933
  diffContent.innerHTML = `<pre class="diff-output">${formatDiffOutput(lastDiffData.diff)}</pre>`;
899
934
  } else {
900
935
  diffContent.innerHTML = '<em class="diff-empty">No uncommitted changes</em>';
901
936
  }
902
937
  } else {
903
- // Show specific file
904
938
  const file = parsedDiffFiles.find(f => f.filename === selectedDiffFile);
905
939
  if (file) {
906
940
  diffContent.innerHTML = `<pre class="diff-output">${formatDiffOutput(file.content)}</pre>`;
907
941
  } else {
908
- // Might be an untracked file
909
942
  diffContent.innerHTML = `<em class="diff-empty">Untracked file: ${escapeHtml(selectedDiffFile)}</em>`;
910
943
  }
911
944
  }
912
945
  }
913
946
 
914
- // Render git diff data
915
947
  function renderGitDiff(data) {
916
948
  if (!data.isGitRepo) {
917
949
  diffStats.innerHTML = '';
@@ -929,10 +961,8 @@ function renderGitDiff(data) {
929
961
  return;
930
962
  }
931
963
 
932
- // Parse diff into files
933
964
  parsedDiffFiles = parseDiffIntoFiles(data.diff);
934
965
 
935
- // Render stats
936
966
  const { filesChanged, insertions, deletions } = data.stats;
937
967
  if (filesChanged > 0 || insertions > 0 || deletions > 0) {
938
968
  diffStats.innerHTML = `
@@ -944,48 +974,33 @@ function renderGitDiff(data) {
944
974
  diffStats.innerHTML = '<span class="diff-stat-none">No changes</span>';
945
975
  }
946
976
 
947
- // Render file list
948
977
  renderDiffFileList(parsedDiffFiles, data.untracked);
949
-
950
- // Render diff content
951
978
  renderDiffContent();
952
979
 
953
- // Render untracked files summary (below diff)
954
980
  if (data.untracked && data.untracked.length > 0) {
955
- diffUntracked.innerHTML = `
956
- <div class="diff-untracked-header">Untracked files (${data.untracked.length})</div>
957
- `;
981
+ diffUntracked.innerHTML = `<div class="diff-untracked-header">Untracked files (${data.untracked.length})</div>`;
958
982
  } else {
959
983
  diffUntracked.innerHTML = '';
960
984
  }
961
985
  }
962
986
 
963
- // Format diff output with syntax highlighting
964
987
  function formatDiffOutput(diff) {
965
988
  return diff.split('\n').map(line => {
966
989
  const escaped = escapeHtml(line);
967
- if (line.startsWith('+++') || line.startsWith('---')) {
968
- return `<span class="diff-file-header">${escaped}</span>`;
969
- } else if (line.startsWith('@@')) {
970
- return `<span class="diff-hunk-header">${escaped}</span>`;
971
- } else if (line.startsWith('+')) {
972
- return `<span class="diff-added">${escaped}</span>`;
973
- } else if (line.startsWith('-')) {
974
- return `<span class="diff-removed">${escaped}</span>`;
975
- } else if (line.startsWith('diff --git')) {
976
- return `<span class="diff-file-separator">${escaped}</span>`;
977
- }
978
- return escaped;
979
- }).join('\n');
990
+ if (line.startsWith('+++') || line.startsWith('---')) return `<span class="diff-file-header">${escaped}</span>`;
991
+ if (line.startsWith('@@')) return `<span class="diff-hunk-header">${escaped}</span>`;
992
+ if (line.startsWith('+')) return `<span class="diff-added">${escaped}</span>`;
993
+ if (line.startsWith('-')) return `<span class="diff-removed">${escaped}</span>`;
994
+ if (line.startsWith('diff --git')) return `<span class="diff-file-separator">${escaped}</span>`;
995
+ return `<span class="diff-context">${escaped}</span>`;
996
+ }).join('');
980
997
  }
981
998
 
982
- // Update diff badge to show when changes exist
983
999
  function updateDiffBadge(data) {
984
1000
  if (!data || !data.isGitRepo) {
985
1001
  diffBadge.style.display = 'none';
986
1002
  return;
987
1003
  }
988
-
989
1004
  const totalChanges = (data.stats?.filesChanged || 0) + (data.untracked?.length || 0);
990
1005
  if (totalChanges > 0) {
991
1006
  diffBadge.textContent = totalChanges;
@@ -995,11 +1010,12 @@ function updateDiffBadge(data) {
995
1010
  }
996
1011
  }
997
1012
 
998
- // Show implementation modal
1013
+ // ═══════════════════════════════════════════════════════════
1014
+ // Implementation Modal
1015
+ // ═══════════════════════════════════════════════════════════
1016
+
999
1017
  function showImplementationModal() {
1000
- // Populate agent selection with enabled agents
1001
1018
  agentSelectionContainer.innerHTML = '';
1002
-
1003
1019
  const enabledAgents = currentConfig.agents || [];
1004
1020
 
1005
1021
  enabledAgents.forEach((agent, index) => {
@@ -1008,9 +1024,7 @@ function showImplementationModal() {
1008
1024
  radio.type = 'radio';
1009
1025
  radio.name = 'implementer';
1010
1026
  radio.value = agent.name;
1011
- if (index === 0 || enabledAgents.length === 1) {
1012
- radio.checked = true; // Auto-select first (or only) agent
1013
- }
1027
+ if (index === 0 || enabledAgents.length === 1) radio.checked = true;
1014
1028
 
1015
1029
  const agentNameSpan = document.createElement('span');
1016
1030
  agentNameSpan.className = 'agent-name';
@@ -1025,12 +1039,10 @@ function showImplementationModal() {
1025
1039
  implementationModal.style.display = 'flex';
1026
1040
  }
1027
1041
 
1028
- // Hide implementation modal
1029
1042
  function hideImplementationModal() {
1030
1043
  implementationModal.style.display = 'none';
1031
1044
  }
1032
1045
 
1033
- // Start implementation with selected agent
1034
1046
  async function startImplementation() {
1035
1047
  const selectedRadio = document.querySelector('input[name="implementer"]:checked');
1036
1048
  if (!selectedRadio) {
@@ -1043,8 +1055,6 @@ async function startImplementation() {
1043
1055
  const otherAgents = allAgents.filter(name => name !== selectedAgent);
1044
1056
 
1045
1057
  hideImplementationModal();
1046
-
1047
- // Mark implementation as started
1048
1058
  implementationStarted = true;
1049
1059
  updateImplementButtonState();
1050
1060
 
@@ -1057,163 +1067,49 @@ async function startImplementation() {
1057
1067
  }
1058
1068
  } catch (error) {
1059
1069
  console.error('Error starting implementation:', error);
1060
- alert('Error starting implementation. Check console for details.');
1061
1070
  implementationStarted = false;
1062
1071
  updateImplementButtonState();
1063
1072
  }
1064
1073
  }
1065
1074
 
1066
- // Stop all polling intervals
1075
+ // ═══════════════════════════════════════════════════════════
1076
+ // Polling
1077
+ // ═══════════════════════════════════════════════════════════
1078
+
1067
1079
  function stopChatPolling() {
1068
1080
  pollingIntervals.forEach(id => clearInterval(id));
1069
1081
  pollingIntervals = [];
1070
1082
  }
1071
1083
 
1072
- // Start polling chat content (fallback if file watcher has issues)
1073
1084
  function startChatPolling() {
1074
- // Clear any existing intervals first
1075
1085
  stopChatPolling();
1076
1086
 
1077
1087
  pollingIntervals.push(setInterval(async () => {
1078
1088
  try {
1079
1089
  const messages = await window.electronAPI.getChatContent();
1080
- if (messages && messages.length > 0) {
1081
- updateChatFromMessages(messages);
1082
- }
1090
+ if (messages && messages.length > 0) updateChatFromMessages(messages);
1083
1091
  } catch (error) {
1084
1092
  console.error('Error polling chat:', error);
1085
1093
  }
1086
1094
  }, 2000));
1087
1095
 
1088
- // Also poll plan
1089
1096
  pollingIntervals.push(setInterval(refreshPlan, 3000));
1090
1097
 
1091
- // Poll for diff updates (also updates badge even when not on diff tab)
1092
1098
  pollingIntervals.push(setInterval(async () => {
1093
1099
  try {
1094
1100
  const data = await window.electronAPI.getGitDiff();
1095
1101
  lastDiffData = data;
1096
1102
  updateDiffBadge(data);
1097
- // Only re-render if diff tab is active
1098
- if (currentMainTab === 'diff') {
1099
- renderGitDiff(data);
1100
- }
1103
+ if (currentRightTab === 'diff') renderGitDiff(data);
1101
1104
  } catch (error) {
1102
1105
  console.error('Error polling git diff:', error);
1103
1106
  }
1104
1107
  }, 5000));
1105
1108
  }
1106
1109
 
1107
- // New Session Modal Functions
1108
- function showNewSessionModal() {
1109
- newSessionModal.style.display = 'flex';
1110
- }
1111
-
1112
- function hideNewSessionModal() {
1113
- newSessionModal.style.display = 'none';
1114
- }
1115
-
1116
- async function startNewSession() {
1117
- hideNewSessionModal();
1118
-
1119
- try {
1120
- // Stop polling intervals to prevent memory leaks
1121
- stopChatPolling();
1122
-
1123
- await window.electronAPI.resetSession();
1124
-
1125
- // Reset UI state
1126
- chatMessages = [];
1127
- planHasContent = false;
1128
- implementationStarted = false;
1129
- agentData = {};
1130
- currentAgentTab = null;
1131
- autoScrollEnabled = true;
1132
- lastDiffData = null;
1133
- parsedDiffFiles = [];
1134
- selectedDiffFile = null;
1135
-
1136
- // Clear terminals (they'll be recreated on new session)
1137
- Object.values(terminals).forEach(({ terminal }) => {
1138
- try {
1139
- terminal.dispose();
1140
- } catch (e) {
1141
- // Ignore disposal errors
1142
- }
1143
- });
1144
- terminals = {};
1145
-
1146
- // Clear input
1147
- challengeInput.value = '';
1148
-
1149
- // Reset UI elements
1150
- agentTabsContainer.innerHTML = '';
1151
- agentOutputsContainer.innerHTML = '';
1152
- chatViewer.innerHTML = '<div class="chat-empty">No messages yet. Agents are starting...</div>';
1153
- planViewer.innerHTML = '<em>Awaiting agent synthesis...</em>';
1154
- diffStats.innerHTML = '';
1155
- diffContent.innerHTML = '<em>Select a file or view all changes...</em>';
1156
- diffUntracked.innerHTML = '';
1157
- diffBadge.style.display = 'none';
1158
- startImplementingButton.style.display = 'none';
1159
- if (diffFileListItems) diffFileListItems.innerHTML = '';
1160
-
1161
- // Reset main tab to Chat
1162
- currentMainTab = 'chat';
1163
- mainTabChat.classList.add('active');
1164
- mainTabPlan.classList.remove('active');
1165
- mainTabDiff.classList.remove('active');
1166
- chatTabContent.classList.add('active');
1167
- planTabContent.classList.remove('active');
1168
- diffTabContent.classList.remove('active');
1169
-
1170
- // Switch screens
1171
- sessionScreen.classList.remove('active');
1172
- challengeScreen.classList.add('active');
1173
-
1174
- // Reload workspace info (keeps selection, refreshes recents)
1175
- await loadWorkspaceInfo();
1176
-
1177
- // Update start button state based on workspace selection
1178
- if (selectedWorkspace) {
1179
- startButton.disabled = false;
1180
- startButton.textContent = 'Start Session';
1181
- } else {
1182
- startButton.disabled = true;
1183
- startButton.textContent = 'Select a Workspace';
1184
- }
1185
-
1186
- } catch (error) {
1187
- console.error('Error resetting session:', error);
1188
- alert('Error resetting session. Check console for details.');
1189
- }
1190
- }
1191
-
1192
- // Utility: Escape HTML
1193
- function escapeHtml(text) {
1194
- const div = document.createElement('div');
1195
- div.textContent = text;
1196
- return div.innerHTML;
1197
- }
1198
-
1199
- function isChatNearBottom() {
1200
- return chatViewer.scrollHeight - chatViewer.scrollTop - chatViewer.clientHeight <= CHAT_SCROLL_THRESHOLD;
1201
- }
1202
-
1203
- function scrollChatToBottom() {
1204
- chatViewer.scrollTop = chatViewer.scrollHeight;
1205
- }
1206
-
1207
- function setNewMessagesBanner(visible) {
1208
- if (!chatNewMessages) {
1209
- return;
1210
- }
1211
- chatNewMessages.classList.toggle('visible', visible);
1212
- chatNewMessages.setAttribute('aria-hidden', visible ? 'false' : 'true');
1213
- }
1214
-
1110
+ // ═══════════════════════════════════════════════════════════
1215
1111
  // Resize Handles
1216
- const LAYOUT_STORAGE_KEY = 'multiagent-layout';
1112
+ // ═══════════════════════════════════════════════════════════
1217
1113
 
1218
1114
  function initResizers() {
1219
1115
  const mainHandle = document.querySelector('[data-resize="main"]');
@@ -1223,11 +1119,11 @@ function initResizers() {
1223
1119
  setupResizer({
1224
1120
  handle: mainHandle,
1225
1121
  direction: 'horizontal',
1226
- container: document.querySelector('.main-content'),
1227
- panelA: document.querySelector('.left-panel'),
1228
- panelB: document.querySelector('.right-panel'),
1229
- minA: 200,
1230
- minB: 200,
1122
+ container: document.querySelector('.app-layout'),
1123
+ panelA: document.querySelector('.main-area'),
1124
+ panelB: document.querySelector('.right-panel-inner'),
1125
+ minA: 300,
1126
+ minB: 300,
1231
1127
  layoutKey: 'mainSplit'
1232
1128
  });
1233
1129
  }
@@ -1245,17 +1141,12 @@ function initResizers() {
1245
1141
  });
1246
1142
  }
1247
1143
 
1248
- // Restore saved layout
1249
1144
  restoreLayout();
1250
1145
  }
1251
1146
 
1252
1147
  function setupResizer(config) {
1253
1148
  const { handle, direction, container, panelA, panelB, minA, minB, layoutKey } = config;
1254
-
1255
- if (!handle || !container || !panelA || !panelB) {
1256
- console.warn('Resizer setup failed: missing elements', config);
1257
- return;
1258
- }
1149
+ if (!handle || !container || !panelA || !panelB) return;
1259
1150
 
1260
1151
  let startPos = 0;
1261
1152
  let startSizeA = 0;
@@ -1263,55 +1154,35 @@ function setupResizer(config) {
1263
1154
  let rafId = null;
1264
1155
 
1265
1156
  function onPointerDown(e) {
1266
- // Only respond to primary button
1267
1157
  if (e.button !== 0) return;
1268
-
1269
1158
  e.preventDefault();
1270
1159
  handle.setPointerCapture(e.pointerId);
1271
1160
  handle.classList.add('dragging');
1272
- document.body.classList.add('resizing');
1273
- document.body.classList.add(direction === 'horizontal' ? 'resizing-h' : 'resizing-v');
1274
-
1275
- startPos = direction === 'horizontal' ? e.clientX : e.clientY;
1276
- startSizeA = direction === 'horizontal'
1277
- ? panelA.getBoundingClientRect().width
1278
- : panelA.getBoundingClientRect().height;
1279
- startSizeB = direction === 'horizontal'
1280
- ? panelB.getBoundingClientRect().width
1281
- : panelB.getBoundingClientRect().height;
1161
+ document.body.classList.add('resizing', 'resizing-h');
1162
+
1163
+ startPos = e.clientX;
1164
+ startSizeA = panelA.getBoundingClientRect().width;
1165
+ startSizeB = panelB.getBoundingClientRect().width;
1282
1166
  }
1283
1167
 
1284
1168
  function onPointerMove(e) {
1285
1169
  if (!handle.hasPointerCapture(e.pointerId)) return;
1286
-
1287
1170
  if (rafId) cancelAnimationFrame(rafId);
1288
1171
 
1289
1172
  rafId = requestAnimationFrame(() => {
1290
- const currentPos = direction === 'horizontal' ? e.clientX : e.clientY;
1291
- const delta = currentPos - startPos;
1292
- const availableSize = startSizeA + startSizeB; // Only resizable panels
1173
+ const delta = e.clientX - startPos;
1174
+ const availableSize = startSizeA + startSizeB;
1293
1175
 
1294
1176
  let newSizeA = startSizeA + delta;
1295
1177
  let newSizeB = startSizeB - delta;
1296
1178
 
1297
- // Clamp to minimums
1298
- if (newSizeA < minA) {
1299
- newSizeA = minA;
1300
- newSizeB = availableSize - minA;
1301
- }
1302
- if (newSizeB < minB) {
1303
- newSizeB = minB;
1304
- newSizeA = availableSize - minB;
1305
- }
1179
+ if (newSizeA < minA) { newSizeA = minA; newSizeB = availableSize - minA; }
1180
+ if (newSizeB < minB) { newSizeB = minB; newSizeA = availableSize - minB; }
1306
1181
 
1307
- // Use pixel values to avoid overflow issues
1308
1182
  panelA.style.flex = `0 0 ${newSizeA}px`;
1309
1183
  panelB.style.flex = `0 0 ${newSizeB}px`;
1310
1184
 
1311
- // Refit terminals if resizing affects them
1312
- if (direction === 'horizontal') {
1313
- refitTerminals();
1314
- }
1185
+ refitTerminals();
1315
1186
  });
1316
1187
  }
1317
1188
 
@@ -1319,19 +1190,12 @@ function setupResizer(config) {
1319
1190
  if (rafId) cancelAnimationFrame(rafId);
1320
1191
  handle.releasePointerCapture(e.pointerId);
1321
1192
  handle.classList.remove('dragging');
1322
- document.body.classList.remove('resizing', 'resizing-h', 'resizing-v');
1323
-
1324
- // Save layout to localStorage as ratio
1325
- const sizeA = direction === 'horizontal'
1326
- ? panelA.getBoundingClientRect().width
1327
- : panelA.getBoundingClientRect().height;
1328
- const sizeB = direction === 'horizontal'
1329
- ? panelB.getBoundingClientRect().width
1330
- : panelB.getBoundingClientRect().height;
1193
+ document.body.classList.remove('resizing', 'resizing-h');
1194
+
1195
+ const sizeA = panelA.getBoundingClientRect().width;
1196
+ const sizeB = panelB.getBoundingClientRect().width;
1331
1197
  const ratio = sizeA / (sizeA + sizeB);
1332
1198
  saveLayoutRatio(layoutKey, ratio);
1333
-
1334
- // Final refit
1335
1199
  refitTerminals();
1336
1200
  }
1337
1201
 
@@ -1356,30 +1220,28 @@ function restoreLayout() {
1356
1220
  const layout = JSON.parse(localStorage.getItem(LAYOUT_STORAGE_KEY) || '{}');
1357
1221
 
1358
1222
  if (layout.mainSplit !== undefined) {
1359
- const leftPanel = document.querySelector('.left-panel');
1360
- const rightPanel = document.querySelector('.right-panel');
1361
- const mainContent = document.querySelector('.main-content');
1362
- const mainHandle = document.querySelector('[data-resize="main"]');
1363
-
1364
- if (leftPanel && rightPanel && mainContent) {
1365
- const containerWidth = mainContent.getBoundingClientRect().width;
1366
- const handleWidth = mainHandle ? mainHandle.offsetWidth : 8;
1367
- const availableWidth = containerWidth - handleWidth;
1368
- const leftWidth = availableWidth * layout.mainSplit;
1369
- const rightWidth = availableWidth * (1 - layout.mainSplit);
1370
-
1371
- leftPanel.style.flex = `0 0 ${leftWidth}px`;
1372
- rightPanel.style.flex = `0 0 ${rightWidth}px`;
1223
+ const mainArea = document.querySelector('.main-area');
1224
+ const rightPanelInner = document.querySelector('.right-panel-inner');
1225
+ const rightPanelEl = document.querySelector('.right-panel');
1226
+
1227
+ if (mainArea && rightPanelInner && rightPanelEl && rightPanelEl.style.display !== 'none') {
1228
+ const appLayout = document.querySelector('.app-layout');
1229
+ const sidebarWidth = document.querySelector('.sidebar')?.getBoundingClientRect().width || 260;
1230
+ const containerWidth = appLayout.getBoundingClientRect().width - sidebarWidth;
1231
+
1232
+ if (containerWidth > 0) {
1233
+ const mainWidth = containerWidth * layout.mainSplit;
1234
+ const rightWidth = containerWidth * (1 - layout.mainSplit);
1235
+ mainArea.style.flex = `0 0 ${mainWidth}px`;
1236
+ rightPanelInner.style.flex = `0 0 ${rightWidth}px`;
1237
+ }
1373
1238
  }
1374
1239
  }
1375
-
1376
- // Note: diffSplit is restored via restoreDiffLayout() when diff tab becomes visible
1377
1240
  } catch (err) {
1378
1241
  console.warn('Failed to restore layout:', err);
1379
1242
  }
1380
1243
  }
1381
1244
 
1382
- // Restore diff pane layout (called when diff tab becomes visible)
1383
1245
  function restoreDiffLayout() {
1384
1246
  try {
1385
1247
  const layout = JSON.parse(localStorage.getItem(LAYOUT_STORAGE_KEY) || '{}');
@@ -1392,14 +1254,11 @@ function restoreDiffLayout() {
1392
1254
 
1393
1255
  if (diffFileList && diffContentPane && diffLayout) {
1394
1256
  const containerWidth = diffLayout.getBoundingClientRect().width;
1395
- // Guard against zero-width container (tab still hidden)
1396
1257
  if (containerWidth <= 0) return;
1397
-
1398
1258
  const handleWidth = diffHandle ? diffHandle.offsetWidth : 8;
1399
1259
  const availableWidth = containerWidth - handleWidth;
1400
1260
  const fileListWidth = availableWidth * layout.diffSplit;
1401
1261
  const contentWidth = availableWidth * (1 - layout.diffSplit);
1402
-
1403
1262
  diffFileList.style.flex = `0 0 ${fileListWidth}px`;
1404
1263
  diffContentPane.style.flex = `0 0 ${contentWidth}px`;
1405
1264
  }
@@ -1409,157 +1268,107 @@ function restoreDiffLayout() {
1409
1268
  }
1410
1269
  }
1411
1270
 
1412
- // Handle container resize to keep panels responsive
1413
- function handleContainerResize() {
1414
- try {
1415
- const layout = JSON.parse(localStorage.getItem(LAYOUT_STORAGE_KEY) || '{}');
1416
-
1417
- // Update main panels
1418
- if (layout.mainSplit !== undefined) {
1419
- const leftPanel = document.querySelector('.left-panel');
1420
- const rightPanel = document.querySelector('.right-panel');
1421
- const mainContent = document.querySelector('.main-content');
1422
- const mainHandle = document.querySelector('[data-resize="main"]');
1423
-
1424
- if (leftPanel && rightPanel && mainContent) {
1425
- const containerWidth = mainContent.getBoundingClientRect().width;
1426
- const handleWidth = mainHandle ? mainHandle.offsetWidth : 8;
1427
- const availableWidth = containerWidth - handleWidth;
1428
-
1429
- // Only update if we have valid dimensions
1430
- if (containerWidth > 0 && availableWidth > 0) {
1431
- const minLeft = 200;
1432
- const minRight = 200;
1433
- const minTotal = minLeft + minRight;
1434
-
1435
- let leftWidth, rightWidth;
1436
-
1437
- // If available space is less than minimum total, scale proportionally
1438
- if (availableWidth < minTotal) {
1439
- leftWidth = Math.max(0, availableWidth * layout.mainSplit);
1440
- rightWidth = Math.max(0, availableWidth * (1 - layout.mainSplit));
1441
- } else {
1442
- // Normal case: apply ratio and clamp to minimums
1443
- leftWidth = availableWidth * layout.mainSplit;
1444
- rightWidth = availableWidth * (1 - layout.mainSplit);
1445
-
1446
- if (leftWidth < minLeft) {
1447
- leftWidth = minLeft;
1448
- rightWidth = Math.max(minRight, availableWidth - minLeft);
1449
- } else if (rightWidth < minRight) {
1450
- rightWidth = minRight;
1451
- leftWidth = Math.max(minLeft, availableWidth - minRight);
1452
- }
1453
- }
1454
-
1455
- leftPanel.style.flex = `0 0 ${leftWidth}px`;
1456
- rightPanel.style.flex = `0 0 ${rightWidth}px`;
1457
-
1458
- refitTerminals();
1459
- }
1460
- }
1461
- }
1462
-
1463
- // Update diff panels if diff tab is active
1464
- const diffTabContent = document.getElementById('diff-tab-content');
1465
- if (diffTabContent && diffTabContent.classList.contains('active') && layout.diffSplit !== undefined) {
1466
- const diffFileList = document.querySelector('.diff-file-list');
1467
- const diffContentPane = document.querySelector('.diff-content-pane');
1468
- const diffLayout = document.querySelector('.diff-layout');
1469
- const diffHandle = document.querySelector('[data-resize="diff"]');
1470
-
1471
- if (diffFileList && diffContentPane && diffLayout) {
1472
- const containerWidth = diffLayout.getBoundingClientRect().width;
1473
- const handleWidth = diffHandle ? diffHandle.offsetWidth : 8;
1474
- const availableWidth = containerWidth - handleWidth;
1475
-
1476
- // Only update if we have valid dimensions
1477
- if (containerWidth > 0 && availableWidth > 0) {
1478
- const minFileList = 150;
1479
- const minContent = 200;
1480
- const minTotal = minFileList + minContent;
1481
-
1482
- let fileListWidth, contentWidth;
1483
-
1484
- // If available space is less than minimum total, scale proportionally
1485
- if (availableWidth < minTotal) {
1486
- fileListWidth = Math.max(0, availableWidth * layout.diffSplit);
1487
- contentWidth = Math.max(0, availableWidth * (1 - layout.diffSplit));
1488
- } else {
1489
- // Normal case: apply ratio and clamp to minimums
1490
- fileListWidth = availableWidth * layout.diffSplit;
1491
- contentWidth = availableWidth * (1 - layout.diffSplit);
1492
-
1493
- if (fileListWidth < minFileList) {
1494
- fileListWidth = minFileList;
1495
- contentWidth = Math.max(minContent, availableWidth - minFileList);
1496
- } else if (contentWidth < minContent) {
1497
- contentWidth = minContent;
1498
- fileListWidth = Math.max(minFileList, availableWidth - minContent);
1499
- }
1500
- }
1501
-
1502
- diffFileList.style.flex = `0 0 ${fileListWidth}px`;
1503
- diffContentPane.style.flex = `0 0 ${contentWidth}px`;
1504
- }
1505
- }
1506
- }
1507
- } catch (err) {
1508
- console.warn('Failed to handle container resize:', err);
1509
- }
1271
+ function refitTerminals() {
1272
+ if (refitTerminals.timeout) clearTimeout(refitTerminals.timeout);
1273
+ refitTerminals.timeout = setTimeout(() => {
1274
+ Object.values(terminals).forEach(({ fitAddon }) => {
1275
+ try { fitAddon.fit(); } catch (err) { /* ignore */ }
1276
+ });
1277
+ }, 50);
1510
1278
  }
1511
1279
 
1512
- // Debounced window resize handler
1280
+ // Debounced window resize
1513
1281
  let resizeTimeout = null;
1514
1282
  function onWindowResize() {
1515
1283
  if (resizeTimeout) clearTimeout(resizeTimeout);
1516
1284
  resizeTimeout = setTimeout(() => {
1517
- handleContainerResize();
1285
+ if (rightPanel.style.display !== 'none') {
1286
+ restoreLayout();
1287
+ refitTerminals();
1288
+ }
1518
1289
  }, 150);
1519
1290
  }
1520
1291
 
1521
- function refitTerminals() {
1522
- // Debounce terminal refitting
1523
- if (refitTerminals.timeout) clearTimeout(refitTerminals.timeout);
1524
- refitTerminals.timeout = setTimeout(() => {
1525
- Object.values(terminals).forEach(({ fitAddon }) => {
1526
- try {
1527
- fitAddon.fit();
1528
- } catch (err) {
1529
- // Ignore fit errors
1530
- }
1531
- });
1532
- }, 50);
1292
+ // ═══════════════════════════════════════════════════════════
1293
+ // Utility
1294
+ // ═══════════════════════════════════════════════════════════
1295
+
1296
+ function escapeHtml(text) {
1297
+ const div = document.createElement('div');
1298
+ div.textContent = text;
1299
+ return div.innerHTML;
1533
1300
  }
1534
1301
 
1302
+ // ═══════════════════════════════════════════════════════════
1535
1303
  // Event Listeners
1536
- startButton.addEventListener('click', startSession);
1537
- newSessionButton.addEventListener('click', showNewSessionModal);
1538
- newSessionCancelButton.addEventListener('click', hideNewSessionModal);
1539
- newSessionConfirmButton.addEventListener('click', startNewSession);
1540
- sendMessageButton.addEventListener('click', sendUserMessage);
1304
+ // ═══════════════════════════════════════════════════════════
1305
+
1306
+ // Welcome prompt
1307
+ promptSendBtn.addEventListener('click', () => handleSendMessage(promptInput.value));
1308
+ promptInput.addEventListener('keydown', (e) => {
1309
+ if (e.key === 'Enter' && !e.shiftKey) {
1310
+ e.preventDefault();
1311
+ handleSendMessage(promptInput.value);
1312
+ }
1313
+ });
1314
+
1315
+ // Session message input
1316
+ sendMessageButton.addEventListener('click', () => handleSendMessage(userMessageInput.value));
1317
+ userMessageInput.addEventListener('keydown', (e) => {
1318
+ if (e.key === 'Enter' && !e.shiftKey) {
1319
+ e.preventDefault();
1320
+ handleSendMessage(userMessageInput.value);
1321
+ }
1322
+ });
1323
+
1324
+ // Sidebar
1325
+ newSessionButton.addEventListener('click', handleNewSessionButton);
1326
+
1327
+ changeWorkspaceButton.addEventListener('click', async () => {
1328
+ const result = await window.electronAPI.browseForWorkspace();
1329
+ if (!result.canceled) {
1330
+ // Stop any active session before switching workspace
1331
+ if (sessionState === 'active') {
1332
+ stopChatPolling();
1333
+ await window.electronAPI.resetSession();
1334
+ cleanupTerminals();
1335
+ }
1336
+
1337
+ currentWorkspace = result.path;
1338
+ sidebarWorkspaceName.textContent = result.path.split('/').pop() || result.path.split('\\').pop();
1339
+ sidebarWorkspacePath.textContent = result.path;
1340
+ sidebarWorkspacePath.title = result.path;
1341
+ await window.electronAPI.addRecentWorkspace(result.path);
1342
+ await loadSessionsList();
1343
+ showWelcomeView();
1344
+ }
1345
+ });
1346
+
1347
+ settingsButton.addEventListener('click', () => {
1348
+ window.electronAPI.openConfigFolder();
1349
+ });
1350
+
1351
+ // Right panel tabs
1352
+ mainTabTerminal.addEventListener('click', () => switchRightTab('terminal'));
1353
+ mainTabPlan.addEventListener('click', () => switchRightTab('plan'));
1354
+ mainTabDiff.addEventListener('click', () => switchRightTab('diff'));
1355
+
1356
+ // Diff/Plan buttons
1357
+ refreshDiffButton.addEventListener('click', refreshGitDiff);
1358
+ if (refreshPlanButton) refreshPlanButton.addEventListener('click', refreshPlan);
1541
1359
  startImplementingButton.addEventListener('click', showImplementationModal);
1360
+
1361
+ // Modal
1542
1362
  modalCancelButton.addEventListener('click', hideImplementationModal);
1543
1363
  modalStartButton.addEventListener('click', startImplementation);
1544
1364
 
1545
- // Workspace picker event listeners
1546
- browseWorkspaceButton.addEventListener('click', browseForWorkspace);
1547
- useCwdButton.addEventListener('click', useCwd);
1548
- editConfigButton.addEventListener('click', openConfigFolder);
1549
-
1550
- // Main tab event listeners
1551
- mainTabChat.addEventListener('click', () => switchMainTab('chat'));
1552
- mainTabPlan.addEventListener('click', () => switchMainTab('plan'));
1553
- mainTabDiff.addEventListener('click', () => switchMainTab('diff'));
1554
- refreshDiffButton.addEventListener('click', refreshGitDiff);
1555
- if (refreshPlanButton) {
1556
- refreshPlanButton.addEventListener('click', refreshPlan);
1557
- }
1365
+ // Chat scroll
1558
1366
  chatNewMessagesButton.addEventListener('click', () => {
1559
1367
  autoScrollEnabled = true;
1560
1368
  scrollChatToBottom();
1561
1369
  setNewMessagesBanner(false);
1562
1370
  });
1371
+
1563
1372
  chatViewer.addEventListener('scroll', () => {
1564
1373
  if (isChatNearBottom()) {
1565
1374
  autoScrollEnabled = true;
@@ -1569,33 +1378,18 @@ chatViewer.addEventListener('scroll', () => {
1569
1378
  }
1570
1379
  });
1571
1380
 
1572
- // Allow Enter+Shift to send message
1573
- userMessageInput.addEventListener('keydown', (e) => {
1574
- if (e.key === 'Enter' && e.shiftKey) {
1575
- e.preventDefault();
1576
- sendUserMessage();
1577
- }
1578
- });
1579
-
1580
- // Allow Enter to submit challenge
1581
- challengeInput.addEventListener('keydown', (e) => {
1582
- if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
1583
- e.preventDefault();
1584
- startSession();
1585
- }
1586
- });
1587
-
1588
1381
  // Keyboard shortcut for New Session (Cmd/Ctrl + Shift + N)
1589
1382
  document.addEventListener('keydown', (e) => {
1590
1383
  if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key === 'N') {
1591
1384
  e.preventDefault();
1592
- if (sessionScreen.classList.contains('active')) {
1593
- showNewSessionModal();
1594
- }
1385
+ handleNewSessionButton();
1595
1386
  }
1596
1387
  });
1597
1388
 
1389
+ // ═══════════════════════════════════════════════════════════
1598
1390
  // IPC Listeners
1391
+ // ═══════════════════════════════════════════════════════════
1392
+
1599
1393
  window.electronAPI.onAgentOutput((data) => {
1600
1394
  updateAgentOutput(data.agentName, data.output, data.isPty);
1601
1395
  });
@@ -1604,23 +1398,18 @@ window.electronAPI.onAgentStatus((data) => {
1604
1398
  updateAgentStatus(data.agentName, data.status, data.exitCode, data.error);
1605
1399
  });
1606
1400
 
1607
- // Listen for full chat refresh (array of messages)
1608
1401
  window.electronAPI.onChatUpdated((messages) => {
1609
1402
  updateChatFromMessages(messages);
1610
1403
  });
1611
1404
 
1612
- // Listen for new individual messages
1613
1405
  window.electronAPI.onChatMessage((message) => {
1614
1406
  addChatMessage(message);
1615
1407
  });
1616
1408
 
1617
- // Initialize on load
1618
- initialize();
1619
- initResizers();
1620
- restoreTabState();
1621
-
1622
- // Apply responsive layout on initial load
1623
- requestAnimationFrame(() => handleContainerResize());
1409
+ // ═══════════════════════════════════════════════════════════
1410
+ // Boot
1411
+ // ═══════════════════════════════════════════════════════════
1624
1412
 
1625
- // Add window resize listener to keep panels responsive
1413
+ initializeApp();
1414
+ initResizers();
1626
1415
  window.addEventListener('resize', onWindowResize);