@kolisachint/hoocode-agent 0.4.113 → 0.4.115
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 +16 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +1 -1
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/extensions/plugins/authoring.d.ts +51 -0
- package/dist/core/extensions/plugins/authoring.d.ts.map +1 -0
- package/dist/core/extensions/plugins/authoring.js +91 -0
- package/dist/core/extensions/plugins/authoring.js.map +1 -0
- package/dist/core/extensions/plugins/default-marketplace/.agents-plugin/marketplace.json +12 -0
- package/dist/core/extensions/plugins/default-marketplace/plugins/hello-world/.agents-plugin/plugin.json +6 -0
- package/dist/core/extensions/plugins/default-marketplace/plugins/hello-world/commands/hello.md +5 -0
- package/dist/core/extensions/plugins/default-marketplace/plugins/hello-world/skills/greet/SKILL.md +7 -0
- package/dist/core/extensions/plugins/formats/agents.d.ts +7 -0
- package/dist/core/extensions/plugins/formats/agents.d.ts.map +1 -0
- package/dist/core/extensions/plugins/formats/agents.js +14 -0
- package/dist/core/extensions/plugins/formats/agents.js.map +1 -0
- package/dist/core/extensions/plugins/formats/claude.d.ts +10 -0
- package/dist/core/extensions/plugins/formats/claude.d.ts.map +1 -0
- package/dist/core/extensions/plugins/formats/claude.js +17 -0
- package/dist/core/extensions/plugins/formats/claude.js.map +1 -0
- package/dist/core/extensions/plugins/formats/copilot.d.ts +23 -0
- package/dist/core/extensions/plugins/formats/copilot.d.ts.map +1 -0
- package/dist/core/extensions/plugins/formats/copilot.js +130 -0
- package/dist/core/extensions/plugins/formats/copilot.js.map +1 -0
- package/dist/core/extensions/plugins/formats/index.d.ts +41 -0
- package/dist/core/extensions/plugins/formats/index.d.ts.map +1 -0
- package/dist/core/extensions/plugins/formats/index.js +76 -0
- package/dist/core/extensions/plugins/formats/index.js.map +1 -0
- package/dist/core/extensions/plugins/formats/jsonManifest.d.ts +21 -0
- package/dist/core/extensions/plugins/formats/jsonManifest.d.ts.map +1 -0
- package/dist/core/extensions/plugins/formats/jsonManifest.js +97 -0
- package/dist/core/extensions/plugins/formats/jsonManifest.js.map +1 -0
- package/dist/core/extensions/plugins/formats/shared.d.ts +44 -0
- package/dist/core/extensions/plugins/formats/shared.d.ts.map +1 -0
- package/dist/core/extensions/plugins/formats/shared.js +107 -0
- package/dist/core/extensions/plugins/formats/shared.js.map +1 -0
- package/dist/core/extensions/plugins/formats/types.d.ts +118 -0
- package/dist/core/extensions/plugins/formats/types.d.ts.map +1 -0
- package/dist/core/extensions/plugins/formats/types.js +19 -0
- package/dist/core/extensions/plugins/formats/types.js.map +1 -0
- package/dist/core/extensions/plugins/install.d.ts +73 -0
- package/dist/core/extensions/plugins/install.d.ts.map +1 -0
- package/dist/core/extensions/plugins/install.js +173 -0
- package/dist/core/extensions/plugins/install.js.map +1 -0
- package/dist/core/extensions/plugins/manifest.d.ts +25 -7
- package/dist/core/extensions/plugins/manifest.d.ts.map +1 -1
- package/dist/core/extensions/plugins/manifest.js +16 -91
- package/dist/core/extensions/plugins/manifest.js.map +1 -1
- package/dist/core/extensions/plugins/marketplace.d.ts +5 -4
- package/dist/core/extensions/plugins/marketplace.d.ts.map +1 -1
- package/dist/core/extensions/plugins/marketplace.js +8 -13
- package/dist/core/extensions/plugins/marketplace.js.map +1 -1
- package/dist/core/settings-defaults.d.ts +2 -0
- package/dist/core/settings-defaults.d.ts.map +1 -1
- package/dist/core/settings-defaults.js +2 -0
- package/dist/core/settings-defaults.js.map +1 -1
- package/dist/core/settings-manager.d.ts +4 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +16 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/settings-types.d.ts +2 -0
- package/dist/core/settings-types.d.ts.map +1 -1
- package/dist/core/settings-types.js.map +1 -1
- package/dist/core/subagent-depth.d.ts +13 -0
- package/dist/core/subagent-depth.d.ts.map +1 -1
- package/dist/core/subagent-depth.js +15 -0
- package/dist/core/subagent-depth.js.map +1 -1
- package/dist/core/subagent-pool.d.ts.map +1 -1
- package/dist/core/subagent-pool.js +4 -1
- package/dist/core/subagent-pool.js.map +1 -1
- package/dist/core/tools/find.d.ts +8 -3
- package/dist/core/tools/find.d.ts.map +1 -1
- package/dist/core/tools/find.js +189 -122
- package/dist/core/tools/find.js.map +1 -1
- package/dist/core/tools/index.d.ts +0 -4
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +2 -5
- package/dist/core/tools/index.js.map +1 -1
- package/dist/core/tools/plugin-tool-names.d.ts +21 -0
- package/dist/core/tools/plugin-tool-names.d.ts.map +1 -0
- package/dist/core/tools/plugin-tool-names.js +31 -0
- package/dist/core/tools/plugin-tool-names.js.map +1 -0
- package/dist/core/tools/plugins.d.ts +49 -0
- package/dist/core/tools/plugins.d.ts.map +1 -0
- package/dist/core/tools/plugins.js +186 -0
- package/dist/core/tools/plugins.js.map +1 -0
- package/dist/core/tools/propose-plugin.d.ts +36 -0
- package/dist/core/tools/propose-plugin.d.ts.map +1 -0
- package/dist/core/tools/propose-plugin.js +219 -0
- package/dist/core/tools/propose-plugin.js.map +1 -0
- package/dist/core/tools/subagent.d.ts.map +1 -1
- package/dist/core/tools/subagent.js +29 -3
- package/dist/core/tools/subagent.js.map +1 -1
- package/dist/core/warm-subagent-pool.d.ts.map +1 -1
- package/dist/core/warm-subagent-pool.js +4 -1
- package/dist/core/warm-subagent-pool.js.map +1 -1
- package/dist/extensions/core/marketplace.d.ts +7 -3
- package/dist/extensions/core/marketplace.d.ts.map +1 -1
- package/dist/extensions/core/marketplace.js +23 -67
- package/dist/extensions/core/marketplace.js.map +1 -1
- package/dist/extensions/core/mcp-deferred.d.ts +28 -0
- package/dist/extensions/core/mcp-deferred.d.ts.map +1 -0
- package/dist/extensions/core/mcp-deferred.js +53 -0
- package/dist/extensions/core/mcp-deferred.js.map +1 -0
- package/dist/extensions/core/mcp-loader.d.ts.map +1 -1
- package/dist/extensions/core/mcp-loader.js +132 -78
- package/dist/extensions/core/mcp-loader.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +19 -1
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/brand.d.ts +41 -0
- package/dist/modes/interactive/brand.d.ts.map +1 -0
- package/dist/modes/interactive/brand.js +40 -0
- package/dist/modes/interactive/brand.js.map +1 -0
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +102 -131
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/task-panel.d.ts +6 -5
- package/dist/modes/interactive/components/task-panel.d.ts.map +1 -1
- package/dist/modes/interactive/components/task-panel.js +13 -9
- package/dist/modes/interactive/components/task-panel.js.map +1 -1
- package/dist/modes/interactive/resource-display.d.ts.map +1 -1
- package/dist/modes/interactive/resource-display.js +131 -86
- package/dist/modes/interactive/resource-display.js.map +1 -1
- package/dist/modes/interactive/theme/dark.json +1 -1
- package/dist/modes/interactive/theme/light.json +1 -1
- package/dist/modes/interactive/voice/voice-controller.d.ts.map +1 -1
- package/dist/modes/interactive/voice/voice-controller.js +15 -7
- package/dist/modes/interactive/voice/voice-controller.js.map +1 -1
- package/dist/modes/interactive/voice/voice-panel.d.ts +27 -8
- package/dist/modes/interactive/voice/voice-panel.d.ts.map +1 -1
- package/dist/modes/interactive/voice/voice-panel.js +89 -27
- package/dist/modes/interactive/voice/voice-panel.js.map +1 -1
- package/dist/utils/tools-manager.d.ts +8 -2
- package/dist/utils/tools-manager.d.ts.map +1 -1
- package/dist/utils/tools-manager.js +15 -6
- package/dist/utils/tools-manager.js.map +1 -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/minimal-mode.ts +1 -1
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +5 -5
- package/dist/core/tools/glob.d.ts +0 -42
- package/dist/core/tools/glob.d.ts.map +0 -1
- package/dist/core/tools/glob.js +0 -432
- package/dist/core/tools/glob.js.map +0 -1
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HooCode brand identity — the single source of truth for the product mark and
|
|
3
|
+
* the glyph vocabulary the interactive surfaces share.
|
|
4
|
+
*
|
|
5
|
+
* The brand *colour* is the theme's `accent` token (see theme/dark.json,
|
|
6
|
+
* theme/light.json) so it stays theme-aware and swappable; this module owns the
|
|
7
|
+
* non-colour identity: the mark and the category glyphs used by the footer,
|
|
8
|
+
* the startup resource summary, and anywhere a capability class is labelled.
|
|
9
|
+
* Keeping them here means a future rebrand touches one file, not twenty.
|
|
10
|
+
*
|
|
11
|
+
* Glyphs are chosen to be single terminal cell, widely supported, and distinct
|
|
12
|
+
* from the task-panel's own status/owner glyphs (◐ ◆ ◇ ▸ ⧉ ✓ ✗ ○) so the two
|
|
13
|
+
* vocabularies never read as the same signal.
|
|
14
|
+
*/
|
|
15
|
+
/** The HooCode mark — a filled hexagon, rendered in the accent colour. */
|
|
16
|
+
export const BRAND_MARK = "⬢";
|
|
17
|
+
/** Product name, for splashes and headers. */
|
|
18
|
+
export const BRAND_NAME = "HooCode";
|
|
19
|
+
/**
|
|
20
|
+
* Glyphs for the capability classes a session can load. Used by the startup
|
|
21
|
+
* "resources ready" summary and reusable anywhere a class needs a label. Each is
|
|
22
|
+
* a single cell so counts and columns stay aligned.
|
|
23
|
+
*/
|
|
24
|
+
export const CATEGORY_GLYPH = {
|
|
25
|
+
skills: "✦",
|
|
26
|
+
commands: "⌘",
|
|
27
|
+
agents: "◈",
|
|
28
|
+
mcp: "⧉",
|
|
29
|
+
plugins: "⬡",
|
|
30
|
+
themes: "◒",
|
|
31
|
+
context: "❯",
|
|
32
|
+
extensions: "⊹",
|
|
33
|
+
};
|
|
34
|
+
/** A soft dot separator used between footer/summary segments. */
|
|
35
|
+
export const SEGMENT_SEP = "·";
|
|
36
|
+
/** Marker appended to a git branch when the working tree is dirty. */
|
|
37
|
+
export const GIT_DIRTY_MARK = "*";
|
|
38
|
+
/** Fork glyph preceding a git branch. */
|
|
39
|
+
export const GIT_BRANCH_GLYPH = "⑂";
|
|
40
|
+
//# sourceMappingURL=brand.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"brand.js","sourceRoot":"","sources":["../../../src/modes/interactive/brand.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,4EAA0E;AAC1E,MAAM,CAAC,MAAM,UAAU,GAAG,KAAG,CAAC;AAE9B,8CAA8C;AAC9C,MAAM,CAAC,MAAM,UAAU,GAAG,SAAS,CAAC;AAEpC;;;;GAIG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG;IAC7B,MAAM,EAAE,KAAG;IACX,QAAQ,EAAE,KAAG;IACb,MAAM,EAAE,KAAG;IACX,GAAG,EAAE,KAAG;IACR,OAAO,EAAE,KAAG;IACZ,MAAM,EAAE,KAAG;IACX,OAAO,EAAE,KAAG;IACZ,UAAU,EAAE,KAAG;CACN,CAAC;AAIX,iEAAiE;AACjE,MAAM,CAAC,MAAM,WAAW,GAAG,IAAG,CAAC;AAE/B,sEAAsE;AACtE,MAAM,CAAC,MAAM,cAAc,GAAG,GAAG,CAAC;AAElC,yCAAyC;AACzC,MAAM,CAAC,MAAM,gBAAgB,GAAG,KAAG,CAAC","sourcesContent":["/**\n * HooCode brand identity — the single source of truth for the product mark and\n * the glyph vocabulary the interactive surfaces share.\n *\n * The brand *colour* is the theme's `accent` token (see theme/dark.json,\n * theme/light.json) so it stays theme-aware and swappable; this module owns the\n * non-colour identity: the mark and the category glyphs used by the footer,\n * the startup resource summary, and anywhere a capability class is labelled.\n * Keeping them here means a future rebrand touches one file, not twenty.\n *\n * Glyphs are chosen to be single terminal cell, widely supported, and distinct\n * from the task-panel's own status/owner glyphs (◐ ◆ ◇ ▸ ⧉ ✓ ✗ ○) so the two\n * vocabularies never read as the same signal.\n */\n\n/** The HooCode mark — a filled hexagon, rendered in the accent colour. */\nexport const BRAND_MARK = \"⬢\";\n\n/** Product name, for splashes and headers. */\nexport const BRAND_NAME = \"HooCode\";\n\n/**\n * Glyphs for the capability classes a session can load. Used by the startup\n * \"resources ready\" summary and reusable anywhere a class needs a label. Each is\n * a single cell so counts and columns stay aligned.\n */\nexport const CATEGORY_GLYPH = {\n\tskills: \"✦\",\n\tcommands: \"⌘\",\n\tagents: \"◈\",\n\tmcp: \"⧉\",\n\tplugins: \"⬡\",\n\tthemes: \"◒\",\n\tcontext: \"❯\",\n\textensions: \"⊹\",\n} as const;\n\nexport type CategoryKey = keyof typeof CATEGORY_GLYPH;\n\n/** A soft dot separator used between footer/summary segments. */\nexport const SEGMENT_SEP = \"·\";\n\n/** Marker appended to a git branch when the working tree is dirty. */\nexport const GIT_DIRTY_MARK = \"*\";\n\n/** Fork glyph preceding a git branch. */\nexport const GIT_BRANCH_GLYPH = \"⑂\";\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"footer.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/footer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAiC,MAAM,0BAA0B,CAAC;AACzF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,uCAAuC,CAAC;AAuBxF;;;GAGG;AACH,qBAAa,eAAgB,YAAW,SAAS;IAI/C,OAAO,CAAC,OAAO;IACf,OAAO,CAAC,UAAU;IAJnB,OAAO,CAAC,kBAAkB,CAAQ;IAElC,YACS,OAAO,EAAE,YAAY,EACrB,UAAU,EAAE,0BAA0B,EAC3C;IAEJ,UAAU,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI,CAEtC;IAED,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAE5C;IAED;;;OAGG;IACH,UAAU,IAAI,IAAI,CAEjB;IAED;;;OAGG;IACH,OAAO,IAAI,IAAI,CAEd;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAkN9B;CACD","sourcesContent":["import { type Component, truncateToWidth, visibleWidth } from \"@kolisachint/hoocode-tui\";\nimport type { AgentSession } from \"../../../core/agent-session.js\";\nimport type { ReadonlyFooterDataProvider } from \"../../../core/footer-data-provider.js\";\nimport { theme } from \"../theme/theme.js\";\n\n/**\n * Sanitize text for display in a single-line status.\n * Removes newlines, tabs, carriage returns, and other control characters.\n */\nfunction sanitizeStatusText(text: string): string {\n\t// Replace newlines, tabs, carriage returns with space, then collapse multiple spaces\n\treturn text\n\t\t.replace(/[\\r\\n\\t]/g, \" \")\n\t\t.replace(/ +/g, \" \")\n\t\t.trim();\n}\n\nfunction 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\n/**\n * Footer component that shows pwd, token stats, and context usage.\n * Computes token/context stats from session, gets git branch and extension statuses from provider.\n */\nexport class FooterComponent implements Component {\n\tprivate autoCompactEnabled = true;\n\n\tconstructor(\n\t\tprivate session: AgentSession,\n\t\tprivate footerData: ReadonlyFooterDataProvider,\n\t) {}\n\n\tsetSession(session: AgentSession): void {\n\t\tthis.session = session;\n\t}\n\n\tsetAutoCompactEnabled(enabled: boolean): void {\n\t\tthis.autoCompactEnabled = enabled;\n\t}\n\n\t/**\n\t * No-op: git branch caching now handled by provider.\n\t * Kept for compatibility with existing call sites in interactive-mode.\n\t */\n\tinvalidate(): void {\n\t\t// No-op: git branch is cached/invalidated by provider\n\t}\n\n\t/**\n\t * Clean up resources.\n\t * Git watcher cleanup now handled by provider.\n\t */\n\tdispose(): void {\n\t\t// Git watcher cleanup handled by provider\n\t}\n\n\trender(width: number): string[] {\n\t\tconst state = this.session.state;\n\n\t\t// Calculate cumulative usage from ALL session entries (not just post-compaction messages)\n\t\tlet totalInput = 0;\n\t\tlet totalOutput = 0;\n\t\tlet totalCacheRead = 0;\n\t\tlet totalCacheWrite = 0;\n\t\tlet totalCost = 0;\n\n\t\tfor (const entry of this.session.sessionManager.getEntries()) {\n\t\t\tif (entry.type === \"message\" && entry.message.role === \"assistant\") {\n\t\t\t\ttotalInput += entry.message.usage.input;\n\t\t\t\ttotalOutput += entry.message.usage.output;\n\t\t\t\ttotalCacheRead += entry.message.usage.cacheRead;\n\t\t\t\ttotalCacheWrite += entry.message.usage.cacheWrite;\n\t\t\t\ttotalCost += entry.message.usage.cost.total;\n\t\t\t}\n\t\t}\n\n\t\t// Calculate context usage from session (handles compaction correctly).\n\t\t// After compaction, tokens are unknown until the next LLM response.\n\t\tconst contextUsage = this.session.getContextUsage();\n\t\tconst contextWindow = contextUsage?.contextWindow ?? state.model?.contextWindow ?? 0;\n\t\tconst contextPercentValue = contextUsage?.percent ?? 0;\n\t\tconst contextPercent = contextUsage?.percent !== null ? contextPercentValue.toFixed(1) : \"?\";\n\n\t\t// Replace home directory with ~\n\t\tlet pwd = this.session.sessionManager.getCwd();\n\t\tconst home = process.env.HOME || process.env.USERPROFILE;\n\t\tif (home && pwd.startsWith(home)) {\n\t\t\tpwd = `~${pwd.slice(home.length)}`;\n\t\t}\n\n\t\t// Add git branch if available\n\t\tconst branch = this.footerData.getGitBranch();\n\t\tif (branch) {\n\t\t\tpwd = `${pwd} (${branch})`;\n\t\t}\n\n\t\t// Add session name if set\n\t\tconst sessionName = this.session.sessionManager.getSessionName();\n\t\tif (sessionName) {\n\t\t\tpwd = `${pwd} • ${sessionName}`;\n\t\t}\n\n\t\t// Format mode for display (e.g., build)\n\t\tconst modeLabel = this.footerData.getActiveMode();\n\n\t\t// Calculate widths for right-aligning mode\n\t\tconst pwdWidth = visibleWidth(pwd);\n\t\tconst modeWidth = visibleWidth(modeLabel);\n\t\tconst modePadding = Math.max(2, width - pwdWidth - modeWidth);\n\n\t\t// Build pwd line with mode right-aligned in accent color (if it fits)\n\t\tlet pwdLine: string;\n\t\tif (pwdWidth + modeWidth + 2 <= width) {\n\t\t\tpwdLine = pwd + \" \".repeat(modePadding) + theme.fg(\"accent\", modeLabel);\n\t\t} else {\n\t\t\t// Not enough space, just show pwd\n\t\t\tpwdLine = pwd;\n\t\t}\n\n\t\t// Build stats line\n\t\tconst statsParts = [];\n\t\tif (totalInput) statsParts.push(`↑${formatTokens(totalInput)}`);\n\t\tif (totalOutput) statsParts.push(`↓${formatTokens(totalOutput)}`);\n\t\tif (totalCacheRead) statsParts.push(`R${formatTokens(totalCacheRead)}`);\n\t\tif (totalCacheWrite) statsParts.push(`W${formatTokens(totalCacheWrite)}`);\n\n\t\t// Show cost with \"(sub)\" indicator if using OAuth subscription\n\t\tconst usingSubscription = state.model ? this.session.modelRegistry.isUsingOAuth(state.model) : false;\n\t\tif (totalCost || usingSubscription) {\n\t\t\tconst costStr = `$${totalCost.toFixed(3)}${usingSubscription ? \" (sub)\" : \"\"}`;\n\t\t\tstatsParts.push(costStr);\n\t\t}\n\n\t\t// Auto-compact threshold: compaction fires when context > (window - reserveTokens).\n\t\t// Show it so users see the actual trip point, not just the raw window percentage.\n\t\tlet thresholdPercent: number | undefined;\n\t\tif (this.autoCompactEnabled && contextWindow > 0) {\n\t\t\tconst reserveTokens = this.session.settingsManager.getCompactionSettings().reserveTokens;\n\t\t\tconst effective = contextWindow - reserveTokens;\n\t\t\tif (effective > 0) thresholdPercent = (effective / contextWindow) * 100;\n\t\t}\n\n\t\tconst autoIndicator =\n\t\t\tthresholdPercent !== undefined\n\t\t\t\t? ` (auto @ ${thresholdPercent.toFixed(0)}%)`\n\t\t\t\t: this.autoCompactEnabled\n\t\t\t\t\t? \" (auto)\"\n\t\t\t\t\t: \"\";\n\t\tconst contextPercentDisplay =\n\t\t\tcontextPercent === \"?\"\n\t\t\t\t? `?/${formatTokens(contextWindow)}${autoIndicator}`\n\t\t\t\t: `${contextPercent}%/${formatTokens(contextWindow)}${autoIndicator}`;\n\n\t\t// Color based on proximity to the auto-compact trip point.\n\t\t// With threshold known: error within 3pp, warning within 10pp. Without it: old fixed bands.\n\t\tlet contextPercentStr: string;\n\t\tconst errorLevel = thresholdPercent !== undefined ? thresholdPercent - 3 : 90;\n\t\tconst warnLevel = thresholdPercent !== undefined ? thresholdPercent - 10 : 70;\n\t\tif (contextPercentValue >= errorLevel) {\n\t\t\tcontextPercentStr = theme.fg(\"error\", contextPercentDisplay);\n\t\t} else if (contextPercentValue >= warnLevel) {\n\t\t\tcontextPercentStr = theme.fg(\"warning\", contextPercentDisplay);\n\t\t} else {\n\t\t\tcontextPercentStr = contextPercentDisplay;\n\t\t}\n\t\tstatsParts.push(contextPercentStr);\n\n\t\tlet statsLeft = statsParts.join(\" \");\n\n\t\t// Add model name on the right side, plus thinking level if model supports it\n\t\tconst modelName = state.model?.id || \"no-model\";\n\n\t\tlet statsLeftWidth = visibleWidth(statsLeft);\n\n\t\t// If statsLeft is too wide, truncate it\n\t\tif (statsLeftWidth > width) {\n\t\t\tstatsLeft = truncateToWidth(statsLeft, width, \"...\");\n\t\t\tstatsLeftWidth = visibleWidth(statsLeft);\n\t\t}\n\n\t\t// Calculate available space for padding (minimum 2 spaces between stats and model)\n\t\tconst minPadding = 2;\n\n\t\t// Add thinking level indicator if model supports reasoning\n\t\tlet rightSideWithoutProvider = modelName;\n\t\tif (state.model?.reasoning) {\n\t\t\tconst thinkingLevel = state.thinkingLevel || \"off\";\n\t\t\trightSideWithoutProvider =\n\t\t\t\tthinkingLevel === \"off\" ? `${modelName} • thinking off` : `${modelName} • ${thinkingLevel}`;\n\t\t}\n\n\t\t// Prepend the provider in parentheses if there are multiple providers and there's enough room\n\t\tlet rightSide = rightSideWithoutProvider;\n\t\tif (this.footerData.getAvailableProviderCount() > 1 && state.model) {\n\t\t\trightSide = `(${state.model!.provider}) ${rightSideWithoutProvider}`;\n\t\t\tif (statsLeftWidth + minPadding + visibleWidth(rightSide) > width) {\n\t\t\t\t// Too wide, fall back\n\t\t\t\trightSide = rightSideWithoutProvider;\n\t\t\t}\n\t\t}\n\n\t\tconst rightSideWidth = visibleWidth(rightSide);\n\t\tconst totalNeeded = statsLeftWidth + minPadding + rightSideWidth;\n\n\t\tlet statsLine: string;\n\t\tif (totalNeeded <= width) {\n\t\t\t// Both fit - add padding to right-align model\n\t\t\tconst padding = \" \".repeat(width - statsLeftWidth - rightSideWidth);\n\t\t\tstatsLine = statsLeft + padding + rightSide;\n\t\t} else {\n\t\t\t// Need to truncate right side\n\t\t\tconst availableForRight = width - statsLeftWidth - minPadding;\n\t\t\tif (availableForRight > 0) {\n\t\t\t\tconst truncatedRight = truncateToWidth(rightSide, availableForRight, \"\");\n\t\t\t\tconst truncatedRightWidth = visibleWidth(truncatedRight);\n\t\t\t\tconst padding = \" \".repeat(Math.max(0, width - statsLeftWidth - truncatedRightWidth));\n\t\t\t\tstatsLine = statsLeft + padding + truncatedRight;\n\t\t\t} else {\n\t\t\t\t// Not enough space for right side at all\n\t\t\t\tstatsLine = statsLeft;\n\t\t\t}\n\t\t}\n\n\t\t// Apply dim to each part separately. statsLeft may contain color codes (for context %)\n\t\t// that end with a reset, which would clear an outer dim wrapper. So we dim the parts\n\t\t// before and after the colored section independently.\n\t\tconst dimStatsLeft = theme.fg(\"dim\", statsLeft);\n\t\tconst remainder = statsLine.slice(statsLeft.length); // padding + rightSide\n\t\tconst dimRemainder = theme.fg(\"dim\", remainder);\n\n\t\t// Build the final pwd line: dim the path, accent the mode label (if present)\n\t\tlet finalPwdLine: string;\n\t\tif (pwdLine === pwd) {\n\t\t\t// Just pwd, no mode (or it didn't fit) - dim the whole thing\n\t\t\tfinalPwdLine = truncateToWidth(theme.fg(\"dim\", pwdLine), width, theme.fg(\"dim\", \"...\"));\n\t\t} else {\n\t\t\t// pwdLine has mode appended with accent color\n\t\t\t// Extract the plain text version for width calculation\n\t\t\tconst pwdLinePlain = pwd + \" \".repeat(modePadding) + modeLabel;\n\t\t\t// Truncate the plain text if needed\n\t\t\tconst truncatedPlain = truncateToWidth(pwdLinePlain, width, \"...\");\n\t\t\t// Check if mode was truncated out\n\t\t\tif (!truncatedPlain.includes(modeLabel)) {\n\t\t\t\t// Mode didn't fit after truncation, just show dimmed truncated pwd\n\t\t\t\tfinalPwdLine = theme.fg(\"dim\", truncatedPlain);\n\t\t\t} else {\n\t\t\t\t// Split at the mode part and apply colors\n\t\t\t\tconst pwdPart = truncatedPlain.slice(0, truncatedPlain.indexOf(modeLabel));\n\t\t\t\tfinalPwdLine = theme.fg(\"dim\", pwdPart) + theme.fg(\"accent\", modeLabel);\n\t\t\t}\n\t\t}\n\n\t\tconst lines = [finalPwdLine, dimStatsLeft + dimRemainder];\n\n\t\t// Add extension statuses on a single line, sorted by key alphabetically\n\t\tconst extensionStatuses = this.footerData.getExtensionStatuses();\n\t\tif (extensionStatuses.size > 0) {\n\t\t\tconst sortedStatuses = Array.from(extensionStatuses.entries())\n\t\t\t\t.sort(([a], [b]) => a.localeCompare(b))\n\t\t\t\t.map(([, text]) => sanitizeStatusText(text));\n\t\t\tconst statusLine = sortedStatuses.join(\" \");\n\t\t\t// Truncate to terminal width with dim ellipsis for consistency with footer style\n\t\t\tlines.push(truncateToWidth(statusLine, width, theme.fg(\"dim\", \"...\")));\n\t\t}\n\n\t\treturn lines;\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"footer.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/footer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAiC,MAAM,0BAA0B,CAAC;AACzF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,uCAAuC,CAAC;AAwDxF;;;GAGG;AACH,qBAAa,eAAgB,YAAW,SAAS;IAI/C,OAAO,CAAC,OAAO;IACf,OAAO,CAAC,UAAU;IAJnB,OAAO,CAAC,kBAAkB,CAAQ;IAElC,YACS,OAAO,EAAE,YAAY,EACrB,UAAU,EAAE,0BAA0B,EAC3C;IAEJ,UAAU,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI,CAEtC;IAED,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAE5C;IAED;;;OAGG;IACH,UAAU,IAAI,IAAI,CAEjB;IAED;;;OAGG;IACH,OAAO,IAAI,IAAI,CAEd;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CA6I9B;CACD","sourcesContent":["import { type Component, truncateToWidth, visibleWidth } from \"@kolisachint/hoocode-tui\";\nimport type { AgentSession } from \"../../../core/agent-session.js\";\nimport type { ReadonlyFooterDataProvider } from \"../../../core/footer-data-provider.js\";\nimport { taskStore } from \"../../../core/task-store.js\";\nimport { BRAND_MARK, GIT_BRANCH_GLYPH } from \"../brand.js\";\nimport { theme } from \"../theme/theme.js\";\n\n/**\n * Assemble one footer line: `left` flush left, `right` flush right when it fits\n * (≥2 cols between), padded to the full width. When it doesn't fit, drop `right`\n * and pad; when even `left` overflows, truncate it. Width math runs on the plain\n * strings; the styled strings carry the colour. Every returned line is exactly\n * `width` cells or fewer — the invariant the footer-width tests hold us to.\n */\nfunction assembleLine(width: number, leftPlain: string, leftStyled: string, rightPlain = \"\", rightStyled = \"\"): string {\n\tconst lw = visibleWidth(leftPlain);\n\tif (rightPlain && lw + 2 + visibleWidth(rightPlain) <= width) {\n\t\treturn leftStyled + \" \".repeat(width - lw - visibleWidth(rightPlain)) + rightStyled;\n\t}\n\tif (lw <= width) return leftStyled + \" \".repeat(width - lw);\n\treturn truncateToWidth(leftStyled, width, theme.fg(\"dim\", \"…\"));\n}\n\n/** A compact context-fill gauge, coloured by proximity to the auto-compact trip point. */\nfunction contextGauge(percent: number, errorLevel: number, warnLevel: number): { plain: string; styled: string } {\n\tconst CELLS = 8;\n\tconst filled = Math.max(0, Math.min(CELLS, Math.round((percent / 100) * CELLS)));\n\tconst fill = \"▰\".repeat(filled);\n\tconst track = \"▱\".repeat(CELLS - filled);\n\tconst color = percent >= errorLevel ? \"error\" : percent >= warnLevel ? \"warning\" : \"accent\";\n\treturn { plain: fill + track, styled: theme.fg(color, fill) + theme.fg(\"dim\", track) };\n}\n\n/** Count subagent runs currently in flight, for the footer's live delegation cue. */\nfunction activeSubagentCount(): number {\n\treturn taskStore.list().filter((t) => t.source === \"subagent\" && t.status === \"in_progress\").length;\n}\n\n/**\n * Sanitize text for display in a single-line status.\n * Removes newlines, tabs, carriage returns, and other control characters.\n */\nfunction sanitizeStatusText(text: string): string {\n\t// Replace newlines, tabs, carriage returns with space, then collapse multiple spaces\n\treturn text\n\t\t.replace(/[\\r\\n\\t]/g, \" \")\n\t\t.replace(/ +/g, \" \")\n\t\t.trim();\n}\n\nfunction 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\n/**\n * Footer component that shows pwd, token stats, and context usage.\n * Computes token/context stats from session, gets git branch and extension statuses from provider.\n */\nexport class FooterComponent implements Component {\n\tprivate autoCompactEnabled = true;\n\n\tconstructor(\n\t\tprivate session: AgentSession,\n\t\tprivate footerData: ReadonlyFooterDataProvider,\n\t) {}\n\n\tsetSession(session: AgentSession): void {\n\t\tthis.session = session;\n\t}\n\n\tsetAutoCompactEnabled(enabled: boolean): void {\n\t\tthis.autoCompactEnabled = enabled;\n\t}\n\n\t/**\n\t * No-op: git branch caching now handled by provider.\n\t * Kept for compatibility with existing call sites in interactive-mode.\n\t */\n\tinvalidate(): void {\n\t\t// No-op: git branch is cached/invalidated by provider\n\t}\n\n\t/**\n\t * Clean up resources.\n\t * Git watcher cleanup now handled by provider.\n\t */\n\tdispose(): void {\n\t\t// Git watcher cleanup handled by provider\n\t}\n\n\trender(width: number): string[] {\n\t\tconst state = this.session.state;\n\n\t\t// Calculate cumulative usage from ALL session entries (not just post-compaction messages)\n\t\tlet totalInput = 0;\n\t\tlet totalOutput = 0;\n\t\tlet totalCacheRead = 0;\n\t\tlet totalCacheWrite = 0;\n\t\tlet totalCost = 0;\n\n\t\tfor (const entry of this.session.sessionManager.getEntries()) {\n\t\t\tif (entry.type === \"message\" && entry.message.role === \"assistant\") {\n\t\t\t\ttotalInput += entry.message.usage.input;\n\t\t\t\ttotalOutput += entry.message.usage.output;\n\t\t\t\ttotalCacheRead += entry.message.usage.cacheRead;\n\t\t\t\ttotalCacheWrite += entry.message.usage.cacheWrite;\n\t\t\t\ttotalCost += entry.message.usage.cost.total;\n\t\t\t}\n\t\t}\n\n\t\t// Calculate context usage from session (handles compaction correctly).\n\t\t// After compaction, tokens are unknown until the next LLM response.\n\t\tconst contextUsage = this.session.getContextUsage();\n\t\tconst contextWindow = contextUsage?.contextWindow ?? state.model?.contextWindow ?? 0;\n\t\tconst contextPercentValue = contextUsage?.percent ?? 0;\n\t\tconst contextPercent = contextUsage?.percent !== null ? contextPercentValue.toFixed(1) : \"?\";\n\n\t\t// Replace home directory with ~\n\t\tlet pwd = this.session.sessionManager.getCwd();\n\t\tconst home = process.env.HOME || process.env.USERPROFILE;\n\t\tif (home && pwd.startsWith(home)) {\n\t\t\tpwd = `~${pwd.slice(home.length)}`;\n\t\t}\n\t\tconst branch = this.footerData.getGitBranch();\n\t\tconst sessionName = this.session.sessionManager.getSessionName();\n\t\tconst modeLabel = this.footerData.getActiveMode();\n\n\t\t// ── Line 1 — identity & location ────────────────────────────────────────\n\t\t// Lead with the brand mark + MODE (the agent's guardrail: Ask/Plan/Build/\n\t\t// Debug) in bold accent so it is the first thing the eye lands on, then the\n\t\t// path, git branch, and session name in descending emphasis. The live\n\t\t// subagent count sits flush right — present only while work is delegated.\n\t\tconst modeUp = modeLabel.toUpperCase();\n\t\tconst brand = `${BRAND_MARK} ${modeUp}`;\n\t\tlet l1Plain = `${brand} ${pwd}`;\n\t\tlet l1Styled = `${theme.bold(theme.fg(\"accent\", brand))} ${theme.fg(\"muted\", pwd)}`;\n\t\tif (branch) {\n\t\t\tl1Plain += ` ${GIT_BRANCH_GLYPH} ${branch}`;\n\t\t\tl1Styled += ` ${theme.fg(\"dim\", GIT_BRANCH_GLYPH)} ${theme.fg(\"muted\", branch)}`;\n\t\t}\n\t\tif (sessionName) {\n\t\t\tl1Plain += ` • ${sessionName}`;\n\t\t\tl1Styled += theme.fg(\"dim\", ` • ${sessionName}`);\n\t\t}\n\t\tconst nSub = activeSubagentCount();\n\t\tconst l1RightPlain = nSub > 0 ? `◇${nSub} running` : \"\";\n\t\tconst l1RightStyled = nSub > 0 ? theme.fg(\"accent\", `◇${nSub}`) + theme.fg(\"dim\", \" running\") : \"\";\n\t\tconst line1 = assembleLine(width, l1Plain, l1Styled, l1RightPlain, l1RightStyled);\n\n\t\t// ── Line 2 — session vitals ─────────────────────────────────────────────\n\t\t// A context-fill gauge (coloured by proximity to the auto-compact trip\n\t\t// point) leads, then token/cost deltas, with the model + thinking level\n\t\t// flush right. Numbers read in muted, labels/arrows in dim — a legible\n\t\t// hierarchy in place of the old uniform grey.\n\t\tlet thresholdPercent: number | undefined;\n\t\tif (this.autoCompactEnabled && contextWindow > 0) {\n\t\t\tconst reserveTokens = this.session.settingsManager.getCompactionSettings().reserveTokens;\n\t\t\tconst effective = contextWindow - reserveTokens;\n\t\t\tif (effective > 0) thresholdPercent = (effective / contextWindow) * 100;\n\t\t}\n\t\tconst errorLevel = thresholdPercent !== undefined ? thresholdPercent - 3 : 90;\n\t\tconst warnLevel = thresholdPercent !== undefined ? thresholdPercent - 10 : 70;\n\t\tconst autoIndicator =\n\t\t\tthresholdPercent !== undefined\n\t\t\t\t? ` auto@${thresholdPercent.toFixed(0)}%`\n\t\t\t\t: this.autoCompactEnabled\n\t\t\t\t\t? \" auto\"\n\t\t\t\t\t: \"\";\n\n\t\tconst gauge = contextGauge(contextPercentValue, errorLevel, warnLevel);\n\t\tconst pctText = contextPercent === \"?\" ? \"?\" : `${contextPercent}%`;\n\t\tconst pctColor =\n\t\t\tcontextPercentValue >= errorLevel ? \"error\" : contextPercentValue >= warnLevel ? \"warning\" : \"muted\";\n\t\tconst winText = `${formatTokens(contextWindow)}${autoIndicator}`;\n\n\t\tconst segs: Array<{ plain: string; styled: string }> = [\n\t\t\t{\n\t\t\t\tplain: `${gauge.plain} ${pctText} ${winText}`,\n\t\t\t\tstyled: `${gauge.styled} ${theme.fg(pctColor, pctText)} ${theme.fg(\"dim\", winText)}`,\n\t\t\t},\n\t\t];\n\t\tconst arrow = (a: string, n: number) => ({\n\t\t\tplain: `${a}${formatTokens(n)}`,\n\t\t\tstyled: theme.fg(\"dim\", a) + theme.fg(\"muted\", formatTokens(n)),\n\t\t});\n\t\tif (totalInput) segs.push(arrow(\"↑\", totalInput));\n\t\tif (totalOutput) segs.push(arrow(\"↓\", totalOutput));\n\t\tif (totalCacheRead) segs.push(arrow(\"R\", totalCacheRead));\n\t\tif (totalCacheWrite) segs.push(arrow(\"W\", totalCacheWrite));\n\t\tconst usingSubscription = state.model ? this.session.modelRegistry.isUsingOAuth(state.model) : false;\n\t\tif (totalCost || usingSubscription) {\n\t\t\tconst costStr = `$${totalCost.toFixed(3)}${usingSubscription ? \" (sub)\" : \"\"}`;\n\t\t\tsegs.push({ plain: costStr, styled: theme.fg(\"muted\", costStr) });\n\t\t}\n\t\tconst l2Plain = segs.map((s) => s.plain).join(\" \");\n\t\tconst l2Styled = segs.map((s) => s.styled).join(\" \");\n\n\t\t// Right: model, thinking level, and provider (when several are configured).\n\t\tconst modelName = state.model?.id || \"no-model\";\n\t\tlet r2Plain = modelName;\n\t\tlet r2Styled = theme.fg(\"muted\", modelName);\n\t\tif (state.model?.reasoning) {\n\t\t\tconst tl = state.thinkingLevel || \"off\";\n\t\t\tconst tstr = tl === \"off\" ? \"thinking off\" : tl;\n\t\t\tr2Plain += ` • ${tstr}`;\n\t\t\tr2Styled += theme.fg(\"dim\", ` • ${tstr}`);\n\t\t}\n\t\tif (this.footerData.getAvailableProviderCount() > 1 && state.model) {\n\t\t\t// Prepend the provider only when the whole right cluster still fits.\n\t\t\tconst withProv = `(${state.model.provider}) ${r2Plain}`;\n\t\t\tif (visibleWidth(l2Plain) + 2 + visibleWidth(withProv) <= width) {\n\t\t\t\tr2Plain = withProv;\n\t\t\t\tr2Styled = theme.fg(\"dim\", `(${state.model.provider}) `) + r2Styled;\n\t\t\t}\n\t\t}\n\t\tconst line2 = assembleLine(width, l2Plain, l2Styled, r2Plain, r2Styled);\n\n\t\tconst lines = [line1, line2];\n\n\t\t// Add extension statuses on a single line, sorted by key alphabetically\n\t\tconst extensionStatuses = this.footerData.getExtensionStatuses();\n\t\tif (extensionStatuses.size > 0) {\n\t\t\tconst sortedStatuses = Array.from(extensionStatuses.entries())\n\t\t\t\t.sort(([a], [b]) => a.localeCompare(b))\n\t\t\t\t.map(([, text]) => sanitizeStatusText(text));\n\t\t\tconst statusLine = sortedStatuses.join(\" \");\n\t\t\t// Truncate to terminal width with dim ellipsis for consistency with footer style\n\t\t\tlines.push(truncateToWidth(statusLine, width, theme.fg(\"dim\", \"...\")));\n\t\t}\n\n\t\treturn lines;\n\t}\n}\n"]}
|
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
import { truncateToWidth, visibleWidth } from "@kolisachint/hoocode-tui";
|
|
2
|
+
import { taskStore } from "../../../core/task-store.js";
|
|
3
|
+
import { BRAND_MARK, GIT_BRANCH_GLYPH } from "../brand.js";
|
|
2
4
|
import { theme } from "../theme/theme.js";
|
|
5
|
+
/**
|
|
6
|
+
* Assemble one footer line: `left` flush left, `right` flush right when it fits
|
|
7
|
+
* (≥2 cols between), padded to the full width. When it doesn't fit, drop `right`
|
|
8
|
+
* and pad; when even `left` overflows, truncate it. Width math runs on the plain
|
|
9
|
+
* strings; the styled strings carry the colour. Every returned line is exactly
|
|
10
|
+
* `width` cells or fewer — the invariant the footer-width tests hold us to.
|
|
11
|
+
*/
|
|
12
|
+
function assembleLine(width, leftPlain, leftStyled, rightPlain = "", rightStyled = "") {
|
|
13
|
+
const lw = visibleWidth(leftPlain);
|
|
14
|
+
if (rightPlain && lw + 2 + visibleWidth(rightPlain) <= width) {
|
|
15
|
+
return leftStyled + " ".repeat(width - lw - visibleWidth(rightPlain)) + rightStyled;
|
|
16
|
+
}
|
|
17
|
+
if (lw <= width)
|
|
18
|
+
return leftStyled + " ".repeat(width - lw);
|
|
19
|
+
return truncateToWidth(leftStyled, width, theme.fg("dim", "…"));
|
|
20
|
+
}
|
|
21
|
+
/** A compact context-fill gauge, coloured by proximity to the auto-compact trip point. */
|
|
22
|
+
function contextGauge(percent, errorLevel, warnLevel) {
|
|
23
|
+
const CELLS = 8;
|
|
24
|
+
const filled = Math.max(0, Math.min(CELLS, Math.round((percent / 100) * CELLS)));
|
|
25
|
+
const fill = "▰".repeat(filled);
|
|
26
|
+
const track = "▱".repeat(CELLS - filled);
|
|
27
|
+
const color = percent >= errorLevel ? "error" : percent >= warnLevel ? "warning" : "accent";
|
|
28
|
+
return { plain: fill + track, styled: theme.fg(color, fill) + theme.fg("dim", track) };
|
|
29
|
+
}
|
|
30
|
+
/** Count subagent runs currently in flight, for the footer's live delegation cue. */
|
|
31
|
+
function activeSubagentCount() {
|
|
32
|
+
return taskStore.list().filter((t) => t.source === "subagent" && t.status === "in_progress").length;
|
|
33
|
+
}
|
|
3
34
|
/**
|
|
4
35
|
* Sanitize text for display in a single-line status.
|
|
5
36
|
* Removes newlines, tabs, carriage returns, and other control characters.
|
|
@@ -83,49 +114,35 @@ export class FooterComponent {
|
|
|
83
114
|
if (home && pwd.startsWith(home)) {
|
|
84
115
|
pwd = `~${pwd.slice(home.length)}`;
|
|
85
116
|
}
|
|
86
|
-
// Add git branch if available
|
|
87
117
|
const branch = this.footerData.getGitBranch();
|
|
88
|
-
if (branch) {
|
|
89
|
-
pwd = `${pwd} (${branch})`;
|
|
90
|
-
}
|
|
91
|
-
// Add session name if set
|
|
92
118
|
const sessionName = this.session.sessionManager.getSessionName();
|
|
93
|
-
if (sessionName) {
|
|
94
|
-
pwd = `${pwd} • ${sessionName}`;
|
|
95
|
-
}
|
|
96
|
-
// Format mode for display (e.g., build)
|
|
97
119
|
const modeLabel = this.footerData.getActiveMode();
|
|
98
|
-
//
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
//
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
111
|
-
// Build stats line
|
|
112
|
-
const statsParts = [];
|
|
113
|
-
if (totalInput)
|
|
114
|
-
statsParts.push(`↑${formatTokens(totalInput)}`);
|
|
115
|
-
if (totalOutput)
|
|
116
|
-
statsParts.push(`↓${formatTokens(totalOutput)}`);
|
|
117
|
-
if (totalCacheRead)
|
|
118
|
-
statsParts.push(`R${formatTokens(totalCacheRead)}`);
|
|
119
|
-
if (totalCacheWrite)
|
|
120
|
-
statsParts.push(`W${formatTokens(totalCacheWrite)}`);
|
|
121
|
-
// Show cost with "(sub)" indicator if using OAuth subscription
|
|
122
|
-
const usingSubscription = state.model ? this.session.modelRegistry.isUsingOAuth(state.model) : false;
|
|
123
|
-
if (totalCost || usingSubscription) {
|
|
124
|
-
const costStr = `$${totalCost.toFixed(3)}${usingSubscription ? " (sub)" : ""}`;
|
|
125
|
-
statsParts.push(costStr);
|
|
120
|
+
// ── Line 1 — identity & location ────────────────────────────────────────
|
|
121
|
+
// Lead with the brand mark + MODE (the agent's guardrail: Ask/Plan/Build/
|
|
122
|
+
// Debug) in bold accent so it is the first thing the eye lands on, then the
|
|
123
|
+
// path, git branch, and session name in descending emphasis. The live
|
|
124
|
+
// subagent count sits flush right — present only while work is delegated.
|
|
125
|
+
const modeUp = modeLabel.toUpperCase();
|
|
126
|
+
const brand = `${BRAND_MARK} ${modeUp}`;
|
|
127
|
+
let l1Plain = `${brand} ${pwd}`;
|
|
128
|
+
let l1Styled = `${theme.bold(theme.fg("accent", brand))} ${theme.fg("muted", pwd)}`;
|
|
129
|
+
if (branch) {
|
|
130
|
+
l1Plain += ` ${GIT_BRANCH_GLYPH} ${branch}`;
|
|
131
|
+
l1Styled += ` ${theme.fg("dim", GIT_BRANCH_GLYPH)} ${theme.fg("muted", branch)}`;
|
|
126
132
|
}
|
|
127
|
-
|
|
128
|
-
|
|
133
|
+
if (sessionName) {
|
|
134
|
+
l1Plain += ` • ${sessionName}`;
|
|
135
|
+
l1Styled += theme.fg("dim", ` • ${sessionName}`);
|
|
136
|
+
}
|
|
137
|
+
const nSub = activeSubagentCount();
|
|
138
|
+
const l1RightPlain = nSub > 0 ? `◇${nSub} running` : "";
|
|
139
|
+
const l1RightStyled = nSub > 0 ? theme.fg("accent", `◇${nSub}`) + theme.fg("dim", " running") : "";
|
|
140
|
+
const line1 = assembleLine(width, l1Plain, l1Styled, l1RightPlain, l1RightStyled);
|
|
141
|
+
// ── Line 2 — session vitals ─────────────────────────────────────────────
|
|
142
|
+
// A context-fill gauge (coloured by proximity to the auto-compact trip
|
|
143
|
+
// point) leads, then token/cost deltas, with the model + thinking level
|
|
144
|
+
// flush right. Numbers read in muted, labels/arrows in dim — a legible
|
|
145
|
+
// hierarchy in place of the old uniform grey.
|
|
129
146
|
let thresholdPercent;
|
|
130
147
|
if (this.autoCompactEnabled && contextWindow > 0) {
|
|
131
148
|
const reserveTokens = this.session.settingsManager.getCompactionSettings().reserveTokens;
|
|
@@ -133,108 +150,62 @@ export class FooterComponent {
|
|
|
133
150
|
if (effective > 0)
|
|
134
151
|
thresholdPercent = (effective / contextWindow) * 100;
|
|
135
152
|
}
|
|
153
|
+
const errorLevel = thresholdPercent !== undefined ? thresholdPercent - 3 : 90;
|
|
154
|
+
const warnLevel = thresholdPercent !== undefined ? thresholdPercent - 10 : 70;
|
|
136
155
|
const autoIndicator = thresholdPercent !== undefined
|
|
137
|
-
? `
|
|
156
|
+
? ` auto@${thresholdPercent.toFixed(0)}%`
|
|
138
157
|
: this.autoCompactEnabled
|
|
139
|
-
? "
|
|
158
|
+
? " auto"
|
|
140
159
|
: "";
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
|
|
160
|
+
const gauge = contextGauge(contextPercentValue, errorLevel, warnLevel);
|
|
161
|
+
const pctText = contextPercent === "?" ? "?" : `${contextPercent}%`;
|
|
162
|
+
const pctColor = contextPercentValue >= errorLevel ? "error" : contextPercentValue >= warnLevel ? "warning" : "muted";
|
|
163
|
+
const winText = `${formatTokens(contextWindow)}${autoIndicator}`;
|
|
164
|
+
const segs = [
|
|
165
|
+
{
|
|
166
|
+
plain: `${gauge.plain} ${pctText} ${winText}`,
|
|
167
|
+
styled: `${gauge.styled} ${theme.fg(pctColor, pctText)} ${theme.fg("dim", winText)}`,
|
|
168
|
+
},
|
|
169
|
+
];
|
|
170
|
+
const arrow = (a, n) => ({
|
|
171
|
+
plain: `${a}${formatTokens(n)}`,
|
|
172
|
+
styled: theme.fg("dim", a) + theme.fg("muted", formatTokens(n)),
|
|
173
|
+
});
|
|
174
|
+
if (totalInput)
|
|
175
|
+
segs.push(arrow("↑", totalInput));
|
|
176
|
+
if (totalOutput)
|
|
177
|
+
segs.push(arrow("↓", totalOutput));
|
|
178
|
+
if (totalCacheRead)
|
|
179
|
+
segs.push(arrow("R", totalCacheRead));
|
|
180
|
+
if (totalCacheWrite)
|
|
181
|
+
segs.push(arrow("W", totalCacheWrite));
|
|
182
|
+
const usingSubscription = state.model ? this.session.modelRegistry.isUsingOAuth(state.model) : false;
|
|
183
|
+
if (totalCost || usingSubscription) {
|
|
184
|
+
const costStr = `$${totalCost.toFixed(3)}${usingSubscription ? " (sub)" : ""}`;
|
|
185
|
+
segs.push({ plain: costStr, styled: theme.fg("muted", costStr) });
|
|
157
186
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
//
|
|
187
|
+
const l2Plain = segs.map((s) => s.plain).join(" ");
|
|
188
|
+
const l2Styled = segs.map((s) => s.styled).join(" ");
|
|
189
|
+
// Right: model, thinking level, and provider (when several are configured).
|
|
161
190
|
const modelName = state.model?.id || "no-model";
|
|
162
|
-
let
|
|
163
|
-
|
|
164
|
-
if (statsLeftWidth > width) {
|
|
165
|
-
statsLeft = truncateToWidth(statsLeft, width, "...");
|
|
166
|
-
statsLeftWidth = visibleWidth(statsLeft);
|
|
167
|
-
}
|
|
168
|
-
// Calculate available space for padding (minimum 2 spaces between stats and model)
|
|
169
|
-
const minPadding = 2;
|
|
170
|
-
// Add thinking level indicator if model supports reasoning
|
|
171
|
-
let rightSideWithoutProvider = modelName;
|
|
191
|
+
let r2Plain = modelName;
|
|
192
|
+
let r2Styled = theme.fg("muted", modelName);
|
|
172
193
|
if (state.model?.reasoning) {
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
194
|
+
const tl = state.thinkingLevel || "off";
|
|
195
|
+
const tstr = tl === "off" ? "thinking off" : tl;
|
|
196
|
+
r2Plain += ` • ${tstr}`;
|
|
197
|
+
r2Styled += theme.fg("dim", ` • ${tstr}`);
|
|
176
198
|
}
|
|
177
|
-
// Prepend the provider in parentheses if there are multiple providers and there's enough room
|
|
178
|
-
let rightSide = rightSideWithoutProvider;
|
|
179
199
|
if (this.footerData.getAvailableProviderCount() > 1 && state.model) {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
}
|
|
186
|
-
const rightSideWidth = visibleWidth(rightSide);
|
|
187
|
-
const totalNeeded = statsLeftWidth + minPadding + rightSideWidth;
|
|
188
|
-
let statsLine;
|
|
189
|
-
if (totalNeeded <= width) {
|
|
190
|
-
// Both fit - add padding to right-align model
|
|
191
|
-
const padding = " ".repeat(width - statsLeftWidth - rightSideWidth);
|
|
192
|
-
statsLine = statsLeft + padding + rightSide;
|
|
193
|
-
}
|
|
194
|
-
else {
|
|
195
|
-
// Need to truncate right side
|
|
196
|
-
const availableForRight = width - statsLeftWidth - minPadding;
|
|
197
|
-
if (availableForRight > 0) {
|
|
198
|
-
const truncatedRight = truncateToWidth(rightSide, availableForRight, "");
|
|
199
|
-
const truncatedRightWidth = visibleWidth(truncatedRight);
|
|
200
|
-
const padding = " ".repeat(Math.max(0, width - statsLeftWidth - truncatedRightWidth));
|
|
201
|
-
statsLine = statsLeft + padding + truncatedRight;
|
|
202
|
-
}
|
|
203
|
-
else {
|
|
204
|
-
// Not enough space for right side at all
|
|
205
|
-
statsLine = statsLeft;
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
// Apply dim to each part separately. statsLeft may contain color codes (for context %)
|
|
209
|
-
// that end with a reset, which would clear an outer dim wrapper. So we dim the parts
|
|
210
|
-
// before and after the colored section independently.
|
|
211
|
-
const dimStatsLeft = theme.fg("dim", statsLeft);
|
|
212
|
-
const remainder = statsLine.slice(statsLeft.length); // padding + rightSide
|
|
213
|
-
const dimRemainder = theme.fg("dim", remainder);
|
|
214
|
-
// Build the final pwd line: dim the path, accent the mode label (if present)
|
|
215
|
-
let finalPwdLine;
|
|
216
|
-
if (pwdLine === pwd) {
|
|
217
|
-
// Just pwd, no mode (or it didn't fit) - dim the whole thing
|
|
218
|
-
finalPwdLine = truncateToWidth(theme.fg("dim", pwdLine), width, theme.fg("dim", "..."));
|
|
219
|
-
}
|
|
220
|
-
else {
|
|
221
|
-
// pwdLine has mode appended with accent color
|
|
222
|
-
// Extract the plain text version for width calculation
|
|
223
|
-
const pwdLinePlain = pwd + " ".repeat(modePadding) + modeLabel;
|
|
224
|
-
// Truncate the plain text if needed
|
|
225
|
-
const truncatedPlain = truncateToWidth(pwdLinePlain, width, "...");
|
|
226
|
-
// Check if mode was truncated out
|
|
227
|
-
if (!truncatedPlain.includes(modeLabel)) {
|
|
228
|
-
// Mode didn't fit after truncation, just show dimmed truncated pwd
|
|
229
|
-
finalPwdLine = theme.fg("dim", truncatedPlain);
|
|
230
|
-
}
|
|
231
|
-
else {
|
|
232
|
-
// Split at the mode part and apply colors
|
|
233
|
-
const pwdPart = truncatedPlain.slice(0, truncatedPlain.indexOf(modeLabel));
|
|
234
|
-
finalPwdLine = theme.fg("dim", pwdPart) + theme.fg("accent", modeLabel);
|
|
200
|
+
// Prepend the provider only when the whole right cluster still fits.
|
|
201
|
+
const withProv = `(${state.model.provider}) ${r2Plain}`;
|
|
202
|
+
if (visibleWidth(l2Plain) + 2 + visibleWidth(withProv) <= width) {
|
|
203
|
+
r2Plain = withProv;
|
|
204
|
+
r2Styled = theme.fg("dim", `(${state.model.provider}) `) + r2Styled;
|
|
235
205
|
}
|
|
236
206
|
}
|
|
237
|
-
const
|
|
207
|
+
const line2 = assembleLine(width, l2Plain, l2Styled, r2Plain, r2Styled);
|
|
208
|
+
const lines = [line1, line2];
|
|
238
209
|
// Add extension statuses on a single line, sorted by key alphabetically
|
|
239
210
|
const extensionStatuses = this.footerData.getExtensionStatuses();
|
|
240
211
|
if (extensionStatuses.size > 0) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"footer.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/footer.ts"],"names":[],"mappings":"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","sourcesContent":["import { type Component, truncateToWidth, visibleWidth } from \"@kolisachint/hoocode-tui\";\nimport type { AgentSession } from \"../../../core/agent-session.js\";\nimport type { ReadonlyFooterDataProvider } from \"../../../core/footer-data-provider.js\";\nimport { theme } from \"../theme/theme.js\";\n\n/**\n * Sanitize text for display in a single-line status.\n * Removes newlines, tabs, carriage returns, and other control characters.\n */\nfunction sanitizeStatusText(text: string): string {\n\t// Replace newlines, tabs, carriage returns with space, then collapse multiple spaces\n\treturn text\n\t\t.replace(/[\\r\\n\\t]/g, \" \")\n\t\t.replace(/ +/g, \" \")\n\t\t.trim();\n}\n\nfunction 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\n/**\n * Footer component that shows pwd, token stats, and context usage.\n * Computes token/context stats from session, gets git branch and extension statuses from provider.\n */\nexport class FooterComponent implements Component {\n\tprivate autoCompactEnabled = true;\n\n\tconstructor(\n\t\tprivate session: AgentSession,\n\t\tprivate footerData: ReadonlyFooterDataProvider,\n\t) {}\n\n\tsetSession(session: AgentSession): void {\n\t\tthis.session = session;\n\t}\n\n\tsetAutoCompactEnabled(enabled: boolean): void {\n\t\tthis.autoCompactEnabled = enabled;\n\t}\n\n\t/**\n\t * No-op: git branch caching now handled by provider.\n\t * Kept for compatibility with existing call sites in interactive-mode.\n\t */\n\tinvalidate(): void {\n\t\t// No-op: git branch is cached/invalidated by provider\n\t}\n\n\t/**\n\t * Clean up resources.\n\t * Git watcher cleanup now handled by provider.\n\t */\n\tdispose(): void {\n\t\t// Git watcher cleanup handled by provider\n\t}\n\n\trender(width: number): string[] {\n\t\tconst state = this.session.state;\n\n\t\t// Calculate cumulative usage from ALL session entries (not just post-compaction messages)\n\t\tlet totalInput = 0;\n\t\tlet totalOutput = 0;\n\t\tlet totalCacheRead = 0;\n\t\tlet totalCacheWrite = 0;\n\t\tlet totalCost = 0;\n\n\t\tfor (const entry of this.session.sessionManager.getEntries()) {\n\t\t\tif (entry.type === \"message\" && entry.message.role === \"assistant\") {\n\t\t\t\ttotalInput += entry.message.usage.input;\n\t\t\t\ttotalOutput += entry.message.usage.output;\n\t\t\t\ttotalCacheRead += entry.message.usage.cacheRead;\n\t\t\t\ttotalCacheWrite += entry.message.usage.cacheWrite;\n\t\t\t\ttotalCost += entry.message.usage.cost.total;\n\t\t\t}\n\t\t}\n\n\t\t// Calculate context usage from session (handles compaction correctly).\n\t\t// After compaction, tokens are unknown until the next LLM response.\n\t\tconst contextUsage = this.session.getContextUsage();\n\t\tconst contextWindow = contextUsage?.contextWindow ?? state.model?.contextWindow ?? 0;\n\t\tconst contextPercentValue = contextUsage?.percent ?? 0;\n\t\tconst contextPercent = contextUsage?.percent !== null ? contextPercentValue.toFixed(1) : \"?\";\n\n\t\t// Replace home directory with ~\n\t\tlet pwd = this.session.sessionManager.getCwd();\n\t\tconst home = process.env.HOME || process.env.USERPROFILE;\n\t\tif (home && pwd.startsWith(home)) {\n\t\t\tpwd = `~${pwd.slice(home.length)}`;\n\t\t}\n\n\t\t// Add git branch if available\n\t\tconst branch = this.footerData.getGitBranch();\n\t\tif (branch) {\n\t\t\tpwd = `${pwd} (${branch})`;\n\t\t}\n\n\t\t// Add session name if set\n\t\tconst sessionName = this.session.sessionManager.getSessionName();\n\t\tif (sessionName) {\n\t\t\tpwd = `${pwd} • ${sessionName}`;\n\t\t}\n\n\t\t// Format mode for display (e.g., build)\n\t\tconst modeLabel = this.footerData.getActiveMode();\n\n\t\t// Calculate widths for right-aligning mode\n\t\tconst pwdWidth = visibleWidth(pwd);\n\t\tconst modeWidth = visibleWidth(modeLabel);\n\t\tconst modePadding = Math.max(2, width - pwdWidth - modeWidth);\n\n\t\t// Build pwd line with mode right-aligned in accent color (if it fits)\n\t\tlet pwdLine: string;\n\t\tif (pwdWidth + modeWidth + 2 <= width) {\n\t\t\tpwdLine = pwd + \" \".repeat(modePadding) + theme.fg(\"accent\", modeLabel);\n\t\t} else {\n\t\t\t// Not enough space, just show pwd\n\t\t\tpwdLine = pwd;\n\t\t}\n\n\t\t// Build stats line\n\t\tconst statsParts = [];\n\t\tif (totalInput) statsParts.push(`↑${formatTokens(totalInput)}`);\n\t\tif (totalOutput) statsParts.push(`↓${formatTokens(totalOutput)}`);\n\t\tif (totalCacheRead) statsParts.push(`R${formatTokens(totalCacheRead)}`);\n\t\tif (totalCacheWrite) statsParts.push(`W${formatTokens(totalCacheWrite)}`);\n\n\t\t// Show cost with \"(sub)\" indicator if using OAuth subscription\n\t\tconst usingSubscription = state.model ? this.session.modelRegistry.isUsingOAuth(state.model) : false;\n\t\tif (totalCost || usingSubscription) {\n\t\t\tconst costStr = `$${totalCost.toFixed(3)}${usingSubscription ? \" (sub)\" : \"\"}`;\n\t\t\tstatsParts.push(costStr);\n\t\t}\n\n\t\t// Auto-compact threshold: compaction fires when context > (window - reserveTokens).\n\t\t// Show it so users see the actual trip point, not just the raw window percentage.\n\t\tlet thresholdPercent: number | undefined;\n\t\tif (this.autoCompactEnabled && contextWindow > 0) {\n\t\t\tconst reserveTokens = this.session.settingsManager.getCompactionSettings().reserveTokens;\n\t\t\tconst effective = contextWindow - reserveTokens;\n\t\t\tif (effective > 0) thresholdPercent = (effective / contextWindow) * 100;\n\t\t}\n\n\t\tconst autoIndicator =\n\t\t\tthresholdPercent !== undefined\n\t\t\t\t? ` (auto @ ${thresholdPercent.toFixed(0)}%)`\n\t\t\t\t: this.autoCompactEnabled\n\t\t\t\t\t? \" (auto)\"\n\t\t\t\t\t: \"\";\n\t\tconst contextPercentDisplay =\n\t\t\tcontextPercent === \"?\"\n\t\t\t\t? `?/${formatTokens(contextWindow)}${autoIndicator}`\n\t\t\t\t: `${contextPercent}%/${formatTokens(contextWindow)}${autoIndicator}`;\n\n\t\t// Color based on proximity to the auto-compact trip point.\n\t\t// With threshold known: error within 3pp, warning within 10pp. Without it: old fixed bands.\n\t\tlet contextPercentStr: string;\n\t\tconst errorLevel = thresholdPercent !== undefined ? thresholdPercent - 3 : 90;\n\t\tconst warnLevel = thresholdPercent !== undefined ? thresholdPercent - 10 : 70;\n\t\tif (contextPercentValue >= errorLevel) {\n\t\t\tcontextPercentStr = theme.fg(\"error\", contextPercentDisplay);\n\t\t} else if (contextPercentValue >= warnLevel) {\n\t\t\tcontextPercentStr = theme.fg(\"warning\", contextPercentDisplay);\n\t\t} else {\n\t\t\tcontextPercentStr = contextPercentDisplay;\n\t\t}\n\t\tstatsParts.push(contextPercentStr);\n\n\t\tlet statsLeft = statsParts.join(\" \");\n\n\t\t// Add model name on the right side, plus thinking level if model supports it\n\t\tconst modelName = state.model?.id || \"no-model\";\n\n\t\tlet statsLeftWidth = visibleWidth(statsLeft);\n\n\t\t// If statsLeft is too wide, truncate it\n\t\tif (statsLeftWidth > width) {\n\t\t\tstatsLeft = truncateToWidth(statsLeft, width, \"...\");\n\t\t\tstatsLeftWidth = visibleWidth(statsLeft);\n\t\t}\n\n\t\t// Calculate available space for padding (minimum 2 spaces between stats and model)\n\t\tconst minPadding = 2;\n\n\t\t// Add thinking level indicator if model supports reasoning\n\t\tlet rightSideWithoutProvider = modelName;\n\t\tif (state.model?.reasoning) {\n\t\t\tconst thinkingLevel = state.thinkingLevel || \"off\";\n\t\t\trightSideWithoutProvider =\n\t\t\t\tthinkingLevel === \"off\" ? `${modelName} • thinking off` : `${modelName} • ${thinkingLevel}`;\n\t\t}\n\n\t\t// Prepend the provider in parentheses if there are multiple providers and there's enough room\n\t\tlet rightSide = rightSideWithoutProvider;\n\t\tif (this.footerData.getAvailableProviderCount() > 1 && state.model) {\n\t\t\trightSide = `(${state.model!.provider}) ${rightSideWithoutProvider}`;\n\t\t\tif (statsLeftWidth + minPadding + visibleWidth(rightSide) > width) {\n\t\t\t\t// Too wide, fall back\n\t\t\t\trightSide = rightSideWithoutProvider;\n\t\t\t}\n\t\t}\n\n\t\tconst rightSideWidth = visibleWidth(rightSide);\n\t\tconst totalNeeded = statsLeftWidth + minPadding + rightSideWidth;\n\n\t\tlet statsLine: string;\n\t\tif (totalNeeded <= width) {\n\t\t\t// Both fit - add padding to right-align model\n\t\t\tconst padding = \" \".repeat(width - statsLeftWidth - rightSideWidth);\n\t\t\tstatsLine = statsLeft + padding + rightSide;\n\t\t} else {\n\t\t\t// Need to truncate right side\n\t\t\tconst availableForRight = width - statsLeftWidth - minPadding;\n\t\t\tif (availableForRight > 0) {\n\t\t\t\tconst truncatedRight = truncateToWidth(rightSide, availableForRight, \"\");\n\t\t\t\tconst truncatedRightWidth = visibleWidth(truncatedRight);\n\t\t\t\tconst padding = \" \".repeat(Math.max(0, width - statsLeftWidth - truncatedRightWidth));\n\t\t\t\tstatsLine = statsLeft + padding + truncatedRight;\n\t\t\t} else {\n\t\t\t\t// Not enough space for right side at all\n\t\t\t\tstatsLine = statsLeft;\n\t\t\t}\n\t\t}\n\n\t\t// Apply dim to each part separately. statsLeft may contain color codes (for context %)\n\t\t// that end with a reset, which would clear an outer dim wrapper. So we dim the parts\n\t\t// before and after the colored section independently.\n\t\tconst dimStatsLeft = theme.fg(\"dim\", statsLeft);\n\t\tconst remainder = statsLine.slice(statsLeft.length); // padding + rightSide\n\t\tconst dimRemainder = theme.fg(\"dim\", remainder);\n\n\t\t// Build the final pwd line: dim the path, accent the mode label (if present)\n\t\tlet finalPwdLine: string;\n\t\tif (pwdLine === pwd) {\n\t\t\t// Just pwd, no mode (or it didn't fit) - dim the whole thing\n\t\t\tfinalPwdLine = truncateToWidth(theme.fg(\"dim\", pwdLine), width, theme.fg(\"dim\", \"...\"));\n\t\t} else {\n\t\t\t// pwdLine has mode appended with accent color\n\t\t\t// Extract the plain text version for width calculation\n\t\t\tconst pwdLinePlain = pwd + \" \".repeat(modePadding) + modeLabel;\n\t\t\t// Truncate the plain text if needed\n\t\t\tconst truncatedPlain = truncateToWidth(pwdLinePlain, width, \"...\");\n\t\t\t// Check if mode was truncated out\n\t\t\tif (!truncatedPlain.includes(modeLabel)) {\n\t\t\t\t// Mode didn't fit after truncation, just show dimmed truncated pwd\n\t\t\t\tfinalPwdLine = theme.fg(\"dim\", truncatedPlain);\n\t\t\t} else {\n\t\t\t\t// Split at the mode part and apply colors\n\t\t\t\tconst pwdPart = truncatedPlain.slice(0, truncatedPlain.indexOf(modeLabel));\n\t\t\t\tfinalPwdLine = theme.fg(\"dim\", pwdPart) + theme.fg(\"accent\", modeLabel);\n\t\t\t}\n\t\t}\n\n\t\tconst lines = [finalPwdLine, dimStatsLeft + dimRemainder];\n\n\t\t// Add extension statuses on a single line, sorted by key alphabetically\n\t\tconst extensionStatuses = this.footerData.getExtensionStatuses();\n\t\tif (extensionStatuses.size > 0) {\n\t\t\tconst sortedStatuses = Array.from(extensionStatuses.entries())\n\t\t\t\t.sort(([a], [b]) => a.localeCompare(b))\n\t\t\t\t.map(([, text]) => sanitizeStatusText(text));\n\t\t\tconst statusLine = sortedStatuses.join(\" \");\n\t\t\t// Truncate to terminal width with dim ellipsis for consistency with footer style\n\t\t\tlines.push(truncateToWidth(statusLine, width, theme.fg(\"dim\", \"...\")));\n\t\t}\n\n\t\treturn lines;\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"footer.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/footer.ts"],"names":[],"mappings":"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","sourcesContent":["import { type Component, truncateToWidth, visibleWidth } from \"@kolisachint/hoocode-tui\";\nimport type { AgentSession } from \"../../../core/agent-session.js\";\nimport type { ReadonlyFooterDataProvider } from \"../../../core/footer-data-provider.js\";\nimport { taskStore } from \"../../../core/task-store.js\";\nimport { BRAND_MARK, GIT_BRANCH_GLYPH } from \"../brand.js\";\nimport { theme } from \"../theme/theme.js\";\n\n/**\n * Assemble one footer line: `left` flush left, `right` flush right when it fits\n * (≥2 cols between), padded to the full width. When it doesn't fit, drop `right`\n * and pad; when even `left` overflows, truncate it. Width math runs on the plain\n * strings; the styled strings carry the colour. Every returned line is exactly\n * `width` cells or fewer — the invariant the footer-width tests hold us to.\n */\nfunction assembleLine(width: number, leftPlain: string, leftStyled: string, rightPlain = \"\", rightStyled = \"\"): string {\n\tconst lw = visibleWidth(leftPlain);\n\tif (rightPlain && lw + 2 + visibleWidth(rightPlain) <= width) {\n\t\treturn leftStyled + \" \".repeat(width - lw - visibleWidth(rightPlain)) + rightStyled;\n\t}\n\tif (lw <= width) return leftStyled + \" \".repeat(width - lw);\n\treturn truncateToWidth(leftStyled, width, theme.fg(\"dim\", \"…\"));\n}\n\n/** A compact context-fill gauge, coloured by proximity to the auto-compact trip point. */\nfunction contextGauge(percent: number, errorLevel: number, warnLevel: number): { plain: string; styled: string } {\n\tconst CELLS = 8;\n\tconst filled = Math.max(0, Math.min(CELLS, Math.round((percent / 100) * CELLS)));\n\tconst fill = \"▰\".repeat(filled);\n\tconst track = \"▱\".repeat(CELLS - filled);\n\tconst color = percent >= errorLevel ? \"error\" : percent >= warnLevel ? \"warning\" : \"accent\";\n\treturn { plain: fill + track, styled: theme.fg(color, fill) + theme.fg(\"dim\", track) };\n}\n\n/** Count subagent runs currently in flight, for the footer's live delegation cue. */\nfunction activeSubagentCount(): number {\n\treturn taskStore.list().filter((t) => t.source === \"subagent\" && t.status === \"in_progress\").length;\n}\n\n/**\n * Sanitize text for display in a single-line status.\n * Removes newlines, tabs, carriage returns, and other control characters.\n */\nfunction sanitizeStatusText(text: string): string {\n\t// Replace newlines, tabs, carriage returns with space, then collapse multiple spaces\n\treturn text\n\t\t.replace(/[\\r\\n\\t]/g, \" \")\n\t\t.replace(/ +/g, \" \")\n\t\t.trim();\n}\n\nfunction 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\n/**\n * Footer component that shows pwd, token stats, and context usage.\n * Computes token/context stats from session, gets git branch and extension statuses from provider.\n */\nexport class FooterComponent implements Component {\n\tprivate autoCompactEnabled = true;\n\n\tconstructor(\n\t\tprivate session: AgentSession,\n\t\tprivate footerData: ReadonlyFooterDataProvider,\n\t) {}\n\n\tsetSession(session: AgentSession): void {\n\t\tthis.session = session;\n\t}\n\n\tsetAutoCompactEnabled(enabled: boolean): void {\n\t\tthis.autoCompactEnabled = enabled;\n\t}\n\n\t/**\n\t * No-op: git branch caching now handled by provider.\n\t * Kept for compatibility with existing call sites in interactive-mode.\n\t */\n\tinvalidate(): void {\n\t\t// No-op: git branch is cached/invalidated by provider\n\t}\n\n\t/**\n\t * Clean up resources.\n\t * Git watcher cleanup now handled by provider.\n\t */\n\tdispose(): void {\n\t\t// Git watcher cleanup handled by provider\n\t}\n\n\trender(width: number): string[] {\n\t\tconst state = this.session.state;\n\n\t\t// Calculate cumulative usage from ALL session entries (not just post-compaction messages)\n\t\tlet totalInput = 0;\n\t\tlet totalOutput = 0;\n\t\tlet totalCacheRead = 0;\n\t\tlet totalCacheWrite = 0;\n\t\tlet totalCost = 0;\n\n\t\tfor (const entry of this.session.sessionManager.getEntries()) {\n\t\t\tif (entry.type === \"message\" && entry.message.role === \"assistant\") {\n\t\t\t\ttotalInput += entry.message.usage.input;\n\t\t\t\ttotalOutput += entry.message.usage.output;\n\t\t\t\ttotalCacheRead += entry.message.usage.cacheRead;\n\t\t\t\ttotalCacheWrite += entry.message.usage.cacheWrite;\n\t\t\t\ttotalCost += entry.message.usage.cost.total;\n\t\t\t}\n\t\t}\n\n\t\t// Calculate context usage from session (handles compaction correctly).\n\t\t// After compaction, tokens are unknown until the next LLM response.\n\t\tconst contextUsage = this.session.getContextUsage();\n\t\tconst contextWindow = contextUsage?.contextWindow ?? state.model?.contextWindow ?? 0;\n\t\tconst contextPercentValue = contextUsage?.percent ?? 0;\n\t\tconst contextPercent = contextUsage?.percent !== null ? contextPercentValue.toFixed(1) : \"?\";\n\n\t\t// Replace home directory with ~\n\t\tlet pwd = this.session.sessionManager.getCwd();\n\t\tconst home = process.env.HOME || process.env.USERPROFILE;\n\t\tif (home && pwd.startsWith(home)) {\n\t\t\tpwd = `~${pwd.slice(home.length)}`;\n\t\t}\n\t\tconst branch = this.footerData.getGitBranch();\n\t\tconst sessionName = this.session.sessionManager.getSessionName();\n\t\tconst modeLabel = this.footerData.getActiveMode();\n\n\t\t// ── Line 1 — identity & location ────────────────────────────────────────\n\t\t// Lead with the brand mark + MODE (the agent's guardrail: Ask/Plan/Build/\n\t\t// Debug) in bold accent so it is the first thing the eye lands on, then the\n\t\t// path, git branch, and session name in descending emphasis. The live\n\t\t// subagent count sits flush right — present only while work is delegated.\n\t\tconst modeUp = modeLabel.toUpperCase();\n\t\tconst brand = `${BRAND_MARK} ${modeUp}`;\n\t\tlet l1Plain = `${brand} ${pwd}`;\n\t\tlet l1Styled = `${theme.bold(theme.fg(\"accent\", brand))} ${theme.fg(\"muted\", pwd)}`;\n\t\tif (branch) {\n\t\t\tl1Plain += ` ${GIT_BRANCH_GLYPH} ${branch}`;\n\t\t\tl1Styled += ` ${theme.fg(\"dim\", GIT_BRANCH_GLYPH)} ${theme.fg(\"muted\", branch)}`;\n\t\t}\n\t\tif (sessionName) {\n\t\t\tl1Plain += ` • ${sessionName}`;\n\t\t\tl1Styled += theme.fg(\"dim\", ` • ${sessionName}`);\n\t\t}\n\t\tconst nSub = activeSubagentCount();\n\t\tconst l1RightPlain = nSub > 0 ? `◇${nSub} running` : \"\";\n\t\tconst l1RightStyled = nSub > 0 ? theme.fg(\"accent\", `◇${nSub}`) + theme.fg(\"dim\", \" running\") : \"\";\n\t\tconst line1 = assembleLine(width, l1Plain, l1Styled, l1RightPlain, l1RightStyled);\n\n\t\t// ── Line 2 — session vitals ─────────────────────────────────────────────\n\t\t// A context-fill gauge (coloured by proximity to the auto-compact trip\n\t\t// point) leads, then token/cost deltas, with the model + thinking level\n\t\t// flush right. Numbers read in muted, labels/arrows in dim — a legible\n\t\t// hierarchy in place of the old uniform grey.\n\t\tlet thresholdPercent: number | undefined;\n\t\tif (this.autoCompactEnabled && contextWindow > 0) {\n\t\t\tconst reserveTokens = this.session.settingsManager.getCompactionSettings().reserveTokens;\n\t\t\tconst effective = contextWindow - reserveTokens;\n\t\t\tif (effective > 0) thresholdPercent = (effective / contextWindow) * 100;\n\t\t}\n\t\tconst errorLevel = thresholdPercent !== undefined ? thresholdPercent - 3 : 90;\n\t\tconst warnLevel = thresholdPercent !== undefined ? thresholdPercent - 10 : 70;\n\t\tconst autoIndicator =\n\t\t\tthresholdPercent !== undefined\n\t\t\t\t? ` auto@${thresholdPercent.toFixed(0)}%`\n\t\t\t\t: this.autoCompactEnabled\n\t\t\t\t\t? \" auto\"\n\t\t\t\t\t: \"\";\n\n\t\tconst gauge = contextGauge(contextPercentValue, errorLevel, warnLevel);\n\t\tconst pctText = contextPercent === \"?\" ? \"?\" : `${contextPercent}%`;\n\t\tconst pctColor =\n\t\t\tcontextPercentValue >= errorLevel ? \"error\" : contextPercentValue >= warnLevel ? \"warning\" : \"muted\";\n\t\tconst winText = `${formatTokens(contextWindow)}${autoIndicator}`;\n\n\t\tconst segs: Array<{ plain: string; styled: string }> = [\n\t\t\t{\n\t\t\t\tplain: `${gauge.plain} ${pctText} ${winText}`,\n\t\t\t\tstyled: `${gauge.styled} ${theme.fg(pctColor, pctText)} ${theme.fg(\"dim\", winText)}`,\n\t\t\t},\n\t\t];\n\t\tconst arrow = (a: string, n: number) => ({\n\t\t\tplain: `${a}${formatTokens(n)}`,\n\t\t\tstyled: theme.fg(\"dim\", a) + theme.fg(\"muted\", formatTokens(n)),\n\t\t});\n\t\tif (totalInput) segs.push(arrow(\"↑\", totalInput));\n\t\tif (totalOutput) segs.push(arrow(\"↓\", totalOutput));\n\t\tif (totalCacheRead) segs.push(arrow(\"R\", totalCacheRead));\n\t\tif (totalCacheWrite) segs.push(arrow(\"W\", totalCacheWrite));\n\t\tconst usingSubscription = state.model ? this.session.modelRegistry.isUsingOAuth(state.model) : false;\n\t\tif (totalCost || usingSubscription) {\n\t\t\tconst costStr = `$${totalCost.toFixed(3)}${usingSubscription ? \" (sub)\" : \"\"}`;\n\t\t\tsegs.push({ plain: costStr, styled: theme.fg(\"muted\", costStr) });\n\t\t}\n\t\tconst l2Plain = segs.map((s) => s.plain).join(\" \");\n\t\tconst l2Styled = segs.map((s) => s.styled).join(\" \");\n\n\t\t// Right: model, thinking level, and provider (when several are configured).\n\t\tconst modelName = state.model?.id || \"no-model\";\n\t\tlet r2Plain = modelName;\n\t\tlet r2Styled = theme.fg(\"muted\", modelName);\n\t\tif (state.model?.reasoning) {\n\t\t\tconst tl = state.thinkingLevel || \"off\";\n\t\t\tconst tstr = tl === \"off\" ? \"thinking off\" : tl;\n\t\t\tr2Plain += ` • ${tstr}`;\n\t\t\tr2Styled += theme.fg(\"dim\", ` • ${tstr}`);\n\t\t}\n\t\tif (this.footerData.getAvailableProviderCount() > 1 && state.model) {\n\t\t\t// Prepend the provider only when the whole right cluster still fits.\n\t\t\tconst withProv = `(${state.model.provider}) ${r2Plain}`;\n\t\t\tif (visibleWidth(l2Plain) + 2 + visibleWidth(withProv) <= width) {\n\t\t\t\tr2Plain = withProv;\n\t\t\t\tr2Styled = theme.fg(\"dim\", `(${state.model.provider}) `) + r2Styled;\n\t\t\t}\n\t\t}\n\t\tconst line2 = assembleLine(width, l2Plain, l2Styled, r2Plain, r2Styled);\n\n\t\tconst lines = [line1, line2];\n\n\t\t// Add extension statuses on a single line, sorted by key alphabetically\n\t\tconst extensionStatuses = this.footerData.getExtensionStatuses();\n\t\tif (extensionStatuses.size > 0) {\n\t\t\tconst sortedStatuses = Array.from(extensionStatuses.entries())\n\t\t\t\t.sort(([a], [b]) => a.localeCompare(b))\n\t\t\t\t.map(([, text]) => sanitizeStatusText(text));\n\t\t\tconst statusLine = sortedStatuses.join(\" \");\n\t\t\t// Truncate to terminal width with dim ellipsis for consistency with footer style\n\t\t\tlines.push(truncateToWidth(statusLine, width, theme.fg(\"dim\", \"...\")));\n\t\t}\n\n\t\treturn lines;\n\t}\n}\n"]}
|
|
@@ -15,11 +15,12 @@ export type TaskPanelView = "flat" | "subagents" | "teams";
|
|
|
15
15
|
* done/total count on the left, the per-turn token/elapsed/cost delta on the right.
|
|
16
16
|
* - Shows all tasks with all statuses (pending / in_progress / done / failed).
|
|
17
17
|
* The active row animates a braille spinner; pending rows read `queued`.
|
|
18
|
-
* - A single-cell owner glyph (
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* title names the owner: the subagent type
|
|
22
|
-
* ("[planner]", fed by `--team <url>`), or the
|
|
18
|
+
* - A single-cell owner glyph (◇ subagent / ▸ team role / ⧉ MCP) sits before
|
|
19
|
+
* the id where it disambiguates; main-owned rows carry no glyph in the flat
|
|
20
|
+
* lens (the rail already attributes the pane to the main agent). A text
|
|
21
|
+
* origin tag before the title names the owner: the subagent type
|
|
22
|
+
* ("[explore]"), the team role ("[planner]", fed by `--team <url>`), or the
|
|
23
|
+
* MCP server ("[github]").
|
|
23
24
|
* - Three views split by ownership (cycled via app.tasks.cycleView, shown as a
|
|
24
25
|
* `tasks · subagents · teams` switcher in the header):
|
|
25
26
|
* - flat ("tasks") — the main agent's own TodoWrite plan, with each
|