@covibes/zeroshot 5.4.0 → 5.4.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.
Files changed (136) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +8 -465
  3. package/cli/index.js +101 -5231
  4. package/package.json +16 -130
  5. package/CHANGELOG.md +0 -537
  6. package/cli/commands/providers.js +0 -149
  7. package/cli/lib/first-run.js +0 -211
  8. package/cli/lib/update-checker.js +0 -281
  9. package/cli/message-formatter-utils.js +0 -75
  10. package/cli/message-formatters-normal.js +0 -275
  11. package/cli/message-formatters-watch.js +0 -181
  12. package/cluster-templates/base-templates/debug-workflow.json +0 -375
  13. package/cluster-templates/base-templates/full-workflow.json +0 -581
  14. package/cluster-templates/base-templates/single-worker.json +0 -63
  15. package/cluster-templates/base-templates/worker-validator.json +0 -208
  16. package/cluster-templates/conductor-bootstrap.json +0 -124
  17. package/docker/zeroshot-cluster/Dockerfile +0 -152
  18. package/docker/zeroshot-cluster/pre-baked-deps.json +0 -28
  19. package/lib/completion.js +0 -174
  20. package/lib/docker-config.js +0 -220
  21. package/lib/git-remote-utils.js +0 -165
  22. package/lib/id-detector.js +0 -56
  23. package/lib/provider-defaults.js +0 -62
  24. package/lib/provider-detection.js +0 -59
  25. package/lib/provider-names.js +0 -57
  26. package/lib/settings/claude-auth.js +0 -78
  27. package/lib/settings.js +0 -503
  28. package/lib/stream-json-parser.js +0 -10
  29. package/scripts/fix-node-pty-permissions.js +0 -75
  30. package/scripts/record-demo.sh +0 -279
  31. package/scripts/setup-merge-queue.sh +0 -170
  32. package/scripts/test-install.sh +0 -40
  33. package/scripts/validate-templates.js +0 -100
  34. package/src/agent/agent-config.js +0 -246
  35. package/src/agent/agent-context-builder.js +0 -444
  36. package/src/agent/agent-hook-executor.js +0 -599
  37. package/src/agent/agent-lifecycle.js +0 -743
  38. package/src/agent/agent-stuck-detector.js +0 -256
  39. package/src/agent/agent-task-executor.js +0 -1690
  40. package/src/agent/agent-trigger-evaluator.js +0 -67
  41. package/src/agent/output-extraction.js +0 -217
  42. package/src/agent/output-reformatter.js +0 -175
  43. package/src/agent/schema-utils.js +0 -146
  44. package/src/agent-wrapper.js +0 -579
  45. package/src/agents/git-pusher-template.js +0 -285
  46. package/src/attach/attach-client.js +0 -438
  47. package/src/attach/attach-server.js +0 -543
  48. package/src/attach/index.js +0 -35
  49. package/src/attach/protocol.js +0 -220
  50. package/src/attach/ring-buffer.js +0 -121
  51. package/src/attach/socket-discovery.js +0 -267
  52. package/src/claude-task-runner.js +0 -574
  53. package/src/config-router.js +0 -80
  54. package/src/config-validator.js +0 -1925
  55. package/src/copy-worker.js +0 -43
  56. package/src/input-helpers.js +0 -65
  57. package/src/isolation-manager.js +0 -1471
  58. package/src/issue-providers/README.md +0 -305
  59. package/src/issue-providers/azure-devops-provider.js +0 -273
  60. package/src/issue-providers/base-provider.js +0 -232
  61. package/src/issue-providers/github-provider.js +0 -179
  62. package/src/issue-providers/gitlab-provider.js +0 -241
  63. package/src/issue-providers/index.js +0 -196
  64. package/src/issue-providers/jira-provider.js +0 -239
  65. package/src/ledger.js +0 -631
  66. package/src/lib/safe-exec.js +0 -88
  67. package/src/logic-engine.js +0 -223
  68. package/src/message-bus-bridge.js +0 -139
  69. package/src/message-bus.js +0 -249
  70. package/src/name-generator.js +0 -232
  71. package/src/orchestrator.js +0 -2815
  72. package/src/preflight.js +0 -639
  73. package/src/process-metrics.js +0 -608
  74. package/src/providers/anthropic/cli-builder.js +0 -73
  75. package/src/providers/anthropic/index.js +0 -204
  76. package/src/providers/anthropic/models.js +0 -23
  77. package/src/providers/anthropic/output-parser.js +0 -177
  78. package/src/providers/base-provider.js +0 -251
  79. package/src/providers/capabilities.js +0 -60
  80. package/src/providers/google/cli-builder.js +0 -55
  81. package/src/providers/google/index.js +0 -116
  82. package/src/providers/google/models.js +0 -24
  83. package/src/providers/google/output-parser.js +0 -101
  84. package/src/providers/index.js +0 -91
  85. package/src/providers/openai/cli-builder.js +0 -133
  86. package/src/providers/openai/index.js +0 -136
  87. package/src/providers/openai/models.js +0 -21
  88. package/src/providers/openai/output-parser.js +0 -143
  89. package/src/providers/opencode/cli-builder.js +0 -42
  90. package/src/providers/opencode/index.js +0 -103
  91. package/src/providers/opencode/models.js +0 -55
  92. package/src/providers/opencode/output-parser.js +0 -122
  93. package/src/schemas/sub-cluster.js +0 -185
  94. package/src/status-footer.js +0 -1026
  95. package/src/sub-cluster-wrapper.js +0 -601
  96. package/src/task-runner.js +0 -30
  97. package/src/template-resolver.js +0 -372
  98. package/src/tui/CHANGES.txt +0 -133
  99. package/src/tui/LAYOUT.md +0 -261
  100. package/src/tui/README.txt +0 -192
  101. package/src/tui/TWO-LEVEL-NAVIGATION.md +0 -186
  102. package/src/tui/data-poller.js +0 -349
  103. package/src/tui/demo.js +0 -208
  104. package/src/tui/formatters.js +0 -129
  105. package/src/tui/index.js +0 -193
  106. package/src/tui/keybindings.js +0 -324
  107. package/src/tui/layout.js +0 -334
  108. package/src/tui/renderer.js +0 -184
  109. package/task-lib/attachable-watcher.js +0 -309
  110. package/task-lib/claude-recovery.js +0 -156
  111. package/task-lib/commands/clean.js +0 -50
  112. package/task-lib/commands/episodes.js +0 -105
  113. package/task-lib/commands/get-log-path.js +0 -23
  114. package/task-lib/commands/kill.js +0 -32
  115. package/task-lib/commands/list.js +0 -105
  116. package/task-lib/commands/logs.js +0 -453
  117. package/task-lib/commands/resume.js +0 -42
  118. package/task-lib/commands/run.js +0 -57
  119. package/task-lib/commands/schedule.js +0 -105
  120. package/task-lib/commands/scheduler-cmd.js +0 -96
  121. package/task-lib/commands/schedules.js +0 -148
  122. package/task-lib/commands/status.js +0 -44
  123. package/task-lib/commands/unschedule.js +0 -16
  124. package/task-lib/completion.js +0 -9
  125. package/task-lib/config.js +0 -8
  126. package/task-lib/name-generator.js +0 -230
  127. package/task-lib/package.json +0 -3
  128. package/task-lib/runner.js +0 -201
  129. package/task-lib/scheduler.js +0 -252
  130. package/task-lib/store.js +0 -529
  131. package/task-lib/tui/formatters.js +0 -215
  132. package/task-lib/tui/index.js +0 -197
  133. package/task-lib/tui/layout.js +0 -111
  134. package/task-lib/tui/renderer.js +0 -129
  135. package/task-lib/tui.js +0 -425
  136. package/task-lib/watcher.js +0 -226
@@ -1,372 +0,0 @@
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
- // Apply defaults for missing params (e.g., timeout: 0)
47
- const paramsWithDefaults = this._applyDefaults(template, params);
48
-
49
- // Deep clone and resolve
50
- const resolved = this._resolveObject(JSON.parse(JSON.stringify(template)), paramsWithDefaults);
51
-
52
- // Filter out conditional agents that don't meet their condition
53
- if (resolved.agents) {
54
- resolved.agents = resolved.agents.filter((/** @type {any} */ agent) => {
55
- if (!agent.condition) return true;
56
- const conditionMet = this._evaluateCondition(agent.condition, params);
57
- delete agent.condition; // Remove condition field from final output
58
- return conditionMet;
59
- });
60
- }
61
-
62
- // Verify no unresolved placeholders remain
63
- this._verifyResolved(resolved);
64
-
65
- // Remove params schema from output (it's metadata, not config)
66
- delete resolved.params;
67
-
68
- return resolved;
69
- }
70
-
71
- /**
72
- * Validate that required params are provided
73
- * @private
74
- * @param {any} template
75
- * @param {any} params
76
- */
77
- _validateParams(template, params) {
78
- if (!template.params) return;
79
-
80
- const missing = [];
81
- for (const [name, schema] of Object.entries(template.params)) {
82
- if (params[name] === undefined && schema.default === undefined) {
83
- missing.push(name);
84
- }
85
- }
86
-
87
- if (missing.length > 0) {
88
- throw new Error(`Missing required params: ${missing.join(', ')}`);
89
- }
90
- }
91
-
92
- /**
93
- * Apply template defaults for any missing params
94
- * @private
95
- * @param {any} template
96
- * @param {any} params
97
- * @returns {any} params with defaults applied
98
- */
99
- _applyDefaults(template, params) {
100
- if (!template.params) return params;
101
-
102
- const result = { ...params };
103
- for (const [name, schema] of Object.entries(template.params)) {
104
- if (result[name] === undefined && schema.default !== undefined) {
105
- result[name] = schema.default;
106
- }
107
- }
108
- return result;
109
- }
110
-
111
- /**
112
- * Recursively resolve placeholders in an object
113
- * @private
114
- * @param {any} obj
115
- * @param {any} params
116
- * @returns {any}
117
- */
118
- _resolveObject(obj, params) {
119
- if (obj === null || obj === undefined) {
120
- return obj;
121
- }
122
-
123
- if (typeof obj === 'string') {
124
- return this._resolveString(obj, params);
125
- }
126
-
127
- if (Array.isArray(obj)) {
128
- return obj.map((item) => this._resolveObject(item, params));
129
- }
130
-
131
- if (typeof obj === 'object') {
132
- /** @type {any} */
133
- const result = {};
134
- for (const [key, value] of Object.entries(obj)) {
135
- result[key] = this._resolveObject(value, params);
136
- }
137
- return result;
138
- }
139
-
140
- return obj;
141
- }
142
-
143
- /**
144
- * Resolve placeholders in a string
145
- * Supports: {{param}} and {{#if condition}}...{{/if}}
146
- * @private
147
- * @param {any} str
148
- * @param {any} params
149
- * @returns {any}
150
- */
151
- _resolveString(str, params) {
152
- // Handle simple {{param}} substitutions
153
- let result = str.replace(
154
- /\{\{(\w+)\}\}/g,
155
- (/** @type {any} */ _match, /** @type {any} */ paramName) => {
156
- if (params[paramName] !== undefined) {
157
- return params[paramName];
158
- }
159
- // Return match unchanged if param not found (will be caught by verify)
160
- return _match;
161
- }
162
- );
163
-
164
- // Handle {{#if condition}}...{{/if}} blocks
165
- result = result.replace(
166
- /\{\{#if\s+([^}]+)\}\}([\s\S]*?)\{\{\/if\}\}/g,
167
- (/** @type {any} */ _match, /** @type {any} */ condition, /** @type {any} */ content) => {
168
- const conditionMet = this._evaluateCondition(condition, params);
169
- return conditionMet ? content : '';
170
- }
171
- );
172
-
173
- // Clean up multiple newlines from removed conditionals
174
- result = result.replace(/\n{3,}/g, '\n\n');
175
-
176
- return result;
177
- }
178
-
179
- /**
180
- * Evaluate a simple condition expression
181
- * Supports: param == 'value', param != 'value', {{param}} >= N
182
- * @private
183
- * @param {any} condition
184
- * @param {any} params
185
- * @returns {boolean}
186
- */
187
- _evaluateCondition(condition, params) {
188
- // Replace {{param}} with actual values first
189
- let expr = condition.trim();
190
-
191
- // Replace {{param}} placeholders
192
- expr = expr.replace(
193
- /\{\{(\w+)\}\}/g,
194
- (/** @type {any} */ _match, /** @type {any} */ paramName) => {
195
- const value = params[paramName];
196
- if (value === undefined) return 'undefined';
197
- if (typeof value === 'string') return `"${value}"`;
198
- return String(value);
199
- }
200
- );
201
-
202
- // Replace bare param names
203
- for (const [name, value] of Object.entries(params)) {
204
- const regex = new RegExp(`\\b${name}\\b`, 'g');
205
- if (typeof value === 'string') {
206
- expr = expr.replace(regex, `"${value}"`);
207
- } else {
208
- expr = expr.replace(regex, String(value));
209
- }
210
- }
211
-
212
- try {
213
- // Parse and evaluate simple comparison expressions without eval
214
- return this._evaluateSimpleExpression(expr);
215
- } catch {
216
- console.error(`Failed to evaluate condition: ${condition} (resolved: ${expr})`);
217
- return false;
218
- }
219
- }
220
-
221
- /**
222
- * Evaluate simple comparison expressions without eval
223
- * Supports: ==, !=, <, >, <=, >=, &&, ||
224
- * @private
225
- * @param {string} expr
226
- * @returns {boolean}
227
- */
228
- _evaluateSimpleExpression(expr) {
229
- // Handle logical operators (&&, ||) by splitting and recursing
230
- if (expr.includes('||')) {
231
- const parts = expr.split('||');
232
- return parts.some((part) => this._evaluateSimpleExpression(part.trim()));
233
- }
234
- if (expr.includes('&&')) {
235
- const parts = expr.split('&&');
236
- return parts.every((part) => this._evaluateSimpleExpression(part.trim()));
237
- }
238
-
239
- // Handle comparison operators
240
- const comparisonOps = ['==', '!=', '<=', '>=', '<', '>'];
241
- for (const op of comparisonOps) {
242
- if (expr.includes(op)) {
243
- const [left, right] = expr.split(op).map((s) => s.trim());
244
- const leftVal = this._parseValue(left);
245
- const rightVal = this._parseValue(right);
246
-
247
- switch (op) {
248
- case '==':
249
- return leftVal === rightVal;
250
- case '!=':
251
- return leftVal !== rightVal;
252
- case '<':
253
- return leftVal < rightVal;
254
- case '>':
255
- return leftVal > rightVal;
256
- case '<=':
257
- return leftVal <= rightVal;
258
- case '>=':
259
- return leftVal >= rightVal;
260
- }
261
- }
262
- }
263
-
264
- // If no operator found, treat as boolean literal or truthy value
265
- return this._parseValue(expr) ? true : false;
266
- }
267
-
268
- /**
269
- * Parse a value from string (number, boolean, string literal)
270
- * @private
271
- * @param {string} str
272
- * @returns {any}
273
- */
274
- _parseValue(str) {
275
- const trimmed = str.trim();
276
-
277
- // String literals (single or double quotes)
278
- if (
279
- (trimmed.startsWith('"') && trimmed.endsWith('"')) ||
280
- (trimmed.startsWith("'") && trimmed.endsWith("'"))
281
- ) {
282
- return trimmed.slice(1, -1);
283
- }
284
-
285
- // Boolean literals
286
- if (trimmed === 'true') return true;
287
- if (trimmed === 'false') return false;
288
- if (trimmed === 'undefined' || trimmed === 'null') return undefined;
289
-
290
- // Numbers
291
- if (/^-?\d+(\.\d+)?$/.test(trimmed)) {
292
- return parseFloat(trimmed);
293
- }
294
-
295
- // Return as-is for other cases
296
- return trimmed;
297
- }
298
-
299
- /**
300
- * Verify no unresolved placeholders remain
301
- * @private
302
- * @param {any} obj
303
- */
304
- _verifyResolved(obj) {
305
- const unresolved = this._findUnresolved(obj);
306
- if (unresolved.length > 0) {
307
- throw new Error(`Unresolved template placeholders: ${unresolved.join(', ')}`);
308
- }
309
- }
310
-
311
- /**
312
- * Find all unresolved {{param}} placeholders
313
- * @private
314
- * @param {any} obj
315
- * @param {string} pathPrefix
316
- * @returns {string[]}
317
- */
318
- _findUnresolved(obj, pathPrefix = '') {
319
- const unresolved = [];
320
-
321
- if (typeof obj === 'string') {
322
- const matches = obj.match(/\{\{(\w+)\}\}/g);
323
- if (matches) {
324
- unresolved.push(...matches.map((m) => `${pathPrefix}: ${m}`));
325
- }
326
- } else if (Array.isArray(obj)) {
327
- obj.forEach((item, i) => {
328
- unresolved.push(...this._findUnresolved(item, `${pathPrefix}[${i}]`));
329
- });
330
- } else if (obj && typeof obj === 'object') {
331
- for (const [key, value] of Object.entries(obj)) {
332
- unresolved.push(...this._findUnresolved(value, `${pathPrefix}.${key}`));
333
- }
334
- }
335
-
336
- return unresolved;
337
- }
338
-
339
- /**
340
- * List available base templates
341
- * @returns {string[]}
342
- */
343
- listTemplates() {
344
- if (!fs.existsSync(this.baseTemplatesDir)) {
345
- return [];
346
- }
347
- return fs
348
- .readdirSync(this.baseTemplatesDir)
349
- .filter((f) => f.endsWith('.json'))
350
- .map((f) => f.replace('.json', ''));
351
- }
352
-
353
- /**
354
- * Get template metadata (name, description, params)
355
- * @param {any} baseName
356
- * @returns {any}
357
- */
358
- getTemplateInfo(baseName) {
359
- const templatePath = path.join(this.baseTemplatesDir, `${baseName}.json`);
360
- if (!fs.existsSync(templatePath)) {
361
- return null;
362
- }
363
- const template = JSON.parse(fs.readFileSync(templatePath, 'utf8'));
364
- return {
365
- name: template.name,
366
- description: template.description,
367
- params: template.params || {},
368
- };
369
- }
370
- }
371
-
372
- module.exports = TemplateResolver;
@@ -1,133 +0,0 @@
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