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