@kolisachint/hoocode-agent 0.4.157 → 0.4.158
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.
- package/CHANGELOG.md +60 -0
- package/dist/core/agent-session-stats.d.ts +17 -1
- package/dist/core/agent-session-stats.d.ts.map +1 -1
- package/dist/core/agent-session-stats.js +20 -0
- package/dist/core/agent-session-stats.js.map +1 -1
- package/dist/core/settings-defaults.d.ts +1 -0
- package/dist/core/settings-defaults.d.ts.map +1 -1
- package/dist/core/settings-defaults.js +2 -1
- package/dist/core/settings-defaults.js.map +1 -1
- package/dist/core/settings-manager.d.ts +2 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +8 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/settings-types.d.ts +1 -0
- package/dist/core/settings-types.d.ts.map +1 -1
- package/dist/core/settings-types.js.map +1 -1
- package/dist/core/tools/todo.d.ts +24 -0
- package/dist/core/tools/todo.d.ts.map +1 -1
- package/dist/core/tools/todo.js +50 -9
- package/dist/core/tools/todo.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +6 -15
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector.d.ts +2 -0
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +15 -2
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/components/task-panel.d.ts +20 -12
- package/dist/modes/interactive/components/task-panel.d.ts.map +1 -1
- package/dist/modes/interactive/components/task-panel.js +170 -224
- package/dist/modes/interactive/components/task-panel.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +42 -7
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +124 -19
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/docs/settings.md +2 -1
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package.json +1 -1
- 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
|
-
//
|
|
207
|
-
//
|
|
208
|
-
|
|
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
|
|
1669
|
-
* tick so the streaming flag settles and any retry has had a chance to arm,
|
|
1670
|
-
*
|
|
1671
|
-
* and no retry is pending
|
|
1672
|
-
*
|
|
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
|
-
|
|
1694
|
+
settleRequestOnIdle() {
|
|
1675
1695
|
setTimeout(() => {
|
|
1676
1696
|
if (this.chimePendingRetry || this.session.isStreaming || this.session.isCompacting) {
|
|
1677
1697
|
return;
|
|
1678
1698
|
}
|
|
1679
|
-
this.
|
|
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.
|
|
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
|
|
1793
|
-
//
|
|
1794
|
-
this.
|
|
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
|
|
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.
|
|
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
|
}
|