@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/README.md +8 -18
- package/config.yaml +58 -1
- package/index.html +66 -110
- package/main.js +871 -397
- package/package.json +2 -2
- package/preload.js +10 -5
- package/renderer.js +633 -838
- package/styles.css +547 -760
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 = {};
|
|
7
|
-
let chatMessages = [];
|
|
8
|
-
let inputLocked = {};
|
|
9
|
-
let planHasContent = false;
|
|
10
|
-
let implementationStarted = false;
|
|
14
|
+
let agentColors = {};
|
|
15
|
+
let chatMessages = [];
|
|
16
|
+
let inputLocked = {};
|
|
17
|
+
let planHasContent = false;
|
|
18
|
+
let implementationStarted = false;
|
|
11
19
|
let autoScrollEnabled = true;
|
|
12
|
-
let
|
|
13
|
-
let lastDiffData = null;
|
|
14
|
-
let parsedDiffFiles = [];
|
|
15
|
-
let selectedDiffFile = null;
|
|
16
|
-
let pollingIntervals = [];
|
|
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
|
|
27
|
+
const LAYOUT_STORAGE_KEY = 'multiagent-layout';
|
|
26
28
|
|
|
29
|
+
// ═══════════════════════════════════════════════════════════
|
|
27
30
|
// DOM Elements
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
const
|
|
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
|
|
35
|
-
const
|
|
36
|
-
const
|
|
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
|
-
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
const
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
84
|
-
|
|
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
|
-
|
|
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
|
-
|
|
116
|
+
// Load config
|
|
103
117
|
currentConfig = await window.electronAPI.loadConfig();
|
|
104
|
-
console.log('Config loaded:', currentConfig);
|
|
105
118
|
displayConfig();
|
|
106
119
|
|
|
107
|
-
//
|
|
108
|
-
await
|
|
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
|
|
111
|
-
configDetails.innerHTML = `<span style="color: #dc3545;">Error
|
|
135
|
+
console.error('Error initializing:', error);
|
|
136
|
+
configDetails.innerHTML = `<span style="color: #dc3545;">Error: ${error.message}</span>`;
|
|
112
137
|
}
|
|
113
138
|
}
|
|
114
139
|
|
|
115
|
-
//
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
122
|
-
|
|
159
|
+
function showSessionView() {
|
|
160
|
+
welcomeView.style.display = 'none';
|
|
161
|
+
sessionView.style.display = 'flex';
|
|
162
|
+
}
|
|
123
163
|
|
|
124
|
-
|
|
125
|
-
|
|
164
|
+
function showRightPanel() {
|
|
165
|
+
rightPanel.style.display = 'flex';
|
|
166
|
+
// Restore layout after showing
|
|
167
|
+
requestAnimationFrame(() => {
|
|
168
|
+
restoreLayout();
|
|
169
|
+
refitTerminals();
|
|
170
|
+
});
|
|
171
|
+
}
|
|
126
172
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
136
|
-
|
|
177
|
+
// ═══════════════════════════════════════════════════════════
|
|
178
|
+
// Session List (Sidebar)
|
|
179
|
+
// ═══════════════════════════════════════════════════════════
|
|
137
180
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
|
146
|
-
|
|
188
|
+
console.error('Error loading sessions:', error);
|
|
189
|
+
sessionsList = [];
|
|
190
|
+
renderSessionsList();
|
|
147
191
|
}
|
|
148
192
|
}
|
|
149
193
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
const
|
|
227
|
-
|
|
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}
|
|
269
|
-
if (diffHours < 24) return `${diffHours}
|
|
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}
|
|
279
|
+
if (diffDays < 7) return `${diffDays}d ago`;
|
|
272
280
|
return date.toLocaleDateString();
|
|
273
281
|
}
|
|
274
282
|
|
|
275
|
-
//
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
-
//
|
|
288
|
-
async function
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
-
//
|
|
299
|
-
async function
|
|
300
|
-
|
|
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
|
-
|
|
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
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
366
|
+
if (result.success) {
|
|
367
|
+
currentSessionId = sessionId;
|
|
368
|
+
agentColors = result.colors || {};
|
|
369
|
+
chatMessages = result.messages || [];
|
|
370
|
+
sessionState = 'loaded';
|
|
314
371
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
}
|
|
372
|
+
// Show session view, hide right panel (dormant - no agents)
|
|
373
|
+
showSessionView();
|
|
374
|
+
hideRightPanel();
|
|
319
375
|
|
|
320
|
-
//
|
|
321
|
-
|
|
322
|
-
const challenge = challengeInput.value.trim();
|
|
376
|
+
// Render chat history
|
|
377
|
+
renderChatMessages();
|
|
323
378
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
return;
|
|
327
|
-
}
|
|
379
|
+
// Update sidebar highlighting
|
|
380
|
+
renderSessionsList();
|
|
328
381
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
-
|
|
335
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
//
|
|
358
|
-
|
|
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
|
|
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
|
|
376
|
-
alert('Error
|
|
377
|
-
|
|
378
|
-
|
|
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}">• ${a.name}</span> (${a.command})`;
|
|
505
|
+
}).join('<br>');
|
|
506
|
+
|
|
507
|
+
configDetails.innerHTML = `<strong>Agents:</strong><br>${agentList}`;
|
|
380
508
|
}
|
|
381
509
|
|
|
382
|
-
//
|
|
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
|
-
|
|
412
|
-
statusDiv.
|
|
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
|
-
|
|
485
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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);
|
|
732
|
+
chatMessages.sort((a, b) => a.seq - b.seq);
|
|
636
733
|
renderChatMessages();
|
|
637
|
-
if (!shouldScroll)
|
|
638
|
-
|
|
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
|
-
|
|
658
|
-
}
|
|
749
|
+
if (!shouldScroll) setNewMessagesBanner(true);
|
|
750
|
+
updateSidebarMessageCount();
|
|
659
751
|
}
|
|
660
752
|
}
|
|
661
753
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
const
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
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
|
-
|
|
671
|
-
|
|
764
|
+
function isChatNearBottom() {
|
|
765
|
+
return chatViewer.scrollHeight - chatViewer.scrollTop - chatViewer.clientHeight <= CHAT_SCROLL_THRESHOLD;
|
|
766
|
+
}
|
|
672
767
|
|
|
673
|
-
|
|
674
|
-
|
|
768
|
+
function scrollChatToBottom() {
|
|
769
|
+
chatViewer.scrollTop = chatViewer.scrollHeight;
|
|
770
|
+
}
|
|
675
771
|
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
728
|
-
|
|
729
|
-
|
|
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
|
-
|
|
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 ${
|
|
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
|
-
|
|
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
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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('.
|
|
1227
|
-
panelA: document.querySelector('.
|
|
1228
|
-
panelB: document.querySelector('.right-panel'),
|
|
1229
|
-
minA:
|
|
1230
|
-
minB:
|
|
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
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
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
|
|
1291
|
-
const
|
|
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
|
-
|
|
1298
|
-
if (
|
|
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
|
-
|
|
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'
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
const
|
|
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
|
|
1360
|
-
const
|
|
1361
|
-
const
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
const
|
|
1366
|
-
const
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
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
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
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
|
|
1286
|
+
// Debounced window resize
|
|
1513
1287
|
let resizeTimeout = null;
|
|
1514
1288
|
function onWindowResize() {
|
|
1515
1289
|
if (resizeTimeout) clearTimeout(resizeTimeout);
|
|
1516
1290
|
resizeTimeout = setTimeout(() => {
|
|
1517
|
-
|
|
1291
|
+
if (rightPanel.style.display !== 'none') {
|
|
1292
|
+
restoreLayout();
|
|
1293
|
+
refitTerminals();
|
|
1294
|
+
}
|
|
1518
1295
|
}, 150);
|
|
1519
1296
|
}
|
|
1520
1297
|
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
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
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
restoreTabState();
|
|
1621
|
-
|
|
1622
|
-
// Apply responsive layout on initial load
|
|
1623
|
-
requestAnimationFrame(() => handleContainerResize());
|
|
1415
|
+
// ═══════════════════════════════════════════════════════════
|
|
1416
|
+
// Boot
|
|
1417
|
+
// ═══════════════════════════════════════════════════════════
|
|
1624
1418
|
|
|
1625
|
-
|
|
1419
|
+
initializeApp();
|
|
1420
|
+
initResizers();
|
|
1626
1421
|
window.addEventListener('resize', onWindowResize);
|