@covibes/zeroshot 1.0.1
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/CHANGELOG.md +167 -0
- package/LICENSE +21 -0
- package/README.md +364 -0
- package/cli/index.js +3990 -0
- package/cluster-templates/base-templates/debug-workflow.json +181 -0
- package/cluster-templates/base-templates/full-workflow.json +455 -0
- package/cluster-templates/base-templates/single-worker.json +48 -0
- package/cluster-templates/base-templates/worker-validator.json +131 -0
- package/cluster-templates/conductor-bootstrap.json +122 -0
- package/cluster-templates/conductor-junior-bootstrap.json +69 -0
- package/docker/zeroshot-cluster/Dockerfile +132 -0
- package/lib/completion.js +174 -0
- package/lib/id-detector.js +53 -0
- package/lib/settings.js +97 -0
- package/lib/stream-json-parser.js +236 -0
- package/package.json +121 -0
- package/src/agent/agent-config.js +121 -0
- package/src/agent/agent-context-builder.js +241 -0
- package/src/agent/agent-hook-executor.js +329 -0
- package/src/agent/agent-lifecycle.js +555 -0
- package/src/agent/agent-stuck-detector.js +256 -0
- package/src/agent/agent-task-executor.js +1034 -0
- package/src/agent/agent-trigger-evaluator.js +67 -0
- package/src/agent-wrapper.js +459 -0
- package/src/agents/git-pusher-agent.json +20 -0
- package/src/attach/attach-client.js +438 -0
- package/src/attach/attach-server.js +543 -0
- package/src/attach/index.js +35 -0
- package/src/attach/protocol.js +220 -0
- package/src/attach/ring-buffer.js +121 -0
- package/src/attach/socket-discovery.js +242 -0
- package/src/claude-task-runner.js +468 -0
- package/src/config-router.js +80 -0
- package/src/config-validator.js +598 -0
- package/src/github.js +103 -0
- package/src/isolation-manager.js +1042 -0
- package/src/ledger.js +429 -0
- package/src/logic-engine.js +223 -0
- package/src/message-bus-bridge.js +139 -0
- package/src/message-bus.js +202 -0
- package/src/name-generator.js +232 -0
- package/src/orchestrator.js +1938 -0
- package/src/schemas/sub-cluster.js +156 -0
- package/src/sub-cluster-wrapper.js +545 -0
- package/src/task-runner.js +28 -0
- package/src/template-resolver.js +347 -0
- package/src/tui/CHANGES.txt +133 -0
- package/src/tui/LAYOUT.md +261 -0
- package/src/tui/README.txt +192 -0
- package/src/tui/TWO-LEVEL-NAVIGATION.md +186 -0
- package/src/tui/data-poller.js +325 -0
- package/src/tui/demo.js +208 -0
- package/src/tui/formatters.js +123 -0
- package/src/tui/index.js +193 -0
- package/src/tui/keybindings.js +383 -0
- package/src/tui/layout.js +317 -0
- package/src/tui/renderer.js +194 -0
package/src/tui/index.js
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TUI - Main interactive dashboard
|
|
3
|
+
*
|
|
4
|
+
* Coordinates:
|
|
5
|
+
* - Screen and layout
|
|
6
|
+
* - Data polling
|
|
7
|
+
* - Rendering
|
|
8
|
+
* - Keybindings
|
|
9
|
+
* - State management
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const blessed = require('blessed');
|
|
13
|
+
const { createLayout } = require('./layout');
|
|
14
|
+
const DataPoller = require('./data-poller');
|
|
15
|
+
const Renderer = require('./renderer');
|
|
16
|
+
const { setupKeybindings } = require('./keybindings');
|
|
17
|
+
|
|
18
|
+
class TUI {
|
|
19
|
+
constructor(options) {
|
|
20
|
+
this.orchestrator = options.orchestrator;
|
|
21
|
+
this.filter = options.filter || 'running';
|
|
22
|
+
this.refreshRate = options.refreshRate || 1000;
|
|
23
|
+
|
|
24
|
+
// State
|
|
25
|
+
this.clusters = [];
|
|
26
|
+
this.resourceStats = new Map();
|
|
27
|
+
this.messages = [];
|
|
28
|
+
this.selectedIndex = 0;
|
|
29
|
+
this.poller = null;
|
|
30
|
+
this.renderer = null;
|
|
31
|
+
this.widgets = null;
|
|
32
|
+
this.screen = null;
|
|
33
|
+
|
|
34
|
+
// View mode: 'overview' or 'detail'
|
|
35
|
+
this.viewMode = 'overview';
|
|
36
|
+
this.detailClusterId = null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
start() {
|
|
40
|
+
// Create screen
|
|
41
|
+
this.screen = blessed.screen({
|
|
42
|
+
smartCSR: true,
|
|
43
|
+
title: 'Vibe Cluster Watch',
|
|
44
|
+
dockBorders: true,
|
|
45
|
+
fullUnicode: true,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Create layout
|
|
49
|
+
this.widgets = createLayout(this.screen);
|
|
50
|
+
|
|
51
|
+
// Show immediate loading message
|
|
52
|
+
this.widgets.statsBox.setContent('{center}{bold}Loading...{/bold}{/center}');
|
|
53
|
+
this.screen.render();
|
|
54
|
+
|
|
55
|
+
// Create renderer
|
|
56
|
+
this.renderer = new Renderer(this.widgets, this.screen);
|
|
57
|
+
|
|
58
|
+
// Setup keybindings (pass TUI instance for state management)
|
|
59
|
+
setupKeybindings(this.screen, this.widgets, this, this.orchestrator);
|
|
60
|
+
|
|
61
|
+
// Create data poller
|
|
62
|
+
this.poller = new DataPoller(this.orchestrator, {
|
|
63
|
+
refreshRate: this.refreshRate,
|
|
64
|
+
onUpdate: (update) => this._handleUpdate(update),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Initial message
|
|
68
|
+
this.messages.push({
|
|
69
|
+
timestamp: new Date().toISOString(),
|
|
70
|
+
text: 'TUI started. Press ? for help.',
|
|
71
|
+
level: 'info',
|
|
72
|
+
});
|
|
73
|
+
this.renderer.renderLogs(this.messages.slice(-20));
|
|
74
|
+
|
|
75
|
+
// Start polling
|
|
76
|
+
this.poller.start();
|
|
77
|
+
|
|
78
|
+
// Initial render
|
|
79
|
+
this.screen.render();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
_handleUpdate(update) {
|
|
83
|
+
// Update state based on update.type
|
|
84
|
+
switch (update.type) {
|
|
85
|
+
case 'cluster_state':
|
|
86
|
+
// Update cluster list
|
|
87
|
+
this.clusters = update.clusters;
|
|
88
|
+
|
|
89
|
+
// Apply filter
|
|
90
|
+
let filteredClusters = this.clusters;
|
|
91
|
+
if (this.filter === 'running') {
|
|
92
|
+
// For "running" filter, only show truly active (running) clusters
|
|
93
|
+
// Exclude initializing, stopped, failed, etc.
|
|
94
|
+
filteredClusters = this.clusters.filter((c) => c.state === 'running');
|
|
95
|
+
} else if (this.filter !== 'all') {
|
|
96
|
+
// For other specific filters, match exact state
|
|
97
|
+
filteredClusters = this.clusters.filter((c) => c.state === this.filter);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Ensure selectedIndex is valid
|
|
101
|
+
if (this.selectedIndex >= filteredClusters.length) {
|
|
102
|
+
this.selectedIndex = Math.max(0, filteredClusters.length - 1);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Render clusters table
|
|
106
|
+
this.renderer.renderClustersTable(filteredClusters, this.selectedIndex);
|
|
107
|
+
|
|
108
|
+
// Render system stats
|
|
109
|
+
this.renderer.renderSystemStats(this.clusters, this.resourceStats);
|
|
110
|
+
|
|
111
|
+
// Update agent table for selected cluster (ONLY in detail view)
|
|
112
|
+
if (this.viewMode === 'detail' && this.detailClusterId) {
|
|
113
|
+
// In detail view, show agents for the detail cluster
|
|
114
|
+
try {
|
|
115
|
+
const status = this.orchestrator.getStatus(this.detailClusterId);
|
|
116
|
+
this.renderer.renderAgentTable(status.agents, this.resourceStats);
|
|
117
|
+
} catch {
|
|
118
|
+
// Cluster might have been stopped/killed
|
|
119
|
+
this.renderer.renderAgentTable([], this.resourceStats);
|
|
120
|
+
}
|
|
121
|
+
} else if (this.viewMode === 'overview') {
|
|
122
|
+
// In overview view, don't show agents (or show empty)
|
|
123
|
+
this.renderer.renderAgentTable([], this.resourceStats);
|
|
124
|
+
}
|
|
125
|
+
break;
|
|
126
|
+
|
|
127
|
+
case 'resource_stats':
|
|
128
|
+
// Update resource stats
|
|
129
|
+
this.resourceStats = update.stats;
|
|
130
|
+
|
|
131
|
+
// Re-render system stats
|
|
132
|
+
this.renderer.renderSystemStats(this.clusters, this.resourceStats);
|
|
133
|
+
|
|
134
|
+
// Update agent table with new resource stats (ONLY in detail view)
|
|
135
|
+
if (this.viewMode === 'detail' && this.detailClusterId) {
|
|
136
|
+
try {
|
|
137
|
+
const status = this.orchestrator.getStatus(this.detailClusterId);
|
|
138
|
+
this.renderer.renderAgentTable(status.agents, this.resourceStats);
|
|
139
|
+
} catch {
|
|
140
|
+
this.renderer.renderAgentTable([], this.resourceStats);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
break;
|
|
144
|
+
|
|
145
|
+
case 'new_message':
|
|
146
|
+
// Only add messages from the selected cluster
|
|
147
|
+
const selectedClusterId = this.renderer.selectedClusterId;
|
|
148
|
+
if (selectedClusterId && update.clusterId === selectedClusterId) {
|
|
149
|
+
// Add new message to log
|
|
150
|
+
this.messages.push(update.message);
|
|
151
|
+
|
|
152
|
+
// Keep only last 100 messages in memory
|
|
153
|
+
if (this.messages.length > 100) {
|
|
154
|
+
this.messages = this.messages.slice(-100);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Render last 20 messages
|
|
158
|
+
this.renderer.renderLogs(this.messages.slice(-20));
|
|
159
|
+
}
|
|
160
|
+
break;
|
|
161
|
+
|
|
162
|
+
case 'error':
|
|
163
|
+
// Add error to log
|
|
164
|
+
this.messages.push({
|
|
165
|
+
timestamp: new Date().toISOString(),
|
|
166
|
+
text: `✗ ${update.error}`,
|
|
167
|
+
level: 'error',
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
if (this.messages.length > 100) {
|
|
171
|
+
this.messages = this.messages.slice(-100);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
this.renderer.renderLogs(this.messages.slice(-20));
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Render screen
|
|
179
|
+
this.screen.render();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
exit() {
|
|
183
|
+
if (this.poller) {
|
|
184
|
+
this.poller.stop();
|
|
185
|
+
}
|
|
186
|
+
if (this.screen) {
|
|
187
|
+
this.screen.destroy();
|
|
188
|
+
}
|
|
189
|
+
process.exit(0);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
module.exports = TUI;
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keybindings for TUI
|
|
3
|
+
*
|
|
4
|
+
* Handles:
|
|
5
|
+
* - Navigation (up/down, j/k)
|
|
6
|
+
* - Actions (kill, stop, export, logs)
|
|
7
|
+
* - Confirmations for destructive actions
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const blessed = require('blessed');
|
|
11
|
+
const { spawn } = require('child_process');
|
|
12
|
+
|
|
13
|
+
function setupKeybindings(screen, widgets, tui, orchestrator) {
|
|
14
|
+
// Enter - drill into cluster detail view
|
|
15
|
+
screen.key(['enter'], () => {
|
|
16
|
+
if (tui.viewMode === 'overview' && tui.clusters.length > 0) {
|
|
17
|
+
const selectedCluster = tui.clusters[tui.selectedIndex];
|
|
18
|
+
if (selectedCluster) {
|
|
19
|
+
tui.viewMode = 'detail';
|
|
20
|
+
tui.detailClusterId = selectedCluster.id;
|
|
21
|
+
tui.renderer.setSelectedCluster(selectedCluster.id);
|
|
22
|
+
tui.messages = []; // Clear old messages
|
|
23
|
+
|
|
24
|
+
// Update help text
|
|
25
|
+
widgets.helpBar.setContent(
|
|
26
|
+
'{cyan-fg}[Esc]{/} Back {cyan-fg}[k]{/} Kill {cyan-fg}[s]{/} Stop {cyan-fg}[e]{/} Export {cyan-fg}[l]{/} Logs {cyan-fg}[r]{/} Refresh {cyan-fg}[q]{/} Quit'
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
// Switch to detail layout: hide clusters/stats, show agents/logs
|
|
30
|
+
widgets.clustersTable.hide();
|
|
31
|
+
widgets.statsBox.hide();
|
|
32
|
+
widgets.agentTable.show();
|
|
33
|
+
widgets.logsBox.show();
|
|
34
|
+
screen.render();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Escape - back to overview
|
|
40
|
+
screen.key(['escape'], () => {
|
|
41
|
+
if (tui.viewMode === 'detail') {
|
|
42
|
+
tui.viewMode = 'overview';
|
|
43
|
+
tui.detailClusterId = null;
|
|
44
|
+
tui.renderer.setSelectedCluster(null);
|
|
45
|
+
tui.messages = []; // Clear messages
|
|
46
|
+
|
|
47
|
+
// Update help text
|
|
48
|
+
widgets.helpBar.setContent(
|
|
49
|
+
'{cyan-fg}[Enter]{/} View {cyan-fg}[↑/↓]{/} Navigate {cyan-fg}[k]{/} Kill {cyan-fg}[s]{/} Stop {cyan-fg}[l]{/} Logs {cyan-fg}[r]{/} Refresh {cyan-fg}[q]{/} Quit'
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
// Switch to overview layout: show clusters/stats, hide agents/logs
|
|
53
|
+
widgets.clustersTable.show();
|
|
54
|
+
widgets.statsBox.show();
|
|
55
|
+
widgets.agentTable.hide();
|
|
56
|
+
widgets.logsBox.hide();
|
|
57
|
+
screen.render();
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Navigation - up
|
|
62
|
+
screen.key(['up', 'k'], () => {
|
|
63
|
+
if (tui.clusters.length === 0) return;
|
|
64
|
+
tui.selectedIndex = Math.max(0, tui.selectedIndex - 1);
|
|
65
|
+
tui.renderer.renderClustersTable(tui.clusters, tui.selectedIndex);
|
|
66
|
+
|
|
67
|
+
// Update agent table and logs for newly selected cluster
|
|
68
|
+
const selectedCluster = tui.clusters[tui.selectedIndex];
|
|
69
|
+
if (selectedCluster) {
|
|
70
|
+
// CRITICAL: Tell renderer which cluster is selected
|
|
71
|
+
tui.renderer.setSelectedCluster(selectedCluster.id);
|
|
72
|
+
|
|
73
|
+
// Clear old messages from previous cluster
|
|
74
|
+
tui.messages = [];
|
|
75
|
+
|
|
76
|
+
const status = orchestrator.getStatus(selectedCluster.id);
|
|
77
|
+
tui.renderer.renderAgentTable(status.agents, tui.resourceStats);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
screen.render();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Navigation - down
|
|
84
|
+
screen.key(['down', 'j'], () => {
|
|
85
|
+
if (tui.clusters.length === 0) return;
|
|
86
|
+
tui.selectedIndex = Math.min(tui.clusters.length - 1, tui.selectedIndex + 1);
|
|
87
|
+
tui.renderer.renderClustersTable(tui.clusters, tui.selectedIndex);
|
|
88
|
+
|
|
89
|
+
// Update agent table and logs for newly selected cluster
|
|
90
|
+
const selectedCluster = tui.clusters[tui.selectedIndex];
|
|
91
|
+
if (selectedCluster) {
|
|
92
|
+
// CRITICAL: Tell renderer which cluster is selected
|
|
93
|
+
tui.renderer.setSelectedCluster(selectedCluster.id);
|
|
94
|
+
|
|
95
|
+
// Clear old messages from previous cluster
|
|
96
|
+
tui.messages = [];
|
|
97
|
+
|
|
98
|
+
const status = orchestrator.getStatus(selectedCluster.id);
|
|
99
|
+
tui.renderer.renderAgentTable(status.agents, tui.resourceStats);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
screen.render();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Kill selected cluster (with confirmation)
|
|
106
|
+
screen.key(['K'], () => {
|
|
107
|
+
if (tui.clusters.length === 0) return;
|
|
108
|
+
const selectedCluster = tui.clusters[tui.selectedIndex];
|
|
109
|
+
if (!selectedCluster) return;
|
|
110
|
+
|
|
111
|
+
// Create confirmation dialog
|
|
112
|
+
const question = blessed.question({
|
|
113
|
+
parent: screen,
|
|
114
|
+
border: 'line',
|
|
115
|
+
height: 'shrink',
|
|
116
|
+
width: 'half',
|
|
117
|
+
top: 'center',
|
|
118
|
+
left: 'center',
|
|
119
|
+
label: ' {bold}{red-fg}Confirm Kill{/red-fg}{/bold} ',
|
|
120
|
+
tags: true,
|
|
121
|
+
keys: true,
|
|
122
|
+
vi: true,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
question.ask(
|
|
126
|
+
`Kill cluster ${selectedCluster.id}?\n\n(This will force-stop all agents)`,
|
|
127
|
+
async (err, value) => {
|
|
128
|
+
if (err) return;
|
|
129
|
+
if (value) {
|
|
130
|
+
try {
|
|
131
|
+
await orchestrator.kill(selectedCluster.id);
|
|
132
|
+
tui.messages.push({
|
|
133
|
+
timestamp: new Date().toISOString(),
|
|
134
|
+
text: `✓ Killed cluster ${selectedCluster.id}`,
|
|
135
|
+
level: 'success',
|
|
136
|
+
});
|
|
137
|
+
tui.renderer.renderLogs(tui.messages.slice(-20));
|
|
138
|
+
} catch (error) {
|
|
139
|
+
tui.messages.push({
|
|
140
|
+
timestamp: new Date().toISOString(),
|
|
141
|
+
text: `✗ Failed to kill cluster: ${error.message}`,
|
|
142
|
+
level: 'error',
|
|
143
|
+
});
|
|
144
|
+
tui.renderer.renderLogs(tui.messages.slice(-20));
|
|
145
|
+
}
|
|
146
|
+
screen.render();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// Stop selected cluster (with confirmation)
|
|
153
|
+
screen.key(['s'], () => {
|
|
154
|
+
if (tui.clusters.length === 0) return;
|
|
155
|
+
const selectedCluster = tui.clusters[tui.selectedIndex];
|
|
156
|
+
if (!selectedCluster) return;
|
|
157
|
+
|
|
158
|
+
// Create confirmation dialog
|
|
159
|
+
const question = blessed.question({
|
|
160
|
+
parent: screen,
|
|
161
|
+
border: 'line',
|
|
162
|
+
height: 'shrink',
|
|
163
|
+
width: 'half',
|
|
164
|
+
top: 'center',
|
|
165
|
+
left: 'center',
|
|
166
|
+
label: ' {bold}{yellow-fg}Confirm Stop{/yellow-fg}{/bold} ',
|
|
167
|
+
tags: true,
|
|
168
|
+
keys: true,
|
|
169
|
+
vi: true,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
question.ask(
|
|
173
|
+
`Stop cluster ${selectedCluster.id}?\n\n(This will gracefully stop all agents)`,
|
|
174
|
+
async (err, value) => {
|
|
175
|
+
if (err) return;
|
|
176
|
+
if (value) {
|
|
177
|
+
try {
|
|
178
|
+
await orchestrator.stop(selectedCluster.id);
|
|
179
|
+
tui.messages.push({
|
|
180
|
+
timestamp: new Date().toISOString(),
|
|
181
|
+
text: `✓ Stopped cluster ${selectedCluster.id}`,
|
|
182
|
+
level: 'success',
|
|
183
|
+
});
|
|
184
|
+
tui.renderer.renderLogs(tui.messages.slice(-20));
|
|
185
|
+
} catch (error) {
|
|
186
|
+
tui.messages.push({
|
|
187
|
+
timestamp: new Date().toISOString(),
|
|
188
|
+
text: `✗ Failed to stop cluster: ${error.message}`,
|
|
189
|
+
level: 'error',
|
|
190
|
+
});
|
|
191
|
+
tui.renderer.renderLogs(tui.messages.slice(-20));
|
|
192
|
+
}
|
|
193
|
+
screen.render();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// Export selected cluster
|
|
200
|
+
screen.key(['e'], () => {
|
|
201
|
+
if (tui.clusters.length === 0) return;
|
|
202
|
+
const selectedCluster = tui.clusters[tui.selectedIndex];
|
|
203
|
+
if (!selectedCluster) return;
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
const markdown = orchestrator.export(selectedCluster.id, 'markdown');
|
|
207
|
+
const fs = require('fs');
|
|
208
|
+
const filename = `${selectedCluster.id}-export.md`;
|
|
209
|
+
fs.writeFileSync(filename, markdown);
|
|
210
|
+
|
|
211
|
+
tui.messages.push({
|
|
212
|
+
timestamp: new Date().toISOString(),
|
|
213
|
+
text: `✓ Exported cluster to ${filename}`,
|
|
214
|
+
level: 'success',
|
|
215
|
+
});
|
|
216
|
+
tui.renderer.renderLogs(tui.messages.slice(-20));
|
|
217
|
+
screen.render();
|
|
218
|
+
} catch (error) {
|
|
219
|
+
tui.messages.push({
|
|
220
|
+
timestamp: new Date().toISOString(),
|
|
221
|
+
text: `✗ Failed to export cluster: ${error.message}`,
|
|
222
|
+
level: 'error',
|
|
223
|
+
});
|
|
224
|
+
tui.renderer.renderLogs(tui.messages.slice(-20));
|
|
225
|
+
screen.render();
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// Show full logs (spawn zeroshot logs -f in new terminal)
|
|
230
|
+
screen.key(['l'], () => {
|
|
231
|
+
if (tui.clusters.length === 0) return;
|
|
232
|
+
const selectedCluster = tui.clusters[tui.selectedIndex];
|
|
233
|
+
if (!selectedCluster) return;
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
// Detect terminal emulator
|
|
237
|
+
const term =
|
|
238
|
+
process.env.TERM_PROGRAM || (process.env.COLORTERM ? 'gnome-terminal' : null) || 'xterm';
|
|
239
|
+
|
|
240
|
+
let cmd, args;
|
|
241
|
+
if (term === 'iTerm.app' || term === 'Apple_Terminal') {
|
|
242
|
+
// macOS
|
|
243
|
+
cmd = 'osascript';
|
|
244
|
+
args = [
|
|
245
|
+
'-e',
|
|
246
|
+
`tell application "Terminal" to do script "zeroshot logs ${selectedCluster.id} -f"`,
|
|
247
|
+
];
|
|
248
|
+
} else {
|
|
249
|
+
// Linux - try common terminal emulators
|
|
250
|
+
const terminals = ['gnome-terminal', 'konsole', 'xterm', 'urxvt', 'alacritty', 'kitty'];
|
|
251
|
+
cmd =
|
|
252
|
+
terminals.find((t) => {
|
|
253
|
+
try {
|
|
254
|
+
require('child_process').execSync(`which ${t}`, {
|
|
255
|
+
stdio: 'ignore',
|
|
256
|
+
});
|
|
257
|
+
return true;
|
|
258
|
+
} catch {
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
}) || 'xterm';
|
|
262
|
+
|
|
263
|
+
if (cmd === 'gnome-terminal' || cmd === 'konsole') {
|
|
264
|
+
args = [
|
|
265
|
+
'--',
|
|
266
|
+
'bash',
|
|
267
|
+
'-c',
|
|
268
|
+
`zeroshot logs ${selectedCluster.id} -f; read -p "Press enter to close..."`,
|
|
269
|
+
];
|
|
270
|
+
} else {
|
|
271
|
+
args = [
|
|
272
|
+
'-e',
|
|
273
|
+
'bash',
|
|
274
|
+
'-c',
|
|
275
|
+
`zeroshot logs ${selectedCluster.id} -f; read -p "Press enter to close..."`,
|
|
276
|
+
];
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
spawn(cmd, args, { detached: true, stdio: 'ignore' });
|
|
281
|
+
|
|
282
|
+
tui.messages.push({
|
|
283
|
+
timestamp: new Date().toISOString(),
|
|
284
|
+
text: `✓ Opened logs for ${selectedCluster.id} in new terminal`,
|
|
285
|
+
level: 'success',
|
|
286
|
+
});
|
|
287
|
+
tui.renderer.renderLogs(tui.messages.slice(-20));
|
|
288
|
+
screen.render();
|
|
289
|
+
} catch (error) {
|
|
290
|
+
tui.messages.push({
|
|
291
|
+
timestamp: new Date().toISOString(),
|
|
292
|
+
text: `✗ Failed to open logs: ${error.message}`,
|
|
293
|
+
level: 'error',
|
|
294
|
+
});
|
|
295
|
+
tui.renderer.renderLogs(tui.messages.slice(-20));
|
|
296
|
+
screen.render();
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
// Force refresh
|
|
301
|
+
screen.key(['r'], () => {
|
|
302
|
+
tui.messages.push({
|
|
303
|
+
timestamp: new Date().toISOString(),
|
|
304
|
+
text: '↻ Refreshing...',
|
|
305
|
+
level: 'info',
|
|
306
|
+
});
|
|
307
|
+
tui.renderer.renderLogs(tui.messages.slice(-20));
|
|
308
|
+
screen.render();
|
|
309
|
+
|
|
310
|
+
// Trigger immediate poll
|
|
311
|
+
if (tui.poller) {
|
|
312
|
+
tui.poller.poll();
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
// Exit (with confirmation)
|
|
317
|
+
screen.key(['q', 'C-c'], () => {
|
|
318
|
+
const question = blessed.question({
|
|
319
|
+
parent: screen,
|
|
320
|
+
border: 'line',
|
|
321
|
+
height: 'shrink',
|
|
322
|
+
width: 'half',
|
|
323
|
+
top: 'center',
|
|
324
|
+
left: 'center',
|
|
325
|
+
label: ' {bold}Confirm Exit{/bold} ',
|
|
326
|
+
tags: true,
|
|
327
|
+
keys: true,
|
|
328
|
+
vi: true,
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
question.ask('Exit TUI?\n\n(Clusters will continue running)', (err, value) => {
|
|
332
|
+
if (err) return;
|
|
333
|
+
if (value) {
|
|
334
|
+
tui.exit();
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
// Help
|
|
340
|
+
screen.key(['?', 'h'], () => {
|
|
341
|
+
const helpBox = blessed.box({
|
|
342
|
+
parent: screen,
|
|
343
|
+
border: 'line',
|
|
344
|
+
height: '80%',
|
|
345
|
+
width: '60%',
|
|
346
|
+
top: 'center',
|
|
347
|
+
left: 'center',
|
|
348
|
+
label: ' {bold}Keybindings{/bold} ',
|
|
349
|
+
tags: true,
|
|
350
|
+
keys: true,
|
|
351
|
+
vi: true,
|
|
352
|
+
scrollable: true,
|
|
353
|
+
alwaysScroll: true,
|
|
354
|
+
content: `
|
|
355
|
+
{bold}Navigation:{/bold}
|
|
356
|
+
↑/k Move selection up
|
|
357
|
+
↓/j Move selection down
|
|
358
|
+
|
|
359
|
+
{bold}Actions:{/bold}
|
|
360
|
+
K Kill selected cluster (force stop)
|
|
361
|
+
s Stop selected cluster (graceful)
|
|
362
|
+
e Export selected cluster to markdown
|
|
363
|
+
l Show full logs in new terminal
|
|
364
|
+
r Force refresh
|
|
365
|
+
|
|
366
|
+
{bold}Other:{/bold}
|
|
367
|
+
?/h Show this help
|
|
368
|
+
q/Ctrl-C Exit TUI
|
|
369
|
+
|
|
370
|
+
Press any key to close...
|
|
371
|
+
`.trim(),
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
helpBox.key(['escape', 'q', 'enter', 'space'], () => {
|
|
375
|
+
helpBox.destroy();
|
|
376
|
+
screen.render();
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
screen.render();
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
module.exports = { setupKeybindings };
|