@appoly/multiagent-chat 1.0.3 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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,348 @@ 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
+ // ═══════════════════════════════════════════════════════════
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
+ }
380
+
381
+ console.log('Workspace base initialized:', workspaceBasePath);
382
+ console.log('Agent working directory:', agentCwd);
383
+ }
384
+
385
+ // ═══════════════════════════════════════════════════════════
47
386
  // 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
387
+ // ═══════════════════════════════════════════════════════════
388
+
52
389
  function parseCommandLineArgs() {
53
- // Check environment variables first
54
390
  if (process.env.WORKSPACE) {
55
391
  customWorkspacePath = process.env.WORKSPACE;
56
392
  console.log('Using workspace from environment variable:', customWorkspacePath);
@@ -61,48 +397,40 @@ function parseCommandLineArgs() {
61
397
  console.log('Using config from environment variable:', customConfigPath);
62
398
  }
63
399
 
64
- // Then check command-line arguments
65
- // process.argv looks like: [electron, main.js, ...args]
66
400
  const args = process.argv.slice(2);
67
401
 
68
402
  for (let i = 0; i < args.length; i++) {
69
- // Parse --workspace flag
70
403
  if (args[i] === '--workspace' && args[i + 1]) {
71
404
  customWorkspacePath = args[i + 1];
72
405
  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]) {
406
+ i++;
407
+ } else if (args[i] === '--config' && args[i + 1]) {
77
408
  customConfigPath = args[i + 1];
78
409
  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) {
410
+ i++;
411
+ } else if (!args[i].startsWith('--') && !customWorkspacePath) {
83
412
  customWorkspacePath = args[i];
84
413
  console.log('Using workspace from positional argument:', customWorkspacePath);
85
414
  }
86
415
  }
87
416
  }
88
417
 
89
- // Ensure home config directory exists and set up first-run defaults
418
+ // ═══════════════════════════════════════════════════════════
419
+ // Home config directory setup
420
+ // ═══════════════════════════════════════════════════════════
421
+
90
422
  async function ensureHomeConfigDir() {
91
423
  try {
92
- // Create ~/.multiagent-chat/ if it doesn't exist
93
424
  await fs.mkdir(HOME_CONFIG_DIR, { recursive: true });
94
425
  console.log('Home config directory ensured:', HOME_CONFIG_DIR);
95
426
 
96
- // Migration: check for existing data in Electron userData
97
427
  const userDataDir = app.getPath('userData');
98
428
  const oldRecentsFile = path.join(userDataDir, 'recent-workspaces.json');
99
429
 
100
- // Check if config.yaml exists in home dir, if not copy default
101
430
  try {
102
431
  await fs.access(HOME_CONFIG_FILE);
103
432
  console.log('Home config exists:', HOME_CONFIG_FILE);
104
433
  } catch (e) {
105
- // Copy bundled default config to home dir
106
434
  const bundledConfig = path.join(__dirname, 'config.yaml');
107
435
  try {
108
436
  await fs.copyFile(bundledConfig, HOME_CONFIG_FILE);
@@ -112,17 +440,14 @@ async function ensureHomeConfigDir() {
112
440
  }
113
441
  }
114
442
 
115
- // Initialize or migrate recent-workspaces.json
116
443
  try {
117
444
  await fs.access(RECENT_WORKSPACES_FILE);
118
445
  } catch (e) {
119
- // Try to migrate from old location first
120
446
  try {
121
447
  await fs.access(oldRecentsFile);
122
448
  await fs.copyFile(oldRecentsFile, RECENT_WORKSPACES_FILE);
123
449
  console.log('Migrated recent workspaces from:', oldRecentsFile);
124
450
  } catch (migrateError) {
125
- // No old file, create new empty one
126
451
  await fs.writeFile(RECENT_WORKSPACES_FILE, JSON.stringify({ recents: [] }, null, 2));
127
452
  console.log('Initialized recent workspaces file:', RECENT_WORKSPACES_FILE);
128
453
  }
@@ -132,7 +457,10 @@ async function ensureHomeConfigDir() {
132
457
  }
133
458
  }
134
459
 
135
- // Load recent workspaces from JSON file
460
+ // ═══════════════════════════════════════════════════════════
461
+ // Recent workspaces (kept for backward compat / sidebar)
462
+ // ═══════════════════════════════════════════════════════════
463
+
136
464
  async function loadRecentWorkspaces() {
137
465
  try {
138
466
  const content = await fs.readFile(RECENT_WORKSPACES_FILE, 'utf8');
@@ -144,7 +472,6 @@ async function loadRecentWorkspaces() {
144
472
  }
145
473
  }
146
474
 
147
- // Save recent workspaces to JSON file
148
475
  async function saveRecentWorkspaces(recents) {
149
476
  try {
150
477
  await fs.writeFile(RECENT_WORKSPACES_FILE, JSON.stringify({ recents }, null, 2));
@@ -153,98 +480,65 @@ async function saveRecentWorkspaces(recents) {
153
480
  }
154
481
  }
155
482
 
156
- // Add or update a workspace in recents
157
- async function addRecentWorkspace(workspacePath) {
483
+ async function addRecentWorkspace(wsPath) {
158
484
  const recents = await loadRecentWorkspaces();
159
485
  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
486
+ const filtered = recents.filter(r => r.path.toLowerCase() !== wsPath.toLowerCase());
487
+ filtered.unshift({ path: wsPath, lastUsed: now });
173
488
  const limited = filtered.slice(0, MAX_RECENT_WORKSPACES);
174
-
175
489
  await saveRecentWorkspaces(limited);
176
490
  return limited;
177
491
  }
178
492
 
179
- // Remove a workspace from recents
180
- async function removeRecentWorkspace(workspacePath) {
493
+ async function removeRecentWorkspace(wsPath) {
181
494
  const recents = await loadRecentWorkspaces();
182
- const filtered = recents.filter(r =>
183
- r.path.toLowerCase() !== workspacePath.toLowerCase()
184
- );
495
+ const filtered = recents.filter(r => r.path.toLowerCase() !== wsPath.toLowerCase());
185
496
  await saveRecentWorkspaces(filtered);
186
497
  return filtered;
187
498
  }
188
499
 
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) {
500
+ async function validateWorkspacePath(wsPath) {
207
501
  try {
208
- const stats = await fs.stat(workspacePath);
502
+ const stats = await fs.stat(wsPath);
209
503
  return stats.isDirectory();
210
504
  } catch (error) {
211
505
  return false;
212
506
  }
213
507
  }
214
508
 
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);
222
-
223
- return {
224
- path: cwd,
225
- isUsable,
226
- appDir
227
- };
228
- }
509
+ // ═══════════════════════════════════════════════════════════
510
+ // Browser window
511
+ // ═══════════════════════════════════════════════════════════
229
512
 
230
- // Create the browser window
231
513
  function createWindow() {
232
514
  console.log('Creating window...');
233
515
 
234
516
  const iconPath = path.join(__dirname, 'robot.png');
235
517
 
236
- mainWindow = new BrowserWindow({
518
+ const windowOptions = {
237
519
  width: 1400,
238
520
  height: 900,
239
521
  icon: iconPath,
522
+ show: false,
523
+ backgroundColor: '#0b0e11',
240
524
  webPreferences: {
241
525
  preload: path.join(__dirname, 'preload.js'),
242
526
  contextIsolation: true,
243
527
  nodeIntegration: false
244
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();
245
540
  });
246
541
 
247
- // Set dock icon on macOS
248
542
  if (process.platform === 'darwin' && app.dock) {
249
543
  app.dock.setIcon(iconPath);
250
544
  }
@@ -259,122 +553,143 @@ function createWindow() {
259
553
  console.log('Window setup complete');
260
554
  }
261
555
 
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);
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];
281
578
  }
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
579
  }
580
+ return result;
291
581
  }
292
582
 
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();
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
+ }
306
602
  }
603
+ return missing;
604
+ }
307
605
 
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);
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
+ }
314
623
 
624
+ async function loadConfig(configPath = null) {
315
625
  try {
316
- await fs.mkdir(workspacePath, { recursive: true });
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);
317
631
 
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 });
632
+ // Determine the user config path
633
+ let userConfigPath = null;
634
+ let isHomeConfig = false;
329
635
 
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';
338
-
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, '');
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);
343
643
  }
344
644
 
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;
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
+ }
363
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;
364
678
  }
365
679
 
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;
680
+ validateConfig(config);
681
+ console.log('Config loaded and validated successfully');
682
+ return config;
371
683
  } catch (error) {
372
- console.error('Error setting up workspace:', error);
684
+ console.error('Error loading config:', error);
373
685
  throw error;
374
686
  }
375
687
  }
376
688
 
689
+ // ═══════════════════════════════════════════════════════════
377
690
  // Agent Process Management
691
+ // ═══════════════════════════════════════════════════════════
692
+
378
693
  class AgentProcess {
379
694
  constructor(agentConfig, index) {
380
695
  this.name = agentConfig.name;
@@ -384,14 +699,21 @@ class AgentProcess {
384
699
  this.index = index;
385
700
  this.process = null;
386
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);
387
707
  }
388
708
 
389
709
  async start(prompt) {
710
+ this.lastPrompt = prompt;
711
+ this.intentionalStop = false;
712
+
390
713
  return new Promise((resolve, reject) => {
391
714
  console.log(`Starting agent ${this.name} with PTY: ${this.use_pty}`);
392
715
 
393
716
  if (this.use_pty) {
394
- // Use PTY for interactive TUI agents
395
717
  const shell = process.env.SHELL || '/bin/bash';
396
718
 
397
719
  this.process = pty.spawn(this.command, this.args, {
@@ -414,17 +736,13 @@ class AgentProcess {
414
736
 
415
737
  console.log(`PTY spawned for ${this.name}, PID: ${this.process.pid}`);
416
738
 
417
- // Respond to cursor position query immediately
418
- // This helps with terminal capability detection (needed for Codex)
419
739
  setTimeout(() => {
420
- this.process.write('\x1b[1;1R'); // Report cursor at position 1,1
740
+ this.process.write('\x1b[1;1R');
421
741
  }, 100);
422
742
 
423
- // Capture all output from PTY
424
743
  this.process.onData((data) => {
425
744
  const output = data.toString();
426
745
  this.outputBuffer.push(output);
427
-
428
746
  if (mainWindow) {
429
747
  mainWindow.webContents.send('agent-output', {
430
748
  agentName: this.name,
@@ -434,75 +752,50 @@ class AgentProcess {
434
752
  }
435
753
  });
436
754
 
437
- // Handle exit - trigger resume if enabled
438
755
  this.process.onExit(({ exitCode, signal }) => {
439
756
  console.log(`Agent ${this.name} exited with code ${exitCode}, signal ${signal}`);
440
757
  this.handleExit(exitCode);
441
758
  });
442
759
 
443
- // Inject prompt via PTY after TUI initializes (original working pattern)
444
- const initDelay = this.name === 'Codex' ? 5000 : 3000;
445
760
  setTimeout(() => {
446
761
  console.log(`Injecting prompt into ${this.name} PTY`);
447
762
  this.process.write(prompt + '\n');
448
-
449
- // Send Enter key after a brief delay to submit
450
763
  setTimeout(() => {
451
764
  this.process.write('\r');
452
765
  }, 500);
453
-
454
766
  resolve();
455
- }, initDelay);
767
+ }, this.initDelay);
456
768
 
457
769
  } else {
458
- // Use regular spawn for non-interactive agents
459
770
  const options = {
460
771
  cwd: agentCwd,
461
- env: {
462
- ...process.env,
463
- AGENT_NAME: this.name
464
- }
772
+ env: { ...process.env, AGENT_NAME: this.name }
465
773
  };
466
774
 
467
775
  this.process = spawn(this.command, this.args, options);
468
-
469
776
  console.log(`Process spawned for ${this.name}, PID: ${this.process.pid}`);
470
777
 
471
- // Capture stdout
472
778
  this.process.stdout.on('data', (data) => {
473
779
  const output = data.toString();
474
780
  this.outputBuffer.push(output);
475
-
476
781
  if (mainWindow) {
477
- mainWindow.webContents.send('agent-output', {
478
- agentName: this.name,
479
- output: output,
480
- isPty: false
481
- });
782
+ mainWindow.webContents.send('agent-output', { agentName: this.name, output, isPty: false });
482
783
  }
483
784
  });
484
785
 
485
- // Capture stderr
486
786
  this.process.stderr.on('data', (data) => {
487
787
  const output = data.toString();
488
788
  this.outputBuffer.push(`[stderr] ${output}`);
489
-
490
789
  if (mainWindow) {
491
- mainWindow.webContents.send('agent-output', {
492
- agentName: this.name,
493
- output: `[stderr] ${output}`,
494
- isPty: false
495
- });
790
+ mainWindow.webContents.send('agent-output', { agentName: this.name, output: `[stderr] ${output}`, isPty: false });
496
791
  }
497
792
  });
498
793
 
499
- // Handle process exit - trigger resume if enabled
500
794
  this.process.on('close', (code) => {
501
795
  console.log(`Agent ${this.name} exited with code ${code}`);
502
796
  this.handleExit(code);
503
797
  });
504
798
 
505
- // Handle errors
506
799
  this.process.on('error', (error) => {
507
800
  console.error(`Error starting agent ${this.name}:`, error);
508
801
  reject(error);
@@ -513,14 +806,65 @@ class AgentProcess {
513
806
  });
514
807
  }
515
808
 
516
- // Handle agent exit
517
809
  handleExit(exitCode) {
518
- if (mainWindow) {
519
- mainWindow.webContents.send('agent-status', {
520
- agentName: this.name,
521
- status: 'stopped',
522
- exitCode: exitCode
523
- });
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
+ }
524
868
  }
525
869
  }
526
870
 
@@ -528,10 +872,7 @@ class AgentProcess {
528
872
  if (this.use_pty) {
529
873
  if (this.process && this.process.write) {
530
874
  this.process.write(message + '\n');
531
- // Send Enter key to submit for PTY
532
- setTimeout(() => {
533
- this.process.write('\r');
534
- }, 300);
875
+ setTimeout(() => { this.process.write('\r'); }, 300);
535
876
  }
536
877
  } else {
537
878
  if (this.process && this.process.stdin) {
@@ -541,6 +882,7 @@ class AgentProcess {
541
882
  }
542
883
 
543
884
  stop() {
885
+ this.intentionalStop = true;
544
886
  if (this.process) {
545
887
  if (this.use_pty) {
546
888
  this.process.kill();
@@ -551,71 +893,67 @@ class AgentProcess {
551
893
  }
552
894
  }
553
895
 
554
- // Initialize agents from config
555
896
  function initializeAgents() {
556
897
  agents = config.agents.map((agentConfig, index) => {
557
898
  return new AgentProcess(agentConfig, index);
558
899
  });
559
-
560
900
  console.log(`Initialized ${agents.length} agents`);
561
901
  return agents;
562
902
  }
563
903
 
564
- // Get agent by name
565
904
  function getAgentByName(name) {
566
905
  return agents.find(a => a.name.toLowerCase() === name.toLowerCase());
567
906
  }
568
907
 
569
- // Send a message to all agents EXCEPT the sender
570
- function sendMessageToOtherAgents(senderName, message) {
571
- 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);
572
916
  const outboxDir = config.outbox_dir || 'outbox';
917
+ return `${relFromProject}/${outboxDir}/${agentName.toLowerCase()}.md`;
918
+ }
573
919
 
920
+ function sendMessageToOtherAgents(senderName, message) {
574
921
  for (const agent of agents) {
575
922
  if (agent.name.toLowerCase() !== senderName.toLowerCase()) {
576
- // Path relative to agentCwd (includes workspace folder)
577
- const outboxFile = `${workspaceFolder}/${outboxDir}/${agent.name.toLowerCase()}.md`;
923
+ const outboxFile = getOutboxRelativePath(agent.name);
578
924
  const formattedMessage = `\n---\n📨 MESSAGE FROM ${senderName.toUpperCase()}:\n\n${message}\n\n---\n(Respond via: cat << 'EOF' > ${outboxFile})\n`;
579
-
580
925
  console.log(`Delivering message from ${senderName} to ${agent.name}`);
581
926
  agent.sendMessage(formattedMessage);
582
927
  }
583
928
  }
584
929
  }
585
930
 
586
- // Send a message to ALL agents (for user messages)
587
931
  function sendMessageToAllAgents(message) {
588
- const workspaceFolder = path.basename(workspacePath);
589
- const outboxDir = config.outbox_dir || 'outbox';
590
-
591
932
  for (const agent of agents) {
592
- // Path relative to agentCwd (includes workspace folder)
593
- const outboxFile = `${workspaceFolder}/${outboxDir}/${agent.name.toLowerCase()}.md`;
933
+ const outboxFile = getOutboxRelativePath(agent.name);
594
934
  const formattedMessage = `\n---\n📨 MESSAGE FROM USER:\n\n${message}\n\n---\n(Respond via: cat << 'EOF' > ${outboxFile})\n`;
595
-
596
935
  console.log(`Delivering user message to ${agent.name}`);
597
936
  agent.sendMessage(formattedMessage);
598
937
  }
599
938
  }
600
939
 
601
- // Build prompt for a specific agent
602
940
  function buildAgentPrompt(challenge, agentName) {
603
- const workspaceFolder = path.basename(workspacePath);
941
+ const relFromProject = path.relative(agentCwd, workspacePath);
604
942
  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'}`;
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`;
608
947
 
609
- return config.prompt_template
948
+ return template
610
949
  .replace('{challenge}', challenge)
611
950
  .replace('{workspace}', workspacePath)
612
- .replace(/{outbox_file}/g, outboxFile) // Replace all occurrences
613
- .replace(/{plan_file}/g, planFile) // Replace all occurrences
951
+ .replace(/{outbox_file}/g, outboxFile)
952
+ .replace(/{plan_file}/g, planFile)
614
953
  .replace('{agent_names}', agents.map(a => a.name).join(', '))
615
954
  .replace('{agent_name}', agentName);
616
955
  }
617
956
 
618
- // Start all agents with their individual prompts
619
957
  async function startAgents(challenge) {
620
958
  console.log('Starting agents with prompts...');
621
959
 
@@ -633,7 +971,6 @@ async function startAgents(challenge) {
633
971
  }
634
972
  } catch (error) {
635
973
  console.error(`Failed to start agent ${agent.name}:`, error);
636
-
637
974
  if (mainWindow) {
638
975
  mainWindow.webContents.send('agent-status', {
639
976
  agentName: agent.name,
@@ -645,7 +982,10 @@ async function startAgents(challenge) {
645
982
  }
646
983
  }
647
984
 
648
- // Watch chat.jsonl for changes (backup - real-time updates via chat-message event)
985
+ // ═══════════════════════════════════════════════════════════
986
+ // File watchers
987
+ // ═══════════════════════════════════════════════════════════
988
+
649
989
  function startFileWatcher() {
650
990
  const chatPath = path.join(workspacePath, config.chat_file || 'chat.jsonl');
651
991
 
@@ -654,8 +994,6 @@ function startFileWatcher() {
654
994
  ignoreInitial: true
655
995
  });
656
996
 
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
997
  fileWatcher.on('change', async () => {
660
998
  try {
661
999
  const messages = await getChatContent();
@@ -670,47 +1008,38 @@ function startFileWatcher() {
670
1008
  console.log('File watcher started for:', chatPath);
671
1009
  }
672
1010
 
673
- // Watch outbox directory and merge messages into chat.jsonl
674
1011
  function startOutboxWatcher() {
675
1012
  const outboxDir = path.join(workspacePath, config.outbox_dir || 'outbox');
676
1013
  const chatPath = path.join(workspacePath, config.chat_file || 'chat.jsonl');
677
1014
 
678
- // Track which files we're currently processing to avoid race conditions
679
1015
  const processing = new Set();
680
1016
 
681
1017
  outboxWatcher = chokidar.watch(outboxDir, {
682
1018
  persistent: true,
683
1019
  ignoreInitial: true,
684
1020
  awaitWriteFinish: {
685
- stabilityThreshold: 500, // Wait for file to be stable for 500ms
1021
+ stabilityThreshold: 500,
686
1022
  pollInterval: 100
687
1023
  }
688
1024
  });
689
1025
 
690
1026
  outboxWatcher.on('change', async (filePath) => {
691
- // Only process .md files
692
1027
  if (!filePath.endsWith('.md')) return;
693
-
694
- // Avoid processing the same file concurrently
695
1028
  if (processing.has(filePath)) return;
696
1029
  processing.add(filePath);
697
1030
 
698
1031
  try {
699
- // Read the outbox file
700
1032
  const content = await fs.readFile(filePath, 'utf8');
701
1033
  const trimmedContent = content.trim();
702
1034
 
703
- // Skip if empty
704
1035
  if (!trimmedContent) {
705
1036
  processing.delete(filePath);
706
1037
  return;
707
1038
  }
708
1039
 
709
- // Extract agent name from filename (e.g., "claude.md" -> "Claude")
710
1040
  const filename = path.basename(filePath, '.md');
711
1041
  const agentName = filename.charAt(0).toUpperCase() + filename.slice(1);
712
1042
 
713
- // Increment sequence and create message object
714
1043
  messageSequence++;
715
1044
  const timestamp = new Date().toISOString();
716
1045
  const message = {
@@ -722,21 +1051,24 @@ function startOutboxWatcher() {
722
1051
  color: agentColors[agentName.toLowerCase()] || '#667eea'
723
1052
  };
724
1053
 
725
- // Append to chat.jsonl
726
1054
  await fs.appendFile(chatPath, JSON.stringify(message) + '\n');
727
1055
  console.log(`Merged message from ${agentName} (#${messageSequence}) into chat.jsonl`);
728
1056
 
729
- // Clear the outbox file
730
1057
  await fs.writeFile(filePath, '');
731
1058
 
732
- // PUSH message to other agents' PTYs
733
1059
  sendMessageToOtherAgents(agentName, trimmedContent);
734
1060
 
735
- // 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
+
736
1069
  if (mainWindow) {
737
1070
  mainWindow.webContents.send('chat-message', message);
738
1071
  }
739
-
740
1072
  } catch (error) {
741
1073
  console.error(`Error processing outbox file ${filePath}:`, error);
742
1074
  } finally {
@@ -747,7 +1079,6 @@ function startOutboxWatcher() {
747
1079
  console.log('Outbox watcher started for:', outboxDir);
748
1080
  }
749
1081
 
750
- // Stop outbox watcher
751
1082
  function stopOutboxWatcher() {
752
1083
  if (outboxWatcher) {
753
1084
  outboxWatcher.close();
@@ -755,7 +1086,10 @@ function stopOutboxWatcher() {
755
1086
  }
756
1087
  }
757
1088
 
758
- // Append user message to chat.jsonl and push to all agents
1089
+ // ═══════════════════════════════════════════════════════════
1090
+ // Chat / Plan / Diff
1091
+ // ═══════════════════════════════════════════════════════════
1092
+
759
1093
  async function sendUserMessage(messageText) {
760
1094
  const chatPath = path.join(workspacePath, config.chat_file || 'chat.jsonl');
761
1095
  messageSequence++;
@@ -771,39 +1105,37 @@ async function sendUserMessage(messageText) {
771
1105
  };
772
1106
 
773
1107
  try {
774
- // Append to chat.jsonl
775
1108
  await fs.appendFile(chatPath, JSON.stringify(message) + '\n');
776
1109
  console.log(`User message #${messageSequence} appended to chat`);
777
1110
 
778
- // PUSH message to all agents' PTYs
779
1111
  sendMessageToAllAgents(messageText);
780
1112
 
781
- // 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
+
782
1121
  if (mainWindow) {
783
1122
  mainWindow.webContents.send('chat-message', message);
784
1123
  }
785
-
786
1124
  } catch (error) {
787
1125
  console.error('Error appending user message:', error);
788
1126
  throw error;
789
1127
  }
790
1128
  }
791
1129
 
792
- // Read current chat content (returns array of message objects)
793
1130
  async function getChatContent() {
794
1131
  const chatPath = path.join(workspacePath, config.chat_file || 'chat.jsonl');
795
1132
  try {
796
1133
  const content = await fs.readFile(chatPath, 'utf8');
797
1134
  if (!content.trim()) return [];
798
1135
 
799
- // Parse JSONL (one JSON object per line)
800
1136
  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
- }
1137
+ try { return JSON.parse(line); }
1138
+ catch (e) { console.error('Failed to parse chat line:', line); return null; }
807
1139
  }).filter(Boolean);
808
1140
 
809
1141
  return messages;
@@ -813,7 +1145,6 @@ async function getChatContent() {
813
1145
  }
814
1146
  }
815
1147
 
816
- // Read final plan
817
1148
  async function getPlanContent() {
818
1149
  const planPath = path.join(workspacePath, config.plan_file || 'PLAN_FINAL.md');
819
1150
  try {
@@ -823,14 +1154,11 @@ async function getPlanContent() {
823
1154
  }
824
1155
  }
825
1156
 
826
- // Get git diff - shows uncommitted changes only (git diff HEAD)
827
1157
  async function getGitDiff() {
828
- // Not a git repo or session hasn't started
829
1158
  if (!agentCwd) {
830
1159
  return { isGitRepo: false, error: 'No session active' };
831
1160
  }
832
1161
 
833
- // Check if git repo
834
1162
  try {
835
1163
  await execAsync('git rev-parse --git-dir', { cwd: agentCwd });
836
1164
  } catch (error) {
@@ -845,7 +1173,6 @@ async function getGitDiff() {
845
1173
  untracked: []
846
1174
  };
847
1175
 
848
- // Check if HEAD exists (repo might have no commits)
849
1176
  let hasHead = true;
850
1177
  try {
851
1178
  await execAsync('git rev-parse HEAD', { cwd: agentCwd });
@@ -853,17 +1180,13 @@ async function getGitDiff() {
853
1180
  hasHead = false;
854
1181
  }
855
1182
 
856
- // Determine diff target - use empty tree hash if no commits yet
857
1183
  const diffTarget = hasHead ? 'HEAD' : '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
858
1184
 
859
- // Get diff stats
860
1185
  try {
861
1186
  const { stdout: statOutput } = await execAsync(
862
1187
  `git diff ${diffTarget} --stat`,
863
1188
  { cwd: agentCwd, maxBuffer: 10 * 1024 * 1024 }
864
1189
  );
865
-
866
- // Parse stats from last line (e.g., "3 files changed, 10 insertions(+), 5 deletions(-)")
867
1190
  const statMatch = statOutput.match(/(\d+) files? changed(?:, (\d+) insertions?\(\+\))?(?:, (\d+) deletions?\(-\))?/);
868
1191
  if (statMatch) {
869
1192
  result.stats.filesChanged = parseInt(statMatch[1]) || 0;
@@ -871,10 +1194,9 @@ async function getGitDiff() {
871
1194
  result.stats.deletions = parseInt(statMatch[3]) || 0;
872
1195
  }
873
1196
  } catch (e) {
874
- // No changes or other error
1197
+ // No changes
875
1198
  }
876
1199
 
877
- // Get full diff
878
1200
  try {
879
1201
  const { stdout: diffOutput } = await execAsync(
880
1202
  `git diff ${diffTarget}`,
@@ -885,7 +1207,6 @@ async function getGitDiff() {
885
1207
  result.diff = '';
886
1208
  }
887
1209
 
888
- // Get untracked files
889
1210
  try {
890
1211
  const { stdout: untrackedOutput } = await execAsync(
891
1212
  'git ls-files --others --exclude-standard',
@@ -903,7 +1224,10 @@ async function getGitDiff() {
903
1224
  }
904
1225
  }
905
1226
 
1227
+ // ═══════════════════════════════════════════════════════════
906
1228
  // Stop all agents and watchers
1229
+ // ═══════════════════════════════════════════════════════════
1230
+
907
1231
  function stopAllAgents() {
908
1232
  agents.forEach(agent => agent.stop());
909
1233
  if (fileWatcher) {
@@ -913,7 +1237,10 @@ function stopAllAgents() {
913
1237
  stopOutboxWatcher();
914
1238
  }
915
1239
 
1240
+ // ═══════════════════════════════════════════════════════════
916
1241
  // IPC Handlers
1242
+ // ═══════════════════════════════════════════════════════════
1243
+
917
1244
  ipcMain.handle('load-config', async () => {
918
1245
  try {
919
1246
  console.log('IPC: load-config called');
@@ -926,41 +1253,224 @@ ipcMain.handle('load-config', async () => {
926
1253
  }
927
1254
  });
928
1255
 
929
- 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 }) => {
930
1300
  try {
931
- // Use selected workspace if provided, otherwise fall back to customWorkspacePath
932
- const workspaceToUse = selectedWorkspace || customWorkspacePath;
933
- 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
934
1364
  initializeAgents();
935
- await startAgents(challenge);
936
- startFileWatcher();
937
- startOutboxWatcher(); // Watch for agent messages and merge into chat.jsonl
938
1365
 
939
- // Add workspace to recents (agentCwd is the project root)
940
- if (agentCwd) {
941
- 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
+ }
942
1392
  }
943
1393
 
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
- );
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);
950
1409
 
951
1410
  return {
952
1411
  success: true,
953
1412
  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
1413
+ workspace: agentCwd,
955
1414
  colors: agentColors,
956
- fromCli: isFromCli
1415
+ sessionId: sessionId
957
1416
  };
958
1417
  } catch (error) {
959
- 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
+
960
1464
  return {
961
- success: false,
962
- 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
963
1470
  };
1471
+ } catch (error) {
1472
+ console.error('Error starting session:', error);
1473
+ return { success: false, error: error.message };
964
1474
  }
965
1475
  });
966
1476
 
@@ -990,46 +1500,24 @@ ipcMain.handle('stop-agents', async () => {
990
1500
  return { success: true };
991
1501
  });
992
1502
 
1503
+ // Reset session - marks current session as completed, stops agents
993
1504
  ipcMain.handle('reset-session', async () => {
994
1505
  try {
995
- // Stop all agents and watchers
996
1506
  stopAllAgents();
997
1507
 
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
- }
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
+ });
1027
1514
  }
1028
1515
 
1029
1516
  // Reset state
1030
1517
  messageSequence = 0;
1031
1518
  agents = [];
1032
1519
  sessionBaseCommit = null;
1520
+ currentSessionId = null;
1033
1521
 
1034
1522
  return { success: true };
1035
1523
  } catch (error) {
@@ -1038,19 +1526,15 @@ ipcMain.handle('reset-session', async () => {
1038
1526
  }
1039
1527
  });
1040
1528
 
1041
- // Handle start implementation request
1042
1529
  ipcMain.handle('start-implementation', async (event, selectedAgent, otherAgents) => {
1043
1530
  try {
1044
- // Get the implementation handoff prompt from config
1045
1531
  const promptTemplate = config.prompts?.implementation_handoff ||
1046
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.';
1047
1533
 
1048
- // Substitute placeholders
1049
1534
  const prompt = promptTemplate
1050
1535
  .replace(/{selected_agent}/g, selectedAgent)
1051
1536
  .replace(/{other_agents}/g, otherAgents.join(', '));
1052
1537
 
1053
- // Send as user message (this handles chat log + delivery to all agents)
1054
1538
  await sendUserMessage(prompt);
1055
1539
 
1056
1540
  console.log(`Implementation started with ${selectedAgent} as implementer`);
@@ -1064,7 +1548,6 @@ ipcMain.handle('start-implementation', async (event, selectedAgent, otherAgents)
1064
1548
  // Workspace Management IPC Handlers
1065
1549
  ipcMain.handle('get-recent-workspaces', async () => {
1066
1550
  const recents = await loadRecentWorkspaces();
1067
- // Add validation info for each workspace
1068
1551
  const withValidation = await Promise.all(
1069
1552
  recents.map(async (r) => ({
1070
1553
  ...r,
@@ -1075,24 +1558,22 @@ ipcMain.handle('get-recent-workspaces', async () => {
1075
1558
  return withValidation;
1076
1559
  });
1077
1560
 
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);
1561
+ ipcMain.handle('add-recent-workspace', async (event, wsPath) => {
1562
+ return await addRecentWorkspace(wsPath);
1084
1563
  });
1085
1564
 
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);
1565
+ ipcMain.handle('remove-recent-workspace', async (event, wsPath) => {
1566
+ return await removeRecentWorkspace(wsPath);
1092
1567
  });
1093
1568
 
1094
1569
  ipcMain.handle('get-current-directory', async () => {
1095
- 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
+ };
1096
1577
  });
1097
1578
 
1098
1579
  ipcMain.handle('browse-for-workspace', async () => {
@@ -1105,10 +1586,7 @@ ipcMain.handle('browse-for-workspace', async () => {
1105
1586
  return { canceled: true };
1106
1587
  }
1107
1588
 
1108
- return {
1109
- canceled: false,
1110
- path: result.filePaths[0]
1111
- };
1589
+ return { canceled: false, path: result.filePaths[0] };
1112
1590
  });
1113
1591
 
1114
1592
  ipcMain.handle('open-config-folder', async () => {
@@ -1136,7 +1614,10 @@ ipcMain.on('pty-input', (event, { agentName, data }) => {
1136
1614
  }
1137
1615
  });
1138
1616
 
1617
+ // ═══════════════════════════════════════════════════════════
1139
1618
  // App lifecycle
1619
+ // ═══════════════════════════════════════════════════════════
1620
+
1140
1621
  app.whenReady().then(async () => {
1141
1622
  console.log('App ready, setting up...');
1142
1623
  parseCommandLineArgs();
@@ -1157,7 +1638,6 @@ app.on('activate', () => {
1157
1638
  }
1158
1639
  });
1159
1640
 
1160
- // Cleanup on quit
1161
1641
  app.on('before-quit', () => {
1162
1642
  stopAllAgents();
1163
1643
  });