@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/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 agentCwd; // Parent directory of workspace - where agents are launched
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
- // Usage: npm start /path/to/workspace
35
- // Or: npm start --workspace /path/to/workspace
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++; // Skip next arg
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++; // Skip next arg
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
- // Ensure home config directory exists and set up first-run defaults
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
- // Load recent workspaces from JSON file
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
- // Add or update a workspace in recents
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
- // Remove existing entry with same path (case-insensitive on Windows)
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
- // Remove a workspace from recents
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
- // Update path of a workspace in recents (for "Locate" functionality)
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(workspacePath);
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
- // Get current working directory info
202
- function getCurrentDirectoryInfo() {
203
- const cwd = process.cwd();
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
- mainWindow = new BrowserWindow({
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
- // Load configuration with priority: CLI arg > home config > bundled default
249
- async function loadConfig(configPath = null) {
250
- try {
251
- let fullPath;
252
-
253
- // Priority 1: CLI argument (--config flag or CONFIG env var)
254
- if (configPath) {
255
- fullPath = path.isAbsolute(configPath) ? configPath : path.join(process.cwd(), configPath);
256
- console.log('Loading config from CLI arg:', fullPath);
257
- }
258
- // Priority 2: Home directory config (~/.multiagent-chat/config.yaml)
259
- else if (fsSync.existsSync(HOME_CONFIG_FILE)) {
260
- fullPath = HOME_CONFIG_FILE;
261
- console.log('Loading config from home dir:', fullPath);
262
- }
263
- // Priority 3: Bundled default (fallback)
264
- else {
265
- fullPath = path.join(__dirname, 'config.yaml');
266
- console.log('Loading bundled config from:', fullPath);
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
- // Setup workspace directory and files
280
- // customPath = project root selected by user (or from CLI)
281
- async function setupWorkspace(customPath = null) {
282
- // Determine project root (agentCwd) and workspace path (.multiagent-chat inside it)
283
- let projectRoot;
284
-
285
- if (customPath && path.isAbsolute(customPath)) {
286
- projectRoot = customPath;
287
- } else if (customPath) {
288
- projectRoot = path.join(process.cwd(), customPath);
289
- } else {
290
- // Fallback: use current directory as project root
291
- projectRoot = process.cwd();
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
- // Set agent working directory to the project root
295
- agentCwd = projectRoot;
296
-
297
- // Set workspace path to .multiagent-chat inside the project root
298
- const workspaceName = config.workspace || '.multiagent-chat';
299
- workspacePath = path.join(projectRoot, workspaceName);
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
- await fs.mkdir(workspacePath, { recursive: true });
303
-
304
- // Initialize chat.jsonl (empty file - JSONL format)
305
- const chatPath = path.join(workspacePath, config.chat_file || 'chat.jsonl');
306
- await fs.writeFile(chatPath, '');
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
- // Clear PLAN_FINAL.md if it exists
309
- const planPath = path.join(workspacePath, config.plan_file || 'PLAN_FINAL.md');
310
- await fs.writeFile(planPath, '');
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
- // Build agent colors map from config
317
- const defaultColors = config.default_agent_colors || ['#667eea', '#f093fb', '#4fd1c5', '#f6ad55', '#68d391', '#fc8181'];
318
- agentColors = {};
319
- config.agents.forEach((agentConfig, index) => {
320
- agentColors[agentConfig.name.toLowerCase()] = agentConfig.color || defaultColors[index % defaultColors.length];
321
- });
322
- // Add user color
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
- // Reset message sequence
332
- messageSequence = 0;
333
-
334
- // Capture git base commit for diff baseline
335
- try {
336
- const { stdout } = await execAsync('git rev-parse HEAD', { cwd: agentCwd });
337
- sessionBaseCommit = stdout.trim();
338
- console.log('Session base commit:', sessionBaseCommit);
339
- } catch (error) {
340
- // Check if it's a git repo with no commits yet
341
- try {
342
- await execAsync('git rev-parse --git-dir', { cwd: agentCwd });
343
- // It's a git repo but no commits - use empty tree hash
344
- sessionBaseCommit = '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
345
- console.log('Git repo with no commits, using empty tree hash for diff baseline');
346
- } catch (e) {
347
- console.log('Not a git repository:', e.message);
348
- sessionBaseCommit = null;
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
- console.log('Workspace setup complete:', workspacePath);
353
- console.log('Agent working directory:', agentCwd);
354
- console.log('Outbox directory created:', outboxDir);
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 setting up workspace:', 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'); // Report cursor at position 1,1
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 (mainWindow) {
505
- mainWindow.webContents.send('agent-status', {
506
- agentName: this.name,
507
- status: 'stopped',
508
- exitCode: exitCode
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
- // Send Enter key to submit for PTY
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
- // Send a message to all agents EXCEPT the sender
556
- function sendMessageToOtherAgents(senderName, message) {
557
- const workspaceFolder = path.basename(workspacePath);
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
- // Path relative to agentCwd (includes workspace folder)
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
- // Path relative to agentCwd (includes workspace folder)
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 workspaceFolder = path.basename(workspacePath);
941
+ const relFromProject = path.relative(agentCwd, workspacePath);
590
942
  const outboxDir = config.outbox_dir || 'outbox';
591
- // Path relative to agentCwd (includes workspace folder)
592
- const outboxFile = `${workspaceFolder}/${outboxDir}/${agentName.toLowerCase()}.md`;
593
- const planFile = `${workspaceFolder}/${config.plan_file || 'PLAN_FINAL.md'}`;
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 config.prompt_template
948
+ return template
596
949
  .replace('{challenge}', challenge)
597
950
  .replace('{workspace}', workspacePath)
598
- .replace(/{outbox_file}/g, outboxFile) // Replace all occurrences
599
- .replace(/{plan_file}/g, planFile) // Replace all occurrences
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
- // Watch chat.jsonl for changes (backup - real-time updates via chat-message event)
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, // Wait for file to be stable for 500ms
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
- // Notify renderer with the new message
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
- // Append user message to chat.jsonl and push to all agents
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
- // Notify renderer with the new message
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
- return JSON.parse(line);
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 or other error
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
- ipcMain.handle('start-session', async (event, { challenge, workspace: selectedWorkspace }) => {
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
- // Use selected workspace if provided, otherwise fall back to customWorkspacePath
918
- const workspaceToUse = selectedWorkspace || customWorkspacePath;
919
- await setupWorkspace(workspaceToUse);
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
- // Add workspace to recents (agentCwd is the project root)
926
- if (agentCwd) {
927
- await addRecentWorkspace(agentCwd);
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
- // Check if this session was started with CLI workspace (and user didn't change it)
931
- const isFromCli = customWorkspacePath && (
932
- workspaceToUse === customWorkspacePath ||
933
- agentCwd === customWorkspacePath ||
934
- agentCwd === path.resolve(customWorkspacePath)
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, // Show the project root, not the internal .multiagent-chat folder
1413
+ workspace: agentCwd,
941
1414
  colors: agentColors,
942
- fromCli: isFromCli
1415
+ sessionId: sessionId
943
1416
  };
944
1417
  } catch (error) {
945
- console.error('Error starting session:', 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: false,
948
- error: error.message
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
- // Clear chat file (handle missing file gracefully)
985
- if (workspacePath) {
986
- const chatPath = path.join(workspacePath, config.chat_file || 'chat.jsonl');
987
- try {
988
- await fs.writeFile(chatPath, '');
989
- } catch (e) {
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, workspacePath) => {
1065
- return await addRecentWorkspace(workspacePath);
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('update-recent-workspace-path', async (event, oldPath, newPath) => {
1073
- return await updateRecentWorkspacePath(oldPath, newPath);
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
- return getCurrentDirectoryInfo();
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
  });