open-agents-ai 0.29.0 → 0.30.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.
Files changed (2) hide show
  1. package/dist/index.js +235 -75
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -18728,7 +18728,7 @@ function themeForTool(toolName) {
18728
18728
  return THEME_DEFAULT;
18729
18729
  }
18730
18730
  }
18731
- var DENSITY, WAVE, THEME_DEFAULT, THEME_FILE, THEME_SHELL, THEME_WEB, THEME_SEARCH, THEME_MEMORY, THEME_SKILL, THEME_TOOL_CREATE, BrailleSpinner;
18731
+ var DENSITY, WAVE, THEME_DEFAULT, THEME_FILE, THEME_SHELL, THEME_WEB, THEME_SEARCH, THEME_MEMORY, THEME_SKILL, THEME_TOOL_CREATE, DEFAULT_METRICS, BrailleSpinner;
18732
18732
  var init_braille_spinner = __esm({
18733
18733
  "packages/cli/dist/tui/braille-spinner.js"() {
18734
18734
  "use strict";
@@ -18785,11 +18785,17 @@ var init_braille_spinner = __esm({
18785
18785
  ramp: [237, 173, 174, 179, 180, 215, 216, 222, 229],
18786
18786
  speed: 2
18787
18787
  };
18788
+ DEFAULT_METRICS = {
18789
+ contextPct: 0,
18790
+ tokenRate: 0,
18791
+ isStreaming: false
18792
+ };
18788
18793
  BrailleSpinner = class {
18789
18794
  frame = 0;
18790
18795
  timer = null;
18791
18796
  theme = THEME_DEFAULT;
18792
18797
  colorRamp = buildColorRamp(THEME_DEFAULT.ramp);
18798
+ _metrics = { ...DEFAULT_METRICS };
18793
18799
  /** Start the animation, calling `onFrame` every tick (~80 ms). */
18794
18800
  start(onFrame) {
18795
18801
  this.frame = 0;
@@ -18806,14 +18812,15 @@ var init_braille_spinner = __esm({
18806
18812
  }
18807
18813
  this.frame = 0;
18808
18814
  this.setTool(null);
18815
+ this._metrics = { ...DEFAULT_METRICS };
18809
18816
  }
18810
18817
  /** Whether the animation timer is active. */
18811
18818
  get isRunning() {
18812
18819
  return this.timer !== null;
18813
18820
  }
18814
18821
  /**
18815
- * Set the active tool, changing the animation color theme and speed.
18816
- * Pass null to return to the default idle theme.
18822
+ * Set the active tool, changing the animation color theme.
18823
+ * Pass null to return to the default idle theme (e.g. on tool_result).
18817
18824
  */
18818
18825
  setTool(toolName) {
18819
18826
  const next = themeForTool(toolName);
@@ -18822,21 +18829,68 @@ var init_braille_spinner = __esm({
18822
18829
  this.colorRamp = buildColorRamp(next.ramp);
18823
18830
  }
18824
18831
  }
18832
+ /**
18833
+ * Update real-time metrics that drive animation dynamics.
18834
+ * Call frequently (e.g. on each metrics update / stream tick).
18835
+ */
18836
+ setMetrics(metrics) {
18837
+ if (metrics.contextPct !== void 0)
18838
+ this._metrics.contextPct = metrics.contextPct;
18839
+ if (metrics.tokenRate !== void 0)
18840
+ this._metrics.tokenRate = metrics.tokenRate;
18841
+ if (metrics.isStreaming !== void 0)
18842
+ this._metrics.isStreaming = metrics.isStreaming;
18843
+ }
18825
18844
  /**
18826
18845
  * Render the current animation frame as an ANSI-colored string.
18827
- * Each column gets a braille character whose density and color vary
18828
- * sinusoidally, shifted by `this.frame` to create movement.
18829
- * The column multiplier (speed) and colors come from the active tool theme.
18846
+ *
18847
+ * State-driven dynamics:
18848
+ * - Speed: base from tool theme, boosted by token rate when streaming,
18849
+ * modulated by a slow breathing oscillation when idle.
18850
+ * - Amplitude: context pressure scales how much of the density range is
18851
+ * used — gentle ripples at low usage, full waves at high usage.
18852
+ * - Slinky: a secondary slow sine wave creates organic compression and
18853
+ * expansion across columns, making the wave feel elastic and alive.
18830
18854
  */
18831
18855
  render(width) {
18832
18856
  const cycleLen = WAVE.length;
18833
- const speed = this.theme.speed;
18857
+ const baseSpeed = this.theme.speed;
18858
+ const m = this._metrics;
18859
+ const breathPhase = Math.sin(this.frame * 0.08);
18860
+ let speed;
18861
+ if (m.isStreaming && m.tokenRate > 0) {
18862
+ const rateBoost = Math.min(4, m.tokenRate / 8);
18863
+ speed = baseSpeed + rateBoost + breathPhase * 0.3;
18864
+ } else {
18865
+ speed = baseSpeed + breathPhase * 0.8;
18866
+ }
18867
+ const pressure = Math.max(0, Math.min(100, m.contextPct)) / 100;
18868
+ const densityScale = 0.3 + pressure * 0.7;
18869
+ const slinkyFreq = 0.02 + (m.isStreaming ? 0.01 : 0);
18870
+ const slinkyAmp = 1.5 + pressure * 2;
18834
18871
  let buf = "";
18835
18872
  let lastColor = -1;
18836
18873
  for (let col = 0; col < width; col++) {
18837
- const idx = (col * speed + this.frame) % cycleLen;
18838
- const ch = WAVE[idx];
18839
- const color = this.colorRamp[idx];
18874
+ const slinkyOffset = Math.sin(col * 0.1 + this.frame * slinkyFreq) * slinkyAmp;
18875
+ const rawPhase = col * speed + this.frame + slinkyOffset;
18876
+ const normalizedPhase = (rawPhase % cycleLen + cycleLen) % cycleLen;
18877
+ const waveIdx = Math.round(normalizedPhase) % cycleLen;
18878
+ let amplitude;
18879
+ if (waveIdx <= 8) {
18880
+ amplitude = waveIdx;
18881
+ } else {
18882
+ amplitude = 16 - waveIdx;
18883
+ }
18884
+ const scaledAmplitude = Math.round(amplitude * densityScale);
18885
+ let scaledIdx;
18886
+ if (waveIdx <= 8) {
18887
+ scaledIdx = scaledAmplitude;
18888
+ } else {
18889
+ scaledIdx = 16 - scaledAmplitude;
18890
+ }
18891
+ scaledIdx = Math.max(0, Math.min(cycleLen - 1, scaledIdx));
18892
+ const ch = WAVE[scaledIdx];
18893
+ const color = this.colorRamp[scaledIdx];
18840
18894
  if (color !== lastColor) {
18841
18895
  buf += `\x1B[38;5;${color}m`;
18842
18896
  lastColor = color;
@@ -18851,13 +18905,12 @@ var init_braille_spinner = __esm({
18851
18905
  });
18852
18906
 
18853
18907
  // packages/cli/dist/tui/status-bar.js
18854
- var FOOTER_ROWS, StatusBar;
18908
+ var StatusBar;
18855
18909
  var init_status_bar = __esm({
18856
18910
  "packages/cli/dist/tui/status-bar.js"() {
18857
18911
  "use strict";
18858
18912
  init_render();
18859
18913
  init_braille_spinner();
18860
- FOOTER_ROWS = 5;
18861
18914
  StatusBar = class {
18862
18915
  metrics = {
18863
18916
  promptTokens: 0,
@@ -18898,6 +18951,10 @@ var init_status_bar = __esm({
18898
18951
  /** Whether agent is actively processing (braille animation) */
18899
18952
  _processing = false;
18900
18953
  _brailleSpinner = new BrailleSpinner();
18954
+ /** Current dynamic footer height (min 5: buffer + topSep + 1 input line + bottomSep + metrics) */
18955
+ _currentFooterHeight = 5;
18956
+ /** Timestamp when streaming started (for token rate calculation) */
18957
+ _streamStartTime = 0;
18901
18958
  /**
18902
18959
  * Provide a callback that returns readline's current input state.
18903
18960
  * StatusBar uses this to render typed text and position the cursor
@@ -18939,11 +18996,13 @@ var init_status_bar = __esm({
18939
18996
  return;
18940
18997
  this._processing = active;
18941
18998
  if (active) {
18999
+ this._brailleSpinner.setMetrics({ isStreaming: true });
18942
19000
  this._brailleSpinner.start(() => {
18943
19001
  if (this.active)
18944
19002
  this.renderBufferLine();
18945
19003
  });
18946
19004
  } else {
19005
+ this._brailleSpinner.setMetrics({ isStreaming: false, tokenRate: 0 });
18947
19006
  this._brailleSpinner.stop();
18948
19007
  if (this.active)
18949
19008
  this.renderBufferLine();
@@ -18984,6 +19043,9 @@ var init_status_bar = __esm({
18984
19043
  if (update.estimatedContextTokens !== void 0)
18985
19044
  this.metrics.estimatedContextTokens = update.estimatedContextTokens;
18986
19045
  this._streamingTokens = 0;
19046
+ this._streamStartTime = 0;
19047
+ this.pushSpinnerContextMetrics();
19048
+ this._brailleSpinner.setMetrics({ tokenRate: 0, isStreaming: false });
18987
19049
  if (this.active)
18988
19050
  this.renderFooterPreserveCursor();
18989
19051
  }
@@ -18993,6 +19055,11 @@ var init_status_bar = __esm({
18993
19055
  /** Increment the live streaming token counter (throttled re-render at 100ms) */
18994
19056
  incrementStreamingTokens(count) {
18995
19057
  this._streamingTokens += count;
19058
+ if (this._streamStartTime === 0)
19059
+ this._streamStartTime = Date.now();
19060
+ const elapsedSec = (Date.now() - this._streamStartTime) / 1e3;
19061
+ const tokenRate = elapsedSec > 0.1 ? this._streamingTokens / elapsedSec : 0;
19062
+ this._brailleSpinner.setMetrics({ tokenRate, isStreaming: true });
18996
19063
  if (!this._streamThrottleTimer && this.active) {
18997
19064
  this._streamThrottleTimer = setTimeout(() => {
18998
19065
  this._streamThrottleTimer = null;
@@ -19011,6 +19078,7 @@ var init_status_bar = __esm({
19011
19078
  this.metrics.completionTokens = 0;
19012
19079
  this.metrics.totalTokens = 0;
19013
19080
  this.metrics.estimatedContextTokens = 0;
19081
+ this.pushSpinnerContextMetrics();
19014
19082
  if (this.active)
19015
19083
  this.renderFooterPreserveCursor();
19016
19084
  }
@@ -19038,7 +19106,7 @@ var init_status_bar = __esm({
19038
19106
  }
19039
19107
  /**
19040
19108
  * Set the prompt text that StatusBar draws on the input row.
19041
- * Called by the REPL whenever the prompt changes (idle active).
19109
+ * Called by the REPL whenever the prompt changes (idle <-> active).
19042
19110
  * The prompt is rendered as part of the atomic footer write so cursor
19043
19111
  * positioning never depends on readline's internal tracking.
19044
19112
  * @param text The ANSI-colored prompt string (e.g. "> " or "+ ")
@@ -19051,21 +19119,29 @@ var init_status_bar = __esm({
19051
19119
  this.renderFooterAndPositionInput();
19052
19120
  }
19053
19121
  }
19054
- /** Number of rows reserved at the bottom */
19122
+ /** Number of rows reserved at the bottom (dynamic based on input wrapping) */
19055
19123
  get reservedRows() {
19056
- return FOOTER_ROWS;
19124
+ return this._currentFooterHeight;
19057
19125
  }
19058
19126
  /** Handle terminal resize — reapply scroll region and redraw footer */
19059
19127
  handleResize() {
19060
19128
  if (!this.active)
19061
19129
  return;
19130
+ this.updateFooterHeight();
19131
+ const rows = process.stdout.rows ?? 24;
19132
+ const pos = this.rowPositions(rows);
19133
+ const w = getTermWidth();
19134
+ const sep = c2.dim("\u2500".repeat(w));
19062
19135
  if (this.writeDepth > 0) {
19063
- const rows = process.stdout.rows ?? 24;
19064
- const scrollEnd = Math.max(rows - FOOTER_ROWS, this.scrollRegionTop + 1);
19065
- const w = getTermWidth();
19066
- const sep = c2.dim("\u2500".repeat(w));
19067
- const inputRow = rows - 2;
19068
- process.stdout.write(`\x1B[${this.scrollRegionTop};${scrollEnd}r\x1B[?25l\x1B[?7l\x1B[${rows - 4};1H\x1B[2K${this.buildBufferContent(w)}\x1B[${rows - 3};1H\x1B[2K${sep}\x1B[${inputRow};1H\x1B[2K${this.promptText}\x1B[${rows - 1};1H\x1B[2K${sep}\x1B[${rows};1H\x1B[2K${this.buildMetricsLine()}\x1B[?7h\x1B[${scrollEnd};1H`);
19136
+ const inputWrap = this.wrapInput(w);
19137
+ let buf = `\x1B[${this.scrollRegionTop};${pos.scrollEnd}r\x1B[?25l\x1B[?7l\x1B[${pos.bufferRow};1H\x1B[2K${this.buildBufferContent(w)}\x1B[${pos.topSepRow};1H\x1B[2K${sep}`;
19138
+ for (let i = 0; i < inputWrap.lines.length; i++) {
19139
+ const row = pos.inputStartRow + i;
19140
+ const prefix = i === 0 ? this.promptText : " ".repeat(this.promptWidth);
19141
+ buf += `\x1B[${row};1H\x1B[2K${prefix}${inputWrap.lines[i]}`;
19142
+ }
19143
+ buf += `\x1B[${pos.bottomSepRow};1H\x1B[2K${sep}\x1B[${pos.metricsRow};1H\x1B[2K${this.buildMetricsLine()}\x1B[?7h\x1B[${pos.scrollEnd};1H`;
19144
+ process.stdout.write(buf);
19069
19145
  } else {
19070
19146
  this.applyScrollRegion();
19071
19147
  this.renderFooterAndPositionInput();
@@ -19093,8 +19169,9 @@ var init_status_bar = __esm({
19093
19169
  if (!this.active)
19094
19170
  return;
19095
19171
  this.writeDepth++;
19172
+ this._brailleSpinner.setMetrics({ isStreaming: true });
19096
19173
  const rows = process.stdout.rows ?? 24;
19097
- const scrollEnd = Math.max(rows - FOOTER_ROWS, this.scrollRegionTop + 1);
19174
+ const scrollEnd = Math.max(rows - this._currentFooterHeight, this.scrollRegionTop + 1);
19098
19175
  process.stdout.write(`\x1B[?25l\x1B[${this.scrollRegionTop};${scrollEnd}r\x1B[${scrollEnd};1H`);
19099
19176
  }
19100
19177
  /**
@@ -19109,6 +19186,7 @@ var init_status_bar = __esm({
19109
19186
  return;
19110
19187
  this.writeDepth = Math.max(0, this.writeDepth - 1);
19111
19188
  if (this.writeDepth === 0) {
19189
+ this._brailleSpinner.setMetrics({ isStreaming: false });
19112
19190
  this.renderFooterAndPositionInput();
19113
19191
  }
19114
19192
  }
@@ -19120,8 +19198,8 @@ var init_status_bar = __esm({
19120
19198
  if (!this.active)
19121
19199
  return;
19122
19200
  const rows = process.stdout.rows ?? 24;
19123
- const inputRow = rows - 2;
19124
- process.stdout.write(`\x1B[${inputRow};1H\x1B[2K`);
19201
+ const pos = this.rowPositions(rows);
19202
+ process.stdout.write(`\x1B[${pos.inputStartRow};1H\x1B[2K`);
19125
19203
  }
19126
19204
  /** Build the metrics line string */
19127
19205
  buildMetricsLine() {
@@ -19162,18 +19240,88 @@ var init_status_bar = __esm({
19162
19240
  // -------------------------------------------------------------------------
19163
19241
  // Private
19164
19242
  // -------------------------------------------------------------------------
19165
- /** Set the DECSTBM scroll region to exclude the footer rows */
19243
+ /** Push current context window usage to the braille spinner */
19244
+ pushSpinnerContextMetrics() {
19245
+ const ctxUsed = this.metrics.estimatedContextTokens;
19246
+ const ctxTotal = this.metrics.contextWindowSize;
19247
+ const contextPct = ctxTotal > 0 ? Math.round(ctxUsed / ctxTotal * 100) : 0;
19248
+ this._brailleSpinner.setMetrics({ contextPct });
19249
+ }
19250
+ /** Compute how many visual lines the current input text occupies */
19251
+ computeInputLineCount(termWidth) {
19252
+ if (!this.inputStateProvider)
19253
+ return 1;
19254
+ const w = termWidth ?? getTermWidth();
19255
+ const availWidth = Math.max(1, w - this.promptWidth);
19256
+ const { line } = this.inputStateProvider();
19257
+ if (line.length <= availWidth)
19258
+ return 1;
19259
+ return Math.ceil(line.length / availWidth);
19260
+ }
19261
+ /** Update _currentFooterHeight based on current input. Returns true if height changed. */
19262
+ updateFooterHeight(termWidth) {
19263
+ const inputLines = this.computeInputLineCount(termWidth);
19264
+ const newHeight = 4 + inputLines;
19265
+ if (newHeight !== this._currentFooterHeight) {
19266
+ this._currentFooterHeight = newHeight;
19267
+ return true;
19268
+ }
19269
+ return false;
19270
+ }
19271
+ /** Compute absolute row positions for all footer elements */
19272
+ rowPositions(rows) {
19273
+ const fh = this._currentFooterHeight;
19274
+ return {
19275
+ scrollEnd: Math.max(rows - fh, this.scrollRegionTop + 1),
19276
+ bufferRow: rows - fh + 1,
19277
+ topSepRow: rows - fh + 2,
19278
+ inputStartRow: rows - fh + 3,
19279
+ bottomSepRow: rows - 1,
19280
+ metricsRow: rows
19281
+ };
19282
+ }
19283
+ /**
19284
+ * Wrap input text into lines of availWidth characters.
19285
+ * Returns the lines, plus cursor position within the wrapped layout.
19286
+ */
19287
+ wrapInput(termWidth) {
19288
+ const availWidth = Math.max(1, termWidth - this.promptWidth);
19289
+ const inputState = this.inputStateProvider?.();
19290
+ const fullLine = inputState?.line ?? "";
19291
+ const cursorPos = inputState?.cursor ?? 0;
19292
+ if (fullLine.length <= availWidth) {
19293
+ return {
19294
+ lines: [fullLine],
19295
+ cursorRow: 0,
19296
+ cursorCol: this.promptWidth + cursorPos + 1
19297
+ };
19298
+ }
19299
+ const lines = [];
19300
+ for (let i = 0; i < fullLine.length; i += availWidth) {
19301
+ lines.push(fullLine.slice(i, i + availWidth));
19302
+ }
19303
+ if (lines.length === 0)
19304
+ lines.push("");
19305
+ const cursorLineIdx = Math.min(Math.floor(cursorPos / availWidth), lines.length - 1);
19306
+ const cursorColInLine = cursorPos - cursorLineIdx * availWidth;
19307
+ return {
19308
+ lines,
19309
+ cursorRow: cursorLineIdx,
19310
+ cursorCol: this.promptWidth + cursorColInLine + 1
19311
+ };
19312
+ }
19313
+ /** Set the DECSTBM scroll region to exclude the dynamic footer rows */
19166
19314
  applyScrollRegion() {
19315
+ this.updateFooterHeight();
19167
19316
  const rows = process.stdout.rows ?? 24;
19168
- const scrollEnd = Math.max(rows - FOOTER_ROWS, this.scrollRegionTop + 1);
19169
- process.stdout.write(`\x1B[${this.scrollRegionTop};${scrollEnd}r\x1B[${scrollEnd};1H`);
19317
+ const pos = this.rowPositions(rows);
19318
+ process.stdout.write(`\x1B[${this.scrollRegionTop};${pos.scrollEnd}r\x1B[${pos.scrollEnd};1H`);
19170
19319
  }
19171
19320
  /**
19172
19321
  * Draw the COMPLETE footer — separators, prompt, metrics — in a single
19173
- * atomic process.stdout.write() call. The prompt is drawn on the input
19174
- * row as part of this write so that cursor positioning never depends on
19175
- * readline's internal `_refreshLine()` (which uses relative cursor
19176
- * movements that conflict with our absolute ANSI positioning).
19322
+ * atomic process.stdout.write() call. Input text wraps across multiple
19323
+ * rows when it exceeds the available width, and the footer dynamically
19324
+ * grows/shrinks to accommodate.
19177
19325
  *
19178
19326
  * Does NOT set DECSTBM — the scroll region is maintained by
19179
19327
  * applyScrollRegion() and beginContentWrite().
@@ -19183,25 +19331,21 @@ var init_status_bar = __esm({
19183
19331
  return;
19184
19332
  const rows = process.stdout.rows ?? 24;
19185
19333
  const w = getTermWidth();
19186
- const sep = c2.dim("\u2500".repeat(w));
19187
- const inputRow = rows - 2;
19188
- const inputState = this.inputStateProvider?.();
19189
- const fullLine = inputState?.line ?? "";
19190
- const cursorPos = inputState?.cursor ?? 0;
19191
- const availWidth = Math.max(1, w - this.promptWidth);
19192
- let visibleText;
19193
- let cursorCol;
19194
- if (fullLine.length <= availWidth) {
19195
- visibleText = fullLine;
19196
- cursorCol = this.promptWidth + cursorPos + 1;
19197
- } else {
19198
- const lookAhead = Math.min(8, Math.floor(availWidth / 4));
19199
- let scrollOffset = cursorPos - (availWidth - lookAhead);
19200
- scrollOffset = Math.max(0, Math.min(scrollOffset, fullLine.length - availWidth));
19201
- visibleText = fullLine.slice(scrollOffset, scrollOffset + availWidth);
19202
- cursorCol = this.promptWidth + (cursorPos - scrollOffset) + 1;
19334
+ const heightChanged = this.updateFooterHeight(w);
19335
+ const pos = this.rowPositions(rows);
19336
+ if (heightChanged) {
19337
+ process.stdout.write(`\x1B[${this.scrollRegionTop};${pos.scrollEnd}r`);
19203
19338
  }
19204
- const buf = `\x1B[?7l\x1B[${rows - 4};1H\x1B[2K${this.buildBufferContent(w)}\x1B[${rows - 3};1H\x1B[2K${sep}\x1B[${inputRow};1H\x1B[2K${this.promptText}${visibleText}\x1B[${rows - 1};1H\x1B[2K${sep}\x1B[${rows};1H\x1B[2K${this.buildMetricsLine()}\x1B[?7h\x1B[?25h\x1B[${inputRow};${cursorCol}H`;
19339
+ const sep = c2.dim("\u2500".repeat(w));
19340
+ const inputWrap = this.wrapInput(w);
19341
+ let buf = `\x1B[?7l\x1B[${pos.bufferRow};1H\x1B[2K${this.buildBufferContent(w)}\x1B[${pos.topSepRow};1H\x1B[2K${sep}`;
19342
+ for (let i = 0; i < inputWrap.lines.length; i++) {
19343
+ const row = pos.inputStartRow + i;
19344
+ const prefix = i === 0 ? this.promptText : " ".repeat(this.promptWidth);
19345
+ buf += `\x1B[${row};1H\x1B[2K${prefix}${inputWrap.lines[i]}`;
19346
+ }
19347
+ const cursorTermRow = pos.inputStartRow + inputWrap.cursorRow;
19348
+ buf += `\x1B[${pos.bottomSepRow};1H\x1B[2K${sep}\x1B[${pos.metricsRow};1H\x1B[2K${this.buildMetricsLine()}\x1B[?7h\x1B[?25h\x1B[${cursorTermRow};${inputWrap.cursorCol}H`;
19205
19349
  process.stdout.write(buf);
19206
19350
  }
19207
19351
  /**
@@ -19210,50 +19354,66 @@ var init_status_bar = __esm({
19210
19354
  * move the cursor away from where readline left it.
19211
19355
  * Uses DEC DECSC/DECRC (\x1B7/\x1B8) for save/restore in a single write.
19212
19356
  *
19357
+ * If the footer height has changed (due to input wrapping), falls back
19358
+ * to a full renderFooterAndPositionInput() instead.
19359
+ *
19213
19360
  * IMPORTANT: Does NOT set DECSTBM here — setting the scroll region between
19214
19361
  * cursor save/restore corrupts the restore position on many terminals.
19215
- * The scroll region is enforced by beginContentWrite() and renderFooterAndPositionInput().
19216
19362
  */
19217
19363
  renderFooterPreserveCursor() {
19218
19364
  if (!this.active)
19219
19365
  return;
19366
+ if (this.updateFooterHeight()) {
19367
+ if (this.writeDepth === 0) {
19368
+ this.renderFooterAndPositionInput();
19369
+ }
19370
+ return;
19371
+ }
19220
19372
  const rows = process.stdout.rows ?? 24;
19221
19373
  const w = getTermWidth();
19374
+ const pos = this.rowPositions(rows);
19222
19375
  const sep = c2.dim("\u2500".repeat(w));
19223
- const buf = `\x1B7\x1B[?7l\x1B[${rows - 4};1H\x1B[2K${this.buildBufferContent(w)}\x1B[${rows - 3};1H\x1B[2K${sep}\x1B[${rows - 1};1H\x1B[2K${sep}\x1B[${rows};1H\x1B[2K${this.buildMetricsLine()}\x1B[?7h\x1B8`;
19376
+ const buf = `\x1B7\x1B[?7l\x1B[${pos.bufferRow};1H\x1B[2K${this.buildBufferContent(w)}\x1B[${pos.topSepRow};1H\x1B[2K${sep}\x1B[${pos.bottomSepRow};1H\x1B[2K${sep}\x1B[${pos.metricsRow};1H\x1B[2K${this.buildMetricsLine()}\x1B[?7h\x1B8`;
19224
19377
  process.stdout.write(buf);
19225
19378
  }
19226
19379
  /**
19227
- * Render ONLY the input row during an active content write (streaming).
19380
+ * Render the input rows during an active content write (streaming).
19228
19381
  * Uses DEC save/restore cursor so the streaming cursor position is preserved.
19229
- * Briefly shows the cursor on the input row so the user can see their typing.
19382
+ * If footer height changed, also updates DECSTBM and redraws full footer.
19230
19383
  */
19231
19384
  renderInputRowDuringStream() {
19232
19385
  if (!this.active || !this.inputStateProvider)
19233
19386
  return;
19234
19387
  const rows = process.stdout.rows ?? 24;
19235
19388
  const w = getTermWidth();
19236
- const inputRow = rows - 2;
19237
- const inputState = this.inputStateProvider();
19238
- const fullLine = inputState.line;
19239
- const cursorPos = inputState.cursor;
19240
- const availWidth = Math.max(1, w - this.promptWidth);
19241
- let visibleText;
19242
- let cursorCol;
19243
- if (fullLine.length <= availWidth) {
19244
- visibleText = fullLine;
19245
- cursorCol = this.promptWidth + cursorPos + 1;
19389
+ const heightChanged = this.updateFooterHeight(w);
19390
+ const pos = this.rowPositions(rows);
19391
+ const inputWrap = this.wrapInput(w);
19392
+ let buf = "\x1B7\x1B[?7l";
19393
+ if (heightChanged) {
19394
+ buf += `\x1B[${this.scrollRegionTop};${pos.scrollEnd}r`;
19395
+ const sep = c2.dim("\u2500".repeat(w));
19396
+ buf += `\x1B[${pos.bufferRow};1H\x1B[2K${this.buildBufferContent(w)}`;
19397
+ buf += `\x1B[${pos.topSepRow};1H\x1B[2K${sep}`;
19398
+ for (let i = 0; i < inputWrap.lines.length; i++) {
19399
+ const row = pos.inputStartRow + i;
19400
+ const prefix = i === 0 ? this.promptText : " ".repeat(this.promptWidth);
19401
+ buf += `\x1B[${row};1H\x1B[2K${prefix}${inputWrap.lines[i]}`;
19402
+ }
19403
+ buf += `\x1B[${pos.bottomSepRow};1H\x1B[2K${sep}`;
19404
+ buf += `\x1B[${pos.metricsRow};1H\x1B[2K${this.buildMetricsLine()}`;
19246
19405
  } else {
19247
- const lookAhead = Math.min(8, Math.floor(availWidth / 4));
19248
- let scrollOffset = cursorPos - (availWidth - lookAhead);
19249
- scrollOffset = Math.max(0, Math.min(scrollOffset, fullLine.length - availWidth));
19250
- visibleText = fullLine.slice(scrollOffset, scrollOffset + availWidth);
19251
- cursorCol = this.promptWidth + (cursorPos - scrollOffset) + 1;
19406
+ for (let i = 0; i < inputWrap.lines.length; i++) {
19407
+ const row = pos.inputStartRow + i;
19408
+ const prefix = i === 0 ? this.promptText : " ".repeat(this.promptWidth);
19409
+ buf += `\x1B[${row};1H\x1B[2K${prefix}${inputWrap.lines[i]}`;
19410
+ }
19252
19411
  }
19253
- process.stdout.write(`\x1B7\x1B[?7l\x1B[${inputRow};1H\x1B[2K${this.promptText}${visibleText}\x1B[?7h\x1B8`);
19412
+ buf += "\x1B[?7h\x1B8";
19413
+ process.stdout.write(buf);
19254
19414
  }
19255
19415
  /**
19256
- * Build the content for the buffer line (rows − 4).
19416
+ * Build the content for the buffer line.
19257
19417
  * Returns braille animation when processing, empty string when idle.
19258
19418
  */
19259
19419
  buildBufferContent(width) {
@@ -19263,7 +19423,7 @@ var init_status_bar = __esm({
19263
19423
  return "";
19264
19424
  }
19265
19425
  /**
19266
- * Render ONLY the buffer line (rows − 4) using DEC save/restore cursor.
19426
+ * Render ONLY the buffer line using DEC save/restore cursor.
19267
19427
  * Called by the braille spinner timer without disrupting scroll or input.
19268
19428
  */
19269
19429
  renderBufferLine() {
@@ -19271,15 +19431,15 @@ var init_status_bar = __esm({
19271
19431
  return;
19272
19432
  const rows = process.stdout.rows ?? 24;
19273
19433
  const w = getTermWidth();
19274
- const bufferRow = rows - 4;
19434
+ const pos = this.rowPositions(rows);
19275
19435
  const content = this.buildBufferContent(w);
19276
- process.stdout.write(`\x1B7\x1B[?7l\x1B[${bufferRow};1H\x1B[2K${content}\x1B[?7h\x1B8`);
19436
+ process.stdout.write(`\x1B7\x1B[?7l\x1B[${pos.bufferRow};1H\x1B[2K${content}\x1B[?7h\x1B8`);
19277
19437
  }
19278
19438
  /**
19279
19439
  * Hook into process.stdin to redraw footer after every keystroke.
19280
19440
  * Since readline's output is suppressed (redirected to a no-op stream),
19281
19441
  * this hook is responsible for rendering typed text on the input row.
19282
- * During streaming (writeDepth > 0), only the input row is updated using
19442
+ * During streaming (writeDepth > 0), only the input rows are updated using
19283
19443
  * cursor save/restore so the streaming position isn't disrupted.
19284
19444
  * When idle (writeDepth === 0), the full footer is redrawn.
19285
19445
  */
@@ -19899,7 +20059,7 @@ async function startInteractive(config, repoPath) {
19899
20059
  let sessionToolCallCount = 0;
19900
20060
  let sessionSudoPassword = null;
19901
20061
  let sudoPromptPending = false;
19902
- const idlePrompt = `${c2.bold(c2.white("\u{1F896} "))}`;
20062
+ const idlePrompt = `${c2.bold(c2.white("\u276F "))}`;
19903
20063
  const activePrompt = `${c2.bold(c2.white("+ "))}`;
19904
20064
  const rl = readline2.createInterface({
19905
20065
  input: process.stdin,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-agents-ai",
3
- "version": "0.29.0",
3
+ "version": "0.30.0",
4
4
  "description": "AI coding agent powered by open-source models (Ollama/vLLM) — interactive TUI with agentic tool-calling loop",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",