@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
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TaskRunner - Strategy Pattern interface for executing Claude tasks
|
|
3
|
+
*
|
|
4
|
+
* Implementations must provide a `run()` method that executes a Claude task
|
|
5
|
+
* with the given context and options. Different runners can implement various
|
|
6
|
+
* execution strategies (Claude CLI, mock responses, etc).
|
|
7
|
+
*/
|
|
8
|
+
class TaskRunner {
|
|
9
|
+
/**
|
|
10
|
+
* Execute a Claude task with the given context and options
|
|
11
|
+
*
|
|
12
|
+
* @param {string} _context - Full prompt/context for Claude to process
|
|
13
|
+
* @param {Object} _options - Execution options
|
|
14
|
+
* @param {string} _options.agentId - Identifier for this agent/task
|
|
15
|
+
* @param {string} _options.model - Model to use (e.g., 'opus', 'sonnet', 'haiku')
|
|
16
|
+
* @param {string} [_options.outputFormat] - Output format ('text', 'json', 'stream-json')
|
|
17
|
+
* @param {Object} [_options.jsonSchema] - JSON schema for structured output validation
|
|
18
|
+
* @param {string} [_options.cwd] - Working directory for task execution
|
|
19
|
+
* @param {boolean} [_options.isolation] - Whether to run in isolated container
|
|
20
|
+
*
|
|
21
|
+
* @returns {Promise<{success: boolean, output: string, error: string|null, taskId?: string}>} Result object with success status, output, error message, and optional taskId
|
|
22
|
+
*/
|
|
23
|
+
run(_context, _options) {
|
|
24
|
+
throw new Error('TaskRunner.run() not implemented');
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
module.exports = TaskRunner;
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TemplateResolver - Resolves parameterized cluster templates
|
|
3
|
+
*
|
|
4
|
+
* Takes a base template with {{param}} placeholders and resolves them
|
|
5
|
+
* with provided parameter values. Pure data transformation, no magic.
|
|
6
|
+
*
|
|
7
|
+
* Resolution rules:
|
|
8
|
+
* 1. Load base template JSON
|
|
9
|
+
* 2. Deep clone
|
|
10
|
+
* 3. Walk all values, replace {{param}} with params[param]
|
|
11
|
+
* 4. Handle conditional agents via "condition" field
|
|
12
|
+
* 5. Fail hard if any {{param}} remains unresolved
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
|
|
18
|
+
class TemplateResolver {
|
|
19
|
+
/**
|
|
20
|
+
* @param {string} templatesDir
|
|
21
|
+
*/
|
|
22
|
+
constructor(templatesDir) {
|
|
23
|
+
this.templatesDir = templatesDir;
|
|
24
|
+
this.baseTemplatesDir = path.join(templatesDir, 'base-templates');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Resolve a template with parameters
|
|
29
|
+
* @param {string} baseName - Name of base template (without .json)
|
|
30
|
+
* @param {Object} params - Parameter values to substitute
|
|
31
|
+
* @returns {Object} Resolved cluster config
|
|
32
|
+
*/
|
|
33
|
+
resolve(baseName, params) {
|
|
34
|
+
// Load base template
|
|
35
|
+
const templatePath = path.join(this.baseTemplatesDir, `${baseName}.json`);
|
|
36
|
+
if (!fs.existsSync(templatePath)) {
|
|
37
|
+
throw new Error(`Base template not found: ${baseName} (looked in ${templatePath})`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const templateJson = fs.readFileSync(templatePath, 'utf8');
|
|
41
|
+
const template = JSON.parse(templateJson);
|
|
42
|
+
|
|
43
|
+
// Validate required params
|
|
44
|
+
this._validateParams(template, params);
|
|
45
|
+
|
|
46
|
+
// Deep clone and resolve
|
|
47
|
+
const resolved = this._resolveObject(JSON.parse(JSON.stringify(template)), params);
|
|
48
|
+
|
|
49
|
+
// Filter out conditional agents that don't meet their condition
|
|
50
|
+
if (resolved.agents) {
|
|
51
|
+
resolved.agents = resolved.agents.filter((/** @type {any} */ agent) => {
|
|
52
|
+
if (!agent.condition) return true;
|
|
53
|
+
const conditionMet = this._evaluateCondition(agent.condition, params);
|
|
54
|
+
delete agent.condition; // Remove condition field from final output
|
|
55
|
+
return conditionMet;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Verify no unresolved placeholders remain
|
|
60
|
+
this._verifyResolved(resolved);
|
|
61
|
+
|
|
62
|
+
// Remove params schema from output (it's metadata, not config)
|
|
63
|
+
delete resolved.params;
|
|
64
|
+
|
|
65
|
+
return resolved;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Validate that required params are provided
|
|
70
|
+
* @private
|
|
71
|
+
* @param {any} template
|
|
72
|
+
* @param {any} params
|
|
73
|
+
*/
|
|
74
|
+
_validateParams(template, params) {
|
|
75
|
+
if (!template.params) return;
|
|
76
|
+
|
|
77
|
+
const missing = [];
|
|
78
|
+
for (const [name, schema] of Object.entries(template.params)) {
|
|
79
|
+
if (params[name] === undefined && schema.default === undefined) {
|
|
80
|
+
missing.push(name);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (missing.length > 0) {
|
|
85
|
+
throw new Error(`Missing required params: ${missing.join(', ')}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Recursively resolve placeholders in an object
|
|
91
|
+
* @private
|
|
92
|
+
* @param {any} obj
|
|
93
|
+
* @param {any} params
|
|
94
|
+
* @returns {any}
|
|
95
|
+
*/
|
|
96
|
+
_resolveObject(obj, params) {
|
|
97
|
+
if (obj === null || obj === undefined) {
|
|
98
|
+
return obj;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (typeof obj === 'string') {
|
|
102
|
+
return this._resolveString(obj, params);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (Array.isArray(obj)) {
|
|
106
|
+
return obj.map((item) => this._resolveObject(item, params));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (typeof obj === 'object') {
|
|
110
|
+
/** @type {any} */
|
|
111
|
+
const result = {};
|
|
112
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
113
|
+
result[key] = this._resolveObject(value, params);
|
|
114
|
+
}
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return obj;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Resolve placeholders in a string
|
|
123
|
+
* Supports: {{param}} and {{#if condition}}...{{/if}}
|
|
124
|
+
* @private
|
|
125
|
+
* @param {any} str
|
|
126
|
+
* @param {any} params
|
|
127
|
+
* @returns {any}
|
|
128
|
+
*/
|
|
129
|
+
_resolveString(str, params) {
|
|
130
|
+
// Handle simple {{param}} substitutions
|
|
131
|
+
let result = str.replace(
|
|
132
|
+
/\{\{(\w+)\}\}/g,
|
|
133
|
+
(/** @type {any} */ _match, /** @type {any} */ paramName) => {
|
|
134
|
+
if (params[paramName] !== undefined) {
|
|
135
|
+
return params[paramName];
|
|
136
|
+
}
|
|
137
|
+
// Return match unchanged if param not found (will be caught by verify)
|
|
138
|
+
return _match;
|
|
139
|
+
}
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
// Handle {{#if condition}}...{{/if}} blocks
|
|
143
|
+
result = result.replace(
|
|
144
|
+
/\{\{#if\s+([^}]+)\}\}([\s\S]*?)\{\{\/if\}\}/g,
|
|
145
|
+
(/** @type {any} */ _match, /** @type {any} */ condition, /** @type {any} */ content) => {
|
|
146
|
+
const conditionMet = this._evaluateCondition(condition, params);
|
|
147
|
+
return conditionMet ? content : '';
|
|
148
|
+
}
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
// Clean up multiple newlines from removed conditionals
|
|
152
|
+
result = result.replace(/\n{3,}/g, '\n\n');
|
|
153
|
+
|
|
154
|
+
return result;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Evaluate a simple condition expression
|
|
159
|
+
* Supports: param == 'value', param != 'value', {{param}} >= N
|
|
160
|
+
* @private
|
|
161
|
+
* @param {any} condition
|
|
162
|
+
* @param {any} params
|
|
163
|
+
* @returns {boolean}
|
|
164
|
+
*/
|
|
165
|
+
_evaluateCondition(condition, params) {
|
|
166
|
+
// Replace {{param}} with actual values first
|
|
167
|
+
let expr = condition.trim();
|
|
168
|
+
|
|
169
|
+
// Replace {{param}} placeholders
|
|
170
|
+
expr = expr.replace(
|
|
171
|
+
/\{\{(\w+)\}\}/g,
|
|
172
|
+
(/** @type {any} */ _match, /** @type {any} */ paramName) => {
|
|
173
|
+
const value = params[paramName];
|
|
174
|
+
if (value === undefined) return 'undefined';
|
|
175
|
+
if (typeof value === 'string') return `"${value}"`;
|
|
176
|
+
return String(value);
|
|
177
|
+
}
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
// Replace bare param names
|
|
181
|
+
for (const [name, value] of Object.entries(params)) {
|
|
182
|
+
const regex = new RegExp(`\\b${name}\\b`, 'g');
|
|
183
|
+
if (typeof value === 'string') {
|
|
184
|
+
expr = expr.replace(regex, `"${value}"`);
|
|
185
|
+
} else {
|
|
186
|
+
expr = expr.replace(regex, String(value));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
// Parse and evaluate simple comparison expressions without eval
|
|
192
|
+
return this._evaluateSimpleExpression(expr);
|
|
193
|
+
} catch {
|
|
194
|
+
console.error(`Failed to evaluate condition: ${condition} (resolved: ${expr})`);
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Evaluate simple comparison expressions without eval
|
|
201
|
+
* Supports: ==, !=, <, >, <=, >=, &&, ||
|
|
202
|
+
* @private
|
|
203
|
+
* @param {string} expr
|
|
204
|
+
* @returns {boolean}
|
|
205
|
+
*/
|
|
206
|
+
_evaluateSimpleExpression(expr) {
|
|
207
|
+
// Handle logical operators (&&, ||) by splitting and recursing
|
|
208
|
+
if (expr.includes('||')) {
|
|
209
|
+
const parts = expr.split('||');
|
|
210
|
+
return parts.some((part) => this._evaluateSimpleExpression(part.trim()));
|
|
211
|
+
}
|
|
212
|
+
if (expr.includes('&&')) {
|
|
213
|
+
const parts = expr.split('&&');
|
|
214
|
+
return parts.every((part) => this._evaluateSimpleExpression(part.trim()));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Handle comparison operators
|
|
218
|
+
const comparisonOps = ['==', '!=', '<=', '>=', '<', '>'];
|
|
219
|
+
for (const op of comparisonOps) {
|
|
220
|
+
if (expr.includes(op)) {
|
|
221
|
+
const [left, right] = expr.split(op).map((s) => s.trim());
|
|
222
|
+
const leftVal = this._parseValue(left);
|
|
223
|
+
const rightVal = this._parseValue(right);
|
|
224
|
+
|
|
225
|
+
switch (op) {
|
|
226
|
+
case '==':
|
|
227
|
+
return leftVal === rightVal;
|
|
228
|
+
case '!=':
|
|
229
|
+
return leftVal !== rightVal;
|
|
230
|
+
case '<':
|
|
231
|
+
return leftVal < rightVal;
|
|
232
|
+
case '>':
|
|
233
|
+
return leftVal > rightVal;
|
|
234
|
+
case '<=':
|
|
235
|
+
return leftVal <= rightVal;
|
|
236
|
+
case '>=':
|
|
237
|
+
return leftVal >= rightVal;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// If no operator found, treat as boolean literal or truthy value
|
|
243
|
+
return this._parseValue(expr) ? true : false;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Parse a value from string (number, boolean, string literal)
|
|
248
|
+
* @private
|
|
249
|
+
* @param {string} str
|
|
250
|
+
* @returns {any}
|
|
251
|
+
*/
|
|
252
|
+
_parseValue(str) {
|
|
253
|
+
str = str.trim();
|
|
254
|
+
|
|
255
|
+
// String literals (single or double quotes)
|
|
256
|
+
if ((str.startsWith('"') && str.endsWith('"')) || (str.startsWith("'") && str.endsWith("'"))) {
|
|
257
|
+
return str.slice(1, -1);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Boolean literals
|
|
261
|
+
if (str === 'true') return true;
|
|
262
|
+
if (str === 'false') return false;
|
|
263
|
+
if (str === 'undefined' || str === 'null') return undefined;
|
|
264
|
+
|
|
265
|
+
// Numbers
|
|
266
|
+
if (/^-?\d+(\.\d+)?$/.test(str)) {
|
|
267
|
+
return parseFloat(str);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Return as-is for other cases
|
|
271
|
+
return str;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Verify no unresolved placeholders remain
|
|
276
|
+
* @private
|
|
277
|
+
* @param {any} obj
|
|
278
|
+
*/
|
|
279
|
+
_verifyResolved(obj) {
|
|
280
|
+
const unresolved = this._findUnresolved(obj);
|
|
281
|
+
if (unresolved.length > 0) {
|
|
282
|
+
throw new Error(`Unresolved template placeholders: ${unresolved.join(', ')}`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Find all unresolved {{param}} placeholders
|
|
288
|
+
* @private
|
|
289
|
+
* @param {any} obj
|
|
290
|
+
* @param {string} pathPrefix
|
|
291
|
+
* @returns {string[]}
|
|
292
|
+
*/
|
|
293
|
+
_findUnresolved(obj, pathPrefix = '') {
|
|
294
|
+
const unresolved = [];
|
|
295
|
+
|
|
296
|
+
if (typeof obj === 'string') {
|
|
297
|
+
const matches = obj.match(/\{\{(\w+)\}\}/g);
|
|
298
|
+
if (matches) {
|
|
299
|
+
unresolved.push(...matches.map((m) => `${pathPrefix}: ${m}`));
|
|
300
|
+
}
|
|
301
|
+
} else if (Array.isArray(obj)) {
|
|
302
|
+
obj.forEach((item, i) => {
|
|
303
|
+
unresolved.push(...this._findUnresolved(item, `${pathPrefix}[${i}]`));
|
|
304
|
+
});
|
|
305
|
+
} else if (obj && typeof obj === 'object') {
|
|
306
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
307
|
+
unresolved.push(...this._findUnresolved(value, `${pathPrefix}.${key}`));
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return unresolved;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* List available base templates
|
|
316
|
+
* @returns {string[]}
|
|
317
|
+
*/
|
|
318
|
+
listTemplates() {
|
|
319
|
+
if (!fs.existsSync(this.baseTemplatesDir)) {
|
|
320
|
+
return [];
|
|
321
|
+
}
|
|
322
|
+
return fs
|
|
323
|
+
.readdirSync(this.baseTemplatesDir)
|
|
324
|
+
.filter((f) => f.endsWith('.json'))
|
|
325
|
+
.map((f) => f.replace('.json', ''));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Get template metadata (name, description, params)
|
|
330
|
+
* @param {any} baseName
|
|
331
|
+
* @returns {any}
|
|
332
|
+
*/
|
|
333
|
+
getTemplateInfo(baseName) {
|
|
334
|
+
const templatePath = path.join(this.baseTemplatesDir, `${baseName}.json`);
|
|
335
|
+
if (!fs.existsSync(templatePath)) {
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
const template = JSON.parse(fs.readFileSync(templatePath, 'utf8'));
|
|
339
|
+
return {
|
|
340
|
+
name: template.name,
|
|
341
|
+
description: template.description,
|
|
342
|
+
params: template.params || {},
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
module.exports = TemplateResolver;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
TUI Performance & UX Improvements
|
|
2
|
+
==================================
|
|
3
|
+
|
|
4
|
+
## New Features
|
|
5
|
+
|
|
6
|
+
### 4. Two-Level Navigation 🎯
|
|
7
|
+
**Feature:** Completely separate layouts for overview vs detail
|
|
8
|
+
**Implementation:**
|
|
9
|
+
- Overview mode: ONLY shows clusters table + stats (agents/logs hidden)
|
|
10
|
+
- Detail mode: ONLY shows agents + logs (clusters/stats hidden)
|
|
11
|
+
- Enter key to drill into detail, Escape to return
|
|
12
|
+
- Help text updates dynamically based on current view
|
|
13
|
+
- Widgets physically shown/hidden (not just empty data)
|
|
14
|
+
|
|
15
|
+
## Fixed Issues
|
|
16
|
+
|
|
17
|
+
### 1. Slow Startup ⚡
|
|
18
|
+
**Problem:** TUI took 5-10 seconds to start due to synchronous cluster loading
|
|
19
|
+
**Solution:**
|
|
20
|
+
- Deferred initial polls by 50-100ms to let UI render first
|
|
21
|
+
- Shows "Loading..." message immediately
|
|
22
|
+
- Lazy-loads cluster ledgers only when needed
|
|
23
|
+
- Startup now instant (<100ms)
|
|
24
|
+
|
|
25
|
+
### 2. Default Filter 🎯
|
|
26
|
+
**Problem:** Showed all clusters (including stopped) by default
|
|
27
|
+
**Solution:**
|
|
28
|
+
- Changed default filter from "all" to "running"
|
|
29
|
+
- User only sees active clusters
|
|
30
|
+
- Can still use `--filter all` to see everything
|
|
31
|
+
|
|
32
|
+
### 3. Cluster Selection 📍
|
|
33
|
+
**Problem:** Agents and logs weren't properly filtered by selected cluster
|
|
34
|
+
**Solution:**
|
|
35
|
+
- Renderer now tracks selectedClusterId
|
|
36
|
+
- Agents shown are for the selected cluster only
|
|
37
|
+
- Logs filtered to show only messages from selected cluster
|
|
38
|
+
- Messages cleared when switching between clusters
|
|
39
|
+
- Navigate with ↑↓ or jk keys
|
|
40
|
+
|
|
41
|
+
## Performance Improvements
|
|
42
|
+
|
|
43
|
+
**Before:**
|
|
44
|
+
- Startup: 5-10 seconds
|
|
45
|
+
- All clusters loaded synchronously
|
|
46
|
+
- All ledgers opened on startup
|
|
47
|
+
- Unfiltered logs from all clusters
|
|
48
|
+
|
|
49
|
+
**After:**
|
|
50
|
+
- Startup: <100ms instant
|
|
51
|
+
- Clusters loaded async after UI renders
|
|
52
|
+
- Ledgers lazy-loaded when needed
|
|
53
|
+
- Logs filtered to selected cluster only
|
|
54
|
+
|
|
55
|
+
## Usage
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
# Shows only running clusters (default)
|
|
59
|
+
vibe watch
|
|
60
|
+
|
|
61
|
+
# Show all clusters (including stopped)
|
|
62
|
+
vibe watch --filter all
|
|
63
|
+
|
|
64
|
+
# Show only stopped clusters
|
|
65
|
+
vibe watch --filter stopped
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Keyboard Navigation
|
|
69
|
+
|
|
70
|
+
### Two-Level Navigation
|
|
71
|
+
- **Overview Mode** (default): ONLY clusters + stats visible
|
|
72
|
+
- Large clusters table (16 rows) with system stats sidebar
|
|
73
|
+
- No agents or logs shown
|
|
74
|
+
- `↑` / `k` - Select previous cluster
|
|
75
|
+
- `↓` / `j` - Select next cluster
|
|
76
|
+
- `Enter` - Switch to detail view for selected cluster
|
|
77
|
+
|
|
78
|
+
- **Detail Mode**: ONLY agents + logs visible
|
|
79
|
+
- Full-width agents table (9 rows)
|
|
80
|
+
- Full-width live logs (9 rows)
|
|
81
|
+
- Clusters table and stats hidden
|
|
82
|
+
- `Escape` - Switch back to overview mode
|
|
83
|
+
|
|
84
|
+
Agents and logs auto-update in real-time when in detail view
|
|
85
|
+
|
|
86
|
+
## Technical Changes
|
|
87
|
+
|
|
88
|
+
### data-poller.js
|
|
89
|
+
- Line 45-53: Deferred initial polls with setTimeout
|
|
90
|
+
- Line 196-208: Added lazy loading for cluster ledgers
|
|
91
|
+
- Line 201-203: Check if ledger DB exists before loading
|
|
92
|
+
|
|
93
|
+
### index.js (TUI)
|
|
94
|
+
- Line 21: Changed default filter to 'running'
|
|
95
|
+
- Line 34-36: Added viewMode state ('overview' or 'detail') and detailClusterId
|
|
96
|
+
- Line 48-49: Show "Loading..." message on startup
|
|
97
|
+
- Line 107-119: Conditional rendering - agents only shown in detail view
|
|
98
|
+
- Line 130-137: Conditional rendering for resource_stats case
|
|
99
|
+
|
|
100
|
+
### keybindings.js
|
|
101
|
+
- Line 14-37: Enter key handler to switch to detail view
|
|
102
|
+
- Line 39-59: Escape key handler to switch to overview view
|
|
103
|
+
- Line 22, 45: Clear messages when switching views
|
|
104
|
+
- Line 29-34: Detail mode - hide clusters/stats, show agents/logs
|
|
105
|
+
- Line 52-57: Overview mode - show clusters/stats, hide agents/logs
|
|
106
|
+
- Line 24-27, 47-50: Update help text based on view mode
|
|
107
|
+
|
|
108
|
+
### layout.js
|
|
109
|
+
- Line 36: Expanded clusters table to 16 rows (from 6)
|
|
110
|
+
- Line 66: Expanded stats box to 16 rows (from 6)
|
|
111
|
+
- Line 85: Repositioned agent table to row 0, 9 rows, full width
|
|
112
|
+
- Line 119: Repositioned logs to row 9, 9 rows, full width
|
|
113
|
+
- Line 165-167: Initially hide agent table and logs (overview mode default)
|
|
114
|
+
|
|
115
|
+
### cli/index.js
|
|
116
|
+
- Line 1161: Changed default filter to 'running' in CLI option
|
|
117
|
+
|
|
118
|
+
## Testing
|
|
119
|
+
|
|
120
|
+
Run integration tests:
|
|
121
|
+
```bash
|
|
122
|
+
node tests/tui-integration.test.js # Basic TUI startup and data loading
|
|
123
|
+
node tests/tui-navigation-test.js # Two-level navigation functionality
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Expected: All tests pass, TUI starts instantly
|
|
127
|
+
|
|
128
|
+
## Notes
|
|
129
|
+
|
|
130
|
+
- Messages are cluster-scoped (only show for selected cluster)
|
|
131
|
+
- Selection persists across refreshes
|
|
132
|
+
- Empty clusters (no agents) still show in list
|
|
133
|
+
- Logs clear when switching clusters to avoid confusion
|