@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.
- package/package.json +1 -1
- 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.
|
|
62
|
+
const config = connector.config.load();
|
|
23
63
|
const agents = config.agents || [];
|
|
24
|
-
const
|
|
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
|
|
33
|
-
|
|
34
|
-
|
|
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 {
|
|
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
|
|
42
|
-
return
|
|
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
|
|
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.
|
|
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 {
|
|
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
|
-
|
|
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({
|
|
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
|
-
|
|
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:
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
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:
|
|
183
|
+
selected: { bg: COLORS.selected.bg, fg: COLORS.selected.fg, bold: true },
|
|
89
184
|
item: { fg: 'white' },
|
|
90
185
|
},
|
|
91
186
|
});
|
|
92
187
|
|
|
93
|
-
// ──
|
|
94
|
-
const
|
|
95
|
-
top: '
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
198
|
+
parent: logPanel,
|
|
199
|
+
top: 0, left: 0, width: '100%-2', height: '100%-2',
|
|
102
200
|
scrollable: true, scrollOnInput: true,
|
|
103
|
-
|
|
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
|
-
|
|
111
|
-
|
|
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(
|
|
117
|
-
screen.append(
|
|
118
|
-
screen.append(logLabel);
|
|
119
|
-
screen.append(logContent);
|
|
214
|
+
screen.append(agentPanel);
|
|
215
|
+
screen.append(logPanel);
|
|
120
216
|
screen.append(footer);
|
|
121
217
|
|
|
122
|
-
|
|
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(
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
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 ?
|
|
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
|
-
//
|
|
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(
|
|
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
|
-
|
|
166
|
-
|
|
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
|
-
|
|
172
|
-
|
|
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%-
|
|
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:
|
|
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
|
-
|
|
191
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
|
|
258
|
-
|
|
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
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
|
268
|
-
let lines = 0;
|
|
654
|
+
const saved = connector.getAgentEnv(agent.type);
|
|
269
655
|
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
//
|
|
899
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
900
|
+
// Shared dialogs
|
|
901
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
308
902
|
|
|
309
|
-
function
|
|
903
|
+
function showConfirmDialog(message, callback) {
|
|
310
904
|
const dialog = blessed.box({
|
|
311
|
-
top: 'center', left: 'center',
|
|
905
|
+
top: 'center', left: 'center',
|
|
906
|
+
width: 50, height: 5,
|
|
312
907
|
border: { type: 'line' },
|
|
313
|
-
|
|
314
|
-
|
|
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
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
-
|
|
938
|
+
value: defaultValue || '',
|
|
939
|
+
style: { focus: { border: { fg: COLORS.accent } }, border: { fg: 'grey' } },
|
|
322
940
|
});
|
|
323
941
|
|
|
324
|
-
blessed.text({
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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
|
-
|
|
948
|
+
screen.append(dialog);
|
|
949
|
+
input.focus();
|
|
950
|
+
screen.render();
|
|
332
951
|
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
-
|
|
347
|
-
|
|
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
|
-
|
|
351
|
-
|
|
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
|
-
|
|
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
|
-
|
|
365
|
-
screen.key('
|
|
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
|
-
|
|
371
|
-
|
|
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
|
-
|
|
378
|
-
|
|
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
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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
|
-
|
|
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
|
-
|
|
397
|
-
|
|
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
|
}
|