@openagents-org/agent-launcher 0.1.4 → 0.1.5

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 +1053 -133
package/src/tui.js CHANGED
@@ -1,5 +1,7 @@
1
1
  /**
2
2
  * Interactive TUI dashboard for OpenAgents — `openagents` or `openagents tui`
3
+ *
4
+ * Mirrors the Python Textual TUI (cli_tui.py) with blessed.
3
5
  */
4
6
 
5
7
  'use strict';
@@ -13,27 +15,80 @@ const { getExtraBinDirs } = require('./paths');
13
15
 
14
16
  const IS_WINDOWS = process.platform === 'win32';
15
17
 
18
+ // ── Color palette ───────────────────────────────────────────────────────────
19
+
20
+ const COLORS = {
21
+ primary: 'blue',
22
+ accent: 'cyan',
23
+ surface: 'black',
24
+ headerBg: 'blue',
25
+ headerFg: 'white',
26
+ footerBg: 'blue',
27
+ footerFg: 'white',
28
+ panelBorder: 'cyan',
29
+ logBorder: 'blue',
30
+ colHeaderBg: 'grey',
31
+ colHeaderFg: 'black',
32
+ selected: { bg: 'blue', fg: 'white' },
33
+ stateRunning: 'green',
34
+ stateStopped: 'gray',
35
+ stateError: 'red',
36
+ stateStarting: 'yellow',
37
+ };
38
+
39
+ const STATE_DISPLAY = {
40
+ online: { sym: '\u25CF', color: COLORS.stateRunning },
41
+ running: { sym: '\u25CF', color: COLORS.stateRunning },
42
+ starting: { sym: '\u25D0', color: COLORS.stateStarting },
43
+ reconnecting: { sym: '\u25D0', color: COLORS.stateStarting },
44
+ stopped: { sym: '\u25CB', color: COLORS.stateStopped },
45
+ 'not configured': { sym: '\u25CB', color: COLORS.stateStopped },
46
+ error: { sym: '\u2717', color: COLORS.stateError },
47
+ };
48
+
49
+ function stateMarkup(state) {
50
+ const d = STATE_DISPLAY[state] || { sym: '?', color: 'white' };
51
+ return `{${d.color}-fg}${d.sym} ${state}{/${d.color}-fg}`;
52
+ }
53
+
54
+ // ── Data helpers ────────────────────────────────────────────────────────────
55
+
16
56
  function getConnector() {
17
57
  const configDir = path.join(process.env.HOME || process.env.USERPROFILE || '.', '.openagents');
18
- return new AgentConnector(configDir);
58
+ return new AgentConnector({ configDir });
19
59
  }
20
60
 
21
61
  function loadAgentRows(connector) {
22
62
  const config = connector.config.load();
23
63
  const agents = config.agents || [];
24
- const status = connector.getDaemonStatus() || {};
25
- const agentStatuses = status.agents || {};
64
+ const agentStatuses = connector.getDaemonStatus() || {};
26
65
  const pid = connector.getDaemonPid();
66
+ const networks = config.networks || [];
27
67
  return agents.map(agent => {
28
68
  const info = agentStatuses[agent.name] || {};
29
69
  const state = pid ? (info.state || 'stopped') : 'stopped';
30
70
  let workspace = '';
31
71
  if (agent.network) {
32
- const nets = config.networks || [];
33
- const net = nets.find(n => n.slug === agent.network || n.id === agent.network);
34
- workspace = net ? `${net.slug || net.id} (${net.name || ''})` : agent.network;
72
+ const net = networks.find(n => n.slug === agent.network || n.id === agent.network);
73
+ if (net) {
74
+ const slug = net.slug || net.id;
75
+ const isLocal = (net.endpoint || '').includes('localhost') || (net.endpoint || '').includes('127.0.0.1');
76
+ if (isLocal) workspace = `${net.endpoint}/${slug}`;
77
+ else workspace = `workspace.openagents.org/${slug}`;
78
+ } else {
79
+ workspace = agent.network;
80
+ }
35
81
  }
36
- return { name: agent.name, type: agent.type, state, workspace };
82
+ return {
83
+ name: agent.name,
84
+ type: agent.type || 'openclaw',
85
+ state,
86
+ workspace,
87
+ path: agent.path || '',
88
+ network: agent.network || '',
89
+ lastError: info.last_error || '',
90
+ configured: true,
91
+ };
37
92
  });
38
93
  }
39
94
 
@@ -41,191 +96,372 @@ function loadCatalog(connector) {
41
96
  const entries = connector.registry.getCatalogSync();
42
97
  return entries.map(e => {
43
98
  let installed = false;
44
- try { const { whichBinary } = require('./paths'); installed = !!whichBinary(e.install?.binary || e.name); } catch {}
99
+ try { const { whichBinary } = require('./paths'); installed = !!whichBinary((e.install && e.install.binary) || e.name); } catch {}
45
100
  if (!installed) {
46
101
  try {
47
102
  const f = path.join(connector._configDir, 'installed_agents.json');
48
103
  if (fs.existsSync(f)) installed = !!JSON.parse(fs.readFileSync(f, 'utf-8'))[e.name];
49
104
  } catch {}
50
105
  }
51
- return { name: e.name, label: e.label || e.name, description: e.description || '', installed };
106
+ return {
107
+ name: e.name,
108
+ label: e.label || e.name,
109
+ description: e.description || '',
110
+ installed,
111
+ envConfig: e.env_config || [],
112
+ checkReady: e.check_ready || null,
113
+ loginCommand: (e.check_ready && e.check_ready.login_command) || null,
114
+ };
52
115
  });
53
116
  }
54
117
 
55
- // ── Main TUI ─────────────────────────────────────────────────────────────
118
+ function generateAgentName(type) {
119
+ const adj = ['swift', 'bright', 'calm', 'keen', 'bold'];
120
+ const noun = ['wolf', 'hawk', 'fox', 'bear', 'lynx'];
121
+ const a = adj[Math.floor(Math.random() * adj.length)];
122
+ const n = noun[Math.floor(Math.random() * noun.length)];
123
+ const num = Math.floor(Math.random() * 900) + 100;
124
+ return `${type}-${a}-${n}-${num}`;
125
+ }
126
+
127
+ // ── Main TUI ────────────────────────────────────────────────────────────────
56
128
 
57
129
  function createTUI() {
58
- const screen = blessed.screen({ smartCSR: true, title: 'OpenAgents', fullUnicode: true });
130
+ const screen = blessed.screen({
131
+ smartCSR: true,
132
+ title: 'OpenAgents',
133
+ fullUnicode: true,
134
+ tags: true,
135
+ });
59
136
  const connector = getConnector();
60
137
  let pkg;
61
138
  try { pkg = require('../package.json'); } catch { pkg = { version: '?' }; }
62
139
 
140
+ let agentRows = [];
141
+ let currentView = 'main';
142
+
63
143
  // ── Header ──
64
144
  const header = blessed.box({
65
145
  top: 0, left: 0, width: '100%', height: 1,
66
- style: { bg: 'blue', fg: 'white', bold: true },
146
+ tags: true,
147
+ style: { bg: COLORS.headerBg, fg: COLORS.headerFg, bold: true },
67
148
  });
68
149
 
69
150
  // ── Title ──
70
151
  const titleBox = blessed.box({
71
- top: 1, left: 0, width: '100%', height: 2,
72
- content: ` OpenAgents v${pkg.version}`,
73
- style: { bold: true },
152
+ top: 1, left: 0, width: '100%', height: 1,
153
+ tags: true,
154
+ content: ` {bold}OpenAgents{/bold} {gray-fg}v${pkg.version}{/gray-fg}`,
155
+ style: { fg: 'white' },
156
+ });
157
+
158
+ // ── Agent Panel (bordered) ──
159
+ const agentPanel = blessed.box({
160
+ top: 2, left: 0, width: '100%', height: '60%-1',
161
+ border: { type: 'line' },
162
+ label: ' {bold}Agents{/bold} ',
163
+ tags: true,
164
+ style: { border: { fg: COLORS.panelBorder }, label: { fg: COLORS.accent } },
74
165
  });
75
166
 
76
167
  // ── Column Headers ──
77
168
  const colHeaders = blessed.box({
78
- top: 3, left: 0, width: '100%', height: 1,
79
- style: { bg: 'white', fg: 'black' },
80
- content: ` ${'NAME'.padEnd(22)} ${'TYPE'.padEnd(14)} ${'STATUS'.padEnd(14)} WORKSPACE`,
169
+ parent: agentPanel,
170
+ top: 0, left: 0, width: '100%-2', height: 1,
171
+ tags: true,
172
+ style: { bg: COLORS.colHeaderBg, fg: COLORS.colHeaderFg },
173
+ content: ` ${'NAME'.padEnd(22)} ${'TYPE'.padEnd(14)} ${'STATUS'.padEnd(18)} WORKSPACE`,
81
174
  });
82
175
 
83
176
  // ── Agent List ──
84
177
  const agentList = blessed.list({
85
- top: 4, left: 0, width: '100%', height: '50%-1',
178
+ parent: agentPanel,
179
+ top: 1, left: 0, width: '100%-2', height: '100%-3',
86
180
  keys: true, vi: true, mouse: true,
181
+ tags: true,
87
182
  style: {
88
- selected: { bg: 'blue', fg: 'white' },
183
+ selected: { bg: COLORS.selected.bg, fg: COLORS.selected.fg, bold: true },
89
184
  item: { fg: 'white' },
90
185
  },
91
186
  });
92
187
 
93
- // ── Activity Section ──
94
- const logLabel = blessed.box({
95
- top: '50%+3', left: 0, width: '100%', height: 1,
96
- content: ' ACTIVITY',
97
- style: { bg: 'white', fg: 'black' },
188
+ // ── Log Panel (bordered) ──
189
+ const logPanel = blessed.box({
190
+ top: '60%+1', left: 0, width: '100%', height: '40%-3',
191
+ border: { type: 'line' },
192
+ label: ' {bold}Activity Log{/bold} ',
193
+ tags: true,
194
+ style: { border: { fg: COLORS.logBorder }, label: { fg: COLORS.primary } },
98
195
  });
99
196
 
100
197
  const logContent = blessed.log({
101
- top: '50%+4', left: 0, width: '100%', height: '50%-7',
198
+ parent: logPanel,
199
+ top: 0, left: 0, width: '100%-2', height: '100%-2',
102
200
  scrollable: true, scrollOnInput: true,
103
- padding: { left: 2 },
201
+ tags: true,
104
202
  style: { fg: 'white' },
105
203
  });
106
204
 
107
205
  // ── Footer ──
108
206
  const footer = blessed.box({
109
207
  bottom: 0, left: 0, width: '100%', height: 1,
110
- style: { bg: 'blue', fg: 'white' },
111
- content: ' i Install n New s Start x Stop c Connect u Daemon r Refresh q Quit',
208
+ tags: true,
209
+ style: { bg: COLORS.footerBg, fg: COLORS.footerFg },
112
210
  });
113
211
 
114
212
  screen.append(header);
115
213
  screen.append(titleBox);
116
- screen.append(colHeaders);
117
- screen.append(agentList);
118
- screen.append(logLabel);
119
- screen.append(logContent);
214
+ screen.append(agentPanel);
215
+ screen.append(logPanel);
120
216
  screen.append(footer);
121
217
 
122
- let agentRows = [];
123
- let currentView = 'main';
124
-
218
+ // ── Log helper ──
125
219
  function log(msg) {
126
220
  const ts = new Date().toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });
127
- logContent.log(`${ts} ${msg}`);
221
+ logContent.log(`{gray-fg}${ts}{/gray-fg} ${msg}`);
128
222
  screen.render();
129
223
  }
130
224
 
225
+ // ── Footer rendering (context-aware) ──
226
+ function updateFooter() {
227
+ const agent = agentRows[agentList.selected];
228
+ const parts = [];
229
+
230
+ parts.push('{cyan-fg}i{/cyan-fg} Install');
231
+ parts.push('{cyan-fg}n{/cyan-fg} New');
232
+
233
+ if (agent && agent.configured) {
234
+ const isRunning = ['running', 'online', 'starting', 'reconnecting'].includes(agent.state);
235
+ const isStopped = ['stopped', 'error'].includes(agent.state);
236
+
237
+ if (isStopped) parts.push('{cyan-fg}s{/cyan-fg} Start');
238
+ if (isRunning) parts.push('{cyan-fg}x{/cyan-fg} Stop');
239
+
240
+ const envFields = connector.registry.getEnvFields(agent.type);
241
+ if (envFields && envFields.length > 0) parts.push('{cyan-fg}e{/cyan-fg} Configure');
242
+
243
+ if (!agent.workspace) parts.push('{cyan-fg}c{/cyan-fg} Connect');
244
+ if (agent.workspace) parts.push('{cyan-fg}d{/cyan-fg} Disconnect');
245
+ if (agent.workspace) parts.push('{cyan-fg}w{/cyan-fg} Workspace');
246
+
247
+ parts.push('{cyan-fg}Del{/cyan-fg} Remove');
248
+ }
249
+
250
+ parts.push('{cyan-fg}u{/cyan-fg} Daemon');
251
+ parts.push('{cyan-fg}r{/cyan-fg} Refresh');
252
+ parts.push('{cyan-fg}q{/cyan-fg} Quit');
253
+
254
+ footer.setContent(' ' + parts.join(' '));
255
+ screen.render();
256
+ }
257
+
258
+ // ── Agent table refresh ──
131
259
  function refreshAgentTable() {
260
+ const savedIdx = agentList.selected || 0;
132
261
  try { agentRows = loadAgentRows(connector); } catch { agentRows = []; }
133
262
 
134
- const items = agentRows.length ? agentRows.map(r => {
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.'];
263
+ if (agentRows.length === 0) {
264
+ agentList.setItems([' {gray-fg}No agents configured. Press {bold}i{/bold} to install, {bold}n{/bold} to create.{/gray-fg}']);
265
+ } else {
266
+ const items = agentRows.map(r => {
267
+ const state = stateMarkup(r.state);
268
+ const ws = r.workspace || '{gray-fg}-{/gray-fg}';
269
+ const pathInfo = r.path ? `{gray-fg} ${r.path}{/gray-fg}` : '';
270
+ return ` ${r.name.padEnd(22)} ${r.type.padEnd(14)} ${state.padEnd(30)} ${ws}${pathInfo}`;
271
+ });
272
+ agentList.setItems(items);
273
+ }
274
+
275
+ // Restore cursor position
276
+ if (agentRows.length > 0) {
277
+ agentList.select(Math.min(savedIdx, agentRows.length - 1));
278
+ }
140
279
 
141
- agentList.setItems(items);
142
280
  updateHeader();
281
+ updateFooter();
143
282
  screen.render();
144
283
  }
145
284
 
146
285
  function updateHeader() {
147
286
  const pid = connector.getDaemonPid();
148
- const dot = pid ? '\u25CF' : '\u25CB';
287
+ const dot = pid ? `{green-fg}\u25CF{/green-fg}` : `{gray-fg}\u25CB{/gray-fg}`;
149
288
  const state = pid ? 'Daemon running' : 'Daemon idle';
150
289
  const count = agentRows.length;
151
- header.setContent(` ${dot} ${state} | ${count} agent${count !== 1 ? 's' : ''} configured`);
290
+ header.setContent(` ${dot} ${state} {gray-fg}|{/gray-fg} ${count} agent${count !== 1 ? 's' : ''} configured`);
152
291
  }
153
292
 
154
- // ── Install Screen ──
293
+ // Update footer when selection changes
294
+ agentList.on('select item', () => updateFooter());
295
+
296
+ // ── Enter key → Context menu ──
297
+ agentList.on('select', (_item, idx) => {
298
+ if (currentView !== 'main') return;
299
+ const agent = agentRows[idx];
300
+ if (!agent || !agent.configured) return;
301
+ showAgentActionMenu(agent);
302
+ });
303
+
304
+ // ────────────────────────────────────────────────────────────────────────
305
+ // Agent Action Menu (context menu on Enter)
306
+ // ────────────────────────────────────────────────────────────────────────
307
+
308
+ function showAgentActionMenu(agent) {
309
+ const actions = [];
310
+ const isRunning = ['running', 'online', 'starting', 'reconnecting'].includes(agent.state);
311
+ const isStopped = ['stopped', 'error'].includes(agent.state);
312
+
313
+ const envFields = connector.registry.getEnvFields(agent.type);
314
+ if (envFields && envFields.length > 0) actions.push({ label: 'Configure', key: 'configure' });
315
+
316
+ const catalog = connector.registry.getCatalogSync();
317
+ const entry = catalog.find(e => e.name === agent.type);
318
+ if (entry && entry.check_ready && entry.check_ready.login_command) {
319
+ actions.push({ label: 'Login', key: 'login' });
320
+ }
321
+
322
+ if (isStopped) actions.push({ label: 'Start', key: 'start' });
323
+ if (isRunning) actions.push({ label: 'Stop', key: 'stop' });
324
+ if (agent.workspace) actions.push({ label: 'Open Workspace', key: 'open_workspace' });
325
+ if (!agent.workspace) actions.push({ label: 'Connect to Workspace', key: 'connect' });
326
+ if (agent.workspace) actions.push({ label: 'Disconnect from Workspace', key: 'disconnect' });
327
+ actions.push({ label: 'Remove', key: 'remove' });
328
+
329
+ if (actions.length === 0) return;
330
+
331
+ const listHeight = Math.min(actions.length + 2, 14);
332
+ const dialog = blessed.box({
333
+ top: 'center', left: 'center',
334
+ width: 40, height: listHeight + 2,
335
+ border: { type: 'line' },
336
+ tags: true,
337
+ label: ` {bold}${agent.name}{/bold} `,
338
+ style: { border: { fg: COLORS.accent }, bg: COLORS.surface },
339
+ });
340
+
341
+ const actionList = blessed.list({
342
+ parent: dialog,
343
+ top: 0, left: 1, width: '100%-4', height: listHeight,
344
+ keys: true, vi: true, mouse: true,
345
+ tags: true,
346
+ style: {
347
+ selected: { bg: COLORS.selected.bg, fg: COLORS.selected.fg, bold: true },
348
+ item: { fg: 'white' },
349
+ },
350
+ items: actions.map(a => ` ${a.label}`),
351
+ });
352
+
353
+ screen.append(dialog);
354
+ actionList.focus();
355
+ screen.render();
356
+
357
+ const close = () => {
358
+ screen.remove(dialog);
359
+ dialog.destroy();
360
+ agentList.focus();
361
+ screen.render();
362
+ };
363
+
364
+ actionList.on('select', (_item, idx) => {
365
+ const action = actions[idx];
366
+ close();
367
+ if (!action) return;
368
+ switch (action.key) {
369
+ case 'configure': showConfigureScreen(agent); break;
370
+ case 'login': doLogin(agent); break;
371
+ case 'start': doStart(agent.name); break;
372
+ case 'stop': doStop(agent.name); break;
373
+ case 'open_workspace': doOpenWorkspace(agent); break;
374
+ case 'connect': showConnectWorkspaceScreen(agent.name); break;
375
+ case 'disconnect': doDisconnect(agent.name); break;
376
+ case 'remove': doRemove(agent.name); break;
377
+ }
378
+ });
379
+
380
+ actionList.key('escape', close);
381
+ dialog.key('escape', close);
382
+ }
383
+
384
+ // ────────────────────────────────────────────────────────────────────────
385
+ // Install Screen
386
+ // ────────────────────────────────────────────────────────────────────────
155
387
 
156
388
  function showInstallScreen() {
157
389
  currentView = 'install';
158
390
  let catalog;
159
- try { catalog = loadCatalog(connector); } catch (e) { log('Error: ' + e.message); return; }
391
+ try { catalog = loadCatalog(connector); } catch (e) { log(`{red-fg}Error:{/red-fg} ${e.message}`); return; }
160
392
 
161
- const box = blessed.box({ top: 0, left: 0, width: '100%', height: '100%' });
393
+ const box = blessed.box({ top: 0, left: 0, width: '100%', height: '100%', style: { bg: COLORS.surface } });
162
394
 
163
395
  blessed.box({
164
396
  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)',
397
+ tags: true,
398
+ style: { bg: COLORS.headerBg, fg: COLORS.headerFg, bold: true },
399
+ content: ' {bold}Install Agent Runtimes{/bold} {gray-fg}\u2014 Enter to install, Esc to go back{/gray-fg}',
167
400
  });
168
401
 
169
402
  blessed.box({
170
403
  parent: box, top: 1, left: 0, width: '100%', height: 1,
171
- style: { bg: 'white', fg: 'black' },
172
- content: ` ${'AGENT'.padEnd(25)} ${'STATUS'.padEnd(16)} DESCRIPTION`,
404
+ tags: true,
405
+ style: { bg: COLORS.colHeaderBg, fg: COLORS.colHeaderFg },
406
+ content: ` ${'AGENT'.padEnd(25)} ${'STATUS'.padEnd(18)} DESCRIPTION`,
173
407
  });
174
408
 
175
409
  const list = blessed.list({
176
- parent: box, top: 2, left: 0, width: '100%', height: '100%-4',
410
+ parent: box, top: 2, left: 0, width: '100%', height: '100%-5',
177
411
  keys: true, vi: true, mouse: true,
412
+ tags: true,
178
413
  style: {
179
- selected: { bg: 'blue', fg: 'white' },
414
+ selected: { bg: COLORS.selected.bg, fg: COLORS.selected.fg, bold: true },
180
415
  item: { fg: 'white' },
181
416
  },
182
417
  });
183
418
 
184
419
  const statusBar = blessed.box({
420
+ parent: box, bottom: 2, left: 0, width: '100%', height: 1,
421
+ tags: true,
422
+ });
423
+
424
+ const installLog = blessed.box({
185
425
  parent: box, bottom: 1, left: 0, width: '100%', height: 1,
426
+ tags: true,
427
+ style: { fg: 'grey' },
186
428
  });
187
429
 
188
430
  blessed.box({
189
431
  parent: box, bottom: 0, left: 0, width: '100%', height: 1,
190
- style: { bg: 'blue', fg: 'white' },
191
- content: ' Enter Install/Update Esc Back',
432
+ tags: true,
433
+ style: { bg: COLORS.footerBg, fg: COLORS.footerFg },
434
+ content: ' {cyan-fg}Enter{/cyan-fg} Install/Update {cyan-fg}Esc{/cyan-fg} Back',
192
435
  });
193
436
 
194
437
  function renderList() {
195
438
  list.setItems(catalog.map(e => {
196
- const st = e.installed ? '\u25CF installed' : '\u25CB available';
439
+ const st = e.installed
440
+ ? `{green-fg}\u25CF installed{/green-fg}`
441
+ : `{yellow-fg}\u25CB available{/yellow-fg}`;
197
442
  const desc = e.description ? e.description.substring(0, 40) : '';
198
- return ` ${e.label.padEnd(25)} ${st.padEnd(16)} ${desc}`;
443
+ return ` ${e.label.padEnd(25)} ${st.padEnd(30)} {gray-fg}${desc}{/gray-fg}`;
199
444
  }));
200
445
  }
201
446
  renderList();
202
447
  list.focus();
203
448
 
449
+ let installing = false;
450
+
204
451
  list.on('select', (_item, idx) => {
452
+ if (installing) return;
205
453
  const entry = catalog[idx];
206
454
  if (!entry) return;
207
455
  const verb = entry.installed ? 'Update' : 'Install';
208
456
 
209
- const dialog = blessed.box({
210
- parent: box, top: 'center', left: 'center',
211
- width: 50, height: 5,
212
- border: { type: 'line' },
213
- style: { border: { fg: 'cyan' } },
214
- content: `\n ${verb} ${entry.label}? (y = yes, n = no)`,
215
- });
216
- screen.render();
217
-
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();
457
+ showConfirmDialog(`${verb} ${entry.label}?`, (yes) => {
458
+ if (yes) {
459
+ installing = true;
460
+ doInstall(entry, statusBar, installLog, list, catalog, renderList, () => { installing = false; });
225
461
  }
462
+ list.focus();
226
463
  screen.render();
227
- };
228
- screen.key(['y', 'n', 'escape'], onKey);
464
+ });
229
465
  });
230
466
 
231
467
  list.key('escape', () => {
@@ -241,136 +477,820 @@ function createTUI() {
241
477
  screen.render();
242
478
  }
243
479
 
244
- function doInstall(entry, statusBar, list, catalog, renderList) {
245
- statusBar.setContent(` Installing ${entry.name}...`);
480
+ function doInstall(entry, statusBar, installLog, list, catalog, renderList, onDone) {
481
+ statusBar.setContent(` {cyan-fg}Installing ${entry.name}...{/cyan-fg}`);
246
482
  screen.render();
247
- log('Installing ' + entry.name + '...');
483
+ log(`Installing {cyan-fg}${entry.name}{/cyan-fg}...`);
248
484
 
249
485
  connector.installer.installStreaming(entry.name, (chunk) => {
250
486
  const lines = chunk.split('\n').filter(l => l.trim());
251
487
  for (const line of lines) {
252
488
  const clean = line.trim().substring(0, 90);
253
- log(' ' + clean);
254
- statusBar.setContent(' ' + clean.substring(0, 70));
489
+ log(` {gray-fg}${clean}{/gray-fg}`);
490
+ installLog.setContent(` {gray-fg}${clean.substring(0, 80)}{/gray-fg}`);
255
491
  screen.render();
256
492
  }
257
493
  }).then(() => {
258
- statusBar.setContent(` Done! ${entry.name} installed successfully.`);
259
- statusBar.style.fg = 'green';
260
- log(entry.name + ' installed successfully');
494
+ statusBar.setContent(` {green-fg}\u2713 ${entry.name} installed successfully{/green-fg}`);
495
+ log(`{green-fg}\u2713{/green-fg} ${entry.name} installed`);
261
496
  const idx = catalog.findIndex(c => c.name === entry.name);
262
497
  if (idx >= 0) catalog[idx].installed = true;
263
498
  renderList();
264
- setTimeout(() => { statusBar.style.fg = 'white'; }, 5000);
499
+ installLog.setContent('');
500
+ onDone();
265
501
  list.focus();
266
502
  screen.render();
267
503
  }).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);
504
+ statusBar.setContent(` {red-fg}\u2717 Failed: ${e.message.substring(0, 60)}{/red-fg}`);
505
+ log(`{red-fg}\u2717 Install failed:{/red-fg} ${e.message}`);
506
+ installLog.setContent('');
507
+ onDone();
272
508
  list.focus();
273
509
  screen.render();
274
510
  });
275
511
  }
276
512
 
277
- // ── New Agent ──
513
+ // ────────────────────────────────────────────────────────────────────────
514
+ // Select Agent Type Screen
515
+ // ────────────────────────────────────────────────────────────────────────
516
+
517
+ function showSelectAgentTypeScreen(callback) {
518
+ const catalog = loadCatalog(connector);
519
+ const installed = catalog.filter(e => e.installed);
520
+
521
+ if (installed.length === 0) {
522
+ log('{yellow-fg}No agent runtimes installed. Press i to install one first.{/yellow-fg}');
523
+ return;
524
+ }
278
525
 
279
- function showNewAgentDialog() {
526
+ const dialogHeight = Math.min(installed.length + 4, 16);
280
527
  const dialog = blessed.box({
281
- top: 'center', left: 'center', width: 56, height: 13,
528
+ top: 'center', left: 'center',
529
+ width: 50, height: dialogHeight,
282
530
  border: { type: 'line' },
283
- style: { border: { fg: 'cyan' } },
284
- label: ' Create Agent ',
531
+ tags: true,
532
+ label: ' {bold}Select Agent Type{/bold} ',
533
+ style: { border: { fg: COLORS.accent }, bg: COLORS.surface },
285
534
  });
286
535
 
287
- blessed.text({ parent: dialog, top: 1, left: 2, content: 'Name:' });
536
+ const typeList = blessed.list({
537
+ parent: dialog,
538
+ top: 1, left: 1, width: '100%-4', height: dialogHeight - 4,
539
+ keys: true, vi: true, mouse: true,
540
+ tags: true,
541
+ style: {
542
+ selected: { bg: COLORS.selected.bg, fg: COLORS.selected.fg, bold: true },
543
+ item: { fg: 'white' },
544
+ },
545
+ items: installed.map(e => ` {green-fg}\u2713{/green-fg} ${e.label} {gray-fg}(${e.name}){/gray-fg}`),
546
+ });
547
+
548
+ blessed.box({
549
+ parent: dialog,
550
+ bottom: 0, left: 0, width: '100%-2', height: 1,
551
+ tags: true,
552
+ content: ' {gray-fg}Enter to select, Esc to cancel{/gray-fg}',
553
+ });
554
+
555
+ screen.append(dialog);
556
+ typeList.focus();
557
+ screen.render();
558
+
559
+ const close = () => {
560
+ screen.remove(dialog);
561
+ dialog.destroy();
562
+ agentList.focus();
563
+ screen.render();
564
+ };
565
+
566
+ typeList.on('select', (_item, idx) => {
567
+ const selected = installed[idx];
568
+ close();
569
+ if (selected) callback(selected.name);
570
+ });
571
+
572
+ typeList.key('escape', close);
573
+ dialog.key('escape', close);
574
+ }
575
+
576
+ // ────────────────────────────────────────────────────────────────────────
577
+ // Start Agent Screen (name + working dir)
578
+ // ────────────────────────────────────────────────────────────────────────
579
+
580
+ function showStartAgentScreen(agentType, callback) {
581
+ const defaultName = generateAgentName(agentType);
582
+ const defaultPath = process.cwd();
583
+
584
+ const dialog = blessed.box({
585
+ top: 'center', left: 'center',
586
+ width: 60, height: 15,
587
+ border: { type: 'line' },
588
+ tags: true,
589
+ label: ` {bold}Start ${agentType} Agent{/bold} `,
590
+ style: { border: { fg: COLORS.accent }, bg: COLORS.surface },
591
+ });
592
+
593
+ blessed.text({ parent: dialog, top: 1, left: 2, tags: true, content: '{bold}Agent name:{/bold}' });
288
594
  const nameInput = blessed.textbox({
289
- parent: dialog, top: 2, left: 2, width: 40, height: 3,
595
+ parent: dialog, top: 2, left: 2, width: 50, height: 3,
596
+ border: { type: 'line' }, inputOnFocus: true,
597
+ value: defaultName,
598
+ style: { focus: { border: { fg: COLORS.accent } }, border: { fg: 'grey' } },
599
+ });
600
+
601
+ blessed.text({ parent: dialog, top: 5, left: 2, tags: true, content: '{bold}Working directory:{/bold}' });
602
+ const pathInput = blessed.textbox({
603
+ parent: dialog, top: 6, left: 2, width: 50, height: 3,
290
604
  border: { type: 'line' }, inputOnFocus: true,
291
- style: { focus: { border: { fg: 'cyan' } } },
605
+ value: defaultPath,
606
+ style: { focus: { border: { fg: COLORS.accent } }, border: { fg: 'grey' } },
292
607
  });
293
608
 
294
- blessed.text({ parent: dialog, top: 5, left: 2, content: 'Type: (openclaw, claude, codex, aider, goose)' });
295
- const typeInput = blessed.textbox({
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' } } },
609
+ blessed.text({
610
+ parent: dialog, top: 10, left: 2,
611
+ tags: true,
612
+ content: '{gray-fg}Enter to confirm, Escape to cancel{/gray-fg}',
299
613
  });
300
614
 
301
- const msg = blessed.text({ parent: dialog, top: 10, left: 2, content: '' });
615
+ const msg = blessed.text({ parent: dialog, top: 11, left: 2, tags: true, content: '' });
302
616
 
303
- const doCreate = () => {
617
+ screen.append(dialog);
618
+ nameInput.focus();
619
+ screen.render();
620
+
621
+ const close = () => {
622
+ screen.remove(dialog);
623
+ dialog.destroy();
624
+ agentList.focus();
625
+ screen.render();
626
+ };
627
+
628
+ nameInput.key('enter', () => pathInput.focus());
629
+ pathInput.key('enter', () => {
304
630
  const name = nameInput.getValue().trim();
305
- const type = typeInput.getValue().trim();
306
- if (!name) { msg.setContent('Name is required'); screen.render(); return; }
307
- if (!type) { msg.setContent('Type is required'); screen.render(); return; }
308
- try {
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();
631
+ const agentPath = pathInput.getValue().trim();
632
+ if (!name) { msg.setContent('{red-fg}Name is required{/red-fg}'); screen.render(); return; }
633
+ close();
634
+ callback({ name, type: agentType, path: agentPath });
635
+ });
636
+
637
+ dialog.key('escape', close);
638
+ nameInput.key('escape', close);
639
+ pathInput.key('escape', close);
640
+ }
641
+
642
+ // ────────────────────────────────────────────────────────────────────────
643
+ // Configure Agent Screen (env vars + LLM test)
644
+ // ────────────────────────────────────────────────────────────────────────
645
+
646
+ function showConfigureScreen(agent) {
647
+ currentView = 'configure';
648
+ const envFields = connector.registry.getEnvFields(agent.type);
649
+ if (!envFields || envFields.length === 0) {
650
+ log('{gray-fg}No configuration required for this agent type.{/gray-fg}');
651
+ return;
652
+ }
653
+
654
+ const saved = connector.getAgentEnv(agent.type);
655
+
656
+ const box = blessed.box({ top: 0, left: 0, width: '100%', height: '100%', style: { bg: COLORS.surface } });
657
+
658
+ blessed.box({
659
+ parent: box, top: 0, left: 0, width: '100%', height: 1,
660
+ tags: true,
661
+ style: { bg: COLORS.headerBg, fg: COLORS.headerFg, bold: true },
662
+ content: ` {bold}Configure ${agent.type}{/bold} {gray-fg}\u2014 Saved to ~/.openagents/env/{/gray-fg}`,
663
+ });
664
+
665
+ const inputs = [];
666
+ let yPos = 2;
667
+
668
+ for (const field of envFields) {
669
+ const current = saved[field.name] || field.default || '';
670
+ const req = field.required ? ' {red-fg}*{/red-fg}' : '';
671
+ const placeholder = field.placeholder || `Enter ${field.name}...`;
672
+
673
+ blessed.text({
674
+ parent: box, top: yPos, left: 2,
675
+ tags: true,
676
+ content: `{bold}${field.description || field.name}{/bold}${req}`,
677
+ });
678
+ yPos++;
679
+
680
+ const input = blessed.textbox({
681
+ parent: box, top: yPos, left: 2, width: '80%', height: 3,
682
+ border: { type: 'line' }, inputOnFocus: true,
683
+ value: current,
684
+ censor: field.password || false,
685
+ style: { focus: { border: { fg: COLORS.accent } }, border: { fg: 'grey' } },
686
+ });
687
+ input._fieldName = field.name;
688
+ inputs.push(input);
689
+ yPos += 3;
690
+ }
691
+
692
+ // Buttons row
693
+ const btnSave = blessed.button({
694
+ parent: box, top: yPos + 1, left: 2,
695
+ width: 12, height: 3,
696
+ border: { type: 'line' },
697
+ tags: true,
698
+ content: ' {bold}Save{/bold}',
699
+ style: { bg: COLORS.primary, fg: 'white', border: { fg: COLORS.accent }, focus: { bg: 'blue' } },
700
+ mouse: true, keys: true,
701
+ });
702
+
703
+ const btnTest = blessed.button({
704
+ parent: box, top: yPos + 1, left: 16,
705
+ width: 12, height: 3,
706
+ border: { type: 'line' },
707
+ tags: true,
708
+ content: ' {bold}Test{/bold}',
709
+ style: { fg: 'white', border: { fg: 'grey' }, focus: { bg: 'blue' } },
710
+ mouse: true, keys: true,
711
+ });
712
+
713
+ const testResult = blessed.text({
714
+ parent: box, top: yPos + 4, left: 2,
715
+ tags: true,
716
+ content: '',
717
+ });
718
+
719
+ blessed.box({
720
+ parent: box, bottom: 0, left: 0, width: '100%', height: 1,
721
+ tags: true,
722
+ style: { bg: COLORS.footerBg, fg: COLORS.footerFg },
723
+ content: ' {cyan-fg}Tab{/cyan-fg} Next field {cyan-fg}Ctrl+S{/cyan-fg} Save {cyan-fg}Ctrl+T{/cyan-fg} Test {cyan-fg}Esc{/cyan-fg} Back',
724
+ });
725
+
726
+ screen.append(box);
727
+ if (inputs.length > 0) inputs[0].focus();
728
+ screen.render();
729
+
730
+ // Tab between fields
731
+ for (let i = 0; i < inputs.length; i++) {
732
+ inputs[i].key('tab', () => {
733
+ const next = (i + 1) % inputs.length;
734
+ inputs[next].focus();
735
+ });
736
+ }
737
+
738
+ function gatherEnv() {
739
+ const env = {};
740
+ for (const input of inputs) {
741
+ const val = input.getValue().trim();
742
+ if (val) env[input._fieldName] = val;
743
+ }
744
+ return env;
745
+ }
746
+
747
+ function doSave() {
748
+ const env = gatherEnv();
749
+ connector.saveAgentEnv(agent.type, env);
750
+ log(`{green-fg}\u2713{/green-fg} Configuration saved for ${agent.type}`);
751
+ closeConfig();
752
+ }
753
+
754
+ function doTest() {
755
+ const env = gatherEnv();
756
+ const resolved = connector.resolveAgentEnv(agent.type, env);
757
+ const effective = { ...env, ...resolved };
758
+
759
+ if (!effective.LLM_API_KEY && !effective.OPENAI_API_KEY && !effective.ANTHROPIC_API_KEY) {
760
+ testResult.setContent('{red-fg}No API key entered{/red-fg}');
761
+ screen.render();
762
+ return;
763
+ }
764
+
765
+ testResult.setContent('{gray-fg}Testing...{/gray-fg}');
766
+ screen.render();
767
+
768
+ connector.testLLM(effective).then(result => {
769
+ if (result.success) {
770
+ testResult.setContent(`{green-fg}\u2713 OK{/green-fg} \u2014 model: ${result.model}, response: ${(result.response || '').substring(0, 50)}`);
771
+ } else {
772
+ testResult.setContent(`{red-fg}\u2717 ${result.error || 'Unknown error'}{/red-fg}`);
773
+ }
774
+ screen.render();
775
+ }).catch(err => {
776
+ testResult.setContent(`{red-fg}\u2717 ${err.message}{/red-fg}`);
777
+ screen.render();
778
+ });
779
+ }
780
+
781
+ function closeConfig() {
782
+ screen.remove(box);
783
+ box.destroy();
784
+ currentView = 'main';
785
+ agentList.focus();
786
+ refreshAgentTable();
787
+ }
788
+
789
+ btnSave.on('press', doSave);
790
+ btnTest.on('press', doTest);
791
+
792
+ box.key('escape', closeConfig);
793
+ box.key('C-s', doSave);
794
+ box.key('C-t', doTest);
795
+ }
796
+
797
+ // ────────────────────────────────────────────────────────────────────────
798
+ // Connect Workspace Screen
799
+ // ────────────────────────────────────────────────────────────────────────
800
+
801
+ function showConnectWorkspaceScreen(agentName) {
802
+ currentView = 'connect';
803
+ const config = connector.config.load();
804
+ const networks = config.networks || [];
805
+
806
+ const box = blessed.box({ top: 0, left: 0, width: '100%', height: '100%', style: { bg: COLORS.surface } });
807
+
808
+ blessed.box({
809
+ parent: box, top: 0, left: 0, width: '100%', height: 1,
810
+ tags: true,
811
+ style: { bg: COLORS.headerBg, fg: COLORS.headerFg, bold: true },
812
+ content: ` {bold}Connect '${agentName}' to Workspace{/bold} {gray-fg}\u2014 Select a workspace and press Enter{/gray-fg}`,
813
+ });
814
+
815
+ blessed.box({
816
+ parent: box, top: 1, left: 0, width: '100%', height: 1,
817
+ tags: true,
818
+ style: { bg: COLORS.colHeaderBg, fg: COLORS.colHeaderFg },
819
+ content: ` ${'WORKSPACE'.padEnd(30)} URL`,
820
+ });
821
+
822
+ const rowActions = [];
823
+ const items = [];
824
+
825
+ for (const net of networks) {
826
+ const name = net.name || net.slug || net.id;
827
+ const slug = net.slug || net.id;
828
+ const isLocal = (net.endpoint || '').includes('localhost') || (net.endpoint || '').includes('127.0.0.1');
829
+ const url = isLocal ? `${net.endpoint}/${slug}` : `https://workspace.openagents.org/${slug}`;
830
+ items.push(` ${name.padEnd(30)} {gray-fg}${url}{/gray-fg}`);
831
+ rowActions.push(`existing:${slug}`);
832
+ }
833
+
834
+ items.push(` {bold}{green-fg}\u271A Create new workspace{/green-fg}{/bold}`);
835
+ rowActions.push('__create__');
836
+ items.push(` {bold}{yellow-fg}\u{1F511} Join with token{/yellow-fg}{/bold}`);
837
+ rowActions.push('__token__');
838
+
839
+ const list = blessed.list({
840
+ parent: box, top: 2, left: 0, width: '100%', height: '100%-4',
841
+ keys: true, vi: true, mouse: true,
842
+ tags: true,
843
+ style: {
844
+ selected: { bg: COLORS.selected.bg, fg: COLORS.selected.fg, bold: true },
845
+ item: { fg: 'white' },
846
+ },
847
+ items,
848
+ });
849
+
850
+ blessed.box({
851
+ parent: box, bottom: 0, left: 0, width: '100%', height: 1,
852
+ tags: true,
853
+ style: { bg: COLORS.footerBg, fg: COLORS.footerFg },
854
+ content: ' {cyan-fg}Enter{/cyan-fg} Select {cyan-fg}Esc{/cyan-fg} Back',
855
+ });
856
+
857
+ screen.append(box);
858
+ list.focus();
859
+ screen.render();
860
+
861
+ const closeScreen = () => {
862
+ screen.remove(box);
863
+ box.destroy();
864
+ currentView = 'main';
865
+ agentList.focus();
866
+ refreshAgentTable();
867
+ };
868
+
869
+ list.on('select', (_item, idx) => {
870
+ const action = rowActions[idx];
871
+ closeScreen();
872
+
873
+ if (action && action.startsWith('existing:')) {
874
+ const slug = action.split(':')[1];
875
+ try {
876
+ connector.connectWorkspace(agentName, slug);
877
+ signalDaemonReload();
878
+ log(`{green-fg}\u2713{/green-fg} Connected {cyan-fg}${agentName}{/cyan-fg} \u2192 ${slug}`);
879
+ } catch (e) {
880
+ log(`{red-fg}\u2717 ${e.message}{/red-fg}`);
881
+ }
882
+ refreshAgentTable();
883
+ } else if (action === '__create__') {
884
+ showTextInputDialog('Workspace name', `${agentName}'s workspace`, (name) => {
885
+ if (!name) return;
886
+ doCreateWorkspace(agentName, name);
887
+ });
888
+ } else if (action === '__token__') {
889
+ showTextInputDialog('Paste workspace token', '', (token) => {
890
+ if (!token) return;
891
+ doJoinToken(agentName, token);
892
+ });
893
+ }
894
+ });
895
+
896
+ list.key('escape', closeScreen);
897
+ }
898
+
899
+ // ────────────────────────────────────────────────────────────────────────
900
+ // Shared dialogs
901
+ // ────────────────────────────────────────────────────────────────────────
902
+
903
+ function showConfirmDialog(message, callback) {
904
+ const dialog = blessed.box({
905
+ top: 'center', left: 'center',
906
+ width: 50, height: 5,
907
+ border: { type: 'line' },
908
+ tags: true,
909
+ style: { border: { fg: COLORS.accent }, bg: COLORS.surface },
910
+ content: `\n ${message}\n {gray-fg}y = yes, n = no{/gray-fg}`,
911
+ });
912
+ screen.append(dialog);
913
+ screen.render();
914
+
915
+ const onKey = (ch) => {
916
+ screen.unkey(['y', 'n', 'escape'], onKey);
917
+ dialog.destroy();
918
+ screen.render();
919
+ callback(ch === 'y');
920
+ };
921
+ screen.key(['y', 'n', 'escape'], onKey);
922
+ }
923
+
924
+ function showTextInputDialog(title, defaultValue, callback) {
925
+ const dialog = blessed.box({
926
+ top: 'center', left: 'center',
927
+ width: 60, height: 8,
928
+ border: { type: 'line' },
929
+ tags: true,
930
+ label: ` {bold}${title}{/bold} `,
931
+ style: { border: { fg: COLORS.accent }, bg: COLORS.surface },
932
+ });
933
+
934
+ const input = blessed.textbox({
935
+ parent: dialog,
936
+ top: 1, left: 2, width: '100%-6', height: 3,
937
+ border: { type: 'line' }, inputOnFocus: true,
938
+ value: defaultValue || '',
939
+ style: { focus: { border: { fg: COLORS.accent } }, border: { fg: 'grey' } },
940
+ });
941
+
942
+ blessed.text({
943
+ parent: dialog, top: 4, left: 2,
944
+ tags: true,
945
+ content: '{gray-fg}Enter to confirm, Escape to cancel{/gray-fg}',
946
+ });
947
+
948
+ screen.append(dialog);
949
+ input.focus();
950
+ screen.render();
951
+
952
+ const close = () => {
953
+ screen.remove(dialog);
954
+ dialog.destroy();
955
+ agentList.focus();
956
+ screen.render();
314
957
  };
315
958
 
316
- nameInput.key('enter', () => typeInput.focus());
317
- typeInput.key('enter', doCreate);
959
+ input.key('enter', () => {
960
+ const val = input.getValue().trim();
961
+ close();
962
+ callback(val || null);
963
+ });
964
+
965
+ input.key('escape', () => {
966
+ close();
967
+ callback(null);
968
+ });
318
969
 
319
970
  dialog.key('escape', () => {
320
- screen.remove(dialog); dialog.destroy();
321
- agentList.focus(); screen.render();
971
+ close();
972
+ callback(null);
973
+ });
974
+ }
975
+
976
+ // ────────────────────────────────────────────────────────────────────────
977
+ // Actions
978
+ // ────────────────────────────────────────────────────────────────────────
979
+
980
+ function signalDaemonReload() {
981
+ try { connector.sendDaemonCommand('reload'); } catch {}
982
+ }
983
+
984
+ function doStart(agentName) {
985
+ log(`Starting {cyan-fg}${agentName}{/cyan-fg}...`);
986
+ const pid = connector.getDaemonPid();
987
+ if (!pid) {
988
+ try {
989
+ connector.startDaemon();
990
+ log(`{green-fg}\u2713{/green-fg} Starting daemon (will launch {cyan-fg}${agentName}{/cyan-fg})`);
991
+ } catch (e) {
992
+ log(`{red-fg}\u2717 Failed to start daemon:{/red-fg} ${e.message}`);
993
+ return;
994
+ }
995
+ } else {
996
+ try {
997
+ connector.sendDaemonCommand(`restart:${agentName}`);
998
+ log(`{green-fg}\u2713{/green-fg} Restarting {cyan-fg}${agentName}{/cyan-fg} via daemon`);
999
+ } catch (e) {
1000
+ log(`{red-fg}\u2717 Failed:{/red-fg} ${e.message}`);
1001
+ return;
1002
+ }
1003
+ }
1004
+ setTimeout(refreshAgentTable, 3000);
1005
+ }
1006
+
1007
+ function doStop(agentName) {
1008
+ log(`Stopping {cyan-fg}${agentName}{/cyan-fg}...`);
1009
+ try {
1010
+ connector.sendDaemonCommand(`stop:${agentName}`);
1011
+ log(`{green-fg}\u2713{/green-fg} Stopped {cyan-fg}${agentName}{/cyan-fg}`);
1012
+ } catch (e) {
1013
+ log(`{red-fg}\u2717{/red-fg} ${e.message}`);
1014
+ }
1015
+ setTimeout(refreshAgentTable, 1000);
1016
+ }
1017
+
1018
+ function doRemove(agentName) {
1019
+ showConfirmDialog(`Remove ${agentName}?`, (yes) => {
1020
+ if (!yes) return;
1021
+ // Disconnect first if connected
1022
+ const agent = agentRows.find(a => a.name === agentName);
1023
+ if (agent && agent.workspace) {
1024
+ try {
1025
+ connector.disconnectWorkspace(agentName);
1026
+ signalDaemonReload();
1027
+ log(`Disconnected {cyan-fg}${agentName}{/cyan-fg}`);
1028
+ } catch {}
1029
+ }
1030
+ // Stop if daemon running
1031
+ const pid = connector.getDaemonPid();
1032
+ if (pid) {
1033
+ try { connector.sendDaemonCommand(`stop:${agentName}`); } catch {}
1034
+ }
1035
+ // Remove from config
1036
+ try {
1037
+ connector.removeAgent(agentName);
1038
+ log(`{green-fg}\u2713{/green-fg} Removed {cyan-fg}${agentName}{/cyan-fg}`);
1039
+ } catch (e) {
1040
+ log(`{red-fg}\u2717{/red-fg} ${e.message}`);
1041
+ }
1042
+ refreshAgentTable();
1043
+ });
1044
+ }
1045
+
1046
+ function doDisconnect(agentName) {
1047
+ try {
1048
+ connector.disconnectWorkspace(agentName);
1049
+ signalDaemonReload();
1050
+ log(`{green-fg}\u2713{/green-fg} Disconnected {cyan-fg}${agentName}{/cyan-fg}`);
1051
+ } catch (e) {
1052
+ log(`{red-fg}\u2717{/red-fg} ${e.message}`);
1053
+ }
1054
+ refreshAgentTable();
1055
+ }
1056
+
1057
+ function doOpenWorkspace(agent) {
1058
+ const config = connector.config.load();
1059
+ const networks = config.networks || [];
1060
+ const net = networks.find(n => n.slug === agent.network || n.id === agent.network);
1061
+ if (!net) {
1062
+ log('{yellow-fg}No workspace config found{/yellow-fg}');
1063
+ return;
1064
+ }
1065
+ const slug = net.slug || net.id;
1066
+ const isLocal = (net.endpoint || '').includes('localhost') || (net.endpoint || '').includes('127.0.0.1');
1067
+ let url;
1068
+ if (isLocal) {
1069
+ url = `${net.endpoint}/${slug}`;
1070
+ } else {
1071
+ url = `https://workspace.openagents.org/${slug}`;
1072
+ }
1073
+ if (net.token) url += `?token=${net.token}`;
1074
+
1075
+ // Try opening in browser
1076
+ let opened = false;
1077
+ try {
1078
+ const { exec } = require('child_process');
1079
+ const cmd = IS_WINDOWS ? `start "${url}"` :
1080
+ process.platform === 'darwin' ? `open "${url}"` :
1081
+ `xdg-open "${url}"`;
1082
+ exec(cmd);
1083
+ opened = true;
1084
+ } catch {}
1085
+
1086
+ // Show URL in a dialog
1087
+ const dialog = blessed.box({
1088
+ top: 'center', left: 'center',
1089
+ width: 70, height: 7,
1090
+ border: { type: 'line' },
1091
+ tags: true,
1092
+ label: ' {bold}Workspace URL{/bold} ',
1093
+ style: { border: { fg: COLORS.accent }, bg: COLORS.surface },
1094
+ content: `\n ${url}\n\n {gray-fg}${opened ? 'Opened in browser.' : 'Copy the URL above.'} Press Esc to close.{/gray-fg}`,
322
1095
  });
323
1096
 
324
1097
  screen.append(dialog);
325
- nameInput.focus();
326
1098
  screen.render();
1099
+
1100
+ const close = () => {
1101
+ screen.remove(dialog);
1102
+ dialog.destroy();
1103
+ agentList.focus();
1104
+ screen.render();
1105
+ };
1106
+ screen.key(['escape', 'enter'], function handler() {
1107
+ screen.unkey(['escape', 'enter'], handler);
1108
+ close();
1109
+ });
1110
+
1111
+ if (opened) log(`{green-fg}\u2713{/green-fg} Opened workspace in browser`);
1112
+ }
1113
+
1114
+ function doLogin(agent) {
1115
+ const catalog = connector.registry.getCatalogSync();
1116
+ const entry = catalog.find(e => e.name === agent.type);
1117
+ if (!entry || !entry.check_ready || !entry.check_ready.login_command) {
1118
+ log('{yellow-fg}No login command for this agent type{/yellow-fg}');
1119
+ return;
1120
+ }
1121
+ const cmd = entry.check_ready.login_command;
1122
+ log(`Running {bold}${cmd}{/bold}...`);
1123
+
1124
+ // Suspend TUI and run login command interactively
1125
+ screen.exec(cmd, {}, (err, ok) => {
1126
+ if (err) {
1127
+ log(`{red-fg}\u2717 Login error:{/red-fg} ${err.message}`);
1128
+ } else {
1129
+ log(`{green-fg}\u2713{/green-fg} Login completed`);
1130
+ }
1131
+ refreshAgentTable();
1132
+ });
1133
+ }
1134
+
1135
+ function doCreateWorkspace(agentName, wsName) {
1136
+ log(`Creating workspace {bold}${wsName}{/bold}...`);
1137
+ connector.createWorkspace({ agentName, name: wsName }).then(result => {
1138
+ const slug = result.slug || result.workspaceId;
1139
+ // Save to config
1140
+ connector.config.addNetwork({
1141
+ id: result.workspaceId,
1142
+ slug,
1143
+ name: wsName,
1144
+ endpoint: connector.workspace.endpoint,
1145
+ token: result.token,
1146
+ });
1147
+ connector.connectWorkspace(agentName, slug);
1148
+ signalDaemonReload();
1149
+ log(`{green-fg}\u2713{/green-fg} Created & connected \u2192 ${result.url || slug}`);
1150
+ refreshAgentTable();
1151
+ }).catch(e => {
1152
+ log(`{red-fg}\u2717 Create failed:{/red-fg} ${e.message}`);
1153
+ });
1154
+ }
1155
+
1156
+ function doJoinToken(agentName, token) {
1157
+ log('Joining workspace with token...');
1158
+ connector.resolveToken(token).then(info => {
1159
+ const slug = info.slug || info.workspace_id;
1160
+ connector.config.addNetwork({
1161
+ id: info.workspace_id,
1162
+ slug,
1163
+ name: info.name || slug,
1164
+ endpoint: connector.workspace.endpoint,
1165
+ token,
1166
+ });
1167
+ connector.connectWorkspace(agentName, slug);
1168
+ signalDaemonReload();
1169
+ log(`{green-fg}\u2713{/green-fg} Joined & connected {cyan-fg}${agentName}{/cyan-fg} \u2192 ${slug}`);
1170
+ refreshAgentTable();
1171
+ }).catch(e => {
1172
+ log(`{red-fg}\u2717 Join failed:{/red-fg} ${e.message}`);
1173
+ });
327
1174
  }
328
1175
 
329
- // ── Keys ──
1176
+ // ────────────────────────────────────────────────────────────────────────
1177
+ // Key bindings
1178
+ // ────────────────────────────────────────────────────────────────────────
330
1179
 
331
1180
  screen.key('q', () => { if (currentView === 'main') process.exit(0); });
332
1181
  screen.key('C-c', () => process.exit(0));
1182
+
333
1183
  screen.key('i', () => { if (currentView === 'main') showInstallScreen(); });
334
- screen.key('n', () => { if (currentView === 'main') showNewAgentDialog(); });
335
- screen.key('r', () => { if (currentView === 'main') { refreshAgentTable(); log('Refreshed'); } });
1184
+
1185
+ screen.key('n', () => {
1186
+ if (currentView !== 'main') return;
1187
+ showSelectAgentTypeScreen((type) => {
1188
+ showStartAgentScreen(type, (result) => {
1189
+ try {
1190
+ connector.addAgent({ name: result.name, type: result.type, path: result.path });
1191
+ log(`{green-fg}\u2713{/green-fg} Created agent {cyan-fg}${result.name}{/cyan-fg} (${result.type})`);
1192
+
1193
+ // Start daemon if not running
1194
+ const pid = connector.getDaemonPid();
1195
+ if (!pid) {
1196
+ connector.startDaemon();
1197
+ log('{green-fg}\u2713{/green-fg} Daemon starting...');
1198
+ } else {
1199
+ signalDaemonReload();
1200
+ }
1201
+ } catch (e) {
1202
+ log(`{red-fg}\u2717{/red-fg} ${e.message}`);
1203
+ }
1204
+ setTimeout(refreshAgentTable, 3000);
1205
+ });
1206
+ });
1207
+ });
1208
+
1209
+ screen.key('r', () => {
1210
+ if (currentView === 'main') {
1211
+ refreshAgentTable();
1212
+ log('{green-fg}\u2713{/green-fg} Refreshed');
1213
+ }
1214
+ });
336
1215
 
337
1216
  screen.key('s', () => {
338
1217
  if (currentView !== 'main' || !agentRows[agentList.selected]) return;
339
1218
  const a = agentRows[agentList.selected];
340
- try { connector.sendDaemonCommand('start:' + a.name); log('Starting ' + a.name + '...'); } catch (e) { log('Error: ' + e.message); }
341
- setTimeout(refreshAgentTable, 2000);
1219
+ if (!a.configured) return;
1220
+ doStart(a.name);
342
1221
  });
343
1222
 
344
1223
  screen.key('x', () => {
345
1224
  if (currentView !== 'main' || !agentRows[agentList.selected]) return;
346
1225
  const a = agentRows[agentList.selected];
347
- try { connector.sendDaemonCommand('stop:' + a.name); log('Stopped ' + a.name); } catch (e) { log('Error: ' + e.message); }
348
- setTimeout(refreshAgentTable, 1000);
1226
+ if (!a.configured) return;
1227
+ doStop(a.name);
349
1228
  });
350
1229
 
351
1230
  screen.key('u', () => {
352
1231
  if (currentView !== 'main') return;
353
- if (connector.getDaemonPid()) { connector.stopDaemon(); log('Daemon stopped'); }
354
- else { connector.startDaemon(); log('Daemon starting...'); }
355
- setTimeout(refreshAgentTable, 2000);
1232
+ const pid = connector.getDaemonPid();
1233
+ if (pid) {
1234
+ showConfirmDialog('Stop daemon? This will disconnect ALL agents.', (yes) => {
1235
+ if (!yes) { log('{gray-fg}Cancelled{/gray-fg}'); return; }
1236
+ try {
1237
+ connector.stopDaemon();
1238
+ log('{green-fg}\u2713{/green-fg} Daemon stopped');
1239
+ } catch (e) {
1240
+ log(`{red-fg}\u2717{/red-fg} ${e.message}`);
1241
+ }
1242
+ setTimeout(refreshAgentTable, 1000);
1243
+ });
1244
+ } else {
1245
+ try {
1246
+ connector.startDaemon();
1247
+ log('{green-fg}\u2713{/green-fg} Daemon starting...');
1248
+ } catch (e) {
1249
+ log(`{red-fg}\u2717{/red-fg} ${e.message}`);
1250
+ }
1251
+ setTimeout(refreshAgentTable, 3000);
1252
+ }
356
1253
  });
357
1254
 
358
1255
  screen.key('c', () => {
359
1256
  if (currentView !== 'main' || !agentRows[agentList.selected]) return;
360
- log('Use: openagents connect ' + agentRows[agentList.selected].name + ' <token>');
1257
+ const a = agentRows[agentList.selected];
1258
+ if (!a.configured || a.workspace) return;
1259
+ showConnectWorkspaceScreen(a.name);
361
1260
  });
362
1261
 
363
1262
  screen.key('d', () => {
364
1263
  if (currentView !== 'main' || !agentRows[agentList.selected]) return;
365
1264
  const a = agentRows[agentList.selected];
366
- try { connector.disconnectWorkspace(a.name); log('Disconnected ' + a.name); } catch (e) { log('Error: ' + e.message); }
367
- refreshAgentTable();
1265
+ if (!a.configured || !a.workspace) return;
1266
+ doDisconnect(a.name);
1267
+ });
1268
+
1269
+ screen.key('w', () => {
1270
+ if (currentView !== 'main' || !agentRows[agentList.selected]) return;
1271
+ const a = agentRows[agentList.selected];
1272
+ if (!a.configured || !a.workspace) return;
1273
+ doOpenWorkspace(a);
1274
+ });
1275
+
1276
+ screen.key('e', () => {
1277
+ if (currentView !== 'main' || !agentRows[agentList.selected]) return;
1278
+ const a = agentRows[agentList.selected];
1279
+ if (!a.configured) return;
1280
+ showConfigureScreen(a);
1281
+ });
1282
+
1283
+ screen.key('delete', () => {
1284
+ if (currentView !== 'main' || !agentRows[agentList.selected]) return;
1285
+ const a = agentRows[agentList.selected];
1286
+ if (!a.configured) return;
1287
+ doRemove(a.name);
368
1288
  });
369
1289
 
370
1290
  // ── Init ──
371
1291
  agentList.focus();
372
1292
  refreshAgentTable();
373
- log('Welcome to OpenAgents. Press i to install agents, n to create one.');
1293
+ log('Welcome to {bold}OpenAgents{/bold}. Press {cyan-fg}i{/cyan-fg} to install agents, {cyan-fg}n{/cyan-fg} to create one.');
374
1294
  setInterval(refreshAgentTable, 5000);
375
1295
  screen.render();
376
1296
  }