@openagents-org/agent-launcher 0.1.2 → 0.1.4

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/tui.js +150 -358
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openagents-org/agent-launcher",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "OpenAgents Launcher — install, configure, and run AI coding agents from your terminal",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/tui.js CHANGED
@@ -1,7 +1,5 @@
1
1
  /**
2
2
  * Interactive TUI dashboard for OpenAgents — `openagents` or `openagents tui`
3
- *
4
- * Ported from Python Textual TUI (cli_tui.py). Uses blessed for terminal UI.
5
3
  */
6
4
 
7
5
  'use strict';
@@ -13,8 +11,6 @@ const fs = require('fs');
13
11
  const { spawn } = require('child_process');
14
12
  const { getExtraBinDirs } = require('./paths');
15
13
 
16
- // ── Helpers ──────────────────────────────────────────────────────────────
17
-
18
14
  const IS_WINDOWS = process.platform === 'win32';
19
15
 
20
16
  function getConnector() {
@@ -23,12 +19,11 @@ function getConnector() {
23
19
  }
24
20
 
25
21
  function loadAgentRows(connector) {
26
- const config = connector.getConfig();
22
+ const config = connector.config.load();
27
23
  const agents = config.agents || [];
28
24
  const status = connector.getDaemonStatus() || {};
29
25
  const agentStatuses = status.agents || {};
30
26
  const pid = connector.getDaemonPid();
31
-
32
27
  return agents.map(agent => {
33
28
  const info = agentStatuses[agent.name] || {};
34
29
  const state = pid ? (info.state || 'stopped') : 'stopped';
@@ -36,182 +31,124 @@ function loadAgentRows(connector) {
36
31
  if (agent.network) {
37
32
  const nets = config.networks || [];
38
33
  const net = nets.find(n => n.slug === agent.network || n.id === agent.network);
39
- if (net) {
40
- workspace = `${net.slug || net.id} (${net.name || ''})`;
41
- } else {
42
- workspace = agent.network;
43
- }
34
+ workspace = net ? `${net.slug || net.id} (${net.name || ''})` : agent.network;
44
35
  }
45
- return { name: agent.name, type: agent.type, state, workspace, role: agent.role || 'worker' };
36
+ return { name: agent.name, type: agent.type, state, workspace };
46
37
  });
47
38
  }
48
39
 
49
40
  function loadCatalog(connector) {
50
- const registry = connector.getRegistry();
51
- const entries = registry.list();
41
+ const entries = connector.registry.getCatalogSync();
52
42
  return entries.map(e => {
53
43
  let installed = false;
54
- try {
55
- const { whichBinary } = require('./paths');
56
- installed = !!whichBinary(e.install?.binary || e.name);
57
- } catch {}
44
+ try { const { whichBinary } = require('./paths'); installed = !!whichBinary(e.install?.binary || e.name); } catch {}
58
45
  if (!installed) {
59
46
  try {
60
- const markerFile = path.join(connector.config?.configDir || '', 'installed_agents.json');
61
- if (fs.existsSync(markerFile)) {
62
- const markers = JSON.parse(fs.readFileSync(markerFile, 'utf-8'));
63
- installed = !!markers[e.name];
64
- }
47
+ const f = path.join(connector._configDir, 'installed_agents.json');
48
+ if (fs.existsSync(f)) installed = !!JSON.parse(fs.readFileSync(f, 'utf-8'))[e.name];
65
49
  } catch {}
66
50
  }
67
- return {
68
- name: e.name,
69
- label: e.label || e.name,
70
- description: e.description || '',
71
- installed,
72
- };
51
+ return { name: e.name, label: e.label || e.name, description: e.description || '', installed };
73
52
  });
74
53
  }
75
54
 
76
- const STATE_STYLES = {
77
- online: { sym: '●', color: 'green', label: 'running' },
78
- running: { sym: '●', color: 'green', label: 'running' },
79
- starting: { sym: '◐', color: 'yellow', label: 'starting' },
80
- reconnecting: { sym: '◐', color: 'yellow', label: 'reconnecting' },
81
- stopped: { sym: '○', color: 'gray', label: 'stopped' },
82
- error: { sym: '✗', color: 'red', label: 'error' },
83
- 'not configured': { sym: '○', color: 'gray', label: 'not configured' },
84
- };
85
-
86
55
  // ── Main TUI ─────────────────────────────────────────────────────────────
87
56
 
88
57
  function createTUI() {
89
- const screen = blessed.screen({
90
- smartCSR: true,
91
- title: 'OpenAgents',
92
- fullUnicode: true,
93
- });
94
-
58
+ const screen = blessed.screen({ smartCSR: true, title: 'OpenAgents', fullUnicode: true });
95
59
  const connector = getConnector();
96
60
  let pkg;
97
61
  try { pkg = require('../package.json'); } catch { pkg = { version: '?' }; }
98
62
 
99
- // ── Layout ──
100
-
101
- // Header bar
63
+ // ── Header ──
102
64
  const header = blessed.box({
103
65
  top: 0, left: 0, width: '100%', height: 1,
104
- tags: true,
105
66
  style: { bg: 'blue', fg: 'white', bold: true },
106
67
  });
107
68
 
108
- // Title area below header
69
+ // ── Title ──
109
70
  const titleBox = blessed.box({
110
- top: 1, left: 0, width: '100%', height: 3,
111
- tags: true,
112
- content: `\n {bold}OpenAgents{/bold} {gray-fg}v${pkg.version}{/gray-fg} {gray-fg}Local AI Agent Manager{/gray-fg}`,
71
+ top: 1, left: 0, width: '100%', height: 2,
72
+ content: ` OpenAgents v${pkg.version}`,
73
+ style: { bold: true },
113
74
  });
114
75
 
115
- // Column headers for agent table
76
+ // ── Column Headers ──
116
77
  const colHeaders = blessed.box({
117
- top: 4, left: 0, width: '100%', height: 1,
118
- tags: true,
78
+ top: 3, left: 0, width: '100%', height: 1,
119
79
  style: { bg: 'white', fg: 'black' },
120
80
  content: ` ${'NAME'.padEnd(22)} ${'TYPE'.padEnd(14)} ${'STATUS'.padEnd(14)} WORKSPACE`,
121
81
  });
122
82
 
123
- // Agent list
83
+ // ── Agent List ──
124
84
  const agentList = blessed.list({
125
- top: 5, left: 0, width: '100%', height: '50%-2',
126
- tags: true, keys: true, vi: true, mouse: true,
127
- border: { type: 'line', left: false, right: false, top: false },
85
+ top: 4, left: 0, width: '100%', height: '50%-1',
86
+ keys: true, vi: true, mouse: true,
128
87
  style: {
129
- selected: { bg: 'blue', fg: 'white', bold: true },
88
+ selected: { bg: 'blue', fg: 'white' },
130
89
  item: { fg: 'white' },
131
- border: { fg: 'gray' },
132
90
  },
133
91
  });
134
92
 
135
- // Separator
136
- const separator = blessed.line({
137
- top: '50%+3', left: 0, width: '100%',
138
- orientation: 'horizontal',
139
- style: { fg: 'gray' },
140
- });
141
-
142
- // Activity log
93
+ // ── Activity Section ──
143
94
  const logLabel = blessed.box({
144
- top: '50%+4', left: 0, width: '100%', height: 1,
145
- tags: true,
146
- content: ' {bold}Activity{/bold}',
147
- style: { fg: 'white' },
95
+ top: '50%+3', left: 0, width: '100%', height: 1,
96
+ content: ' ACTIVITY',
97
+ style: { bg: 'white', fg: 'black' },
148
98
  });
149
99
 
150
100
  const logContent = blessed.log({
151
- top: '50%+5', left: 0, width: '100%', height: '50%-8',
152
- tags: true,
153
- scrollable: true,
154
- scrollOnInput: true,
101
+ top: '50%+4', left: 0, width: '100%', height: '50%-7',
102
+ scrollable: true, scrollOnInput: true,
155
103
  padding: { left: 2 },
156
- style: { fg: 'gray' },
104
+ style: { fg: 'white' },
157
105
  });
158
106
 
159
- // Footer
107
+ // ── Footer ──
160
108
  const footer = blessed.box({
161
109
  bottom: 0, left: 0, width: '100%', height: 1,
162
- tags: true,
163
110
  style: { bg: 'blue', fg: 'white' },
111
+ content: ' i Install n New s Start x Stop c Connect u Daemon r Refresh q Quit',
164
112
  });
165
113
 
166
114
  screen.append(header);
167
115
  screen.append(titleBox);
168
116
  screen.append(colHeaders);
169
117
  screen.append(agentList);
170
- screen.append(separator);
171
118
  screen.append(logLabel);
172
119
  screen.append(logContent);
173
120
  screen.append(footer);
174
121
 
175
- // ── State ──
176
-
177
122
  let agentRows = [];
178
123
  let currentView = 'main';
179
124
 
180
125
  function log(msg) {
181
126
  const ts = new Date().toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });
182
- logContent.log(`{gray-fg}${ts}{/gray-fg} ${msg}`);
127
+ logContent.log(`${ts} ${msg}`);
183
128
  screen.render();
184
129
  }
185
130
 
186
- // ── Refresh ──
187
-
188
131
  function refreshAgentTable() {
189
- try {
190
- agentRows = loadAgentRows(connector);
191
- } catch { agentRows = []; }
132
+ try { agentRows = loadAgentRows(connector); } catch { agentRows = []; }
192
133
 
193
134
  const items = agentRows.length ? agentRows.map(r => {
194
- const st = STATE_STYLES[r.state] || STATE_STYLES.stopped;
195
- const ws = r.workspace || '{gray-fg}—{/gray-fg}';
196
- return ` {${st.color}-fg}${st.sym}{/${st.color}-fg} ${r.name.padEnd(20)} ${r.type.padEnd(14)} {${st.color}-fg}${st.label.padEnd(14)}{/${st.color}-fg} ${ws}`;
197
- }) : [' {gray-fg}No agents configured. Press {bold}i{/bold} to install, {bold}n{/bold} to create.{/gray-fg}'];
135
+ const sym = r.state === 'running' || r.state === 'online' ? '\u25CF' :
136
+ r.state === 'error' ? '\u2717' : '\u25CB';
137
+ const ws = r.workspace || '-';
138
+ return ` ${sym} ${r.name.padEnd(20)} ${r.type.padEnd(14)} ${r.state.padEnd(14)} ${ws}`;
139
+ }) : [' No agents configured. Press i to install, n to create.'];
198
140
 
199
141
  agentList.setItems(items);
200
142
  updateHeader();
201
- updateFooter();
202
143
  screen.render();
203
144
  }
204
145
 
205
146
  function updateHeader() {
206
147
  const pid = connector.getDaemonPid();
207
- const dot = pid ? '{green-fg}●{/green-fg}' : '{gray-fg}○{/gray-fg}';
148
+ const dot = pid ? '\u25CF' : '\u25CB';
208
149
  const state = pid ? 'Daemon running' : 'Daemon idle';
209
150
  const count = agentRows.length;
210
- header.setContent(` ${dot} ${state} ${count} agent${count !== 1 ? 's' : ''} configured`);
211
- }
212
-
213
- function updateFooter() {
214
- footer.setContent(' {bold}i{/bold} Install {bold}n{/bold} New {bold}s{/bold} Start {bold}x{/bold} Stop {bold}c{/bold} Connect {bold}u{/bold} Daemon {bold}r{/bold} Refresh {bold}q{/bold} Quit');
151
+ header.setContent(` ${dot} ${state} | ${count} agent${count !== 1 ? 's' : ''} configured`);
215
152
  }
216
153
 
217
154
  // ── Install Screen ──
@@ -219,275 +156,169 @@ function createTUI() {
219
156
  function showInstallScreen() {
220
157
  currentView = 'install';
221
158
  let catalog;
222
- try {
223
- catalog = loadCatalog(connector);
224
- } catch (e) {
225
- log(`{red-fg}Error loading catalog: ${e.message}{/red-fg}`);
226
- return;
227
- }
159
+ try { catalog = loadCatalog(connector); } catch (e) { log('Error: ' + e.message); return; }
228
160
 
229
- const installBox = blessed.box({
230
- top: 0, left: 0, width: '100%', height: '100%',
231
- tags: true,
232
- });
161
+ const box = blessed.box({ top: 0, left: 0, width: '100%', height: '100%' });
233
162
 
234
- // Header
235
- const installHeader = blessed.box({
236
- parent: installBox,
237
- top: 0, left: 0, width: '100%', height: 1,
238
- tags: true,
239
- style: { bg: 'blue', fg: 'white', bold: true },
240
- content: ' Install Agent Runtimes',
241
- });
242
-
243
- // Subtitle
244
163
  blessed.box({
245
- parent: installBox,
246
- top: 1, left: 0, width: '100%', height: 2,
247
- tags: true,
248
- content: '\n {gray-fg}Select a runtime and press Enter to install or update.{/gray-fg}',
164
+ parent: box, top: 0, left: 0, width: '100%', height: 1,
165
+ style: { bg: 'blue', fg: 'white', bold: true },
166
+ content: ' Install Agent Runtimes (Enter = install, Esc = back)',
249
167
  });
250
168
 
251
- // Column headers
252
169
  blessed.box({
253
- parent: installBox,
254
- top: 3, left: 0, width: '100%', height: 1,
255
- tags: true,
170
+ parent: box, top: 1, left: 0, width: '100%', height: 1,
256
171
  style: { bg: 'white', fg: 'black' },
257
172
  content: ` ${'AGENT'.padEnd(25)} ${'STATUS'.padEnd(16)} DESCRIPTION`,
258
173
  });
259
174
 
260
- // Install list
261
- const installList = blessed.list({
262
- parent: installBox,
263
- top: 4, left: 0, width: '100%', height: '100%-7',
264
- tags: true, keys: true, vi: true, mouse: true,
175
+ const list = blessed.list({
176
+ parent: box, top: 2, left: 0, width: '100%', height: '100%-4',
177
+ keys: true, vi: true, mouse: true,
265
178
  style: {
266
- selected: { bg: 'blue', fg: 'white', bold: true },
179
+ selected: { bg: 'blue', fg: 'white' },
267
180
  item: { fg: 'white' },
268
181
  },
269
182
  });
270
183
 
271
- // Status bar at bottom
272
184
  const statusBar = blessed.box({
273
- parent: installBox,
274
- bottom: 1, left: 0, width: '100%', height: 1,
275
- tags: true,
276
- content: '',
185
+ parent: box, bottom: 1, left: 0, width: '100%', height: 1,
277
186
  });
278
187
 
279
- // Footer
280
188
  blessed.box({
281
- parent: installBox,
282
- bottom: 0, left: 0, width: '100%', height: 1,
283
- tags: true,
189
+ parent: box, bottom: 0, left: 0, width: '100%', height: 1,
284
190
  style: { bg: 'blue', fg: 'white' },
285
- content: ' {bold}Enter{/bold} Install/Update {bold}Esc{/bold} Back',
191
+ content: ' Enter Install/Update Esc Back',
286
192
  });
287
193
 
288
- function renderCatalog() {
289
- const items = catalog.map(e => {
290
- const st = e.installed
291
- ? '{green-fg}● installed{/green-fg} '
292
- : '{yellow-fg} available{/yellow-fg} ';
293
- const desc = e.description ? `{gray-fg}${e.description.substring(0, 45)}{/gray-fg}` : '';
294
- return ` ${e.label.padEnd(25)} ${st} ${desc}`;
295
- });
296
- installList.setItems(items);
194
+ function renderList() {
195
+ list.setItems(catalog.map(e => {
196
+ const st = e.installed ? '\u25CF installed' : '\u25CB available';
197
+ const desc = e.description ? e.description.substring(0, 40) : '';
198
+ return ` ${e.label.padEnd(25)} ${st.padEnd(16)} ${desc}`;
199
+ }));
297
200
  }
201
+ renderList();
202
+ list.focus();
298
203
 
299
- renderCatalog();
300
- installList.focus();
301
-
302
- installList.on('select', (_item, idx) => {
204
+ list.on('select', (_item, idx) => {
303
205
  const entry = catalog[idx];
304
206
  if (!entry) return;
305
-
306
207
  const verb = entry.installed ? 'Update' : 'Install';
307
- const confirm = blessed.question({
308
- parent: installBox,
309
- top: 'center', left: 'center',
310
- width: 55, height: 7,
208
+
209
+ const dialog = blessed.box({
210
+ parent: box, top: 'center', left: 'center',
211
+ width: 50, height: 5,
311
212
  border: { type: 'line' },
312
- tags: true,
313
- label: ` ${verb} `,
314
- style: { bg: 'black', fg: 'white', border: { fg: 'cyan' }, label: { fg: 'cyan', bold: true } },
213
+ style: { border: { fg: 'cyan' } },
214
+ content: `\n ${verb} ${entry.label}? (y = yes, n = no)`,
315
215
  });
216
+ screen.render();
316
217
 
317
- confirm.ask(`${verb} ${entry.label}? (y/n)`, (_err, ok) => {
318
- confirm.destroy();
319
- if (!ok) { installList.focus(); screen.render(); return; }
320
- doInstall(entry, statusBar, installList, catalog, renderCatalog);
321
- });
218
+ const onKey = (ch) => {
219
+ screen.unkey(['y', 'n', 'escape'], onKey);
220
+ dialog.destroy();
221
+ if (ch === 'y') {
222
+ doInstall(entry, statusBar, list, catalog, renderList);
223
+ } else {
224
+ list.focus();
225
+ }
226
+ screen.render();
227
+ };
228
+ screen.key(['y', 'n', 'escape'], onKey);
322
229
  });
323
230
 
324
- installList.key('escape', () => {
325
- screen.remove(installBox);
326
- installBox.destroy();
231
+ list.key('escape', () => {
232
+ screen.remove(box);
233
+ box.destroy();
327
234
  currentView = 'main';
328
235
  agentList.focus();
329
236
  refreshAgentTable();
330
237
  });
331
238
 
332
- screen.append(installBox);
333
- installList.focus();
239
+ screen.append(box);
240
+ list.focus();
334
241
  screen.render();
335
242
  }
336
243
 
337
- function doInstall(entry, statusBar, installList, catalog, renderCatalog) {
338
- statusBar.setContent(` {cyan-fg}Installing ${entry.name}...{/cyan-fg}`);
244
+ function doInstall(entry, statusBar, list, catalog, renderList) {
245
+ statusBar.setContent(` Installing ${entry.name}...`);
339
246
  screen.render();
247
+ log('Installing ' + entry.name + '...');
340
248
 
341
- const installer = connector.getInstaller();
342
- const installCmd = installer._resolveInstallCommand(entry.name);
343
- if (!installCmd) {
344
- statusBar.setContent(` {red-fg}✗ No install command for ${entry.name}{/red-fg}`);
345
- screen.render();
346
- installList.focus();
347
- return;
348
- }
349
-
350
- log(`{cyan-fg}$ ${installCmd}{/cyan-fg}`);
351
- statusBar.setContent(` {gray-fg}$ ${installCmd.substring(0, 80)}{/gray-fg}`);
352
- screen.render();
353
-
354
- const env = { ...process.env };
355
- env.npm_config_yes = 'true';
356
- env.CI = '1';
357
-
358
- const extraDirs = getExtraBinDirs();
359
- if (extraDirs.length) {
360
- const sep = IS_WINDOWS ? ';' : ':';
361
- env.PATH = extraDirs.join(sep) + sep + (env.PATH || '');
362
- }
363
-
364
- const proc = spawn(installCmd, [], {
365
- shell: true, env,
366
- stdio: ['ignore', 'pipe', 'pipe'],
367
- });
368
-
369
- let lastLine = '';
370
- let lineCount = 0;
371
- const onData = (data) => {
372
- const lines = data.toString().split('\n').filter(l => l.trim());
249
+ connector.installer.installStreaming(entry.name, (chunk) => {
250
+ const lines = chunk.split('\n').filter(l => l.trim());
373
251
  for (const line of lines) {
374
- lastLine = line.trim().substring(0, 100);
375
- lineCount++;
376
- log(` ${lastLine}`);
377
- statusBar.setContent(` {gray-fg}[${lineCount} lines] ${lastLine.substring(0, 70)}{/gray-fg}`);
252
+ const clean = line.trim().substring(0, 90);
253
+ log(' ' + clean);
254
+ statusBar.setContent(' ' + clean.substring(0, 70));
378
255
  screen.render();
379
256
  }
380
- };
381
- proc.stdout.on('data', onData);
382
- proc.stderr.on('data', onData);
383
-
384
- proc.on('close', (code) => {
385
- if (code === 0) {
386
- statusBar.setContent(` {green-fg}✓ ${entry.name} installed successfully{/green-fg}`);
387
- log(`{green-fg}✓ ${entry.name} installed successfully{/green-fg}`);
388
- // Mark installed
389
- try {
390
- const markerFile = path.join(connector.config?.configDir || '', 'installed_agents.json');
391
- let markers = {};
392
- try { markers = JSON.parse(fs.readFileSync(markerFile, 'utf-8')); } catch {}
393
- markers[entry.name] = { installed_at: new Date().toISOString() };
394
- fs.writeFileSync(markerFile, JSON.stringify(markers, null, 2));
395
- } catch {}
396
- // Refresh
397
- try {
398
- const newCatalog = loadCatalog(connector);
399
- for (let i = 0; i < catalog.length; i++) {
400
- const updated = newCatalog.find(c => c.name === catalog[i].name);
401
- if (updated) catalog[i] = updated;
402
- }
403
- // Also mark the just-installed one
404
- const idx = catalog.findIndex(c => c.name === entry.name);
405
- if (idx >= 0) catalog[idx].installed = true;
406
- renderCatalog();
407
- } catch {}
408
- } else {
409
- statusBar.setContent(` {red-fg}✗ Failed (exit ${code}): ${lastLine.substring(0, 60)}{/red-fg}`);
410
- log(`{red-fg}✗ ${entry.name} install failed (exit ${code}){/red-fg}`);
411
- }
412
- installList.focus();
257
+ }).then(() => {
258
+ statusBar.setContent(` Done! ${entry.name} installed successfully.`);
259
+ statusBar.style.fg = 'green';
260
+ log(entry.name + ' installed successfully');
261
+ const idx = catalog.findIndex(c => c.name === entry.name);
262
+ if (idx >= 0) catalog[idx].installed = true;
263
+ renderList();
264
+ setTimeout(() => { statusBar.style.fg = 'white'; }, 5000);
265
+ list.focus();
266
+ screen.render();
267
+ }).catch((e) => {
268
+ statusBar.setContent(` Failed: ${e.message.substring(0, 60)}`);
269
+ statusBar.style.fg = 'red';
270
+ log('Install failed: ' + e.message);
271
+ setTimeout(() => { statusBar.style.fg = 'white'; }, 5000);
272
+ list.focus();
413
273
  screen.render();
414
274
  });
415
275
  }
416
276
 
417
- // ── New Agent Dialog ──
277
+ // ── New Agent ──
418
278
 
419
279
  function showNewAgentDialog() {
420
280
  const dialog = blessed.box({
421
- top: 'center', left: 'center',
422
- width: 60, height: 16,
281
+ top: 'center', left: 'center', width: 56, height: 13,
423
282
  border: { type: 'line' },
424
- tags: true, keys: true,
425
- label: ' {bold}Create Agent{/bold} ',
426
- style: { border: { fg: 'cyan' }, label: { fg: 'cyan' } },
283
+ style: { border: { fg: 'cyan' } },
284
+ label: ' Create Agent ',
427
285
  });
428
286
 
429
- blessed.text({ parent: dialog, top: 1, left: 2, tags: true, content: '{bold}Name:{/bold}' });
287
+ blessed.text({ parent: dialog, top: 1, left: 2, content: 'Name:' });
430
288
  const nameInput = blessed.textbox({
431
- parent: dialog, top: 2, left: 2, width: 44, height: 3,
432
- border: { type: 'line' },
433
- inputOnFocus: true,
434
- style: { border: { fg: 'gray' }, focus: { border: { fg: 'cyan' } } },
289
+ parent: dialog, top: 2, left: 2, width: 40, height: 3,
290
+ border: { type: 'line' }, inputOnFocus: true,
291
+ style: { focus: { border: { fg: 'cyan' } } },
435
292
  });
436
293
 
437
- blessed.text({ parent: dialog, top: 5, left: 2, tags: true, content: '{bold}Type:{/bold} {gray-fg}(openclaw, claude, codex, aider, goose){/gray-fg}' });
294
+ blessed.text({ parent: dialog, top: 5, left: 2, content: 'Type: (openclaw, claude, codex, aider, goose)' });
438
295
  const typeInput = blessed.textbox({
439
- parent: dialog, top: 6, left: 2, width: 44, height: 3,
440
- border: { type: 'line' },
441
- inputOnFocus: true,
442
- value: 'openclaw',
443
- style: { border: { fg: 'gray' }, focus: { border: { fg: 'cyan' } } },
444
- });
445
-
446
- const statusLabel = blessed.text({ parent: dialog, top: 10, left: 2, tags: true, content: '' });
447
-
448
- const createBtn = blessed.button({
449
- parent: dialog, top: 12, left: 2, width: 14, height: 1,
450
- content: ' Create ', tags: true, mouse: true,
451
- style: { bg: 'blue', fg: 'white', focus: { bg: 'cyan' }, hover: { bg: 'cyan' } },
296
+ parent: dialog, top: 6, left: 2, width: 40, height: 3,
297
+ border: { type: 'line' }, inputOnFocus: true, value: 'openclaw',
298
+ style: { focus: { border: { fg: 'cyan' } } },
452
299
  });
453
300
 
454
- const cancelBtn = blessed.button({
455
- parent: dialog, top: 12, left: 18, width: 14, height: 1,
456
- content: ' Cancel ', tags: true, mouse: true,
457
- style: { bg: 'gray', fg: 'white', focus: { bg: 'red' }, hover: { bg: 'red' } },
458
- });
301
+ const msg = blessed.text({ parent: dialog, top: 10, left: 2, content: '' });
459
302
 
460
- function doCreate() {
303
+ const doCreate = () => {
461
304
  const name = nameInput.getValue().trim();
462
305
  const type = typeInput.getValue().trim();
463
- if (!name) { statusLabel.setContent('{red-fg}Name is required{/red-fg}'); screen.render(); return; }
464
- if (!type) { statusLabel.setContent('{red-fg}Type is required{/red-fg}'); screen.render(); return; }
306
+ if (!name) { msg.setContent('Name is required'); screen.render(); return; }
307
+ if (!type) { msg.setContent('Type is required'); screen.render(); return; }
465
308
  try {
466
- connector.createAgent(name, type);
467
- log(`{green-fg}✓ Agent '${name}' (${type}) created{/green-fg}`);
468
- screen.remove(dialog);
469
- dialog.destroy();
470
- agentList.focus();
471
- refreshAgentTable();
472
- } catch (e) {
473
- statusLabel.setContent(`{red-fg}${e.message}{/red-fg}`);
474
- screen.render();
475
- }
476
- }
309
+ connector.addAgent({ name, type });
310
+ log('Agent ' + name + ' (' + type + ') created');
311
+ } catch (e) { msg.setContent(e.message); screen.render(); return; }
312
+ screen.remove(dialog); dialog.destroy();
313
+ agentList.focus(); refreshAgentTable();
314
+ };
477
315
 
478
- createBtn.on('press', doCreate);
479
- cancelBtn.on('press', () => {
480
- screen.remove(dialog);
481
- dialog.destroy();
482
- agentList.focus();
483
- screen.render();
484
- });
316
+ nameInput.key('enter', () => typeInput.focus());
317
+ typeInput.key('enter', doCreate);
485
318
 
486
319
  dialog.key('escape', () => {
487
- screen.remove(dialog);
488
- dialog.destroy();
489
- agentList.focus();
490
- screen.render();
320
+ screen.remove(dialog); dialog.destroy();
321
+ agentList.focus(); screen.render();
491
322
  });
492
323
 
493
324
  screen.append(dialog);
@@ -495,93 +326,54 @@ function createTUI() {
495
326
  screen.render();
496
327
  }
497
328
 
498
- // ── Keybindings ──
329
+ // ── Keys ──
499
330
 
500
331
  screen.key('q', () => { if (currentView === 'main') process.exit(0); });
501
332
  screen.key('C-c', () => process.exit(0));
502
-
503
333
  screen.key('i', () => { if (currentView === 'main') showInstallScreen(); });
504
334
  screen.key('n', () => { if (currentView === 'main') showNewAgentDialog(); });
505
-
506
- screen.key('r', () => {
507
- if (currentView === 'main') {
508
- refreshAgentTable();
509
- log('Refreshed');
510
- }
511
- });
335
+ screen.key('r', () => { if (currentView === 'main') { refreshAgentTable(); log('Refreshed'); } });
512
336
 
513
337
  screen.key('s', () => {
514
- if (currentView !== 'main') return;
515
- const idx = agentList.selected;
516
- const agent = agentRows[idx];
517
- if (!agent) { log('{yellow-fg}No agent selected{/yellow-fg}'); return; }
518
- try {
519
- connector.startAgent(agent.name);
520
- log(`Starting ${agent.name}...`);
521
- } catch (e) { log(`{red-fg}Error: ${e.message}{/red-fg}`); }
338
+ if (currentView !== 'main' || !agentRows[agentList.selected]) return;
339
+ const a = agentRows[agentList.selected];
340
+ try { connector.sendDaemonCommand('start:' + a.name); log('Starting ' + a.name + '...'); } catch (e) { log('Error: ' + e.message); }
522
341
  setTimeout(refreshAgentTable, 2000);
523
342
  });
524
343
 
525
344
  screen.key('x', () => {
526
- if (currentView !== 'main') return;
527
- const idx = agentList.selected;
528
- const agent = agentRows[idx];
529
- if (!agent) { log('{yellow-fg}No agent selected{/yellow-fg}'); return; }
530
- try {
531
- connector.stopAgent(agent.name);
532
- log(`Stopped ${agent.name}`);
533
- } catch (e) { log(`{red-fg}Error: ${e.message}{/red-fg}`); }
345
+ if (currentView !== 'main' || !agentRows[agentList.selected]) return;
346
+ const a = agentRows[agentList.selected];
347
+ try { connector.sendDaemonCommand('stop:' + a.name); log('Stopped ' + a.name); } catch (e) { log('Error: ' + e.message); }
534
348
  setTimeout(refreshAgentTable, 1000);
535
349
  });
536
350
 
537
351
  screen.key('u', () => {
538
352
  if (currentView !== 'main') return;
539
- const pid = connector.getDaemonPid();
540
- if (pid) {
541
- connector.stopDaemon();
542
- log('Daemon stopped');
543
- } else {
544
- connector.startDaemon();
545
- log('Daemon starting...');
546
- }
353
+ if (connector.getDaemonPid()) { connector.stopDaemon(); log('Daemon stopped'); }
354
+ else { connector.startDaemon(); log('Daemon starting...'); }
547
355
  setTimeout(refreshAgentTable, 2000);
548
356
  });
549
357
 
550
358
  screen.key('c', () => {
551
- if (currentView !== 'main') return;
552
- const idx = agentList.selected;
553
- const agent = agentRows[idx];
554
- if (!agent) { log('{yellow-fg}No agent selected{/yellow-fg}'); return; }
555
- log(`{gray-fg}Use: openagents connect ${agent.name} <workspace-token>{/gray-fg}`);
359
+ if (currentView !== 'main' || !agentRows[agentList.selected]) return;
360
+ log('Use: openagents connect ' + agentRows[agentList.selected].name + ' <token>');
556
361
  });
557
362
 
558
363
  screen.key('d', () => {
559
- if (currentView !== 'main') return;
560
- const idx = agentList.selected;
561
- const agent = agentRows[idx];
562
- if (!agent) { log('{yellow-fg}No agent selected{/yellow-fg}'); return; }
563
- try {
564
- connector.disconnectAgent(agent.name);
565
- log(`Disconnected ${agent.name}`);
566
- } catch (e) { log(`{red-fg}Error: ${e.message}{/red-fg}`); }
364
+ if (currentView !== 'main' || !agentRows[agentList.selected]) return;
365
+ const a = agentRows[agentList.selected];
366
+ try { connector.disconnectWorkspace(a.name); log('Disconnected ' + a.name); } catch (e) { log('Error: ' + e.message); }
567
367
  refreshAgentTable();
568
368
  });
569
369
 
570
370
  // ── Init ──
571
-
572
371
  agentList.focus();
573
372
  refreshAgentTable();
574
- log('Welcome to {bold}OpenAgents{/bold}. Press {bold}i{/bold} to install agents, {bold}n{/bold} to create one.');
575
-
373
+ log('Welcome to OpenAgents. Press i to install agents, n to create one.');
576
374
  setInterval(refreshAgentTable, 5000);
577
375
  screen.render();
578
- return screen;
579
- }
580
-
581
- // ── Entry point ──────────────────────────────────────────────────────────
582
-
583
- function run() {
584
- createTUI();
585
376
  }
586
377
 
378
+ function run() { createTUI(); }
587
379
  module.exports = { run };