@covibes/zeroshot 5.3.0 → 5.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +94 -14
  2. package/cli/commands/providers.js +8 -9
  3. package/cli/index.js +3032 -2409
  4. package/cli/message-formatters-normal.js +28 -6
  5. package/cluster-templates/base-templates/debug-workflow.json +1 -1
  6. package/cluster-templates/base-templates/full-workflow.json +72 -188
  7. package/cluster-templates/base-templates/worker-validator.json +59 -3
  8. package/cluster-templates/conductor-bootstrap.json +4 -4
  9. package/lib/docker-config.js +8 -0
  10. package/lib/git-remote-utils.js +165 -0
  11. package/lib/id-detector.js +10 -7
  12. package/lib/provider-defaults.js +62 -0
  13. package/lib/provider-names.js +2 -1
  14. package/lib/settings/claude-auth.js +78 -0
  15. package/lib/settings.js +161 -63
  16. package/package.json +7 -2
  17. package/scripts/setup-merge-queue.sh +170 -0
  18. package/src/agent/agent-config.js +135 -82
  19. package/src/agent/agent-context-builder.js +297 -188
  20. package/src/agent/agent-hook-executor.js +310 -113
  21. package/src/agent/agent-lifecycle.js +385 -325
  22. package/src/agent/agent-stuck-detector.js +7 -7
  23. package/src/agent/agent-task-executor.js +824 -565
  24. package/src/agent/output-extraction.js +41 -24
  25. package/src/agent/output-reformatter.js +1 -1
  26. package/src/agent/schema-utils.js +108 -73
  27. package/src/agent-wrapper.js +10 -1
  28. package/src/agents/git-pusher-template.js +285 -0
  29. package/src/claude-task-runner.js +85 -34
  30. package/src/config-validator.js +922 -657
  31. package/src/input-helpers.js +65 -0
  32. package/src/isolation-manager.js +289 -199
  33. package/src/issue-providers/README.md +305 -0
  34. package/src/issue-providers/azure-devops-provider.js +273 -0
  35. package/src/issue-providers/base-provider.js +232 -0
  36. package/src/issue-providers/github-provider.js +179 -0
  37. package/src/issue-providers/gitlab-provider.js +241 -0
  38. package/src/issue-providers/index.js +196 -0
  39. package/src/issue-providers/jira-provider.js +239 -0
  40. package/src/ledger.js +22 -5
  41. package/src/lib/safe-exec.js +88 -0
  42. package/src/orchestrator.js +1107 -811
  43. package/src/preflight.js +313 -159
  44. package/src/process-metrics.js +98 -56
  45. package/src/providers/anthropic/cli-builder.js +53 -25
  46. package/src/providers/anthropic/index.js +72 -2
  47. package/src/providers/anthropic/output-parser.js +32 -14
  48. package/src/providers/base-provider.js +70 -0
  49. package/src/providers/capabilities.js +9 -0
  50. package/src/providers/google/output-parser.js +47 -38
  51. package/src/providers/index.js +19 -3
  52. package/src/providers/openai/cli-builder.js +14 -3
  53. package/src/providers/openai/index.js +1 -0
  54. package/src/providers/openai/output-parser.js +44 -30
  55. package/src/providers/opencode/cli-builder.js +42 -0
  56. package/src/providers/opencode/index.js +103 -0
  57. package/src/providers/opencode/models.js +55 -0
  58. package/src/providers/opencode/output-parser.js +122 -0
  59. package/src/schemas/sub-cluster.js +68 -39
  60. package/src/status-footer.js +94 -48
  61. package/src/sub-cluster-wrapper.js +76 -35
  62. package/src/template-resolver.js +12 -9
  63. package/src/tui/data-poller.js +123 -99
  64. package/src/tui/formatters.js +4 -3
  65. package/src/tui/keybindings.js +259 -318
  66. package/src/tui/renderer.js +11 -21
  67. package/task-lib/attachable-watcher.js +118 -81
  68. package/task-lib/claude-recovery.js +61 -24
  69. package/task-lib/commands/episodes.js +105 -0
  70. package/task-lib/commands/list.js +2 -2
  71. package/task-lib/commands/logs.js +231 -189
  72. package/task-lib/commands/schedules.js +111 -61
  73. package/task-lib/config.js +0 -2
  74. package/task-lib/runner.js +84 -27
  75. package/task-lib/scheduler.js +1 -1
  76. package/task-lib/store.js +467 -168
  77. package/task-lib/tui/formatters.js +94 -45
  78. package/task-lib/tui/renderer.js +13 -3
  79. package/task-lib/tui.js +73 -32
  80. package/task-lib/watcher.js +128 -90
  81. package/src/agents/git-pusher-agent.json +0 -20
  82. package/src/github.js +0 -139
@@ -8,350 +8,278 @@
8
8
  */
9
9
 
10
10
  const blessed = require('blessed');
11
+ const fs = require('fs');
11
12
  const { spawn } = require('child_process');
13
+ const { execSync } = require('../lib/safe-exec'); // Enforces timeouts
12
14
 
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
- });
15
+ const HELP_TEXT_DETAIL =
16
+ '{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';
17
+ const HELP_TEXT_OVERVIEW =
18
+ '{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';
38
19
 
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
- }
20
+ function getSelectedCluster(tui) {
21
+ if (tui.clusters.length === 0) {
22
+ return null;
23
+ }
24
+
25
+ return tui.clusters[tui.selectedIndex] || null;
26
+ }
27
+
28
+ function pushLogMessage(tui, text, level) {
29
+ tui.messages.push({
30
+ timestamp: new Date().toISOString(),
31
+ text,
32
+ level,
59
33
  });
34
+ tui.renderer.renderLogs(tui.messages.slice(-20));
35
+ }
60
36
 
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);
37
+ function enterDetailView(screen, widgets, tui) {
38
+ if (tui.viewMode !== 'overview') {
39
+ return;
40
+ }
41
+
42
+ const selectedCluster = getSelectedCluster(tui);
43
+ if (!selectedCluster) {
44
+ return;
45
+ }
46
+
47
+ tui.viewMode = 'detail';
48
+ tui.detailClusterId = selectedCluster.id;
49
+ tui.renderer.setSelectedCluster(selectedCluster.id);
50
+ tui.messages = [];
51
+
52
+ widgets.helpBar.setContent(HELP_TEXT_DETAIL);
53
+ widgets.clustersTable.hide();
54
+ widgets.statsBox.hide();
55
+ widgets.agentTable.show();
56
+ widgets.logsBox.show();
57
+ screen.render();
58
+ }
66
59
 
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);
60
+ function exitDetailView(screen, widgets, tui) {
61
+ if (tui.viewMode !== 'detail') {
62
+ return;
63
+ }
64
+
65
+ tui.viewMode = 'overview';
66
+ tui.detailClusterId = null;
67
+ tui.renderer.setSelectedCluster(null);
68
+ tui.messages = [];
69
+
70
+ widgets.helpBar.setContent(HELP_TEXT_OVERVIEW);
71
+ widgets.clustersTable.show();
72
+ widgets.statsBox.show();
73
+ widgets.agentTable.hide();
74
+ widgets.logsBox.hide();
75
+ screen.render();
76
+ }
72
77
 
73
- // Clear old messages from previous cluster
74
- tui.messages = [];
78
+ function moveSelection(screen, tui, orchestrator, delta) {
79
+ if (tui.clusters.length === 0) {
80
+ return;
81
+ }
75
82
 
76
- const status = orchestrator.getStatus(selectedCluster.id);
77
- tui.renderer.renderAgentTable(status.agents, tui.resourceStats);
78
- }
83
+ tui.selectedIndex = Math.min(tui.clusters.length - 1, Math.max(0, tui.selectedIndex + delta));
84
+ tui.renderer.renderClustersTable(tui.clusters, tui.selectedIndex);
79
85
 
80
- screen.render();
81
- });
86
+ const selectedCluster = tui.clusters[tui.selectedIndex];
87
+ if (selectedCluster) {
88
+ tui.renderer.setSelectedCluster(selectedCluster.id);
89
+ tui.messages = [];
82
90
 
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);
91
+ const status = orchestrator.getStatus(selectedCluster.id);
92
+ tui.renderer.renderAgentTable(status.agents, tui.resourceStats);
93
+ }
88
94
 
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);
95
+ screen.render();
96
+ }
97
+
98
+ function createConfirmationDialog(screen, label, color) {
99
+ const labelText = color
100
+ ? ` {bold}{${color}-fg}${label}{/${color}-fg}{/bold} `
101
+ : ` {bold}${label}{/bold} `;
102
+
103
+ return blessed.question({
104
+ parent: screen,
105
+ border: 'line',
106
+ height: 'shrink',
107
+ width: 'half',
108
+ top: 'center',
109
+ left: 'center',
110
+ label: labelText,
111
+ tags: true,
112
+ keys: true,
113
+ vi: true,
114
+ });
115
+ }
94
116
 
95
- // Clear old messages from previous cluster
96
- tui.messages = [];
117
+ function confirmClusterAction(options) {
118
+ const { screen, tui, selectedCluster, label, color, prompt, action, successText, failureText } =
119
+ options;
120
+ const question = createConfirmationDialog(screen, label, color);
97
121
 
98
- const status = orchestrator.getStatus(selectedCluster.id);
99
- tui.renderer.renderAgentTable(status.agents, tui.resourceStats);
122
+ question.ask(prompt, async (err, value) => {
123
+ if (err || !value) {
124
+ return;
125
+ }
126
+
127
+ try {
128
+ await action(selectedCluster);
129
+ pushLogMessage(tui, successText(selectedCluster), 'success');
130
+ } catch (error) {
131
+ pushLogMessage(tui, failureText(error), 'error');
100
132
  }
101
133
 
102
134
  screen.render();
103
135
  });
136
+ }
104
137
 
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
- );
138
+ function handleKillCluster(screen, tui, orchestrator) {
139
+ const selectedCluster = getSelectedCluster(tui);
140
+ if (!selectedCluster) {
141
+ return;
142
+ }
143
+
144
+ confirmClusterAction({
145
+ screen,
146
+ tui,
147
+ selectedCluster,
148
+ label: 'Confirm Kill',
149
+ color: 'red',
150
+ prompt: `Kill cluster ${selectedCluster.id}?\n\n(This will force-stop all agents)`,
151
+ action: (cluster) => orchestrator.kill(cluster.id),
152
+ successText: (cluster) => `✓ Killed cluster ${cluster.id}`,
153
+ failureText: (error) => `✗ Failed to kill cluster: ${error.message}`,
150
154
  });
155
+ }
151
156
 
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
- );
157
+ function handleStopCluster(screen, tui, orchestrator) {
158
+ const selectedCluster = getSelectedCluster(tui);
159
+ if (!selectedCluster) {
160
+ return;
161
+ }
162
+
163
+ confirmClusterAction({
164
+ screen,
165
+ tui,
166
+ selectedCluster,
167
+ label: 'Confirm Stop',
168
+ color: 'yellow',
169
+ prompt: `Stop cluster ${selectedCluster.id}?\n\n(This will gracefully stop all agents)`,
170
+ action: (cluster) => orchestrator.stop(cluster.id),
171
+ successText: (cluster) => `✓ Stopped cluster ${cluster.id}`,
172
+ failureText: (error) => `✗ Failed to stop cluster: ${error.message}`,
197
173
  });
174
+ }
175
+
176
+ function handleExportCluster(screen, tui, orchestrator) {
177
+ const selectedCluster = getSelectedCluster(tui);
178
+ if (!selectedCluster) {
179
+ return;
180
+ }
181
+
182
+ try {
183
+ const markdown = orchestrator.export(selectedCluster.id, 'markdown');
184
+ const filename = `${selectedCluster.id}-export.md`;
185
+ fs.writeFileSync(filename, markdown);
186
+ pushLogMessage(tui, `✓ Exported cluster to ${filename}`, 'success');
187
+ } catch (error) {
188
+ pushLogMessage(tui, `✗ Failed to export cluster: ${error.message}`, 'error');
189
+ }
190
+
191
+ screen.render();
192
+ }
198
193
 
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;
194
+ function findTerminalCommand() {
195
+ const terminals = ['gnome-terminal', 'konsole', 'xterm', 'urxvt', 'alacritty', 'kitty'];
204
196
 
197
+ for (const terminal of terminals) {
205
198
  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();
199
+ execSync(`which ${terminal}`, { stdio: 'ignore' });
200
+ return terminal;
201
+ } catch {
202
+ // Ignore missing terminal
226
203
  }
227
- });
204
+ }
228
205
 
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;
206
+ return 'xterm';
207
+ }
234
208
 
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
- });
209
+ function buildLogCommand(clusterId) {
210
+ const term =
211
+ process.env.TERM_PROGRAM || (process.env.COLORTERM ? 'gnome-terminal' : null) || 'xterm';
299
212
 
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();
213
+ if (term === 'iTerm.app' || term === 'Apple_Terminal') {
214
+ return {
215
+ cmd: 'osascript',
216
+ args: ['-e', `tell application "Terminal" to do script "zeroshot logs ${clusterId} -f"`],
217
+ };
218
+ }
219
+
220
+ const cmd = findTerminalCommand();
221
+ const logCommand = `zeroshot logs ${clusterId} -f; read -p "Press enter to close..."`;
222
+
223
+ if (cmd === 'gnome-terminal' || cmd === 'konsole') {
224
+ return { cmd, args: ['--', 'bash', '-c', logCommand] };
225
+ }
226
+
227
+ return { cmd, args: ['-e', 'bash', '-c', logCommand] };
228
+ }
229
+
230
+ function handleOpenLogs(screen, tui) {
231
+ const selectedCluster = getSelectedCluster(tui);
232
+ if (!selectedCluster) {
233
+ return;
234
+ }
235
+
236
+ try {
237
+ const { cmd, args } = buildLogCommand(selectedCluster.id);
238
+ spawn(cmd, args, { detached: true, stdio: 'ignore' });
239
+ pushLogMessage(tui, `✓ Opened logs for ${selectedCluster.id} in new terminal`, 'success');
240
+ } catch (error) {
241
+ pushLogMessage(tui, `✗ Failed to open logs: ${error.message}`, 'error');
242
+ }
243
+
244
+ screen.render();
245
+ }
246
+
247
+ function handleRefresh(screen, tui) {
248
+ pushLogMessage(tui, '↻ Refreshing...', 'info');
249
+ screen.render();
250
+
251
+ if (tui.poller) {
252
+ tui.poller.poll();
253
+ }
254
+ }
309
255
 
310
- // Trigger immediate poll
311
- if (tui.poller) {
312
- tui.poller.poll();
256
+ function handleExit(screen, tui) {
257
+ const question = createConfirmationDialog(screen, 'Confirm Exit');
258
+
259
+ question.ask('Exit TUI?\n\n(Clusters will continue running)', (err, value) => {
260
+ if (err || !value) {
261
+ return;
313
262
  }
314
- });
315
263
 
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
- });
264
+ tui.exit();
337
265
  });
266
+ }
338
267
 
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: `
268
+ function handleHelp(screen) {
269
+ const helpBox = blessed.box({
270
+ parent: screen,
271
+ border: 'line',
272
+ height: '80%',
273
+ width: '60%',
274
+ top: 'center',
275
+ left: 'center',
276
+ label: ' {bold}Keybindings{/bold} ',
277
+ tags: true,
278
+ keys: true,
279
+ vi: true,
280
+ scrollable: true,
281
+ alwaysScroll: true,
282
+ content: `
355
283
  {bold}Navigation:{/bold}
356
284
  ↑/k Move selection up
357
285
  ↓/j Move selection down
@@ -368,16 +296,29 @@ function setupKeybindings(screen, widgets, tui, orchestrator) {
368
296
  q/Ctrl-C Exit TUI
369
297
 
370
298
  Press any key to close...
371
- `.trim(),
372
- });
373
-
374
- helpBox.key(['escape', 'q', 'enter', 'space'], () => {
375
- helpBox.destroy();
376
- screen.render();
377
- });
299
+ `.trim(),
300
+ });
378
301
 
302
+ helpBox.key(['escape', 'q', 'enter', 'space'], () => {
303
+ helpBox.destroy();
379
304
  screen.render();
380
305
  });
306
+
307
+ screen.render();
308
+ }
309
+
310
+ function setupKeybindings(screen, widgets, tui, orchestrator) {
311
+ screen.key(['enter'], () => enterDetailView(screen, widgets, tui));
312
+ screen.key(['escape'], () => exitDetailView(screen, widgets, tui));
313
+ screen.key(['up', 'k'], () => moveSelection(screen, tui, orchestrator, -1));
314
+ screen.key(['down', 'j'], () => moveSelection(screen, tui, orchestrator, 1));
315
+ screen.key(['K'], () => handleKillCluster(screen, tui, orchestrator));
316
+ screen.key(['s'], () => handleStopCluster(screen, tui, orchestrator));
317
+ screen.key(['e'], () => handleExportCluster(screen, tui, orchestrator));
318
+ screen.key(['l'], () => handleOpenLogs(screen, tui));
319
+ screen.key(['r'], () => handleRefresh(screen, tui));
320
+ screen.key(['q', 'C-c'], () => handleExit(screen, tui));
321
+ screen.key(['?', 'h'], () => handleHelp(screen));
381
322
  }
382
323
 
383
324
  module.exports = { setupKeybindings };