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