@kolisachint/hoocode-agent 0.4.157 → 0.4.159

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 (47) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/dist/core/agent-session-stats.d.ts +17 -1
  3. package/dist/core/agent-session-stats.d.ts.map +1 -1
  4. package/dist/core/agent-session-stats.js +20 -0
  5. package/dist/core/agent-session-stats.js.map +1 -1
  6. package/dist/core/settings-defaults.d.ts +1 -0
  7. package/dist/core/settings-defaults.d.ts.map +1 -1
  8. package/dist/core/settings-defaults.js +2 -1
  9. package/dist/core/settings-defaults.js.map +1 -1
  10. package/dist/core/settings-manager.d.ts +2 -0
  11. package/dist/core/settings-manager.d.ts.map +1 -1
  12. package/dist/core/settings-manager.js +8 -0
  13. package/dist/core/settings-manager.js.map +1 -1
  14. package/dist/core/settings-types.d.ts +1 -0
  15. package/dist/core/settings-types.d.ts.map +1 -1
  16. package/dist/core/settings-types.js.map +1 -1
  17. package/dist/core/subagent-pool.d.ts.map +1 -1
  18. package/dist/core/subagent-pool.js +5 -1
  19. package/dist/core/subagent-pool.js.map +1 -1
  20. package/dist/core/tools/todo.d.ts +24 -0
  21. package/dist/core/tools/todo.d.ts.map +1 -1
  22. package/dist/core/tools/todo.js +50 -9
  23. package/dist/core/tools/todo.js.map +1 -1
  24. package/dist/core/warm-subagent-pool.d.ts.map +1 -1
  25. package/dist/core/warm-subagent-pool.js +5 -1
  26. package/dist/core/warm-subagent-pool.js.map +1 -1
  27. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  28. package/dist/modes/interactive/components/footer.js +6 -15
  29. package/dist/modes/interactive/components/footer.js.map +1 -1
  30. package/dist/modes/interactive/components/settings-selector.d.ts +2 -0
  31. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  32. package/dist/modes/interactive/components/settings-selector.js +15 -2
  33. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  34. package/dist/modes/interactive/components/task-panel.d.ts +20 -12
  35. package/dist/modes/interactive/components/task-panel.d.ts.map +1 -1
  36. package/dist/modes/interactive/components/task-panel.js +170 -224
  37. package/dist/modes/interactive/components/task-panel.js.map +1 -1
  38. package/dist/modes/interactive/interactive-mode.d.ts +42 -7
  39. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  40. package/dist/modes/interactive/interactive-mode.js +124 -19
  41. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  42. package/docs/settings.md +2 -1
  43. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  44. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  45. package/examples/extensions/sandbox/package.json +1 -1
  46. package/examples/extensions/with-deps/package.json +1 -1
  47. package/package.json +4 -4
@@ -12,13 +12,17 @@ import { spawnSync } from "child_process";
12
12
  import { APP_NAME, APP_TITLE, VERSION } from "../../config.js";
13
13
  import { loadAgentRegistry } from "../../core/agent-registry.js";
14
14
  import { parseSkillBlock } from "../../core/agent-session.js";
15
+ import { sumAssistantUsage } from "../../core/agent-session-stats.js";
15
16
  import { FooterDataProvider } from "../../core/footer-data-provider.js";
17
+ import { formatDurationSecs } from "../../core/format-duration.js";
18
+ import { formatTokens } from "../../core/format-tokens.js";
16
19
  import { KeybindingsManager } from "../../core/keybindings.js";
17
20
  import { formatMissingSessionCwdPrompt, MissingSessionCwdError } from "../../core/session-cwd.js";
18
21
  import { SessionManager } from "../../core/session-manager.js";
19
22
  import { BUILTIN_SLASH_COMMANDS } from "../../core/slash-commands.js";
20
23
  import { startupProgress } from "../../core/startup-progress.js";
21
24
  import { taskStore } from "../../core/task-store.js";
25
+ import { settleDanglingMainTasks } from "../../core/tools/todo.js";
22
26
  import { buildCompactWordmark } from "../../core/wordmark.js";
23
27
  import { extensionForImageMimeType, readClipboardImage } from "../../utils/clipboard-image.js";
24
28
  import { parseGitUrl } from "../../utils/git.js";
@@ -203,9 +207,17 @@ export class InteractiveMode {
203
207
  // True between auto_retry_start and the next agent_start: the turn is not done,
204
208
  // it is about to re-run, so the deferred completion check must not fire the chime.
205
209
  chimePendingRetry = false;
206
- // Whether the current turn's final assistant message was user-aborted (Ctrl+C /
207
- // Esc). An aborted turn means the user is present, so no chime.
208
- chimeTurnAborted = false;
210
+ // Stop reason of the request's latest assistant message, reset when the user
211
+ // starts a new one. Read at settle time by two consumers that ask the same
212
+ // question — did this request end cleanly? An "aborted" turn means the user is
213
+ // present, so no chime; anything other than a clean "stop" means dangling plan
214
+ // items must not be claimed as completed.
215
+ turnStopReason = undefined;
216
+ // Cumulative usage + wall clock captured at the first agent_start of a request,
217
+ // diffed at agent_end to print that request's own cost into the transcript.
218
+ // Anchored on the FIRST start so a retried request reports one span, not one
219
+ // per attempt (same reason the chime anchors its duration there).
220
+ turnCostAnchor;
209
221
  // Shutdown state
210
222
  shutdownRequested = false;
211
223
  // Extension UI state
@@ -332,11 +344,12 @@ export class InteractiveMode {
332
344
  const editorPaddingX = this.settingsManager.getEditorPaddingX();
333
345
  const autocompleteMaxVisible = this.settingsManager.getAutocompleteMaxVisible();
334
346
  this.defaultEditor = new CustomEditor(this.ui, getEditorTheme(), this.keybindings, {
347
+ border: this.settingsManager.getEditorBorder(),
335
348
  paddingX: editorPaddingX,
336
349
  autocompleteMaxVisible,
337
350
  });
338
351
  this.editor = this.defaultEditor;
339
- this.editor.promptPrefix = ">";
352
+ this.editor.promptPrefix = "";
340
353
  this.editorContainer = new Container();
341
354
  this.editorContainer.addChild(this.editor);
342
355
  this.footerDataProvider = new FooterDataProvider(this.sessionManager.getCwd());
@@ -965,11 +978,14 @@ export class InteractiveMode {
965
978
  this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();
966
979
  this.ui.setShowHardwareCursor(this.settingsManager.getShowHardwareCursor());
967
980
  this.ui.setClearOnShrink(this.settingsManager.getClearOnShrink());
981
+ const editorBorder = this.settingsManager.getEditorBorder();
968
982
  const editorPaddingX = this.settingsManager.getEditorPaddingX();
969
983
  const autocompleteMaxVisible = this.settingsManager.getAutocompleteMaxVisible();
984
+ this.defaultEditor.setBorder(editorBorder);
970
985
  this.defaultEditor.setPaddingX(editorPaddingX);
971
986
  this.defaultEditor.setAutocompleteMaxVisible(autocompleteMaxVisible);
972
987
  if (this.editor !== this.defaultEditor) {
988
+ this.editor.setBorder?.(editorBorder);
973
989
  this.editor.setPaddingX?.(editorPaddingX);
974
990
  this.editor.setAutocompleteMaxVisible?.(autocompleteMaxVisible);
975
991
  }
@@ -1665,20 +1681,94 @@ export class InteractiveMode {
1665
1681
  });
1666
1682
  }
1667
1683
  /**
1668
- * Deferred completion chime. Called from the agent_end handler, it waits one
1669
- * tick so the streaming flag settles and any retry has had a chance to arm, then
1670
- * rings only when the session is genuinely idle (not streaming, not compacting)
1671
- * and no retry is pending. The chime itself enforces the enable/threshold/debounce
1672
- * rules; here we only decide whether the turn has truly ended.
1684
+ * Deferred end-of-request settle. Called from the agent_end handler, it waits
1685
+ * one tick so the streaming flag settles and any retry has had a chance to arm,
1686
+ * then acts only when the session is genuinely idle (not streaming, not
1687
+ * compacting) and no retry is pending so a retried or auto-continued request
1688
+ * neither rings a premature "it's done" nor prints a partial cost line.
1689
+ *
1690
+ * The completion chime, the transcript cost line, and settling dangling plan
1691
+ * items all hang off this single check because they answer the same question:
1692
+ * has the request actually ended?
1673
1693
  */
1674
- maybeChimeOnIdle() {
1694
+ settleRequestOnIdle() {
1675
1695
  setTimeout(() => {
1676
1696
  if (this.chimePendingRetry || this.session.isStreaming || this.session.isCompacting) {
1677
1697
  return;
1678
1698
  }
1679
- this.chime?.onTurnComplete({ aborted: this.chimeTurnAborted });
1699
+ this.showTurnCost();
1700
+ this.settleDanglingPlanItems();
1701
+ this.chime?.onTurnComplete({ aborted: this.turnStopReason === "aborted" });
1680
1702
  }, 0);
1681
1703
  }
1704
+ /**
1705
+ * Flip main-plan rows the model left at in_progress to a settled status now the
1706
+ * request is over, instead of letting them claim live work until the next user
1707
+ * message wipes the pane.
1708
+ *
1709
+ * The model marks its own TodoWrite items and routinely drops the final call
1710
+ * that completes the last one, so the row outlives the work. A clean "stop" is
1711
+ * taken as the model believing it was finished — it chose to stop talking, and
1712
+ * its closing message is the report — so those items settle to done. An abort,
1713
+ * error, or length cutoff says the opposite, so they settle to cancelled: an
1714
+ * honest "never finished" rather than a fabricated completion.
1715
+ *
1716
+ * Skipped while messages are queued: a follow-up/steer arriving during the run
1717
+ * means the request continues, and the plan with it.
1718
+ */
1719
+ settleDanglingPlanItems() {
1720
+ if (this.session.pendingMessageCount > 0)
1721
+ return;
1722
+ const settled = settleDanglingMainTasks(this.turnStopReason === "stop" ? "done" : "cancelled");
1723
+ if (settled > 0)
1724
+ this.ui.requestRender();
1725
+ }
1726
+ /**
1727
+ * Append this request's own token/time/cost to the transcript.
1728
+ *
1729
+ * This is the honest home for the number. The task panel is a live instrument —
1730
+ * present tense, wiped on the next user message — and the footer carries
1731
+ * cumulative session vitals, so after the fact neither can answer "what did that
1732
+ * request cost". Fired at agent_end rather than turn_end because one request is
1733
+ * commonly tens of turns; per-turn would wedge a number between every tool block.
1734
+ *
1735
+ * Delegated runs are reported separately rather than folded into the totals:
1736
+ * subagents bill against their own sessions, so their tokens never appear in this
1737
+ * session's entries and adding them to ↑/↓ would misreport what the parent spent.
1738
+ */
1739
+ showTurnCost() {
1740
+ const anchor = this.turnCostAnchor;
1741
+ this.turnCostAnchor = undefined;
1742
+ if (!anchor)
1743
+ return;
1744
+ const now = sumAssistantUsage(this.session.sessionManager.getEntries());
1745
+ const input = now.input - anchor.totals.input;
1746
+ const output = now.output - anchor.totals.output;
1747
+ const cost = now.cost - anchor.totals.cost;
1748
+ // Nothing was accounted (e.g. aborted before the first response landed): stay
1749
+ // silent rather than print a row of zeroes.
1750
+ if (input <= 0 && output <= 0)
1751
+ return;
1752
+ const segs = [
1753
+ theme.fg("dim", "↑") +
1754
+ theme.fg("muted", formatTokens(input)) +
1755
+ theme.fg("dim", " ↓") +
1756
+ theme.fg("muted", formatTokens(output)),
1757
+ theme.fg("muted", formatDurationSecs((Date.now() - anchor.at) / 1000)),
1758
+ ];
1759
+ if (cost > 0)
1760
+ segs.push(theme.fg("muted", `$${cost.toFixed(3)}`));
1761
+ const runs = taskStore.list().filter((task) => task.source === "subagent");
1762
+ if (runs.length > 0) {
1763
+ const delegatedTokens = runs.reduce((sum, r) => sum + (r.usage ? r.usage.input + r.usage.output : 0), 0);
1764
+ const label = runs.length === 1 ? (runs[0]?.subagentMode ?? "subagent") : "subagents";
1765
+ const text = `◇${runs.length} ${label}${delegatedTokens > 0 ? ` ${formatTokens(delegatedTokens)}` : ""}`;
1766
+ segs.push(theme.fg("dim", text));
1767
+ }
1768
+ this.chatContainer.addChild(new Spacer(1));
1769
+ this.chatContainer.addChild(new Text(segs.join(theme.fg("dim", " · ")), 1, 0));
1770
+ this.ui.requestRender();
1771
+ }
1682
1772
  async handleEvent(event) {
1683
1773
  if (!this.isInitialized) {
1684
1774
  await this.init();
@@ -1691,6 +1781,10 @@ export class InteractiveMode {
1691
1781
  // first start and clear the pending-retry gate now that it has resumed.
1692
1782
  this.chime?.onTurnStart();
1693
1783
  this.chimePendingRetry = false;
1784
+ this.turnCostAnchor ??= {
1785
+ totals: sumAssistantUsage(this.session.sessionManager.getEntries()),
1786
+ at: Date.now(),
1787
+ };
1694
1788
  if (this.settingsManager.getShowTerminalProgress()) {
1695
1789
  this.ui.terminal.setProgress(true);
1696
1790
  }
@@ -1745,7 +1839,7 @@ export class InteractiveMode {
1745
1839
  // turn, drop any lingering bars/notices (e.g. an unavailable-index line)
1746
1840
  // so they don't sit in the footer for the rest of the session.
1747
1841
  startupProgress.clear();
1748
- this.chimeTurnAborted = false;
1842
+ this.turnStopReason = undefined;
1749
1843
  this.addMessageToChat(event.message);
1750
1844
  this.messageQueue.updatePendingMessagesDisplay();
1751
1845
  this.ui.requestRender();
@@ -1789,9 +1883,9 @@ export class InteractiveMode {
1789
1883
  if (event.message.role === "user")
1790
1884
  break;
1791
1885
  if (event.message.role === "assistant") {
1792
- // Remember whether the turn's latest assistant message was user-aborted,
1793
- // so the completion chime stays silent when the user is clearly present.
1794
- this.chimeTurnAborted = event.message.stopReason === "aborted";
1886
+ // Remember how the turn's latest assistant message ended, so the deferred
1887
+ // settle can tell a clean finish from an abort/error.
1888
+ this.turnStopReason = event.message.stopReason;
1795
1889
  }
1796
1890
  if (this.streamingComponent && event.message.role === "assistant") {
1797
1891
  this.streamingMessage = event.message;
@@ -1881,10 +1975,10 @@ export class InteractiveMode {
1881
1975
  await this.checkShutdownRequested();
1882
1976
  // agent_end fires before the streaming flag clears and before the retry
1883
1977
  // decision is made (both happen after this listener returns), so defer a
1884
- // tick and re-check: only chime once the session is genuinely idle and no
1978
+ // tick and re-check: only settle once the session is genuinely idle and no
1885
1979
  // retry is pending — otherwise a retried or auto-continued turn would ring
1886
- // a premature "it's done".
1887
- this.maybeChimeOnIdle();
1980
+ // a premature "it's done" and print a partial cost line.
1981
+ this.settleRequestOnIdle();
1888
1982
  this.ui.requestRender();
1889
1983
  break;
1890
1984
  case "compaction_start": {
@@ -2372,7 +2466,7 @@ export class InteractiveMode {
2372
2466
  this.editor.promptColor = theme.getBashModeBorderColor();
2373
2467
  }
2374
2468
  else {
2375
- this.editor.promptPrefix = ">";
2469
+ this.editor.promptPrefix = "";
2376
2470
  this.editor.promptColor = (s) => s;
2377
2471
  }
2378
2472
  this.ui.requestRender();
@@ -2591,6 +2685,7 @@ export class InteractiveMode {
2591
2685
  doubleEscapeAction: this.settingsManager.getDoubleEscapeAction(),
2592
2686
  treeFilterMode: this.settingsManager.getTreeFilterMode(),
2593
2687
  showHardwareCursor: this.settingsManager.getShowHardwareCursor(),
2688
+ editorBorder: this.settingsManager.getEditorBorder(),
2594
2689
  editorPaddingX: this.settingsManager.getEditorPaddingX(),
2595
2690
  autocompleteMaxVisible: this.settingsManager.getAutocompleteMaxVisible(),
2596
2691
  quietStartup: this.settingsManager.getQuietStartup(),
@@ -2760,6 +2855,13 @@ export class InteractiveMode {
2760
2855
  this.settingsManager.setShowHardwareCursor(enabled);
2761
2856
  this.ui.setShowHardwareCursor(enabled);
2762
2857
  },
2858
+ onEditorBorderChange: (border) => {
2859
+ this.settingsManager.setEditorBorder(border);
2860
+ this.defaultEditor.setBorder(border);
2861
+ if (this.editor !== this.defaultEditor && this.editor.setBorder !== undefined) {
2862
+ this.editor.setBorder(border);
2863
+ }
2864
+ },
2763
2865
  onEditorPaddingXChange: (padding) => {
2764
2866
  this.settingsManager.setEditorPaddingX(padding);
2765
2867
  this.defaultEditor.setPaddingX(padding);
@@ -3043,11 +3145,14 @@ export class InteractiveMode {
3043
3145
  if (!themeResult.success) {
3044
3146
  this.showError(`Failed to load theme "${themeName}": ${themeResult.error}\nFell back to dark theme.`);
3045
3147
  }
3148
+ const editorBorder = this.settingsManager.getEditorBorder();
3046
3149
  const editorPaddingX = this.settingsManager.getEditorPaddingX();
3047
3150
  const autocompleteMaxVisible = this.settingsManager.getAutocompleteMaxVisible();
3151
+ this.defaultEditor.setBorder(editorBorder);
3048
3152
  this.defaultEditor.setPaddingX(editorPaddingX);
3049
3153
  this.defaultEditor.setAutocompleteMaxVisible(autocompleteMaxVisible);
3050
3154
  if (this.editor !== this.defaultEditor) {
3155
+ this.editor.setBorder?.(editorBorder);
3051
3156
  this.editor.setPaddingX?.(editorPaddingX);
3052
3157
  this.editor.setAutocompleteMaxVisible?.(autocompleteMaxVisible);
3053
3158
  }