@covibes/zeroshot 5.2.1 → 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 (98) hide show
  1. package/CHANGELOG.md +174 -189
  2. package/README.md +226 -195
  3. package/cli/commands/providers.js +149 -0
  4. package/cli/index.js +3145 -2366
  5. package/cli/lib/first-run.js +40 -3
  6. package/cli/message-formatters-normal.js +28 -6
  7. package/cluster-templates/base-templates/debug-workflow.json +24 -78
  8. package/cluster-templates/base-templates/full-workflow.json +99 -316
  9. package/cluster-templates/base-templates/single-worker.json +23 -15
  10. package/cluster-templates/base-templates/worker-validator.json +105 -36
  11. package/cluster-templates/conductor-bootstrap.json +9 -7
  12. package/lib/docker-config.js +14 -1
  13. package/lib/git-remote-utils.js +165 -0
  14. package/lib/id-detector.js +10 -7
  15. package/lib/provider-defaults.js +62 -0
  16. package/lib/provider-detection.js +59 -0
  17. package/lib/provider-names.js +57 -0
  18. package/lib/settings/claude-auth.js +78 -0
  19. package/lib/settings.js +298 -15
  20. package/lib/stream-json-parser.js +4 -238
  21. package/package.json +27 -6
  22. package/scripts/setup-merge-queue.sh +170 -0
  23. package/scripts/validate-templates.js +100 -0
  24. package/src/agent/agent-config.js +140 -63
  25. package/src/agent/agent-context-builder.js +336 -165
  26. package/src/agent/agent-hook-executor.js +337 -67
  27. package/src/agent/agent-lifecycle.js +386 -287
  28. package/src/agent/agent-stuck-detector.js +7 -7
  29. package/src/agent/agent-task-executor.js +944 -683
  30. package/src/agent/output-extraction.js +217 -0
  31. package/src/agent/output-reformatter.js +175 -0
  32. package/src/agent/schema-utils.js +146 -0
  33. package/src/agent-wrapper.js +112 -31
  34. package/src/agents/git-pusher-template.js +285 -0
  35. package/src/claude-task-runner.js +145 -44
  36. package/src/config-router.js +13 -13
  37. package/src/config-validator.js +1049 -563
  38. package/src/input-helpers.js +65 -0
  39. package/src/isolation-manager.js +499 -320
  40. package/src/issue-providers/README.md +305 -0
  41. package/src/issue-providers/azure-devops-provider.js +273 -0
  42. package/src/issue-providers/base-provider.js +232 -0
  43. package/src/issue-providers/github-provider.js +179 -0
  44. package/src/issue-providers/gitlab-provider.js +241 -0
  45. package/src/issue-providers/index.js +196 -0
  46. package/src/issue-providers/jira-provider.js +239 -0
  47. package/src/ledger.js +50 -11
  48. package/src/lib/safe-exec.js +88 -0
  49. package/src/orchestrator.js +1348 -757
  50. package/src/preflight.js +306 -149
  51. package/src/process-metrics.js +98 -56
  52. package/src/providers/anthropic/cli-builder.js +73 -0
  53. package/src/providers/anthropic/index.js +204 -0
  54. package/src/providers/anthropic/models.js +23 -0
  55. package/src/providers/anthropic/output-parser.js +177 -0
  56. package/src/providers/base-provider.js +251 -0
  57. package/src/providers/capabilities.js +60 -0
  58. package/src/providers/google/cli-builder.js +55 -0
  59. package/src/providers/google/index.js +116 -0
  60. package/src/providers/google/models.js +24 -0
  61. package/src/providers/google/output-parser.js +101 -0
  62. package/src/providers/index.js +91 -0
  63. package/src/providers/openai/cli-builder.js +133 -0
  64. package/src/providers/openai/index.js +136 -0
  65. package/src/providers/openai/models.js +21 -0
  66. package/src/providers/openai/output-parser.js +143 -0
  67. package/src/providers/opencode/cli-builder.js +42 -0
  68. package/src/providers/opencode/index.js +103 -0
  69. package/src/providers/opencode/models.js +55 -0
  70. package/src/providers/opencode/output-parser.js +122 -0
  71. package/src/schemas/sub-cluster.js +68 -39
  72. package/src/status-footer.js +94 -48
  73. package/src/sub-cluster-wrapper.js +92 -36
  74. package/src/task-runner.js +8 -6
  75. package/src/template-resolver.js +12 -9
  76. package/src/tui/data-poller.js +123 -99
  77. package/src/tui/formatters.js +4 -3
  78. package/src/tui/keybindings.js +259 -318
  79. package/src/tui/layout.js +20 -3
  80. package/src/tui/renderer.js +11 -21
  81. package/task-lib/attachable-watcher.js +150 -111
  82. package/task-lib/claude-recovery.js +156 -0
  83. package/task-lib/commands/episodes.js +105 -0
  84. package/task-lib/commands/list.js +3 -3
  85. package/task-lib/commands/logs.js +231 -189
  86. package/task-lib/commands/resume.js +3 -2
  87. package/task-lib/commands/run.js +12 -3
  88. package/task-lib/commands/schedules.js +111 -61
  89. package/task-lib/config.js +0 -2
  90. package/task-lib/runner.js +128 -50
  91. package/task-lib/scheduler.js +3 -3
  92. package/task-lib/store.js +464 -152
  93. package/task-lib/tui/formatters.js +94 -45
  94. package/task-lib/tui/renderer.js +13 -3
  95. package/task-lib/tui.js +73 -32
  96. package/task-lib/watcher.js +157 -100
  97. package/src/agents/git-pusher-agent.json +0 -20
  98. package/src/github.js +0 -103
@@ -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 };
package/src/tui/layout.js CHANGED
@@ -33,7 +33,7 @@ function createLayout(screen) {
33
33
  // - Help Bar (18-20 rows, 12 cols)
34
34
  // ============================================================
35
35
 
36
- const clustersTable = grid.set(0, 0, 16, 8, contrib.table, {
36
+ const clustersTable = grid.set(0, 0, 15, 8, contrib.table, {
37
37
  keys: true,
38
38
  fg: 'white',
39
39
  selectedFg: 'black',
@@ -63,7 +63,7 @@ function createLayout(screen) {
63
63
  data: [],
64
64
  });
65
65
 
66
- const statsBox = grid.set(0, 8, 16, 4, blessed.box, {
66
+ const statsBox = grid.set(0, 8, 15, 4, blessed.box, {
67
67
  label: ' System Stats ',
68
68
  content: '',
69
69
  tags: true,
@@ -131,12 +131,28 @@ function createLayout(screen) {
131
131
  keyable: true,
132
132
  });
133
133
 
134
+ // ============================================================
135
+ // WARNING BAR (experimental notice)
136
+ // ============================================================
137
+
138
+ const warningBar = grid.set(15, 0, 2, 12, blessed.box, {
139
+ content: '{yellow-fg}⚠ Watch TUI is experimental{/}',
140
+ tags: true,
141
+ border: { type: 'line', fg: 'yellow' },
142
+ style: {
143
+ border: { fg: 'yellow' },
144
+ },
145
+ padding: {
146
+ left: 1,
147
+ },
148
+ });
149
+
134
150
  // ============================================================
135
151
  // HELP BAR (2 rows x 12 cols):
136
152
  // - Keyboard shortcuts and commands
137
153
  // ============================================================
138
154
 
139
- const helpBar = grid.set(18, 0, 2, 12, blessed.box, {
155
+ const helpBar = grid.set(17, 0, 3, 12, blessed.box, {
140
156
  label: ' Help ',
141
157
  content:
142
158
  '{cyan-fg}[Enter]{/} View ' +
@@ -200,6 +216,7 @@ function createLayout(screen) {
200
216
  statsBox,
201
217
  agentTable,
202
218
  logsBox,
219
+ warningBar,
203
220
  helpBar,
204
221
  widgets,
205
222
  focus: (widgetIndex) => {