@covibes/zeroshot 1.4.0 → 1.5.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.
@@ -96,6 +96,11 @@ class StatusFooter {
96
96
  this.minRows = 8; // Minimum rows for footer display (graceful degradation)
97
97
  this.hidden = false; // True when terminal too small for footer
98
98
 
99
+ // Output queue - serializes all stdout to prevent cursor corruption
100
+ // When scroll region is active, console.log() can corrupt cursor position
101
+ // All output must go through print() to coordinate with render cycles
102
+ this.printQueue = [];
103
+
99
104
  // Debounced resize handler (100ms) - prevents rapid-fire redraws
100
105
  this._debouncedResize = debounce(() => this._handleResize(), 100);
101
106
  }
@@ -108,6 +113,38 @@ class StatusFooter {
108
113
  return process.stdout.isTTY === true;
109
114
  }
110
115
 
116
+ /**
117
+ * Print text to stdout, coordinating with the render cycle.
118
+ * When a render is in progress, queues output to prevent cursor corruption.
119
+ * When no render is active, writes immediately.
120
+ *
121
+ * MUST be used instead of console.log() when status footer is active.
122
+ * @param {string} text - Text to print (newline will be added)
123
+ */
124
+ print(text) {
125
+ if (this.isRendering) {
126
+ // Queue for later - render() will flush after restoring cursor
127
+ this.printQueue.push(text);
128
+ } else {
129
+ // Write immediately - no render in progress
130
+ process.stdout.write(text + '\n');
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Flush queued output to stdout.
136
+ * Called after render() restores cursor to ensure proper positioning.
137
+ * @private
138
+ */
139
+ _flushPrintQueue() {
140
+ if (this.printQueue.length === 0) return;
141
+
142
+ // Write all queued output
143
+ const output = this.printQueue.map(text => text + '\n').join('');
144
+ this.printQueue = [];
145
+ process.stdout.write(output);
146
+ }
147
+
111
148
  /**
112
149
  * Get terminal dimensions
113
150
  * @returns {{ rows: number, cols: number }}
@@ -495,6 +532,19 @@ class StatusFooter {
495
532
  } finally {
496
533
  this.isRendering = false;
497
534
 
535
+ // CRITICAL: Position cursor at bottom of scroll region before flushing
536
+ // Without this, output goes below footer if cursor was restored outside scroll region
537
+ // (RESTORE_CURSOR at line 527 may restore to row outside the scrollable area)
538
+ if (this.scrollRegionSet) {
539
+ const { rows } = this.getTerminalSize();
540
+ const scrollEnd = rows - this.footerHeight;
541
+ process.stdout.write(this._moveToStr(scrollEnd, 1));
542
+ }
543
+
544
+ // Flush any output that was queued during render
545
+ // Must happen BEFORE pending resize to preserve output order
546
+ this._flushPrintQueue();
547
+
498
548
  // Process pending resize if one was queued during render
499
549
  if (this.pendingResize) {
500
550
  this.pendingResize = false;
@@ -728,13 +778,15 @@ class StatusFooter {
728
778
  // Prevents interleaving with agent output during cleanup
729
779
  let buffer = '';
730
780
 
731
- // Reset scroll region
781
+ // CRITICAL: Clear footer area BEFORE resetting scroll region
782
+ // While scroll region is active, footer area contains only status bar content
783
+ // After reset, those lines may contain scrolled output (which we DON'T want to clear)
784
+ buffer += this._clearFooterAreaStr();
785
+
786
+ // Now reset scroll region (full terminal is scrollable again)
732
787
  buffer += this._resetScrollRegionStr();
733
788
  this.scrollRegionSet = false;
734
789
 
735
- // Clear all footer lines
736
- buffer += this._clearFooterAreaStr();
737
-
738
790
  // Move cursor to safe position and show cursor
739
791
  const { rows } = this.getTerminalSize();
740
792
  const startRow = rows - this.footerHeight + 1;
@@ -753,9 +805,10 @@ class StatusFooter {
753
805
  if (!this.isTTY()) return;
754
806
 
755
807
  // Single atomic write for hide operation
756
- let buffer = this._resetScrollRegionStr();
808
+ // CRITICAL: Clear footer BEFORE resetting scroll region (same reason as stop())
809
+ let buffer = this._clearFooterAreaStr();
810
+ buffer += this._resetScrollRegionStr();
757
811
  this.scrollRegionSet = false;
758
- buffer += this._clearFooterAreaStr();
759
812
  process.stdout.write(buffer);
760
813
  }
761
814
 
@@ -43,8 +43,11 @@ class TemplateResolver {
43
43
  // Validate required params
44
44
  this._validateParams(template, params);
45
45
 
46
+ // Apply defaults for missing params (e.g., timeout: 0)
47
+ const paramsWithDefaults = this._applyDefaults(template, params);
48
+
46
49
  // Deep clone and resolve
47
- const resolved = this._resolveObject(JSON.parse(JSON.stringify(template)), params);
50
+ const resolved = this._resolveObject(JSON.parse(JSON.stringify(template)), paramsWithDefaults);
48
51
 
49
52
  // Filter out conditional agents that don't meet their condition
50
53
  if (resolved.agents) {
@@ -86,6 +89,25 @@ class TemplateResolver {
86
89
  }
87
90
  }
88
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
+
89
111
  /**
90
112
  * Recursively resolve placeholders in an object
91
113
  * @private