@openagents-org/agent-launcher 0.1.0 → 0.1.2

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