@kolisachint/hoocode-agent 0.4.156 → 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 +91 -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/context-files.d.ts +14 -4
- package/dist/core/context-files.d.ts.map +1 -1
- package/dist/core/context-files.js +7 -4
- package/dist/core/context-files.js.map +1 -1
- package/dist/core/format-tokens.d.ts +9 -0
- package/dist/core/format-tokens.d.ts.map +1 -0
- package/dist/core/format-tokens.js +19 -0
- package/dist/core/format-tokens.js.map +1 -0
- package/dist/core/mcp-status.d.ts +32 -0
- package/dist/core/mcp-status.d.ts.map +1 -0
- package/dist/core/mcp-status.js +27 -0
- package/dist/core/mcp-status.js.map +1 -0
- package/dist/core/resource-loader.d.ts +6 -16
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.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/core/wordmark.d.ts +2 -2
- package/dist/core/wordmark.d.ts.map +1 -1
- package/dist/core/wordmark.js +4 -4
- package/dist/core/wordmark.js.map +1 -1
- package/dist/extensions/core/hoo-core.d.ts +3 -0
- package/dist/extensions/core/hoo-core.d.ts.map +1 -1
- package/dist/extensions/core/hoo-core.js +3 -0
- package/dist/extensions/core/hoo-core.js.map +1 -1
- package/dist/extensions/core/mcp-loader.d.ts.map +1 -1
- package/dist/extensions/core/mcp-loader.js +27 -3
- package/dist/extensions/core/mcp-loader.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +7 -26
- 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 +171 -233
- package/dist/modes/interactive/components/task-panel.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +56 -7
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +174 -20
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/model-controller.d.ts +6 -0
- package/dist/modes/interactive/model-controller.d.ts.map +1 -1
- package/dist/modes/interactive/model-controller.js +33 -14
- package/dist/modes/interactive/model-controller.js.map +1 -1
- package/dist/modes/interactive/resource-display.d.ts +22 -0
- package/dist/modes/interactive/resource-display.d.ts.map +1 -1
- package/dist/modes/interactive/resource-display.js +73 -16
- package/dist/modes/interactive/resource-display.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
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,96 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.4.158] - 2026-08-07
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- The input editor is now a bordered box by default, replacing the two horizontal
|
|
8
|
+
rules. The rules were ambiguous at more than one line: a wrapped prompt read as
|
|
9
|
+
loose text between two dividers, with nothing tying the continuation lines to the
|
|
10
|
+
input. Side borders make the field's extent explicit, and the scroll indicators
|
|
11
|
+
now close (`┌─── ↑ 3 more ───┐`) instead of reading as section rules. The box
|
|
12
|
+
costs two columns of text width, so `editorBorder: "rule"` (also under
|
|
13
|
+
`/settings` → Interface → Editor border) restores the previous rendering.
|
|
14
|
+
`editorPaddingX` now defaults to `1` to match.
|
|
15
|
+
- The input prompt is now `❯` (U+276F) instead of `>`. Inside the box the ASCII
|
|
16
|
+
`>` read as a quote marker rather than a prompt; the ornament is unambiguous,
|
|
17
|
+
is one cell wide in every terminal (it is not East Asian ambiguous and has no
|
|
18
|
+
emoji presentation, so it cannot desync the box borders), and is the glyph most
|
|
19
|
+
shell prompts already use. Bash mode keeps its green `!`.
|
|
20
|
+
|
|
21
|
+
- Each request now ends with its own cost in the transcript (`↑3.9k ↓450 · 1.1s ·
|
|
22
|
+
$0.015 · ◇1 explore 12.4k`). This is where a retrospective number belongs: the
|
|
23
|
+
task panel is a live instrument that resets on the next message, and the footer
|
|
24
|
+
carries cumulative session vitals, so neither could answer what a given request
|
|
25
|
+
cost after the fact. Delegated runs are reported separately because subagents
|
|
26
|
+
bill against their own sessions.
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- The task panel header is now a lens tab strip on the left, replacing the state
|
|
31
|
+
stamp, progress bar and turn delta on the right. Each tab carries its own
|
|
32
|
+
`done/total`, so unfinished work in the lens you are *not* viewing stays visible
|
|
33
|
+
(`tasks 1/3 subagents 0/2`), and the selected tab fills only while its lens has
|
|
34
|
+
live work. With a single lens there is nothing to switch to and the header
|
|
35
|
+
disappears entirely — a plain TodoWrite session is now just its rows. The cycle
|
|
36
|
+
hint reads from the configured keybinding rather than a hardcoded key, and the
|
|
37
|
+
tab strip now survives narrowing instead of being the first thing dropped.
|
|
38
|
+
- An in-progress task renders the static `◐` its glyph table specifies. The panel
|
|
39
|
+
used to animate a braille spinner that duplicated the transcript loader's own
|
|
40
|
+
spinner — same frames, independent timer, permanently out of phase — and
|
|
41
|
+
re-rendered the whole component tree eight times a second. The only motion left
|
|
42
|
+
is a 1s run clock on delegated rows, and it runs only while one is live.
|
|
43
|
+
- Rows no longer restate their glyph in words (`queued`, `running…`, `cancelled`).
|
|
44
|
+
|
|
45
|
+
### Fixed
|
|
46
|
+
|
|
47
|
+
- TodoWrite rows no longer sit at `in_progress` after the request that owned them
|
|
48
|
+
has ended. The model marks its own plan items and routinely drops the final call
|
|
49
|
+
that completes the last one, so the panel kept claiming live work until the next
|
|
50
|
+
user message wiped it. Dangling items now settle when the agent goes idle: a
|
|
51
|
+
clean stop means the model believed it was finished, so they settle to done; an
|
|
52
|
+
abort, error or length cutoff settles them to cancelled (`⊘`) rather than
|
|
53
|
+
fabricating a completion. Only main-plan items are touched, and nothing settles
|
|
54
|
+
while a subagent is still running or a follow-up message is queued.
|
|
55
|
+
- A live TodoWrite row no longer shows an elapsed time. Every item in a plan shares
|
|
56
|
+
a `createdAt` (the tool creates the whole list in one batch), so the timer was
|
|
57
|
+
reporting the age of the *plan*, not of the task — item #3 included all the time
|
|
58
|
+
spent on #1 and #2. Delegated rows keep their clock, where `createdAt` is the
|
|
59
|
+
dispatch time and the number is real.
|
|
60
|
+
- The footer and the new per-request cost line share one usage sum, so they can no
|
|
61
|
+
longer disagree about what a session has spent.
|
|
62
|
+
|
|
63
|
+
## [0.4.157] - 2026-08-07
|
|
64
|
+
|
|
65
|
+
### Changed
|
|
66
|
+
|
|
67
|
+
- The startup page is one column and states the session instead of narrating it.
|
|
68
|
+
Everything on it now sits on a single left rail under the banner, and the model,
|
|
69
|
+
thinking level, and Anthropic subscription-billing caveat collapse into one
|
|
70
|
+
state line (`claude-sonnet-4-5 · thinking high · anthropic sub — billed as extra
|
|
71
|
+
usage`) rather than three separate status/warning lines that the footer already
|
|
72
|
+
carries for the rest of the session.
|
|
73
|
+
- MCP servers are reported as a capability, not as boot chatter. A successful
|
|
74
|
+
connect no longer prints a line above the summary; live servers get an `⧉` cell
|
|
75
|
+
in the counted grid and a `[MCP]` details row with tool count, background mode,
|
|
76
|
+
and schema deferral. Failures and authorization prompts still surface inline,
|
|
77
|
+
since those need action. The count now covers every source (`mcp.json`,
|
|
78
|
+
per-server files, plugins) instead of only plugin-contributed servers.
|
|
79
|
+
- The agents cell counts dispatchable subagents from the agent registry (and is
|
|
80
|
+
hidden when the Task tool is off). It previously counted only agents contributed
|
|
81
|
+
by package manifests, so a session with five usable agents could show none.
|
|
82
|
+
- Context files are priced where they are listed: an oversized `AGENTS.md` is
|
|
83
|
+
annotated inline (`~3.6k tokens · consider trimming`) instead of repeating the
|
|
84
|
+
file on a separate warning line. Size is now structural data on the loaded file
|
|
85
|
+
(`tokens`, `size`), and `warnings` carries only real failures.
|
|
86
|
+
- The banner tagline is `coding agent`.
|
|
87
|
+
|
|
88
|
+
### Removed
|
|
89
|
+
|
|
90
|
+
- `hoo-core` no longer appears in the loaded-resources summary or details. It is
|
|
91
|
+
built-in plumbing that is always present, so listing it as "1 extension" said
|
|
92
|
+
nothing about the session.
|
|
93
|
+
|
|
3
94
|
## [0.4.156] - 2026-08-07
|
|
4
95
|
|
|
5
96
|
### Added
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import type { AgentMessage } from "@kolisachint/hoocode-agent-core";
|
|
10
10
|
import type { Model } from "@kolisachint/hoocode-ai";
|
|
11
11
|
import type { ContextUsage } from "./extensions/index.js";
|
|
12
|
-
import type { SessionManager } from "./session-manager.js";
|
|
12
|
+
import type { SessionEntry, SessionManager } from "./session-manager.js";
|
|
13
13
|
/** Session statistics for /session command */
|
|
14
14
|
export interface SessionStats {
|
|
15
15
|
sessionFile: string | undefined;
|
|
@@ -29,6 +29,22 @@ export interface SessionStats {
|
|
|
29
29
|
cost: number;
|
|
30
30
|
contextUsage?: ContextUsage;
|
|
31
31
|
}
|
|
32
|
+
/** Token + cost totals accumulated across a session's assistant messages. */
|
|
33
|
+
export interface AssistantUsageTotals {
|
|
34
|
+
input: number;
|
|
35
|
+
output: number;
|
|
36
|
+
cacheRead: number;
|
|
37
|
+
cacheWrite: number;
|
|
38
|
+
cost: number;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Sum token + cost usage over every assistant message in a set of session
|
|
42
|
+
* entries. Shared by the footer (cumulative session vitals) and the per-request
|
|
43
|
+
* cost line the transcript emits at agent_end, which diffs two snapshots of this
|
|
44
|
+
* against each other — one source of truth, so the two can never disagree about
|
|
45
|
+
* what a request cost.
|
|
46
|
+
*/
|
|
47
|
+
export declare function sumAssistantUsage(entries: readonly SessionEntry[]): AssistantUsageTotals;
|
|
32
48
|
/** Extract concatenated text from a user message content value. */
|
|
33
49
|
export declare function extractUserMessageText(content: string | Array<{
|
|
34
50
|
type: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent-session-stats.d.ts","sourceRoot":"","sources":["../../src/core/agent-session-stats.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AAEpE,OAAO,KAAK,EAAoB,KAAK,EAAE,MAAM,yBAAyB,CAAC;AACvE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAC1D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAG3D,8CAA8C;AAC9C,MAAM,WAAW,YAAY;IAC5B,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE;QACP,KAAK,EAAE,MAAM,CAAC;QACd,MAAM,EAAE,MAAM,CAAC;QACf,SAAS,EAAE,MAAM,CAAC;QAClB,UAAU,EAAE,MAAM,CAAC;QACnB,KAAK,EAAE,MAAM,CAAC;KACd,CAAC;IACF,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,CAAC,EAAE,YAAY,CAAC;CAC5B;AAED,mEAAmE;AACnE,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,MAAM,GAAG,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,GAAG,MAAM,CASvG;AAED,sFAAsF;AACtF,wBAAgB,mBAAmB,CAAC,MAAM,EAAE;IAC3C,QAAQ,EAAE,YAAY,EAAE,CAAC;IACzB,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,YAAY,GAAG,SAAS,CAAC;CACvC,GAAG,YAAY,CA2Cf;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE;IAC3C,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,SAAS,CAAC;IAC9B,cAAc,EAAE,cAAc,CAAC;IAC/B,QAAQ,EAAE,YAAY,EAAE,CAAC;CACzB,GAAG,YAAY,GAAG,SAAS,CAyC3B;AAED,wEAAwE;AACxE,wBAAgB,6BAA6B,CAC5C,cAAc,EAAE,cAAc,GAC5B,KAAK,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAe1C;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,YAAY,EAAE,GAAG,MAAM,GAAG,SAAS,CAsBjF;AAED;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,cAAc,EAAE,cAAc,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CA4BtG","sourcesContent":["/**\n * Pure helpers for AgentSession reporting and export.\n *\n * These functions derive statistics, context-usage, forkable user messages, and\n * JSONL exports from session state without touching the live agent or extension\n * runner. AgentSession delegates to them so the class stays focused on lifecycle\n * and event wiring.\n */\n\nimport { existsSync, mkdirSync, writeFileSync } from \"node:fs\";\nimport { dirname, resolve } from \"node:path\";\nimport type { AgentMessage } from \"@kolisachint/hoocode-agent-core\";\nimport { calculateContextTokens, estimateContextTokens } from \"@kolisachint/hoocode-agent-core\";\nimport type { AssistantMessage, Model } from \"@kolisachint/hoocode-ai\";\nimport type { ContextUsage } from \"./extensions/index.js\";\nimport type { SessionManager } from \"./session-manager.js\";\nimport { CURRENT_SESSION_VERSION, getLatestCompactionEntry, type SessionHeader } from \"./session-manager.js\";\n\n/** Session statistics for /session command */\nexport interface SessionStats {\n\tsessionFile: string | undefined;\n\tsessionId: string;\n\tuserMessages: number;\n\tassistantMessages: number;\n\ttoolCalls: number;\n\ttoolResults: number;\n\ttotalMessages: number;\n\ttokens: {\n\t\tinput: number;\n\t\toutput: number;\n\t\tcacheRead: number;\n\t\tcacheWrite: number;\n\t\ttotal: number;\n\t};\n\tcost: number;\n\tcontextUsage?: ContextUsage;\n}\n\n/** Extract concatenated text from a user message content value. */\nexport function extractUserMessageText(content: string | Array<{ type: string; text?: string }>): string {\n\tif (typeof content === \"string\") return content;\n\tif (Array.isArray(content)) {\n\t\treturn content\n\t\t\t.filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n\t\t\t.map((c) => c.text)\n\t\t\t.join(\"\");\n\t}\n\treturn \"\";\n}\n\n/** Compute aggregate statistics (message counts, token usage, cost) for a session. */\nexport function computeSessionStats(params: {\n\tmessages: AgentMessage[];\n\tsessionFile: string | undefined;\n\tsessionId: string;\n\tcontextUsage: ContextUsage | undefined;\n}): SessionStats {\n\tconst { messages, sessionFile, sessionId, contextUsage } = params;\n\tconst userMessages = messages.filter((m) => m.role === \"user\").length;\n\tconst assistantMessages = messages.filter((m) => m.role === \"assistant\").length;\n\tconst toolResults = messages.filter((m) => m.role === \"toolResult\").length;\n\n\tlet toolCalls = 0;\n\tlet totalInput = 0;\n\tlet totalOutput = 0;\n\tlet totalCacheRead = 0;\n\tlet totalCacheWrite = 0;\n\tlet totalCost = 0;\n\n\tfor (const message of messages) {\n\t\tif (message.role === \"assistant\") {\n\t\t\tconst assistantMsg = message as AssistantMessage;\n\t\t\ttoolCalls += assistantMsg.content.filter((c) => c.type === \"toolCall\").length;\n\t\t\ttotalInput += assistantMsg.usage.input;\n\t\t\ttotalOutput += assistantMsg.usage.output;\n\t\t\ttotalCacheRead += assistantMsg.usage.cacheRead;\n\t\t\ttotalCacheWrite += assistantMsg.usage.cacheWrite;\n\t\t\ttotalCost += assistantMsg.usage.cost.total;\n\t\t}\n\t}\n\n\treturn {\n\t\tsessionFile,\n\t\tsessionId,\n\t\tuserMessages,\n\t\tassistantMessages,\n\t\ttoolCalls,\n\t\ttoolResults,\n\t\ttotalMessages: messages.length,\n\t\ttokens: {\n\t\t\tinput: totalInput,\n\t\t\toutput: totalOutput,\n\t\t\tcacheRead: totalCacheRead,\n\t\t\tcacheWrite: totalCacheWrite,\n\t\t\ttotal: totalInput + totalOutput + totalCacheRead + totalCacheWrite,\n\t\t},\n\t\tcost: totalCost,\n\t\tcontextUsage,\n\t};\n}\n\n/**\n * Estimate current context-window usage.\n *\n * After compaction, the last assistant usage reflects pre-compaction context\n * size, so usage is only trusted from an assistant that responded after the\n * latest compaction boundary. When no such assistant exists yet, tokens are\n * reported as null (unknown until the next LLM response).\n */\nexport function computeContextUsage(params: {\n\tmodel: Model<any> | undefined;\n\tsessionManager: SessionManager;\n\tmessages: AgentMessage[];\n}): ContextUsage | undefined {\n\tconst { model, sessionManager, messages } = params;\n\tif (!model) return undefined;\n\n\tconst contextWindow = model.contextWindow ?? 0;\n\tif (contextWindow <= 0) return undefined;\n\n\tconst branchEntries = sessionManager.getBranch();\n\tconst latestCompaction = getLatestCompactionEntry(branchEntries);\n\n\tif (latestCompaction) {\n\t\t// Check if there's a valid assistant usage after the compaction boundary\n\t\tconst compactionIndex = branchEntries.lastIndexOf(latestCompaction);\n\t\tlet hasPostCompactionUsage = false;\n\t\tfor (let i = branchEntries.length - 1; i > compactionIndex; i--) {\n\t\t\tconst entry = branchEntries[i];\n\t\t\tif (entry.type === \"message\" && entry.message.role === \"assistant\") {\n\t\t\t\tconst assistant = entry.message;\n\t\t\t\tif (assistant.stopReason !== \"aborted\" && assistant.stopReason !== \"error\") {\n\t\t\t\t\tconst contextTokens = calculateContextTokens(assistant.usage);\n\t\t\t\t\tif (contextTokens > 0) {\n\t\t\t\t\t\thasPostCompactionUsage = true;\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (!hasPostCompactionUsage) {\n\t\t\treturn { tokens: null, contextWindow, percent: null };\n\t\t}\n\t}\n\n\tconst estimate = estimateContextTokens(messages);\n\tconst percent = (estimate.tokens / contextWindow) * 100;\n\n\treturn {\n\t\ttokens: estimate.tokens,\n\t\tcontextWindow,\n\t\tpercent,\n\t};\n}\n\n/** Collect all user messages on the session (for the fork selector). */\nexport function collectUserMessagesForForking(\n\tsessionManager: SessionManager,\n): Array<{ entryId: string; text: string }> {\n\tconst entries = sessionManager.getEntries();\n\tconst result: Array<{ entryId: string; text: string }> = [];\n\n\tfor (const entry of entries) {\n\t\tif (entry.type !== \"message\") continue;\n\t\tif (entry.message.role !== \"user\") continue;\n\n\t\tconst text = extractUserMessageText(entry.message.content);\n\t\tif (text) {\n\t\t\tresult.push({ entryId: entry.id, text });\n\t\t}\n\t}\n\n\treturn result;\n}\n\n/**\n * Get the text content of the last non-empty assistant message (for /copy).\n * Returns undefined if no assistant message with text exists.\n */\nexport function getLastAssistantText(messages: AgentMessage[]): string | undefined {\n\tconst lastAssistant = messages\n\t\t.slice()\n\t\t.reverse()\n\t\t.find((m) => {\n\t\t\tif (m.role !== \"assistant\") return false;\n\t\t\tconst msg = m as AssistantMessage;\n\t\t\t// Skip aborted messages with no content\n\t\t\tif (msg.stopReason === \"aborted\" && msg.content.length === 0) return false;\n\t\t\treturn true;\n\t\t});\n\n\tif (!lastAssistant) return undefined;\n\n\tlet text = \"\";\n\tfor (const content of (lastAssistant as AssistantMessage).content) {\n\t\tif (content.type === \"text\") {\n\t\t\ttext += content.text;\n\t\t}\n\t}\n\n\treturn text.trim() || undefined;\n}\n\n/**\n * Export the current session branch to a JSONL file.\n * Writes the session header followed by all entries on the current branch path,\n * re-chaining parentIds into a linear sequence.\n * @returns The resolved output file path.\n */\nexport function exportSessionBranchToJsonl(sessionManager: SessionManager, outputPath?: string): string {\n\tconst filePath = resolve(outputPath ?? `session-${new Date().toISOString().replace(/[:.]/g, \"-\")}.jsonl`);\n\tconst dir = dirname(filePath);\n\tif (!existsSync(dir)) {\n\t\tmkdirSync(dir, { recursive: true });\n\t}\n\n\tconst header: SessionHeader = {\n\t\ttype: \"session\",\n\t\tversion: CURRENT_SESSION_VERSION,\n\t\tid: sessionManager.getSessionId(),\n\t\ttimestamp: new Date().toISOString(),\n\t\tcwd: sessionManager.getCwd(),\n\t};\n\n\tconst branchEntries = sessionManager.getBranch();\n\tconst lines = [JSON.stringify(header)];\n\n\t// Re-chain parentIds to form a linear sequence\n\tlet prevId: string | null = null;\n\tfor (const entry of branchEntries) {\n\t\tconst linear = { ...entry, parentId: prevId };\n\t\tlines.push(JSON.stringify(linear));\n\t\tprevId = entry.id;\n\t}\n\n\twriteFileSync(filePath, `${lines.join(\"\\n\")}\\n`);\n\treturn filePath;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"agent-session-stats.d.ts","sourceRoot":"","sources":["../../src/core/agent-session-stats.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AAEpE,OAAO,KAAK,EAAoB,KAAK,EAAE,MAAM,yBAAyB,CAAC;AACvE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAC1D,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAGzE,8CAA8C;AAC9C,MAAM,WAAW,YAAY;IAC5B,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE;QACP,KAAK,EAAE,MAAM,CAAC;QACd,MAAM,EAAE,MAAM,CAAC;QACf,SAAS,EAAE,MAAM,CAAC;QAClB,UAAU,EAAE,MAAM,CAAC;QACnB,KAAK,EAAE,MAAM,CAAC;KACd,CAAC;IACF,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,CAAC,EAAE,YAAY,CAAC;CAC5B;AAED,6EAA6E;AAC7E,MAAM,WAAW,oBAAoB;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;CACb;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,SAAS,YAAY,EAAE,GAAG,oBAAoB,CAWxF;AAED,mEAAmE;AACnE,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,MAAM,GAAG,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,GAAG,MAAM,CASvG;AAED,sFAAsF;AACtF,wBAAgB,mBAAmB,CAAC,MAAM,EAAE;IAC3C,QAAQ,EAAE,YAAY,EAAE,CAAC;IACzB,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,YAAY,GAAG,SAAS,CAAC;CACvC,GAAG,YAAY,CA2Cf;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE;IAC3C,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,SAAS,CAAC;IAC9B,cAAc,EAAE,cAAc,CAAC;IAC/B,QAAQ,EAAE,YAAY,EAAE,CAAC;CACzB,GAAG,YAAY,GAAG,SAAS,CAyC3B;AAED,wEAAwE;AACxE,wBAAgB,6BAA6B,CAC5C,cAAc,EAAE,cAAc,GAC5B,KAAK,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAe1C;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,YAAY,EAAE,GAAG,MAAM,GAAG,SAAS,CAsBjF;AAED;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,cAAc,EAAE,cAAc,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CA4BtG","sourcesContent":["/**\n * Pure helpers for AgentSession reporting and export.\n *\n * These functions derive statistics, context-usage, forkable user messages, and\n * JSONL exports from session state without touching the live agent or extension\n * runner. AgentSession delegates to them so the class stays focused on lifecycle\n * and event wiring.\n */\n\nimport { existsSync, mkdirSync, writeFileSync } from \"node:fs\";\nimport { dirname, resolve } from \"node:path\";\nimport type { AgentMessage } from \"@kolisachint/hoocode-agent-core\";\nimport { calculateContextTokens, estimateContextTokens } from \"@kolisachint/hoocode-agent-core\";\nimport type { AssistantMessage, Model } from \"@kolisachint/hoocode-ai\";\nimport type { ContextUsage } from \"./extensions/index.js\";\nimport type { SessionEntry, SessionManager } from \"./session-manager.js\";\nimport { CURRENT_SESSION_VERSION, getLatestCompactionEntry, type SessionHeader } from \"./session-manager.js\";\n\n/** Session statistics for /session command */\nexport interface SessionStats {\n\tsessionFile: string | undefined;\n\tsessionId: string;\n\tuserMessages: number;\n\tassistantMessages: number;\n\ttoolCalls: number;\n\ttoolResults: number;\n\ttotalMessages: number;\n\ttokens: {\n\t\tinput: number;\n\t\toutput: number;\n\t\tcacheRead: number;\n\t\tcacheWrite: number;\n\t\ttotal: number;\n\t};\n\tcost: number;\n\tcontextUsage?: ContextUsage;\n}\n\n/** Token + cost totals accumulated across a session's assistant messages. */\nexport interface AssistantUsageTotals {\n\tinput: number;\n\toutput: number;\n\tcacheRead: number;\n\tcacheWrite: number;\n\tcost: number;\n}\n\n/**\n * Sum token + cost usage over every assistant message in a set of session\n * entries. Shared by the footer (cumulative session vitals) and the per-request\n * cost line the transcript emits at agent_end, which diffs two snapshots of this\n * against each other — one source of truth, so the two can never disagree about\n * what a request cost.\n */\nexport function sumAssistantUsage(entries: readonly SessionEntry[]): AssistantUsageTotals {\n\tconst totals: AssistantUsageTotals = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };\n\tfor (const entry of entries) {\n\t\tif (entry.type !== \"message\" || entry.message.role !== \"assistant\") continue;\n\t\ttotals.input += entry.message.usage.input;\n\t\ttotals.output += entry.message.usage.output;\n\t\ttotals.cacheRead += entry.message.usage.cacheRead;\n\t\ttotals.cacheWrite += entry.message.usage.cacheWrite;\n\t\ttotals.cost += entry.message.usage.cost.total;\n\t}\n\treturn totals;\n}\n\n/** Extract concatenated text from a user message content value. */\nexport function extractUserMessageText(content: string | Array<{ type: string; text?: string }>): string {\n\tif (typeof content === \"string\") return content;\n\tif (Array.isArray(content)) {\n\t\treturn content\n\t\t\t.filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n\t\t\t.map((c) => c.text)\n\t\t\t.join(\"\");\n\t}\n\treturn \"\";\n}\n\n/** Compute aggregate statistics (message counts, token usage, cost) for a session. */\nexport function computeSessionStats(params: {\n\tmessages: AgentMessage[];\n\tsessionFile: string | undefined;\n\tsessionId: string;\n\tcontextUsage: ContextUsage | undefined;\n}): SessionStats {\n\tconst { messages, sessionFile, sessionId, contextUsage } = params;\n\tconst userMessages = messages.filter((m) => m.role === \"user\").length;\n\tconst assistantMessages = messages.filter((m) => m.role === \"assistant\").length;\n\tconst toolResults = messages.filter((m) => m.role === \"toolResult\").length;\n\n\tlet toolCalls = 0;\n\tlet totalInput = 0;\n\tlet totalOutput = 0;\n\tlet totalCacheRead = 0;\n\tlet totalCacheWrite = 0;\n\tlet totalCost = 0;\n\n\tfor (const message of messages) {\n\t\tif (message.role === \"assistant\") {\n\t\t\tconst assistantMsg = message as AssistantMessage;\n\t\t\ttoolCalls += assistantMsg.content.filter((c) => c.type === \"toolCall\").length;\n\t\t\ttotalInput += assistantMsg.usage.input;\n\t\t\ttotalOutput += assistantMsg.usage.output;\n\t\t\ttotalCacheRead += assistantMsg.usage.cacheRead;\n\t\t\ttotalCacheWrite += assistantMsg.usage.cacheWrite;\n\t\t\ttotalCost += assistantMsg.usage.cost.total;\n\t\t}\n\t}\n\n\treturn {\n\t\tsessionFile,\n\t\tsessionId,\n\t\tuserMessages,\n\t\tassistantMessages,\n\t\ttoolCalls,\n\t\ttoolResults,\n\t\ttotalMessages: messages.length,\n\t\ttokens: {\n\t\t\tinput: totalInput,\n\t\t\toutput: totalOutput,\n\t\t\tcacheRead: totalCacheRead,\n\t\t\tcacheWrite: totalCacheWrite,\n\t\t\ttotal: totalInput + totalOutput + totalCacheRead + totalCacheWrite,\n\t\t},\n\t\tcost: totalCost,\n\t\tcontextUsage,\n\t};\n}\n\n/**\n * Estimate current context-window usage.\n *\n * After compaction, the last assistant usage reflects pre-compaction context\n * size, so usage is only trusted from an assistant that responded after the\n * latest compaction boundary. When no such assistant exists yet, tokens are\n * reported as null (unknown until the next LLM response).\n */\nexport function computeContextUsage(params: {\n\tmodel: Model<any> | undefined;\n\tsessionManager: SessionManager;\n\tmessages: AgentMessage[];\n}): ContextUsage | undefined {\n\tconst { model, sessionManager, messages } = params;\n\tif (!model) return undefined;\n\n\tconst contextWindow = model.contextWindow ?? 0;\n\tif (contextWindow <= 0) return undefined;\n\n\tconst branchEntries = sessionManager.getBranch();\n\tconst latestCompaction = getLatestCompactionEntry(branchEntries);\n\n\tif (latestCompaction) {\n\t\t// Check if there's a valid assistant usage after the compaction boundary\n\t\tconst compactionIndex = branchEntries.lastIndexOf(latestCompaction);\n\t\tlet hasPostCompactionUsage = false;\n\t\tfor (let i = branchEntries.length - 1; i > compactionIndex; i--) {\n\t\t\tconst entry = branchEntries[i];\n\t\t\tif (entry.type === \"message\" && entry.message.role === \"assistant\") {\n\t\t\t\tconst assistant = entry.message;\n\t\t\t\tif (assistant.stopReason !== \"aborted\" && assistant.stopReason !== \"error\") {\n\t\t\t\t\tconst contextTokens = calculateContextTokens(assistant.usage);\n\t\t\t\t\tif (contextTokens > 0) {\n\t\t\t\t\t\thasPostCompactionUsage = true;\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (!hasPostCompactionUsage) {\n\t\t\treturn { tokens: null, contextWindow, percent: null };\n\t\t}\n\t}\n\n\tconst estimate = estimateContextTokens(messages);\n\tconst percent = (estimate.tokens / contextWindow) * 100;\n\n\treturn {\n\t\ttokens: estimate.tokens,\n\t\tcontextWindow,\n\t\tpercent,\n\t};\n}\n\n/** Collect all user messages on the session (for the fork selector). */\nexport function collectUserMessagesForForking(\n\tsessionManager: SessionManager,\n): Array<{ entryId: string; text: string }> {\n\tconst entries = sessionManager.getEntries();\n\tconst result: Array<{ entryId: string; text: string }> = [];\n\n\tfor (const entry of entries) {\n\t\tif (entry.type !== \"message\") continue;\n\t\tif (entry.message.role !== \"user\") continue;\n\n\t\tconst text = extractUserMessageText(entry.message.content);\n\t\tif (text) {\n\t\t\tresult.push({ entryId: entry.id, text });\n\t\t}\n\t}\n\n\treturn result;\n}\n\n/**\n * Get the text content of the last non-empty assistant message (for /copy).\n * Returns undefined if no assistant message with text exists.\n */\nexport function getLastAssistantText(messages: AgentMessage[]): string | undefined {\n\tconst lastAssistant = messages\n\t\t.slice()\n\t\t.reverse()\n\t\t.find((m) => {\n\t\t\tif (m.role !== \"assistant\") return false;\n\t\t\tconst msg = m as AssistantMessage;\n\t\t\t// Skip aborted messages with no content\n\t\t\tif (msg.stopReason === \"aborted\" && msg.content.length === 0) return false;\n\t\t\treturn true;\n\t\t});\n\n\tif (!lastAssistant) return undefined;\n\n\tlet text = \"\";\n\tfor (const content of (lastAssistant as AssistantMessage).content) {\n\t\tif (content.type === \"text\") {\n\t\t\ttext += content.text;\n\t\t}\n\t}\n\n\treturn text.trim() || undefined;\n}\n\n/**\n * Export the current session branch to a JSONL file.\n * Writes the session header followed by all entries on the current branch path,\n * re-chaining parentIds into a linear sequence.\n * @returns The resolved output file path.\n */\nexport function exportSessionBranchToJsonl(sessionManager: SessionManager, outputPath?: string): string {\n\tconst filePath = resolve(outputPath ?? `session-${new Date().toISOString().replace(/[:.]/g, \"-\")}.jsonl`);\n\tconst dir = dirname(filePath);\n\tif (!existsSync(dir)) {\n\t\tmkdirSync(dir, { recursive: true });\n\t}\n\n\tconst header: SessionHeader = {\n\t\ttype: \"session\",\n\t\tversion: CURRENT_SESSION_VERSION,\n\t\tid: sessionManager.getSessionId(),\n\t\ttimestamp: new Date().toISOString(),\n\t\tcwd: sessionManager.getCwd(),\n\t};\n\n\tconst branchEntries = sessionManager.getBranch();\n\tconst lines = [JSON.stringify(header)];\n\n\t// Re-chain parentIds to form a linear sequence\n\tlet prevId: string | null = null;\n\tfor (const entry of branchEntries) {\n\t\tconst linear = { ...entry, parentId: prevId };\n\t\tlines.push(JSON.stringify(linear));\n\t\tprevId = entry.id;\n\t}\n\n\twriteFileSync(filePath, `${lines.join(\"\\n\")}\\n`);\n\treturn filePath;\n}\n"]}
|
|
@@ -10,6 +10,26 @@ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
|
10
10
|
import { dirname, resolve } from "node:path";
|
|
11
11
|
import { calculateContextTokens, estimateContextTokens } from "@kolisachint/hoocode-agent-core";
|
|
12
12
|
import { CURRENT_SESSION_VERSION, getLatestCompactionEntry } from "./session-manager.js";
|
|
13
|
+
/**
|
|
14
|
+
* Sum token + cost usage over every assistant message in a set of session
|
|
15
|
+
* entries. Shared by the footer (cumulative session vitals) and the per-request
|
|
16
|
+
* cost line the transcript emits at agent_end, which diffs two snapshots of this
|
|
17
|
+
* against each other — one source of truth, so the two can never disagree about
|
|
18
|
+
* what a request cost.
|
|
19
|
+
*/
|
|
20
|
+
export function sumAssistantUsage(entries) {
|
|
21
|
+
const totals = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
|
|
22
|
+
for (const entry of entries) {
|
|
23
|
+
if (entry.type !== "message" || entry.message.role !== "assistant")
|
|
24
|
+
continue;
|
|
25
|
+
totals.input += entry.message.usage.input;
|
|
26
|
+
totals.output += entry.message.usage.output;
|
|
27
|
+
totals.cacheRead += entry.message.usage.cacheRead;
|
|
28
|
+
totals.cacheWrite += entry.message.usage.cacheWrite;
|
|
29
|
+
totals.cost += entry.message.usage.cost.total;
|
|
30
|
+
}
|
|
31
|
+
return totals;
|
|
32
|
+
}
|
|
13
33
|
/** Extract concatenated text from a user message content value. */
|
|
14
34
|
export function extractUserMessageText(content) {
|
|
15
35
|
if (typeof content === "string")
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent-session-stats.js","sourceRoot":"","sources":["../../src/core/agent-session-stats.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Pure helpers for AgentSession reporting and export.\n *\n * These functions derive statistics, context-usage, forkable user messages, and\n * JSONL exports from session state without touching the live agent or extension\n * runner. AgentSession delegates to them so the class stays focused on lifecycle\n * and event wiring.\n */\n\nimport { existsSync, mkdirSync, writeFileSync } from \"node:fs\";\nimport { dirname, resolve } from \"node:path\";\nimport type { AgentMessage } from \"@kolisachint/hoocode-agent-core\";\nimport { calculateContextTokens, estimateContextTokens } from \"@kolisachint/hoocode-agent-core\";\nimport type { AssistantMessage, Model } from \"@kolisachint/hoocode-ai\";\nimport type { ContextUsage } from \"./extensions/index.js\";\nimport type { SessionManager } from \"./session-manager.js\";\nimport { CURRENT_SESSION_VERSION, getLatestCompactionEntry, type SessionHeader } from \"./session-manager.js\";\n\n/** Session statistics for /session command */\nexport interface SessionStats {\n\tsessionFile: string | undefined;\n\tsessionId: string;\n\tuserMessages: number;\n\tassistantMessages: number;\n\ttoolCalls: number;\n\ttoolResults: number;\n\ttotalMessages: number;\n\ttokens: {\n\t\tinput: number;\n\t\toutput: number;\n\t\tcacheRead: number;\n\t\tcacheWrite: number;\n\t\ttotal: number;\n\t};\n\tcost: number;\n\tcontextUsage?: ContextUsage;\n}\n\n/** Extract concatenated text from a user message content value. */\nexport function extractUserMessageText(content: string | Array<{ type: string; text?: string }>): string {\n\tif (typeof content === \"string\") return content;\n\tif (Array.isArray(content)) {\n\t\treturn content\n\t\t\t.filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n\t\t\t.map((c) => c.text)\n\t\t\t.join(\"\");\n\t}\n\treturn \"\";\n}\n\n/** Compute aggregate statistics (message counts, token usage, cost) for a session. */\nexport function computeSessionStats(params: {\n\tmessages: AgentMessage[];\n\tsessionFile: string | undefined;\n\tsessionId: string;\n\tcontextUsage: ContextUsage | undefined;\n}): SessionStats {\n\tconst { messages, sessionFile, sessionId, contextUsage } = params;\n\tconst userMessages = messages.filter((m) => m.role === \"user\").length;\n\tconst assistantMessages = messages.filter((m) => m.role === \"assistant\").length;\n\tconst toolResults = messages.filter((m) => m.role === \"toolResult\").length;\n\n\tlet toolCalls = 0;\n\tlet totalInput = 0;\n\tlet totalOutput = 0;\n\tlet totalCacheRead = 0;\n\tlet totalCacheWrite = 0;\n\tlet totalCost = 0;\n\n\tfor (const message of messages) {\n\t\tif (message.role === \"assistant\") {\n\t\t\tconst assistantMsg = message as AssistantMessage;\n\t\t\ttoolCalls += assistantMsg.content.filter((c) => c.type === \"toolCall\").length;\n\t\t\ttotalInput += assistantMsg.usage.input;\n\t\t\ttotalOutput += assistantMsg.usage.output;\n\t\t\ttotalCacheRead += assistantMsg.usage.cacheRead;\n\t\t\ttotalCacheWrite += assistantMsg.usage.cacheWrite;\n\t\t\ttotalCost += assistantMsg.usage.cost.total;\n\t\t}\n\t}\n\n\treturn {\n\t\tsessionFile,\n\t\tsessionId,\n\t\tuserMessages,\n\t\tassistantMessages,\n\t\ttoolCalls,\n\t\ttoolResults,\n\t\ttotalMessages: messages.length,\n\t\ttokens: {\n\t\t\tinput: totalInput,\n\t\t\toutput: totalOutput,\n\t\t\tcacheRead: totalCacheRead,\n\t\t\tcacheWrite: totalCacheWrite,\n\t\t\ttotal: totalInput + totalOutput + totalCacheRead + totalCacheWrite,\n\t\t},\n\t\tcost: totalCost,\n\t\tcontextUsage,\n\t};\n}\n\n/**\n * Estimate current context-window usage.\n *\n * After compaction, the last assistant usage reflects pre-compaction context\n * size, so usage is only trusted from an assistant that responded after the\n * latest compaction boundary. When no such assistant exists yet, tokens are\n * reported as null (unknown until the next LLM response).\n */\nexport function computeContextUsage(params: {\n\tmodel: Model<any> | undefined;\n\tsessionManager: SessionManager;\n\tmessages: AgentMessage[];\n}): ContextUsage | undefined {\n\tconst { model, sessionManager, messages } = params;\n\tif (!model) return undefined;\n\n\tconst contextWindow = model.contextWindow ?? 0;\n\tif (contextWindow <= 0) return undefined;\n\n\tconst branchEntries = sessionManager.getBranch();\n\tconst latestCompaction = getLatestCompactionEntry(branchEntries);\n\n\tif (latestCompaction) {\n\t\t// Check if there's a valid assistant usage after the compaction boundary\n\t\tconst compactionIndex = branchEntries.lastIndexOf(latestCompaction);\n\t\tlet hasPostCompactionUsage = false;\n\t\tfor (let i = branchEntries.length - 1; i > compactionIndex; i--) {\n\t\t\tconst entry = branchEntries[i];\n\t\t\tif (entry.type === \"message\" && entry.message.role === \"assistant\") {\n\t\t\t\tconst assistant = entry.message;\n\t\t\t\tif (assistant.stopReason !== \"aborted\" && assistant.stopReason !== \"error\") {\n\t\t\t\t\tconst contextTokens = calculateContextTokens(assistant.usage);\n\t\t\t\t\tif (contextTokens > 0) {\n\t\t\t\t\t\thasPostCompactionUsage = true;\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (!hasPostCompactionUsage) {\n\t\t\treturn { tokens: null, contextWindow, percent: null };\n\t\t}\n\t}\n\n\tconst estimate = estimateContextTokens(messages);\n\tconst percent = (estimate.tokens / contextWindow) * 100;\n\n\treturn {\n\t\ttokens: estimate.tokens,\n\t\tcontextWindow,\n\t\tpercent,\n\t};\n}\n\n/** Collect all user messages on the session (for the fork selector). */\nexport function collectUserMessagesForForking(\n\tsessionManager: SessionManager,\n): Array<{ entryId: string; text: string }> {\n\tconst entries = sessionManager.getEntries();\n\tconst result: Array<{ entryId: string; text: string }> = [];\n\n\tfor (const entry of entries) {\n\t\tif (entry.type !== \"message\") continue;\n\t\tif (entry.message.role !== \"user\") continue;\n\n\t\tconst text = extractUserMessageText(entry.message.content);\n\t\tif (text) {\n\t\t\tresult.push({ entryId: entry.id, text });\n\t\t}\n\t}\n\n\treturn result;\n}\n\n/**\n * Get the text content of the last non-empty assistant message (for /copy).\n * Returns undefined if no assistant message with text exists.\n */\nexport function getLastAssistantText(messages: AgentMessage[]): string | undefined {\n\tconst lastAssistant = messages\n\t\t.slice()\n\t\t.reverse()\n\t\t.find((m) => {\n\t\t\tif (m.role !== \"assistant\") return false;\n\t\t\tconst msg = m as AssistantMessage;\n\t\t\t// Skip aborted messages with no content\n\t\t\tif (msg.stopReason === \"aborted\" && msg.content.length === 0) return false;\n\t\t\treturn true;\n\t\t});\n\n\tif (!lastAssistant) return undefined;\n\n\tlet text = \"\";\n\tfor (const content of (lastAssistant as AssistantMessage).content) {\n\t\tif (content.type === \"text\") {\n\t\t\ttext += content.text;\n\t\t}\n\t}\n\n\treturn text.trim() || undefined;\n}\n\n/**\n * Export the current session branch to a JSONL file.\n * Writes the session header followed by all entries on the current branch path,\n * re-chaining parentIds into a linear sequence.\n * @returns The resolved output file path.\n */\nexport function exportSessionBranchToJsonl(sessionManager: SessionManager, outputPath?: string): string {\n\tconst filePath = resolve(outputPath ?? `session-${new Date().toISOString().replace(/[:.]/g, \"-\")}.jsonl`);\n\tconst dir = dirname(filePath);\n\tif (!existsSync(dir)) {\n\t\tmkdirSync(dir, { recursive: true });\n\t}\n\n\tconst header: SessionHeader = {\n\t\ttype: \"session\",\n\t\tversion: CURRENT_SESSION_VERSION,\n\t\tid: sessionManager.getSessionId(),\n\t\ttimestamp: new Date().toISOString(),\n\t\tcwd: sessionManager.getCwd(),\n\t};\n\n\tconst branchEntries = sessionManager.getBranch();\n\tconst lines = [JSON.stringify(header)];\n\n\t// Re-chain parentIds to form a linear sequence\n\tlet prevId: string | null = null;\n\tfor (const entry of branchEntries) {\n\t\tconst linear = { ...entry, parentId: prevId };\n\t\tlines.push(JSON.stringify(linear));\n\t\tprevId = entry.id;\n\t}\n\n\twriteFileSync(filePath, `${lines.join(\"\\n\")}\\n`);\n\treturn filePath;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"agent-session-stats.js","sourceRoot":"","sources":["../../src/core/agent-session-stats.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Pure helpers for AgentSession reporting and export.\n *\n * These functions derive statistics, context-usage, forkable user messages, and\n * JSONL exports from session state without touching the live agent or extension\n * runner. AgentSession delegates to them so the class stays focused on lifecycle\n * and event wiring.\n */\n\nimport { existsSync, mkdirSync, writeFileSync } from \"node:fs\";\nimport { dirname, resolve } from \"node:path\";\nimport type { AgentMessage } from \"@kolisachint/hoocode-agent-core\";\nimport { calculateContextTokens, estimateContextTokens } from \"@kolisachint/hoocode-agent-core\";\nimport type { AssistantMessage, Model } from \"@kolisachint/hoocode-ai\";\nimport type { ContextUsage } from \"./extensions/index.js\";\nimport type { SessionEntry, SessionManager } from \"./session-manager.js\";\nimport { CURRENT_SESSION_VERSION, getLatestCompactionEntry, type SessionHeader } from \"./session-manager.js\";\n\n/** Session statistics for /session command */\nexport interface SessionStats {\n\tsessionFile: string | undefined;\n\tsessionId: string;\n\tuserMessages: number;\n\tassistantMessages: number;\n\ttoolCalls: number;\n\ttoolResults: number;\n\ttotalMessages: number;\n\ttokens: {\n\t\tinput: number;\n\t\toutput: number;\n\t\tcacheRead: number;\n\t\tcacheWrite: number;\n\t\ttotal: number;\n\t};\n\tcost: number;\n\tcontextUsage?: ContextUsage;\n}\n\n/** Token + cost totals accumulated across a session's assistant messages. */\nexport interface AssistantUsageTotals {\n\tinput: number;\n\toutput: number;\n\tcacheRead: number;\n\tcacheWrite: number;\n\tcost: number;\n}\n\n/**\n * Sum token + cost usage over every assistant message in a set of session\n * entries. Shared by the footer (cumulative session vitals) and the per-request\n * cost line the transcript emits at agent_end, which diffs two snapshots of this\n * against each other — one source of truth, so the two can never disagree about\n * what a request cost.\n */\nexport function sumAssistantUsage(entries: readonly SessionEntry[]): AssistantUsageTotals {\n\tconst totals: AssistantUsageTotals = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };\n\tfor (const entry of entries) {\n\t\tif (entry.type !== \"message\" || entry.message.role !== \"assistant\") continue;\n\t\ttotals.input += entry.message.usage.input;\n\t\ttotals.output += entry.message.usage.output;\n\t\ttotals.cacheRead += entry.message.usage.cacheRead;\n\t\ttotals.cacheWrite += entry.message.usage.cacheWrite;\n\t\ttotals.cost += entry.message.usage.cost.total;\n\t}\n\treturn totals;\n}\n\n/** Extract concatenated text from a user message content value. */\nexport function extractUserMessageText(content: string | Array<{ type: string; text?: string }>): string {\n\tif (typeof content === \"string\") return content;\n\tif (Array.isArray(content)) {\n\t\treturn content\n\t\t\t.filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n\t\t\t.map((c) => c.text)\n\t\t\t.join(\"\");\n\t}\n\treturn \"\";\n}\n\n/** Compute aggregate statistics (message counts, token usage, cost) for a session. */\nexport function computeSessionStats(params: {\n\tmessages: AgentMessage[];\n\tsessionFile: string | undefined;\n\tsessionId: string;\n\tcontextUsage: ContextUsage | undefined;\n}): SessionStats {\n\tconst { messages, sessionFile, sessionId, contextUsage } = params;\n\tconst userMessages = messages.filter((m) => m.role === \"user\").length;\n\tconst assistantMessages = messages.filter((m) => m.role === \"assistant\").length;\n\tconst toolResults = messages.filter((m) => m.role === \"toolResult\").length;\n\n\tlet toolCalls = 0;\n\tlet totalInput = 0;\n\tlet totalOutput = 0;\n\tlet totalCacheRead = 0;\n\tlet totalCacheWrite = 0;\n\tlet totalCost = 0;\n\n\tfor (const message of messages) {\n\t\tif (message.role === \"assistant\") {\n\t\t\tconst assistantMsg = message as AssistantMessage;\n\t\t\ttoolCalls += assistantMsg.content.filter((c) => c.type === \"toolCall\").length;\n\t\t\ttotalInput += assistantMsg.usage.input;\n\t\t\ttotalOutput += assistantMsg.usage.output;\n\t\t\ttotalCacheRead += assistantMsg.usage.cacheRead;\n\t\t\ttotalCacheWrite += assistantMsg.usage.cacheWrite;\n\t\t\ttotalCost += assistantMsg.usage.cost.total;\n\t\t}\n\t}\n\n\treturn {\n\t\tsessionFile,\n\t\tsessionId,\n\t\tuserMessages,\n\t\tassistantMessages,\n\t\ttoolCalls,\n\t\ttoolResults,\n\t\ttotalMessages: messages.length,\n\t\ttokens: {\n\t\t\tinput: totalInput,\n\t\t\toutput: totalOutput,\n\t\t\tcacheRead: totalCacheRead,\n\t\t\tcacheWrite: totalCacheWrite,\n\t\t\ttotal: totalInput + totalOutput + totalCacheRead + totalCacheWrite,\n\t\t},\n\t\tcost: totalCost,\n\t\tcontextUsage,\n\t};\n}\n\n/**\n * Estimate current context-window usage.\n *\n * After compaction, the last assistant usage reflects pre-compaction context\n * size, so usage is only trusted from an assistant that responded after the\n * latest compaction boundary. When no such assistant exists yet, tokens are\n * reported as null (unknown until the next LLM response).\n */\nexport function computeContextUsage(params: {\n\tmodel: Model<any> | undefined;\n\tsessionManager: SessionManager;\n\tmessages: AgentMessage[];\n}): ContextUsage | undefined {\n\tconst { model, sessionManager, messages } = params;\n\tif (!model) return undefined;\n\n\tconst contextWindow = model.contextWindow ?? 0;\n\tif (contextWindow <= 0) return undefined;\n\n\tconst branchEntries = sessionManager.getBranch();\n\tconst latestCompaction = getLatestCompactionEntry(branchEntries);\n\n\tif (latestCompaction) {\n\t\t// Check if there's a valid assistant usage after the compaction boundary\n\t\tconst compactionIndex = branchEntries.lastIndexOf(latestCompaction);\n\t\tlet hasPostCompactionUsage = false;\n\t\tfor (let i = branchEntries.length - 1; i > compactionIndex; i--) {\n\t\t\tconst entry = branchEntries[i];\n\t\t\tif (entry.type === \"message\" && entry.message.role === \"assistant\") {\n\t\t\t\tconst assistant = entry.message;\n\t\t\t\tif (assistant.stopReason !== \"aborted\" && assistant.stopReason !== \"error\") {\n\t\t\t\t\tconst contextTokens = calculateContextTokens(assistant.usage);\n\t\t\t\t\tif (contextTokens > 0) {\n\t\t\t\t\t\thasPostCompactionUsage = true;\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (!hasPostCompactionUsage) {\n\t\t\treturn { tokens: null, contextWindow, percent: null };\n\t\t}\n\t}\n\n\tconst estimate = estimateContextTokens(messages);\n\tconst percent = (estimate.tokens / contextWindow) * 100;\n\n\treturn {\n\t\ttokens: estimate.tokens,\n\t\tcontextWindow,\n\t\tpercent,\n\t};\n}\n\n/** Collect all user messages on the session (for the fork selector). */\nexport function collectUserMessagesForForking(\n\tsessionManager: SessionManager,\n): Array<{ entryId: string; text: string }> {\n\tconst entries = sessionManager.getEntries();\n\tconst result: Array<{ entryId: string; text: string }> = [];\n\n\tfor (const entry of entries) {\n\t\tif (entry.type !== \"message\") continue;\n\t\tif (entry.message.role !== \"user\") continue;\n\n\t\tconst text = extractUserMessageText(entry.message.content);\n\t\tif (text) {\n\t\t\tresult.push({ entryId: entry.id, text });\n\t\t}\n\t}\n\n\treturn result;\n}\n\n/**\n * Get the text content of the last non-empty assistant message (for /copy).\n * Returns undefined if no assistant message with text exists.\n */\nexport function getLastAssistantText(messages: AgentMessage[]): string | undefined {\n\tconst lastAssistant = messages\n\t\t.slice()\n\t\t.reverse()\n\t\t.find((m) => {\n\t\t\tif (m.role !== \"assistant\") return false;\n\t\t\tconst msg = m as AssistantMessage;\n\t\t\t// Skip aborted messages with no content\n\t\t\tif (msg.stopReason === \"aborted\" && msg.content.length === 0) return false;\n\t\t\treturn true;\n\t\t});\n\n\tif (!lastAssistant) return undefined;\n\n\tlet text = \"\";\n\tfor (const content of (lastAssistant as AssistantMessage).content) {\n\t\tif (content.type === \"text\") {\n\t\t\ttext += content.text;\n\t\t}\n\t}\n\n\treturn text.trim() || undefined;\n}\n\n/**\n * Export the current session branch to a JSONL file.\n * Writes the session header followed by all entries on the current branch path,\n * re-chaining parentIds into a linear sequence.\n * @returns The resolved output file path.\n */\nexport function exportSessionBranchToJsonl(sessionManager: SessionManager, outputPath?: string): string {\n\tconst filePath = resolve(outputPath ?? `session-${new Date().toISOString().replace(/[:.]/g, \"-\")}.jsonl`);\n\tconst dir = dirname(filePath);\n\tif (!existsSync(dir)) {\n\t\tmkdirSync(dir, { recursive: true });\n\t}\n\n\tconst header: SessionHeader = {\n\t\ttype: \"session\",\n\t\tversion: CURRENT_SESSION_VERSION,\n\t\tid: sessionManager.getSessionId(),\n\t\ttimestamp: new Date().toISOString(),\n\t\tcwd: sessionManager.getCwd(),\n\t};\n\n\tconst branchEntries = sessionManager.getBranch();\n\tconst lines = [JSON.stringify(header)];\n\n\t// Re-chain parentIds to form a linear sequence\n\tlet prevId: string | null = null;\n\tfor (const entry of branchEntries) {\n\t\tconst linear = { ...entry, parentId: prevId };\n\t\tlines.push(JSON.stringify(linear));\n\t\tprevId = entry.id;\n\t}\n\n\twriteFileSync(filePath, `${lines.join(\"\\n\")}\\n`);\n\treturn filePath;\n}\n"]}
|
|
@@ -6,6 +6,19 @@
|
|
|
6
6
|
* into the system prompt every turn), and resolves a system-prompt input that
|
|
7
7
|
* may be either an inline string or a file path. Extracted from resource-loader.ts.
|
|
8
8
|
*/
|
|
9
|
+
/**
|
|
10
|
+
* A loaded context file plus the recurring-cost metadata the UI surfaces.
|
|
11
|
+
* `tokens`/`size` are optional so callers that synthesize context files (SDK
|
|
12
|
+
* overrides, tests) can keep passing plain `{ path, content }`.
|
|
13
|
+
*/
|
|
14
|
+
export interface ContextFile {
|
|
15
|
+
path: string;
|
|
16
|
+
content: string;
|
|
17
|
+
/** Rough token estimate (bytes / 4) of the content as loaded. */
|
|
18
|
+
tokens?: number;
|
|
19
|
+
/** Set only past the soft limit; "truncated" means the hard limit clipped it. */
|
|
20
|
+
size?: "large" | "truncated";
|
|
21
|
+
}
|
|
9
22
|
/**
|
|
10
23
|
* Resolve a prompt input that is either an inline string or a path to a file.
|
|
11
24
|
* If the input names an existing file, its contents are returned; otherwise the
|
|
@@ -16,10 +29,7 @@ export declare function loadProjectContextFiles(options: {
|
|
|
16
29
|
cwd: string;
|
|
17
30
|
agentDir: string;
|
|
18
31
|
}): {
|
|
19
|
-
agentsFiles:
|
|
20
|
-
path: string;
|
|
21
|
-
content: string;
|
|
22
|
-
}>;
|
|
32
|
+
agentsFiles: ContextFile[];
|
|
23
33
|
warnings: string[];
|
|
24
34
|
};
|
|
25
35
|
//# sourceMappingURL=context-files.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context-files.d.ts","sourceRoot":"","sources":["../../src/core/context-files.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAMH;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAerG;
|
|
1
|
+
{"version":3,"file":"context-files.d.ts","sourceRoot":"","sources":["../../src/core/context-files.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAMH;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,iEAAiE;IACjE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iFAAiF;IACjF,IAAI,CAAC,EAAE,OAAO,GAAG,WAAW,CAAC;CAC7B;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAerG;AAsCD,wBAAgB,uBAAuB,CAAC,OAAO,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG;IACpF,WAAW,EAAE,WAAW,EAAE,CAAC;IAC3B,QAAQ,EAAE,MAAM,EAAE,CAAC;CACnB,CAsCA","sourcesContent":["/**\n * Context-file and prompt-input loading for the resource loader.\n *\n * Reads AGENTS.md / CLAUDE.md context files from the agent dir and the cwd\n * ancestor chain (warning/truncating oversized ones, since they are injected\n * into the system prompt every turn), and resolves a system-prompt input that\n * may be either an inline string or a file path. Extracted from resource-loader.ts.\n */\n\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { join, resolve } from \"node:path\";\nimport chalk from \"chalk\";\n\n/**\n * A loaded context file plus the recurring-cost metadata the UI surfaces.\n * `tokens`/`size` are optional so callers that synthesize context files (SDK\n * overrides, tests) can keep passing plain `{ path, content }`.\n */\nexport interface ContextFile {\n\tpath: string;\n\tcontent: string;\n\t/** Rough token estimate (bytes / 4) of the content as loaded. */\n\ttokens?: number;\n\t/** Set only past the soft limit; \"truncated\" means the hard limit clipped it. */\n\tsize?: \"large\" | \"truncated\";\n}\n\n/**\n * Resolve a prompt input that is either an inline string or a path to a file.\n * If the input names an existing file, its contents are returned; otherwise the\n * input is treated as the prompt text itself.\n */\nexport function resolvePromptInput(input: string | undefined, description: string): string | undefined {\n\tif (!input) {\n\t\treturn undefined;\n\t}\n\n\tif (existsSync(input)) {\n\t\ttry {\n\t\t\treturn readFileSync(input, \"utf-8\");\n\t\t} catch (error) {\n\t\t\tconsole.error(chalk.yellow(`Warning: Could not read ${description} file ${input}: ${error}`));\n\t\t\treturn input;\n\t\t}\n\t}\n\n\treturn input;\n}\n\n// Context files (AGENTS.md / CLAUDE.md) are injected into the system prompt on\n// every turn, so their size has a recurring cost on every provider. Warn the\n// user past a soft limit (~2k tokens) and truncate at a hard limit (~10k tokens)\n// so a pasted spec can't silently bloat every request forever.\nconst CONTEXT_FILE_WARN_BYTES = 8 * 1024;\nconst CONTEXT_FILE_MAX_BYTES = 40 * 1024;\n\nfunction loadContextFileFromDir(dir: string): { file: ContextFile | null; warnings: string[] } {\n\tconst warnings: string[] = [];\n\tconst candidates = [\"AGENTS.md\", \"AGENTS.MD\", \"CLAUDE.md\", \"CLAUDE.MD\"];\n\tfor (const filename of candidates) {\n\t\tconst filePath = join(dir, filename);\n\t\tif (existsSync(filePath)) {\n\t\t\ttry {\n\t\t\t\tlet content = readFileSync(filePath, \"utf-8\");\n\t\t\t\tconst bytes = Buffer.byteLength(content, \"utf-8\");\n\t\t\t\t// Size is reported structurally (not as a warning string) so the UI can\n\t\t\t\t// annotate the file where it is already listed instead of repeating it.\n\t\t\t\tlet size: ContextFile[\"size\"];\n\t\t\t\tif (bytes > CONTEXT_FILE_MAX_BYTES) {\n\t\t\t\t\tcontent =\n\t\t\t\t\t\tcontent.slice(0, CONTEXT_FILE_MAX_BYTES) +\n\t\t\t\t\t\t`\\n\\n[truncated: file exceeded ${CONTEXT_FILE_MAX_BYTES} bytes (~10k tokens); keep context files brief — large specs belong in linked files, not in the system prompt]`;\n\t\t\t\t\tsize = \"truncated\";\n\t\t\t\t} else if (bytes > CONTEXT_FILE_WARN_BYTES) {\n\t\t\t\t\tsize = \"large\";\n\t\t\t\t}\n\t\t\t\treturn { file: { path: filePath, content, tokens: Math.round(bytes / 4), size }, warnings };\n\t\t\t} catch (error) {\n\t\t\t\twarnings.push(`Could not read ${filePath}: ${error}`);\n\t\t\t}\n\t\t}\n\t}\n\treturn { file: null, warnings };\n}\n\nexport function loadProjectContextFiles(options: { cwd: string; agentDir: string }): {\n\tagentsFiles: ContextFile[];\n\twarnings: string[];\n} {\n\tconst resolvedCwd = options.cwd;\n\tconst resolvedAgentDir = options.agentDir;\n\n\tconst contextFiles: ContextFile[] = [];\n\tconst warnings: string[] = [];\n\tconst seenPaths = new Set<string>();\n\n\tconst globalResult = loadContextFileFromDir(resolvedAgentDir);\n\tif (globalResult.file) {\n\t\tcontextFiles.push(globalResult.file);\n\t\tseenPaths.add(globalResult.file.path);\n\t}\n\twarnings.push(...globalResult.warnings);\n\n\tconst ancestorContextFiles: ContextFile[] = [];\n\n\tlet currentDir = resolvedCwd;\n\tconst root = resolve(\"/\");\n\n\twhile (true) {\n\t\tconst result = loadContextFileFromDir(currentDir);\n\t\tif (result.file && !seenPaths.has(result.file.path)) {\n\t\t\tancestorContextFiles.unshift(result.file);\n\t\t\tseenPaths.add(result.file.path);\n\t\t}\n\t\twarnings.push(...result.warnings);\n\n\t\tif (currentDir === root) break;\n\n\t\tconst parentDir = resolve(currentDir, \"..\");\n\t\tif (parentDir === currentDir) break;\n\t\tcurrentDir = parentDir;\n\t}\n\n\tcontextFiles.push(...ancestorContextFiles);\n\n\treturn { agentsFiles: contextFiles, warnings };\n}\n"]}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* may be either an inline string or a file path. Extracted from resource-loader.ts.
|
|
8
8
|
*/
|
|
9
9
|
import { existsSync, readFileSync } from "node:fs";
|
|
10
|
-
import {
|
|
10
|
+
import { join, resolve } from "node:path";
|
|
11
11
|
import chalk from "chalk";
|
|
12
12
|
/**
|
|
13
13
|
* Resolve a prompt input that is either an inline string or a path to a file.
|
|
@@ -44,16 +44,19 @@ function loadContextFileFromDir(dir) {
|
|
|
44
44
|
try {
|
|
45
45
|
let content = readFileSync(filePath, "utf-8");
|
|
46
46
|
const bytes = Buffer.byteLength(content, "utf-8");
|
|
47
|
+
// Size is reported structurally (not as a warning string) so the UI can
|
|
48
|
+
// annotate the file where it is already listed instead of repeating it.
|
|
49
|
+
let size;
|
|
47
50
|
if (bytes > CONTEXT_FILE_MAX_BYTES) {
|
|
48
51
|
content =
|
|
49
52
|
content.slice(0, CONTEXT_FILE_MAX_BYTES) +
|
|
50
53
|
`\n\n[truncated: file exceeded ${CONTEXT_FILE_MAX_BYTES} bytes (~10k tokens); keep context files brief — large specs belong in linked files, not in the system prompt]`;
|
|
51
|
-
|
|
54
|
+
size = "truncated";
|
|
52
55
|
}
|
|
53
56
|
else if (bytes > CONTEXT_FILE_WARN_BYTES) {
|
|
54
|
-
|
|
57
|
+
size = "large";
|
|
55
58
|
}
|
|
56
|
-
return { file: { path: filePath, content }, warnings };
|
|
59
|
+
return { file: { path: filePath, content, tokens: Math.round(bytes / 4), size }, warnings };
|
|
57
60
|
}
|
|
58
61
|
catch (error) {
|
|
59
62
|
warnings.push(`Could not read ${filePath}: ${error}`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context-files.js","sourceRoot":"","sources":["../../src/core/context-files.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"context-files.js","sourceRoot":"","sources":["../../src/core/context-files.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,KAAK,MAAM,OAAO,CAAC;AAgB1B;;;;GAIG;AACH,MAAM,UAAU,kBAAkB,CAAC,KAAyB,EAAE,WAAmB,EAAsB;IACtG,IAAI,CAAC,KAAK,EAAE,CAAC;QACZ,OAAO,SAAS,CAAC;IAClB,CAAC;IAED,IAAI,UAAU,CAAC,KAAK,CAAC,EAAE,CAAC;QACvB,IAAI,CAAC;YACJ,OAAO,YAAY,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;QACrC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,2BAA2B,WAAW,SAAS,KAAK,KAAK,KAAK,EAAE,CAAC,CAAC,CAAC;YAC9F,OAAO,KAAK,CAAC;QACd,CAAC;IACF,CAAC;IAED,OAAO,KAAK,CAAC;AAAA,CACb;AAED,+EAA+E;AAC/E,6EAA6E;AAC7E,iFAAiF;AACjF,+DAA+D;AAC/D,MAAM,uBAAuB,GAAG,CAAC,GAAG,IAAI,CAAC;AACzC,MAAM,sBAAsB,GAAG,EAAE,GAAG,IAAI,CAAC;AAEzC,SAAS,sBAAsB,CAAC,GAAW,EAAoD;IAC9F,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,MAAM,UAAU,GAAG,CAAC,WAAW,EAAE,WAAW,EAAE,WAAW,EAAE,WAAW,CAAC,CAAC;IACxE,KAAK,MAAM,QAAQ,IAAI,UAAU,EAAE,CAAC;QACnC,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;QACrC,IAAI,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC1B,IAAI,CAAC;gBACJ,IAAI,OAAO,GAAG,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;gBAC9C,MAAM,KAAK,GAAG,MAAM,CAAC,UAAU,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;gBAClD,wEAAwE;gBACxE,wEAAwE;gBACxE,IAAI,IAAyB,CAAC;gBAC9B,IAAI,KAAK,GAAG,sBAAsB,EAAE,CAAC;oBACpC,OAAO;wBACN,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,sBAAsB,CAAC;4BACxC,iCAAiC,sBAAsB,kHAAgH,CAAC;oBACzK,IAAI,GAAG,WAAW,CAAC;gBACpB,CAAC;qBAAM,IAAI,KAAK,GAAG,uBAAuB,EAAE,CAAC;oBAC5C,IAAI,GAAG,OAAO,CAAC;gBAChB,CAAC;gBACD,OAAO,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE,QAAQ,EAAE,CAAC;YAC7F,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBAChB,QAAQ,CAAC,IAAI,CAAC,kBAAkB,QAAQ,KAAK,KAAK,EAAE,CAAC,CAAC;YACvD,CAAC;QACF,CAAC;IACF,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;AAAA,CAChC;AAED,MAAM,UAAU,uBAAuB,CAAC,OAA0C,EAGhF;IACD,MAAM,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC;IAChC,MAAM,gBAAgB,GAAG,OAAO,CAAC,QAAQ,CAAC;IAE1C,MAAM,YAAY,GAAkB,EAAE,CAAC;IACvC,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,MAAM,SAAS,GAAG,IAAI,GAAG,EAAU,CAAC;IAEpC,MAAM,YAAY,GAAG,sBAAsB,CAAC,gBAAgB,CAAC,CAAC;IAC9D,IAAI,YAAY,CAAC,IAAI,EAAE,CAAC;QACvB,YAAY,CAAC,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;QACrC,SAAS,CAAC,GAAG,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACvC,CAAC;IACD,QAAQ,CAAC,IAAI,CAAC,GAAG,YAAY,CAAC,QAAQ,CAAC,CAAC;IAExC,MAAM,oBAAoB,GAAkB,EAAE,CAAC;IAE/C,IAAI,UAAU,GAAG,WAAW,CAAC;IAC7B,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;IAE1B,OAAO,IAAI,EAAE,CAAC;QACb,MAAM,MAAM,GAAG,sBAAsB,CAAC,UAAU,CAAC,CAAC;QAClD,IAAI,MAAM,CAAC,IAAI,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YACrD,oBAAoB,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;YAC1C,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACjC,CAAC;QACD,QAAQ,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC;QAElC,IAAI,UAAU,KAAK,IAAI;YAAE,MAAM;QAE/B,MAAM,SAAS,GAAG,OAAO,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;QAC5C,IAAI,SAAS,KAAK,UAAU;YAAE,MAAM;QACpC,UAAU,GAAG,SAAS,CAAC;IACxB,CAAC;IAED,YAAY,CAAC,IAAI,CAAC,GAAG,oBAAoB,CAAC,CAAC;IAE3C,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,QAAQ,EAAE,CAAC;AAAA,CAC/C","sourcesContent":["/**\n * Context-file and prompt-input loading for the resource loader.\n *\n * Reads AGENTS.md / CLAUDE.md context files from the agent dir and the cwd\n * ancestor chain (warning/truncating oversized ones, since they are injected\n * into the system prompt every turn), and resolves a system-prompt input that\n * may be either an inline string or a file path. Extracted from resource-loader.ts.\n */\n\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { join, resolve } from \"node:path\";\nimport chalk from \"chalk\";\n\n/**\n * A loaded context file plus the recurring-cost metadata the UI surfaces.\n * `tokens`/`size` are optional so callers that synthesize context files (SDK\n * overrides, tests) can keep passing plain `{ path, content }`.\n */\nexport interface ContextFile {\n\tpath: string;\n\tcontent: string;\n\t/** Rough token estimate (bytes / 4) of the content as loaded. */\n\ttokens?: number;\n\t/** Set only past the soft limit; \"truncated\" means the hard limit clipped it. */\n\tsize?: \"large\" | \"truncated\";\n}\n\n/**\n * Resolve a prompt input that is either an inline string or a path to a file.\n * If the input names an existing file, its contents are returned; otherwise the\n * input is treated as the prompt text itself.\n */\nexport function resolvePromptInput(input: string | undefined, description: string): string | undefined {\n\tif (!input) {\n\t\treturn undefined;\n\t}\n\n\tif (existsSync(input)) {\n\t\ttry {\n\t\t\treturn readFileSync(input, \"utf-8\");\n\t\t} catch (error) {\n\t\t\tconsole.error(chalk.yellow(`Warning: Could not read ${description} file ${input}: ${error}`));\n\t\t\treturn input;\n\t\t}\n\t}\n\n\treturn input;\n}\n\n// Context files (AGENTS.md / CLAUDE.md) are injected into the system prompt on\n// every turn, so their size has a recurring cost on every provider. Warn the\n// user past a soft limit (~2k tokens) and truncate at a hard limit (~10k tokens)\n// so a pasted spec can't silently bloat every request forever.\nconst CONTEXT_FILE_WARN_BYTES = 8 * 1024;\nconst CONTEXT_FILE_MAX_BYTES = 40 * 1024;\n\nfunction loadContextFileFromDir(dir: string): { file: ContextFile | null; warnings: string[] } {\n\tconst warnings: string[] = [];\n\tconst candidates = [\"AGENTS.md\", \"AGENTS.MD\", \"CLAUDE.md\", \"CLAUDE.MD\"];\n\tfor (const filename of candidates) {\n\t\tconst filePath = join(dir, filename);\n\t\tif (existsSync(filePath)) {\n\t\t\ttry {\n\t\t\t\tlet content = readFileSync(filePath, \"utf-8\");\n\t\t\t\tconst bytes = Buffer.byteLength(content, \"utf-8\");\n\t\t\t\t// Size is reported structurally (not as a warning string) so the UI can\n\t\t\t\t// annotate the file where it is already listed instead of repeating it.\n\t\t\t\tlet size: ContextFile[\"size\"];\n\t\t\t\tif (bytes > CONTEXT_FILE_MAX_BYTES) {\n\t\t\t\t\tcontent =\n\t\t\t\t\t\tcontent.slice(0, CONTEXT_FILE_MAX_BYTES) +\n\t\t\t\t\t\t`\\n\\n[truncated: file exceeded ${CONTEXT_FILE_MAX_BYTES} bytes (~10k tokens); keep context files brief — large specs belong in linked files, not in the system prompt]`;\n\t\t\t\t\tsize = \"truncated\";\n\t\t\t\t} else if (bytes > CONTEXT_FILE_WARN_BYTES) {\n\t\t\t\t\tsize = \"large\";\n\t\t\t\t}\n\t\t\t\treturn { file: { path: filePath, content, tokens: Math.round(bytes / 4), size }, warnings };\n\t\t\t} catch (error) {\n\t\t\t\twarnings.push(`Could not read ${filePath}: ${error}`);\n\t\t\t}\n\t\t}\n\t}\n\treturn { file: null, warnings };\n}\n\nexport function loadProjectContextFiles(options: { cwd: string; agentDir: string }): {\n\tagentsFiles: ContextFile[];\n\twarnings: string[];\n} {\n\tconst resolvedCwd = options.cwd;\n\tconst resolvedAgentDir = options.agentDir;\n\n\tconst contextFiles: ContextFile[] = [];\n\tconst warnings: string[] = [];\n\tconst seenPaths = new Set<string>();\n\n\tconst globalResult = loadContextFileFromDir(resolvedAgentDir);\n\tif (globalResult.file) {\n\t\tcontextFiles.push(globalResult.file);\n\t\tseenPaths.add(globalResult.file.path);\n\t}\n\twarnings.push(...globalResult.warnings);\n\n\tconst ancestorContextFiles: ContextFile[] = [];\n\n\tlet currentDir = resolvedCwd;\n\tconst root = resolve(\"/\");\n\n\twhile (true) {\n\t\tconst result = loadContextFileFromDir(currentDir);\n\t\tif (result.file && !seenPaths.has(result.file.path)) {\n\t\t\tancestorContextFiles.unshift(result.file);\n\t\t\tseenPaths.add(result.file.path);\n\t\t}\n\t\twarnings.push(...result.warnings);\n\n\t\tif (currentDir === root) break;\n\n\t\tconst parentDir = resolve(currentDir, \"..\");\n\t\tif (parentDir === currentDir) break;\n\t\tcurrentDir = parentDir;\n\t}\n\n\tcontextFiles.push(...ancestorContextFiles);\n\n\treturn { agentsFiles: contextFiles, warnings };\n}\n"]}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compact token/count formatting shared by the surfaces that report numbers
|
|
3
|
+
* (footer, task panel, startup resource summary).
|
|
4
|
+
*
|
|
5
|
+
* Numbers stay exact where they fit and degrade to one decimal of `k`/`M` so a
|
|
6
|
+
* count never grows the fixed-width chrome it sits in.
|
|
7
|
+
*/
|
|
8
|
+
export declare function formatTokens(count: number): string;
|
|
9
|
+
//# sourceMappingURL=format-tokens.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"format-tokens.d.ts","sourceRoot":"","sources":["../../src/core/format-tokens.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAMlD","sourcesContent":["/**\n * Compact token/count formatting shared by the surfaces that report numbers\n * (footer, task panel, startup resource summary).\n *\n * Numbers stay exact where they fit and degrade to one decimal of `k`/`M` so a\n * count never grows the fixed-width chrome it sits in.\n */\nexport function formatTokens(count: number): string {\n\tif (count < 1000) return count.toString();\n\tif (count < 10000) return `${(count / 1000).toFixed(1)}k`;\n\tif (count < 1000000) return `${Math.round(count / 1000)}k`;\n\tif (count < 10000000) return `${(count / 1000000).toFixed(1)}M`;\n\treturn `${Math.round(count / 1000000)}M`;\n}\n"]}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compact token/count formatting shared by the surfaces that report numbers
|
|
3
|
+
* (footer, task panel, startup resource summary).
|
|
4
|
+
*
|
|
5
|
+
* Numbers stay exact where they fit and degrade to one decimal of `k`/`M` so a
|
|
6
|
+
* count never grows the fixed-width chrome it sits in.
|
|
7
|
+
*/
|
|
8
|
+
export function formatTokens(count) {
|
|
9
|
+
if (count < 1000)
|
|
10
|
+
return count.toString();
|
|
11
|
+
if (count < 10000)
|
|
12
|
+
return `${(count / 1000).toFixed(1)}k`;
|
|
13
|
+
if (count < 1000000)
|
|
14
|
+
return `${Math.round(count / 1000)}k`;
|
|
15
|
+
if (count < 10000000)
|
|
16
|
+
return `${(count / 1000000).toFixed(1)}M`;
|
|
17
|
+
return `${Math.round(count / 1000000)}M`;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=format-tokens.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"format-tokens.js","sourceRoot":"","sources":["../../src/core/format-tokens.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,UAAU,YAAY,CAAC,KAAa,EAAU;IACnD,IAAI,KAAK,GAAG,IAAI;QAAE,OAAO,KAAK,CAAC,QAAQ,EAAE,CAAC;IAC1C,IAAI,KAAK,GAAG,KAAK;QAAE,OAAO,GAAG,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC;IAC1D,IAAI,KAAK,GAAG,OAAO;QAAE,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC;IAC3D,IAAI,KAAK,GAAG,QAAQ;QAAE,OAAO,GAAG,CAAC,KAAK,GAAG,OAAO,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC;IAChE,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC;AAAA,CACzC","sourcesContent":["/**\n * Compact token/count formatting shared by the surfaces that report numbers\n * (footer, task panel, startup resource summary).\n *\n * Numbers stay exact where they fit and degrade to one decimal of `k`/`M` so a\n * count never grows the fixed-width chrome it sits in.\n */\nexport function formatTokens(count: number): string {\n\tif (count < 1000) return count.toString();\n\tif (count < 10000) return `${(count / 1000).toFixed(1)}k`;\n\tif (count < 1000000) return `${Math.round(count / 1000)}k`;\n\tif (count < 10000000) return `${(count / 1000000).toFixed(1)}M`;\n\treturn `${Math.round(count / 1000000)}M`;\n}\n"]}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process-global registry of MCP servers that actually connected this session.
|
|
3
|
+
*
|
|
4
|
+
* MCP discovery and connection happen inside the hoo-core `mcp-loader` module on
|
|
5
|
+
* `session_start`, from several sources (standard `mcp.json` files, hoocode's
|
|
6
|
+
* per-server JSON files, plugin registrations). The startup resource summary
|
|
7
|
+
* needs a single answer to "which servers are live and how many tools do they
|
|
8
|
+
* bring", so the loader records each outcome here and the UI reads it.
|
|
9
|
+
*
|
|
10
|
+
* Mirrors the module-global approach of `extension-mcp-servers.ts`. The registry
|
|
11
|
+
* is cleared at the start of each connect pass so a session switch or reload
|
|
12
|
+
* rebuilds it instead of accumulating.
|
|
13
|
+
*/
|
|
14
|
+
export interface McpServerStatus {
|
|
15
|
+
/** Server name, as used in the `mcp_<server>_<tool>` prefix. */
|
|
16
|
+
name: string;
|
|
17
|
+
/** Tools the server exposed on connect. */
|
|
18
|
+
toolCount: number;
|
|
19
|
+
/** Tools default to background execution (task-panel rows) rather than inline. */
|
|
20
|
+
background: boolean;
|
|
21
|
+
/** Tool names are injected but schemas are materialized on demand. */
|
|
22
|
+
deferred: boolean;
|
|
23
|
+
/** `authorizing` means a browser OAuth flow is in flight; tools register when it completes. */
|
|
24
|
+
state: "connected" | "authorizing";
|
|
25
|
+
}
|
|
26
|
+
/** Record (or replace) the connection outcome for one server. */
|
|
27
|
+
export declare function setMcpServerStatus(status: McpServerStatus): void;
|
|
28
|
+
/** All servers that connected (or are awaiting authorization) this session. */
|
|
29
|
+
export declare function getMcpServerStatuses(): McpServerStatus[];
|
|
30
|
+
/** Clear the registry. Called before each connect pass so reloads rebuild it. */
|
|
31
|
+
export declare function clearMcpServerStatuses(): void;
|
|
32
|
+
//# sourceMappingURL=mcp-status.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp-status.d.ts","sourceRoot":"","sources":["../../src/core/mcp-status.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,MAAM,WAAW,eAAe;IAC/B,gEAAgE;IAChE,IAAI,EAAE,MAAM,CAAC;IACb,2CAA2C;IAC3C,SAAS,EAAE,MAAM,CAAC;IAClB,kFAAkF;IAClF,UAAU,EAAE,OAAO,CAAC;IACpB,sEAAsE;IACtE,QAAQ,EAAE,OAAO,CAAC;IAClB,+FAA+F;IAC/F,KAAK,EAAE,WAAW,GAAG,aAAa,CAAC;CACnC;AAID,iEAAiE;AACjE,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,eAAe,GAAG,IAAI,CAEhE;AAED,+EAA+E;AAC/E,wBAAgB,oBAAoB,IAAI,eAAe,EAAE,CAExD;AAED,iFAAiF;AACjF,wBAAgB,sBAAsB,IAAI,IAAI,CAE7C","sourcesContent":["/**\n * Process-global registry of MCP servers that actually connected this session.\n *\n * MCP discovery and connection happen inside the hoo-core `mcp-loader` module on\n * `session_start`, from several sources (standard `mcp.json` files, hoocode's\n * per-server JSON files, plugin registrations). The startup resource summary\n * needs a single answer to \"which servers are live and how many tools do they\n * bring\", so the loader records each outcome here and the UI reads it.\n *\n * Mirrors the module-global approach of `extension-mcp-servers.ts`. The registry\n * is cleared at the start of each connect pass so a session switch or reload\n * rebuilds it instead of accumulating.\n */\n\nexport interface McpServerStatus {\n\t/** Server name, as used in the `mcp_<server>_<tool>` prefix. */\n\tname: string;\n\t/** Tools the server exposed on connect. */\n\ttoolCount: number;\n\t/** Tools default to background execution (task-panel rows) rather than inline. */\n\tbackground: boolean;\n\t/** Tool names are injected but schemas are materialized on demand. */\n\tdeferred: boolean;\n\t/** `authorizing` means a browser OAuth flow is in flight; tools register when it completes. */\n\tstate: \"connected\" | \"authorizing\";\n}\n\nconst statuses = new Map<string, McpServerStatus>();\n\n/** Record (or replace) the connection outcome for one server. */\nexport function setMcpServerStatus(status: McpServerStatus): void {\n\tstatuses.set(status.name, status);\n}\n\n/** All servers that connected (or are awaiting authorization) this session. */\nexport function getMcpServerStatuses(): McpServerStatus[] {\n\treturn [...statuses.values()];\n}\n\n/** Clear the registry. Called before each connect pass so reloads rebuild it. */\nexport function clearMcpServerStatuses(): void {\n\tstatuses.clear();\n}\n"]}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process-global registry of MCP servers that actually connected this session.
|
|
3
|
+
*
|
|
4
|
+
* MCP discovery and connection happen inside the hoo-core `mcp-loader` module on
|
|
5
|
+
* `session_start`, from several sources (standard `mcp.json` files, hoocode's
|
|
6
|
+
* per-server JSON files, plugin registrations). The startup resource summary
|
|
7
|
+
* needs a single answer to "which servers are live and how many tools do they
|
|
8
|
+
* bring", so the loader records each outcome here and the UI reads it.
|
|
9
|
+
*
|
|
10
|
+
* Mirrors the module-global approach of `extension-mcp-servers.ts`. The registry
|
|
11
|
+
* is cleared at the start of each connect pass so a session switch or reload
|
|
12
|
+
* rebuilds it instead of accumulating.
|
|
13
|
+
*/
|
|
14
|
+
const statuses = new Map();
|
|
15
|
+
/** Record (or replace) the connection outcome for one server. */
|
|
16
|
+
export function setMcpServerStatus(status) {
|
|
17
|
+
statuses.set(status.name, status);
|
|
18
|
+
}
|
|
19
|
+
/** All servers that connected (or are awaiting authorization) this session. */
|
|
20
|
+
export function getMcpServerStatuses() {
|
|
21
|
+
return [...statuses.values()];
|
|
22
|
+
}
|
|
23
|
+
/** Clear the registry. Called before each connect pass so reloads rebuild it. */
|
|
24
|
+
export function clearMcpServerStatuses() {
|
|
25
|
+
statuses.clear();
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=mcp-status.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp-status.js","sourceRoot":"","sources":["../../src/core/mcp-status.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAeH,MAAM,QAAQ,GAAG,IAAI,GAAG,EAA2B,CAAC;AAEpD,iEAAiE;AACjE,MAAM,UAAU,kBAAkB,CAAC,MAAuB,EAAQ;IACjE,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;AAAA,CAClC;AAED,+EAA+E;AAC/E,MAAM,UAAU,oBAAoB,GAAsB;IACzD,OAAO,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;AAAA,CAC9B;AAED,iFAAiF;AACjF,MAAM,UAAU,sBAAsB,GAAS;IAC9C,QAAQ,CAAC,KAAK,EAAE,CAAC;AAAA,CACjB","sourcesContent":["/**\n * Process-global registry of MCP servers that actually connected this session.\n *\n * MCP discovery and connection happen inside the hoo-core `mcp-loader` module on\n * `session_start`, from several sources (standard `mcp.json` files, hoocode's\n * per-server JSON files, plugin registrations). The startup resource summary\n * needs a single answer to \"which servers are live and how many tools do they\n * bring\", so the loader records each outcome here and the UI reads it.\n *\n * Mirrors the module-global approach of `extension-mcp-servers.ts`. The registry\n * is cleared at the start of each connect pass so a session switch or reload\n * rebuilds it instead of accumulating.\n */\n\nexport interface McpServerStatus {\n\t/** Server name, as used in the `mcp_<server>_<tool>` prefix. */\n\tname: string;\n\t/** Tools the server exposed on connect. */\n\ttoolCount: number;\n\t/** Tools default to background execution (task-panel rows) rather than inline. */\n\tbackground: boolean;\n\t/** Tool names are injected but schemas are materialized on demand. */\n\tdeferred: boolean;\n\t/** `authorizing` means a browser OAuth flow is in flight; tools register when it completes. */\n\tstate: \"connected\" | \"authorizing\";\n}\n\nconst statuses = new Map<string, McpServerStatus>();\n\n/** Record (or replace) the connection outcome for one server. */\nexport function setMcpServerStatus(status: McpServerStatus): void {\n\tstatuses.set(status.name, status);\n}\n\n/** All servers that connected (or are awaiting authorization) this session. */\nexport function getMcpServerStatuses(): McpServerStatus[] {\n\treturn [...statuses.values()];\n}\n\n/** Clear the registry. Called before each connect pass so reloads rebuild it. */\nexport function clearMcpServerStatuses(): void {\n\tstatuses.clear();\n}\n"]}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { type Theme } from "../modes/interactive/theme/theme.js";
|
|
2
2
|
import type { ResourceDiagnostic } from "./diagnostics.js";
|
|
3
|
+
export type { ContextFile } from "./context-files.js";
|
|
3
4
|
export { loadProjectContextFiles } from "./context-files.js";
|
|
4
5
|
export type { ResourceCollision, ResourceDiagnostic } from "./diagnostics.js";
|
|
6
|
+
import { type ContextFile } from "./context-files.js";
|
|
5
7
|
import { type EventBus } from "./event-bus.js";
|
|
6
8
|
import type { ExtensionFactory, LoadExtensionsResult } from "./extensions/types.js";
|
|
7
9
|
import { type PathMetadata } from "./package-manager.js";
|
|
@@ -56,10 +58,7 @@ export interface ResourceLoader {
|
|
|
56
58
|
diagnostics: ResourceDiagnostic[];
|
|
57
59
|
};
|
|
58
60
|
getAgentsFiles(): {
|
|
59
|
-
agentsFiles:
|
|
60
|
-
path: string;
|
|
61
|
-
content: string;
|
|
62
|
-
}>;
|
|
61
|
+
agentsFiles: ContextFile[];
|
|
63
62
|
warnings: string[];
|
|
64
63
|
};
|
|
65
64
|
getSystemPrompt(): string | undefined;
|
|
@@ -110,16 +109,10 @@ export interface DefaultResourceLoaderOptions {
|
|
|
110
109
|
diagnostics: ResourceDiagnostic[];
|
|
111
110
|
};
|
|
112
111
|
agentsFilesOverride?: (base: {
|
|
113
|
-
agentsFiles:
|
|
114
|
-
path: string;
|
|
115
|
-
content: string;
|
|
116
|
-
}>;
|
|
112
|
+
agentsFiles: ContextFile[];
|
|
117
113
|
warnings: string[];
|
|
118
114
|
}) => {
|
|
119
|
-
agentsFiles:
|
|
120
|
-
path: string;
|
|
121
|
-
content: string;
|
|
122
|
-
}>;
|
|
115
|
+
agentsFiles: ContextFile[];
|
|
123
116
|
warnings?: string[];
|
|
124
117
|
};
|
|
125
118
|
systemPromptOverride?: (base: string | undefined) => string | undefined;
|
|
@@ -188,10 +181,7 @@ export declare class DefaultResourceLoader implements ResourceLoader {
|
|
|
188
181
|
diagnostics: ResourceDiagnostic[];
|
|
189
182
|
};
|
|
190
183
|
getAgentsFiles(): {
|
|
191
|
-
agentsFiles:
|
|
192
|
-
path: string;
|
|
193
|
-
content: string;
|
|
194
|
-
}>;
|
|
184
|
+
agentsFiles: ContextFile[];
|
|
195
185
|
warnings: string[];
|
|
196
186
|
};
|
|
197
187
|
getSystemPrompt(): string | undefined;
|