@kolisachint/hoocode-agent 0.4.135 → 0.4.136

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.
@@ -25,6 +25,7 @@ import { ensureTool } from "../../utils/tools-manager.js";
25
25
  import { checkForNewHooCodeVersion } from "../../utils/version-check.js";
26
26
  import { BashExecutionController } from "./bash-execution-controller.js";
27
27
  import { CommandExecutor } from "./command-executor.js";
28
+ import { BELL, CompletionChime } from "./completion-chime.js";
28
29
  import { AssistantMessageComponent } from "./components/assistant-message.js";
29
30
  import { BashExecutionComponent } from "./components/bash-execution.js";
30
31
  import { BranchSummaryMessageComponent } from "./components/branch-summary-message.js";
@@ -172,6 +173,14 @@ export class InteractiveMode {
172
173
  retryLoader = undefined;
173
174
  retryCountdown = undefined;
174
175
  retryEscapeHandler;
176
+ // Completion chime state
177
+ chime;
178
+ // True between auto_retry_start and the next agent_start: the turn is not done,
179
+ // it is about to re-run, so the deferred completion check must not fire the chime.
180
+ chimePendingRetry = false;
181
+ // Whether the current turn's final assistant message was user-aborted (Ctrl+C /
182
+ // Esc). An aborted turn means the user is present, so no chime.
183
+ chimeTurnAborted = false;
175
184
  // Shutdown state
176
185
  shutdownRequested = false;
177
186
  // Extension UI state
@@ -394,6 +403,12 @@ export class InteractiveMode {
394
403
  this.taskPanel.onExitFocus = () => this.teamFocus.exitFocus();
395
404
  // Load hide thinking block setting
396
405
  this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();
406
+ // Completion chime: rings the terminal bell when a long turn finishes or the
407
+ // agent blocks awaiting input. Enable is read fresh so a live toggle applies.
408
+ this.chime = new CompletionChime({
409
+ isEnabled: () => this.settingsManager.getChimeOnTurnComplete(),
410
+ ring: () => this.ui.terminal.write(BELL),
411
+ });
397
412
  // Register themes from resource loader and initialize
398
413
  setRegisteredThemes(this.session.resourceLoader.getThemes().themes);
399
414
  initTheme(this.settingsManager.getTheme(), true);
@@ -1049,7 +1064,12 @@ export class InteractiveMode {
1049
1064
  select: (title, options, opts) => this.dialogs.showSelector(title, options, opts),
1050
1065
  confirm: (title, message, opts) => this.dialogs.confirm(title, message, opts),
1051
1066
  input: (title, placeholder, opts) => this.dialogs.showInput(title, placeholder, opts),
1052
- askOptions: (questions, opts) => this.dialogs.showAskOptions(questions, opts),
1067
+ askOptions: (questions, opts) => {
1068
+ // The agent is blocking on the user (the ask_options pane): ring now,
1069
+ // bypassing the duration threshold — this is the "it needs you" cue.
1070
+ this.chime?.onBlockedForInput();
1071
+ return this.dialogs.showAskOptions(questions, opts);
1072
+ },
1053
1073
  notify: (message, type) => this.showExtensionNotify(message, type),
1054
1074
  onTerminalInput: (handler) => this.addExtensionTerminalInputListener(handler),
1055
1075
  setStatus: (key, text) => this.setExtensionStatus(key, text),
@@ -1536,6 +1556,21 @@ export class InteractiveMode {
1536
1556
  await this.handleEvent(event);
1537
1557
  });
1538
1558
  }
1559
+ /**
1560
+ * Deferred completion chime. Called from the agent_end handler, it waits one
1561
+ * tick so the streaming flag settles and any retry has had a chance to arm, then
1562
+ * rings only when the session is genuinely idle (not streaming, not compacting)
1563
+ * and no retry is pending. The chime itself enforces the enable/threshold/debounce
1564
+ * rules; here we only decide whether the turn has truly ended.
1565
+ */
1566
+ maybeChimeOnIdle() {
1567
+ setTimeout(() => {
1568
+ if (this.chimePendingRetry || this.session.isStreaming || this.session.isCompacting) {
1569
+ return;
1570
+ }
1571
+ this.chime?.onTurnComplete({ aborted: this.chimeTurnAborted });
1572
+ }, 0);
1573
+ }
1539
1574
  async handleEvent(event) {
1540
1575
  if (!this.isInitialized) {
1541
1576
  await this.init();
@@ -1544,6 +1579,10 @@ export class InteractiveMode {
1544
1579
  switch (event.type) {
1545
1580
  case "agent_start":
1546
1581
  this.pendingTools.clear();
1582
+ // A (possibly retried) turn is (re)starting: anchor its duration on the
1583
+ // first start and clear the pending-retry gate now that it has resumed.
1584
+ this.chime?.onTurnStart();
1585
+ this.chimePendingRetry = false;
1547
1586
  if (this.settingsManager.getShowTerminalProgress()) {
1548
1587
  this.ui.terminal.setProgress(true);
1549
1588
  }
@@ -1594,6 +1633,7 @@ export class InteractiveMode {
1594
1633
  // glanceable for the whole turn. Active tasks are kept: a follow-up/steer
1595
1634
  // message can arrive while a subagent is still running.
1596
1635
  taskStore.reset();
1636
+ this.chimeTurnAborted = false;
1597
1637
  this.addMessageToChat(event.message);
1598
1638
  this.messageQueue.updatePendingMessagesDisplay();
1599
1639
  this.ui.requestRender();
@@ -1635,6 +1675,11 @@ export class InteractiveMode {
1635
1675
  case "message_end":
1636
1676
  if (event.message.role === "user")
1637
1677
  break;
1678
+ if (event.message.role === "assistant") {
1679
+ // Remember whether the turn's latest assistant message was user-aborted,
1680
+ // so the completion chime stays silent when the user is clearly present.
1681
+ this.chimeTurnAborted = event.message.stopReason === "aborted";
1682
+ }
1638
1683
  if (this.streamingComponent && event.message.role === "assistant") {
1639
1684
  this.streamingMessage = event.message;
1640
1685
  let errorMessage;
@@ -1720,6 +1765,12 @@ export class InteractiveMode {
1720
1765
  }
1721
1766
  this.pendingTools.clear();
1722
1767
  await this.checkShutdownRequested();
1768
+ // agent_end fires before the streaming flag clears and before the retry
1769
+ // decision is made (both happen after this listener returns), so defer a
1770
+ // tick and re-check: only chime once the session is genuinely idle and no
1771
+ // retry is pending — otherwise a retried or auto-continued turn would ring
1772
+ // a premature "it's done".
1773
+ this.maybeChimeOnIdle();
1723
1774
  this.ui.requestRender();
1724
1775
  break;
1725
1776
  case "compaction_start": {
@@ -1782,6 +1833,9 @@ export class InteractiveMode {
1782
1833
  break;
1783
1834
  }
1784
1835
  case "auto_retry_start": {
1836
+ // The turn is not done — it will re-run after a backoff (during which the
1837
+ // session reads as idle). Gate the deferred completion chime until then.
1838
+ this.chimePendingRetry = true;
1785
1839
  // Set up escape to abort retry
1786
1840
  this.retryEscapeHandler = this.defaultEditor.onEscape;
1787
1841
  this.defaultEditor.onEscape = () => {