open-agents-ai 0.28.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 +285 -112
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -10413,32 +10413,31 @@ Rules:
10413
10413
  * Build a self-eval prompt for the agent when approaching timeout.
10414
10414
  * Returns the prompt to inject. The agent will respond with a plan.
10415
10415
  */
10416
- buildTimeoutSelfEvalPrompt(elapsedMs2, toolCallCount, repetitionScore, remainingMs) {
10416
+ buildHealthCheckPrompt(elapsedMs2, toolCallCount, repetitionScore, checkNumber) {
10417
10417
  const elapsedMin = (elapsedMs2 / 6e4).toFixed(1);
10418
- const remainingMin = (remainingMs / 6e4).toFixed(1);
10419
10418
  const stuckWarning = repetitionScore > 0.5 ? `
10420
10419
  \u26A0 REPETITION DETECTED: Your recent tool calls are ${Math.round(repetitionScore * 100)}% repetitive. You may be stuck in a loop.` : "";
10421
- return `[TIMEOUT APPROACHING \u2014 Self-Assessment Required]
10420
+ return `[HEALTH CHECK #${checkNumber} \u2014 Progress Assessment]
10422
10421
 
10423
- You have been working for ${elapsedMin} minutes with ${toolCallCount} tool calls. You have approximately ${remainingMin} minutes remaining.${stuckWarning}
10422
+ You have been working for ${elapsedMin} minutes with ${toolCallCount} tool calls. There is no time limit \u2014 take as long as you need.${stuckWarning}
10424
10423
 
10425
- ASSESS YOUR SITUATION and choose ONE action:
10424
+ Briefly assess your situation and choose ONE action:
10426
10425
 
10427
- 1. CONTINUE \u2014 If you are making genuine progress on a long task, say "CONTINUE" and briefly explain what progress you've made and what remains. You will get an extended time window.
10426
+ 1. CONTINUE \u2014 If you are making progress, briefly note what you've done and what remains. Keep working.
10428
10427
 
10429
- 2. PIVOT \u2014 If your current approach isn't working, say "PIVOT" and describe a completely different strategy. Then immediately try that new approach.
10428
+ 2. PIVOT \u2014 If your current approach isn't working, describe a different strategy and immediately try it.
10430
10429
 
10431
- 3. CHECKPOINT \u2014 If you've made partial progress, say "CHECKPOINT" and call task_complete with a summary of what you accomplished so far. The user can continue from where you left off.
10430
+ 3. CHECKPOINT \u2014 If you've made partial progress and want to save it, call task_complete with a summary. The user can continue later.
10432
10431
 
10433
- Respond with your assessment, then take action. Do NOT just say you'll continue without explaining concrete progress. Be honest about whether you're stuck.`;
10432
+ Respond with your assessment, then take action.`;
10434
10433
  }
10435
10434
  /** Run a task through the agentic loop */
10436
10435
  async run(task, context) {
10437
10436
  const start = Date.now();
10438
10437
  const taskTimeoutMs = this.options.taskTimeoutMs;
10439
- const softDeadline = start + Math.floor(taskTimeoutMs * 0.8);
10440
- const hardDeadline = start + Math.floor(taskTimeoutMs * 1.5);
10441
- let softTimeoutTriggered = false;
10438
+ const selfEvalInterval = taskTimeoutMs;
10439
+ let nextSelfEval = start + selfEvalInterval;
10440
+ let selfEvalCount = 0;
10442
10441
  const toolCallLog = [];
10443
10442
  this.aborted = false;
10444
10443
  this.pendingUserMessages.length = 0;
@@ -10467,24 +10466,20 @@ TASK: ${task}` : task }
10467
10466
  break;
10468
10467
  }
10469
10468
  const now = Date.now();
10470
- if (now > hardDeadline) {
10471
- this.emit({ type: "error", content: "Task hard timeout reached", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
10472
- break;
10473
- }
10474
- if (!softTimeoutTriggered && now > softDeadline) {
10475
- softTimeoutTriggered = true;
10469
+ if (now > nextSelfEval) {
10470
+ selfEvalCount++;
10476
10471
  const elapsed = now - start;
10477
- const remaining = hardDeadline - now;
10478
10472
  const repetitionScore = this.detectRepetition(toolCallLog);
10479
10473
  this.emit({
10480
10474
  type: "compaction",
10481
- content: `Timeout approaching (${(elapsed / 6e4).toFixed(1)}m elapsed) \u2014 injecting self-assessment`,
10475
+ content: `Health check #${selfEvalCount} (${(elapsed / 6e4).toFixed(1)}m elapsed) \u2014 assessing progress`,
10482
10476
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
10483
10477
  });
10484
10478
  messages.push({
10485
10479
  role: "user",
10486
- content: this.buildTimeoutSelfEvalPrompt(elapsed, toolCallCount, repetitionScore, remaining)
10480
+ content: this.buildHealthCheckPrompt(elapsed, toolCallCount, repetitionScore, selfEvalCount)
10487
10481
  });
10482
+ nextSelfEval = now + selfEvalInterval;
10488
10483
  }
10489
10484
  while (this.pendingUserMessages.length > 0) {
10490
10485
  const userMsg = this.pendingUserMessages.shift();
@@ -10693,7 +10688,7 @@ ${result.output.length > maxLen ? this.foldOutput(result.output, maxLen) : resul
10693
10688
  });
10694
10689
  }
10695
10690
  }
10696
- while (!completed && !this.aborted && this.options.bruteForce && bruteForceCycle < this.options.bruteForceMaxCycles && Date.now() < hardDeadline) {
10691
+ while (!completed && !this.aborted && this.options.bruteForce && bruteForceCycle < this.options.bruteForceMaxCycles) {
10697
10692
  bruteForceCycle++;
10698
10693
  const totalTurns = messages.filter((m) => m.role === "assistant").length;
10699
10694
  this.emit({
@@ -10727,24 +10722,20 @@ You have ${this.options.maxTurns} more turns. Continue making progress. Call tas
10727
10722
  break;
10728
10723
  }
10729
10724
  const bfNow = Date.now();
10730
- if (bfNow > hardDeadline) {
10731
- this.emit({ type: "error", content: "Task hard timeout reached", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
10732
- break;
10733
- }
10734
- if (!softTimeoutTriggered && bfNow > softDeadline) {
10735
- softTimeoutTriggered = true;
10725
+ if (bfNow > nextSelfEval) {
10726
+ selfEvalCount++;
10736
10727
  const elapsed = bfNow - start;
10737
- const remaining = hardDeadline - bfNow;
10738
10728
  const repetitionScore = this.detectRepetition(toolCallLog);
10739
10729
  this.emit({
10740
10730
  type: "compaction",
10741
- content: `Timeout approaching (${(elapsed / 6e4).toFixed(1)}m elapsed) \u2014 injecting self-assessment`,
10731
+ content: `Health check #${selfEvalCount} (${(elapsed / 6e4).toFixed(1)}m elapsed) \u2014 assessing progress`,
10742
10732
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
10743
10733
  });
10744
10734
  messages.push({
10745
10735
  role: "user",
10746
- content: this.buildTimeoutSelfEvalPrompt(elapsed, toolCallCount, repetitionScore, remaining)
10736
+ content: this.buildHealthCheckPrompt(elapsed, toolCallCount, repetitionScore, selfEvalCount)
10747
10737
  });
10738
+ nextSelfEval = bfNow + selfEvalInterval;
10748
10739
  }
10749
10740
  while (this.pendingUserMessages.length > 0) {
10750
10741
  const userMsg = this.pendingUserMessages.shift();
@@ -11416,8 +11407,7 @@ ${newerSummary}` : newerSummary;
11416
11407
  const resp = await fetch(`${this.baseUrl}/v1/chat/completions`, {
11417
11408
  method: "POST",
11418
11409
  headers: this.authHeaders(),
11419
- body: JSON.stringify(body),
11420
- signal: AbortSignal.timeout(request.timeoutMs)
11410
+ body: JSON.stringify(body)
11421
11411
  });
11422
11412
  if (!resp.ok) {
11423
11413
  const text = await resp.text().catch(() => "");
@@ -11477,8 +11467,7 @@ ${newerSummary}` : newerSummary;
11477
11467
  const resp = await fetch(`${this.baseUrl}/v1/chat/completions`, {
11478
11468
  method: "POST",
11479
11469
  headers: this.authHeaders(),
11480
- body: JSON.stringify(body),
11481
- signal: AbortSignal.timeout(request.timeoutMs)
11470
+ body: JSON.stringify(body)
11482
11471
  });
11483
11472
  if (!resp.ok) {
11484
11473
  const text = await resp.text().catch(() => "");
@@ -18739,7 +18728,7 @@ function themeForTool(toolName) {
18739
18728
  return THEME_DEFAULT;
18740
18729
  }
18741
18730
  }
18742
- 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;
18743
18732
  var init_braille_spinner = __esm({
18744
18733
  "packages/cli/dist/tui/braille-spinner.js"() {
18745
18734
  "use strict";
@@ -18796,11 +18785,17 @@ var init_braille_spinner = __esm({
18796
18785
  ramp: [237, 173, 174, 179, 180, 215, 216, 222, 229],
18797
18786
  speed: 2
18798
18787
  };
18788
+ DEFAULT_METRICS = {
18789
+ contextPct: 0,
18790
+ tokenRate: 0,
18791
+ isStreaming: false
18792
+ };
18799
18793
  BrailleSpinner = class {
18800
18794
  frame = 0;
18801
18795
  timer = null;
18802
18796
  theme = THEME_DEFAULT;
18803
18797
  colorRamp = buildColorRamp(THEME_DEFAULT.ramp);
18798
+ _metrics = { ...DEFAULT_METRICS };
18804
18799
  /** Start the animation, calling `onFrame` every tick (~80 ms). */
18805
18800
  start(onFrame) {
18806
18801
  this.frame = 0;
@@ -18817,14 +18812,15 @@ var init_braille_spinner = __esm({
18817
18812
  }
18818
18813
  this.frame = 0;
18819
18814
  this.setTool(null);
18815
+ this._metrics = { ...DEFAULT_METRICS };
18820
18816
  }
18821
18817
  /** Whether the animation timer is active. */
18822
18818
  get isRunning() {
18823
18819
  return this.timer !== null;
18824
18820
  }
18825
18821
  /**
18826
- * Set the active tool, changing the animation color theme and speed.
18827
- * 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).
18828
18824
  */
18829
18825
  setTool(toolName) {
18830
18826
  const next = themeForTool(toolName);
@@ -18833,21 +18829,68 @@ var init_braille_spinner = __esm({
18833
18829
  this.colorRamp = buildColorRamp(next.ramp);
18834
18830
  }
18835
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
+ }
18836
18844
  /**
18837
18845
  * Render the current animation frame as an ANSI-colored string.
18838
- * Each column gets a braille character whose density and color vary
18839
- * sinusoidally, shifted by `this.frame` to create movement.
18840
- * 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.
18841
18854
  */
18842
18855
  render(width) {
18843
18856
  const cycleLen = WAVE.length;
18844
- 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;
18845
18871
  let buf = "";
18846
18872
  let lastColor = -1;
18847
18873
  for (let col = 0; col < width; col++) {
18848
- const idx = (col * speed + this.frame) % cycleLen;
18849
- const ch = WAVE[idx];
18850
- 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];
18851
18894
  if (color !== lastColor) {
18852
18895
  buf += `\x1B[38;5;${color}m`;
18853
18896
  lastColor = color;
@@ -18862,13 +18905,12 @@ var init_braille_spinner = __esm({
18862
18905
  });
18863
18906
 
18864
18907
  // packages/cli/dist/tui/status-bar.js
18865
- var FOOTER_ROWS, StatusBar;
18908
+ var StatusBar;
18866
18909
  var init_status_bar = __esm({
18867
18910
  "packages/cli/dist/tui/status-bar.js"() {
18868
18911
  "use strict";
18869
18912
  init_render();
18870
18913
  init_braille_spinner();
18871
- FOOTER_ROWS = 5;
18872
18914
  StatusBar = class {
18873
18915
  metrics = {
18874
18916
  promptTokens: 0,
@@ -18909,6 +18951,10 @@ var init_status_bar = __esm({
18909
18951
  /** Whether agent is actively processing (braille animation) */
18910
18952
  _processing = false;
18911
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;
18912
18958
  /**
18913
18959
  * Provide a callback that returns readline's current input state.
18914
18960
  * StatusBar uses this to render typed text and position the cursor
@@ -18950,11 +18996,13 @@ var init_status_bar = __esm({
18950
18996
  return;
18951
18997
  this._processing = active;
18952
18998
  if (active) {
18999
+ this._brailleSpinner.setMetrics({ isStreaming: true });
18953
19000
  this._brailleSpinner.start(() => {
18954
19001
  if (this.active)
18955
19002
  this.renderBufferLine();
18956
19003
  });
18957
19004
  } else {
19005
+ this._brailleSpinner.setMetrics({ isStreaming: false, tokenRate: 0 });
18958
19006
  this._brailleSpinner.stop();
18959
19007
  if (this.active)
18960
19008
  this.renderBufferLine();
@@ -18994,15 +19042,43 @@ var init_status_bar = __esm({
18994
19042
  this.metrics.totalTokens = update.totalTokens;
18995
19043
  if (update.estimatedContextTokens !== void 0)
18996
19044
  this.metrics.estimatedContextTokens = update.estimatedContextTokens;
19045
+ this._streamingTokens = 0;
19046
+ this._streamStartTime = 0;
19047
+ this.pushSpinnerContextMetrics();
19048
+ this._brailleSpinner.setMetrics({ tokenRate: 0, isStreaming: false });
18997
19049
  if (this.active)
18998
19050
  this.renderFooterPreserveCursor();
18999
19051
  }
19052
+ /** Running count of tokens estimated during streaming (reset on authoritative updateMetrics) */
19053
+ _streamingTokens = 0;
19054
+ _streamThrottleTimer = null;
19055
+ /** Increment the live streaming token counter (throttled re-render at 100ms) */
19056
+ incrementStreamingTokens(count) {
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 });
19063
+ if (!this._streamThrottleTimer && this.active) {
19064
+ this._streamThrottleTimer = setTimeout(() => {
19065
+ this._streamThrottleTimer = null;
19066
+ if (this.active)
19067
+ this.renderFooterPreserveCursor();
19068
+ }, 100);
19069
+ }
19070
+ }
19071
+ /** Get the effective completion tokens (authoritative + live streaming estimate) */
19072
+ get effectiveCompletionTokens() {
19073
+ return this.metrics.completionTokens + this._streamingTokens;
19074
+ }
19000
19075
  /** Reset metrics (e.g. on session start) */
19001
19076
  resetMetrics() {
19002
19077
  this.metrics.promptTokens = 0;
19003
19078
  this.metrics.completionTokens = 0;
19004
19079
  this.metrics.totalTokens = 0;
19005
19080
  this.metrics.estimatedContextTokens = 0;
19081
+ this.pushSpinnerContextMetrics();
19006
19082
  if (this.active)
19007
19083
  this.renderFooterPreserveCursor();
19008
19084
  }
@@ -19030,7 +19106,7 @@ var init_status_bar = __esm({
19030
19106
  }
19031
19107
  /**
19032
19108
  * Set the prompt text that StatusBar draws on the input row.
19033
- * Called by the REPL whenever the prompt changes (idle active).
19109
+ * Called by the REPL whenever the prompt changes (idle <-> active).
19034
19110
  * The prompt is rendered as part of the atomic footer write so cursor
19035
19111
  * positioning never depends on readline's internal tracking.
19036
19112
  * @param text The ANSI-colored prompt string (e.g. "> " or "+ ")
@@ -19043,21 +19119,29 @@ var init_status_bar = __esm({
19043
19119
  this.renderFooterAndPositionInput();
19044
19120
  }
19045
19121
  }
19046
- /** Number of rows reserved at the bottom */
19122
+ /** Number of rows reserved at the bottom (dynamic based on input wrapping) */
19047
19123
  get reservedRows() {
19048
- return FOOTER_ROWS;
19124
+ return this._currentFooterHeight;
19049
19125
  }
19050
19126
  /** Handle terminal resize — reapply scroll region and redraw footer */
19051
19127
  handleResize() {
19052
19128
  if (!this.active)
19053
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));
19054
19135
  if (this.writeDepth > 0) {
19055
- const rows = process.stdout.rows ?? 24;
19056
- const scrollEnd = Math.max(rows - FOOTER_ROWS, this.scrollRegionTop + 1);
19057
- const w = getTermWidth();
19058
- const sep = c2.dim("\u2500".repeat(w));
19059
- const inputRow = rows - 2;
19060
- 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);
19061
19145
  } else {
19062
19146
  this.applyScrollRegion();
19063
19147
  this.renderFooterAndPositionInput();
@@ -19085,8 +19169,9 @@ var init_status_bar = __esm({
19085
19169
  if (!this.active)
19086
19170
  return;
19087
19171
  this.writeDepth++;
19172
+ this._brailleSpinner.setMetrics({ isStreaming: true });
19088
19173
  const rows = process.stdout.rows ?? 24;
19089
- const scrollEnd = Math.max(rows - FOOTER_ROWS, this.scrollRegionTop + 1);
19174
+ const scrollEnd = Math.max(rows - this._currentFooterHeight, this.scrollRegionTop + 1);
19090
19175
  process.stdout.write(`\x1B[?25l\x1B[${this.scrollRegionTop};${scrollEnd}r\x1B[${scrollEnd};1H`);
19091
19176
  }
19092
19177
  /**
@@ -19101,6 +19186,7 @@ var init_status_bar = __esm({
19101
19186
  return;
19102
19187
  this.writeDepth = Math.max(0, this.writeDepth - 1);
19103
19188
  if (this.writeDepth === 0) {
19189
+ this._brailleSpinner.setMetrics({ isStreaming: false });
19104
19190
  this.renderFooterAndPositionInput();
19105
19191
  }
19106
19192
  }
@@ -19112,8 +19198,8 @@ var init_status_bar = __esm({
19112
19198
  if (!this.active)
19113
19199
  return;
19114
19200
  const rows = process.stdout.rows ?? 24;
19115
- const inputRow = rows - 2;
19116
- process.stdout.write(`\x1B[${inputRow};1H\x1B[2K`);
19201
+ const pos = this.rowPositions(rows);
19202
+ process.stdout.write(`\x1B[${pos.inputStartRow};1H\x1B[2K`);
19117
19203
  }
19118
19204
  /** Build the metrics line string */
19119
19205
  buildMetricsLine() {
@@ -19122,7 +19208,8 @@ var init_status_bar = __esm({
19122
19208
  const pipe = pastel2(60, " \u2502 ");
19123
19209
  const tokIn = m.promptTokens > 0 ? m.promptTokens.toLocaleString() : `~${Math.max(m.estimatedContextTokens, 0).toLocaleString()}`;
19124
19210
  const tokInLabel = pastel2(117, "In: ") + c2.bold(tokIn);
19125
- const tokOut = m.completionTokens > 0 ? m.completionTokens.toLocaleString() : `~${Math.ceil(m.totalTokens > 0 ? m.totalTokens - m.promptTokens : m.estimatedContextTokens * 0.3).toLocaleString()}`;
19211
+ const effectiveOut = this.effectiveCompletionTokens;
19212
+ const tokOut = effectiveOut > 0 ? effectiveOut.toLocaleString() : `~${Math.ceil(m.totalTokens > 0 ? m.totalTokens - m.promptTokens : m.estimatedContextTokens * 0.3).toLocaleString()}`;
19126
19213
  const tokOutLabel = pastel2(151, "Out: ") + c2.bold(tokOut);
19127
19214
  const ctxUsed = m.estimatedContextTokens;
19128
19215
  const ctxTotal = m.contextWindowSize;
@@ -19153,18 +19240,88 @@ var init_status_bar = __esm({
19153
19240
  // -------------------------------------------------------------------------
19154
19241
  // Private
19155
19242
  // -------------------------------------------------------------------------
19156
- /** 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 */
19157
19314
  applyScrollRegion() {
19315
+ this.updateFooterHeight();
19158
19316
  const rows = process.stdout.rows ?? 24;
19159
- const scrollEnd = Math.max(rows - FOOTER_ROWS, this.scrollRegionTop + 1);
19160
- 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`);
19161
19319
  }
19162
19320
  /**
19163
19321
  * Draw the COMPLETE footer — separators, prompt, metrics — in a single
19164
- * atomic process.stdout.write() call. The prompt is drawn on the input
19165
- * row as part of this write so that cursor positioning never depends on
19166
- * readline's internal `_refreshLine()` (which uses relative cursor
19167
- * 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.
19168
19325
  *
19169
19326
  * Does NOT set DECSTBM — the scroll region is maintained by
19170
19327
  * applyScrollRegion() and beginContentWrite().
@@ -19174,25 +19331,21 @@ var init_status_bar = __esm({
19174
19331
  return;
19175
19332
  const rows = process.stdout.rows ?? 24;
19176
19333
  const w = getTermWidth();
19177
- const sep = c2.dim("\u2500".repeat(w));
19178
- const inputRow = rows - 2;
19179
- const inputState = this.inputStateProvider?.();
19180
- const fullLine = inputState?.line ?? "";
19181
- const cursorPos = inputState?.cursor ?? 0;
19182
- const availWidth = Math.max(1, w - this.promptWidth);
19183
- let visibleText;
19184
- let cursorCol;
19185
- if (fullLine.length <= availWidth) {
19186
- visibleText = fullLine;
19187
- cursorCol = this.promptWidth + cursorPos + 1;
19188
- } else {
19189
- const lookAhead = Math.min(8, Math.floor(availWidth / 4));
19190
- let scrollOffset = cursorPos - (availWidth - lookAhead);
19191
- scrollOffset = Math.max(0, Math.min(scrollOffset, fullLine.length - availWidth));
19192
- visibleText = fullLine.slice(scrollOffset, scrollOffset + availWidth);
19193
- 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`);
19194
19338
  }
19195
- 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`;
19196
19349
  process.stdout.write(buf);
19197
19350
  }
19198
19351
  /**
@@ -19201,50 +19354,66 @@ var init_status_bar = __esm({
19201
19354
  * move the cursor away from where readline left it.
19202
19355
  * Uses DEC DECSC/DECRC (\x1B7/\x1B8) for save/restore in a single write.
19203
19356
  *
19357
+ * If the footer height has changed (due to input wrapping), falls back
19358
+ * to a full renderFooterAndPositionInput() instead.
19359
+ *
19204
19360
  * IMPORTANT: Does NOT set DECSTBM here — setting the scroll region between
19205
19361
  * cursor save/restore corrupts the restore position on many terminals.
19206
- * The scroll region is enforced by beginContentWrite() and renderFooterAndPositionInput().
19207
19362
  */
19208
19363
  renderFooterPreserveCursor() {
19209
19364
  if (!this.active)
19210
19365
  return;
19366
+ if (this.updateFooterHeight()) {
19367
+ if (this.writeDepth === 0) {
19368
+ this.renderFooterAndPositionInput();
19369
+ }
19370
+ return;
19371
+ }
19211
19372
  const rows = process.stdout.rows ?? 24;
19212
19373
  const w = getTermWidth();
19374
+ const pos = this.rowPositions(rows);
19213
19375
  const sep = c2.dim("\u2500".repeat(w));
19214
- 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`;
19215
19377
  process.stdout.write(buf);
19216
19378
  }
19217
19379
  /**
19218
- * Render ONLY the input row during an active content write (streaming).
19380
+ * Render the input rows during an active content write (streaming).
19219
19381
  * Uses DEC save/restore cursor so the streaming cursor position is preserved.
19220
- * 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.
19221
19383
  */
19222
19384
  renderInputRowDuringStream() {
19223
19385
  if (!this.active || !this.inputStateProvider)
19224
19386
  return;
19225
19387
  const rows = process.stdout.rows ?? 24;
19226
19388
  const w = getTermWidth();
19227
- const inputRow = rows - 2;
19228
- const inputState = this.inputStateProvider();
19229
- const fullLine = inputState.line;
19230
- const cursorPos = inputState.cursor;
19231
- const availWidth = Math.max(1, w - this.promptWidth);
19232
- let visibleText;
19233
- let cursorCol;
19234
- if (fullLine.length <= availWidth) {
19235
- visibleText = fullLine;
19236
- 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()}`;
19237
19405
  } else {
19238
- const lookAhead = Math.min(8, Math.floor(availWidth / 4));
19239
- let scrollOffset = cursorPos - (availWidth - lookAhead);
19240
- scrollOffset = Math.max(0, Math.min(scrollOffset, fullLine.length - availWidth));
19241
- visibleText = fullLine.slice(scrollOffset, scrollOffset + availWidth);
19242
- 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
+ }
19243
19411
  }
19244
- 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);
19245
19414
  }
19246
19415
  /**
19247
- * Build the content for the buffer line (rows − 4).
19416
+ * Build the content for the buffer line.
19248
19417
  * Returns braille animation when processing, empty string when idle.
19249
19418
  */
19250
19419
  buildBufferContent(width) {
@@ -19254,7 +19423,7 @@ var init_status_bar = __esm({
19254
19423
  return "";
19255
19424
  }
19256
19425
  /**
19257
- * Render ONLY the buffer line (rows − 4) using DEC save/restore cursor.
19426
+ * Render ONLY the buffer line using DEC save/restore cursor.
19258
19427
  * Called by the braille spinner timer without disrupting scroll or input.
19259
19428
  */
19260
19429
  renderBufferLine() {
@@ -19262,15 +19431,15 @@ var init_status_bar = __esm({
19262
19431
  return;
19263
19432
  const rows = process.stdout.rows ?? 24;
19264
19433
  const w = getTermWidth();
19265
- const bufferRow = rows - 4;
19434
+ const pos = this.rowPositions(rows);
19266
19435
  const content = this.buildBufferContent(w);
19267
- 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`);
19268
19437
  }
19269
19438
  /**
19270
19439
  * Hook into process.stdin to redraw footer after every keystroke.
19271
19440
  * Since readline's output is suppressed (redirected to a no-op stream),
19272
19441
  * this hook is responsible for rendering typed text on the input row.
19273
- * During streaming (writeDepth > 0), only the input row is updated using
19442
+ * During streaming (writeDepth > 0), only the input rows are updated using
19274
19443
  * cursor save/restore so the streaming position isn't disrupted.
19275
19444
  * When idle (writeDepth === 0), the full footer is redrawn.
19276
19445
  */
@@ -19505,7 +19674,7 @@ function startTask(task, config, repoRoot, voice, stream, taskStores, bruteForce
19505
19674
  streamEnabled: stream?.enabled ?? false,
19506
19675
  bruteForce: bruteForce ?? true,
19507
19676
  bruteForceMaxCycles: 100,
19508
- // effectively unlimited — hard timeout is the real bound
19677
+ // effectively unlimited — no hard timeout, agent runs until complete or aborted
19509
19678
  contextWindowSize: contextWindowSize ?? 0
19510
19679
  });
19511
19680
  const tools = buildTools(repoRoot, config);
@@ -19594,6 +19763,10 @@ function startTask(task, config, repoRoot, voice, stream, taskStores, bruteForce
19594
19763
  if (stream?.enabled) {
19595
19764
  stream.renderer.write(event.content ?? "", event.streamKind ?? "content");
19596
19765
  }
19766
+ if (statusBar && event.content) {
19767
+ const estimatedNewTokens = Math.max(1, Math.ceil(event.content.length / 4));
19768
+ statusBar.incrementStreamingTokens(estimatedNewTokens);
19769
+ }
19597
19770
  break;
19598
19771
  case "stream_end":
19599
19772
  if (stream?.enabled) {
@@ -19886,7 +20059,7 @@ async function startInteractive(config, repoPath) {
19886
20059
  let sessionToolCallCount = 0;
19887
20060
  let sessionSudoPassword = null;
19888
20061
  let sudoPromptPending = false;
19889
- const idlePrompt = `${c2.bold(c2.white("\u{1F896} "))}`;
20062
+ const idlePrompt = `${c2.bold(c2.white("\u276F "))}`;
19890
20063
  const activePrompt = `${c2.bold(c2.white("+ "))}`;
19891
20064
  const rl = readline2.createInterface({
19892
20065
  input: process.stdin,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-agents-ai",
3
- "version": "0.28.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",