@appoly/multiagent-chat 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +76 -0
- package/bin/multiagent-chat.js +14 -0
- package/config.yaml +158 -0
- package/index.html +211 -0
- package/main.js +1149 -0
- package/package.json +52 -0
- package/preload.js +71 -0
- package/renderer.js +1626 -0
- package/robot.png +0 -0
- package/styles.css +1626 -0
package/main.js
ADDED
|
@@ -0,0 +1,1149 @@
|
|
|
1
|
+
const { app, BrowserWindow, ipcMain, dialog, shell } = require('electron');
|
|
2
|
+
const { spawn, exec } = require('child_process');
|
|
3
|
+
const { promisify } = require('util');
|
|
4
|
+
const pty = require('node-pty');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const fs = require('fs').promises;
|
|
7
|
+
const fsSync = require('fs');
|
|
8
|
+
const os = require('os');
|
|
9
|
+
const yaml = require('yaml');
|
|
10
|
+
const chokidar = require('chokidar');
|
|
11
|
+
|
|
12
|
+
// Home config directory: ~/.multiagent-chat/
|
|
13
|
+
const HOME_CONFIG_DIR = path.join(os.homedir(), '.multiagent-chat');
|
|
14
|
+
const RECENT_WORKSPACES_FILE = path.join(HOME_CONFIG_DIR, 'recent-workspaces.json');
|
|
15
|
+
const HOME_CONFIG_FILE = path.join(HOME_CONFIG_DIR, 'config.yaml');
|
|
16
|
+
const MAX_RECENT_WORKSPACES = 8;
|
|
17
|
+
|
|
18
|
+
const execAsync = promisify(exec);
|
|
19
|
+
|
|
20
|
+
let mainWindow;
|
|
21
|
+
let agents = [];
|
|
22
|
+
let config;
|
|
23
|
+
let workspacePath;
|
|
24
|
+
let agentCwd; // Parent directory of workspace - where agents are launched
|
|
25
|
+
let fileWatcher;
|
|
26
|
+
let outboxWatcher;
|
|
27
|
+
let customWorkspacePath = null;
|
|
28
|
+
let customConfigPath = null; // CLI --config path
|
|
29
|
+
let messageSequence = 0; // For ordering messages in chat
|
|
30
|
+
let agentColors = {}; // Map of agent name -> color
|
|
31
|
+
let sessionBaseCommit = null; // Git commit hash at session start for diff baseline
|
|
32
|
+
|
|
33
|
+
// Parse command-line arguments
|
|
34
|
+
// Usage: npm start /path/to/workspace
|
|
35
|
+
// Or: npm start --workspace /path/to/workspace
|
|
36
|
+
// Or: npm start --config /path/to/config.yaml
|
|
37
|
+
// Or: WORKSPACE=/path/to/workspace npm start
|
|
38
|
+
function parseCommandLineArgs() {
|
|
39
|
+
// Check environment variables first
|
|
40
|
+
if (process.env.WORKSPACE) {
|
|
41
|
+
customWorkspacePath = process.env.WORKSPACE;
|
|
42
|
+
console.log('Using workspace from environment variable:', customWorkspacePath);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (process.env.CONFIG) {
|
|
46
|
+
customConfigPath = process.env.CONFIG;
|
|
47
|
+
console.log('Using config from environment variable:', customConfigPath);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Then check command-line arguments
|
|
51
|
+
// process.argv looks like: [electron, main.js, ...args]
|
|
52
|
+
const args = process.argv.slice(2);
|
|
53
|
+
|
|
54
|
+
for (let i = 0; i < args.length; i++) {
|
|
55
|
+
// Parse --workspace flag
|
|
56
|
+
if (args[i] === '--workspace' && args[i + 1]) {
|
|
57
|
+
customWorkspacePath = args[i + 1];
|
|
58
|
+
console.log('Using workspace from --workspace flag:', customWorkspacePath);
|
|
59
|
+
i++; // Skip next arg
|
|
60
|
+
}
|
|
61
|
+
// Parse --config flag
|
|
62
|
+
else if (args[i] === '--config' && args[i + 1]) {
|
|
63
|
+
customConfigPath = args[i + 1];
|
|
64
|
+
console.log('Using config from --config flag:', customConfigPath);
|
|
65
|
+
i++; // Skip next arg
|
|
66
|
+
}
|
|
67
|
+
// Positional arg (assume workspace path if not a flag)
|
|
68
|
+
else if (!args[i].startsWith('--') && !customWorkspacePath) {
|
|
69
|
+
customWorkspacePath = args[i];
|
|
70
|
+
console.log('Using workspace from positional argument:', customWorkspacePath);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Ensure home config directory exists and set up first-run defaults
|
|
76
|
+
async function ensureHomeConfigDir() {
|
|
77
|
+
try {
|
|
78
|
+
// Create ~/.multiagent-chat/ if it doesn't exist
|
|
79
|
+
await fs.mkdir(HOME_CONFIG_DIR, { recursive: true });
|
|
80
|
+
console.log('Home config directory ensured:', HOME_CONFIG_DIR);
|
|
81
|
+
|
|
82
|
+
// Migration: check for existing data in Electron userData
|
|
83
|
+
const userDataDir = app.getPath('userData');
|
|
84
|
+
const oldRecentsFile = path.join(userDataDir, 'recent-workspaces.json');
|
|
85
|
+
|
|
86
|
+
// Check if config.yaml exists in home dir, if not copy default
|
|
87
|
+
try {
|
|
88
|
+
await fs.access(HOME_CONFIG_FILE);
|
|
89
|
+
console.log('Home config exists:', HOME_CONFIG_FILE);
|
|
90
|
+
} catch (e) {
|
|
91
|
+
// Copy bundled default config to home dir
|
|
92
|
+
const bundledConfig = path.join(__dirname, 'config.yaml');
|
|
93
|
+
try {
|
|
94
|
+
await fs.copyFile(bundledConfig, HOME_CONFIG_FILE);
|
|
95
|
+
console.log('Copied default config to:', HOME_CONFIG_FILE);
|
|
96
|
+
} catch (copyError) {
|
|
97
|
+
console.warn('Could not copy default config:', copyError.message);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Initialize or migrate recent-workspaces.json
|
|
102
|
+
try {
|
|
103
|
+
await fs.access(RECENT_WORKSPACES_FILE);
|
|
104
|
+
} catch (e) {
|
|
105
|
+
// Try to migrate from old location first
|
|
106
|
+
try {
|
|
107
|
+
await fs.access(oldRecentsFile);
|
|
108
|
+
await fs.copyFile(oldRecentsFile, RECENT_WORKSPACES_FILE);
|
|
109
|
+
console.log('Migrated recent workspaces from:', oldRecentsFile);
|
|
110
|
+
} catch (migrateError) {
|
|
111
|
+
// No old file, create new empty one
|
|
112
|
+
await fs.writeFile(RECENT_WORKSPACES_FILE, JSON.stringify({ recents: [] }, null, 2));
|
|
113
|
+
console.log('Initialized recent workspaces file:', RECENT_WORKSPACES_FILE);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
} catch (error) {
|
|
117
|
+
console.error('Error setting up home config directory:', error);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Load recent workspaces from JSON file
|
|
122
|
+
async function loadRecentWorkspaces() {
|
|
123
|
+
try {
|
|
124
|
+
const content = await fs.readFile(RECENT_WORKSPACES_FILE, 'utf8');
|
|
125
|
+
const data = JSON.parse(content);
|
|
126
|
+
return data.recents || [];
|
|
127
|
+
} catch (error) {
|
|
128
|
+
console.warn('Could not load recent workspaces:', error.message);
|
|
129
|
+
return [];
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Save recent workspaces to JSON file
|
|
134
|
+
async function saveRecentWorkspaces(recents) {
|
|
135
|
+
try {
|
|
136
|
+
await fs.writeFile(RECENT_WORKSPACES_FILE, JSON.stringify({ recents }, null, 2));
|
|
137
|
+
} catch (error) {
|
|
138
|
+
console.error('Error saving recent workspaces:', error);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Add or update a workspace in recents
|
|
143
|
+
async function addRecentWorkspace(workspacePath) {
|
|
144
|
+
const recents = await loadRecentWorkspaces();
|
|
145
|
+
const now = new Date().toISOString();
|
|
146
|
+
|
|
147
|
+
// Remove existing entry with same path (case-insensitive on Windows)
|
|
148
|
+
const filtered = recents.filter(r =>
|
|
149
|
+
r.path.toLowerCase() !== workspacePath.toLowerCase()
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
// Add new entry at the beginning
|
|
153
|
+
filtered.unshift({
|
|
154
|
+
path: workspacePath,
|
|
155
|
+
lastUsed: now
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Limit to max entries
|
|
159
|
+
const limited = filtered.slice(0, MAX_RECENT_WORKSPACES);
|
|
160
|
+
|
|
161
|
+
await saveRecentWorkspaces(limited);
|
|
162
|
+
return limited;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Remove a workspace from recents
|
|
166
|
+
async function removeRecentWorkspace(workspacePath) {
|
|
167
|
+
const recents = await loadRecentWorkspaces();
|
|
168
|
+
const filtered = recents.filter(r =>
|
|
169
|
+
r.path.toLowerCase() !== workspacePath.toLowerCase()
|
|
170
|
+
);
|
|
171
|
+
await saveRecentWorkspaces(filtered);
|
|
172
|
+
return filtered;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Update path of a workspace in recents (for "Locate" functionality)
|
|
176
|
+
async function updateRecentWorkspacePath(oldPath, newPath) {
|
|
177
|
+
const recents = await loadRecentWorkspaces();
|
|
178
|
+
const now = new Date().toISOString();
|
|
179
|
+
|
|
180
|
+
const updated = recents.map(r => {
|
|
181
|
+
if (r.path.toLowerCase() === oldPath.toLowerCase()) {
|
|
182
|
+
return { path: newPath, lastUsed: now };
|
|
183
|
+
}
|
|
184
|
+
return r;
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
await saveRecentWorkspaces(updated);
|
|
188
|
+
return updated;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Validate if a workspace path exists and is a directory
|
|
192
|
+
async function validateWorkspacePath(workspacePath) {
|
|
193
|
+
try {
|
|
194
|
+
const stats = await fs.stat(workspacePath);
|
|
195
|
+
return stats.isDirectory();
|
|
196
|
+
} catch (error) {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Get current working directory info
|
|
202
|
+
function getCurrentDirectoryInfo() {
|
|
203
|
+
const cwd = process.cwd();
|
|
204
|
+
const appDir = __dirname;
|
|
205
|
+
|
|
206
|
+
// Check if cwd is different from app directory and exists
|
|
207
|
+
const isUsable = cwd !== appDir && fsSync.existsSync(cwd);
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
path: cwd,
|
|
211
|
+
isUsable,
|
|
212
|
+
appDir
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Create the browser window
|
|
217
|
+
function createWindow() {
|
|
218
|
+
console.log('Creating window...');
|
|
219
|
+
|
|
220
|
+
const iconPath = path.join(__dirname, 'robot.png');
|
|
221
|
+
|
|
222
|
+
mainWindow = new BrowserWindow({
|
|
223
|
+
width: 1400,
|
|
224
|
+
height: 900,
|
|
225
|
+
icon: iconPath,
|
|
226
|
+
webPreferences: {
|
|
227
|
+
preload: path.join(__dirname, 'preload.js'),
|
|
228
|
+
contextIsolation: true,
|
|
229
|
+
nodeIntegration: false
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// Set dock icon on macOS
|
|
234
|
+
if (process.platform === 'darwin' && app.dock) {
|
|
235
|
+
app.dock.setIcon(iconPath);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
console.log('Window created, loading index.html...');
|
|
239
|
+
mainWindow.loadFile('index.html');
|
|
240
|
+
|
|
241
|
+
mainWindow.webContents.on('did-finish-load', () => {
|
|
242
|
+
console.log('Page loaded successfully');
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
console.log('Window setup complete');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Load configuration with priority: CLI arg > home config > bundled default
|
|
249
|
+
async function loadConfig(configPath = null) {
|
|
250
|
+
try {
|
|
251
|
+
let fullPath;
|
|
252
|
+
|
|
253
|
+
// Priority 1: CLI argument (--config flag or CONFIG env var)
|
|
254
|
+
if (configPath) {
|
|
255
|
+
fullPath = path.isAbsolute(configPath) ? configPath : path.join(process.cwd(), configPath);
|
|
256
|
+
console.log('Loading config from CLI arg:', fullPath);
|
|
257
|
+
}
|
|
258
|
+
// Priority 2: Home directory config (~/.multiagent-chat/config.yaml)
|
|
259
|
+
else if (fsSync.existsSync(HOME_CONFIG_FILE)) {
|
|
260
|
+
fullPath = HOME_CONFIG_FILE;
|
|
261
|
+
console.log('Loading config from home dir:', fullPath);
|
|
262
|
+
}
|
|
263
|
+
// Priority 3: Bundled default (fallback)
|
|
264
|
+
else {
|
|
265
|
+
fullPath = path.join(__dirname, 'config.yaml');
|
|
266
|
+
console.log('Loading bundled config from:', fullPath);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const configFile = await fs.readFile(fullPath, 'utf8');
|
|
270
|
+
config = yaml.parse(configFile);
|
|
271
|
+
console.log('Config loaded successfully');
|
|
272
|
+
return config;
|
|
273
|
+
} catch (error) {
|
|
274
|
+
console.error('Error loading config:', error);
|
|
275
|
+
throw error;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Setup workspace directory and files
|
|
280
|
+
// customPath = project root selected by user (or from CLI)
|
|
281
|
+
async function setupWorkspace(customPath = null) {
|
|
282
|
+
// Determine project root (agentCwd) and workspace path (.multiagent-chat inside it)
|
|
283
|
+
let projectRoot;
|
|
284
|
+
|
|
285
|
+
if (customPath && path.isAbsolute(customPath)) {
|
|
286
|
+
projectRoot = customPath;
|
|
287
|
+
} else if (customPath) {
|
|
288
|
+
projectRoot = path.join(process.cwd(), customPath);
|
|
289
|
+
} else {
|
|
290
|
+
// Fallback: use current directory as project root
|
|
291
|
+
projectRoot = process.cwd();
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Set agent working directory to the project root
|
|
295
|
+
agentCwd = projectRoot;
|
|
296
|
+
|
|
297
|
+
// Set workspace path to .multiagent-chat inside the project root
|
|
298
|
+
const workspaceName = config.workspace || '.multiagent-chat';
|
|
299
|
+
workspacePath = path.join(projectRoot, workspaceName);
|
|
300
|
+
|
|
301
|
+
try {
|
|
302
|
+
await fs.mkdir(workspacePath, { recursive: true });
|
|
303
|
+
|
|
304
|
+
// Initialize chat.jsonl (empty file - JSONL format)
|
|
305
|
+
const chatPath = path.join(workspacePath, config.chat_file || 'chat.jsonl');
|
|
306
|
+
await fs.writeFile(chatPath, '');
|
|
307
|
+
|
|
308
|
+
// Clear PLAN_FINAL.md if it exists
|
|
309
|
+
const planPath = path.join(workspacePath, config.plan_file || 'PLAN_FINAL.md');
|
|
310
|
+
await fs.writeFile(planPath, '');
|
|
311
|
+
|
|
312
|
+
// Create outbox directory and per-agent outbox files
|
|
313
|
+
const outboxDir = path.join(workspacePath, config.outbox_dir || 'outbox');
|
|
314
|
+
await fs.mkdir(outboxDir, { recursive: true });
|
|
315
|
+
|
|
316
|
+
// Build agent colors map from config
|
|
317
|
+
const defaultColors = config.default_agent_colors || ['#667eea', '#f093fb', '#4fd1c5', '#f6ad55', '#68d391', '#fc8181'];
|
|
318
|
+
agentColors = {};
|
|
319
|
+
config.agents.forEach((agentConfig, index) => {
|
|
320
|
+
agentColors[agentConfig.name.toLowerCase()] = agentConfig.color || defaultColors[index % defaultColors.length];
|
|
321
|
+
});
|
|
322
|
+
// Add user color
|
|
323
|
+
agentColors['user'] = config.user_color || '#a0aec0';
|
|
324
|
+
|
|
325
|
+
// Create empty outbox file for each agent
|
|
326
|
+
for (const agentConfig of config.agents) {
|
|
327
|
+
const outboxFile = path.join(outboxDir, `${agentConfig.name.toLowerCase()}.md`);
|
|
328
|
+
await fs.writeFile(outboxFile, '');
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Reset message sequence
|
|
332
|
+
messageSequence = 0;
|
|
333
|
+
|
|
334
|
+
// Capture git base commit for diff baseline
|
|
335
|
+
try {
|
|
336
|
+
const { stdout } = await execAsync('git rev-parse HEAD', { cwd: agentCwd });
|
|
337
|
+
sessionBaseCommit = stdout.trim();
|
|
338
|
+
console.log('Session base commit:', sessionBaseCommit);
|
|
339
|
+
} catch (error) {
|
|
340
|
+
// Check if it's a git repo with no commits yet
|
|
341
|
+
try {
|
|
342
|
+
await execAsync('git rev-parse --git-dir', { cwd: agentCwd });
|
|
343
|
+
// It's a git repo but no commits - use empty tree hash
|
|
344
|
+
sessionBaseCommit = '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
|
|
345
|
+
console.log('Git repo with no commits, using empty tree hash for diff baseline');
|
|
346
|
+
} catch (e) {
|
|
347
|
+
console.log('Not a git repository:', e.message);
|
|
348
|
+
sessionBaseCommit = null;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
console.log('Workspace setup complete:', workspacePath);
|
|
353
|
+
console.log('Agent working directory:', agentCwd);
|
|
354
|
+
console.log('Outbox directory created:', outboxDir);
|
|
355
|
+
console.log('Agent colors:', agentColors);
|
|
356
|
+
return workspacePath;
|
|
357
|
+
} catch (error) {
|
|
358
|
+
console.error('Error setting up workspace:', error);
|
|
359
|
+
throw error;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Agent Process Management
|
|
364
|
+
class AgentProcess {
|
|
365
|
+
constructor(agentConfig, index) {
|
|
366
|
+
this.name = agentConfig.name;
|
|
367
|
+
this.command = agentConfig.command;
|
|
368
|
+
this.args = agentConfig.args || [];
|
|
369
|
+
this.use_pty = agentConfig.use_pty || false;
|
|
370
|
+
this.index = index;
|
|
371
|
+
this.process = null;
|
|
372
|
+
this.outputBuffer = [];
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async start(prompt) {
|
|
376
|
+
return new Promise((resolve, reject) => {
|
|
377
|
+
console.log(`Starting agent ${this.name} with PTY: ${this.use_pty}`);
|
|
378
|
+
|
|
379
|
+
if (this.use_pty) {
|
|
380
|
+
// Use PTY for interactive TUI agents
|
|
381
|
+
const shell = process.env.SHELL || '/bin/bash';
|
|
382
|
+
|
|
383
|
+
this.process = pty.spawn(this.command, this.args, {
|
|
384
|
+
name: 'xterm-256color',
|
|
385
|
+
cols: 120,
|
|
386
|
+
rows: 40,
|
|
387
|
+
cwd: agentCwd,
|
|
388
|
+
env: {
|
|
389
|
+
...process.env,
|
|
390
|
+
AGENT_NAME: this.name,
|
|
391
|
+
TERM: 'xterm-256color',
|
|
392
|
+
PATH: process.env.PATH,
|
|
393
|
+
HOME: process.env.HOME,
|
|
394
|
+
SHELL: shell,
|
|
395
|
+
LINES: '40',
|
|
396
|
+
COLUMNS: '120'
|
|
397
|
+
},
|
|
398
|
+
handleFlowControl: true
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
console.log(`PTY spawned for ${this.name}, PID: ${this.process.pid}`);
|
|
402
|
+
|
|
403
|
+
// Respond to cursor position query immediately
|
|
404
|
+
// This helps with terminal capability detection (needed for Codex)
|
|
405
|
+
setTimeout(() => {
|
|
406
|
+
this.process.write('\x1b[1;1R'); // Report cursor at position 1,1
|
|
407
|
+
}, 100);
|
|
408
|
+
|
|
409
|
+
// Capture all output from PTY
|
|
410
|
+
this.process.onData((data) => {
|
|
411
|
+
const output = data.toString();
|
|
412
|
+
this.outputBuffer.push(output);
|
|
413
|
+
|
|
414
|
+
if (mainWindow) {
|
|
415
|
+
mainWindow.webContents.send('agent-output', {
|
|
416
|
+
agentName: this.name,
|
|
417
|
+
output: output,
|
|
418
|
+
isPty: true
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
// Handle exit - trigger resume if enabled
|
|
424
|
+
this.process.onExit(({ exitCode, signal }) => {
|
|
425
|
+
console.log(`Agent ${this.name} exited with code ${exitCode}, signal ${signal}`);
|
|
426
|
+
this.handleExit(exitCode);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
// Inject prompt via PTY after TUI initializes (original working pattern)
|
|
430
|
+
const initDelay = this.name === 'Codex' ? 5000 : 3000;
|
|
431
|
+
setTimeout(() => {
|
|
432
|
+
console.log(`Injecting prompt into ${this.name} PTY`);
|
|
433
|
+
this.process.write(prompt + '\n');
|
|
434
|
+
|
|
435
|
+
// Send Enter key after a brief delay to submit
|
|
436
|
+
setTimeout(() => {
|
|
437
|
+
this.process.write('\r');
|
|
438
|
+
}, 500);
|
|
439
|
+
|
|
440
|
+
resolve();
|
|
441
|
+
}, initDelay);
|
|
442
|
+
|
|
443
|
+
} else {
|
|
444
|
+
// Use regular spawn for non-interactive agents
|
|
445
|
+
const options = {
|
|
446
|
+
cwd: agentCwd,
|
|
447
|
+
env: {
|
|
448
|
+
...process.env,
|
|
449
|
+
AGENT_NAME: this.name
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
this.process = spawn(this.command, this.args, options);
|
|
454
|
+
|
|
455
|
+
console.log(`Process spawned for ${this.name}, PID: ${this.process.pid}`);
|
|
456
|
+
|
|
457
|
+
// Capture stdout
|
|
458
|
+
this.process.stdout.on('data', (data) => {
|
|
459
|
+
const output = data.toString();
|
|
460
|
+
this.outputBuffer.push(output);
|
|
461
|
+
|
|
462
|
+
if (mainWindow) {
|
|
463
|
+
mainWindow.webContents.send('agent-output', {
|
|
464
|
+
agentName: this.name,
|
|
465
|
+
output: output,
|
|
466
|
+
isPty: false
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
// Capture stderr
|
|
472
|
+
this.process.stderr.on('data', (data) => {
|
|
473
|
+
const output = data.toString();
|
|
474
|
+
this.outputBuffer.push(`[stderr] ${output}`);
|
|
475
|
+
|
|
476
|
+
if (mainWindow) {
|
|
477
|
+
mainWindow.webContents.send('agent-output', {
|
|
478
|
+
agentName: this.name,
|
|
479
|
+
output: `[stderr] ${output}`,
|
|
480
|
+
isPty: false
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
// Handle process exit - trigger resume if enabled
|
|
486
|
+
this.process.on('close', (code) => {
|
|
487
|
+
console.log(`Agent ${this.name} exited with code ${code}`);
|
|
488
|
+
this.handleExit(code);
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
// Handle errors
|
|
492
|
+
this.process.on('error', (error) => {
|
|
493
|
+
console.error(`Error starting agent ${this.name}:`, error);
|
|
494
|
+
reject(error);
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
resolve();
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Handle agent exit
|
|
503
|
+
handleExit(exitCode) {
|
|
504
|
+
if (mainWindow) {
|
|
505
|
+
mainWindow.webContents.send('agent-status', {
|
|
506
|
+
agentName: this.name,
|
|
507
|
+
status: 'stopped',
|
|
508
|
+
exitCode: exitCode
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
sendMessage(message) {
|
|
514
|
+
if (this.use_pty) {
|
|
515
|
+
if (this.process && this.process.write) {
|
|
516
|
+
this.process.write(message + '\n');
|
|
517
|
+
// Send Enter key to submit for PTY
|
|
518
|
+
setTimeout(() => {
|
|
519
|
+
this.process.write('\r');
|
|
520
|
+
}, 300);
|
|
521
|
+
}
|
|
522
|
+
} else {
|
|
523
|
+
if (this.process && this.process.stdin) {
|
|
524
|
+
this.process.stdin.write(message + '\n');
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
stop() {
|
|
530
|
+
if (this.process) {
|
|
531
|
+
if (this.use_pty) {
|
|
532
|
+
this.process.kill();
|
|
533
|
+
} else {
|
|
534
|
+
this.process.kill('SIGTERM');
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Initialize agents from config
|
|
541
|
+
function initializeAgents() {
|
|
542
|
+
agents = config.agents.map((agentConfig, index) => {
|
|
543
|
+
return new AgentProcess(agentConfig, index);
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
console.log(`Initialized ${agents.length} agents`);
|
|
547
|
+
return agents;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Get agent by name
|
|
551
|
+
function getAgentByName(name) {
|
|
552
|
+
return agents.find(a => a.name.toLowerCase() === name.toLowerCase());
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Send a message to all agents EXCEPT the sender
|
|
556
|
+
function sendMessageToOtherAgents(senderName, message) {
|
|
557
|
+
const workspaceFolder = path.basename(workspacePath);
|
|
558
|
+
const outboxDir = config.outbox_dir || 'outbox';
|
|
559
|
+
|
|
560
|
+
for (const agent of agents) {
|
|
561
|
+
if (agent.name.toLowerCase() !== senderName.toLowerCase()) {
|
|
562
|
+
// Path relative to agentCwd (includes workspace folder)
|
|
563
|
+
const outboxFile = `${workspaceFolder}/${outboxDir}/${agent.name.toLowerCase()}.md`;
|
|
564
|
+
const formattedMessage = `\n---\n📨 MESSAGE FROM ${senderName.toUpperCase()}:\n\n${message}\n\n---\n(Respond via: cat << 'EOF' > ${outboxFile})\n`;
|
|
565
|
+
|
|
566
|
+
console.log(`Delivering message from ${senderName} to ${agent.name}`);
|
|
567
|
+
agent.sendMessage(formattedMessage);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Send a message to ALL agents (for user messages)
|
|
573
|
+
function sendMessageToAllAgents(message) {
|
|
574
|
+
const workspaceFolder = path.basename(workspacePath);
|
|
575
|
+
const outboxDir = config.outbox_dir || 'outbox';
|
|
576
|
+
|
|
577
|
+
for (const agent of agents) {
|
|
578
|
+
// Path relative to agentCwd (includes workspace folder)
|
|
579
|
+
const outboxFile = `${workspaceFolder}/${outboxDir}/${agent.name.toLowerCase()}.md`;
|
|
580
|
+
const formattedMessage = `\n---\n📨 MESSAGE FROM USER:\n\n${message}\n\n---\n(Respond via: cat << 'EOF' > ${outboxFile})\n`;
|
|
581
|
+
|
|
582
|
+
console.log(`Delivering user message to ${agent.name}`);
|
|
583
|
+
agent.sendMessage(formattedMessage);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Build prompt for a specific agent
|
|
588
|
+
function buildAgentPrompt(challenge, agentName) {
|
|
589
|
+
const workspaceFolder = path.basename(workspacePath);
|
|
590
|
+
const outboxDir = config.outbox_dir || 'outbox';
|
|
591
|
+
// Path relative to agentCwd (includes workspace folder)
|
|
592
|
+
const outboxFile = `${workspaceFolder}/${outboxDir}/${agentName.toLowerCase()}.md`;
|
|
593
|
+
const planFile = `${workspaceFolder}/${config.plan_file || 'PLAN_FINAL.md'}`;
|
|
594
|
+
|
|
595
|
+
return config.prompt_template
|
|
596
|
+
.replace('{challenge}', challenge)
|
|
597
|
+
.replace('{workspace}', workspacePath)
|
|
598
|
+
.replace(/{outbox_file}/g, outboxFile) // Replace all occurrences
|
|
599
|
+
.replace(/{plan_file}/g, planFile) // Replace all occurrences
|
|
600
|
+
.replace('{agent_names}', agents.map(a => a.name).join(', '))
|
|
601
|
+
.replace('{agent_name}', agentName);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Start all agents with their individual prompts
|
|
605
|
+
async function startAgents(challenge) {
|
|
606
|
+
console.log('Starting agents with prompts...');
|
|
607
|
+
|
|
608
|
+
for (const agent of agents) {
|
|
609
|
+
try {
|
|
610
|
+
const prompt = buildAgentPrompt(challenge, agent.name);
|
|
611
|
+
await agent.start(prompt);
|
|
612
|
+
console.log(`Started agent: ${agent.name}`);
|
|
613
|
+
|
|
614
|
+
if (mainWindow) {
|
|
615
|
+
mainWindow.webContents.send('agent-status', {
|
|
616
|
+
agentName: agent.name,
|
|
617
|
+
status: 'running'
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
} catch (error) {
|
|
621
|
+
console.error(`Failed to start agent ${agent.name}:`, error);
|
|
622
|
+
|
|
623
|
+
if (mainWindow) {
|
|
624
|
+
mainWindow.webContents.send('agent-status', {
|
|
625
|
+
agentName: agent.name,
|
|
626
|
+
status: 'error',
|
|
627
|
+
error: error.message
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Watch chat.jsonl for changes (backup - real-time updates via chat-message event)
|
|
635
|
+
function startFileWatcher() {
|
|
636
|
+
const chatPath = path.join(workspacePath, config.chat_file || 'chat.jsonl');
|
|
637
|
+
|
|
638
|
+
fileWatcher = chokidar.watch(chatPath, {
|
|
639
|
+
persistent: true,
|
|
640
|
+
ignoreInitial: true
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
// Note: Primary updates happen via 'chat-message' events sent when outbox is processed
|
|
644
|
+
// This watcher is a backup for any external modifications
|
|
645
|
+
fileWatcher.on('change', async () => {
|
|
646
|
+
try {
|
|
647
|
+
const messages = await getChatContent();
|
|
648
|
+
if (mainWindow) {
|
|
649
|
+
mainWindow.webContents.send('chat-updated', messages);
|
|
650
|
+
}
|
|
651
|
+
} catch (error) {
|
|
652
|
+
console.error('Error reading chat file:', error);
|
|
653
|
+
}
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
console.log('File watcher started for:', chatPath);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// Watch outbox directory and merge messages into chat.jsonl
|
|
660
|
+
function startOutboxWatcher() {
|
|
661
|
+
const outboxDir = path.join(workspacePath, config.outbox_dir || 'outbox');
|
|
662
|
+
const chatPath = path.join(workspacePath, config.chat_file || 'chat.jsonl');
|
|
663
|
+
|
|
664
|
+
// Track which files we're currently processing to avoid race conditions
|
|
665
|
+
const processing = new Set();
|
|
666
|
+
|
|
667
|
+
outboxWatcher = chokidar.watch(outboxDir, {
|
|
668
|
+
persistent: true,
|
|
669
|
+
ignoreInitial: true,
|
|
670
|
+
awaitWriteFinish: {
|
|
671
|
+
stabilityThreshold: 500, // Wait for file to be stable for 500ms
|
|
672
|
+
pollInterval: 100
|
|
673
|
+
}
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
outboxWatcher.on('change', async (filePath) => {
|
|
677
|
+
// Only process .md files
|
|
678
|
+
if (!filePath.endsWith('.md')) return;
|
|
679
|
+
|
|
680
|
+
// Avoid processing the same file concurrently
|
|
681
|
+
if (processing.has(filePath)) return;
|
|
682
|
+
processing.add(filePath);
|
|
683
|
+
|
|
684
|
+
try {
|
|
685
|
+
// Read the outbox file
|
|
686
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
687
|
+
const trimmedContent = content.trim();
|
|
688
|
+
|
|
689
|
+
// Skip if empty
|
|
690
|
+
if (!trimmedContent) {
|
|
691
|
+
processing.delete(filePath);
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// Extract agent name from filename (e.g., "claude.md" -> "Claude")
|
|
696
|
+
const filename = path.basename(filePath, '.md');
|
|
697
|
+
const agentName = filename.charAt(0).toUpperCase() + filename.slice(1);
|
|
698
|
+
|
|
699
|
+
// Increment sequence and create message object
|
|
700
|
+
messageSequence++;
|
|
701
|
+
const timestamp = new Date().toISOString();
|
|
702
|
+
const message = {
|
|
703
|
+
seq: messageSequence,
|
|
704
|
+
type: 'agent',
|
|
705
|
+
agent: agentName,
|
|
706
|
+
timestamp: timestamp,
|
|
707
|
+
content: trimmedContent,
|
|
708
|
+
color: agentColors[agentName.toLowerCase()] || '#667eea'
|
|
709
|
+
};
|
|
710
|
+
|
|
711
|
+
// Append to chat.jsonl
|
|
712
|
+
await fs.appendFile(chatPath, JSON.stringify(message) + '\n');
|
|
713
|
+
console.log(`Merged message from ${agentName} (#${messageSequence}) into chat.jsonl`);
|
|
714
|
+
|
|
715
|
+
// Clear the outbox file
|
|
716
|
+
await fs.writeFile(filePath, '');
|
|
717
|
+
|
|
718
|
+
// PUSH message to other agents' PTYs
|
|
719
|
+
sendMessageToOtherAgents(agentName, trimmedContent);
|
|
720
|
+
|
|
721
|
+
// Notify renderer with the new message
|
|
722
|
+
if (mainWindow) {
|
|
723
|
+
mainWindow.webContents.send('chat-message', message);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
} catch (error) {
|
|
727
|
+
console.error(`Error processing outbox file ${filePath}:`, error);
|
|
728
|
+
} finally {
|
|
729
|
+
processing.delete(filePath);
|
|
730
|
+
}
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
console.log('Outbox watcher started for:', outboxDir);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// Stop outbox watcher
|
|
737
|
+
function stopOutboxWatcher() {
|
|
738
|
+
if (outboxWatcher) {
|
|
739
|
+
outboxWatcher.close();
|
|
740
|
+
outboxWatcher = null;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// Append user message to chat.jsonl and push to all agents
|
|
745
|
+
async function sendUserMessage(messageText) {
|
|
746
|
+
const chatPath = path.join(workspacePath, config.chat_file || 'chat.jsonl');
|
|
747
|
+
messageSequence++;
|
|
748
|
+
const timestamp = new Date().toISOString();
|
|
749
|
+
|
|
750
|
+
const message = {
|
|
751
|
+
seq: messageSequence,
|
|
752
|
+
type: 'user',
|
|
753
|
+
agent: 'User',
|
|
754
|
+
timestamp: timestamp,
|
|
755
|
+
content: messageText,
|
|
756
|
+
color: agentColors['user'] || '#a0aec0'
|
|
757
|
+
};
|
|
758
|
+
|
|
759
|
+
try {
|
|
760
|
+
// Append to chat.jsonl
|
|
761
|
+
await fs.appendFile(chatPath, JSON.stringify(message) + '\n');
|
|
762
|
+
console.log(`User message #${messageSequence} appended to chat`);
|
|
763
|
+
|
|
764
|
+
// PUSH message to all agents' PTYs
|
|
765
|
+
sendMessageToAllAgents(messageText);
|
|
766
|
+
|
|
767
|
+
// Notify renderer with the new message
|
|
768
|
+
if (mainWindow) {
|
|
769
|
+
mainWindow.webContents.send('chat-message', message);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
} catch (error) {
|
|
773
|
+
console.error('Error appending user message:', error);
|
|
774
|
+
throw error;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// Read current chat content (returns array of message objects)
|
|
779
|
+
async function getChatContent() {
|
|
780
|
+
const chatPath = path.join(workspacePath, config.chat_file || 'chat.jsonl');
|
|
781
|
+
try {
|
|
782
|
+
const content = await fs.readFile(chatPath, 'utf8');
|
|
783
|
+
if (!content.trim()) return [];
|
|
784
|
+
|
|
785
|
+
// Parse JSONL (one JSON object per line)
|
|
786
|
+
const messages = content.trim().split('\n').map(line => {
|
|
787
|
+
try {
|
|
788
|
+
return JSON.parse(line);
|
|
789
|
+
} catch (e) {
|
|
790
|
+
console.error('Failed to parse chat line:', line);
|
|
791
|
+
return null;
|
|
792
|
+
}
|
|
793
|
+
}).filter(Boolean);
|
|
794
|
+
|
|
795
|
+
return messages;
|
|
796
|
+
} catch (error) {
|
|
797
|
+
console.error('Error reading chat:', error);
|
|
798
|
+
return [];
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// Read final plan
|
|
803
|
+
async function getPlanContent() {
|
|
804
|
+
const planPath = path.join(workspacePath, config.plan_file || 'PLAN_FINAL.md');
|
|
805
|
+
try {
|
|
806
|
+
return await fs.readFile(planPath, 'utf8');
|
|
807
|
+
} catch (error) {
|
|
808
|
+
return '';
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// Get git diff - shows uncommitted changes only (git diff HEAD)
|
|
813
|
+
async function getGitDiff() {
|
|
814
|
+
// Not a git repo or session hasn't started
|
|
815
|
+
if (!agentCwd) {
|
|
816
|
+
return { isGitRepo: false, error: 'No session active' };
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// Check if git repo
|
|
820
|
+
try {
|
|
821
|
+
await execAsync('git rev-parse --git-dir', { cwd: agentCwd });
|
|
822
|
+
} catch (error) {
|
|
823
|
+
return { isGitRepo: false, error: 'Not a git repository' };
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
try {
|
|
827
|
+
const result = {
|
|
828
|
+
isGitRepo: true,
|
|
829
|
+
stats: { filesChanged: 0, insertions: 0, deletions: 0 },
|
|
830
|
+
diff: '',
|
|
831
|
+
untracked: []
|
|
832
|
+
};
|
|
833
|
+
|
|
834
|
+
// Check if HEAD exists (repo might have no commits)
|
|
835
|
+
let hasHead = true;
|
|
836
|
+
try {
|
|
837
|
+
await execAsync('git rev-parse HEAD', { cwd: agentCwd });
|
|
838
|
+
} catch (e) {
|
|
839
|
+
hasHead = false;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// Determine diff target - use empty tree hash if no commits yet
|
|
843
|
+
const diffTarget = hasHead ? 'HEAD' : '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
|
|
844
|
+
|
|
845
|
+
// Get diff stats
|
|
846
|
+
try {
|
|
847
|
+
const { stdout: statOutput } = await execAsync(
|
|
848
|
+
`git diff ${diffTarget} --stat`,
|
|
849
|
+
{ cwd: agentCwd, maxBuffer: 10 * 1024 * 1024 }
|
|
850
|
+
);
|
|
851
|
+
|
|
852
|
+
// Parse stats from last line (e.g., "3 files changed, 10 insertions(+), 5 deletions(-)")
|
|
853
|
+
const statMatch = statOutput.match(/(\d+) files? changed(?:, (\d+) insertions?\(\+\))?(?:, (\d+) deletions?\(-\))?/);
|
|
854
|
+
if (statMatch) {
|
|
855
|
+
result.stats.filesChanged = parseInt(statMatch[1]) || 0;
|
|
856
|
+
result.stats.insertions = parseInt(statMatch[2]) || 0;
|
|
857
|
+
result.stats.deletions = parseInt(statMatch[3]) || 0;
|
|
858
|
+
}
|
|
859
|
+
} catch (e) {
|
|
860
|
+
// No changes or other error
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// Get full diff
|
|
864
|
+
try {
|
|
865
|
+
const { stdout: diffOutput } = await execAsync(
|
|
866
|
+
`git diff ${diffTarget}`,
|
|
867
|
+
{ cwd: agentCwd, maxBuffer: 10 * 1024 * 1024 }
|
|
868
|
+
);
|
|
869
|
+
result.diff = diffOutput;
|
|
870
|
+
} catch (e) {
|
|
871
|
+
result.diff = '';
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// Get untracked files
|
|
875
|
+
try {
|
|
876
|
+
const { stdout: untrackedOutput } = await execAsync(
|
|
877
|
+
'git ls-files --others --exclude-standard',
|
|
878
|
+
{ cwd: agentCwd }
|
|
879
|
+
);
|
|
880
|
+
result.untracked = untrackedOutput.trim().split('\n').filter(Boolean);
|
|
881
|
+
} catch (e) {
|
|
882
|
+
result.untracked = [];
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
return result;
|
|
886
|
+
} catch (error) {
|
|
887
|
+
console.error('Error getting git diff:', error);
|
|
888
|
+
return { isGitRepo: true, error: error.message };
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// Stop all agents and watchers
|
|
893
|
+
function stopAllAgents() {
|
|
894
|
+
agents.forEach(agent => agent.stop());
|
|
895
|
+
if (fileWatcher) {
|
|
896
|
+
fileWatcher.close();
|
|
897
|
+
fileWatcher = null;
|
|
898
|
+
}
|
|
899
|
+
stopOutboxWatcher();
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// IPC Handlers
|
|
903
|
+
ipcMain.handle('load-config', async () => {
|
|
904
|
+
try {
|
|
905
|
+
console.log('IPC: load-config called');
|
|
906
|
+
await loadConfig(customConfigPath);
|
|
907
|
+
console.log('IPC: load-config returning:', config);
|
|
908
|
+
return config;
|
|
909
|
+
} catch (error) {
|
|
910
|
+
console.error('IPC: load-config error:', error);
|
|
911
|
+
throw error;
|
|
912
|
+
}
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
ipcMain.handle('start-session', async (event, { challenge, workspace: selectedWorkspace }) => {
|
|
916
|
+
try {
|
|
917
|
+
// Use selected workspace if provided, otherwise fall back to customWorkspacePath
|
|
918
|
+
const workspaceToUse = selectedWorkspace || customWorkspacePath;
|
|
919
|
+
await setupWorkspace(workspaceToUse);
|
|
920
|
+
initializeAgents();
|
|
921
|
+
await startAgents(challenge);
|
|
922
|
+
startFileWatcher();
|
|
923
|
+
startOutboxWatcher(); // Watch for agent messages and merge into chat.jsonl
|
|
924
|
+
|
|
925
|
+
// Add workspace to recents (agentCwd is the project root)
|
|
926
|
+
if (agentCwd) {
|
|
927
|
+
await addRecentWorkspace(agentCwd);
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// Check if this session was started with CLI workspace (and user didn't change it)
|
|
931
|
+
const isFromCli = customWorkspacePath && (
|
|
932
|
+
workspaceToUse === customWorkspacePath ||
|
|
933
|
+
agentCwd === customWorkspacePath ||
|
|
934
|
+
agentCwd === path.resolve(customWorkspacePath)
|
|
935
|
+
);
|
|
936
|
+
|
|
937
|
+
return {
|
|
938
|
+
success: true,
|
|
939
|
+
agents: agents.map(a => ({ name: a.name, use_pty: a.use_pty })),
|
|
940
|
+
workspace: agentCwd, // Show the project root, not the internal .multiagent-chat folder
|
|
941
|
+
colors: agentColors,
|
|
942
|
+
fromCli: isFromCli
|
|
943
|
+
};
|
|
944
|
+
} catch (error) {
|
|
945
|
+
console.error('Error starting session:', error);
|
|
946
|
+
return {
|
|
947
|
+
success: false,
|
|
948
|
+
error: error.message
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
});
|
|
952
|
+
|
|
953
|
+
ipcMain.handle('send-user-message', async (event, message) => {
|
|
954
|
+
try {
|
|
955
|
+
await sendUserMessage(message);
|
|
956
|
+
return { success: true };
|
|
957
|
+
} catch (error) {
|
|
958
|
+
return { success: false, error: error.message };
|
|
959
|
+
}
|
|
960
|
+
});
|
|
961
|
+
|
|
962
|
+
ipcMain.handle('get-chat-content', async () => {
|
|
963
|
+
return await getChatContent();
|
|
964
|
+
});
|
|
965
|
+
|
|
966
|
+
ipcMain.handle('get-plan-content', async () => {
|
|
967
|
+
return await getPlanContent();
|
|
968
|
+
});
|
|
969
|
+
|
|
970
|
+
ipcMain.handle('get-git-diff', async () => {
|
|
971
|
+
return await getGitDiff();
|
|
972
|
+
});
|
|
973
|
+
|
|
974
|
+
ipcMain.handle('stop-agents', async () => {
|
|
975
|
+
stopAllAgents();
|
|
976
|
+
return { success: true };
|
|
977
|
+
});
|
|
978
|
+
|
|
979
|
+
ipcMain.handle('reset-session', async () => {
|
|
980
|
+
try {
|
|
981
|
+
// Stop all agents and watchers
|
|
982
|
+
stopAllAgents();
|
|
983
|
+
|
|
984
|
+
// Clear chat file (handle missing file gracefully)
|
|
985
|
+
if (workspacePath) {
|
|
986
|
+
const chatPath = path.join(workspacePath, config.chat_file || 'chat.jsonl');
|
|
987
|
+
try {
|
|
988
|
+
await fs.writeFile(chatPath, '');
|
|
989
|
+
} catch (e) {
|
|
990
|
+
if (e.code !== 'ENOENT') throw e;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// Clear plan file (handle missing file gracefully)
|
|
994
|
+
const planPath = path.join(workspacePath, config.plan_file || 'PLAN_FINAL.md');
|
|
995
|
+
try {
|
|
996
|
+
await fs.writeFile(planPath, '');
|
|
997
|
+
} catch (e) {
|
|
998
|
+
if (e.code !== 'ENOENT') throw e;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
// Clear outbox files
|
|
1002
|
+
const outboxDir = path.join(workspacePath, config.outbox_dir || 'outbox');
|
|
1003
|
+
try {
|
|
1004
|
+
const files = await fs.readdir(outboxDir);
|
|
1005
|
+
for (const file of files) {
|
|
1006
|
+
if (file.endsWith('.md')) {
|
|
1007
|
+
await fs.writeFile(path.join(outboxDir, file), '');
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
} catch (e) {
|
|
1011
|
+
if (e.code !== 'ENOENT') throw e;
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
// Reset state
|
|
1016
|
+
messageSequence = 0;
|
|
1017
|
+
agents = [];
|
|
1018
|
+
sessionBaseCommit = null;
|
|
1019
|
+
|
|
1020
|
+
return { success: true };
|
|
1021
|
+
} catch (error) {
|
|
1022
|
+
console.error('Error resetting session:', error);
|
|
1023
|
+
return { success: false, error: error.message };
|
|
1024
|
+
}
|
|
1025
|
+
});
|
|
1026
|
+
|
|
1027
|
+
// Handle start implementation request
|
|
1028
|
+
ipcMain.handle('start-implementation', async (event, selectedAgent, otherAgents) => {
|
|
1029
|
+
try {
|
|
1030
|
+
// Get the implementation handoff prompt from config
|
|
1031
|
+
const promptTemplate = config.prompts?.implementation_handoff ||
|
|
1032
|
+
'{selected_agent}, please now implement this plan. {other_agents} please wait for confirmation from {selected_agent} that they have completed the implementation. You should then check the changes, and provide feedback if necessary. Keep iterating together until you are all happy with the implementation.';
|
|
1033
|
+
|
|
1034
|
+
// Substitute placeholders
|
|
1035
|
+
const prompt = promptTemplate
|
|
1036
|
+
.replace(/{selected_agent}/g, selectedAgent)
|
|
1037
|
+
.replace(/{other_agents}/g, otherAgents.join(', '));
|
|
1038
|
+
|
|
1039
|
+
// Send as user message (this handles chat log + delivery to all agents)
|
|
1040
|
+
await sendUserMessage(prompt);
|
|
1041
|
+
|
|
1042
|
+
console.log(`Implementation started with ${selectedAgent} as implementer`);
|
|
1043
|
+
return { success: true };
|
|
1044
|
+
} catch (error) {
|
|
1045
|
+
console.error('Error starting implementation:', error);
|
|
1046
|
+
return { success: false, error: error.message };
|
|
1047
|
+
}
|
|
1048
|
+
});
|
|
1049
|
+
|
|
1050
|
+
// Workspace Management IPC Handlers
|
|
1051
|
+
ipcMain.handle('get-recent-workspaces', async () => {
|
|
1052
|
+
const recents = await loadRecentWorkspaces();
|
|
1053
|
+
// Add validation info for each workspace
|
|
1054
|
+
const withValidation = await Promise.all(
|
|
1055
|
+
recents.map(async (r) => ({
|
|
1056
|
+
...r,
|
|
1057
|
+
exists: await validateWorkspacePath(r.path),
|
|
1058
|
+
name: path.basename(r.path)
|
|
1059
|
+
}))
|
|
1060
|
+
);
|
|
1061
|
+
return withValidation;
|
|
1062
|
+
});
|
|
1063
|
+
|
|
1064
|
+
ipcMain.handle('add-recent-workspace', async (event, workspacePath) => {
|
|
1065
|
+
return await addRecentWorkspace(workspacePath);
|
|
1066
|
+
});
|
|
1067
|
+
|
|
1068
|
+
ipcMain.handle('remove-recent-workspace', async (event, workspacePath) => {
|
|
1069
|
+
return await removeRecentWorkspace(workspacePath);
|
|
1070
|
+
});
|
|
1071
|
+
|
|
1072
|
+
ipcMain.handle('update-recent-workspace-path', async (event, oldPath, newPath) => {
|
|
1073
|
+
return await updateRecentWorkspacePath(oldPath, newPath);
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1076
|
+
ipcMain.handle('validate-workspace-path', async (event, workspacePath) => {
|
|
1077
|
+
return await validateWorkspacePath(workspacePath);
|
|
1078
|
+
});
|
|
1079
|
+
|
|
1080
|
+
ipcMain.handle('get-current-directory', async () => {
|
|
1081
|
+
return getCurrentDirectoryInfo();
|
|
1082
|
+
});
|
|
1083
|
+
|
|
1084
|
+
ipcMain.handle('browse-for-workspace', async () => {
|
|
1085
|
+
const result = await dialog.showOpenDialog(mainWindow, {
|
|
1086
|
+
properties: ['openDirectory'],
|
|
1087
|
+
title: 'Select Workspace Directory'
|
|
1088
|
+
});
|
|
1089
|
+
|
|
1090
|
+
if (result.canceled || result.filePaths.length === 0) {
|
|
1091
|
+
return { canceled: true };
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
return {
|
|
1095
|
+
canceled: false,
|
|
1096
|
+
path: result.filePaths[0]
|
|
1097
|
+
};
|
|
1098
|
+
});
|
|
1099
|
+
|
|
1100
|
+
ipcMain.handle('open-config-folder', async () => {
|
|
1101
|
+
try {
|
|
1102
|
+
await shell.openPath(HOME_CONFIG_DIR);
|
|
1103
|
+
return { success: true };
|
|
1104
|
+
} catch (error) {
|
|
1105
|
+
return { success: false, error: error.message };
|
|
1106
|
+
}
|
|
1107
|
+
});
|
|
1108
|
+
|
|
1109
|
+
ipcMain.handle('get-home-config-path', async () => {
|
|
1110
|
+
return HOME_CONFIG_DIR;
|
|
1111
|
+
});
|
|
1112
|
+
|
|
1113
|
+
ipcMain.handle('get-cli-workspace', async () => {
|
|
1114
|
+
return customWorkspacePath;
|
|
1115
|
+
});
|
|
1116
|
+
|
|
1117
|
+
// Handle PTY input from renderer (user typing into terminal)
|
|
1118
|
+
ipcMain.on('pty-input', (event, { agentName, data }) => {
|
|
1119
|
+
const agent = getAgentByName(agentName);
|
|
1120
|
+
if (agent && agent.use_pty && agent.process) {
|
|
1121
|
+
agent.process.write(data);
|
|
1122
|
+
}
|
|
1123
|
+
});
|
|
1124
|
+
|
|
1125
|
+
// App lifecycle
|
|
1126
|
+
app.whenReady().then(async () => {
|
|
1127
|
+
console.log('App ready, setting up...');
|
|
1128
|
+
parseCommandLineArgs();
|
|
1129
|
+
await ensureHomeConfigDir();
|
|
1130
|
+
createWindow();
|
|
1131
|
+
});
|
|
1132
|
+
|
|
1133
|
+
app.on('window-all-closed', () => {
|
|
1134
|
+
stopAllAgents();
|
|
1135
|
+
if (process.platform !== 'darwin') {
|
|
1136
|
+
app.quit();
|
|
1137
|
+
}
|
|
1138
|
+
});
|
|
1139
|
+
|
|
1140
|
+
app.on('activate', () => {
|
|
1141
|
+
if (BrowserWindow.getAllWindows().length === 0) {
|
|
1142
|
+
createWindow();
|
|
1143
|
+
}
|
|
1144
|
+
});
|
|
1145
|
+
|
|
1146
|
+
// Cleanup on quit
|
|
1147
|
+
app.on('before-quit', () => {
|
|
1148
|
+
stopAllAgents();
|
|
1149
|
+
});
|