@appoly/multiagent-chat 1.0.3 → 1.0.7

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