@covibes/zeroshot 1.3.0 → 1.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.
- package/CHANGELOG.md +7 -0
- package/cli/index.js +1 -0
- package/package.json +1 -1
- package/src/status-footer.js +133 -40
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
# [1.4.0](https://github.com/covibes/zeroshot/compare/v1.3.0...v1.4.0) (2025-12-28)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* **status-footer:** atomic writes + token cost display ([7baf0c2](https://github.com/covibes/zeroshot/commit/7baf0c228dd5f3489013f75a1782abe6cbe39661))
|
|
7
|
+
|
|
1
8
|
# [1.3.0](https://github.com/covibes/zeroshot/compare/v1.2.0...v1.3.0) (2025-12-28)
|
|
2
9
|
|
|
3
10
|
|
package/cli/index.js
CHANGED
|
@@ -613,6 +613,7 @@ Input formats:
|
|
|
613
613
|
});
|
|
614
614
|
statusFooter.setCluster(clusterId);
|
|
615
615
|
statusFooter.setClusterState('running');
|
|
616
|
+
statusFooter.setMessageBus(cluster.messageBus);
|
|
616
617
|
|
|
617
618
|
// Subscribe to AGENT_LIFECYCLE to track agent states and PIDs
|
|
618
619
|
const lifecycleUnsubscribe = cluster.messageBus.subscribeTopic('AGENT_LIFECYCLE', (msg) => {
|
package/package.json
CHANGED
package/src/status-footer.js
CHANGED
|
@@ -86,6 +86,7 @@ class StatusFooter {
|
|
|
86
86
|
this.clusterId = null;
|
|
87
87
|
this.clusterState = 'initializing';
|
|
88
88
|
this.startTime = Date.now();
|
|
89
|
+
this.messageBus = null; // MessageBus for token usage tracking
|
|
89
90
|
|
|
90
91
|
// Robust resize handling state
|
|
91
92
|
this.isRendering = false; // Render lock - prevents concurrent renders
|
|
@@ -137,23 +138,60 @@ class StatusFooter {
|
|
|
137
138
|
}
|
|
138
139
|
|
|
139
140
|
/**
|
|
140
|
-
*
|
|
141
|
+
* Generate move cursor ANSI sequence (returns string, doesn't write)
|
|
142
|
+
* Used for atomic buffered writes to prevent interleaving
|
|
143
|
+
* @param {number} row - 1-based row
|
|
144
|
+
* @param {number} col - 1-based column
|
|
145
|
+
* @returns {string} ANSI escape sequence
|
|
141
146
|
* @private
|
|
142
147
|
*/
|
|
143
|
-
|
|
148
|
+
_moveToStr(row, col) {
|
|
149
|
+
return `${CSI}${row};${col}H`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Generate clear line ANSI sequence (returns string, doesn't write)
|
|
154
|
+
* Used for atomic buffered writes to prevent interleaving
|
|
155
|
+
* @param {number} row - 1-based row number
|
|
156
|
+
* @returns {string} ANSI escape sequence
|
|
157
|
+
* @private
|
|
158
|
+
*/
|
|
159
|
+
_clearLineStr(row) {
|
|
160
|
+
return `${CSI}${row};1H${CLEAR_LINE}`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Generate ANSI sequences to clear all footer lines (returns string)
|
|
165
|
+
* Used for atomic buffered writes to prevent interleaving
|
|
166
|
+
* @returns {string} ANSI escape sequences
|
|
167
|
+
* @private
|
|
168
|
+
*/
|
|
169
|
+
_clearFooterAreaStr() {
|
|
144
170
|
const { rows } = this.getTerminalSize();
|
|
145
171
|
// Use max of current and last footer height to ensure full cleanup
|
|
146
172
|
const heightToClear = Math.max(this.footerHeight, this.lastFooterHeight, 3);
|
|
147
173
|
const startRow = Math.max(1, rows - heightToClear + 1);
|
|
148
174
|
|
|
175
|
+
let buffer = '';
|
|
149
176
|
for (let row = startRow; row <= rows; row++) {
|
|
150
|
-
this.
|
|
177
|
+
buffer += this._clearLineStr(row);
|
|
151
178
|
}
|
|
179
|
+
return buffer;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Clear all footer lines (uses last known height for safety)
|
|
184
|
+
* Uses single atomic write to prevent interleaving with other processes
|
|
185
|
+
* @private
|
|
186
|
+
*/
|
|
187
|
+
_clearFooterArea() {
|
|
188
|
+
process.stdout.write(this._clearFooterAreaStr());
|
|
152
189
|
}
|
|
153
190
|
|
|
154
191
|
/**
|
|
155
192
|
* Set up scroll region to reserve space for footer
|
|
156
193
|
* ROBUST: Clears footer area first, resets to full screen, then sets new region
|
|
194
|
+
* Uses single atomic write to prevent interleaving with other processes
|
|
157
195
|
*/
|
|
158
196
|
setupScrollRegion() {
|
|
159
197
|
if (!this.isTTY()) return;
|
|
@@ -178,39 +216,53 @@ class StatusFooter {
|
|
|
178
216
|
|
|
179
217
|
const scrollEnd = rows - this.footerHeight;
|
|
180
218
|
|
|
181
|
-
//
|
|
182
|
-
|
|
183
|
-
process.stdout.write(HIDE_CURSOR);
|
|
219
|
+
// BUILD ENTIRE OUTPUT INTO SINGLE BUFFER for atomic write
|
|
220
|
+
let buffer = '';
|
|
184
221
|
|
|
185
|
-
// Step 1:
|
|
186
|
-
|
|
222
|
+
// Step 1: Save cursor before any manipulation
|
|
223
|
+
buffer += SAVE_CURSOR;
|
|
224
|
+
buffer += HIDE_CURSOR;
|
|
187
225
|
|
|
188
|
-
// Step 2:
|
|
189
|
-
|
|
226
|
+
// Step 2: Reset scroll region to full screen first (prevents artifacts)
|
|
227
|
+
buffer += `${CSI}1;${rows}r`;
|
|
190
228
|
|
|
191
|
-
// Step 3:
|
|
192
|
-
|
|
229
|
+
// Step 3: Clear footer area completely (prevents ghosting)
|
|
230
|
+
buffer += this._clearFooterAreaStr();
|
|
193
231
|
|
|
194
|
-
// Step 4:
|
|
195
|
-
|
|
232
|
+
// Step 4: Set new scroll region (lines 1 to scrollEnd)
|
|
233
|
+
buffer += `${CSI}1;${scrollEnd}r`;
|
|
196
234
|
|
|
197
|
-
//
|
|
198
|
-
|
|
199
|
-
|
|
235
|
+
// Step 5: Move cursor to bottom of scroll region (safe position)
|
|
236
|
+
buffer += this._moveToStr(scrollEnd, 1);
|
|
237
|
+
|
|
238
|
+
// Step 6: Restore cursor and show it
|
|
239
|
+
buffer += RESTORE_CURSOR;
|
|
240
|
+
buffer += SHOW_CURSOR;
|
|
241
|
+
|
|
242
|
+
// SINGLE ATOMIC WRITE - prevents interleaving
|
|
243
|
+
process.stdout.write(buffer);
|
|
200
244
|
|
|
201
245
|
this.scrollRegionSet = true;
|
|
202
246
|
this.lastKnownRows = rows;
|
|
203
247
|
this.lastKnownCols = cols;
|
|
204
248
|
}
|
|
205
249
|
|
|
250
|
+
/**
|
|
251
|
+
* Generate reset scroll region string (returns string, doesn't write)
|
|
252
|
+
* @private
|
|
253
|
+
*/
|
|
254
|
+
_resetScrollRegionStr() {
|
|
255
|
+
const { rows } = this.getTerminalSize();
|
|
256
|
+
return `${CSI}1;${rows}r`;
|
|
257
|
+
}
|
|
258
|
+
|
|
206
259
|
/**
|
|
207
260
|
* Reset scroll region to full terminal
|
|
208
261
|
*/
|
|
209
262
|
resetScrollRegion() {
|
|
210
263
|
if (!this.isTTY()) return;
|
|
211
264
|
|
|
212
|
-
|
|
213
|
-
process.stdout.write(`${CSI}1;${rows}r`);
|
|
265
|
+
process.stdout.write(this._resetScrollRegionStr());
|
|
214
266
|
this.scrollRegionSet = false;
|
|
215
267
|
}
|
|
216
268
|
|
|
@@ -251,6 +303,14 @@ class StatusFooter {
|
|
|
251
303
|
this.clusterId = clusterId;
|
|
252
304
|
}
|
|
253
305
|
|
|
306
|
+
/**
|
|
307
|
+
* Set message bus for token usage tracking
|
|
308
|
+
* @param {object} messageBus - MessageBus instance with getTokensByRole()
|
|
309
|
+
*/
|
|
310
|
+
setMessageBus(messageBus) {
|
|
311
|
+
this.messageBus = messageBus;
|
|
312
|
+
}
|
|
313
|
+
|
|
254
314
|
/**
|
|
255
315
|
* Update cluster state
|
|
256
316
|
* @param {string} state
|
|
@@ -401,32 +461,37 @@ class StatusFooter {
|
|
|
401
461
|
const agentRows = this.buildAgentRows(executingAgents, cols);
|
|
402
462
|
const summaryLine = this.buildSummaryLine(cols);
|
|
403
463
|
|
|
404
|
-
//
|
|
405
|
-
|
|
406
|
-
|
|
464
|
+
// BUILD ENTIRE OUTPUT INTO SINGLE BUFFER for atomic write
|
|
465
|
+
// This prevents interleaving with other processes writing to stdout
|
|
466
|
+
let buffer = '';
|
|
467
|
+
buffer += SAVE_CURSOR;
|
|
468
|
+
buffer += HIDE_CURSOR;
|
|
407
469
|
|
|
408
470
|
// Render from top of footer area
|
|
409
471
|
let currentRow = rows - this.footerHeight + 1;
|
|
410
472
|
|
|
411
473
|
// Header line
|
|
412
|
-
this.
|
|
413
|
-
|
|
414
|
-
|
|
474
|
+
buffer += this._moveToStr(currentRow++, 1);
|
|
475
|
+
buffer += CLEAR_LINE;
|
|
476
|
+
buffer += `${COLORS.bgBlack}${headerLine}${COLORS.reset}`;
|
|
415
477
|
|
|
416
478
|
// Agent rows
|
|
417
479
|
for (const agentRow of agentRows) {
|
|
418
|
-
this.
|
|
419
|
-
|
|
420
|
-
|
|
480
|
+
buffer += this._moveToStr(currentRow++, 1);
|
|
481
|
+
buffer += CLEAR_LINE;
|
|
482
|
+
buffer += `${COLORS.bgBlack}${agentRow}${COLORS.reset}`;
|
|
421
483
|
}
|
|
422
484
|
|
|
423
485
|
// Summary line (with bottom border)
|
|
424
|
-
this.
|
|
425
|
-
|
|
426
|
-
|
|
486
|
+
buffer += this._moveToStr(currentRow, 1);
|
|
487
|
+
buffer += CLEAR_LINE;
|
|
488
|
+
buffer += `${COLORS.bgBlack}${summaryLine}${COLORS.reset}`;
|
|
489
|
+
|
|
490
|
+
buffer += RESTORE_CURSOR;
|
|
491
|
+
buffer += SHOW_CURSOR;
|
|
427
492
|
|
|
428
|
-
|
|
429
|
-
process.stdout.write(
|
|
493
|
+
// SINGLE ATOMIC WRITE - prevents interleaving
|
|
494
|
+
process.stdout.write(buffer);
|
|
430
495
|
} finally {
|
|
431
496
|
this.isRendering = false;
|
|
432
497
|
|
|
@@ -554,6 +619,21 @@ class StatusFooter {
|
|
|
554
619
|
const total = this.agents.size;
|
|
555
620
|
parts.push(` ${COLORS.gray}│${COLORS.reset} ${COLORS.green}${executing}/${total}${COLORS.reset} active`);
|
|
556
621
|
|
|
622
|
+
// Token cost (from message bus)
|
|
623
|
+
if (this.messageBus && this.clusterId) {
|
|
624
|
+
try {
|
|
625
|
+
const tokensByRole = this.messageBus.getTokensByRole(this.clusterId);
|
|
626
|
+
const totalCost = tokensByRole?._total?.totalCostUsd || 0;
|
|
627
|
+
if (totalCost > 0) {
|
|
628
|
+
// Format: $0.05 or $1.23 or $12.34
|
|
629
|
+
const costStr = totalCost < 0.01 ? '<$0.01' : `$${totalCost.toFixed(2)}`;
|
|
630
|
+
parts.push(` ${COLORS.gray}│${COLORS.reset} ${COLORS.yellow}${costStr}${COLORS.reset}`);
|
|
631
|
+
}
|
|
632
|
+
} catch {
|
|
633
|
+
// Ignore errors - token tracking is optional
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
557
637
|
// Aggregate metrics
|
|
558
638
|
let totalCpu = 0;
|
|
559
639
|
let totalMem = 0;
|
|
@@ -621,7 +701,9 @@ class StatusFooter {
|
|
|
621
701
|
process.stdout.on('resize', this._debouncedResize);
|
|
622
702
|
|
|
623
703
|
// Start refresh interval
|
|
704
|
+
// Guard: Skip if previous render still running (prevents overlapping renders)
|
|
624
705
|
this.intervalId = setInterval(() => {
|
|
706
|
+
if (this.isRendering) return;
|
|
625
707
|
this.render();
|
|
626
708
|
}, this.refreshInterval);
|
|
627
709
|
|
|
@@ -642,17 +724,25 @@ class StatusFooter {
|
|
|
642
724
|
process.stdout.removeListener('resize', this._debouncedResize);
|
|
643
725
|
|
|
644
726
|
if (this.isTTY() && !this.hidden) {
|
|
727
|
+
// BUILD SINGLE BUFFER for atomic shutdown write
|
|
728
|
+
// Prevents interleaving with agent output during cleanup
|
|
729
|
+
let buffer = '';
|
|
730
|
+
|
|
645
731
|
// Reset scroll region
|
|
646
|
-
this.
|
|
732
|
+
buffer += this._resetScrollRegionStr();
|
|
733
|
+
this.scrollRegionSet = false;
|
|
647
734
|
|
|
648
735
|
// Clear all footer lines
|
|
649
|
-
this.
|
|
736
|
+
buffer += this._clearFooterAreaStr();
|
|
650
737
|
|
|
651
|
-
// Move cursor to safe position
|
|
738
|
+
// Move cursor to safe position and show cursor
|
|
652
739
|
const { rows } = this.getTerminalSize();
|
|
653
740
|
const startRow = rows - this.footerHeight + 1;
|
|
654
|
-
this.
|
|
655
|
-
|
|
741
|
+
buffer += this._moveToStr(startRow, 1);
|
|
742
|
+
buffer += SHOW_CURSOR;
|
|
743
|
+
|
|
744
|
+
// SINGLE ATOMIC WRITE
|
|
745
|
+
process.stdout.write(buffer);
|
|
656
746
|
}
|
|
657
747
|
}
|
|
658
748
|
|
|
@@ -662,8 +752,11 @@ class StatusFooter {
|
|
|
662
752
|
hide() {
|
|
663
753
|
if (!this.isTTY()) return;
|
|
664
754
|
|
|
665
|
-
|
|
666
|
-
this.
|
|
755
|
+
// Single atomic write for hide operation
|
|
756
|
+
let buffer = this._resetScrollRegionStr();
|
|
757
|
+
this.scrollRegionSet = false;
|
|
758
|
+
buffer += this._clearFooterAreaStr();
|
|
759
|
+
process.stdout.write(buffer);
|
|
667
760
|
}
|
|
668
761
|
|
|
669
762
|
/**
|