@dreb/coding-agent 2.31.1 → 2.32.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.
@@ -7,7 +7,7 @@ import * as fs from "node:fs";
7
7
  import * as os from "node:os";
8
8
  import * as path from "node:path";
9
9
  import { supportsAdaptiveThinking } from "@dreb/ai";
10
- import { CombinedAutocompleteProvider, Container, fuzzyFilter, Loader, Markdown, matchesKey, ProcessTerminal, Spacer, setKeybindings, Text, TruncatedText, TUI, visibleWidth, } from "@dreb/tui";
10
+ import { CombinedAutocompleteProvider, Container, fuzzyFilter, Markdown, matchesKey, ProcessTerminal, Spacer, setKeybindings, Text, TruncatedText, TUI, visibleWidth, } from "@dreb/tui";
11
11
  import { spawn, spawnSync } from "child_process";
12
12
  import { APP_NAME, getAgentDir, getAuthPath, getDebugLogPath, getUpdateInstruction, VERSION } from "../../config.js";
13
13
  import { parseSkillBlock } from "../../core/agent-session.js";
@@ -87,9 +87,16 @@ export class InteractiveMode {
87
87
  version;
88
88
  isInitialized = false;
89
89
  onInputCallback;
90
- loadingAnimation = undefined;
91
90
  pendingWorkingMessage = undefined;
92
91
  defaultWorkingMessage = "Working...";
92
+ workingFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
93
+ workingFrame = 0;
94
+ workingInterval = undefined;
95
+ inlineStatusOwner = undefined;
96
+ inlineStatusSpinner = (spinner) => theme.fg("accent", spinner);
97
+ warnedMissingInlineStatus = false;
98
+ isAgentWorking = false;
99
+ currentWorkingMessage = this.defaultWorkingMessage;
93
100
  lastSigintTime = 0;
94
101
  lastEscapeTime = 0;
95
102
  changelogMarkdown = undefined;
@@ -1016,10 +1023,7 @@ export class InteractiveMode {
1016
1023
  commandContextActions: {
1017
1024
  waitForIdle: () => this.session.agent.waitForIdle(),
1018
1025
  newSession: async (options) => {
1019
- if (this.loadingAnimation) {
1020
- this.loadingAnimation.stop();
1021
- this.loadingAnimation = undefined;
1022
- }
1026
+ this.stopAllInlineStatus();
1023
1027
  this.statusContainer.clear();
1024
1028
  // Delegate to AgentSession (handles setup + agent state sync)
1025
1029
  const success = await this.session.newSession(options);
@@ -1230,8 +1234,8 @@ export class InteractiveMode {
1230
1234
  this.setCustomEditorComponent(undefined);
1231
1235
  this.defaultEditor.onExtensionShortcut = undefined;
1232
1236
  this.updateTerminalTitle();
1233
- if (this.loadingAnimation) {
1234
- this.loadingAnimation.setMessage(`${this.defaultWorkingMessage} (${keyText("app.interrupt")} to interrupt)`);
1237
+ if (this.isAgentWorking) {
1238
+ this.setWorkingMessage();
1235
1239
  }
1236
1240
  // Re-mount buddy so it survives reload
1237
1241
  if (buddyState) {
@@ -1343,6 +1347,100 @@ export class InteractiveMode {
1343
1347
  }
1344
1348
  this.extensionTerminalInputUnsubscribers.clear();
1345
1349
  }
1350
+ defaultInterruptWorkingMessage() {
1351
+ return `${this.defaultWorkingMessage} (${keyText("app.interrupt")} to interrupt)`;
1352
+ }
1353
+ setEditorInlineStatus(text) {
1354
+ this.defaultEditor.setInlineStatus(text);
1355
+ if (this.editor === this.defaultEditor)
1356
+ return;
1357
+ this.editor.setInlineStatus?.(text);
1358
+ if (!text || this.editor.setInlineStatus || this.warnedMissingInlineStatus)
1359
+ return;
1360
+ this.warnedMissingInlineStatus = true;
1361
+ this.showWarning("Custom editor component does not support inline working status; progress and interrupt hints may be hidden.");
1362
+ }
1363
+ renderWorkingIndicator() {
1364
+ if (!this.inlineStatusOwner)
1365
+ return;
1366
+ const frame = this.workingFrames[this.workingFrame] ?? this.workingFrames[0];
1367
+ this.setEditorInlineStatus(`${this.inlineStatusSpinner(frame)} ${theme.fg("muted", this.currentWorkingMessage)}`);
1368
+ }
1369
+ startInlineStatus(owner, message, spinner = (frame) => theme.fg("accent", frame)) {
1370
+ this.stopInlineStatus();
1371
+ this.inlineStatusOwner = owner;
1372
+ this.inlineStatusSpinner = spinner;
1373
+ this.currentWorkingMessage = message;
1374
+ this.workingFrame = 0;
1375
+ this.renderWorkingIndicator();
1376
+ this.workingInterval = setInterval(() => {
1377
+ this.workingFrame = (this.workingFrame + 1) % this.workingFrames.length;
1378
+ this.renderWorkingIndicator();
1379
+ }, 80);
1380
+ }
1381
+ stopInlineStatus(owner) {
1382
+ if (owner && this.inlineStatusOwner !== owner)
1383
+ return;
1384
+ if (this.workingInterval) {
1385
+ clearInterval(this.workingInterval);
1386
+ this.workingInterval = undefined;
1387
+ }
1388
+ if (this.inlineStatusOwner) {
1389
+ this.inlineStatusOwner = undefined;
1390
+ this.currentWorkingMessage = this.defaultWorkingMessage;
1391
+ this.inlineStatusSpinner = (spinner) => theme.fg("accent", spinner);
1392
+ this.setEditorInlineStatus(null);
1393
+ }
1394
+ }
1395
+ startInlineLoader(owner, message, spinner = (frame) => theme.fg("accent", frame)) {
1396
+ this.startInlineStatus(owner, message, spinner);
1397
+ return {
1398
+ stop: () => this.stopInlineStatus(owner),
1399
+ setText: (text) => {
1400
+ if (this.inlineStatusOwner !== owner)
1401
+ return;
1402
+ this.currentWorkingMessage = text;
1403
+ this.renderWorkingIndicator();
1404
+ },
1405
+ };
1406
+ }
1407
+ startAgentWorking(message) {
1408
+ this.stopAgentWorking();
1409
+ this.isAgentWorking = true;
1410
+ this.startInlineStatus("agent", message || this.defaultInterruptWorkingMessage());
1411
+ }
1412
+ stopAgentWorking() {
1413
+ if (this.isAgentWorking) {
1414
+ this.isAgentWorking = false;
1415
+ this.stopInlineStatus("agent");
1416
+ }
1417
+ }
1418
+ stopAllInlineStatus() {
1419
+ this.isAgentWorking = false;
1420
+ this.pendingWorkingMessage = undefined;
1421
+ this.stopInlineStatus();
1422
+ this.autoCompactionLoader = undefined;
1423
+ this.retryLoader = undefined;
1424
+ if (this.autoCompactionEscapeHandler) {
1425
+ this.defaultEditor.onEscape = this.autoCompactionEscapeHandler;
1426
+ this.autoCompactionEscapeHandler = undefined;
1427
+ }
1428
+ if (this.retryEscapeHandler) {
1429
+ this.defaultEditor.onEscape = this.retryEscapeHandler;
1430
+ this.retryEscapeHandler = undefined;
1431
+ }
1432
+ }
1433
+ setWorkingMessage(message) {
1434
+ const nextMessage = message || this.defaultInterruptWorkingMessage();
1435
+ if (this.isAgentWorking) {
1436
+ this.currentWorkingMessage = nextMessage;
1437
+ this.renderWorkingIndicator();
1438
+ }
1439
+ else {
1440
+ // Queue message for when the next agent_start arrives.
1441
+ this.pendingWorkingMessage = message;
1442
+ }
1443
+ }
1346
1444
  /**
1347
1445
  * Create the ExtensionUIContext for extensions.
1348
1446
  */
@@ -1354,20 +1452,7 @@ export class InteractiveMode {
1354
1452
  notify: (message, type) => this.showExtensionNotify(message, type),
1355
1453
  onTerminalInput: (handler) => this.addExtensionTerminalInputListener(handler),
1356
1454
  setStatus: (key, text) => this.setExtensionStatus(key, text),
1357
- setWorkingMessage: (message) => {
1358
- if (this.loadingAnimation) {
1359
- if (message) {
1360
- this.loadingAnimation.setMessage(message);
1361
- }
1362
- else {
1363
- this.loadingAnimation.setMessage(`${this.defaultWorkingMessage} (${keyText("app.interrupt")} to interrupt)`);
1364
- }
1365
- }
1366
- else {
1367
- // Queue message for when loadingAnimation is created (handles agent_start race)
1368
- this.pendingWorkingMessage = message;
1369
- }
1370
- },
1455
+ setWorkingMessage: (message) => this.setWorkingMessage(message),
1371
1456
  setWidget: (key, content, options) => this.setExtensionWidget(key, content, options),
1372
1457
  setFooter: (factory) => this.setExtensionFooter(factory),
1373
1458
  setHeader: (factory) => this.setExtensionHeader(factory),
@@ -1563,6 +1648,9 @@ export class InteractiveMode {
1563
1648
  this.defaultEditor.setText(currentText);
1564
1649
  this.editor = this.defaultEditor;
1565
1650
  }
1651
+ if (this.isAgentWorking) {
1652
+ this.renderWorkingIndicator();
1653
+ }
1566
1654
  this.editorContainer.addChild(this.editor);
1567
1655
  this.ui.setFocus(this.editor);
1568
1656
  this.ui.requestRender();
@@ -1676,7 +1764,7 @@ export class InteractiveMode {
1676
1764
  this.defaultEditor.onEscape = () => {
1677
1765
  // Always clear ghost text on Escape (CustomEditor intercepts before super.handleInput)
1678
1766
  this.editor.setGhostText?.(null);
1679
- if (this.loadingAnimation) {
1767
+ if (this.isAgentWorking) {
1680
1768
  this.cancelBackgroundAgents();
1681
1769
  this.restoreQueuedMessagesToEditor({ abort: true });
1682
1770
  }
@@ -1972,20 +2060,9 @@ export class InteractiveMode {
1972
2060
  this.retryLoader.stop();
1973
2061
  this.retryLoader = undefined;
1974
2062
  }
1975
- if (this.loadingAnimation) {
1976
- this.loadingAnimation.stop();
1977
- }
1978
2063
  this.statusContainer.clear();
1979
- this.loadingAnimation = new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), this.defaultWorkingMessage);
1980
- this.statusContainer.addChild(this.loadingAnimation);
1981
- // Apply any pending working message queued before loader existed
1982
- if (this.pendingWorkingMessage !== undefined) {
1983
- if (this.pendingWorkingMessage) {
1984
- this.loadingAnimation.setMessage(this.pendingWorkingMessage);
1985
- }
1986
- this.pendingWorkingMessage = undefined;
1987
- }
1988
- this.ui.requestRender();
2064
+ this.startAgentWorking(this.pendingWorkingMessage);
2065
+ this.pendingWorkingMessage = undefined;
1989
2066
  break;
1990
2067
  case "message_start":
1991
2068
  if (event.message.role === "custom") {
@@ -2128,10 +2205,7 @@ export class InteractiveMode {
2128
2205
  this.retryLoader.stop();
2129
2206
  this.retryLoader = undefined;
2130
2207
  }
2131
- if (this.loadingAnimation) {
2132
- this.loadingAnimation.stop();
2133
- this.loadingAnimation = undefined;
2134
- }
2208
+ this.stopAgentWorking();
2135
2209
  this.statusContainer.clear();
2136
2210
  if (this.streamingComponent) {
2137
2211
  this.chatContainer.removeChild(this.streamingComponent);
@@ -2155,12 +2229,10 @@ export class InteractiveMode {
2155
2229
  this.defaultEditor.onEscape = () => {
2156
2230
  this.session.abortCompaction();
2157
2231
  };
2158
- // Show compacting indicator with reason
2232
+ // Show compacting indicator inside the editor line so start/end is constant-height.
2159
2233
  this.statusContainer.clear();
2160
2234
  const reasonText = event.reason === "overflow" ? "Context overflow detected, " : "";
2161
- this.autoCompactionLoader = new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), `${reasonText}Auto-compacting... (${keyText("app.interrupt")} to cancel)`);
2162
- this.statusContainer.addChild(this.autoCompactionLoader);
2163
- this.ui.requestRender();
2235
+ this.autoCompactionLoader = this.startInlineLoader("autoCompaction", `${reasonText}Auto-compacting... (${keyText("app.interrupt")} to cancel)`);
2164
2236
  break;
2165
2237
  }
2166
2238
  case "auto_compaction_end": {
@@ -2208,12 +2280,10 @@ export class InteractiveMode {
2208
2280
  this.defaultEditor.onEscape = () => {
2209
2281
  this.session.abortRetry();
2210
2282
  };
2211
- // Show retry indicator
2283
+ // Show retry indicator inside the editor line so start/end is constant-height.
2212
2284
  this.statusContainer.clear();
2213
2285
  const delaySeconds = Math.round(event.delayMs / 1000);
2214
- this.retryLoader = new Loader(this.ui, (spinner) => theme.fg("warning", spinner), (text) => theme.fg("muted", text), `Retrying (${event.attempt}/${event.maxAttempts}) in ${delaySeconds}s... (${keyText("app.interrupt")} to cancel)`);
2215
- this.statusContainer.addChild(this.retryLoader);
2216
- this.ui.requestRender();
2286
+ this.retryLoader = this.startInlineLoader("autoRetry", `Retrying (${event.attempt}/${event.maxAttempts}) in ${delaySeconds}s... (${keyText("app.interrupt")} to cancel)`, (spinner) => theme.fg("warning", spinner));
2217
2287
  break;
2218
2288
  }
2219
2289
  case "auto_retry_end": {
@@ -2247,8 +2317,8 @@ export class InteractiveMode {
2247
2317
  this.chatContainer.removeChild(component);
2248
2318
  }
2249
2319
  this.pendingTools.clear();
2250
- // Warn in the chat scrollback — keep the working spinner running so ESC
2251
- // aborts via the normal loadingAnimation path (same AbortController).
2320
+ // Warn in the chat scrollback — keep the inline working indicator active so ESC
2321
+ // aborts via the normal agent-working path (same AbortController).
2252
2322
  this.showWarning(`Stream dropped, retrying (${event.attempt}/${event.maxAttempts})…`);
2253
2323
  break;
2254
2324
  }
@@ -2264,8 +2334,8 @@ export class InteractiveMode {
2264
2334
  this.chatContainer.removeChild(component);
2265
2335
  }
2266
2336
  this.pendingTools.clear();
2267
- // Warn in the chat scrollback — keep the working spinner running so ESC
2268
- // aborts via the normal loadingAnimation path (same AbortController).
2337
+ // Warn in the chat scrollback — keep the inline working indicator active so ESC
2338
+ // aborts via the normal agent-working path (same AbortController).
2269
2339
  this.showWarning(`Response truncated, retrying with larger token budget (${event.attempt}/${event.maxAttempts})…`);
2270
2340
  break;
2271
2341
  }
@@ -3492,7 +3562,7 @@ export class InteractiveMode {
3492
3562
  break;
3493
3563
  }
3494
3564
  }
3495
- // Set up escape handler and loader if summarizing
3565
+ // Set up escape handler and inline loader if summarizing
3496
3566
  let summaryLoader;
3497
3567
  const originalOnEscape = this.defaultEditor.onEscape;
3498
3568
  if (wantsSummary) {
@@ -3500,9 +3570,7 @@ export class InteractiveMode {
3500
3570
  this.session.abortBranchSummary();
3501
3571
  };
3502
3572
  this.chatContainer.addChild(new Spacer(1));
3503
- summaryLoader = new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), `Summarizing branch... (${keyText("app.interrupt")} to cancel)`);
3504
- this.statusContainer.addChild(summaryLoader);
3505
- this.ui.requestRender();
3573
+ summaryLoader = this.startInlineLoader("branchSummary", `Summarizing branch... (${keyText("app.interrupt")} to cancel)`);
3506
3574
  }
3507
3575
  try {
3508
3576
  const result = await this.session.navigateTree(entryId, {
@@ -3571,11 +3639,8 @@ export class InteractiveMode {
3571
3639
  });
3572
3640
  }
3573
3641
  async handleResumeSession(sessionPath) {
3574
- // Stop loading animation
3575
- if (this.loadingAnimation) {
3576
- this.loadingAnimation.stop();
3577
- this.loadingAnimation = undefined;
3578
- }
3642
+ // Stop inline working indicator/status loader
3643
+ this.stopAllInlineStatus();
3579
3644
  this.statusContainer.clear();
3580
3645
  // Clear UI state
3581
3646
  this.editor.setGhostText?.(null);
@@ -3803,11 +3868,8 @@ export class InteractiveMode {
3803
3868
  return;
3804
3869
  }
3805
3870
  try {
3806
- // Stop loading animation
3807
- if (this.loadingAnimation) {
3808
- this.loadingAnimation.stop();
3809
- this.loadingAnimation = undefined;
3810
- }
3871
+ // Stop inline working indicator/status loader
3872
+ this.stopAllInlineStatus();
3811
3873
  this.statusContainer.clear();
3812
3874
  // Clear UI state
3813
3875
  this.pendingMessagesContainer.clear();
@@ -4108,11 +4170,8 @@ ${cycleModelForward || cycleModelBackward ? `| \`${cycleModelForward}\` / \`${cy
4108
4170
  this.ui.requestRender();
4109
4171
  }
4110
4172
  async handleClearCommand() {
4111
- // Stop loading animation
4112
- if (this.loadingAnimation) {
4113
- this.loadingAnimation.stop();
4114
- this.loadingAnimation = undefined;
4115
- }
4173
+ // Stop inline working indicator/status loader
4174
+ this.stopAllInlineStatus();
4116
4175
  this.statusContainer.clear();
4117
4176
  // New session via session (emits extension session events)
4118
4177
  await this.session.newSession();
@@ -4275,11 +4334,8 @@ ${cycleModelForward || cycleModelBackward ? `| \`${cycleModelForward}\` / \`${cy
4275
4334
  }
4276
4335
  }
4277
4336
  async executeDream() {
4278
- // Stop any existing loading animation
4279
- if (this.loadingAnimation) {
4280
- this.loadingAnimation.stop();
4281
- this.loadingAnimation = undefined;
4282
- }
4337
+ // Stop any existing inline working indicator/status loader
4338
+ this.stopAllInlineStatus();
4283
4339
  this.statusContainer.clear();
4284
4340
  let releaseLock;
4285
4341
  let dreamContext;
@@ -4289,12 +4345,10 @@ ${cycleModelForward || cycleModelBackward ? `| \`${cycleModelForward}\` / \`${cy
4289
4345
  // User pressed escape — abort the dream
4290
4346
  this.session.abort();
4291
4347
  };
4292
- // Show loading spinner
4348
+ // Show loading spinner inside the editor line so start/end is constant-height.
4293
4349
  this.chatContainer.addChild(new Spacer(1));
4294
4350
  const cancelHint = `(${keyText("app.interrupt")} to cancel)`;
4295
- const dreamLoader = new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), `Dreaming... ${cancelHint}`);
4296
- this.statusContainer.addChild(dreamLoader);
4297
- this.ui.requestRender();
4351
+ const dreamLoader = this.startInlineLoader("dream", `Dreaming... ${cancelHint}`);
4298
4352
  try {
4299
4353
  // Acquire lock
4300
4354
  try {
@@ -4334,7 +4388,6 @@ ${cycleModelForward || cycleModelBackward ? `| \`${cycleModelForward}\` / \`${cy
4334
4388
  }
4335
4389
  // Update loader text
4336
4390
  dreamLoader.setText(`Consolidating memories... ${cancelHint}`);
4337
- this.ui.requestRender();
4338
4391
  // Build prompt and inject into session
4339
4392
  const prompt = buildDreamPrompt(dreamContext, backupResult);
4340
4393
  await this.session.prompt(prompt);
@@ -4381,24 +4434,19 @@ ${cycleModelForward || cycleModelBackward ? `| \`${cycleModelForward}\` / \`${cy
4381
4434
  await this.executeCompaction(customInstructions, false);
4382
4435
  }
4383
4436
  async executeCompaction(customInstructions, isAuto = false) {
4384
- // Stop loading animation
4385
- if (this.loadingAnimation) {
4386
- this.loadingAnimation.stop();
4387
- this.loadingAnimation = undefined;
4388
- }
4437
+ // Stop inline working indicator/status loader
4438
+ this.stopAllInlineStatus();
4389
4439
  this.statusContainer.clear();
4390
4440
  // Set up escape handler during compaction
4391
4441
  const originalOnEscape = this.defaultEditor.onEscape;
4392
4442
  this.defaultEditor.onEscape = () => {
4393
4443
  this.session.abortCompaction();
4394
4444
  };
4395
- // Show compacting status
4445
+ // Show compacting status inside the editor line so start/end is constant-height.
4396
4446
  this.chatContainer.addChild(new Spacer(1));
4397
4447
  const cancelHint = `(${keyText("app.interrupt")} to cancel)`;
4398
4448
  const label = isAuto ? `Auto-compacting context... ${cancelHint}` : `Compacting context... ${cancelHint}`;
4399
- const compactingLoader = new Loader(this.ui, (spinner) => theme.fg("accent", spinner), (text) => theme.fg("muted", text), label);
4400
- this.statusContainer.addChild(compactingLoader);
4401
- this.ui.requestRender();
4449
+ const compactingLoader = this.startInlineLoader("compaction", label);
4402
4450
  let result;
4403
4451
  try {
4404
4452
  result = await this.session.compact(customInstructions);
@@ -4632,10 +4680,7 @@ ${cycleModelForward || cycleModelBackward ? `| \`${cycleModelForward}\` / \`${cy
4632
4680
  }
4633
4681
  stop() {
4634
4682
  this.buddyController.stop();
4635
- if (this.loadingAnimation) {
4636
- this.loadingAnimation.stop();
4637
- this.loadingAnimation = undefined;
4638
- }
4683
+ this.stopAllInlineStatus();
4639
4684
  this.removeBuddy();
4640
4685
  this.clearExtensionTerminalInputListeners();
4641
4686
  this.footer.dispose();