@openagents-org/agent-launcher 0.1.3 → 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 +1059 -169
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,219 +15,453 @@ 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
- const config = connector.getConfig();
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
 
40
95
  function loadCatalog(connector) {
41
- const registry = connector.getRegistry();
42
- return registry.list().map(e => {
96
+ const entries = connector.registry.getCatalogSync();
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
- const f = path.join(connector.config?.configDir || '', 'installed_agents.json');
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}`);
222
+ screen.render();
223
+ }
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(' '));
128
255
  screen.render();
129
256
  }
130
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`);
291
+ }
292
+
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);
152
382
  }
153
383
 
154
- // ── Install Screen ──
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,166 +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();
483
+ log(`Installing {cyan-fg}${entry.name}{/cyan-fg}...`);
247
484
 
248
- const installer = connector.getInstaller();
249
- const cmd = installer._resolveInstallCommand(entry.name);
250
- if (!cmd) {
251
- statusBar.setContent(` Error: No install command for ${entry.name}`);
485
+ connector.installer.installStreaming(entry.name, (chunk) => {
486
+ const lines = chunk.split('\n').filter(l => l.trim());
487
+ for (const line of lines) {
488
+ const clean = line.trim().substring(0, 90);
489
+ log(` {gray-fg}${clean}{/gray-fg}`);
490
+ installLog.setContent(` {gray-fg}${clean.substring(0, 80)}{/gray-fg}`);
491
+ screen.render();
492
+ }
493
+ }).then(() => {
494
+ statusBar.setContent(` {green-fg}\u2713 ${entry.name} installed successfully{/green-fg}`);
495
+ log(`{green-fg}\u2713{/green-fg} ${entry.name} installed`);
496
+ const idx = catalog.findIndex(c => c.name === entry.name);
497
+ if (idx >= 0) catalog[idx].installed = true;
498
+ renderList();
499
+ installLog.setContent('');
500
+ onDone();
501
+ list.focus();
252
502
  screen.render();
503
+ }).catch((e) => {
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();
253
508
  list.focus();
509
+ screen.render();
510
+ });
511
+ }
512
+
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}');
254
523
  return;
255
524
  }
256
525
 
257
- log('$ ' + cmd);
258
- statusBar.setContent(' Running: ' + cmd.substring(0, 70));
526
+ const dialogHeight = Math.min(installed.length + 4, 16);
527
+ const dialog = blessed.box({
528
+ top: 'center', left: 'center',
529
+ width: 50, height: dialogHeight,
530
+ border: { type: 'line' },
531
+ tags: true,
532
+ label: ' {bold}Select Agent Type{/bold} ',
533
+ style: { border: { fg: COLORS.accent }, bg: COLORS.surface },
534
+ });
535
+
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}' });
594
+ const nameInput = blessed.textbox({
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,
604
+ border: { type: 'line' }, inputOnFocus: true,
605
+ value: defaultPath,
606
+ style: { focus: { border: { fg: COLORS.accent } }, border: { fg: 'grey' } },
607
+ });
608
+
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}',
613
+ });
614
+
615
+ const msg = blessed.text({ parent: dialog, top: 11, left: 2, tags: true, content: '' });
616
+
617
+ screen.append(dialog);
618
+ nameInput.focus();
259
619
  screen.render();
260
620
 
261
- const env = { ...process.env, npm_config_yes: 'true', CI: '1' };
262
- const extra = getExtraBinDirs();
263
- if (extra.length) {
264
- env.PATH = extra.join(IS_WINDOWS ? ';' : ':') + (IS_WINDOWS ? ';' : ':') + (env.PATH || '');
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', () => {
630
+ const name = nameInput.getValue().trim();
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;
265
652
  }
266
653
 
267
- const proc = spawn(cmd, [], { shell: true, env, stdio: ['ignore', 'pipe', 'pipe'] });
268
- let lines = 0;
654
+ const saved = connector.getAgentEnv(agent.type);
269
655
 
270
- const onData = (data) => {
271
- data.toString().split('\n').filter(l => l.trim()).forEach(line => {
272
- lines++;
273
- const clean = line.trim().substring(0, 90);
274
- log(' ' + clean);
275
- statusBar.setContent(` [${lines}] ${clean.substring(0, 70)}`);
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}`);
276
777
  screen.render();
277
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();
278
867
  };
279
- proc.stdout.on('data', onData);
280
- proc.stderr.on('data', onData);
281
-
282
- proc.on('close', (code) => {
283
- if (code === 0) {
284
- statusBar.setContent(` Done! ${entry.name} installed successfully.`);
285
- statusBar.style.fg = 'green';
286
- log(entry.name + ' installed successfully');
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];
287
875
  try {
288
- const f = path.join(connector.config?.configDir || '', 'installed_agents.json');
289
- let m = {}; try { m = JSON.parse(fs.readFileSync(f, 'utf-8')); } catch {}
290
- m[entry.name] = { installed_at: new Date().toISOString() };
291
- fs.writeFileSync(f, JSON.stringify(m, null, 2));
292
- } catch {}
293
- const idx = catalog.findIndex(c => c.name === entry.name);
294
- if (idx >= 0) catalog[idx].installed = true;
295
- renderList();
296
- } else {
297
- statusBar.setContent(` Failed (exit ${code})`);
298
- statusBar.style.fg = 'red';
299
- log(entry.name + ' install failed (exit ' + code + ')');
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
+ });
300
893
  }
301
- setTimeout(() => { statusBar.style.fg = 'white'; }, 5000);
302
- list.focus();
303
- screen.render();
304
894
  });
895
+
896
+ list.key('escape', closeScreen);
305
897
  }
306
898
 
307
- // ── New Agent ──
899
+ // ────────────────────────────────────────────────────────────────────────
900
+ // Shared dialogs
901
+ // ────────────────────────────────────────────────────────────────────────
308
902
 
309
- function showNewAgentDialog() {
903
+ function showConfirmDialog(message, callback) {
310
904
  const dialog = blessed.box({
311
- top: 'center', left: 'center', width: 56, height: 13,
905
+ top: 'center', left: 'center',
906
+ width: 50, height: 5,
312
907
  border: { type: 'line' },
313
- style: { border: { fg: 'cyan' } },
314
- label: ' Create Agent ',
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}`,
315
911
  });
912
+ screen.append(dialog);
913
+ screen.render();
316
914
 
317
- blessed.text({ parent: dialog, top: 1, left: 2, content: 'Name:' });
318
- const nameInput = blessed.textbox({
319
- parent: dialog, top: 2, left: 2, width: 40, height: 3,
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,
320
937
  border: { type: 'line' }, inputOnFocus: true,
321
- style: { focus: { border: { fg: 'cyan' } } },
938
+ value: defaultValue || '',
939
+ style: { focus: { border: { fg: COLORS.accent } }, border: { fg: 'grey' } },
322
940
  });
323
941
 
324
- blessed.text({ parent: dialog, top: 5, left: 2, content: 'Type: (openclaw, claude, codex, aider, goose)' });
325
- const typeInput = blessed.textbox({
326
- parent: dialog, top: 6, left: 2, width: 40, height: 3,
327
- border: { type: 'line' }, inputOnFocus: true, value: 'openclaw',
328
- style: { focus: { border: { fg: 'cyan' } } },
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}',
329
946
  });
330
947
 
331
- const msg = blessed.text({ parent: dialog, top: 10, left: 2, content: '' });
948
+ screen.append(dialog);
949
+ input.focus();
950
+ screen.render();
332
951
 
333
- const doCreate = () => {
334
- const name = nameInput.getValue().trim();
335
- const type = typeInput.getValue().trim();
336
- if (!name) { msg.setContent('Name is required'); screen.render(); return; }
337
- if (!type) { msg.setContent('Type is required'); screen.render(); return; }
338
- try {
339
- connector.createAgent(name, type);
340
- log('Agent ' + name + ' (' + type + ') created');
341
- } catch (e) { msg.setContent(e.message); screen.render(); return; }
342
- screen.remove(dialog); dialog.destroy();
343
- agentList.focus(); refreshAgentTable();
952
+ const close = () => {
953
+ screen.remove(dialog);
954
+ dialog.destroy();
955
+ agentList.focus();
956
+ screen.render();
344
957
  };
345
958
 
346
- nameInput.key('enter', () => typeInput.focus());
347
- 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
+ });
348
969
 
349
970
  dialog.key('escape', () => {
350
- screen.remove(dialog); dialog.destroy();
351
- 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}`,
352
1095
  });
353
1096
 
354
1097
  screen.append(dialog);
355
- nameInput.focus();
356
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`);
357
1112
  }
358
1113
 
359
- // ── Keys ──
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
+ });
1174
+ }
1175
+
1176
+ // ────────────────────────────────────────────────────────────────────────
1177
+ // Key bindings
1178
+ // ────────────────────────────────────────────────────────────────────────
360
1179
 
361
1180
  screen.key('q', () => { if (currentView === 'main') process.exit(0); });
362
1181
  screen.key('C-c', () => process.exit(0));
1182
+
363
1183
  screen.key('i', () => { if (currentView === 'main') showInstallScreen(); });
364
- screen.key('n', () => { if (currentView === 'main') showNewAgentDialog(); });
365
- 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
+ });
366
1215
 
367
1216
  screen.key('s', () => {
368
1217
  if (currentView !== 'main' || !agentRows[agentList.selected]) return;
369
1218
  const a = agentRows[agentList.selected];
370
- try { connector.startAgent(a.name); log('Starting ' + a.name + '...'); } catch (e) { log('Error: ' + e.message); }
371
- setTimeout(refreshAgentTable, 2000);
1219
+ if (!a.configured) return;
1220
+ doStart(a.name);
372
1221
  });
373
1222
 
374
1223
  screen.key('x', () => {
375
1224
  if (currentView !== 'main' || !agentRows[agentList.selected]) return;
376
1225
  const a = agentRows[agentList.selected];
377
- try { connector.stopAgent(a.name); log('Stopped ' + a.name); } catch (e) { log('Error: ' + e.message); }
378
- setTimeout(refreshAgentTable, 1000);
1226
+ if (!a.configured) return;
1227
+ doStop(a.name);
379
1228
  });
380
1229
 
381
1230
  screen.key('u', () => {
382
1231
  if (currentView !== 'main') return;
383
- if (connector.getDaemonPid()) { connector.stopDaemon(); log('Daemon stopped'); }
384
- else { connector.startDaemon(); log('Daemon starting...'); }
385
- 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
+ }
386
1253
  });
387
1254
 
388
1255
  screen.key('c', () => {
389
1256
  if (currentView !== 'main' || !agentRows[agentList.selected]) return;
390
- 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);
391
1260
  });
392
1261
 
393
1262
  screen.key('d', () => {
394
1263
  if (currentView !== 'main' || !agentRows[agentList.selected]) return;
395
1264
  const a = agentRows[agentList.selected];
396
- try { connector.disconnectAgent(a.name); log('Disconnected ' + a.name); } catch (e) { log('Error: ' + e.message); }
397
- 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);
398
1288
  });
399
1289
 
400
1290
  // ── Init ──
401
1291
  agentList.focus();
402
1292
  refreshAgentTable();
403
- 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.');
404
1294
  setInterval(refreshAgentTable, 5000);
405
1295
  screen.render();
406
1296
  }