@covibes/zeroshot 1.1.4 → 1.2.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.
- package/CHANGELOG.md +13 -0
- package/lib/stream-json-parser.js +9 -1
- package/package.json +1 -1
- package/src/agent/agent-lifecycle.js +20 -0
- package/src/agent/agent-task-executor.js +41 -0
- package/src/status-footer.js +231 -74
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
# [1.2.0](https://github.com/covibes/zeroshot/compare/v1.1.4...v1.2.0) (2025-12-28)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* **status-footer:** robust terminal resize handling ([767a610](https://github.com/covibes/zeroshot/commit/767a610027b3e2bb238b54c31a3a7e93db635319))
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* **agent:** publish TOKEN_USAGE events with task completion ([c79482c](https://github.com/covibes/zeroshot/commit/c79482c82582b75a692ba71005c821decdc1d769))
|
|
12
|
+
* **stream-parser:** add token usage tracking to result events ([91ad850](https://github.com/covibes/zeroshot/commit/91ad8507f42fd1a398bdc06f3b91b0a13eec8941))
|
|
13
|
+
|
|
1
14
|
## [1.1.4](https://github.com/covibes/zeroshot/compare/v1.1.3...v1.1.4) (2025-12-28)
|
|
2
15
|
|
|
3
16
|
|
|
@@ -45,8 +45,9 @@ function parseStreamLine(line) {
|
|
|
45
45
|
return parseUserMessage(event.message);
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
// result - final task result
|
|
48
|
+
// result - final task result (includes token usage and cost)
|
|
49
49
|
if (event.type === 'result') {
|
|
50
|
+
const usage = event.usage || {};
|
|
50
51
|
return {
|
|
51
52
|
type: 'result',
|
|
52
53
|
success: event.subtype === 'success',
|
|
@@ -54,6 +55,13 @@ function parseStreamLine(line) {
|
|
|
54
55
|
error: event.is_error ? event.result : null,
|
|
55
56
|
cost: event.total_cost_usd,
|
|
56
57
|
duration: event.duration_ms,
|
|
58
|
+
// Token usage from Claude API
|
|
59
|
+
inputTokens: usage.input_tokens || 0,
|
|
60
|
+
outputTokens: usage.output_tokens || 0,
|
|
61
|
+
cacheReadInputTokens: usage.cache_read_input_tokens || 0,
|
|
62
|
+
cacheCreationInputTokens: usage.cache_creation_input_tokens || 0,
|
|
63
|
+
// Per-model breakdown (for multi-model tasks)
|
|
64
|
+
modelUsage: event.modelUsage || null,
|
|
57
65
|
};
|
|
58
66
|
}
|
|
59
67
|
|
package/package.json
CHANGED
|
@@ -278,8 +278,28 @@ async function executeTask(agent, triggeringMessage) {
|
|
|
278
278
|
iteration: agent.iteration,
|
|
279
279
|
success: true,
|
|
280
280
|
taskId: agent.currentTaskId,
|
|
281
|
+
tokenUsage: result.tokenUsage || null,
|
|
281
282
|
});
|
|
282
283
|
|
|
284
|
+
// Publish TOKEN_USAGE event for aggregation and tracking
|
|
285
|
+
if (result.tokenUsage) {
|
|
286
|
+
agent.messageBus.publish({
|
|
287
|
+
cluster_id: agent.cluster.id,
|
|
288
|
+
topic: 'TOKEN_USAGE',
|
|
289
|
+
sender: agent.id,
|
|
290
|
+
content: {
|
|
291
|
+
text: `${agent.id} used ${result.tokenUsage.inputTokens} input + ${result.tokenUsage.outputTokens} output tokens`,
|
|
292
|
+
data: {
|
|
293
|
+
agentId: agent.id,
|
|
294
|
+
role: agent.role,
|
|
295
|
+
model: agent._selectModel(),
|
|
296
|
+
iteration: agent.iteration,
|
|
297
|
+
...result.tokenUsage,
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
283
303
|
// Execute onComplete hook
|
|
284
304
|
await executeHook({
|
|
285
305
|
hook: agent.config.hooks?.onComplete,
|
|
@@ -54,6 +54,44 @@ function sanitizeErrorMessage(error) {
|
|
|
54
54
|
// Track if we've already ensured the AskUserQuestion hook is installed
|
|
55
55
|
let askUserQuestionHookInstalled = false;
|
|
56
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Extract token usage from NDJSON output.
|
|
59
|
+
* Looks for the 'result' event line which contains usage data.
|
|
60
|
+
*
|
|
61
|
+
* @param {string} output - Full NDJSON output from Claude CLI
|
|
62
|
+
* @returns {Object|null} Token usage data or null if not found
|
|
63
|
+
*/
|
|
64
|
+
function extractTokenUsage(output) {
|
|
65
|
+
if (!output) return null;
|
|
66
|
+
|
|
67
|
+
const lines = output.split('\n');
|
|
68
|
+
|
|
69
|
+
// Find the result line containing usage data
|
|
70
|
+
for (const line of lines) {
|
|
71
|
+
if (!line.trim()) continue;
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const event = JSON.parse(line.trim());
|
|
75
|
+
if (event.type === 'result') {
|
|
76
|
+
const usage = event.usage || {};
|
|
77
|
+
return {
|
|
78
|
+
inputTokens: usage.input_tokens || 0,
|
|
79
|
+
outputTokens: usage.output_tokens || 0,
|
|
80
|
+
cacheReadInputTokens: usage.cache_read_input_tokens || 0,
|
|
81
|
+
cacheCreationInputTokens: usage.cache_creation_input_tokens || 0,
|
|
82
|
+
totalCostUsd: event.total_cost_usd || null,
|
|
83
|
+
durationMs: event.duration_ms || null,
|
|
84
|
+
modelUsage: event.modelUsage || null,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
} catch {
|
|
88
|
+
// Not valid JSON, continue
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
57
95
|
/**
|
|
58
96
|
* Ensure the AskUserQuestion blocking hook is installed in user's Claude config.
|
|
59
97
|
* This adds defense-in-depth by blocking the tool at the Claude CLI level.
|
|
@@ -569,6 +607,7 @@ function followClaudeTaskLogs(agent, taskId) {
|
|
|
569
607
|
success,
|
|
570
608
|
output,
|
|
571
609
|
error: sanitizeErrorMessage(errorContext),
|
|
610
|
+
tokenUsage: extractTokenUsage(output),
|
|
572
611
|
});
|
|
573
612
|
}, 500);
|
|
574
613
|
}
|
|
@@ -590,6 +629,7 @@ function followClaudeTaskLogs(agent, taskId) {
|
|
|
590
629
|
success: false,
|
|
591
630
|
output,
|
|
592
631
|
error: reason,
|
|
632
|
+
tokenUsage: extractTokenUsage(output),
|
|
593
633
|
});
|
|
594
634
|
},
|
|
595
635
|
};
|
|
@@ -907,6 +947,7 @@ function followClaudeTaskLogsIsolated(agent, taskId) {
|
|
|
907
947
|
output: fullOutput,
|
|
908
948
|
taskId,
|
|
909
949
|
result: parsedResult,
|
|
950
|
+
tokenUsage: extractTokenUsage(fullOutput),
|
|
910
951
|
});
|
|
911
952
|
}
|
|
912
953
|
} catch (pollErr) {
|
package/src/status-footer.js
CHANGED
|
@@ -8,6 +8,13 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Uses ANSI escape sequences to maintain a fixed footer while
|
|
10
10
|
* allowing normal terminal output to scroll above it.
|
|
11
|
+
*
|
|
12
|
+
* ROBUST RESIZE HANDLING:
|
|
13
|
+
* - Debounced resize events (100ms) prevent rapid-fire redraws
|
|
14
|
+
* - Render lock prevents concurrent renders from corrupting state
|
|
15
|
+
* - Full footer clear before scroll region reset prevents artifacts
|
|
16
|
+
* - Dimension checkpointing skips unnecessary redraws
|
|
17
|
+
* - Graceful degradation for terminals < 8 rows
|
|
11
18
|
*/
|
|
12
19
|
|
|
13
20
|
const { getProcessMetrics } = require('./process-metrics');
|
|
@@ -37,6 +44,20 @@ const COLORS = {
|
|
|
37
44
|
bgBlack: `${CSI}40m`,
|
|
38
45
|
};
|
|
39
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Debounce function - prevents rapid-fire calls during resize
|
|
49
|
+
* @param {Function} fn - Function to debounce
|
|
50
|
+
* @param {number} ms - Debounce delay in milliseconds
|
|
51
|
+
* @returns {Function} Debounced function
|
|
52
|
+
*/
|
|
53
|
+
function debounce(fn, ms) {
|
|
54
|
+
let timeout;
|
|
55
|
+
return (...args) => {
|
|
56
|
+
clearTimeout(timeout);
|
|
57
|
+
timeout = setTimeout(() => fn(...args), ms);
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
40
61
|
/**
|
|
41
62
|
* @typedef {Object} AgentState
|
|
42
63
|
* @property {string} id - Agent ID
|
|
@@ -65,6 +86,17 @@ class StatusFooter {
|
|
|
65
86
|
this.clusterId = null;
|
|
66
87
|
this.clusterState = 'initializing';
|
|
67
88
|
this.startTime = Date.now();
|
|
89
|
+
|
|
90
|
+
// Robust resize handling state
|
|
91
|
+
this.isRendering = false; // Render lock - prevents concurrent renders
|
|
92
|
+
this.pendingResize = false; // Queue resize if render in progress
|
|
93
|
+
this.lastKnownRows = 0; // Track terminal dimensions for change detection
|
|
94
|
+
this.lastKnownCols = 0;
|
|
95
|
+
this.minRows = 8; // Minimum rows for footer display (graceful degradation)
|
|
96
|
+
this.hidden = false; // True when terminal too small for footer
|
|
97
|
+
|
|
98
|
+
// Debounced resize handler (100ms) - prevents rapid-fire redraws
|
|
99
|
+
this._debouncedResize = debounce(() => this._handleResize(), 100);
|
|
68
100
|
}
|
|
69
101
|
|
|
70
102
|
/**
|
|
@@ -95,22 +127,80 @@ class StatusFooter {
|
|
|
95
127
|
process.stdout.write(`${CSI}${row};${col}H`);
|
|
96
128
|
}
|
|
97
129
|
|
|
130
|
+
/**
|
|
131
|
+
* Clear a specific line completely
|
|
132
|
+
* @param {number} row - 1-based row number
|
|
133
|
+
* @private
|
|
134
|
+
*/
|
|
135
|
+
_clearLine(row) {
|
|
136
|
+
process.stdout.write(`${CSI}${row};1H${CLEAR_LINE}`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Clear all footer lines (uses last known height for safety)
|
|
141
|
+
* @private
|
|
142
|
+
*/
|
|
143
|
+
_clearFooterArea() {
|
|
144
|
+
const { rows } = this.getTerminalSize();
|
|
145
|
+
// Use max of current and last footer height to ensure full cleanup
|
|
146
|
+
const heightToClear = Math.max(this.footerHeight, this.lastFooterHeight, 3);
|
|
147
|
+
const startRow = Math.max(1, rows - heightToClear + 1);
|
|
148
|
+
|
|
149
|
+
for (let row = startRow; row <= rows; row++) {
|
|
150
|
+
this._clearLine(row);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
98
154
|
/**
|
|
99
155
|
* Set up scroll region to reserve space for footer
|
|
156
|
+
* ROBUST: Clears footer area first, resets to full screen, then sets new region
|
|
100
157
|
*/
|
|
101
158
|
setupScrollRegion() {
|
|
102
159
|
if (!this.isTTY()) return;
|
|
103
160
|
|
|
104
|
-
const { rows } = this.getTerminalSize();
|
|
161
|
+
const { rows, cols } = this.getTerminalSize();
|
|
162
|
+
|
|
163
|
+
// Graceful degradation: hide footer if terminal too small
|
|
164
|
+
if (rows < this.minRows) {
|
|
165
|
+
if (!this.hidden) {
|
|
166
|
+
this.hidden = true;
|
|
167
|
+
// Reset to full screen scroll
|
|
168
|
+
process.stdout.write(`${CSI}1;${rows}r`);
|
|
169
|
+
this.scrollRegionSet = false;
|
|
170
|
+
}
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Restore footer if terminal grew large enough
|
|
175
|
+
if (this.hidden) {
|
|
176
|
+
this.hidden = false;
|
|
177
|
+
}
|
|
178
|
+
|
|
105
179
|
const scrollEnd = rows - this.footerHeight;
|
|
106
180
|
|
|
107
|
-
//
|
|
181
|
+
// CRITICAL: Save cursor before any manipulation
|
|
182
|
+
process.stdout.write(SAVE_CURSOR);
|
|
183
|
+
process.stdout.write(HIDE_CURSOR);
|
|
184
|
+
|
|
185
|
+
// Step 1: Reset scroll region to full screen first (prevents artifacts)
|
|
186
|
+
process.stdout.write(`${CSI}1;${rows}r`);
|
|
187
|
+
|
|
188
|
+
// Step 2: Clear footer area completely (prevents ghosting)
|
|
189
|
+
this._clearFooterArea();
|
|
190
|
+
|
|
191
|
+
// Step 3: Set new scroll region (lines 1 to scrollEnd)
|
|
108
192
|
process.stdout.write(`${CSI}1;${scrollEnd}r`);
|
|
109
193
|
|
|
110
|
-
// Move cursor to
|
|
111
|
-
process.stdout.write(`${CSI}
|
|
194
|
+
// Step 4: Move cursor to bottom of scroll region (safe position)
|
|
195
|
+
process.stdout.write(`${CSI}${scrollEnd};1H`);
|
|
196
|
+
|
|
197
|
+
// Restore cursor and show it
|
|
198
|
+
process.stdout.write(RESTORE_CURSOR);
|
|
199
|
+
process.stdout.write(SHOW_CURSOR);
|
|
112
200
|
|
|
113
201
|
this.scrollRegionSet = true;
|
|
202
|
+
this.lastKnownRows = rows;
|
|
203
|
+
this.lastKnownCols = cols;
|
|
114
204
|
}
|
|
115
205
|
|
|
116
206
|
/**
|
|
@@ -124,6 +214,35 @@ class StatusFooter {
|
|
|
124
214
|
this.scrollRegionSet = false;
|
|
125
215
|
}
|
|
126
216
|
|
|
217
|
+
/**
|
|
218
|
+
* Handle terminal resize event
|
|
219
|
+
* Called via debounced wrapper to prevent rapid-fire redraws
|
|
220
|
+
* @private
|
|
221
|
+
*/
|
|
222
|
+
_handleResize() {
|
|
223
|
+
if (!this.isTTY()) return;
|
|
224
|
+
|
|
225
|
+
const { rows, cols } = this.getTerminalSize();
|
|
226
|
+
|
|
227
|
+
// Skip if dimensions haven't actually changed (debounce may still fire)
|
|
228
|
+
if (rows === this.lastKnownRows && cols === this.lastKnownCols) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// If render in progress, queue resize for after
|
|
233
|
+
if (this.isRendering) {
|
|
234
|
+
this.pendingResize = true;
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Update dimensions and reconfigure
|
|
239
|
+
this.lastKnownRows = rows;
|
|
240
|
+
this.lastKnownCols = cols;
|
|
241
|
+
|
|
242
|
+
this.setupScrollRegion();
|
|
243
|
+
this.render();
|
|
244
|
+
}
|
|
245
|
+
|
|
127
246
|
/**
|
|
128
247
|
* Register cluster for monitoring
|
|
129
248
|
* @param {string} clusterId
|
|
@@ -223,72 +342,101 @@ class StatusFooter {
|
|
|
223
342
|
|
|
224
343
|
/**
|
|
225
344
|
* Render the footer
|
|
345
|
+
* ROBUST: Uses render lock to prevent concurrent renders from corrupting state
|
|
226
346
|
*/
|
|
227
347
|
async render() {
|
|
228
348
|
if (!this.enabled || !this.isTTY()) return;
|
|
229
349
|
|
|
230
|
-
|
|
350
|
+
// Graceful degradation: don't render if hidden
|
|
351
|
+
if (this.hidden) return;
|
|
231
352
|
|
|
232
|
-
//
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
try {
|
|
236
|
-
const metrics = await getProcessMetrics(agent.pid, { samplePeriodMs: 500 });
|
|
237
|
-
this.metricsCache.set(agentId, metrics);
|
|
238
|
-
} catch {
|
|
239
|
-
// Process may have exited
|
|
240
|
-
this.metricsCache.delete(agentId);
|
|
241
|
-
}
|
|
242
|
-
}
|
|
353
|
+
// Render lock: prevent concurrent renders
|
|
354
|
+
if (this.isRendering) {
|
|
355
|
+
return;
|
|
243
356
|
}
|
|
357
|
+
this.isRendering = true;
|
|
244
358
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
.filter(([, agent]) => agent.state === 'executing')
|
|
248
|
-
.slice(0, this.maxAgentRows);
|
|
359
|
+
try {
|
|
360
|
+
const { rows, cols } = this.getTerminalSize();
|
|
249
361
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
362
|
+
// Double-check terminal size (may have changed since last check)
|
|
363
|
+
if (rows < this.minRows) {
|
|
364
|
+
this.hidden = true;
|
|
365
|
+
this.resetScrollRegion();
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
254
368
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
369
|
+
// Collect metrics for all agents with PIDs
|
|
370
|
+
for (const [agentId, agent] of this.agents) {
|
|
371
|
+
if (agent.pid) {
|
|
372
|
+
try {
|
|
373
|
+
const metrics = await getProcessMetrics(agent.pid, { samplePeriodMs: 500 });
|
|
374
|
+
this.metricsCache.set(agentId, metrics);
|
|
375
|
+
} catch {
|
|
376
|
+
// Process may have exited
|
|
377
|
+
this.metricsCache.delete(agentId);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
260
381
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
382
|
+
// Get executing agents for display
|
|
383
|
+
const executingAgents = Array.from(this.agents.entries())
|
|
384
|
+
.filter(([, agent]) => agent.state === 'executing')
|
|
385
|
+
.slice(0, this.maxAgentRows);
|
|
386
|
+
|
|
387
|
+
// Calculate dynamic footer height: header + agent rows + summary
|
|
388
|
+
// Minimum 3 lines (header + "no agents" message + summary)
|
|
389
|
+
const agentRowCount = Math.max(1, executingAgents.length);
|
|
390
|
+
const newHeight = 2 + agentRowCount + 1; // header + agents + summary
|
|
391
|
+
|
|
392
|
+
// Update scroll region if height changed
|
|
393
|
+
if (newHeight !== this.footerHeight) {
|
|
394
|
+
this.lastFooterHeight = this.footerHeight;
|
|
395
|
+
this.footerHeight = newHeight;
|
|
396
|
+
this.setupScrollRegion();
|
|
397
|
+
}
|
|
265
398
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
399
|
+
// Build footer lines
|
|
400
|
+
const headerLine = this.buildHeaderLine(cols);
|
|
401
|
+
const agentRows = this.buildAgentRows(executingAgents, cols);
|
|
402
|
+
const summaryLine = this.buildSummaryLine(cols);
|
|
269
403
|
|
|
270
|
-
|
|
271
|
-
|
|
404
|
+
// Save cursor, render footer, restore cursor
|
|
405
|
+
process.stdout.write(SAVE_CURSOR);
|
|
406
|
+
process.stdout.write(HIDE_CURSOR);
|
|
272
407
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
process.stdout.write(CLEAR_LINE);
|
|
276
|
-
process.stdout.write(`${COLORS.bgBlack}${headerLine}${COLORS.reset}`);
|
|
408
|
+
// Render from top of footer area
|
|
409
|
+
let currentRow = rows - this.footerHeight + 1;
|
|
277
410
|
|
|
278
|
-
|
|
279
|
-
for (const agentRow of agentRows) {
|
|
411
|
+
// Header line
|
|
280
412
|
this.moveTo(currentRow++, 1);
|
|
281
413
|
process.stdout.write(CLEAR_LINE);
|
|
282
|
-
process.stdout.write(`${COLORS.bgBlack}${
|
|
283
|
-
|
|
414
|
+
process.stdout.write(`${COLORS.bgBlack}${headerLine}${COLORS.reset}`);
|
|
415
|
+
|
|
416
|
+
// Agent rows
|
|
417
|
+
for (const agentRow of agentRows) {
|
|
418
|
+
this.moveTo(currentRow++, 1);
|
|
419
|
+
process.stdout.write(CLEAR_LINE);
|
|
420
|
+
process.stdout.write(`${COLORS.bgBlack}${agentRow}${COLORS.reset}`);
|
|
421
|
+
}
|
|
284
422
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
423
|
+
// Summary line (with bottom border)
|
|
424
|
+
this.moveTo(currentRow, 1);
|
|
425
|
+
process.stdout.write(CLEAR_LINE);
|
|
426
|
+
process.stdout.write(`${COLORS.bgBlack}${summaryLine}${COLORS.reset}`);
|
|
289
427
|
|
|
290
|
-
|
|
291
|
-
|
|
428
|
+
process.stdout.write(RESTORE_CURSOR);
|
|
429
|
+
process.stdout.write(SHOW_CURSOR);
|
|
430
|
+
} finally {
|
|
431
|
+
this.isRendering = false;
|
|
432
|
+
|
|
433
|
+
// Process pending resize if one was queued during render
|
|
434
|
+
if (this.pendingResize) {
|
|
435
|
+
this.pendingResize = false;
|
|
436
|
+
// Use setImmediate to avoid deep recursion
|
|
437
|
+
setImmediate(() => this._handleResize());
|
|
438
|
+
}
|
|
439
|
+
}
|
|
292
440
|
}
|
|
293
441
|
|
|
294
442
|
/**
|
|
@@ -402,7 +550,7 @@ class StatusFooter {
|
|
|
402
550
|
parts.push(` ${COLORS.gray}│${COLORS.reset} ${COLORS.dim}${duration}${COLORS.reset}`);
|
|
403
551
|
|
|
404
552
|
// Agent counts
|
|
405
|
-
const executing = Array.from(this.agents.values()).filter(a => a.state === 'executing').length;
|
|
553
|
+
const executing = Array.from(this.agents.values()).filter((a) => a.state === 'executing').length;
|
|
406
554
|
const total = this.agents.size;
|
|
407
555
|
parts.push(` ${COLORS.gray}│${COLORS.reset} ${COLORS.green}${executing}/${total}${COLORS.reset} active`);
|
|
408
556
|
|
|
@@ -456,13 +604,21 @@ class StatusFooter {
|
|
|
456
604
|
return;
|
|
457
605
|
}
|
|
458
606
|
|
|
607
|
+
// Initialize dimension tracking
|
|
608
|
+
const { rows, cols } = this.getTerminalSize();
|
|
609
|
+
this.lastKnownRows = rows;
|
|
610
|
+
this.lastKnownCols = cols;
|
|
611
|
+
|
|
612
|
+
// Check for graceful degradation at startup
|
|
613
|
+
if (rows < this.minRows) {
|
|
614
|
+
this.hidden = true;
|
|
615
|
+
return; // Don't set up scroll region for tiny terminals
|
|
616
|
+
}
|
|
617
|
+
|
|
459
618
|
this.setupScrollRegion();
|
|
460
619
|
|
|
461
|
-
// Handle terminal resize
|
|
462
|
-
process.stdout.on('resize',
|
|
463
|
-
this.setupScrollRegion();
|
|
464
|
-
this.render();
|
|
465
|
-
});
|
|
620
|
+
// Handle terminal resize with debounced handler
|
|
621
|
+
process.stdout.on('resize', this._debouncedResize);
|
|
466
622
|
|
|
467
623
|
// Start refresh interval
|
|
468
624
|
this.intervalId = setInterval(() => {
|
|
@@ -482,19 +638,19 @@ class StatusFooter {
|
|
|
482
638
|
this.intervalId = null;
|
|
483
639
|
}
|
|
484
640
|
|
|
485
|
-
|
|
641
|
+
// Remove resize listener
|
|
642
|
+
process.stdout.removeListener('resize', this._debouncedResize);
|
|
643
|
+
|
|
644
|
+
if (this.isTTY() && !this.hidden) {
|
|
486
645
|
// Reset scroll region
|
|
487
646
|
this.resetScrollRegion();
|
|
488
647
|
|
|
489
|
-
// Clear all footer lines
|
|
490
|
-
|
|
491
|
-
const startRow = rows - this.footerHeight + 1;
|
|
492
|
-
for (let row = startRow; row <= rows; row++) {
|
|
493
|
-
this.moveTo(row, 1);
|
|
494
|
-
process.stdout.write(CLEAR_LINE);
|
|
495
|
-
}
|
|
648
|
+
// Clear all footer lines
|
|
649
|
+
this._clearFooterArea();
|
|
496
650
|
|
|
497
651
|
// Move cursor to safe position
|
|
652
|
+
const { rows } = this.getTerminalSize();
|
|
653
|
+
const startRow = rows - this.footerHeight + 1;
|
|
498
654
|
this.moveTo(startRow, 1);
|
|
499
655
|
process.stdout.write(SHOW_CURSOR);
|
|
500
656
|
}
|
|
@@ -507,14 +663,7 @@ class StatusFooter {
|
|
|
507
663
|
if (!this.isTTY()) return;
|
|
508
664
|
|
|
509
665
|
this.resetScrollRegion();
|
|
510
|
-
|
|
511
|
-
// Clear all footer lines (dynamic height)
|
|
512
|
-
const { rows } = this.getTerminalSize();
|
|
513
|
-
const startRow = rows - this.footerHeight + 1;
|
|
514
|
-
for (let row = startRow; row <= rows; row++) {
|
|
515
|
-
this.moveTo(row, 1);
|
|
516
|
-
process.stdout.write(CLEAR_LINE);
|
|
517
|
-
}
|
|
666
|
+
this._clearFooterArea();
|
|
518
667
|
}
|
|
519
668
|
|
|
520
669
|
/**
|
|
@@ -523,6 +672,14 @@ class StatusFooter {
|
|
|
523
672
|
show() {
|
|
524
673
|
if (!this.isTTY()) return;
|
|
525
674
|
|
|
675
|
+
// Reset hidden state and check terminal size
|
|
676
|
+
const { rows } = this.getTerminalSize();
|
|
677
|
+
if (rows < this.minRows) {
|
|
678
|
+
this.hidden = true;
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
this.hidden = false;
|
|
526
683
|
this.setupScrollRegion();
|
|
527
684
|
this.render();
|
|
528
685
|
}
|