@appoly/multiagent-chat 1.0.2 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -18
- package/config.yaml +58 -1
- package/index.html +66 -110
- package/main.js +864 -370
- package/package.json +3 -4
- package/preload.js +10 -5
- package/renderer.js +627 -838
- package/styles.css +547 -760
package/main.js
CHANGED
|
@@ -3,11 +3,26 @@ const { spawn, exec } = require('child_process');
|
|
|
3
3
|
const { promisify } = require('util');
|
|
4
4
|
const pty = require('node-pty');
|
|
5
5
|
const path = require('path');
|
|
6
|
+
|
|
7
|
+
// Diagnostic: Log node-pty module info for debugging ABI issues
|
|
8
|
+
// Enable with MULTIAGENT_PTY_DEBUG=1
|
|
9
|
+
if (process.env.MULTIAGENT_PTY_DEBUG) {
|
|
10
|
+
try {
|
|
11
|
+
const ptyPath = require.resolve('node-pty');
|
|
12
|
+
console.log('[node-pty] Module path:', ptyPath);
|
|
13
|
+
console.log('[node-pty] Node version:', process.version);
|
|
14
|
+
console.log('[node-pty] Electron version:', process.versions.electron || 'N/A');
|
|
15
|
+
console.log('[node-pty] ABI:', process.versions.modules);
|
|
16
|
+
} catch (e) {
|
|
17
|
+
console.warn('[node-pty] Could not resolve module path:', e.message);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
6
20
|
const fs = require('fs').promises;
|
|
7
21
|
const fsSync = require('fs');
|
|
8
22
|
const os = require('os');
|
|
9
23
|
const yaml = require('yaml');
|
|
10
24
|
const chokidar = require('chokidar');
|
|
25
|
+
const crypto = require('crypto');
|
|
11
26
|
|
|
12
27
|
// Home config directory: ~/.multiagent-chat/
|
|
13
28
|
const HOME_CONFIG_DIR = path.join(os.homedir(), '.multiagent-chat');
|
|
@@ -20,8 +35,9 @@ const execAsync = promisify(exec);
|
|
|
20
35
|
let mainWindow;
|
|
21
36
|
let agents = [];
|
|
22
37
|
let config;
|
|
23
|
-
let workspacePath;
|
|
24
|
-
let
|
|
38
|
+
let workspacePath; // Points to current session dir (e.g. .multiagent-chat/sessions/<id>/)
|
|
39
|
+
let workspaceBasePath; // Points to .multiagent-chat/ inside project root
|
|
40
|
+
let agentCwd; // Parent directory of workspace - where agents are launched
|
|
25
41
|
let fileWatcher;
|
|
26
42
|
let outboxWatcher;
|
|
27
43
|
let customWorkspacePath = null;
|
|
@@ -29,14 +45,348 @@ let customConfigPath = null; // CLI --config path
|
|
|
29
45
|
let messageSequence = 0; // For ordering messages in chat
|
|
30
46
|
let agentColors = {}; // Map of agent name -> color
|
|
31
47
|
let sessionBaseCommit = null; // Git commit hash at session start for diff baseline
|
|
48
|
+
let currentSessionId = null; // Current active session ID
|
|
49
|
+
|
|
50
|
+
// ═══════════════════════════════════════════════════════════
|
|
51
|
+
// Session Storage Functions
|
|
52
|
+
// ═══════════════════════════════════════════════════════════
|
|
53
|
+
|
|
54
|
+
function generateSessionId() {
|
|
55
|
+
const now = new Date();
|
|
56
|
+
const ts = now.toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
57
|
+
const hex = crypto.randomBytes(2).toString('hex');
|
|
58
|
+
return `${ts}-${hex}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function generateSessionTitle(prompt) {
|
|
62
|
+
if (!prompt) return 'Untitled Session';
|
|
63
|
+
// Truncate at ~60 chars on a word boundary
|
|
64
|
+
if (prompt.length <= 60) return prompt.split('\n')[0];
|
|
65
|
+
const truncated = prompt.slice(0, 60);
|
|
66
|
+
const lastSpace = truncated.lastIndexOf(' ');
|
|
67
|
+
return (lastSpace > 30 ? truncated.slice(0, lastSpace) : truncated) + '...';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function getSessionsIndexPath(projectRoot) {
|
|
71
|
+
const wsName = (config && config.workspace) || '.multiagent-chat';
|
|
72
|
+
return path.join(projectRoot, wsName, 'sessions.json');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function getSessionsDir(projectRoot) {
|
|
76
|
+
const wsName = (config && config.workspace) || '.multiagent-chat';
|
|
77
|
+
return path.join(projectRoot, wsName, 'sessions');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function loadSessionsIndex(projectRoot) {
|
|
81
|
+
const indexPath = getSessionsIndexPath(projectRoot);
|
|
82
|
+
try {
|
|
83
|
+
const content = await fs.readFile(indexPath, 'utf8');
|
|
84
|
+
return JSON.parse(content);
|
|
85
|
+
} catch (error) {
|
|
86
|
+
if (error instanceof SyntaxError) {
|
|
87
|
+
console.error('Corrupt sessions.json - failed to parse:', indexPath, error.message);
|
|
88
|
+
}
|
|
89
|
+
return { version: 1, sessions: [] };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function saveSessionsIndex(projectRoot, index) {
|
|
94
|
+
const indexPath = getSessionsIndexPath(projectRoot);
|
|
95
|
+
await fs.mkdir(path.dirname(indexPath), { recursive: true });
|
|
96
|
+
// Atomic write: write to temp file then rename (rename is atomic on all platforms)
|
|
97
|
+
const tmpPath = indexPath + '.tmp';
|
|
98
|
+
await fs.writeFile(tmpPath, JSON.stringify(index, null, 2));
|
|
99
|
+
await fs.rename(tmpPath, indexPath);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Async mutex to serialize all sessions.json read-modify-write cycles
|
|
103
|
+
let sessionsWriteQueue = Promise.resolve();
|
|
104
|
+
|
|
105
|
+
function withSessionsLock(fn) {
|
|
106
|
+
const next = sessionsWriteQueue.then(fn, fn);
|
|
107
|
+
sessionsWriteQueue = next.catch(() => {});
|
|
108
|
+
return next;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function addSessionToIndex(projectRoot, sessionMeta) {
|
|
112
|
+
return withSessionsLock(async () => {
|
|
113
|
+
const index = await loadSessionsIndex(projectRoot);
|
|
114
|
+
index.sessions.unshift(sessionMeta);
|
|
115
|
+
await saveSessionsIndex(projectRoot, index);
|
|
116
|
+
return index;
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function updateSessionInIndex(projectRoot, sessionId, updates) {
|
|
121
|
+
return withSessionsLock(async () => {
|
|
122
|
+
const index = await loadSessionsIndex(projectRoot);
|
|
123
|
+
const session = index.sessions.find(s => s.id === sessionId);
|
|
124
|
+
if (session) {
|
|
125
|
+
Object.assign(session, updates);
|
|
126
|
+
await saveSessionsIndex(projectRoot, index);
|
|
127
|
+
}
|
|
128
|
+
return index;
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function setupSessionDirectory(projectRoot, sessionId) {
|
|
133
|
+
const sessionsDir = getSessionsDir(projectRoot);
|
|
134
|
+
const sessionDir = path.join(sessionsDir, sessionId);
|
|
135
|
+
|
|
136
|
+
await fs.mkdir(sessionDir, { recursive: true });
|
|
137
|
+
|
|
138
|
+
// Initialize chat.jsonl
|
|
139
|
+
const chatPath = path.join(sessionDir, config.chat_file || 'chat.jsonl');
|
|
140
|
+
await fs.writeFile(chatPath, '');
|
|
141
|
+
|
|
142
|
+
// Initialize PLAN_FINAL.md
|
|
143
|
+
const planPath = path.join(sessionDir, config.plan_file || 'PLAN_FINAL.md');
|
|
144
|
+
await fs.writeFile(planPath, '');
|
|
145
|
+
|
|
146
|
+
// Create outbox directory and per-agent outbox files
|
|
147
|
+
const outboxDir = path.join(sessionDir, config.outbox_dir || 'outbox');
|
|
148
|
+
await fs.mkdir(outboxDir, { recursive: true });
|
|
149
|
+
|
|
150
|
+
for (const agentConfig of config.agents) {
|
|
151
|
+
const outboxFile = path.join(outboxDir, `${agentConfig.name.toLowerCase()}.md`);
|
|
152
|
+
await fs.writeFile(outboxFile, '');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return sessionDir;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function loadSessionData(projectRoot, sessionId) {
|
|
159
|
+
const sessionsDir = getSessionsDir(projectRoot);
|
|
160
|
+
const sessionDir = path.join(sessionsDir, sessionId);
|
|
161
|
+
|
|
162
|
+
// Read chat
|
|
163
|
+
let messages = [];
|
|
164
|
+
const chatPath = path.join(sessionDir, config.chat_file || 'chat.jsonl');
|
|
165
|
+
try {
|
|
166
|
+
const content = await fs.readFile(chatPath, 'utf8');
|
|
167
|
+
if (content.trim()) {
|
|
168
|
+
messages = content.trim().split('\n').map(line => {
|
|
169
|
+
try { return JSON.parse(line); }
|
|
170
|
+
catch (e) { return null; }
|
|
171
|
+
}).filter(Boolean);
|
|
172
|
+
}
|
|
173
|
+
} catch (e) {
|
|
174
|
+
// No chat file
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Read plan
|
|
178
|
+
let plan = '';
|
|
179
|
+
const planPath = path.join(sessionDir, config.plan_file || 'PLAN_FINAL.md');
|
|
180
|
+
try {
|
|
181
|
+
plan = await fs.readFile(planPath, 'utf8');
|
|
182
|
+
} catch (e) {
|
|
183
|
+
// No plan file
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return { messages, plan, sessionDir };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Migrate from old flat .multiagent-chat/ to session-based structure
|
|
190
|
+
async function migrateFromFlatWorkspace(projectRoot) {
|
|
191
|
+
const wsName = (config && config.workspace) || '.multiagent-chat';
|
|
192
|
+
const wsBase = path.join(projectRoot, wsName);
|
|
193
|
+
|
|
194
|
+
// Check if old flat structure exists (chat.jsonl directly in .multiagent-chat/)
|
|
195
|
+
const oldChatPath = path.join(wsBase, config.chat_file || 'chat.jsonl');
|
|
196
|
+
const sessionsDir = path.join(wsBase, 'sessions');
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
await fs.access(oldChatPath);
|
|
200
|
+
// Old flat structure exists - check if sessions dir already exists
|
|
201
|
+
try {
|
|
202
|
+
await fs.access(sessionsDir);
|
|
203
|
+
// Sessions dir exists, already migrated or mixed state - skip
|
|
204
|
+
return;
|
|
205
|
+
} catch (e) {
|
|
206
|
+
// Sessions dir doesn't exist, migrate
|
|
207
|
+
}
|
|
208
|
+
} catch (e) {
|
|
209
|
+
// No old chat file, nothing to migrate
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
console.log('Migrating flat workspace to session-based structure...');
|
|
214
|
+
|
|
215
|
+
// Read old chat to determine session metadata
|
|
216
|
+
let oldMessages = [];
|
|
217
|
+
try {
|
|
218
|
+
const content = await fs.readFile(oldChatPath, 'utf8');
|
|
219
|
+
if (content.trim()) {
|
|
220
|
+
oldMessages = content.trim().split('\n').map(line => {
|
|
221
|
+
try { return JSON.parse(line); }
|
|
222
|
+
catch (e) { return null; }
|
|
223
|
+
}).filter(Boolean);
|
|
224
|
+
}
|
|
225
|
+
} catch (e) {
|
|
226
|
+
// Empty or unreadable
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Create a session for the old data
|
|
230
|
+
const sessionId = generateSessionId();
|
|
231
|
+
const sessionDir = path.join(sessionsDir, sessionId);
|
|
232
|
+
await fs.mkdir(sessionDir, { recursive: true });
|
|
233
|
+
|
|
234
|
+
// Move chat.jsonl
|
|
235
|
+
try {
|
|
236
|
+
await fs.rename(oldChatPath, path.join(sessionDir, config.chat_file || 'chat.jsonl'));
|
|
237
|
+
} catch (e) {
|
|
238
|
+
// Copy instead if rename fails (cross-device)
|
|
239
|
+
try {
|
|
240
|
+
await fs.copyFile(oldChatPath, path.join(sessionDir, config.chat_file || 'chat.jsonl'));
|
|
241
|
+
await fs.unlink(oldChatPath);
|
|
242
|
+
} catch (copyErr) {
|
|
243
|
+
console.warn('Could not migrate chat file:', copyErr.message);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Move PLAN_FINAL.md
|
|
248
|
+
const oldPlanPath = path.join(wsBase, config.plan_file || 'PLAN_FINAL.md');
|
|
249
|
+
try {
|
|
250
|
+
await fs.rename(oldPlanPath, path.join(sessionDir, config.plan_file || 'PLAN_FINAL.md'));
|
|
251
|
+
} catch (e) {
|
|
252
|
+
// Create empty if not found
|
|
253
|
+
await fs.writeFile(path.join(sessionDir, config.plan_file || 'PLAN_FINAL.md'), '');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Move outbox directory
|
|
257
|
+
const oldOutboxDir = path.join(wsBase, config.outbox_dir || 'outbox');
|
|
258
|
+
const newOutboxDir = path.join(sessionDir, config.outbox_dir || 'outbox');
|
|
259
|
+
try {
|
|
260
|
+
await fs.rename(oldOutboxDir, newOutboxDir);
|
|
261
|
+
} catch (e) {
|
|
262
|
+
// Create fresh outbox
|
|
263
|
+
await fs.mkdir(newOutboxDir, { recursive: true });
|
|
264
|
+
for (const agentConfig of config.agents) {
|
|
265
|
+
await fs.writeFile(path.join(newOutboxDir, `${agentConfig.name.toLowerCase()}.md`), '');
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Create sessions.json index
|
|
270
|
+
const firstPrompt = oldMessages.find(m => m.type === 'user')?.content || '';
|
|
271
|
+
const firstTs = oldMessages.length > 0 ? oldMessages[0].timestamp : new Date().toISOString();
|
|
272
|
+
const lastTs = oldMessages.length > 0 ? oldMessages[oldMessages.length - 1].timestamp : new Date().toISOString();
|
|
273
|
+
|
|
274
|
+
const sessionMeta = {
|
|
275
|
+
id: sessionId,
|
|
276
|
+
title: generateSessionTitle(firstPrompt),
|
|
277
|
+
firstPrompt: firstPrompt.slice(0, 200),
|
|
278
|
+
workspace: projectRoot,
|
|
279
|
+
createdAt: firstTs,
|
|
280
|
+
lastActiveAt: lastTs,
|
|
281
|
+
messageCount: oldMessages.length,
|
|
282
|
+
status: 'completed'
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
await withSessionsLock(async () => {
|
|
286
|
+
await saveSessionsIndex(projectRoot, { version: 1, sessions: [sessionMeta] });
|
|
287
|
+
});
|
|
288
|
+
console.log('Migration complete. Created session:', sessionId);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function generateChatSummary(messages) {
|
|
292
|
+
// Take last ~20 messages and create a condensed summary
|
|
293
|
+
const recent = messages.slice(-20);
|
|
294
|
+
if (recent.length === 0) return 'No previous messages.';
|
|
295
|
+
|
|
296
|
+
return recent.map(m => {
|
|
297
|
+
const content = (m.content || '').slice(0, 200);
|
|
298
|
+
return `[${m.agent}]: ${content}${m.content && m.content.length > 200 ? '...' : ''}`;
|
|
299
|
+
}).join('\n\n');
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function buildResumePrompt(chatSummary, plan, newMessage, agentName) {
|
|
303
|
+
const template = config.resume_template || `## Multi-Agent Collaboration Session (Resumed)
|
|
304
|
+
**You are: {agent_name}**
|
|
305
|
+
You are collaborating with: {agent_names}
|
|
306
|
+
|
|
307
|
+
### Previous Discussion Summary
|
|
308
|
+
{chat_summary}
|
|
309
|
+
|
|
310
|
+
### Existing Plan
|
|
311
|
+
{existing_plan}
|
|
312
|
+
|
|
313
|
+
### How to Send Messages
|
|
314
|
+
\`\`\`bash
|
|
315
|
+
cat << 'EOF' > {outbox_file}
|
|
316
|
+
Your message here.
|
|
317
|
+
EOF
|
|
318
|
+
\`\`\`
|
|
319
|
+
|
|
320
|
+
## New Message from User
|
|
321
|
+
{new_message}
|
|
322
|
+
|
|
323
|
+
### Behavior on Resume
|
|
324
|
+
- Default to discussion-first collaboration with the other agents
|
|
325
|
+
- Do NOT implement or edit files unless the newest user message explicitly asks for implementation
|
|
326
|
+
- If intent is ambiguous, ask a quick clarification before making code changes
|
|
327
|
+
|
|
328
|
+
Please respond taking into account the context above.`;
|
|
329
|
+
|
|
330
|
+
const relFromProject = path.relative(agentCwd, workspacePath);
|
|
331
|
+
const outboxDir = config.outbox_dir || 'outbox';
|
|
332
|
+
const outboxFile = `${relFromProject}/${outboxDir}/${agentName.toLowerCase()}.md`;
|
|
333
|
+
const planFile = `${relFromProject}/${config.plan_file || 'PLAN_FINAL.md'}`;
|
|
334
|
+
|
|
335
|
+
return template
|
|
336
|
+
.replace(/{agent_name}/g, agentName)
|
|
337
|
+
.replace(/{agent_names}/g, config.agents.map(a => a.name).join(', '))
|
|
338
|
+
.replace(/{chat_summary}/g, chatSummary)
|
|
339
|
+
.replace(/{existing_plan}/g, plan || 'No plan yet.')
|
|
340
|
+
.replace(/{new_message}/g, newMessage)
|
|
341
|
+
.replace(/{outbox_file}/g, outboxFile)
|
|
342
|
+
.replace(/{plan_file}/g, planFile);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// ═══════════════════════════════════════════════════════════
|
|
346
|
+
// Init workspace base (just ensures dirs exist, sets agentCwd)
|
|
347
|
+
// ═══════════════════════════════════════════════════════════
|
|
348
|
+
|
|
349
|
+
async function initWorkspaceBase(projectRoot) {
|
|
350
|
+
agentCwd = projectRoot;
|
|
351
|
+
|
|
352
|
+
const wsName = (config && config.workspace) || '.multiagent-chat';
|
|
353
|
+
workspaceBasePath = path.join(projectRoot, wsName);
|
|
354
|
+
await fs.mkdir(workspaceBasePath, { recursive: true });
|
|
355
|
+
await fs.mkdir(path.join(workspaceBasePath, 'sessions'), { recursive: true });
|
|
356
|
+
|
|
357
|
+
// Build agent colors map from config
|
|
358
|
+
const defaultColors = config.default_agent_colors || ['#667eea', '#f093fb', '#4fd1c5', '#f6ad55', '#68d391', '#fc8181'];
|
|
359
|
+
agentColors = {};
|
|
360
|
+
config.agents.forEach((agentConfig, index) => {
|
|
361
|
+
agentColors[agentConfig.name.toLowerCase()] = agentConfig.color || defaultColors[index % defaultColors.length];
|
|
362
|
+
});
|
|
363
|
+
agentColors['user'] = config.user_color || '#a0aec0';
|
|
364
|
+
|
|
365
|
+
// Capture git base commit for diff baseline
|
|
366
|
+
try {
|
|
367
|
+
const { stdout } = await execAsync('git rev-parse HEAD', { cwd: agentCwd });
|
|
368
|
+
sessionBaseCommit = stdout.trim();
|
|
369
|
+
console.log('Session base commit:', sessionBaseCommit);
|
|
370
|
+
} catch (error) {
|
|
371
|
+
try {
|
|
372
|
+
await execAsync('git rev-parse --git-dir', { cwd: agentCwd });
|
|
373
|
+
sessionBaseCommit = '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
|
|
374
|
+
console.log('Git repo with no commits, using empty tree hash for diff baseline');
|
|
375
|
+
} catch (e) {
|
|
376
|
+
console.log('Not a git repository:', e.message);
|
|
377
|
+
sessionBaseCommit = null;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
32
380
|
|
|
381
|
+
console.log('Workspace base initialized:', workspaceBasePath);
|
|
382
|
+
console.log('Agent working directory:', agentCwd);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// ═══════════════════════════════════════════════════════════
|
|
33
386
|
// Parse command-line arguments
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
// Or: npm start --config /path/to/config.yaml
|
|
37
|
-
// Or: WORKSPACE=/path/to/workspace npm start
|
|
387
|
+
// ═══════════════════════════════════════════════════════════
|
|
388
|
+
|
|
38
389
|
function parseCommandLineArgs() {
|
|
39
|
-
// Check environment variables first
|
|
40
390
|
if (process.env.WORKSPACE) {
|
|
41
391
|
customWorkspacePath = process.env.WORKSPACE;
|
|
42
392
|
console.log('Using workspace from environment variable:', customWorkspacePath);
|
|
@@ -47,48 +397,40 @@ function parseCommandLineArgs() {
|
|
|
47
397
|
console.log('Using config from environment variable:', customConfigPath);
|
|
48
398
|
}
|
|
49
399
|
|
|
50
|
-
// Then check command-line arguments
|
|
51
|
-
// process.argv looks like: [electron, main.js, ...args]
|
|
52
400
|
const args = process.argv.slice(2);
|
|
53
401
|
|
|
54
402
|
for (let i = 0; i < args.length; i++) {
|
|
55
|
-
// Parse --workspace flag
|
|
56
403
|
if (args[i] === '--workspace' && args[i + 1]) {
|
|
57
404
|
customWorkspacePath = args[i + 1];
|
|
58
405
|
console.log('Using workspace from --workspace flag:', customWorkspacePath);
|
|
59
|
-
i++;
|
|
60
|
-
}
|
|
61
|
-
// Parse --config flag
|
|
62
|
-
else if (args[i] === '--config' && args[i + 1]) {
|
|
406
|
+
i++;
|
|
407
|
+
} else if (args[i] === '--config' && args[i + 1]) {
|
|
63
408
|
customConfigPath = args[i + 1];
|
|
64
409
|
console.log('Using config from --config flag:', customConfigPath);
|
|
65
|
-
i++;
|
|
66
|
-
}
|
|
67
|
-
// Positional arg (assume workspace path if not a flag)
|
|
68
|
-
else if (!args[i].startsWith('--') && !customWorkspacePath) {
|
|
410
|
+
i++;
|
|
411
|
+
} else if (!args[i].startsWith('--') && !customWorkspacePath) {
|
|
69
412
|
customWorkspacePath = args[i];
|
|
70
413
|
console.log('Using workspace from positional argument:', customWorkspacePath);
|
|
71
414
|
}
|
|
72
415
|
}
|
|
73
416
|
}
|
|
74
417
|
|
|
75
|
-
//
|
|
418
|
+
// ═══════════════════════════════════════════════════════════
|
|
419
|
+
// Home config directory setup
|
|
420
|
+
// ═══════════════════════════════════════════════════════════
|
|
421
|
+
|
|
76
422
|
async function ensureHomeConfigDir() {
|
|
77
423
|
try {
|
|
78
|
-
// Create ~/.multiagent-chat/ if it doesn't exist
|
|
79
424
|
await fs.mkdir(HOME_CONFIG_DIR, { recursive: true });
|
|
80
425
|
console.log('Home config directory ensured:', HOME_CONFIG_DIR);
|
|
81
426
|
|
|
82
|
-
// Migration: check for existing data in Electron userData
|
|
83
427
|
const userDataDir = app.getPath('userData');
|
|
84
428
|
const oldRecentsFile = path.join(userDataDir, 'recent-workspaces.json');
|
|
85
429
|
|
|
86
|
-
// Check if config.yaml exists in home dir, if not copy default
|
|
87
430
|
try {
|
|
88
431
|
await fs.access(HOME_CONFIG_FILE);
|
|
89
432
|
console.log('Home config exists:', HOME_CONFIG_FILE);
|
|
90
433
|
} catch (e) {
|
|
91
|
-
// Copy bundled default config to home dir
|
|
92
434
|
const bundledConfig = path.join(__dirname, 'config.yaml');
|
|
93
435
|
try {
|
|
94
436
|
await fs.copyFile(bundledConfig, HOME_CONFIG_FILE);
|
|
@@ -98,17 +440,14 @@ async function ensureHomeConfigDir() {
|
|
|
98
440
|
}
|
|
99
441
|
}
|
|
100
442
|
|
|
101
|
-
// Initialize or migrate recent-workspaces.json
|
|
102
443
|
try {
|
|
103
444
|
await fs.access(RECENT_WORKSPACES_FILE);
|
|
104
445
|
} catch (e) {
|
|
105
|
-
// Try to migrate from old location first
|
|
106
446
|
try {
|
|
107
447
|
await fs.access(oldRecentsFile);
|
|
108
448
|
await fs.copyFile(oldRecentsFile, RECENT_WORKSPACES_FILE);
|
|
109
449
|
console.log('Migrated recent workspaces from:', oldRecentsFile);
|
|
110
450
|
} catch (migrateError) {
|
|
111
|
-
// No old file, create new empty one
|
|
112
451
|
await fs.writeFile(RECENT_WORKSPACES_FILE, JSON.stringify({ recents: [] }, null, 2));
|
|
113
452
|
console.log('Initialized recent workspaces file:', RECENT_WORKSPACES_FILE);
|
|
114
453
|
}
|
|
@@ -118,7 +457,10 @@ async function ensureHomeConfigDir() {
|
|
|
118
457
|
}
|
|
119
458
|
}
|
|
120
459
|
|
|
121
|
-
//
|
|
460
|
+
// ═══════════════════════════════════════════════════════════
|
|
461
|
+
// Recent workspaces (kept for backward compat / sidebar)
|
|
462
|
+
// ═══════════════════════════════════════════════════════════
|
|
463
|
+
|
|
122
464
|
async function loadRecentWorkspaces() {
|
|
123
465
|
try {
|
|
124
466
|
const content = await fs.readFile(RECENT_WORKSPACES_FILE, 'utf8');
|
|
@@ -130,7 +472,6 @@ async function loadRecentWorkspaces() {
|
|
|
130
472
|
}
|
|
131
473
|
}
|
|
132
474
|
|
|
133
|
-
// Save recent workspaces to JSON file
|
|
134
475
|
async function saveRecentWorkspaces(recents) {
|
|
135
476
|
try {
|
|
136
477
|
await fs.writeFile(RECENT_WORKSPACES_FILE, JSON.stringify({ recents }, null, 2));
|
|
@@ -139,98 +480,65 @@ async function saveRecentWorkspaces(recents) {
|
|
|
139
480
|
}
|
|
140
481
|
}
|
|
141
482
|
|
|
142
|
-
|
|
143
|
-
async function addRecentWorkspace(workspacePath) {
|
|
483
|
+
async function addRecentWorkspace(wsPath) {
|
|
144
484
|
const recents = await loadRecentWorkspaces();
|
|
145
485
|
const now = new Date().toISOString();
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const filtered = recents.filter(r =>
|
|
149
|
-
r.path.toLowerCase() !== workspacePath.toLowerCase()
|
|
150
|
-
);
|
|
151
|
-
|
|
152
|
-
// Add new entry at the beginning
|
|
153
|
-
filtered.unshift({
|
|
154
|
-
path: workspacePath,
|
|
155
|
-
lastUsed: now
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
// Limit to max entries
|
|
486
|
+
const filtered = recents.filter(r => r.path.toLowerCase() !== wsPath.toLowerCase());
|
|
487
|
+
filtered.unshift({ path: wsPath, lastUsed: now });
|
|
159
488
|
const limited = filtered.slice(0, MAX_RECENT_WORKSPACES);
|
|
160
|
-
|
|
161
489
|
await saveRecentWorkspaces(limited);
|
|
162
490
|
return limited;
|
|
163
491
|
}
|
|
164
492
|
|
|
165
|
-
|
|
166
|
-
async function removeRecentWorkspace(workspacePath) {
|
|
493
|
+
async function removeRecentWorkspace(wsPath) {
|
|
167
494
|
const recents = await loadRecentWorkspaces();
|
|
168
|
-
const filtered = recents.filter(r =>
|
|
169
|
-
r.path.toLowerCase() !== workspacePath.toLowerCase()
|
|
170
|
-
);
|
|
495
|
+
const filtered = recents.filter(r => r.path.toLowerCase() !== wsPath.toLowerCase());
|
|
171
496
|
await saveRecentWorkspaces(filtered);
|
|
172
497
|
return filtered;
|
|
173
498
|
}
|
|
174
499
|
|
|
175
|
-
|
|
176
|
-
async function updateRecentWorkspacePath(oldPath, newPath) {
|
|
177
|
-
const recents = await loadRecentWorkspaces();
|
|
178
|
-
const now = new Date().toISOString();
|
|
179
|
-
|
|
180
|
-
const updated = recents.map(r => {
|
|
181
|
-
if (r.path.toLowerCase() === oldPath.toLowerCase()) {
|
|
182
|
-
return { path: newPath, lastUsed: now };
|
|
183
|
-
}
|
|
184
|
-
return r;
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
await saveRecentWorkspaces(updated);
|
|
188
|
-
return updated;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// Validate if a workspace path exists and is a directory
|
|
192
|
-
async function validateWorkspacePath(workspacePath) {
|
|
500
|
+
async function validateWorkspacePath(wsPath) {
|
|
193
501
|
try {
|
|
194
|
-
const stats = await fs.stat(
|
|
502
|
+
const stats = await fs.stat(wsPath);
|
|
195
503
|
return stats.isDirectory();
|
|
196
504
|
} catch (error) {
|
|
197
505
|
return false;
|
|
198
506
|
}
|
|
199
507
|
}
|
|
200
508
|
|
|
201
|
-
//
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
const appDir = __dirname;
|
|
205
|
-
|
|
206
|
-
// Check if cwd is different from app directory and exists
|
|
207
|
-
const isUsable = cwd !== appDir && fsSync.existsSync(cwd);
|
|
509
|
+
// ═══════════════════════════════════════════════════════════
|
|
510
|
+
// Browser window
|
|
511
|
+
// ═══════════════════════════════════════════════════════════
|
|
208
512
|
|
|
209
|
-
return {
|
|
210
|
-
path: cwd,
|
|
211
|
-
isUsable,
|
|
212
|
-
appDir
|
|
213
|
-
};
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// Create the browser window
|
|
217
513
|
function createWindow() {
|
|
218
514
|
console.log('Creating window...');
|
|
219
515
|
|
|
220
516
|
const iconPath = path.join(__dirname, 'robot.png');
|
|
221
517
|
|
|
222
|
-
|
|
518
|
+
const windowOptions = {
|
|
223
519
|
width: 1400,
|
|
224
520
|
height: 900,
|
|
225
521
|
icon: iconPath,
|
|
522
|
+
show: false,
|
|
523
|
+
backgroundColor: '#0b0e11',
|
|
226
524
|
webPreferences: {
|
|
227
525
|
preload: path.join(__dirname, 'preload.js'),
|
|
228
526
|
contextIsolation: true,
|
|
229
527
|
nodeIntegration: false
|
|
230
528
|
}
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
if (process.platform === 'darwin') {
|
|
532
|
+
windowOptions.titleBarStyle = 'hiddenInset';
|
|
533
|
+
windowOptions.trafficLightPosition = { x: 16, y: 16 };
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
mainWindow = new BrowserWindow(windowOptions);
|
|
537
|
+
|
|
538
|
+
mainWindow.once('ready-to-show', () => {
|
|
539
|
+
mainWindow.show();
|
|
231
540
|
});
|
|
232
541
|
|
|
233
|
-
// Set dock icon on macOS
|
|
234
542
|
if (process.platform === 'darwin' && app.dock) {
|
|
235
543
|
app.dock.setIcon(iconPath);
|
|
236
544
|
}
|
|
@@ -245,122 +553,143 @@ function createWindow() {
|
|
|
245
553
|
console.log('Window setup complete');
|
|
246
554
|
}
|
|
247
555
|
|
|
248
|
-
//
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
556
|
+
// ═══════════════════════════════════════════════════════════
|
|
557
|
+
// Config loading
|
|
558
|
+
// ═══════════════════════════════════════════════════════════
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Deep-merge two objects. Objects are merged recursively;
|
|
562
|
+
* arrays and primitives from `override` replace `base` wholesale.
|
|
563
|
+
*/
|
|
564
|
+
function deepMerge(base, override) {
|
|
565
|
+
const result = { ...base };
|
|
566
|
+
for (const key of Object.keys(override)) {
|
|
567
|
+
if (
|
|
568
|
+
override[key] !== null &&
|
|
569
|
+
typeof override[key] === 'object' &&
|
|
570
|
+
!Array.isArray(override[key]) &&
|
|
571
|
+
typeof result[key] === 'object' &&
|
|
572
|
+
result[key] !== null &&
|
|
573
|
+
!Array.isArray(result[key])
|
|
574
|
+
) {
|
|
575
|
+
result[key] = deepMerge(result[key], override[key]);
|
|
576
|
+
} else {
|
|
577
|
+
result[key] = override[key];
|
|
267
578
|
}
|
|
268
|
-
|
|
269
|
-
const configFile = await fs.readFile(fullPath, 'utf8');
|
|
270
|
-
config = yaml.parse(configFile);
|
|
271
|
-
console.log('Config loaded successfully');
|
|
272
|
-
return config;
|
|
273
|
-
} catch (error) {
|
|
274
|
-
console.error('Error loading config:', error);
|
|
275
|
-
throw error;
|
|
276
579
|
}
|
|
580
|
+
return result;
|
|
277
581
|
}
|
|
278
582
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
583
|
+
/**
|
|
584
|
+
* Collect dot-paths present in `base` but missing from `obj`.
|
|
585
|
+
*/
|
|
586
|
+
function findMissingKeys(base, obj, prefix = '') {
|
|
587
|
+
const missing = [];
|
|
588
|
+
for (const key of Object.keys(base)) {
|
|
589
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
590
|
+
if (!(key in obj)) {
|
|
591
|
+
missing.push(fullKey);
|
|
592
|
+
} else if (
|
|
593
|
+
typeof base[key] === 'object' &&
|
|
594
|
+
base[key] !== null &&
|
|
595
|
+
!Array.isArray(base[key]) &&
|
|
596
|
+
typeof obj[key] === 'object' &&
|
|
597
|
+
obj[key] !== null &&
|
|
598
|
+
!Array.isArray(obj[key])
|
|
599
|
+
) {
|
|
600
|
+
missing.push(...findMissingKeys(base[key], obj[key], fullKey));
|
|
601
|
+
}
|
|
292
602
|
}
|
|
603
|
+
return missing;
|
|
604
|
+
}
|
|
293
605
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
606
|
+
/**
|
|
607
|
+
* Validate that required config fields are present after merge.
|
|
608
|
+
*/
|
|
609
|
+
function validateConfig(cfg) {
|
|
610
|
+
if (!cfg.agents || !Array.isArray(cfg.agents) || cfg.agents.length === 0) {
|
|
611
|
+
throw new Error('Config validation failed: "agents" must be a non-empty array.');
|
|
612
|
+
}
|
|
613
|
+
for (let i = 0; i < cfg.agents.length; i++) {
|
|
614
|
+
const agent = cfg.agents[i];
|
|
615
|
+
if (!agent.name || typeof agent.name !== 'string') {
|
|
616
|
+
throw new Error(`Config validation failed: agents[${i}] is missing a valid "name".`);
|
|
617
|
+
}
|
|
618
|
+
if (!agent.command || typeof agent.command !== 'string') {
|
|
619
|
+
throw new Error(`Config validation failed: agents[${i}] ("${agent.name}") is missing a valid "command".`);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
300
623
|
|
|
624
|
+
async function loadConfig(configPath = null) {
|
|
301
625
|
try {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
const
|
|
306
|
-
|
|
626
|
+
// Always load bundled config as the defaults base
|
|
627
|
+
const bundledPath = path.join(__dirname, 'config.yaml');
|
|
628
|
+
const bundledFile = await fs.readFile(bundledPath, 'utf8');
|
|
629
|
+
const defaults = yaml.parse(bundledFile);
|
|
630
|
+
console.log('Loaded bundled defaults from:', bundledPath);
|
|
307
631
|
|
|
308
|
-
//
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
// Create outbox directory and per-agent outbox files
|
|
313
|
-
const outboxDir = path.join(workspacePath, config.outbox_dir || 'outbox');
|
|
314
|
-
await fs.mkdir(outboxDir, { recursive: true });
|
|
632
|
+
// Determine the user config path
|
|
633
|
+
let userConfigPath = null;
|
|
634
|
+
let isHomeConfig = false;
|
|
315
635
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
agentColors['user'] = config.user_color || '#a0aec0';
|
|
324
|
-
|
|
325
|
-
// Create empty outbox file for each agent
|
|
326
|
-
for (const agentConfig of config.agents) {
|
|
327
|
-
const outboxFile = path.join(outboxDir, `${agentConfig.name.toLowerCase()}.md`);
|
|
328
|
-
await fs.writeFile(outboxFile, '');
|
|
636
|
+
if (configPath) {
|
|
637
|
+
userConfigPath = path.isAbsolute(configPath) ? configPath : path.join(process.cwd(), configPath);
|
|
638
|
+
console.log('Loading user config from CLI arg:', userConfigPath);
|
|
639
|
+
} else if (fsSync.existsSync(HOME_CONFIG_FILE)) {
|
|
640
|
+
userConfigPath = HOME_CONFIG_FILE;
|
|
641
|
+
isHomeConfig = true;
|
|
642
|
+
console.log('Loading user config from home dir:', userConfigPath);
|
|
329
643
|
}
|
|
330
644
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
645
|
+
if (userConfigPath) {
|
|
646
|
+
const userFile = await fs.readFile(userConfigPath, 'utf8');
|
|
647
|
+
const userConfig = yaml.parse(userFile);
|
|
648
|
+
|
|
649
|
+
// Detect keys that will be backfilled from defaults
|
|
650
|
+
const missingKeys = findMissingKeys(defaults, userConfig);
|
|
651
|
+
if (missingKeys.length > 0) {
|
|
652
|
+
console.log('Config: backfilling missing keys from defaults:', missingKeys.join(', '));
|
|
653
|
+
|
|
654
|
+
// Write back merged config for home config only (not --config custom paths)
|
|
655
|
+
if (isHomeConfig) {
|
|
656
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
657
|
+
const backupPath = path.join(HOME_CONFIG_DIR, `config.yaml.bak.${timestamp}`);
|
|
658
|
+
await fs.copyFile(HOME_CONFIG_FILE, backupPath);
|
|
659
|
+
console.log('Config: backed up existing config to:', backupPath);
|
|
660
|
+
|
|
661
|
+
const merged = deepMerge(defaults, userConfig);
|
|
662
|
+
const tmpPath = HOME_CONFIG_FILE + '.tmp';
|
|
663
|
+
const yamlStr = '# Multi-Agent Chat Configuration\n'
|
|
664
|
+
+ '# Auto-updated with new defaults. Your values have been preserved.\n'
|
|
665
|
+
+ '# Backup of previous config: ' + path.basename(backupPath) + '\n\n'
|
|
666
|
+
+ yaml.stringify(merged, { indent: 2, lineWidth: 0 });
|
|
667
|
+
await fs.writeFile(tmpPath, yamlStr, 'utf8');
|
|
668
|
+
await fs.rename(tmpPath, HOME_CONFIG_FILE);
|
|
669
|
+
console.log('Config: wrote merged config back to:', HOME_CONFIG_FILE);
|
|
670
|
+
}
|
|
349
671
|
}
|
|
672
|
+
|
|
673
|
+
// Deep-merge: user values override defaults
|
|
674
|
+
config = deepMerge(defaults, userConfig);
|
|
675
|
+
} else {
|
|
676
|
+
console.log('No user config found, using bundled defaults');
|
|
677
|
+
config = defaults;
|
|
350
678
|
}
|
|
351
679
|
|
|
352
|
-
|
|
353
|
-
console.log('
|
|
354
|
-
|
|
355
|
-
console.log('Agent colors:', agentColors);
|
|
356
|
-
return workspacePath;
|
|
680
|
+
validateConfig(config);
|
|
681
|
+
console.log('Config loaded and validated successfully');
|
|
682
|
+
return config;
|
|
357
683
|
} catch (error) {
|
|
358
|
-
console.error('Error
|
|
684
|
+
console.error('Error loading config:', error);
|
|
359
685
|
throw error;
|
|
360
686
|
}
|
|
361
687
|
}
|
|
362
688
|
|
|
689
|
+
// ═══════════════════════════════════════════════════════════
|
|
363
690
|
// Agent Process Management
|
|
691
|
+
// ═══════════════════════════════════════════════════════════
|
|
692
|
+
|
|
364
693
|
class AgentProcess {
|
|
365
694
|
constructor(agentConfig, index) {
|
|
366
695
|
this.name = agentConfig.name;
|
|
@@ -370,14 +699,21 @@ class AgentProcess {
|
|
|
370
699
|
this.index = index;
|
|
371
700
|
this.process = null;
|
|
372
701
|
this.outputBuffer = [];
|
|
702
|
+
this.lastPrompt = null;
|
|
703
|
+
this.intentionalStop = false;
|
|
704
|
+
this.restartCount = 0;
|
|
705
|
+
this.maxRestarts = 3;
|
|
706
|
+
this.initDelay = agentConfig.init_delay_ms || (agentConfig.name === 'Codex' ? 5000 : 3000);
|
|
373
707
|
}
|
|
374
708
|
|
|
375
709
|
async start(prompt) {
|
|
710
|
+
this.lastPrompt = prompt;
|
|
711
|
+
this.intentionalStop = false;
|
|
712
|
+
|
|
376
713
|
return new Promise((resolve, reject) => {
|
|
377
714
|
console.log(`Starting agent ${this.name} with PTY: ${this.use_pty}`);
|
|
378
715
|
|
|
379
716
|
if (this.use_pty) {
|
|
380
|
-
// Use PTY for interactive TUI agents
|
|
381
717
|
const shell = process.env.SHELL || '/bin/bash';
|
|
382
718
|
|
|
383
719
|
this.process = pty.spawn(this.command, this.args, {
|
|
@@ -400,17 +736,13 @@ class AgentProcess {
|
|
|
400
736
|
|
|
401
737
|
console.log(`PTY spawned for ${this.name}, PID: ${this.process.pid}`);
|
|
402
738
|
|
|
403
|
-
// Respond to cursor position query immediately
|
|
404
|
-
// This helps with terminal capability detection (needed for Codex)
|
|
405
739
|
setTimeout(() => {
|
|
406
|
-
this.process.write('\x1b[1;1R');
|
|
740
|
+
this.process.write('\x1b[1;1R');
|
|
407
741
|
}, 100);
|
|
408
742
|
|
|
409
|
-
// Capture all output from PTY
|
|
410
743
|
this.process.onData((data) => {
|
|
411
744
|
const output = data.toString();
|
|
412
745
|
this.outputBuffer.push(output);
|
|
413
|
-
|
|
414
746
|
if (mainWindow) {
|
|
415
747
|
mainWindow.webContents.send('agent-output', {
|
|
416
748
|
agentName: this.name,
|
|
@@ -420,75 +752,50 @@ class AgentProcess {
|
|
|
420
752
|
}
|
|
421
753
|
});
|
|
422
754
|
|
|
423
|
-
// Handle exit - trigger resume if enabled
|
|
424
755
|
this.process.onExit(({ exitCode, signal }) => {
|
|
425
756
|
console.log(`Agent ${this.name} exited with code ${exitCode}, signal ${signal}`);
|
|
426
757
|
this.handleExit(exitCode);
|
|
427
758
|
});
|
|
428
759
|
|
|
429
|
-
// Inject prompt via PTY after TUI initializes (original working pattern)
|
|
430
|
-
const initDelay = this.name === 'Codex' ? 5000 : 3000;
|
|
431
760
|
setTimeout(() => {
|
|
432
761
|
console.log(`Injecting prompt into ${this.name} PTY`);
|
|
433
762
|
this.process.write(prompt + '\n');
|
|
434
|
-
|
|
435
|
-
// Send Enter key after a brief delay to submit
|
|
436
763
|
setTimeout(() => {
|
|
437
764
|
this.process.write('\r');
|
|
438
765
|
}, 500);
|
|
439
|
-
|
|
440
766
|
resolve();
|
|
441
|
-
}, initDelay);
|
|
767
|
+
}, this.initDelay);
|
|
442
768
|
|
|
443
769
|
} else {
|
|
444
|
-
// Use regular spawn for non-interactive agents
|
|
445
770
|
const options = {
|
|
446
771
|
cwd: agentCwd,
|
|
447
|
-
env: {
|
|
448
|
-
...process.env,
|
|
449
|
-
AGENT_NAME: this.name
|
|
450
|
-
}
|
|
772
|
+
env: { ...process.env, AGENT_NAME: this.name }
|
|
451
773
|
};
|
|
452
774
|
|
|
453
775
|
this.process = spawn(this.command, this.args, options);
|
|
454
|
-
|
|
455
776
|
console.log(`Process spawned for ${this.name}, PID: ${this.process.pid}`);
|
|
456
777
|
|
|
457
|
-
// Capture stdout
|
|
458
778
|
this.process.stdout.on('data', (data) => {
|
|
459
779
|
const output = data.toString();
|
|
460
780
|
this.outputBuffer.push(output);
|
|
461
|
-
|
|
462
781
|
if (mainWindow) {
|
|
463
|
-
mainWindow.webContents.send('agent-output', {
|
|
464
|
-
agentName: this.name,
|
|
465
|
-
output: output,
|
|
466
|
-
isPty: false
|
|
467
|
-
});
|
|
782
|
+
mainWindow.webContents.send('agent-output', { agentName: this.name, output, isPty: false });
|
|
468
783
|
}
|
|
469
784
|
});
|
|
470
785
|
|
|
471
|
-
// Capture stderr
|
|
472
786
|
this.process.stderr.on('data', (data) => {
|
|
473
787
|
const output = data.toString();
|
|
474
788
|
this.outputBuffer.push(`[stderr] ${output}`);
|
|
475
|
-
|
|
476
789
|
if (mainWindow) {
|
|
477
|
-
mainWindow.webContents.send('agent-output', {
|
|
478
|
-
agentName: this.name,
|
|
479
|
-
output: `[stderr] ${output}`,
|
|
480
|
-
isPty: false
|
|
481
|
-
});
|
|
790
|
+
mainWindow.webContents.send('agent-output', { agentName: this.name, output: `[stderr] ${output}`, isPty: false });
|
|
482
791
|
}
|
|
483
792
|
});
|
|
484
793
|
|
|
485
|
-
// Handle process exit - trigger resume if enabled
|
|
486
794
|
this.process.on('close', (code) => {
|
|
487
795
|
console.log(`Agent ${this.name} exited with code ${code}`);
|
|
488
796
|
this.handleExit(code);
|
|
489
797
|
});
|
|
490
798
|
|
|
491
|
-
// Handle errors
|
|
492
799
|
this.process.on('error', (error) => {
|
|
493
800
|
console.error(`Error starting agent ${this.name}:`, error);
|
|
494
801
|
reject(error);
|
|
@@ -499,14 +806,65 @@ class AgentProcess {
|
|
|
499
806
|
});
|
|
500
807
|
}
|
|
501
808
|
|
|
502
|
-
// Handle agent exit
|
|
503
809
|
handleExit(exitCode) {
|
|
504
|
-
if (
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
status
|
|
508
|
-
|
|
509
|
-
|
|
810
|
+
if (this.intentionalStop) {
|
|
811
|
+
console.log(`Agent ${this.name} stopped intentionally`);
|
|
812
|
+
if (mainWindow) {
|
|
813
|
+
mainWindow.webContents.send('agent-status', {
|
|
814
|
+
agentName: this.name,
|
|
815
|
+
status: 'stopped',
|
|
816
|
+
exitCode: exitCode
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// Unexpected exit - attempt relaunch
|
|
823
|
+
if (this.restartCount < this.maxRestarts && this.lastPrompt) {
|
|
824
|
+
this.restartCount++;
|
|
825
|
+
console.log(`Agent ${this.name} exited unexpectedly (code ${exitCode}), restarting (attempt ${this.restartCount}/${this.maxRestarts})...`);
|
|
826
|
+
|
|
827
|
+
if (mainWindow) {
|
|
828
|
+
mainWindow.webContents.send('agent-status', {
|
|
829
|
+
agentName: this.name,
|
|
830
|
+
status: 'restarting',
|
|
831
|
+
exitCode: exitCode,
|
|
832
|
+
restartCount: this.restartCount
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// Wait a moment before relaunching (give time for auto-updates etc.)
|
|
837
|
+
const delay = 2000 * this.restartCount;
|
|
838
|
+
setTimeout(() => {
|
|
839
|
+
this.start(this.lastPrompt).then(() => {
|
|
840
|
+
console.log(`Agent ${this.name} restarted successfully (attempt ${this.restartCount})`);
|
|
841
|
+
if (mainWindow) {
|
|
842
|
+
mainWindow.webContents.send('agent-status', {
|
|
843
|
+
agentName: this.name,
|
|
844
|
+
status: 'running'
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
}).catch(err => {
|
|
848
|
+
console.error(`Failed to restart agent ${this.name}:`, err);
|
|
849
|
+
if (mainWindow) {
|
|
850
|
+
mainWindow.webContents.send('agent-status', {
|
|
851
|
+
agentName: this.name,
|
|
852
|
+
status: 'stopped',
|
|
853
|
+
exitCode: exitCode,
|
|
854
|
+
error: `Restart failed: ${err.message}`
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
});
|
|
858
|
+
}, delay);
|
|
859
|
+
} else {
|
|
860
|
+
console.log(`Agent ${this.name} exited (code ${exitCode}), max restarts reached or no prompt stored`);
|
|
861
|
+
if (mainWindow) {
|
|
862
|
+
mainWindow.webContents.send('agent-status', {
|
|
863
|
+
agentName: this.name,
|
|
864
|
+
status: 'stopped',
|
|
865
|
+
exitCode: exitCode
|
|
866
|
+
});
|
|
867
|
+
}
|
|
510
868
|
}
|
|
511
869
|
}
|
|
512
870
|
|
|
@@ -514,10 +872,7 @@ class AgentProcess {
|
|
|
514
872
|
if (this.use_pty) {
|
|
515
873
|
if (this.process && this.process.write) {
|
|
516
874
|
this.process.write(message + '\n');
|
|
517
|
-
|
|
518
|
-
setTimeout(() => {
|
|
519
|
-
this.process.write('\r');
|
|
520
|
-
}, 300);
|
|
875
|
+
setTimeout(() => { this.process.write('\r'); }, 300);
|
|
521
876
|
}
|
|
522
877
|
} else {
|
|
523
878
|
if (this.process && this.process.stdin) {
|
|
@@ -527,6 +882,7 @@ class AgentProcess {
|
|
|
527
882
|
}
|
|
528
883
|
|
|
529
884
|
stop() {
|
|
885
|
+
this.intentionalStop = true;
|
|
530
886
|
if (this.process) {
|
|
531
887
|
if (this.use_pty) {
|
|
532
888
|
this.process.kill();
|
|
@@ -537,71 +893,67 @@ class AgentProcess {
|
|
|
537
893
|
}
|
|
538
894
|
}
|
|
539
895
|
|
|
540
|
-
// Initialize agents from config
|
|
541
896
|
function initializeAgents() {
|
|
542
897
|
agents = config.agents.map((agentConfig, index) => {
|
|
543
898
|
return new AgentProcess(agentConfig, index);
|
|
544
899
|
});
|
|
545
|
-
|
|
546
900
|
console.log(`Initialized ${agents.length} agents`);
|
|
547
901
|
return agents;
|
|
548
902
|
}
|
|
549
903
|
|
|
550
|
-
// Get agent by name
|
|
551
904
|
function getAgentByName(name) {
|
|
552
905
|
return agents.find(a => a.name.toLowerCase() === name.toLowerCase());
|
|
553
906
|
}
|
|
554
907
|
|
|
555
|
-
//
|
|
556
|
-
|
|
557
|
-
|
|
908
|
+
// ═══════════════════════════════════════════════════════════
|
|
909
|
+
// Message routing
|
|
910
|
+
// ═══════════════════════════════════════════════════════════
|
|
911
|
+
|
|
912
|
+
function getOutboxRelativePath(agentName) {
|
|
913
|
+
// Build path relative to agentCwd: sessions/<id>/outbox/<agent>.md
|
|
914
|
+
// But we need it relative from agentCwd perspective
|
|
915
|
+
const relFromProject = path.relative(agentCwd, workspacePath);
|
|
558
916
|
const outboxDir = config.outbox_dir || 'outbox';
|
|
917
|
+
return `${relFromProject}/${outboxDir}/${agentName.toLowerCase()}.md`;
|
|
918
|
+
}
|
|
559
919
|
|
|
920
|
+
function sendMessageToOtherAgents(senderName, message) {
|
|
560
921
|
for (const agent of agents) {
|
|
561
922
|
if (agent.name.toLowerCase() !== senderName.toLowerCase()) {
|
|
562
|
-
|
|
563
|
-
const outboxFile = `${workspaceFolder}/${outboxDir}/${agent.name.toLowerCase()}.md`;
|
|
923
|
+
const outboxFile = getOutboxRelativePath(agent.name);
|
|
564
924
|
const formattedMessage = `\n---\n📨 MESSAGE FROM ${senderName.toUpperCase()}:\n\n${message}\n\n---\n(Respond via: cat << 'EOF' > ${outboxFile})\n`;
|
|
565
|
-
|
|
566
925
|
console.log(`Delivering message from ${senderName} to ${agent.name}`);
|
|
567
926
|
agent.sendMessage(formattedMessage);
|
|
568
927
|
}
|
|
569
928
|
}
|
|
570
929
|
}
|
|
571
930
|
|
|
572
|
-
// Send a message to ALL agents (for user messages)
|
|
573
931
|
function sendMessageToAllAgents(message) {
|
|
574
|
-
const workspaceFolder = path.basename(workspacePath);
|
|
575
|
-
const outboxDir = config.outbox_dir || 'outbox';
|
|
576
|
-
|
|
577
932
|
for (const agent of agents) {
|
|
578
|
-
|
|
579
|
-
const outboxFile = `${workspaceFolder}/${outboxDir}/${agent.name.toLowerCase()}.md`;
|
|
933
|
+
const outboxFile = getOutboxRelativePath(agent.name);
|
|
580
934
|
const formattedMessage = `\n---\n📨 MESSAGE FROM USER:\n\n${message}\n\n---\n(Respond via: cat << 'EOF' > ${outboxFile})\n`;
|
|
581
|
-
|
|
582
935
|
console.log(`Delivering user message to ${agent.name}`);
|
|
583
936
|
agent.sendMessage(formattedMessage);
|
|
584
937
|
}
|
|
585
938
|
}
|
|
586
939
|
|
|
587
|
-
// Build prompt for a specific agent
|
|
588
940
|
function buildAgentPrompt(challenge, agentName) {
|
|
589
|
-
const
|
|
941
|
+
const relFromProject = path.relative(agentCwd, workspacePath);
|
|
590
942
|
const outboxDir = config.outbox_dir || 'outbox';
|
|
591
|
-
|
|
592
|
-
const
|
|
593
|
-
|
|
943
|
+
const outboxFile = `${relFromProject}/${outboxDir}/${agentName.toLowerCase()}.md`;
|
|
944
|
+
const planFile = `${relFromProject}/${config.plan_file || 'PLAN_FINAL.md'}`;
|
|
945
|
+
|
|
946
|
+
const template = config.prompt_template || `## Multi-Agent Collaboration Session\n\n**You are: {agent_name}**\nYou are collaborating with: {agent_names}\n\n## Topic\n\n{challenge}\n\nSend messages by writing to: {outbox_file}\nFinal plan goes in: {plan_file}\n`;
|
|
594
947
|
|
|
595
|
-
return
|
|
948
|
+
return template
|
|
596
949
|
.replace('{challenge}', challenge)
|
|
597
950
|
.replace('{workspace}', workspacePath)
|
|
598
|
-
.replace(/{outbox_file}/g, outboxFile)
|
|
599
|
-
.replace(/{plan_file}/g, planFile)
|
|
951
|
+
.replace(/{outbox_file}/g, outboxFile)
|
|
952
|
+
.replace(/{plan_file}/g, planFile)
|
|
600
953
|
.replace('{agent_names}', agents.map(a => a.name).join(', '))
|
|
601
954
|
.replace('{agent_name}', agentName);
|
|
602
955
|
}
|
|
603
956
|
|
|
604
|
-
// Start all agents with their individual prompts
|
|
605
957
|
async function startAgents(challenge) {
|
|
606
958
|
console.log('Starting agents with prompts...');
|
|
607
959
|
|
|
@@ -619,7 +971,6 @@ async function startAgents(challenge) {
|
|
|
619
971
|
}
|
|
620
972
|
} catch (error) {
|
|
621
973
|
console.error(`Failed to start agent ${agent.name}:`, error);
|
|
622
|
-
|
|
623
974
|
if (mainWindow) {
|
|
624
975
|
mainWindow.webContents.send('agent-status', {
|
|
625
976
|
agentName: agent.name,
|
|
@@ -631,7 +982,10 @@ async function startAgents(challenge) {
|
|
|
631
982
|
}
|
|
632
983
|
}
|
|
633
984
|
|
|
634
|
-
//
|
|
985
|
+
// ═══════════════════════════════════════════════════════════
|
|
986
|
+
// File watchers
|
|
987
|
+
// ═══════════════════════════════════════════════════════════
|
|
988
|
+
|
|
635
989
|
function startFileWatcher() {
|
|
636
990
|
const chatPath = path.join(workspacePath, config.chat_file || 'chat.jsonl');
|
|
637
991
|
|
|
@@ -640,8 +994,6 @@ function startFileWatcher() {
|
|
|
640
994
|
ignoreInitial: true
|
|
641
995
|
});
|
|
642
996
|
|
|
643
|
-
// Note: Primary updates happen via 'chat-message' events sent when outbox is processed
|
|
644
|
-
// This watcher is a backup for any external modifications
|
|
645
997
|
fileWatcher.on('change', async () => {
|
|
646
998
|
try {
|
|
647
999
|
const messages = await getChatContent();
|
|
@@ -656,47 +1008,38 @@ function startFileWatcher() {
|
|
|
656
1008
|
console.log('File watcher started for:', chatPath);
|
|
657
1009
|
}
|
|
658
1010
|
|
|
659
|
-
// Watch outbox directory and merge messages into chat.jsonl
|
|
660
1011
|
function startOutboxWatcher() {
|
|
661
1012
|
const outboxDir = path.join(workspacePath, config.outbox_dir || 'outbox');
|
|
662
1013
|
const chatPath = path.join(workspacePath, config.chat_file || 'chat.jsonl');
|
|
663
1014
|
|
|
664
|
-
// Track which files we're currently processing to avoid race conditions
|
|
665
1015
|
const processing = new Set();
|
|
666
1016
|
|
|
667
1017
|
outboxWatcher = chokidar.watch(outboxDir, {
|
|
668
1018
|
persistent: true,
|
|
669
1019
|
ignoreInitial: true,
|
|
670
1020
|
awaitWriteFinish: {
|
|
671
|
-
stabilityThreshold: 500,
|
|
1021
|
+
stabilityThreshold: 500,
|
|
672
1022
|
pollInterval: 100
|
|
673
1023
|
}
|
|
674
1024
|
});
|
|
675
1025
|
|
|
676
1026
|
outboxWatcher.on('change', async (filePath) => {
|
|
677
|
-
// Only process .md files
|
|
678
1027
|
if (!filePath.endsWith('.md')) return;
|
|
679
|
-
|
|
680
|
-
// Avoid processing the same file concurrently
|
|
681
1028
|
if (processing.has(filePath)) return;
|
|
682
1029
|
processing.add(filePath);
|
|
683
1030
|
|
|
684
1031
|
try {
|
|
685
|
-
// Read the outbox file
|
|
686
1032
|
const content = await fs.readFile(filePath, 'utf8');
|
|
687
1033
|
const trimmedContent = content.trim();
|
|
688
1034
|
|
|
689
|
-
// Skip if empty
|
|
690
1035
|
if (!trimmedContent) {
|
|
691
1036
|
processing.delete(filePath);
|
|
692
1037
|
return;
|
|
693
1038
|
}
|
|
694
1039
|
|
|
695
|
-
// Extract agent name from filename (e.g., "claude.md" -> "Claude")
|
|
696
1040
|
const filename = path.basename(filePath, '.md');
|
|
697
1041
|
const agentName = filename.charAt(0).toUpperCase() + filename.slice(1);
|
|
698
1042
|
|
|
699
|
-
// Increment sequence and create message object
|
|
700
1043
|
messageSequence++;
|
|
701
1044
|
const timestamp = new Date().toISOString();
|
|
702
1045
|
const message = {
|
|
@@ -708,21 +1051,24 @@ function startOutboxWatcher() {
|
|
|
708
1051
|
color: agentColors[agentName.toLowerCase()] || '#667eea'
|
|
709
1052
|
};
|
|
710
1053
|
|
|
711
|
-
// Append to chat.jsonl
|
|
712
1054
|
await fs.appendFile(chatPath, JSON.stringify(message) + '\n');
|
|
713
1055
|
console.log(`Merged message from ${agentName} (#${messageSequence}) into chat.jsonl`);
|
|
714
1056
|
|
|
715
|
-
// Clear the outbox file
|
|
716
1057
|
await fs.writeFile(filePath, '');
|
|
717
1058
|
|
|
718
|
-
// PUSH message to other agents' PTYs
|
|
719
1059
|
sendMessageToOtherAgents(agentName, trimmedContent);
|
|
720
1060
|
|
|
721
|
-
//
|
|
1061
|
+
// Update session index with message count and lastActiveAt
|
|
1062
|
+
if (currentSessionId && agentCwd) {
|
|
1063
|
+
updateSessionInIndex(agentCwd, currentSessionId, {
|
|
1064
|
+
lastActiveAt: timestamp,
|
|
1065
|
+
messageCount: messageSequence
|
|
1066
|
+
}).catch(e => console.warn('Failed to update session index:', e.message));
|
|
1067
|
+
}
|
|
1068
|
+
|
|
722
1069
|
if (mainWindow) {
|
|
723
1070
|
mainWindow.webContents.send('chat-message', message);
|
|
724
1071
|
}
|
|
725
|
-
|
|
726
1072
|
} catch (error) {
|
|
727
1073
|
console.error(`Error processing outbox file ${filePath}:`, error);
|
|
728
1074
|
} finally {
|
|
@@ -733,7 +1079,6 @@ function startOutboxWatcher() {
|
|
|
733
1079
|
console.log('Outbox watcher started for:', outboxDir);
|
|
734
1080
|
}
|
|
735
1081
|
|
|
736
|
-
// Stop outbox watcher
|
|
737
1082
|
function stopOutboxWatcher() {
|
|
738
1083
|
if (outboxWatcher) {
|
|
739
1084
|
outboxWatcher.close();
|
|
@@ -741,7 +1086,10 @@ function stopOutboxWatcher() {
|
|
|
741
1086
|
}
|
|
742
1087
|
}
|
|
743
1088
|
|
|
744
|
-
//
|
|
1089
|
+
// ═══════════════════════════════════════════════════════════
|
|
1090
|
+
// Chat / Plan / Diff
|
|
1091
|
+
// ═══════════════════════════════════════════════════════════
|
|
1092
|
+
|
|
745
1093
|
async function sendUserMessage(messageText) {
|
|
746
1094
|
const chatPath = path.join(workspacePath, config.chat_file || 'chat.jsonl');
|
|
747
1095
|
messageSequence++;
|
|
@@ -757,39 +1105,37 @@ async function sendUserMessage(messageText) {
|
|
|
757
1105
|
};
|
|
758
1106
|
|
|
759
1107
|
try {
|
|
760
|
-
// Append to chat.jsonl
|
|
761
1108
|
await fs.appendFile(chatPath, JSON.stringify(message) + '\n');
|
|
762
1109
|
console.log(`User message #${messageSequence} appended to chat`);
|
|
763
1110
|
|
|
764
|
-
// PUSH message to all agents' PTYs
|
|
765
1111
|
sendMessageToAllAgents(messageText);
|
|
766
1112
|
|
|
767
|
-
//
|
|
1113
|
+
// Update session index
|
|
1114
|
+
if (currentSessionId && agentCwd) {
|
|
1115
|
+
updateSessionInIndex(agentCwd, currentSessionId, {
|
|
1116
|
+
lastActiveAt: timestamp,
|
|
1117
|
+
messageCount: messageSequence
|
|
1118
|
+
}).catch(e => console.warn('Failed to update session index:', e.message));
|
|
1119
|
+
}
|
|
1120
|
+
|
|
768
1121
|
if (mainWindow) {
|
|
769
1122
|
mainWindow.webContents.send('chat-message', message);
|
|
770
1123
|
}
|
|
771
|
-
|
|
772
1124
|
} catch (error) {
|
|
773
1125
|
console.error('Error appending user message:', error);
|
|
774
1126
|
throw error;
|
|
775
1127
|
}
|
|
776
1128
|
}
|
|
777
1129
|
|
|
778
|
-
// Read current chat content (returns array of message objects)
|
|
779
1130
|
async function getChatContent() {
|
|
780
1131
|
const chatPath = path.join(workspacePath, config.chat_file || 'chat.jsonl');
|
|
781
1132
|
try {
|
|
782
1133
|
const content = await fs.readFile(chatPath, 'utf8');
|
|
783
1134
|
if (!content.trim()) return [];
|
|
784
1135
|
|
|
785
|
-
// Parse JSONL (one JSON object per line)
|
|
786
1136
|
const messages = content.trim().split('\n').map(line => {
|
|
787
|
-
try {
|
|
788
|
-
|
|
789
|
-
} catch (e) {
|
|
790
|
-
console.error('Failed to parse chat line:', line);
|
|
791
|
-
return null;
|
|
792
|
-
}
|
|
1137
|
+
try { return JSON.parse(line); }
|
|
1138
|
+
catch (e) { console.error('Failed to parse chat line:', line); return null; }
|
|
793
1139
|
}).filter(Boolean);
|
|
794
1140
|
|
|
795
1141
|
return messages;
|
|
@@ -799,7 +1145,6 @@ async function getChatContent() {
|
|
|
799
1145
|
}
|
|
800
1146
|
}
|
|
801
1147
|
|
|
802
|
-
// Read final plan
|
|
803
1148
|
async function getPlanContent() {
|
|
804
1149
|
const planPath = path.join(workspacePath, config.plan_file || 'PLAN_FINAL.md');
|
|
805
1150
|
try {
|
|
@@ -809,14 +1154,11 @@ async function getPlanContent() {
|
|
|
809
1154
|
}
|
|
810
1155
|
}
|
|
811
1156
|
|
|
812
|
-
// Get git diff - shows uncommitted changes only (git diff HEAD)
|
|
813
1157
|
async function getGitDiff() {
|
|
814
|
-
// Not a git repo or session hasn't started
|
|
815
1158
|
if (!agentCwd) {
|
|
816
1159
|
return { isGitRepo: false, error: 'No session active' };
|
|
817
1160
|
}
|
|
818
1161
|
|
|
819
|
-
// Check if git repo
|
|
820
1162
|
try {
|
|
821
1163
|
await execAsync('git rev-parse --git-dir', { cwd: agentCwd });
|
|
822
1164
|
} catch (error) {
|
|
@@ -831,7 +1173,6 @@ async function getGitDiff() {
|
|
|
831
1173
|
untracked: []
|
|
832
1174
|
};
|
|
833
1175
|
|
|
834
|
-
// Check if HEAD exists (repo might have no commits)
|
|
835
1176
|
let hasHead = true;
|
|
836
1177
|
try {
|
|
837
1178
|
await execAsync('git rev-parse HEAD', { cwd: agentCwd });
|
|
@@ -839,17 +1180,13 @@ async function getGitDiff() {
|
|
|
839
1180
|
hasHead = false;
|
|
840
1181
|
}
|
|
841
1182
|
|
|
842
|
-
// Determine diff target - use empty tree hash if no commits yet
|
|
843
1183
|
const diffTarget = hasHead ? 'HEAD' : '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
|
|
844
1184
|
|
|
845
|
-
// Get diff stats
|
|
846
1185
|
try {
|
|
847
1186
|
const { stdout: statOutput } = await execAsync(
|
|
848
1187
|
`git diff ${diffTarget} --stat`,
|
|
849
1188
|
{ cwd: agentCwd, maxBuffer: 10 * 1024 * 1024 }
|
|
850
1189
|
);
|
|
851
|
-
|
|
852
|
-
// Parse stats from last line (e.g., "3 files changed, 10 insertions(+), 5 deletions(-)")
|
|
853
1190
|
const statMatch = statOutput.match(/(\d+) files? changed(?:, (\d+) insertions?\(\+\))?(?:, (\d+) deletions?\(-\))?/);
|
|
854
1191
|
if (statMatch) {
|
|
855
1192
|
result.stats.filesChanged = parseInt(statMatch[1]) || 0;
|
|
@@ -857,10 +1194,9 @@ async function getGitDiff() {
|
|
|
857
1194
|
result.stats.deletions = parseInt(statMatch[3]) || 0;
|
|
858
1195
|
}
|
|
859
1196
|
} catch (e) {
|
|
860
|
-
// No changes
|
|
1197
|
+
// No changes
|
|
861
1198
|
}
|
|
862
1199
|
|
|
863
|
-
// Get full diff
|
|
864
1200
|
try {
|
|
865
1201
|
const { stdout: diffOutput } = await execAsync(
|
|
866
1202
|
`git diff ${diffTarget}`,
|
|
@@ -871,7 +1207,6 @@ async function getGitDiff() {
|
|
|
871
1207
|
result.diff = '';
|
|
872
1208
|
}
|
|
873
1209
|
|
|
874
|
-
// Get untracked files
|
|
875
1210
|
try {
|
|
876
1211
|
const { stdout: untrackedOutput } = await execAsync(
|
|
877
1212
|
'git ls-files --others --exclude-standard',
|
|
@@ -889,7 +1224,10 @@ async function getGitDiff() {
|
|
|
889
1224
|
}
|
|
890
1225
|
}
|
|
891
1226
|
|
|
1227
|
+
// ═══════════════════════════════════════════════════════════
|
|
892
1228
|
// Stop all agents and watchers
|
|
1229
|
+
// ═══════════════════════════════════════════════════════════
|
|
1230
|
+
|
|
893
1231
|
function stopAllAgents() {
|
|
894
1232
|
agents.forEach(agent => agent.stop());
|
|
895
1233
|
if (fileWatcher) {
|
|
@@ -899,7 +1237,10 @@ function stopAllAgents() {
|
|
|
899
1237
|
stopOutboxWatcher();
|
|
900
1238
|
}
|
|
901
1239
|
|
|
1240
|
+
// ═══════════════════════════════════════════════════════════
|
|
902
1241
|
// IPC Handlers
|
|
1242
|
+
// ═══════════════════════════════════════════════════════════
|
|
1243
|
+
|
|
903
1244
|
ipcMain.handle('load-config', async () => {
|
|
904
1245
|
try {
|
|
905
1246
|
console.log('IPC: load-config called');
|
|
@@ -912,41 +1253,224 @@ ipcMain.handle('load-config', async () => {
|
|
|
912
1253
|
}
|
|
913
1254
|
});
|
|
914
1255
|
|
|
915
|
-
|
|
1256
|
+
// Returns CWD (or CLI workspace) as the default workspace
|
|
1257
|
+
ipcMain.handle('get-current-workspace', async () => {
|
|
1258
|
+
const ws = customWorkspacePath || process.cwd();
|
|
1259
|
+
const resolved = path.isAbsolute(ws) ? ws : path.resolve(ws);
|
|
1260
|
+
return {
|
|
1261
|
+
path: resolved,
|
|
1262
|
+
name: path.basename(resolved)
|
|
1263
|
+
};
|
|
1264
|
+
});
|
|
1265
|
+
|
|
1266
|
+
// Returns sessions for a given workspace/project root
|
|
1267
|
+
ipcMain.handle('get-sessions-for-workspace', async (event, projectRoot) => {
|
|
1268
|
+
try {
|
|
1269
|
+
// Migrate from flat workspace if needed
|
|
1270
|
+
await migrateFromFlatWorkspace(projectRoot);
|
|
1271
|
+
|
|
1272
|
+
const index = await withSessionsLock(async () => {
|
|
1273
|
+
const idx = await loadSessionsIndex(projectRoot);
|
|
1274
|
+
|
|
1275
|
+
// Reconcile stale active sessions: if no agents are running for a session,
|
|
1276
|
+
// downgrade it from 'active' to 'completed'
|
|
1277
|
+
let reconciled = false;
|
|
1278
|
+
for (const session of idx.sessions) {
|
|
1279
|
+
if (session.status === 'active' && session.id !== currentSessionId) {
|
|
1280
|
+
session.status = 'completed';
|
|
1281
|
+
reconciled = true;
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
if (reconciled) {
|
|
1285
|
+
await saveSessionsIndex(projectRoot, idx);
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
return idx;
|
|
1289
|
+
});
|
|
1290
|
+
|
|
1291
|
+
return index.sessions || [];
|
|
1292
|
+
} catch (error) {
|
|
1293
|
+
console.error('Error getting sessions:', error);
|
|
1294
|
+
return [];
|
|
1295
|
+
}
|
|
1296
|
+
});
|
|
1297
|
+
|
|
1298
|
+
// Load session data (chat, plan) without starting agents
|
|
1299
|
+
ipcMain.handle('load-session', async (event, { projectRoot, sessionId }) => {
|
|
916
1300
|
try {
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
1301
|
+
const data = await loadSessionData(projectRoot, sessionId);
|
|
1302
|
+
|
|
1303
|
+
// Set workspace path to session dir so getChatContent/getPlanContent work
|
|
1304
|
+
workspacePath = data.sessionDir;
|
|
1305
|
+
agentCwd = projectRoot;
|
|
1306
|
+
currentSessionId = sessionId;
|
|
1307
|
+
|
|
1308
|
+
// Build agent colors
|
|
1309
|
+
const defaultColors = config.default_agent_colors || ['#667eea', '#f093fb', '#4fd1c5', '#f6ad55', '#68d391', '#fc8181'];
|
|
1310
|
+
agentColors = {};
|
|
1311
|
+
config.agents.forEach((agentConfig, index) => {
|
|
1312
|
+
agentColors[agentConfig.name.toLowerCase()] = agentConfig.color || defaultColors[index % defaultColors.length];
|
|
1313
|
+
});
|
|
1314
|
+
agentColors['user'] = config.user_color || '#a0aec0';
|
|
1315
|
+
|
|
1316
|
+
// Set messageSequence to last message's seq
|
|
1317
|
+
if (data.messages.length > 0) {
|
|
1318
|
+
messageSequence = data.messages[data.messages.length - 1].seq || data.messages.length;
|
|
1319
|
+
} else {
|
|
1320
|
+
messageSequence = 0;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
return {
|
|
1324
|
+
success: true,
|
|
1325
|
+
messages: data.messages,
|
|
1326
|
+
plan: data.plan,
|
|
1327
|
+
colors: agentColors,
|
|
1328
|
+
sessionDir: data.sessionDir
|
|
1329
|
+
};
|
|
1330
|
+
} catch (error) {
|
|
1331
|
+
console.error('Error loading session:', error);
|
|
1332
|
+
return { success: false, error: error.message };
|
|
1333
|
+
}
|
|
1334
|
+
});
|
|
1335
|
+
|
|
1336
|
+
// Resume session - initialize agents with resume context
|
|
1337
|
+
ipcMain.handle('resume-session', async (event, { projectRoot, sessionId, newMessage }) => {
|
|
1338
|
+
try {
|
|
1339
|
+
const data = await loadSessionData(projectRoot, sessionId);
|
|
1340
|
+
|
|
1341
|
+
// Set workspace path and agentCwd
|
|
1342
|
+
workspacePath = data.sessionDir;
|
|
1343
|
+
agentCwd = projectRoot;
|
|
1344
|
+
currentSessionId = sessionId;
|
|
1345
|
+
|
|
1346
|
+
await initWorkspaceBase(projectRoot);
|
|
1347
|
+
|
|
1348
|
+
// Set messageSequence
|
|
1349
|
+
if (data.messages.length > 0) {
|
|
1350
|
+
messageSequence = data.messages[data.messages.length - 1].seq || data.messages.length;
|
|
1351
|
+
} else {
|
|
1352
|
+
messageSequence = 0;
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
// Ensure outbox files are clean
|
|
1356
|
+
const outboxDir = path.join(workspacePath, config.outbox_dir || 'outbox');
|
|
1357
|
+
await fs.mkdir(outboxDir, { recursive: true });
|
|
1358
|
+
for (const agentConfig of config.agents) {
|
|
1359
|
+
const outboxFile = path.join(outboxDir, `${agentConfig.name.toLowerCase()}.md`);
|
|
1360
|
+
await fs.writeFile(outboxFile, '');
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
// Initialize agents
|
|
920
1364
|
initializeAgents();
|
|
921
|
-
await startAgents(challenge);
|
|
922
|
-
startFileWatcher();
|
|
923
|
-
startOutboxWatcher(); // Watch for agent messages and merge into chat.jsonl
|
|
924
1365
|
|
|
925
|
-
//
|
|
926
|
-
|
|
927
|
-
|
|
1366
|
+
// Build resume prompts and start agents
|
|
1367
|
+
const chatSummary = generateChatSummary(data.messages);
|
|
1368
|
+
console.log('Starting agents with resume prompts...');
|
|
1369
|
+
|
|
1370
|
+
for (const agent of agents) {
|
|
1371
|
+
try {
|
|
1372
|
+
const prompt = buildResumePrompt(chatSummary, data.plan, newMessage, agent.name);
|
|
1373
|
+
await agent.start(prompt);
|
|
1374
|
+
console.log(`Started agent: ${agent.name} (resumed)`);
|
|
1375
|
+
|
|
1376
|
+
if (mainWindow) {
|
|
1377
|
+
mainWindow.webContents.send('agent-status', {
|
|
1378
|
+
agentName: agent.name,
|
|
1379
|
+
status: 'running'
|
|
1380
|
+
});
|
|
1381
|
+
}
|
|
1382
|
+
} catch (error) {
|
|
1383
|
+
console.error(`Failed to start agent ${agent.name}:`, error);
|
|
1384
|
+
if (mainWindow) {
|
|
1385
|
+
mainWindow.webContents.send('agent-status', {
|
|
1386
|
+
agentName: agent.name,
|
|
1387
|
+
status: 'error',
|
|
1388
|
+
error: error.message
|
|
1389
|
+
});
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
928
1392
|
}
|
|
929
1393
|
|
|
930
|
-
//
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
);
|
|
1394
|
+
// Append user message to chat
|
|
1395
|
+
await sendUserMessage(newMessage);
|
|
1396
|
+
|
|
1397
|
+
// Start watchers
|
|
1398
|
+
startFileWatcher();
|
|
1399
|
+
startOutboxWatcher();
|
|
1400
|
+
|
|
1401
|
+
// Update session index
|
|
1402
|
+
await updateSessionInIndex(projectRoot, sessionId, {
|
|
1403
|
+
lastActiveAt: new Date().toISOString(),
|
|
1404
|
+
status: 'active'
|
|
1405
|
+
});
|
|
1406
|
+
|
|
1407
|
+
// Add to recent workspaces
|
|
1408
|
+
await addRecentWorkspace(projectRoot);
|
|
936
1409
|
|
|
937
1410
|
return {
|
|
938
1411
|
success: true,
|
|
939
1412
|
agents: agents.map(a => ({ name: a.name, use_pty: a.use_pty })),
|
|
940
|
-
workspace: agentCwd,
|
|
1413
|
+
workspace: agentCwd,
|
|
941
1414
|
colors: agentColors,
|
|
942
|
-
|
|
1415
|
+
sessionId: sessionId
|
|
943
1416
|
};
|
|
944
1417
|
} catch (error) {
|
|
945
|
-
console.error('Error
|
|
1418
|
+
console.error('Error resuming session:', error);
|
|
1419
|
+
return { success: false, error: error.message };
|
|
1420
|
+
}
|
|
1421
|
+
});
|
|
1422
|
+
|
|
1423
|
+
// Start new session - creates session dir, starts agents
|
|
1424
|
+
ipcMain.handle('start-session', async (event, { challenge, workspace: selectedWorkspace }) => {
|
|
1425
|
+
try {
|
|
1426
|
+
const projectRoot = selectedWorkspace || customWorkspacePath || process.cwd();
|
|
1427
|
+
const resolvedRoot = path.isAbsolute(projectRoot) ? projectRoot : path.resolve(projectRoot);
|
|
1428
|
+
|
|
1429
|
+
await initWorkspaceBase(resolvedRoot);
|
|
1430
|
+
|
|
1431
|
+
// Migrate if needed
|
|
1432
|
+
await migrateFromFlatWorkspace(resolvedRoot);
|
|
1433
|
+
|
|
1434
|
+
// Create new session
|
|
1435
|
+
const sessionId = generateSessionId();
|
|
1436
|
+
currentSessionId = sessionId;
|
|
1437
|
+
const sessionDir = await setupSessionDirectory(resolvedRoot, sessionId);
|
|
1438
|
+
workspacePath = sessionDir;
|
|
1439
|
+
|
|
1440
|
+
// Add to session index
|
|
1441
|
+
const sessionMeta = {
|
|
1442
|
+
id: sessionId,
|
|
1443
|
+
title: generateSessionTitle(challenge),
|
|
1444
|
+
firstPrompt: challenge.slice(0, 200),
|
|
1445
|
+
workspace: resolvedRoot,
|
|
1446
|
+
createdAt: new Date().toISOString(),
|
|
1447
|
+
lastActiveAt: new Date().toISOString(),
|
|
1448
|
+
messageCount: 0,
|
|
1449
|
+
status: 'active'
|
|
1450
|
+
};
|
|
1451
|
+
await addSessionToIndex(resolvedRoot, sessionMeta);
|
|
1452
|
+
|
|
1453
|
+
// Reset message sequence
|
|
1454
|
+
messageSequence = 0;
|
|
1455
|
+
|
|
1456
|
+
initializeAgents();
|
|
1457
|
+
await startAgents(challenge);
|
|
1458
|
+
startFileWatcher();
|
|
1459
|
+
startOutboxWatcher();
|
|
1460
|
+
|
|
1461
|
+
// Add workspace to recents
|
|
1462
|
+
await addRecentWorkspace(resolvedRoot);
|
|
1463
|
+
|
|
946
1464
|
return {
|
|
947
|
-
success:
|
|
948
|
-
|
|
1465
|
+
success: true,
|
|
1466
|
+
agents: agents.map(a => ({ name: a.name, use_pty: a.use_pty })),
|
|
1467
|
+
workspace: agentCwd,
|
|
1468
|
+
colors: agentColors,
|
|
1469
|
+
sessionId: sessionId
|
|
949
1470
|
};
|
|
1471
|
+
} catch (error) {
|
|
1472
|
+
console.error('Error starting session:', error);
|
|
1473
|
+
return { success: false, error: error.message };
|
|
950
1474
|
}
|
|
951
1475
|
});
|
|
952
1476
|
|
|
@@ -976,46 +1500,24 @@ ipcMain.handle('stop-agents', async () => {
|
|
|
976
1500
|
return { success: true };
|
|
977
1501
|
});
|
|
978
1502
|
|
|
1503
|
+
// Reset session - marks current session as completed, stops agents
|
|
979
1504
|
ipcMain.handle('reset-session', async () => {
|
|
980
1505
|
try {
|
|
981
|
-
// Stop all agents and watchers
|
|
982
1506
|
stopAllAgents();
|
|
983
1507
|
|
|
984
|
-
//
|
|
985
|
-
if (
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
}
|
|
990
|
-
if (e.code !== 'ENOENT') throw e;
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
// Clear plan file (handle missing file gracefully)
|
|
994
|
-
const planPath = path.join(workspacePath, config.plan_file || 'PLAN_FINAL.md');
|
|
995
|
-
try {
|
|
996
|
-
await fs.writeFile(planPath, '');
|
|
997
|
-
} catch (e) {
|
|
998
|
-
if (e.code !== 'ENOENT') throw e;
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
// Clear outbox files
|
|
1002
|
-
const outboxDir = path.join(workspacePath, config.outbox_dir || 'outbox');
|
|
1003
|
-
try {
|
|
1004
|
-
const files = await fs.readdir(outboxDir);
|
|
1005
|
-
for (const file of files) {
|
|
1006
|
-
if (file.endsWith('.md')) {
|
|
1007
|
-
await fs.writeFile(path.join(outboxDir, file), '');
|
|
1008
|
-
}
|
|
1009
|
-
}
|
|
1010
|
-
} catch (e) {
|
|
1011
|
-
if (e.code !== 'ENOENT') throw e;
|
|
1012
|
-
}
|
|
1508
|
+
// Mark current session as completed in index
|
|
1509
|
+
if (currentSessionId && agentCwd) {
|
|
1510
|
+
await updateSessionInIndex(agentCwd, currentSessionId, {
|
|
1511
|
+
status: 'completed',
|
|
1512
|
+
lastActiveAt: new Date().toISOString()
|
|
1513
|
+
});
|
|
1013
1514
|
}
|
|
1014
1515
|
|
|
1015
1516
|
// Reset state
|
|
1016
1517
|
messageSequence = 0;
|
|
1017
1518
|
agents = [];
|
|
1018
1519
|
sessionBaseCommit = null;
|
|
1520
|
+
currentSessionId = null;
|
|
1019
1521
|
|
|
1020
1522
|
return { success: true };
|
|
1021
1523
|
} catch (error) {
|
|
@@ -1024,19 +1526,15 @@ ipcMain.handle('reset-session', async () => {
|
|
|
1024
1526
|
}
|
|
1025
1527
|
});
|
|
1026
1528
|
|
|
1027
|
-
// Handle start implementation request
|
|
1028
1529
|
ipcMain.handle('start-implementation', async (event, selectedAgent, otherAgents) => {
|
|
1029
1530
|
try {
|
|
1030
|
-
// Get the implementation handoff prompt from config
|
|
1031
1531
|
const promptTemplate = config.prompts?.implementation_handoff ||
|
|
1032
1532
|
'{selected_agent}, please now implement this plan. {other_agents} please wait for confirmation from {selected_agent} that they have completed the implementation. You should then check the changes, and provide feedback if necessary. Keep iterating together until you are all happy with the implementation.';
|
|
1033
1533
|
|
|
1034
|
-
// Substitute placeholders
|
|
1035
1534
|
const prompt = promptTemplate
|
|
1036
1535
|
.replace(/{selected_agent}/g, selectedAgent)
|
|
1037
1536
|
.replace(/{other_agents}/g, otherAgents.join(', '));
|
|
1038
1537
|
|
|
1039
|
-
// Send as user message (this handles chat log + delivery to all agents)
|
|
1040
1538
|
await sendUserMessage(prompt);
|
|
1041
1539
|
|
|
1042
1540
|
console.log(`Implementation started with ${selectedAgent} as implementer`);
|
|
@@ -1050,7 +1548,6 @@ ipcMain.handle('start-implementation', async (event, selectedAgent, otherAgents)
|
|
|
1050
1548
|
// Workspace Management IPC Handlers
|
|
1051
1549
|
ipcMain.handle('get-recent-workspaces', async () => {
|
|
1052
1550
|
const recents = await loadRecentWorkspaces();
|
|
1053
|
-
// Add validation info for each workspace
|
|
1054
1551
|
const withValidation = await Promise.all(
|
|
1055
1552
|
recents.map(async (r) => ({
|
|
1056
1553
|
...r,
|
|
@@ -1061,24 +1558,22 @@ ipcMain.handle('get-recent-workspaces', async () => {
|
|
|
1061
1558
|
return withValidation;
|
|
1062
1559
|
});
|
|
1063
1560
|
|
|
1064
|
-
ipcMain.handle('add-recent-workspace', async (event,
|
|
1065
|
-
return await addRecentWorkspace(
|
|
1066
|
-
});
|
|
1067
|
-
|
|
1068
|
-
ipcMain.handle('remove-recent-workspace', async (event, workspacePath) => {
|
|
1069
|
-
return await removeRecentWorkspace(workspacePath);
|
|
1561
|
+
ipcMain.handle('add-recent-workspace', async (event, wsPath) => {
|
|
1562
|
+
return await addRecentWorkspace(wsPath);
|
|
1070
1563
|
});
|
|
1071
1564
|
|
|
1072
|
-
ipcMain.handle('
|
|
1073
|
-
return await
|
|
1074
|
-
});
|
|
1075
|
-
|
|
1076
|
-
ipcMain.handle('validate-workspace-path', async (event, workspacePath) => {
|
|
1077
|
-
return await validateWorkspacePath(workspacePath);
|
|
1565
|
+
ipcMain.handle('remove-recent-workspace', async (event, wsPath) => {
|
|
1566
|
+
return await removeRecentWorkspace(wsPath);
|
|
1078
1567
|
});
|
|
1079
1568
|
|
|
1080
1569
|
ipcMain.handle('get-current-directory', async () => {
|
|
1081
|
-
|
|
1570
|
+
const cwd = process.cwd();
|
|
1571
|
+
const appDir = __dirname;
|
|
1572
|
+
return {
|
|
1573
|
+
path: cwd,
|
|
1574
|
+
isUsable: cwd !== appDir && fsSync.existsSync(cwd),
|
|
1575
|
+
appDir
|
|
1576
|
+
};
|
|
1082
1577
|
});
|
|
1083
1578
|
|
|
1084
1579
|
ipcMain.handle('browse-for-workspace', async () => {
|
|
@@ -1091,10 +1586,7 @@ ipcMain.handle('browse-for-workspace', async () => {
|
|
|
1091
1586
|
return { canceled: true };
|
|
1092
1587
|
}
|
|
1093
1588
|
|
|
1094
|
-
return {
|
|
1095
|
-
canceled: false,
|
|
1096
|
-
path: result.filePaths[0]
|
|
1097
|
-
};
|
|
1589
|
+
return { canceled: false, path: result.filePaths[0] };
|
|
1098
1590
|
});
|
|
1099
1591
|
|
|
1100
1592
|
ipcMain.handle('open-config-folder', async () => {
|
|
@@ -1122,7 +1614,10 @@ ipcMain.on('pty-input', (event, { agentName, data }) => {
|
|
|
1122
1614
|
}
|
|
1123
1615
|
});
|
|
1124
1616
|
|
|
1617
|
+
// ═══════════════════════════════════════════════════════════
|
|
1125
1618
|
// App lifecycle
|
|
1619
|
+
// ═══════════════════════════════════════════════════════════
|
|
1620
|
+
|
|
1126
1621
|
app.whenReady().then(async () => {
|
|
1127
1622
|
console.log('App ready, setting up...');
|
|
1128
1623
|
parseCommandLineArgs();
|
|
@@ -1143,7 +1638,6 @@ app.on('activate', () => {
|
|
|
1143
1638
|
}
|
|
1144
1639
|
});
|
|
1145
1640
|
|
|
1146
|
-
// Cleanup on quit
|
|
1147
1641
|
app.on('before-quit', () => {
|
|
1148
1642
|
stopAllAgents();
|
|
1149
1643
|
});
|