@appoly/multiagent-chat 1.0.3 → 1.0.7

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
+ }
123
163
 
124
- // Get current directory info
125
- cwdInfo = await window.electronAPI.getCurrentDirectory();
164
+ function showRightPanel() {
165
+ rightPanel.style.display = 'flex';
166
+ // Restore layout after showing
167
+ requestAnimationFrame(() => {
168
+ restoreLayout();
169
+ refitTerminals();
170
+ });
171
+ }
126
172
 
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
- }
173
+ function hideRightPanel() {
174
+ rightPanel.style.display = 'none';
175
+ }
134
176
 
135
- // Render workspace list
136
- renderWorkspaceList();
177
+ // ═══════════════════════════════════════════════════════════
178
+ // Session List (Sidebar)
179
+ // ═══════════════════════════════════════════════════════════
137
180
 
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
- }
181
+ async function loadSessionsList() {
182
+ if (!currentWorkspace) return;
183
+
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>
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);
223
+ }
224
+ }
225
+
226
+ let html = '';
227
+
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>
177
245
  </div>
178
246
  `;
179
247
  }
248
+ return groupHtml;
249
+ };
180
250
 
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>' : ''}
186
- </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('');
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);
193
255
 
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
- });
211
-
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,244 @@ 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
+ if (result.initialMessage) {
315
+ chatMessages = [result.initialMessage];
316
+ } else if (Array.isArray(result.messages)) {
317
+ chatMessages = result.messages;
318
+ } else {
319
+ chatMessages = [];
320
+ }
321
+ sessionState = 'active';
322
+
323
+ result.agents.forEach(agent => {
324
+ agentData[agent.name] = {
325
+ ...agentData[agent.name],
326
+ name: agent.name,
327
+ status: agentData[agent.name]?.status || 'starting',
328
+ output: agentData[agent.name]?.output || [],
329
+ use_pty: agent.use_pty
330
+ };
331
+ });
332
+
333
+ // Switch to session view
334
+ showSessionView();
335
+ showRightPanel();
336
+ createAgentTabs(result.agents);
337
+ renderChatMessages();
338
+ startChatPolling();
339
+
340
+ // Refresh sessions list
341
+ await loadSessionsList();
342
+ } else {
343
+ alert(`Failed to start session: ${result.error}`);
344
+ }
345
+ } catch (error) {
346
+ console.error('Error starting session:', error);
347
+ alert('Error starting session. Check console for details.');
348
+ } finally {
349
+ promptSendBtn.disabled = false;
350
+ promptSendBtn.classList.remove('loading');
295
351
  }
296
352
  }
297
353
 
298
- // Open config folder
299
- async function openConfigFolder() {
300
- await window.electronAPI.openConfigFolder();
301
- }
354
+ // Load past session (click sidebar) - dormant, no agents
355
+ async function handleLoadSession(sessionId) {
356
+ try {
357
+ // Stop any running agents first
358
+ if (sessionState === 'active') {
359
+ stopChatPolling();
360
+ await window.electronAPI.resetSession();
361
+ cleanupTerminals();
362
+ }
302
363
 
303
- // Display configuration info
304
- function displayConfig() {
305
- if (!currentConfig) {
306
- configDetails.innerHTML = '<span style="color: #dc3545;">No configuration loaded</span>';
307
- return;
308
- }
364
+ const result = await window.electronAPI.loadSession(currentWorkspace, sessionId);
309
365
 
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>');
366
+ if (result.success) {
367
+ currentSessionId = sessionId;
368
+ agentColors = result.colors || {};
369
+ chatMessages = result.messages || [];
370
+ sessionState = 'loaded';
314
371
 
315
- configDetails.innerHTML = `
316
- <strong>Agents:</strong><br>${agentList}
317
- `;
318
- }
372
+ // Show session view, hide right panel (dormant - no agents)
373
+ showSessionView();
374
+ hideRightPanel();
319
375
 
320
- // Start session
321
- async function startSession() {
322
- const challenge = challengeInput.value.trim();
376
+ // Render chat history
377
+ renderChatMessages();
323
378
 
324
- if (!challenge) {
325
- alert('Please enter a challenge for the agents to work on.');
326
- return;
327
- }
379
+ // Update sidebar highlighting
380
+ renderSessionsList();
328
381
 
329
- if (!selectedWorkspace) {
330
- alert('Please select a workspace first.');
331
- return;
382
+ // Set placeholder to indicate resume behavior
383
+ userMessageInput.placeholder = 'Type a message to resume this session with agents...';
384
+ } else {
385
+ console.error('Failed to load session:', result.error);
386
+ }
387
+ } catch (error) {
388
+ console.error('Error loading session:', error);
332
389
  }
390
+ }
333
391
 
334
- startButton.disabled = true;
335
- startButton.textContent = 'Starting...';
392
+ // Resume session: user typed while dormant (loaded state)
393
+ async function handleResumeSession(newMessage) {
394
+ sendMessageButton.disabled = true;
395
+ sendMessageButton.classList.add('loading');
336
396
 
337
397
  try {
338
- const result = await window.electronAPI.startSession({
339
- challenge,
340
- workspace: selectedWorkspace
341
- });
398
+ const result = await window.electronAPI.resumeSession(currentWorkspace, currentSessionId, newMessage);
342
399
 
343
400
  if (result.success) {
344
- // Initialize agent data and colors
345
401
  agentColors = result.colors || {};
346
- chatMessages = []; // Reset chat messages
402
+ sessionState = 'active';
347
403
 
348
404
  result.agents.forEach(agent => {
349
405
  agentData[agent.name] = {
406
+ ...agentData[agent.name],
350
407
  name: agent.name,
351
- status: 'starting',
352
- output: [],
408
+ status: agentData[agent.name]?.status || 'starting',
409
+ output: agentData[agent.name]?.output || [],
353
410
  use_pty: agent.use_pty
354
411
  };
355
412
  });
356
413
 
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
- }
414
+ // Show right panel, create terminals
415
+ showRightPanel();
366
416
  createAgentTabs(result.agents);
367
- renderChatMessages(); // Initial render (empty)
368
417
  startChatPolling();
418
+
419
+ // Reset placeholder
420
+ userMessageInput.placeholder = 'Send a message to all agents... (Enter to send)';
421
+ userMessageInput.value = '';
422
+
423
+ // Refresh sessions list
424
+ await loadSessionsList();
369
425
  } else {
370
- alert(`Failed to start session: ${result.error}`);
371
- startButton.disabled = false;
372
- startButton.textContent = 'Start Session';
426
+ alert(`Failed to resume session: ${result.error}`);
373
427
  }
374
428
  } 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';
429
+ console.error('Error resuming session:', error);
430
+ alert('Error resuming session. Check console for details.');
431
+ } finally {
432
+ sendMessageButton.disabled = false;
433
+ sendMessageButton.classList.remove('loading');
434
+ }
435
+ }
436
+
437
+ // Send user message to active session
438
+ async function handleSendUserMessage(message) {
439
+ sendMessageButton.disabled = true;
440
+ sendMessageButton.classList.add('loading');
441
+
442
+ try {
443
+ const result = await window.electronAPI.sendUserMessage(message);
444
+ if (result.success) {
445
+ userMessageInput.value = '';
446
+ } else {
447
+ alert(`Failed to send message: ${result.error}`);
448
+ }
449
+ } catch (error) {
450
+ console.error('Error sending message:', error);
451
+ alert('Error sending message. Check console for details.');
452
+ } finally {
453
+ sendMessageButton.disabled = false;
454
+ sendMessageButton.classList.remove('loading');
455
+ }
456
+ }
457
+
458
+ // New session button - return to welcome view
459
+ async function handleNewSessionButton() {
460
+ if (sessionState === 'active') {
461
+ stopChatPolling();
462
+ await window.electronAPI.resetSession();
463
+ cleanupTerminals();
464
+ }
465
+
466
+ // Reset UI state
467
+ chatMessages = [];
468
+ planHasContent = false;
469
+ implementationStarted = false;
470
+ agentData = {};
471
+ currentAgentTab = null;
472
+ autoScrollEnabled = true;
473
+ lastDiffData = null;
474
+ parsedDiffFiles = [];
475
+ selectedDiffFile = null;
476
+ agentTabsContainer.innerHTML = '';
477
+ agentOutputsContainer.innerHTML = '';
478
+
479
+ showWelcomeView();
480
+
481
+ // Refresh sessions list
482
+ await loadSessionsList();
483
+ }
484
+
485
+ function cleanupTerminals() {
486
+ Object.values(terminals).forEach(({ terminal }) => {
487
+ try { terminal.dispose(); } catch (e) { /* ignore */ }
488
+ });
489
+ terminals = {};
490
+ }
491
+
492
+ // ═══════════════════════════════════════════════════════════
493
+ // Display Config
494
+ // ═══════════════════════════════════════════════════════════
495
+
496
+ function displayConfig() {
497
+ if (!currentConfig) {
498
+ configDetails.innerHTML = '<span style="color: #dc3545;">No configuration loaded</span>';
499
+ return;
379
500
  }
501
+
502
+ const agentList = currentConfig.agents.map(a => {
503
+ const color = a.color || '#667eea';
504
+ return `<span style="color: ${color}">&#8226; ${a.name}</span> (${a.command})`;
505
+ }).join('<br>');
506
+
507
+ configDetails.innerHTML = `<strong>Agents:</strong><br>${agentList}`;
380
508
  }
381
509
 
382
- // Create agent tabs
510
+ // ═══════════════════════════════════════════════════════════
511
+ // Agent Tabs & Terminal
512
+ // ═══════════════════════════════════════════════════════════
513
+
383
514
  function createAgentTabs(agents) {
384
515
  agentTabsContainer.innerHTML = '';
385
516
  agentOutputsContainer.innerHTML = '';
@@ -387,7 +518,6 @@ function createAgentTabs(agents) {
387
518
  agents.forEach((agent, index) => {
388
519
  const agentInfo = agentData[agent.name];
389
520
 
390
- // Create tab
391
521
  const tab = document.createElement('button');
392
522
  tab.className = 'tab';
393
523
  if (index === 0) {
@@ -398,37 +528,37 @@ function createAgentTabs(agents) {
398
528
  tab.onclick = () => switchAgentTab(agent.name);
399
529
  agentTabsContainer.appendChild(tab);
400
530
 
401
- // Create output container
402
531
  const outputDiv = document.createElement('div');
403
532
  outputDiv.className = 'agent-output';
404
533
  outputDiv.id = `output-${agent.name}`;
405
- if (index === 0) {
406
- outputDiv.classList.add('active');
407
- }
534
+ if (index === 0) outputDiv.classList.add('active');
408
535
 
409
- // Add status indicator
410
536
  const statusDiv = document.createElement('div');
411
- statusDiv.className = 'agent-status starting';
412
- statusDiv.textContent = 'Starting...';
537
+ const knownStatus = agentInfo?.status || 'starting';
538
+ statusDiv.className = `agent-status ${knownStatus}`;
413
539
  statusDiv.id = `status-${agent.name}`;
540
+ if (knownStatus === 'running') {
541
+ statusDiv.textContent = 'Running';
542
+ } else if (knownStatus === 'stopped') {
543
+ statusDiv.textContent = 'Stopped';
544
+ } else if (knownStatus === 'error') {
545
+ statusDiv.textContent = 'Error';
546
+ } else {
547
+ statusDiv.textContent = 'Starting...';
548
+ }
414
549
  outputDiv.appendChild(statusDiv);
415
550
 
416
551
  if (agentInfo && agentInfo.use_pty) {
417
- // Create xterm terminal for PTY agents
418
552
  const terminalDiv = document.createElement('div');
419
553
  terminalDiv.id = `terminal-${agent.name}`;
420
554
  terminalDiv.className = 'terminal-container';
421
555
  outputDiv.appendChild(terminalDiv);
422
556
 
423
- // Create and initialize terminal
424
557
  const terminal = new Terminal({
425
558
  cursorBlink: true,
426
559
  fontSize: 13,
427
560
  fontFamily: 'Courier New, monospace',
428
- theme: {
429
- background: '#1e1e1e',
430
- foreground: '#e0e0e0'
431
- },
561
+ theme: getTerminalTheme(),
432
562
  rows: 40,
433
563
  cols: 120
434
564
  });
@@ -439,35 +569,27 @@ function createAgentTabs(agents) {
439
569
  fitAddon.fit();
440
570
 
441
571
  terminals[agent.name] = { terminal, fitAddon };
442
-
443
- // Initialize input lock state (default: locked)
444
572
  inputLocked[agent.name] = true;
445
-
446
- // Disable stdin by default (prevents cursor/typing when locked)
447
573
  terminal.options.disableStdin = true;
448
574
 
449
- // Add lock toggle button (inside terminal container so it stays anchored)
450
575
  const lockToggle = document.createElement('button');
451
576
  lockToggle.className = 'input-lock-toggle';
452
577
  lockToggle.innerHTML = '🔒 Input locked';
453
578
  lockToggle.onclick = () => toggleInputLock(agent.name);
454
579
  terminalDiv.appendChild(lockToggle);
455
580
 
456
- // Wire terminal input to PTY (only when unlocked)
457
581
  terminal.onData((data) => {
458
582
  if (!inputLocked[agent.name]) {
459
583
  window.electronAPI.sendPtyInput(agent.name, data);
460
584
  }
461
585
  });
462
586
 
463
- // Fit terminal on window resize
464
587
  window.addEventListener('resize', () => {
465
588
  if (terminals[agent.name]) {
466
589
  terminals[agent.name].fitAddon.fit();
467
590
  }
468
591
  });
469
592
  } else {
470
- // Create regular text output for non-PTY agents
471
593
  const contentPre = document.createElement('pre');
472
594
  contentPre.id = `content-${agent.name}`;
473
595
  outputDiv.appendChild(contentPre);
@@ -477,37 +599,22 @@ function createAgentTabs(agents) {
477
599
  });
478
600
  }
479
601
 
480
- // Switch agent tab
481
602
  function switchAgentTab(agentName) {
482
603
  currentAgentTab = agentName;
483
604
 
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
- }
605
+ document.querySelectorAll('#agent-tabs .tab').forEach(tab => {
606
+ tab.classList.toggle('active', tab.textContent === agentName);
491
607
  });
492
608
 
493
- // Update outputs
494
609
  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
- }
610
+ output.classList.toggle('active', output.id === `output-${agentName}`);
500
611
  });
501
612
 
502
- // Fit terminal if this agent uses PTY
503
613
  if (terminals[agentName]) {
504
- setTimeout(() => {
505
- terminals[agentName].fitAddon.fit();
506
- }, 100);
614
+ setTimeout(() => terminals[agentName].fitAddon.fit(), 100);
507
615
  }
508
616
  }
509
617
 
510
- // Toggle input lock for a terminal
511
618
  function toggleInputLock(agentName) {
512
619
  inputLocked[agentName] = !inputLocked[agentName];
513
620
  const toggle = document.querySelector(`#terminal-${agentName} .input-lock-toggle`);
@@ -516,17 +623,13 @@ function toggleInputLock(agentName) {
516
623
  if (inputLocked[agentName]) {
517
624
  toggle.innerHTML = '🔒 Input locked';
518
625
  toggle.classList.remove('unlocked');
519
- // Disable stdin and blur terminal when locked
520
626
  if (terminal) {
521
627
  terminal.options.disableStdin = true;
522
- if (terminal.textarea) {
523
- terminal.textarea.blur();
524
- }
628
+ if (terminal.textarea) terminal.textarea.blur();
525
629
  }
526
630
  } else {
527
631
  toggle.innerHTML = '🔓 Input unlocked';
528
632
  toggle.classList.add('unlocked');
529
- // Enable stdin and focus terminal when unlocked
530
633
  if (terminal) {
531
634
  terminal.options.disableStdin = false;
532
635
  terminal.focus();
@@ -534,47 +637,40 @@ function toggleInputLock(agentName) {
534
637
  }
535
638
  }
536
639
 
537
- // Update agent output
538
640
  function updateAgentOutput(agentName, output, isPty) {
539
641
  if (!agentData[agentName]) {
540
642
  agentData[agentName] = { name: agentName, output: [] };
541
643
  }
542
-
543
644
  agentData[agentName].output.push(output);
544
645
 
545
- // Check if this agent uses PTY/terminal
546
646
  if (isPty && terminals[agentName]) {
547
- // Write directly to xterm terminal
548
647
  terminals[agentName].terminal.write(output);
549
648
  } else {
550
- // Use regular text output
551
649
  const contentElement = document.getElementById(`content-${agentName}`);
552
650
  if (contentElement) {
553
651
  contentElement.textContent = agentData[agentName].output.join('');
554
-
555
- // Auto-scroll if this is the active tab
556
652
  if (currentAgentTab === agentName) {
557
653
  const outputContainer = document.getElementById(`output-${agentName}`);
558
- if (outputContainer) {
559
- outputContainer.scrollTop = outputContainer.scrollHeight;
560
- }
654
+ if (outputContainer) outputContainer.scrollTop = outputContainer.scrollHeight;
561
655
  }
562
656
  }
563
657
  }
564
658
  }
565
659
 
566
- // Update agent status
567
660
  function updateAgentStatus(agentName, status, exitCode = null, error = null) {
568
- if (agentData[agentName]) {
661
+ if (!agentData[agentName]) {
662
+ agentData[agentName] = { name: agentName, status, output: [] };
663
+ } else {
569
664
  agentData[agentName].status = status;
570
665
  }
571
666
 
572
667
  const statusElement = document.getElementById(`status-${agentName}`);
573
668
  if (statusElement) {
574
669
  statusElement.className = `agent-status ${status}`;
575
-
576
670
  if (status === 'running') {
577
671
  statusElement.textContent = 'Running';
672
+ } else if (status === 'restarting') {
673
+ statusElement.textContent = 'Restarting...';
578
674
  } else if (status === 'stopped') {
579
675
  statusElement.textContent = `Stopped (exit code: ${exitCode})`;
580
676
  } else if (status === 'error') {
@@ -583,19 +679,19 @@ function updateAgentStatus(agentName, status, exitCode = null, error = null) {
583
679
  }
584
680
  }
585
681
 
586
- // Format timestamp for display
682
+ // ═══════════════════════════════════════════════════════════
683
+ // Chat
684
+ // ═══════════════════════════════════════════════════════════
685
+
587
686
  function formatTimestamp(isoString) {
588
687
  const date = new Date(isoString);
589
688
  return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
590
689
  }
591
690
 
592
- // Render a single chat message as HTML
593
691
  function renderChatMessage(message) {
594
692
  const isUser = message.type === 'user';
595
693
  const alignClass = isUser ? 'chat-message-right' : 'chat-message-left';
596
694
  const color = message.color || agentColors[message.agent?.toLowerCase()] || '#667eea';
597
-
598
- // Parse markdown content
599
695
  const htmlContent = marked.parse(message.content || '');
600
696
 
601
697
  return `
@@ -611,88 +707,110 @@ function renderChatMessage(message) {
611
707
  `;
612
708
  }
613
709
 
614
- // Render all chat messages
615
710
  function renderChatMessages() {
616
711
  if (chatMessages.length === 0) {
617
- chatViewer.innerHTML = '<div class="chat-empty">No messages yet. Agents are starting...</div>';
712
+ if (sessionState === 'active') {
713
+ chatViewer.innerHTML = '<div class="chat-empty">No messages yet. Agents are starting...</div>';
714
+ } else if (sessionState === 'loaded') {
715
+ chatViewer.innerHTML = '<div class="chat-empty">Empty session. Type a message to start.</div>';
716
+ } else {
717
+ chatViewer.innerHTML = '';
718
+ }
618
719
  setNewMessagesBanner(false);
619
720
  return;
620
721
  }
621
722
 
622
723
  chatViewer.innerHTML = chatMessages.map(renderChatMessage).join('');
623
- if (autoScrollEnabled) {
624
- scrollChatToBottom();
625
- }
724
+ if (autoScrollEnabled) scrollChatToBottom();
626
725
  }
627
726
 
628
- // Add a new message to chat
629
727
  function addChatMessage(message) {
630
- // Check if message already exists (by sequence number)
631
728
  const exists = chatMessages.some(m => m.seq === message.seq);
632
729
  if (!exists) {
633
730
  const shouldScroll = autoScrollEnabled;
634
731
  chatMessages.push(message);
635
- chatMessages.sort((a, b) => a.seq - b.seq); // Ensure order
732
+ chatMessages.sort((a, b) => a.seq - b.seq);
636
733
  renderChatMessages();
637
- if (!shouldScroll) {
638
- setNewMessagesBanner(true);
639
- }
734
+ if (!shouldScroll) setNewMessagesBanner(true);
735
+ updateSidebarMessageCount();
640
736
  }
641
737
  }
642
738
 
643
- // Update chat from full message array (for refresh/sync)
644
739
  function updateChatFromMessages(messages) {
645
740
  if (Array.isArray(messages)) {
646
741
  const prevLastSeq = chatMessages.length > 0 ? chatMessages[chatMessages.length - 1].seq : null;
647
742
  const nextLastSeq = messages.length > 0 ? messages[messages.length - 1].seq : null;
648
743
  const hasChanges = messages.length !== chatMessages.length || prevLastSeq !== nextLastSeq;
649
- if (!hasChanges) {
650
- return;
651
- }
744
+ if (!hasChanges) return;
652
745
 
653
746
  const shouldScroll = autoScrollEnabled;
654
747
  chatMessages = messages;
655
748
  renderChatMessages();
656
- if (!shouldScroll) {
657
- setNewMessagesBanner(true);
658
- }
749
+ if (!shouldScroll) setNewMessagesBanner(true);
750
+ updateSidebarMessageCount();
659
751
  }
660
752
  }
661
753
 
662
- // Send user message
663
- async function sendUserMessage() {
664
- const message = userMessageInput.value.trim();
665
-
666
- if (!message) {
667
- return;
754
+ function updateSidebarMessageCount() {
755
+ if (!currentSessionId) return;
756
+ const currentSession = sessionsList.find(s => s.id === currentSessionId);
757
+ if (currentSession && currentSession.messageCount !== chatMessages.length) {
758
+ currentSession.messageCount = chatMessages.length;
759
+ currentSession.lastActiveAt = new Date().toISOString();
760
+ renderSessionsList();
668
761
  }
762
+ }
669
763
 
670
- sendMessageButton.disabled = true;
671
- sendMessageButton.textContent = 'Sending...';
764
+ function isChatNearBottom() {
765
+ return chatViewer.scrollHeight - chatViewer.scrollTop - chatViewer.clientHeight <= CHAT_SCROLL_THRESHOLD;
766
+ }
672
767
 
673
- try {
674
- const result = await window.electronAPI.sendUserMessage(message);
768
+ function scrollChatToBottom() {
769
+ chatViewer.scrollTop = chatViewer.scrollHeight;
770
+ }
675
771
 
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';
772
+ function setNewMessagesBanner(visible) {
773
+ if (!chatNewMessages) return;
774
+ chatNewMessages.classList.toggle('visible', visible);
775
+ chatNewMessages.setAttribute('aria-hidden', visible ? 'false' : 'true');
776
+ }
777
+
778
+ // ═══════════════════════════════════════════════════════════
779
+ // Right Panel Tabs (Terminal / Plan / Diff)
780
+ // ═══════════════════════════════════════════════════════════
781
+
782
+ function switchRightTab(tabName) {
783
+ currentRightTab = tabName;
784
+
785
+ mainTabTerminal.classList.toggle('active', tabName === 'terminal');
786
+ mainTabPlan.classList.toggle('active', tabName === 'plan');
787
+ mainTabDiff.classList.toggle('active', tabName === 'diff');
788
+
789
+ terminalTabContent.classList.toggle('active', tabName === 'terminal');
790
+ planTabContent.classList.toggle('active', tabName === 'plan');
791
+ diffTabContent.classList.toggle('active', tabName === 'diff');
792
+
793
+ if (tabName === 'terminal') {
794
+ requestAnimationFrame(() => refitTerminals());
795
+ }
796
+
797
+ if (tabName === 'diff') {
798
+ refreshGitDiff();
799
+ requestAnimationFrame(() => restoreDiffLayout());
800
+ }
801
+
802
+ if (tabName === 'plan') {
803
+ refreshPlan();
688
804
  }
689
805
  }
690
806
 
691
- // Refresh plan and update button visibility
807
+ // ═══════════════════════════════════════════════════════════
808
+ // Plan
809
+ // ═══════════════════════════════════════════════════════════
810
+
692
811
  async function refreshPlan() {
693
812
  try {
694
813
  const content = await window.electronAPI.getPlanContent();
695
-
696
814
  if (content.trim()) {
697
815
  const htmlContent = marked.parse(content);
698
816
  planViewer.innerHTML = `<div class="markdown-content">${htmlContent}</div>`;
@@ -701,15 +819,12 @@ async function refreshPlan() {
701
819
  planViewer.innerHTML = '<em>No plan yet...</em>';
702
820
  planHasContent = false;
703
821
  }
704
-
705
- // Update button visibility
706
822
  updateImplementButtonState();
707
823
  } catch (error) {
708
824
  console.error('Error refreshing plan:', error);
709
825
  }
710
826
  }
711
827
 
712
- // Update the Start Implementing button state
713
828
  function updateImplementButtonState() {
714
829
  if (implementationStarted) {
715
830
  startImplementingButton.textContent = 'Implementation in progress';
@@ -724,56 +839,10 @@ function updateImplementButtonState() {
724
839
  }
725
840
  }
726
841
 
727
- // Switch between main tabs (Chat, Plan, Diff)
728
- function switchMainTab(tabName) {
729
- currentMainTab = tabName;
730
-
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');
842
+ // ═══════════════════════════════════════════════════════════
843
+ // Diff
844
+ // ═══════════════════════════════════════════════════════════
740
845
 
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
846
  async function refreshGitDiff() {
778
847
  try {
779
848
  const data = await window.electronAPI.getGitDiff();
@@ -786,7 +855,6 @@ async function refreshGitDiff() {
786
855
  }
787
856
  }
788
857
 
789
- // Parse diff into per-file blocks
790
858
  function parseDiffIntoFiles(diffText) {
791
859
  if (!diffText || !diffText.trim()) return [];
792
860
 
@@ -797,35 +865,21 @@ function parseDiffIntoFiles(diffText) {
797
865
 
798
866
  for (const line of lines) {
799
867
  if (line.startsWith('diff --git')) {
800
- // Save previous file if exists
801
868
  if (currentFile) {
802
869
  currentFile.content = currentContent.join('\n');
803
870
  files.push(currentFile);
804
871
  }
805
-
806
- // Extract filename from diff header
807
872
  const match = line.match(/diff --git a\/(.+?) b\/(.+)/);
808
873
  const filename = match ? match[2] : 'unknown';
809
-
810
- currentFile = {
811
- filename,
812
- status: 'modified',
813
- content: ''
814
- };
874
+ currentFile = { filename, status: 'modified', content: '' };
815
875
  currentContent = [line];
816
876
  } else if (currentFile) {
817
877
  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
- }
878
+ if (line.startsWith('new file mode')) currentFile.status = 'added';
879
+ else if (line.startsWith('deleted file mode')) currentFile.status = 'deleted';
825
880
  }
826
881
  }
827
882
 
828
- // Don't forget the last file
829
883
  if (currentFile) {
830
884
  currentFile.content = currentContent.join('\n');
831
885
  files.push(currentFile);
@@ -834,28 +888,22 @@ function parseDiffIntoFiles(diffText) {
834
888
  return files;
835
889
  }
836
890
 
837
- // Render file list in diff view
838
891
  function renderDiffFileList(files, untracked) {
839
892
  if (!diffFileListItems) return;
840
893
 
841
894
  let html = '';
842
-
843
- // Add "All Files" option
844
895
  const allActive = selectedDiffFile === null ? 'active' : '';
845
896
  html += `<li class="file-list-item all-files ${allActive}" data-file="__all__">All Files</li>`;
846
897
 
847
- // Add changed files
848
898
  for (const file of files) {
849
899
  const isActive = selectedDiffFile === file.filename ? 'active' : '';
850
- const statusClass = file.status;
851
900
  const statusLabel = file.status === 'added' ? 'A' : file.status === 'deleted' ? 'D' : 'M';
852
901
  html += `<li class="file-list-item ${isActive}" data-file="${escapeHtml(file.filename)}">
853
- <span class="file-status ${statusClass}">${statusLabel}</span>
902
+ <span class="file-status ${file.status}">${statusLabel}</span>
854
903
  <span class="file-name">${escapeHtml(file.filename)}</span>
855
904
  </li>`;
856
905
  }
857
906
 
858
- // Add untracked files
859
907
  for (const file of (untracked || [])) {
860
908
  const isActive = selectedDiffFile === file ? 'active' : '';
861
909
  html += `<li class="file-list-item ${isActive}" data-file="${escapeHtml(file)}" data-untracked="true">
@@ -870,22 +918,16 @@ function renderDiffFileList(files, untracked) {
870
918
 
871
919
  diffFileListItems.innerHTML = html;
872
920
 
873
- // Add click handlers (skip items without data-file attribute)
874
921
  diffFileListItems.querySelectorAll('.file-list-item[data-file]').forEach(item => {
875
922
  item.addEventListener('click', () => {
876
923
  const file = item.dataset.file;
877
- if (file === '__all__') {
878
- selectedDiffFile = null;
879
- } else {
880
- selectedDiffFile = file;
881
- }
924
+ selectedDiffFile = file === '__all__' ? null : file;
882
925
  renderDiffFileList(parsedDiffFiles, lastDiffData?.untracked);
883
926
  renderDiffContent();
884
927
  });
885
928
  });
886
929
  }
887
930
 
888
- // Render diff content based on selected file
889
931
  function renderDiffContent() {
890
932
  if (!lastDiffData) {
891
933
  diffContent.innerHTML = '<em class="diff-empty">No diff data available</em>';
@@ -893,25 +935,21 @@ function renderDiffContent() {
893
935
  }
894
936
 
895
937
  if (selectedDiffFile === null) {
896
- // Show all files
897
938
  if (lastDiffData.diff && lastDiffData.diff.trim()) {
898
939
  diffContent.innerHTML = `<pre class="diff-output">${formatDiffOutput(lastDiffData.diff)}</pre>`;
899
940
  } else {
900
941
  diffContent.innerHTML = '<em class="diff-empty">No uncommitted changes</em>';
901
942
  }
902
943
  } else {
903
- // Show specific file
904
944
  const file = parsedDiffFiles.find(f => f.filename === selectedDiffFile);
905
945
  if (file) {
906
946
  diffContent.innerHTML = `<pre class="diff-output">${formatDiffOutput(file.content)}</pre>`;
907
947
  } else {
908
- // Might be an untracked file
909
948
  diffContent.innerHTML = `<em class="diff-empty">Untracked file: ${escapeHtml(selectedDiffFile)}</em>`;
910
949
  }
911
950
  }
912
951
  }
913
952
 
914
- // Render git diff data
915
953
  function renderGitDiff(data) {
916
954
  if (!data.isGitRepo) {
917
955
  diffStats.innerHTML = '';
@@ -929,10 +967,8 @@ function renderGitDiff(data) {
929
967
  return;
930
968
  }
931
969
 
932
- // Parse diff into files
933
970
  parsedDiffFiles = parseDiffIntoFiles(data.diff);
934
971
 
935
- // Render stats
936
972
  const { filesChanged, insertions, deletions } = data.stats;
937
973
  if (filesChanged > 0 || insertions > 0 || deletions > 0) {
938
974
  diffStats.innerHTML = `
@@ -944,48 +980,33 @@ function renderGitDiff(data) {
944
980
  diffStats.innerHTML = '<span class="diff-stat-none">No changes</span>';
945
981
  }
946
982
 
947
- // Render file list
948
983
  renderDiffFileList(parsedDiffFiles, data.untracked);
949
-
950
- // Render diff content
951
984
  renderDiffContent();
952
985
 
953
- // Render untracked files summary (below diff)
954
986
  if (data.untracked && data.untracked.length > 0) {
955
- diffUntracked.innerHTML = `
956
- <div class="diff-untracked-header">Untracked files (${data.untracked.length})</div>
957
- `;
987
+ diffUntracked.innerHTML = `<div class="diff-untracked-header">Untracked files (${data.untracked.length})</div>`;
958
988
  } else {
959
989
  diffUntracked.innerHTML = '';
960
990
  }
961
991
  }
962
992
 
963
- // Format diff output with syntax highlighting
964
993
  function formatDiffOutput(diff) {
965
994
  return diff.split('\n').map(line => {
966
995
  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');
996
+ if (line.startsWith('+++') || line.startsWith('---')) return `<span class="diff-file-header">${escaped}</span>`;
997
+ if (line.startsWith('@@')) return `<span class="diff-hunk-header">${escaped}</span>`;
998
+ if (line.startsWith('+')) return `<span class="diff-added">${escaped}</span>`;
999
+ if (line.startsWith('-')) return `<span class="diff-removed">${escaped}</span>`;
1000
+ if (line.startsWith('diff --git')) return `<span class="diff-file-separator">${escaped}</span>`;
1001
+ return `<span class="diff-context">${escaped}</span>`;
1002
+ }).join('');
980
1003
  }
981
1004
 
982
- // Update diff badge to show when changes exist
983
1005
  function updateDiffBadge(data) {
984
1006
  if (!data || !data.isGitRepo) {
985
1007
  diffBadge.style.display = 'none';
986
1008
  return;
987
1009
  }
988
-
989
1010
  const totalChanges = (data.stats?.filesChanged || 0) + (data.untracked?.length || 0);
990
1011
  if (totalChanges > 0) {
991
1012
  diffBadge.textContent = totalChanges;
@@ -995,11 +1016,12 @@ function updateDiffBadge(data) {
995
1016
  }
996
1017
  }
997
1018
 
998
- // Show implementation modal
1019
+ // ═══════════════════════════════════════════════════════════
1020
+ // Implementation Modal
1021
+ // ═══════════════════════════════════════════════════════════
1022
+
999
1023
  function showImplementationModal() {
1000
- // Populate agent selection with enabled agents
1001
1024
  agentSelectionContainer.innerHTML = '';
1002
-
1003
1025
  const enabledAgents = currentConfig.agents || [];
1004
1026
 
1005
1027
  enabledAgents.forEach((agent, index) => {
@@ -1008,9 +1030,7 @@ function showImplementationModal() {
1008
1030
  radio.type = 'radio';
1009
1031
  radio.name = 'implementer';
1010
1032
  radio.value = agent.name;
1011
- if (index === 0 || enabledAgents.length === 1) {
1012
- radio.checked = true; // Auto-select first (or only) agent
1013
- }
1033
+ if (index === 0 || enabledAgents.length === 1) radio.checked = true;
1014
1034
 
1015
1035
  const agentNameSpan = document.createElement('span');
1016
1036
  agentNameSpan.className = 'agent-name';
@@ -1025,12 +1045,10 @@ function showImplementationModal() {
1025
1045
  implementationModal.style.display = 'flex';
1026
1046
  }
1027
1047
 
1028
- // Hide implementation modal
1029
1048
  function hideImplementationModal() {
1030
1049
  implementationModal.style.display = 'none';
1031
1050
  }
1032
1051
 
1033
- // Start implementation with selected agent
1034
1052
  async function startImplementation() {
1035
1053
  const selectedRadio = document.querySelector('input[name="implementer"]:checked');
1036
1054
  if (!selectedRadio) {
@@ -1043,8 +1061,6 @@ async function startImplementation() {
1043
1061
  const otherAgents = allAgents.filter(name => name !== selectedAgent);
1044
1062
 
1045
1063
  hideImplementationModal();
1046
-
1047
- // Mark implementation as started
1048
1064
  implementationStarted = true;
1049
1065
  updateImplementButtonState();
1050
1066
 
@@ -1057,163 +1073,49 @@ async function startImplementation() {
1057
1073
  }
1058
1074
  } catch (error) {
1059
1075
  console.error('Error starting implementation:', error);
1060
- alert('Error starting implementation. Check console for details.');
1061
1076
  implementationStarted = false;
1062
1077
  updateImplementButtonState();
1063
1078
  }
1064
1079
  }
1065
1080
 
1066
- // Stop all polling intervals
1081
+ // ═══════════════════════════════════════════════════════════
1082
+ // Polling
1083
+ // ═══════════════════════════════════════════════════════════
1084
+
1067
1085
  function stopChatPolling() {
1068
1086
  pollingIntervals.forEach(id => clearInterval(id));
1069
1087
  pollingIntervals = [];
1070
1088
  }
1071
1089
 
1072
- // Start polling chat content (fallback if file watcher has issues)
1073
1090
  function startChatPolling() {
1074
- // Clear any existing intervals first
1075
1091
  stopChatPolling();
1076
1092
 
1077
1093
  pollingIntervals.push(setInterval(async () => {
1078
1094
  try {
1079
1095
  const messages = await window.electronAPI.getChatContent();
1080
- if (messages && messages.length > 0) {
1081
- updateChatFromMessages(messages);
1082
- }
1096
+ if (messages && messages.length > 0) updateChatFromMessages(messages);
1083
1097
  } catch (error) {
1084
1098
  console.error('Error polling chat:', error);
1085
1099
  }
1086
1100
  }, 2000));
1087
1101
 
1088
- // Also poll plan
1089
1102
  pollingIntervals.push(setInterval(refreshPlan, 3000));
1090
1103
 
1091
- // Poll for diff updates (also updates badge even when not on diff tab)
1092
1104
  pollingIntervals.push(setInterval(async () => {
1093
1105
  try {
1094
1106
  const data = await window.electronAPI.getGitDiff();
1095
1107
  lastDiffData = data;
1096
1108
  updateDiffBadge(data);
1097
- // Only re-render if diff tab is active
1098
- if (currentMainTab === 'diff') {
1099
- renderGitDiff(data);
1100
- }
1109
+ if (currentRightTab === 'diff') renderGitDiff(data);
1101
1110
  } catch (error) {
1102
1111
  console.error('Error polling git diff:', error);
1103
1112
  }
1104
1113
  }, 5000));
1105
1114
  }
1106
1115
 
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
-
1116
+ // ═══════════════════════════════════════════════════════════
1215
1117
  // Resize Handles
1216
- const LAYOUT_STORAGE_KEY = 'multiagent-layout';
1118
+ // ═══════════════════════════════════════════════════════════
1217
1119
 
1218
1120
  function initResizers() {
1219
1121
  const mainHandle = document.querySelector('[data-resize="main"]');
@@ -1223,11 +1125,11 @@ function initResizers() {
1223
1125
  setupResizer({
1224
1126
  handle: mainHandle,
1225
1127
  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,
1128
+ container: document.querySelector('.app-layout'),
1129
+ panelA: document.querySelector('.main-area'),
1130
+ panelB: document.querySelector('.right-panel-inner'),
1131
+ minA: 300,
1132
+ minB: 300,
1231
1133
  layoutKey: 'mainSplit'
1232
1134
  });
1233
1135
  }
@@ -1245,17 +1147,12 @@ function initResizers() {
1245
1147
  });
1246
1148
  }
1247
1149
 
1248
- // Restore saved layout
1249
1150
  restoreLayout();
1250
1151
  }
1251
1152
 
1252
1153
  function setupResizer(config) {
1253
1154
  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
- }
1155
+ if (!handle || !container || !panelA || !panelB) return;
1259
1156
 
1260
1157
  let startPos = 0;
1261
1158
  let startSizeA = 0;
@@ -1263,55 +1160,35 @@ function setupResizer(config) {
1263
1160
  let rafId = null;
1264
1161
 
1265
1162
  function onPointerDown(e) {
1266
- // Only respond to primary button
1267
1163
  if (e.button !== 0) return;
1268
-
1269
1164
  e.preventDefault();
1270
1165
  handle.setPointerCapture(e.pointerId);
1271
1166
  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;
1167
+ document.body.classList.add('resizing', 'resizing-h');
1168
+
1169
+ startPos = e.clientX;
1170
+ startSizeA = panelA.getBoundingClientRect().width;
1171
+ startSizeB = panelB.getBoundingClientRect().width;
1282
1172
  }
1283
1173
 
1284
1174
  function onPointerMove(e) {
1285
1175
  if (!handle.hasPointerCapture(e.pointerId)) return;
1286
-
1287
1176
  if (rafId) cancelAnimationFrame(rafId);
1288
1177
 
1289
1178
  rafId = requestAnimationFrame(() => {
1290
- const currentPos = direction === 'horizontal' ? e.clientX : e.clientY;
1291
- const delta = currentPos - startPos;
1292
- const availableSize = startSizeA + startSizeB; // Only resizable panels
1179
+ const delta = e.clientX - startPos;
1180
+ const availableSize = startSizeA + startSizeB;
1293
1181
 
1294
1182
  let newSizeA = startSizeA + delta;
1295
1183
  let newSizeB = startSizeB - delta;
1296
1184
 
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
- }
1185
+ if (newSizeA < minA) { newSizeA = minA; newSizeB = availableSize - minA; }
1186
+ if (newSizeB < minB) { newSizeB = minB; newSizeA = availableSize - minB; }
1306
1187
 
1307
- // Use pixel values to avoid overflow issues
1308
1188
  panelA.style.flex = `0 0 ${newSizeA}px`;
1309
1189
  panelB.style.flex = `0 0 ${newSizeB}px`;
1310
1190
 
1311
- // Refit terminals if resizing affects them
1312
- if (direction === 'horizontal') {
1313
- refitTerminals();
1314
- }
1191
+ refitTerminals();
1315
1192
  });
1316
1193
  }
1317
1194
 
@@ -1319,19 +1196,12 @@ function setupResizer(config) {
1319
1196
  if (rafId) cancelAnimationFrame(rafId);
1320
1197
  handle.releasePointerCapture(e.pointerId);
1321
1198
  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;
1199
+ document.body.classList.remove('resizing', 'resizing-h');
1200
+
1201
+ const sizeA = panelA.getBoundingClientRect().width;
1202
+ const sizeB = panelB.getBoundingClientRect().width;
1331
1203
  const ratio = sizeA / (sizeA + sizeB);
1332
1204
  saveLayoutRatio(layoutKey, ratio);
1333
-
1334
- // Final refit
1335
1205
  refitTerminals();
1336
1206
  }
1337
1207
 
@@ -1356,30 +1226,28 @@ function restoreLayout() {
1356
1226
  const layout = JSON.parse(localStorage.getItem(LAYOUT_STORAGE_KEY) || '{}');
1357
1227
 
1358
1228
  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`;
1229
+ const mainArea = document.querySelector('.main-area');
1230
+ const rightPanelInner = document.querySelector('.right-panel-inner');
1231
+ const rightPanelEl = document.querySelector('.right-panel');
1232
+
1233
+ if (mainArea && rightPanelInner && rightPanelEl && rightPanelEl.style.display !== 'none') {
1234
+ const appLayout = document.querySelector('.app-layout');
1235
+ const sidebarWidth = document.querySelector('.sidebar')?.getBoundingClientRect().width || 260;
1236
+ const containerWidth = appLayout.getBoundingClientRect().width - sidebarWidth;
1237
+
1238
+ if (containerWidth > 0) {
1239
+ const mainWidth = containerWidth * layout.mainSplit;
1240
+ const rightWidth = containerWidth * (1 - layout.mainSplit);
1241
+ mainArea.style.flex = `0 0 ${mainWidth}px`;
1242
+ rightPanelInner.style.flex = `0 0 ${rightWidth}px`;
1243
+ }
1373
1244
  }
1374
1245
  }
1375
-
1376
- // Note: diffSplit is restored via restoreDiffLayout() when diff tab becomes visible
1377
1246
  } catch (err) {
1378
1247
  console.warn('Failed to restore layout:', err);
1379
1248
  }
1380
1249
  }
1381
1250
 
1382
- // Restore diff pane layout (called when diff tab becomes visible)
1383
1251
  function restoreDiffLayout() {
1384
1252
  try {
1385
1253
  const layout = JSON.parse(localStorage.getItem(LAYOUT_STORAGE_KEY) || '{}');
@@ -1392,14 +1260,11 @@ function restoreDiffLayout() {
1392
1260
 
1393
1261
  if (diffFileList && diffContentPane && diffLayout) {
1394
1262
  const containerWidth = diffLayout.getBoundingClientRect().width;
1395
- // Guard against zero-width container (tab still hidden)
1396
1263
  if (containerWidth <= 0) return;
1397
-
1398
1264
  const handleWidth = diffHandle ? diffHandle.offsetWidth : 8;
1399
1265
  const availableWidth = containerWidth - handleWidth;
1400
1266
  const fileListWidth = availableWidth * layout.diffSplit;
1401
1267
  const contentWidth = availableWidth * (1 - layout.diffSplit);
1402
-
1403
1268
  diffFileList.style.flex = `0 0 ${fileListWidth}px`;
1404
1269
  diffContentPane.style.flex = `0 0 ${contentWidth}px`;
1405
1270
  }
@@ -1409,157 +1274,107 @@ function restoreDiffLayout() {
1409
1274
  }
1410
1275
  }
1411
1276
 
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
- }
1277
+ function refitTerminals() {
1278
+ if (refitTerminals.timeout) clearTimeout(refitTerminals.timeout);
1279
+ refitTerminals.timeout = setTimeout(() => {
1280
+ Object.values(terminals).forEach(({ fitAddon }) => {
1281
+ try { fitAddon.fit(); } catch (err) { /* ignore */ }
1282
+ });
1283
+ }, 50);
1510
1284
  }
1511
1285
 
1512
- // Debounced window resize handler
1286
+ // Debounced window resize
1513
1287
  let resizeTimeout = null;
1514
1288
  function onWindowResize() {
1515
1289
  if (resizeTimeout) clearTimeout(resizeTimeout);
1516
1290
  resizeTimeout = setTimeout(() => {
1517
- handleContainerResize();
1291
+ if (rightPanel.style.display !== 'none') {
1292
+ restoreLayout();
1293
+ refitTerminals();
1294
+ }
1518
1295
  }, 150);
1519
1296
  }
1520
1297
 
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);
1298
+ // ═══════════════════════════════════════════════════════════
1299
+ // Utility
1300
+ // ═══════════════════════════════════════════════════════════
1301
+
1302
+ function escapeHtml(text) {
1303
+ const div = document.createElement('div');
1304
+ div.textContent = text;
1305
+ return div.innerHTML;
1533
1306
  }
1534
1307
 
1308
+ // ═══════════════════════════════════════════════════════════
1535
1309
  // 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);
1310
+ // ═══════════════════════════════════════════════════════════
1311
+
1312
+ // Welcome prompt
1313
+ promptSendBtn.addEventListener('click', () => handleSendMessage(promptInput.value));
1314
+ promptInput.addEventListener('keydown', (e) => {
1315
+ if (e.key === 'Enter' && !e.shiftKey) {
1316
+ e.preventDefault();
1317
+ handleSendMessage(promptInput.value);
1318
+ }
1319
+ });
1320
+
1321
+ // Session message input
1322
+ sendMessageButton.addEventListener('click', () => handleSendMessage(userMessageInput.value));
1323
+ userMessageInput.addEventListener('keydown', (e) => {
1324
+ if (e.key === 'Enter' && !e.shiftKey) {
1325
+ e.preventDefault();
1326
+ handleSendMessage(userMessageInput.value);
1327
+ }
1328
+ });
1329
+
1330
+ // Sidebar
1331
+ newSessionButton.addEventListener('click', handleNewSessionButton);
1332
+
1333
+ changeWorkspaceButton.addEventListener('click', async () => {
1334
+ const result = await window.electronAPI.browseForWorkspace();
1335
+ if (!result.canceled) {
1336
+ // Stop any active session before switching workspace
1337
+ if (sessionState === 'active') {
1338
+ stopChatPolling();
1339
+ await window.electronAPI.resetSession();
1340
+ cleanupTerminals();
1341
+ }
1342
+
1343
+ currentWorkspace = result.path;
1344
+ sidebarWorkspaceName.textContent = result.path.split('/').pop() || result.path.split('\\').pop();
1345
+ sidebarWorkspacePath.textContent = result.path;
1346
+ sidebarWorkspacePath.title = result.path;
1347
+ await window.electronAPI.addRecentWorkspace(result.path);
1348
+ await loadSessionsList();
1349
+ showWelcomeView();
1350
+ }
1351
+ });
1352
+
1353
+ settingsButton.addEventListener('click', () => {
1354
+ window.electronAPI.openConfigFolder();
1355
+ });
1356
+
1357
+ // Right panel tabs
1358
+ mainTabTerminal.addEventListener('click', () => switchRightTab('terminal'));
1359
+ mainTabPlan.addEventListener('click', () => switchRightTab('plan'));
1360
+ mainTabDiff.addEventListener('click', () => switchRightTab('diff'));
1361
+
1362
+ // Diff/Plan buttons
1363
+ refreshDiffButton.addEventListener('click', refreshGitDiff);
1364
+ if (refreshPlanButton) refreshPlanButton.addEventListener('click', refreshPlan);
1541
1365
  startImplementingButton.addEventListener('click', showImplementationModal);
1366
+
1367
+ // Modal
1542
1368
  modalCancelButton.addEventListener('click', hideImplementationModal);
1543
1369
  modalStartButton.addEventListener('click', startImplementation);
1544
1370
 
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
- }
1371
+ // Chat scroll
1558
1372
  chatNewMessagesButton.addEventListener('click', () => {
1559
1373
  autoScrollEnabled = true;
1560
1374
  scrollChatToBottom();
1561
1375
  setNewMessagesBanner(false);
1562
1376
  });
1377
+
1563
1378
  chatViewer.addEventListener('scroll', () => {
1564
1379
  if (isChatNearBottom()) {
1565
1380
  autoScrollEnabled = true;
@@ -1569,33 +1384,18 @@ chatViewer.addEventListener('scroll', () => {
1569
1384
  }
1570
1385
  });
1571
1386
 
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
1387
  // Keyboard shortcut for New Session (Cmd/Ctrl + Shift + N)
1589
1388
  document.addEventListener('keydown', (e) => {
1590
1389
  if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key === 'N') {
1591
1390
  e.preventDefault();
1592
- if (sessionScreen.classList.contains('active')) {
1593
- showNewSessionModal();
1594
- }
1391
+ handleNewSessionButton();
1595
1392
  }
1596
1393
  });
1597
1394
 
1395
+ // ═══════════════════════════════════════════════════════════
1598
1396
  // IPC Listeners
1397
+ // ═══════════════════════════════════════════════════════════
1398
+
1599
1399
  window.electronAPI.onAgentOutput((data) => {
1600
1400
  updateAgentOutput(data.agentName, data.output, data.isPty);
1601
1401
  });
@@ -1604,23 +1404,18 @@ window.electronAPI.onAgentStatus((data) => {
1604
1404
  updateAgentStatus(data.agentName, data.status, data.exitCode, data.error);
1605
1405
  });
1606
1406
 
1607
- // Listen for full chat refresh (array of messages)
1608
1407
  window.electronAPI.onChatUpdated((messages) => {
1609
1408
  updateChatFromMessages(messages);
1610
1409
  });
1611
1410
 
1612
- // Listen for new individual messages
1613
1411
  window.electronAPI.onChatMessage((message) => {
1614
1412
  addChatMessage(message);
1615
1413
  });
1616
1414
 
1617
- // Initialize on load
1618
- initialize();
1619
- initResizers();
1620
- restoreTabState();
1621
-
1622
- // Apply responsive layout on initial load
1623
- requestAnimationFrame(() => handleContainerResize());
1415
+ // ═══════════════════════════════════════════════════════════
1416
+ // Boot
1417
+ // ═══════════════════════════════════════════════════════════
1624
1418
 
1625
- // Add window resize listener to keep panels responsive
1419
+ initializeApp();
1420
+ initResizers();
1626
1421
  window.addEventListener('resize', onWindowResize);