apex-code 0.0.1-alpha.10 → 0.0.1-alpha.11
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 +11 -0
- package/README.md +9 -8
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +24 -5
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session.d.ts +26 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +77 -1
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/bash-executor.d.ts.map +1 -1
- package/dist/core/bash-executor.js +7 -0
- package/dist/core/bash-executor.js.map +1 -1
- package/dist/core/checkpoints/git-checkpoints.d.ts +33 -0
- package/dist/core/checkpoints/git-checkpoints.d.ts.map +1 -0
- package/dist/core/checkpoints/git-checkpoints.js +230 -0
- package/dist/core/checkpoints/git-checkpoints.js.map +1 -0
- package/dist/core/checkpoints/session-checkpoints.d.ts +23 -0
- package/dist/core/checkpoints/session-checkpoints.d.ts.map +1 -0
- package/dist/core/checkpoints/session-checkpoints.js +29 -0
- package/dist/core/checkpoints/session-checkpoints.js.map +1 -0
- package/dist/core/exec.d.ts.map +1 -1
- package/dist/core/exec.js +5 -2
- package/dist/core/exec.js.map +1 -1
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +7 -1
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/mcp/config.d.ts +16 -0
- package/dist/core/mcp/config.d.ts.map +1 -0
- package/dist/core/mcp/config.js +152 -0
- package/dist/core/mcp/config.js.map +1 -0
- package/dist/core/mcp/connector.d.ts +10 -0
- package/dist/core/mcp/connector.d.ts.map +1 -0
- package/dist/core/mcp/connector.js +70 -0
- package/dist/core/mcp/connector.js.map +1 -0
- package/dist/core/mcp/contract.d.ts +19 -0
- package/dist/core/mcp/contract.d.ts.map +1 -0
- package/dist/core/mcp/contract.js +89 -0
- package/dist/core/mcp/contract.js.map +1 -0
- package/dist/core/mcp/mcp-tool.d.ts +32 -0
- package/dist/core/mcp/mcp-tool.d.ts.map +1 -0
- package/dist/core/mcp/mcp-tool.js +165 -0
- package/dist/core/mcp/mcp-tool.js.map +1 -0
- package/dist/core/mcp/metadata-cache.d.ts +29 -0
- package/dist/core/mcp/metadata-cache.d.ts.map +1 -0
- package/dist/core/mcp/metadata-cache.js +99 -0
- package/dist/core/mcp/metadata-cache.js.map +1 -0
- package/dist/core/mcp/runtime.d.ts +18 -0
- package/dist/core/mcp/runtime.d.ts.map +1 -0
- package/dist/core/mcp/runtime.js +35 -0
- package/dist/core/mcp/runtime.js.map +1 -0
- package/dist/core/mcp/schema.d.ts +22 -0
- package/dist/core/mcp/schema.d.ts.map +1 -0
- package/dist/core/mcp/schema.js +20 -0
- package/dist/core/mcp/schema.js.map +1 -0
- package/dist/core/mcp/server-manager.d.ts +43 -0
- package/dist/core/mcp/server-manager.d.ts.map +1 -0
- package/dist/core/mcp/server-manager.js +142 -0
- package/dist/core/mcp/server-manager.js.map +1 -0
- package/dist/core/mcp/types.d.ts +98 -0
- package/dist/core/mcp/types.d.ts.map +1 -0
- package/dist/core/mcp/types.js +8 -0
- package/dist/core/mcp/types.js.map +1 -0
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +14 -1
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-manager.d.ts +13 -1
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +30 -3
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/settings-manager.d.ts +7 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +3 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +1 -0
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/tools/contract-snapshot.d.ts +58 -0
- package/dist/core/tools/contract-snapshot.d.ts.map +1 -0
- package/dist/core/tools/contract-snapshot.js +51 -0
- package/dist/core/tools/contract-snapshot.js.map +1 -0
- package/dist/core/tools/contract.d.ts +4 -2
- package/dist/core/tools/contract.d.ts.map +1 -1
- package/dist/core/tools/contract.js +4 -2
- package/dist/core/tools/contract.js.map +1 -1
- package/dist/core/tools/index.d.ts +9 -0
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +3 -0
- package/dist/core/tools/index.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +6 -6
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/assistant-message.d.ts +29 -0
- package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/assistant-message.js +182 -23
- package/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/dist/modes/interactive/components/bash-execution.d.ts +7 -0
- package/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/bash-execution.js +20 -0
- package/dist/modes/interactive/components/bash-execution.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts +8 -0
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +29 -9
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts +12 -0
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +31 -0
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +29 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +88 -4
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/package-manager-cli.d.ts.map +1 -1
- package/dist/package-manager-cli.js +0 -6
- package/dist/package-manager-cli.js.map +1 -1
- package/dist/utils/version-check.d.ts.map +1 -1
- package/dist/utils/version-check.js +6 -4
- package/dist/utils/version-check.js.map +1 -1
- package/docs/compaction.md +2 -0
- package/docs/environment-variables.md +1 -1
- package/examples/extensions/git-checkpoint.ts +24 -36
- package/npm-shrinkwrap.json +176 -28
- package/package.json +10 -8
- package/dist/server/create-harness.d.ts +0 -26
- package/dist/server/create-harness.d.ts.map +0 -1
- package/dist/server/create-harness.js +0 -107
- package/dist/server/create-harness.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"footer.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/footer.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,SAAS,EAAiC,MAAM,wBAAwB,CAAC;AACvF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAEnE,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,uCAAuC,CAAC;AACxF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AAgBzE;;GAEG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAMlD;AAKD;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CACjC,OAAO,EAAE,MAAM,GAAG,SAAS,EAC3B,YAAY,EAAE,SAAS,GAAG,OAAO,GAC/B;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAWnC;AAED,mFAAmF;AACnF,MAAM,WAAW,2BAA2B;IAC3C,eAAe,IAAI,SAAS,GAAG,OAAO,CAAC;IACvC,iBAAiB,IAAI,OAAO,CAAC;IAC7B,oBAAoB,IAAI,KAAK,GAAG,SAAS,GAAG,MAAM,CAAC;CACnD;AAQD,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAYhF;AAED;;;GAGG;AACH,qBAAa,eAAgB,YAAW,SAAS;IAChD,OAAO,CAAC,kBAAkB,CAAQ;IAClC,OAAO,CAAC,cAAc,CAA6B;IACnD,OAAO,CAAC,OAAO,CAAe;IAC9B,OAAO,CAAC,UAAU,CAA6B;IAC/C,OAAO,CAAC,qBAAqB,CAA8B;IAE3D,YACC,OAAO,EAAE,YAAY,EACrB,UAAU,EAAE,0BAA0B,EACtC,qBAAqB,GAAE,2BAA4D,EAKnF;IAED,UAAU,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI,CAEtC;IAED,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAE5C;IAED,iBAAiB,CAAC,IAAI,EAAE,cAAc,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,CAmT9B;CACD","sourcesContent":["import { isAbsolute, relative, resolve, sep } from \"node:path\";\nimport { type Component, truncateToWidth, visibleWidth } from \"@earendil-works/pi-tui\";\nimport type { AgentSession } from \"../../../core/agent-session.ts\";\nimport { areExperimentalFeaturesEnabled } from \"../../../core/experimental.ts\";\nimport type { ReadonlyFooterDataProvider } from \"../../../core/footer-data-provider.ts\";\nimport type { PermissionMode } from \"../../../core/permissions/store.ts\";\nimport { addUsageToTotals, createUsageTotals } from \"../../../core/usage-totals.ts\";\nimport { theme } from \"../theme/theme.ts\";\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\n/**\n * Format token counts for compact footer display.\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\n/** Cells in the context gauge. Eight reads as a bar without crowding the tray. */\nconst CONTEXT_GAUGE_CELLS = 8;\n\n/**\n * A filled bar for the context window, split into its filled and empty runs so\n * the caller can colour them separately.\n *\n * This is a redundant channel and never the only one: the percentage beside it\n * states the same value as text (WCAG 1.4.1). An unknown percentage renders an\n * empty track rather than guessing a position.\n */\nexport function renderContextGauge(\n\tpercent: number | undefined,\n\tsymbolPreset: \"unicode\" | \"ascii\",\n): { filled: string; empty: string } {\n\tconst filledGlyph = symbolPreset === \"ascii\" ? \"#\" : \"█\";\n\tconst emptyGlyph = symbolPreset === \"ascii\" ? \"-\" : \"░\";\n\tif (percent === undefined) {\n\t\treturn { filled: \"\", empty: emptyGlyph.repeat(CONTEXT_GAUGE_CELLS) };\n\t}\n\tconst clamped = Math.min(100, Math.max(0, percent));\n\t// Any non-zero usage lights at least one cell, so the bar never reads empty\n\t// while the window is genuinely filling.\n\tconst cells = clamped === 0 ? 0 : Math.max(1, Math.round((clamped / 100) * CONTEXT_GAUGE_CELLS));\n\treturn { filled: filledGlyph.repeat(cells), empty: emptyGlyph.repeat(CONTEXT_GAUGE_CELLS - cells) };\n}\n\n/** Accessibility/display settings the footer reads (roadmap Phase 8, task 8.6). */\nexport interface FooterAccessibilitySettings {\n\tgetSymbolPreset(): \"unicode\" | \"ascii\";\n\tgetColorBlindMode(): boolean;\n\tgetTokenUsageDisplay(): \"off\" | \"compact\" | \"full\";\n}\n\nconst DEFAULT_ACCESSIBILITY_SETTINGS: FooterAccessibilitySettings = {\n\tgetSymbolPreset: () => \"unicode\",\n\tgetColorBlindMode: () => false,\n\tgetTokenUsageDisplay: () => \"compact\",\n};\n\nexport function formatCwdForFooter(cwd: string, home: string | undefined): string {\n\tif (!home) return cwd;\n\n\tconst resolvedCwd = resolve(cwd);\n\tconst resolvedHome = resolve(home);\n\tconst relativeToHome = relative(resolvedHome, resolvedCwd);\n\tconst isInsideHome =\n\t\trelativeToHome === \"\" ||\n\t\t(relativeToHome !== \"..\" && !relativeToHome.startsWith(`..${sep}`) && !isAbsolute(relativeToHome));\n\n\tif (!isInsideHome) return cwd;\n\treturn relativeToHome === \"\" ? \"~\" : `~/${relativeToHome.replaceAll(\"\\\\\", \"/\")}`;\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\tprivate permissionMode: PermissionMode = \"default\";\n\tprivate session: AgentSession;\n\tprivate footerData: ReadonlyFooterDataProvider;\n\tprivate accessibilitySettings: FooterAccessibilitySettings;\n\n\tconstructor(\n\t\tsession: AgentSession,\n\t\tfooterData: ReadonlyFooterDataProvider,\n\t\taccessibilitySettings: FooterAccessibilitySettings = DEFAULT_ACCESSIBILITY_SETTINGS,\n\t) {\n\t\tthis.session = session;\n\t\tthis.footerData = footerData;\n\t\tthis.accessibilitySettings = accessibilitySettings;\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\tsetPermissionMode(mode: PermissionMode): void {\n\t\tthis.permissionMode = mode;\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\t\tconst symbolPreset = this.accessibilitySettings.getSymbolPreset();\n\t\tconst colorBlindMode = this.accessibilitySettings.getColorBlindMode();\n\t\tconst tokenUsageDisplay = this.accessibilitySettings.getTokenUsageDisplay();\n\t\tconst upSymbol = symbolPreset === \"ascii\" ? \"^\" : \"↑\";\n\t\tconst downSymbol = symbolPreset === \"ascii\" ? \"v\" : \"↓\";\n\t\tconst bulletSymbol = symbolPreset === \"ascii\" ? \"-\" : \"•\";\n\n\t\t// Calculate cumulative usage from ALL session entries (not just post-compaction messages)\n\t\tconst usageTotals = createUsageTotals();\n\t\tlet latestCacheHitRate: number | undefined;\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\taddUsageToTotals(usageTotals, entry.message.usage);\n\n\t\t\t\tconst latestPromptTokens =\n\t\t\t\t\tentry.message.usage.input + entry.message.usage.cacheRead + entry.message.usage.cacheWrite;\n\t\t\t\tlatestCacheHitRate =\n\t\t\t\t\tlatestPromptTokens > 0 ? (entry.message.usage.cacheRead / latestPromptTokens) * 100 : undefined;\n\t\t\t} else if (entry.type === \"message\" && entry.message.role === \"toolResult\" && entry.message.usage) {\n\t\t\t\taddUsageToTotals(usageTotals, entry.message.usage);\n\t\t\t} else if ((entry.type === \"branch_summary\" || entry.type === \"compaction\") && entry.usage) {\n\t\t\t\taddUsageToTotals(usageTotals, entry.usage);\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 = formatCwdForFooter(this.session.sessionManager.getCwd(), process.env.HOME || process.env.USERPROFILE);\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} ${bulletSymbol} ${sessionName}`;\n\t\t}\n\n\t\t// Build stats line. tokenUsageDisplay: \"off\" hides this whole section\n\t\t// (context% and model name are separate and always shown); \"full\" shows\n\t\t// exact counts instead of formatTokens' abbreviation.\n\t\tconst formatTokenCount = tokenUsageDisplay === \"full\" ? (n: number) => n.toLocaleString() : formatTokens;\n\t\tconst statsParts = [];\n\t\tif (tokenUsageDisplay !== \"off\") {\n\t\t\tif (usageTotals.input) statsParts.push(`${upSymbol}${formatTokenCount(usageTotals.input)}`);\n\t\t\tif (usageTotals.output) statsParts.push(`${downSymbol}${formatTokenCount(usageTotals.output)}`);\n\t\t\tif (usageTotals.cacheRead) statsParts.push(`R${formatTokenCount(usageTotals.cacheRead)}`);\n\t\t\tif (usageTotals.cacheWrite) statsParts.push(`W${formatTokenCount(usageTotals.cacheWrite)}`);\n\t\t\tif ((usageTotals.cacheRead > 0 || usageTotals.cacheWrite > 0) && latestCacheHitRate !== undefined) {\n\t\t\t\tstatsParts.push(`CH${latestCacheHitRate.toFixed(1)}%`);\n\t\t\t}\n\t\t}\n\n\t\t// Kimi Coding is subscription-backed despite using API-key authentication.\n\t\tconst usingSubscription = state.model\n\t\t\t? state.model.provider === \"kimi-coding\" || this.session.modelRuntime.isUsingSubscription(state.model.provider)\n\t\t\t: false;\n\t\tif (tokenUsageDisplay !== \"off\" && (usageTotals.cost || usingSubscription)) {\n\t\t\tconst costStr = `$${usageTotals.cost.toFixed(3)}${usingSubscription ? \" (sub)\" : \"\"}`;\n\t\t\tstatsParts.push(costStr);\n\t\t}\n\n\t\t// Context pressure is signalled through text at every setting -- never\n\t\t// color alone (WCAG 1.4.1) -- with color as an additional channel. The\n\t\t// default error/warning pair (red/amber) is already distinguishable for\n\t\t// most red-green colorblindness; colorBlindMode's palette adjustment\n\t\t// moves the critical (>90%) threshold to \"accent\" -- a hue outside that\n\t\t// red/amber family entirely -- rather than relying on a style attribute\n\t\t// like bold/underline, which several terminals (and this theme's own\n\t\t// chalk-backed style methods, suppressed outside a real TTY) silently\n\t\t// drop, making it an unreliable second channel.\n\t\tlet contextPercentStr: string;\n\t\tconst autoIndicator = this.autoCompactEnabled ? \" (auto)\" : \"\";\n\t\tconst pressureMarker = contextPercentValue > 90 ? \"!!\" : contextPercentValue > 70 ? \"!\" : \"\";\n\t\tconst contextPercentDisplay =\n\t\t\tcontextPercent === \"?\"\n\t\t\t\t? `?/${formatTokens(contextWindow)}${autoIndicator}`\n\t\t\t\t: `${contextPercent}%${pressureMarker}/${formatTokens(contextWindow)}${autoIndicator}`;\n\t\tif (contextPercentValue > 90) {\n\t\t\tcontextPercentStr = theme.fg(colorBlindMode ? \"accent\" : \"error\", contextPercentDisplay);\n\t\t} else if (contextPercentValue > 70) {\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\t\tif (areExperimentalFeaturesEnabled()) {\n\t\t\tstatsParts.push(`${theme.fg(\"dim\", bulletSymbol)} ${theme.bold(theme.fg(\"warning\", \"xp\"))}`);\n\t\t}\n\n\t\t// Permission posture is always named, including `default`. It is operational\n\t\t// state rather than routine telemetry: hiding the safe default would make the\n\t\t// tray change meaning by omission, while hiding bypass would conceal risk.\n\t\t// Text carries the signal and color remains a second channel (WCAG 1.4.1).\n\t\tconst modeColor =\n\t\t\tthis.permissionMode === \"bypassPermissions\" ? \"error\" : this.permissionMode === \"default\" ? \"dim\" : \"warning\";\n\t\tstatsParts.unshift(theme.fg(modeColor, this.permissionMode));\n\n\t\tif (tokenUsageDisplay !== \"full\") {\n\t\t\tconst separator = theme.fg(\"dim\", symbolPreset === \"ascii\" ? \" - \" : \" · \");\n\t\t\tconst compactPermissionNames: Record<PermissionMode, string> = {\n\t\t\t\tdefault: \"default\",\n\t\t\t\tplan: \"plan\",\n\t\t\t\tacceptEdits: \"accept\",\n\t\t\t\tbypassPermissions: \"bypass\",\n\t\t\t\tdontAsk: \"dontAsk\",\n\t\t\t};\n\t\t\tconst permissionColor =\n\t\t\t\tthis.permissionMode === \"bypassPermissions\"\n\t\t\t\t\t? \"error\"\n\t\t\t\t\t: this.permissionMode === \"default\"\n\t\t\t\t\t\t? \"dim\"\n\t\t\t\t\t\t: \"warning\";\n\t\t\tconst permissionFull = theme.fg(permissionColor, this.permissionMode);\n\t\t\tconst permissionCompact = theme.fg(permissionColor, compactPermissionNames[this.permissionMode]);\n\t\t\tconst compactPercent = contextPercent === \"?\" ? \"?\" : contextPercentValue.toFixed(0);\n\t\t\tconst compactPressure = contextPercentValue > 90 ? \"!!\" : contextPercentValue > 70 ? \"!\" : \"\";\n\t\t\tconst contextColor =\n\t\t\t\tcontextPercentValue > 90\n\t\t\t\t\t? colorBlindMode\n\t\t\t\t\t\t? \"accent\"\n\t\t\t\t\t\t: \"error\"\n\t\t\t\t\t: contextPercentValue > 70\n\t\t\t\t\t\t? \"warning\"\n\t\t\t\t\t\t: \"dim\";\n\t\t\t// The gauge rides along only in the roomy form. The compact form drops\n\t\t\t// it, which is what lets it keep winning the fit ladder below.\n\t\t\tconst gauge = renderContextGauge(contextPercent === \"?\" ? undefined : contextPercentValue, symbolPreset);\n\t\t\t// Below the warning threshold the text is dim, which would make the bar\n\t\t\t// invisible; the fill takes the accent there and tracks the text colour\n\t\t\t// at every level that actually signals pressure.\n\t\t\tconst gaugeColor = contextColor === \"dim\" ? \"accent\" : contextColor;\n\t\t\t// An empty run would still emit an open/close SGR pair, so skip it.\n\t\t\tconst paintRun = (color: Parameters<typeof theme.fg>[0], text: string) => (text ? theme.fg(color, text) : \"\");\n\t\t\tconst contextGauged =\n\t\t\t\ttheme.fg(contextColor, \"context \") +\n\t\t\t\tpaintRun(gaugeColor, gauge.filled) +\n\t\t\t\tpaintRun(\"borderMuted\", gauge.empty) +\n\t\t\t\ttheme.fg(contextColor, ` ${contextPercent}%${pressureMarker}`);\n\t\t\tconst contextFull = theme.fg(contextColor, `context ${contextPercent}%${pressureMarker}`);\n\t\t\tconst contextCompact = theme.fg(contextColor, `ctx ${compactPercent}%${compactPressure}`);\n\n\t\t\tconst joinSegments = (segments: string[]): string => segments.join(separator);\n\t\t\tconst fits = (segments: string[]): boolean => visibleWidth(joinSegments(segments)) <= width;\n\t\t\tlet left: string[] = [permissionFull];\n\t\t\tlet right = contextGauged;\n\n\t\t\t// The gauge is decoration over a value the text already states, so it\n\t\t\t// is the first thing to go. Dropping it must not cost the tray its\n\t\t\t// spelled-out permission mode, which is why this is its own rung.\n\t\t\tif (!fits([...left, right])) {\n\t\t\t\tright = contextFull;\n\t\t\t}\n\n\t\t\tif (!fits([...left, right])) {\n\t\t\t\tleft = [permissionCompact];\n\t\t\t\tright = contextCompact;\n\t\t\t}\n\n\t\t\tif (!fits([...left, right])) {\n\t\t\t\tconst safetyOnly = left[0];\n\t\t\t\tconst line =\n\t\t\t\t\tvisibleWidth(safetyOnly) <= width ? safetyOnly : truncateToWidth(safetyOnly, Math.max(0, width), \"\");\n\t\t\t\treturn width > 0 ? [line] : [];\n\t\t\t}\n\n\t\t\tconst optional: string[] = [];\n\t\t\toptional.push(theme.fg(\"dim\", state.model?.id || \"no-model\"));\n\t\t\tif (state.model?.reasoning && state.thinkingLevel && state.thinkingLevel !== \"off\") {\n\t\t\t\toptional.push(theme.fg(\"dim\", state.thinkingLevel));\n\t\t\t}\n\t\t\tif (tokenUsageDisplay !== \"off\" && (usageTotals.input || usageTotals.output)) {\n\t\t\t\toptional.push(\n\t\t\t\t\ttheme.fg(\n\t\t\t\t\t\t\"dim\",\n\t\t\t\t\t\t`${upSymbol}${formatTokens(usageTotals.input)} ${downSymbol}${formatTokens(usageTotals.output)}`,\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (\n\t\t\t\ttokenUsageDisplay !== \"off\" &&\n\t\t\t\t(usageTotals.cacheRead > 0 || usageTotals.cacheWrite > 0) &&\n\t\t\t\tlatestCacheHitRate !== undefined\n\t\t\t) {\n\t\t\t\toptional.push(theme.fg(\"dim\", `CH${latestCacheHitRate.toFixed(1)}%`));\n\t\t\t}\n\t\t\tif (tokenUsageDisplay !== \"off\" && (usageTotals.cost || usingSubscription)) {\n\t\t\t\toptional.push(theme.fg(\"dim\", `$${usageTotals.cost.toFixed(3)}${usingSubscription ? \" (sub)\" : \"\"}`));\n\t\t\t}\n\t\t\tif (this.footerData.getAvailableProviderCount() > 1 && state.model) {\n\t\t\t\toptional.push(theme.fg(\"dim\", state.model.provider));\n\t\t\t}\n\t\t\tif (areExperimentalFeaturesEnabled()) optional.push(theme.fg(\"warning\", \"xp\"));\n\t\t\toptional.push(theme.fg(\"dim\", pwd));\n\n\t\t\tfor (const segment of optional) {\n\t\t\t\tif (fits([...left, segment, right])) left.push(segment);\n\t\t\t}\n\n\t\t\tconst leftText = joinSegments(left);\n\t\t\tconst padding = \" \".repeat(Math.max(1, width - visibleWidth(leftText) - visibleWidth(right)));\n\t\t\tconst tray = leftText ? `${leftText}${padding}${right}` : right;\n\t\t\tconst lines = [tray];\n\t\t\tconst extensionStatuses = this.footerData.getExtensionStatuses();\n\t\t\tif (extensionStatuses.size > 0) {\n\t\t\t\tconst sortedStatuses = Array.from(extensionStatuses.entries())\n\t\t\t\t\t.sort(([a], [b]) => a.localeCompare(b))\n\t\t\t\t\t.map(([, text]) => sanitizeStatusText(text));\n\t\t\t\tlines.push(truncateToWidth(sortedStatuses.join(\" \"), width, theme.fg(\"dim\", \"...\")));\n\t\t\t}\n\t\t\treturn lines;\n\t\t}\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\t// Permission mode is the first segment and must remain textual at every\n\t\t\t// positive width. An ellipsis suffix can consume the entire narrow line.\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\"\n\t\t\t\t\t? `${modelName} ${bulletSymbol} thinking off`\n\t\t\t\t\t: `${modelName} ${bulletSymbol} ${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\tconst pwdLine = truncateToWidth(theme.fg(\"dim\", pwd), width, theme.fg(\"dim\", \"...\"));\n\t\tconst lines = [pwdLine, 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":"AACA,OAAO,EAAE,KAAK,SAAS,EAAiC,MAAM,wBAAwB,CAAC;AACvF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAEnE,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,uCAAuC,CAAC;AACxF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AAgBzE;;GAEG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAMlD;AAKD;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CACjC,OAAO,EAAE,MAAM,GAAG,SAAS,EAC3B,YAAY,EAAE,SAAS,GAAG,OAAO,GAC/B;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAWnC;AAED,mFAAmF;AACnF,MAAM,WAAW,2BAA2B;IAC3C,eAAe,IAAI,SAAS,GAAG,OAAO,CAAC;IACvC,iBAAiB,IAAI,OAAO,CAAC;IAC7B,oBAAoB,IAAI,KAAK,GAAG,SAAS,GAAG,MAAM,CAAC;CACnD;AAQD,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAYhF;AAED;;;GAGG;AACH,qBAAa,eAAgB,YAAW,SAAS;IAChD,OAAO,CAAC,kBAAkB,CAAQ;IAClC,OAAO,CAAC,cAAc,CAA6B;IACnD,OAAO,CAAC,OAAO,CAAe;IAC9B,OAAO,CAAC,UAAU,CAA6B;IAC/C,OAAO,CAAC,qBAAqB,CAA8B;IAE3D,YACC,OAAO,EAAE,YAAY,EACrB,UAAU,EAAE,0BAA0B,EACtC,qBAAqB,GAAE,2BAA4D,EAKnF;IAED,UAAU,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI,CAKtC;IAED,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAE5C;IAED,iBAAiB,CAAC,IAAI,EAAE,cAAc,GAAG,IAAI,CAE5C;IAED;;;OAGG;IACH,UAAU,IAAI,IAAI,CAEjB;IAED;;;OAGG;IACH,OAAO,IAAI,IAAI,CAEd;IAED;;;;;OAKG;IACH,OAAO,CAAC,UAAU,CAAC,CAIjB;IAEF,OAAO,CAAC,YAAY;IA+BpB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAoS9B;CACD","sourcesContent":["import { isAbsolute, relative, resolve, sep } from \"node:path\";\nimport { type Component, truncateToWidth, visibleWidth } from \"@earendil-works/pi-tui\";\nimport type { AgentSession } from \"../../../core/agent-session.ts\";\nimport { areExperimentalFeaturesEnabled } from \"../../../core/experimental.ts\";\nimport type { ReadonlyFooterDataProvider } from \"../../../core/footer-data-provider.ts\";\nimport type { PermissionMode } from \"../../../core/permissions/store.ts\";\nimport { addUsageToTotals, createUsageTotals } from \"../../../core/usage-totals.ts\";\nimport { theme } from \"../theme/theme.ts\";\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\n/**\n * Format token counts for compact footer display.\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\n/** Cells in the context gauge. Eight reads as a bar without crowding the tray. */\nconst CONTEXT_GAUGE_CELLS = 8;\n\n/**\n * A filled bar for the context window, split into its filled and empty runs so\n * the caller can colour them separately.\n *\n * This is a redundant channel and never the only one: the percentage beside it\n * states the same value as text (WCAG 1.4.1). An unknown percentage renders an\n * empty track rather than guessing a position.\n */\nexport function renderContextGauge(\n\tpercent: number | undefined,\n\tsymbolPreset: \"unicode\" | \"ascii\",\n): { filled: string; empty: string } {\n\tconst filledGlyph = symbolPreset === \"ascii\" ? \"#\" : \"█\";\n\tconst emptyGlyph = symbolPreset === \"ascii\" ? \"-\" : \"░\";\n\tif (percent === undefined) {\n\t\treturn { filled: \"\", empty: emptyGlyph.repeat(CONTEXT_GAUGE_CELLS) };\n\t}\n\tconst clamped = Math.min(100, Math.max(0, percent));\n\t// Any non-zero usage lights at least one cell, so the bar never reads empty\n\t// while the window is genuinely filling.\n\tconst cells = clamped === 0 ? 0 : Math.max(1, Math.round((clamped / 100) * CONTEXT_GAUGE_CELLS));\n\treturn { filled: filledGlyph.repeat(cells), empty: emptyGlyph.repeat(CONTEXT_GAUGE_CELLS - cells) };\n}\n\n/** Accessibility/display settings the footer reads (roadmap Phase 8, task 8.6). */\nexport interface FooterAccessibilitySettings {\n\tgetSymbolPreset(): \"unicode\" | \"ascii\";\n\tgetColorBlindMode(): boolean;\n\tgetTokenUsageDisplay(): \"off\" | \"compact\" | \"full\";\n}\n\nconst DEFAULT_ACCESSIBILITY_SETTINGS: FooterAccessibilitySettings = {\n\tgetSymbolPreset: () => \"unicode\",\n\tgetColorBlindMode: () => false,\n\tgetTokenUsageDisplay: () => \"compact\",\n};\n\nexport function formatCwdForFooter(cwd: string, home: string | undefined): string {\n\tif (!home) return cwd;\n\n\tconst resolvedCwd = resolve(cwd);\n\tconst resolvedHome = resolve(home);\n\tconst relativeToHome = relative(resolvedHome, resolvedCwd);\n\tconst isInsideHome =\n\t\trelativeToHome === \"\" ||\n\t\t(relativeToHome !== \"..\" && !relativeToHome.startsWith(`..${sep}`) && !isAbsolute(relativeToHome));\n\n\tif (!isInsideHome) return cwd;\n\treturn relativeToHome === \"\" ? \"~\" : `~/${relativeToHome.replaceAll(\"\\\\\", \"/\")}`;\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\tprivate permissionMode: PermissionMode = \"default\";\n\tprivate session: AgentSession;\n\tprivate footerData: ReadonlyFooterDataProvider;\n\tprivate accessibilitySettings: FooterAccessibilitySettings;\n\n\tconstructor(\n\t\tsession: AgentSession,\n\t\tfooterData: ReadonlyFooterDataProvider,\n\t\taccessibilitySettings: FooterAccessibilitySettings = DEFAULT_ACCESSIBILITY_SETTINGS,\n\t) {\n\t\tthis.session = session;\n\t\tthis.footerData = footerData;\n\t\tthis.accessibilitySettings = accessibilitySettings;\n\t}\n\n\tsetSession(session: AgentSession): void {\n\t\tthis.session = session;\n\t\t// The usage totals are cached against the previous session's entry\n\t\t// version; a session switch must not reuse them.\n\t\tthis.usageCache = undefined;\n\t}\n\n\tsetAutoCompactEnabled(enabled: boolean): void {\n\t\tthis.autoCompactEnabled = enabled;\n\t}\n\n\tsetPermissionMode(mode: PermissionMode): void {\n\t\tthis.permissionMode = mode;\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\t/**\n\t * Usage totals walked from all session entries. getEntries() copies the\n\t * whole entry list, so this is cached against the session manager's\n\t * entries version: without the cache the footer re-walked the entire\n\t * transcript on every rendered frame.\n\t */\n\tprivate usageCache?: {\n\t\tversion: number;\n\t\ttotals: ReturnType<typeof createUsageTotals>;\n\t\tlatestCacheHitRate?: number;\n\t};\n\n\tprivate computeUsage(): {\n\t\ttotals: ReturnType<typeof createUsageTotals>;\n\t\tlatestCacheHitRate?: number;\n\t} {\n\t\tconst version = this.session.sessionManager.getEntriesVersion();\n\t\tif (this.usageCache && this.usageCache.version === version) {\n\t\t\treturn { totals: this.usageCache.totals, latestCacheHitRate: this.usageCache.latestCacheHitRate };\n\t\t}\n\n\t\tconst usageTotals = createUsageTotals();\n\t\tlet latestCacheHitRate: number | undefined;\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\taddUsageToTotals(usageTotals, entry.message.usage);\n\n\t\t\t\tconst latestPromptTokens =\n\t\t\t\t\tentry.message.usage.input + entry.message.usage.cacheRead + entry.message.usage.cacheWrite;\n\t\t\t\tlatestCacheHitRate =\n\t\t\t\t\tlatestPromptTokens > 0 ? (entry.message.usage.cacheRead / latestPromptTokens) * 100 : undefined;\n\t\t\t} else if (entry.type === \"message\" && entry.message.role === \"toolResult\" && entry.message.usage) {\n\t\t\t\taddUsageToTotals(usageTotals, entry.message.usage);\n\t\t\t} else if ((entry.type === \"branch_summary\" || entry.type === \"compaction\") && entry.usage) {\n\t\t\t\taddUsageToTotals(usageTotals, entry.usage);\n\t\t\t}\n\t\t}\n\n\t\tthis.usageCache = { version, totals: usageTotals, latestCacheHitRate };\n\t\treturn { totals: usageTotals, latestCacheHitRate };\n\t}\n\n\trender(width: number): string[] {\n\t\tconst state = this.session.state;\n\t\tconst symbolPreset = this.accessibilitySettings.getSymbolPreset();\n\t\tconst colorBlindMode = this.accessibilitySettings.getColorBlindMode();\n\t\tconst tokenUsageDisplay = this.accessibilitySettings.getTokenUsageDisplay();\n\t\tconst upSymbol = symbolPreset === \"ascii\" ? \"^\" : \"↑\";\n\t\tconst downSymbol = symbolPreset === \"ascii\" ? \"v\" : \"↓\";\n\t\tconst bulletSymbol = symbolPreset === \"ascii\" ? \"-\" : \"•\";\n\n\t\t// Calculate cumulative usage from ALL session entries (not just post-compaction messages),\n\t\t// cached against the session's entry version (see computeUsage).\n\t\tconst { totals: usageTotals, latestCacheHitRate } = this.computeUsage();\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 = formatCwdForFooter(this.session.sessionManager.getCwd(), process.env.HOME || process.env.USERPROFILE);\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} ${bulletSymbol} ${sessionName}`;\n\t\t}\n\n\t\t// Build stats line. tokenUsageDisplay: \"off\" hides this whole section\n\t\t// (context% and model name are separate and always shown); \"full\" shows\n\t\t// exact counts instead of formatTokens' abbreviation.\n\t\tconst formatTokenCount = tokenUsageDisplay === \"full\" ? (n: number) => n.toLocaleString() : formatTokens;\n\t\tconst statsParts = [];\n\t\tif (tokenUsageDisplay !== \"off\") {\n\t\t\tif (usageTotals.input) statsParts.push(`${upSymbol}${formatTokenCount(usageTotals.input)}`);\n\t\t\tif (usageTotals.output) statsParts.push(`${downSymbol}${formatTokenCount(usageTotals.output)}`);\n\t\t\tif (usageTotals.cacheRead) statsParts.push(`R${formatTokenCount(usageTotals.cacheRead)}`);\n\t\t\tif (usageTotals.cacheWrite) statsParts.push(`W${formatTokenCount(usageTotals.cacheWrite)}`);\n\t\t\tif ((usageTotals.cacheRead > 0 || usageTotals.cacheWrite > 0) && latestCacheHitRate !== undefined) {\n\t\t\t\tstatsParts.push(`CH${latestCacheHitRate.toFixed(1)}%`);\n\t\t\t}\n\t\t}\n\n\t\t// Kimi Coding is subscription-backed despite using API-key authentication.\n\t\tconst usingSubscription = state.model\n\t\t\t? state.model.provider === \"kimi-coding\" || this.session.modelRuntime.isUsingSubscription(state.model.provider)\n\t\t\t: false;\n\t\tif (tokenUsageDisplay !== \"off\" && (usageTotals.cost || usingSubscription)) {\n\t\t\tconst costStr = `$${usageTotals.cost.toFixed(3)}${usingSubscription ? \" (sub)\" : \"\"}`;\n\t\t\tstatsParts.push(costStr);\n\t\t}\n\n\t\t// Context pressure is signalled through text at every setting -- never\n\t\t// color alone (WCAG 1.4.1) -- with color as an additional channel. The\n\t\t// default error/warning pair (red/amber) is already distinguishable for\n\t\t// most red-green colorblindness; colorBlindMode's palette adjustment\n\t\t// moves the critical (>90%) threshold to \"accent\" -- a hue outside that\n\t\t// red/amber family entirely -- rather than relying on a style attribute\n\t\t// like bold/underline, which several terminals (and this theme's own\n\t\t// chalk-backed style methods, suppressed outside a real TTY) silently\n\t\t// drop, making it an unreliable second channel.\n\t\tlet contextPercentStr: string;\n\t\tconst autoIndicator = this.autoCompactEnabled ? \" (auto)\" : \"\";\n\t\tconst pressureMarker = contextPercentValue > 90 ? \"!!\" : contextPercentValue > 70 ? \"!\" : \"\";\n\t\tconst contextPercentDisplay =\n\t\t\tcontextPercent === \"?\"\n\t\t\t\t? `?/${formatTokens(contextWindow)}${autoIndicator}`\n\t\t\t\t: `${contextPercent}%${pressureMarker}/${formatTokens(contextWindow)}${autoIndicator}`;\n\t\tif (contextPercentValue > 90) {\n\t\t\tcontextPercentStr = theme.fg(colorBlindMode ? \"accent\" : \"error\", contextPercentDisplay);\n\t\t} else if (contextPercentValue > 70) {\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\t\tif (areExperimentalFeaturesEnabled()) {\n\t\t\tstatsParts.push(`${theme.fg(\"dim\", bulletSymbol)} ${theme.bold(theme.fg(\"warning\", \"xp\"))}`);\n\t\t}\n\n\t\t// Permission posture is always named, including `default`. It is operational\n\t\t// state rather than routine telemetry: hiding the safe default would make the\n\t\t// tray change meaning by omission, while hiding bypass would conceal risk.\n\t\t// Text carries the signal and color remains a second channel (WCAG 1.4.1).\n\t\tconst modeColor =\n\t\t\tthis.permissionMode === \"bypassPermissions\" ? \"error\" : this.permissionMode === \"default\" ? \"dim\" : \"warning\";\n\t\tstatsParts.unshift(theme.fg(modeColor, this.permissionMode));\n\n\t\tif (tokenUsageDisplay !== \"full\") {\n\t\t\tconst separator = theme.fg(\"dim\", symbolPreset === \"ascii\" ? \" - \" : \" · \");\n\t\t\tconst compactPermissionNames: Record<PermissionMode, string> = {\n\t\t\t\tdefault: \"default\",\n\t\t\t\tplan: \"plan\",\n\t\t\t\tacceptEdits: \"accept\",\n\t\t\t\tbypassPermissions: \"bypass\",\n\t\t\t\tdontAsk: \"dontAsk\",\n\t\t\t};\n\t\t\tconst permissionColor =\n\t\t\t\tthis.permissionMode === \"bypassPermissions\"\n\t\t\t\t\t? \"error\"\n\t\t\t\t\t: this.permissionMode === \"default\"\n\t\t\t\t\t\t? \"dim\"\n\t\t\t\t\t\t: \"warning\";\n\t\t\tconst permissionFull = theme.fg(permissionColor, this.permissionMode);\n\t\t\tconst permissionCompact = theme.fg(permissionColor, compactPermissionNames[this.permissionMode]);\n\t\t\tconst compactPercent = contextPercent === \"?\" ? \"?\" : contextPercentValue.toFixed(0);\n\t\t\tconst compactPressure = contextPercentValue > 90 ? \"!!\" : contextPercentValue > 70 ? \"!\" : \"\";\n\t\t\tconst contextColor =\n\t\t\t\tcontextPercentValue > 90\n\t\t\t\t\t? colorBlindMode\n\t\t\t\t\t\t? \"accent\"\n\t\t\t\t\t\t: \"error\"\n\t\t\t\t\t: contextPercentValue > 70\n\t\t\t\t\t\t? \"warning\"\n\t\t\t\t\t\t: \"dim\";\n\t\t\t// The gauge rides along only in the roomy form. The compact form drops\n\t\t\t// it, which is what lets it keep winning the fit ladder below.\n\t\t\tconst gauge = renderContextGauge(contextPercent === \"?\" ? undefined : contextPercentValue, symbolPreset);\n\t\t\t// Below the warning threshold the text is dim, which would make the bar\n\t\t\t// invisible; the fill takes the accent there and tracks the text colour\n\t\t\t// at every level that actually signals pressure.\n\t\t\tconst gaugeColor = contextColor === \"dim\" ? \"accent\" : contextColor;\n\t\t\t// An empty run would still emit an open/close SGR pair, so skip it.\n\t\t\tconst paintRun = (color: Parameters<typeof theme.fg>[0], text: string) => (text ? theme.fg(color, text) : \"\");\n\t\t\tconst contextGauged =\n\t\t\t\ttheme.fg(contextColor, \"context \") +\n\t\t\t\tpaintRun(gaugeColor, gauge.filled) +\n\t\t\t\tpaintRun(\"borderMuted\", gauge.empty) +\n\t\t\t\ttheme.fg(contextColor, ` ${contextPercent}%${pressureMarker}`);\n\t\t\tconst contextFull = theme.fg(contextColor, `context ${contextPercent}%${pressureMarker}`);\n\t\t\tconst contextCompact = theme.fg(contextColor, `ctx ${compactPercent}%${compactPressure}`);\n\n\t\t\tconst joinSegments = (segments: string[]): string => segments.join(separator);\n\t\t\tconst fits = (segments: string[]): boolean => visibleWidth(joinSegments(segments)) <= width;\n\t\t\tlet left: string[] = [permissionFull];\n\t\t\tlet right = contextGauged;\n\n\t\t\t// The gauge is decoration over a value the text already states, so it\n\t\t\t// is the first thing to go. Dropping it must not cost the tray its\n\t\t\t// spelled-out permission mode, which is why this is its own rung.\n\t\t\tif (!fits([...left, right])) {\n\t\t\t\tright = contextFull;\n\t\t\t}\n\n\t\t\tif (!fits([...left, right])) {\n\t\t\t\tleft = [permissionCompact];\n\t\t\t\tright = contextCompact;\n\t\t\t}\n\n\t\t\tif (!fits([...left, right])) {\n\t\t\t\tconst safetyOnly = left[0];\n\t\t\t\tconst line =\n\t\t\t\t\tvisibleWidth(safetyOnly) <= width ? safetyOnly : truncateToWidth(safetyOnly, Math.max(0, width), \"\");\n\t\t\t\treturn width > 0 ? [line] : [];\n\t\t\t}\n\n\t\t\tconst optional: string[] = [];\n\t\t\toptional.push(theme.fg(\"dim\", state.model?.id || \"no-model\"));\n\t\t\tif (state.model?.reasoning && state.thinkingLevel && state.thinkingLevel !== \"off\") {\n\t\t\t\toptional.push(theme.fg(\"dim\", state.thinkingLevel));\n\t\t\t}\n\t\t\tif (tokenUsageDisplay !== \"off\" && (usageTotals.input || usageTotals.output)) {\n\t\t\t\toptional.push(\n\t\t\t\t\ttheme.fg(\n\t\t\t\t\t\t\"dim\",\n\t\t\t\t\t\t`${upSymbol}${formatTokens(usageTotals.input)} ${downSymbol}${formatTokens(usageTotals.output)}`,\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (\n\t\t\t\ttokenUsageDisplay !== \"off\" &&\n\t\t\t\t(usageTotals.cacheRead > 0 || usageTotals.cacheWrite > 0) &&\n\t\t\t\tlatestCacheHitRate !== undefined\n\t\t\t) {\n\t\t\t\toptional.push(theme.fg(\"dim\", `CH${latestCacheHitRate.toFixed(1)}%`));\n\t\t\t}\n\t\t\tif (tokenUsageDisplay !== \"off\" && (usageTotals.cost || usingSubscription)) {\n\t\t\t\toptional.push(theme.fg(\"dim\", `$${usageTotals.cost.toFixed(3)}${usingSubscription ? \" (sub)\" : \"\"}`));\n\t\t\t}\n\t\t\tif (this.footerData.getAvailableProviderCount() > 1 && state.model) {\n\t\t\t\toptional.push(theme.fg(\"dim\", state.model.provider));\n\t\t\t}\n\t\t\tif (areExperimentalFeaturesEnabled()) optional.push(theme.fg(\"warning\", \"xp\"));\n\t\t\toptional.push(theme.fg(\"dim\", pwd));\n\n\t\t\tfor (const segment of optional) {\n\t\t\t\tif (fits([...left, segment, right])) left.push(segment);\n\t\t\t}\n\n\t\t\tconst leftText = joinSegments(left);\n\t\t\tconst padding = \" \".repeat(Math.max(1, width - visibleWidth(leftText) - visibleWidth(right)));\n\t\t\tconst tray = leftText ? `${leftText}${padding}${right}` : right;\n\t\t\tconst lines = [tray];\n\t\t\tconst extensionStatuses = this.footerData.getExtensionStatuses();\n\t\t\tif (extensionStatuses.size > 0) {\n\t\t\t\tconst sortedStatuses = Array.from(extensionStatuses.entries())\n\t\t\t\t\t.sort(([a], [b]) => a.localeCompare(b))\n\t\t\t\t\t.map(([, text]) => sanitizeStatusText(text));\n\t\t\t\tlines.push(truncateToWidth(sortedStatuses.join(\" \"), width, theme.fg(\"dim\", \"...\")));\n\t\t\t}\n\t\t\treturn lines;\n\t\t}\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\t// Permission mode is the first segment and must remain textual at every\n\t\t\t// positive width. An ellipsis suffix can consume the entire narrow line.\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\"\n\t\t\t\t\t? `${modelName} ${bulletSymbol} thinking off`\n\t\t\t\t\t: `${modelName} ${bulletSymbol} ${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\tconst pwdLine = truncateToWidth(theme.fg(\"dim\", pwd), width, theme.fg(\"dim\", \"...\"));\n\t\tconst lines = [pwdLine, 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"]}
|
|
@@ -84,6 +84,9 @@ export class FooterComponent {
|
|
|
84
84
|
}
|
|
85
85
|
setSession(session) {
|
|
86
86
|
this.session = session;
|
|
87
|
+
// The usage totals are cached against the previous session's entry
|
|
88
|
+
// version; a session switch must not reuse them.
|
|
89
|
+
this.usageCache = undefined;
|
|
87
90
|
}
|
|
88
91
|
setAutoCompactEnabled(enabled) {
|
|
89
92
|
this.autoCompactEnabled = enabled;
|
|
@@ -105,15 +108,18 @@ export class FooterComponent {
|
|
|
105
108
|
dispose() {
|
|
106
109
|
// Git watcher cleanup handled by provider
|
|
107
110
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
111
|
+
/**
|
|
112
|
+
* Usage totals walked from all session entries. getEntries() copies the
|
|
113
|
+
* whole entry list, so this is cached against the session manager's
|
|
114
|
+
* entries version: without the cache the footer re-walked the entire
|
|
115
|
+
* transcript on every rendered frame.
|
|
116
|
+
*/
|
|
117
|
+
usageCache;
|
|
118
|
+
computeUsage() {
|
|
119
|
+
const version = this.session.sessionManager.getEntriesVersion();
|
|
120
|
+
if (this.usageCache && this.usageCache.version === version) {
|
|
121
|
+
return { totals: this.usageCache.totals, latestCacheHitRate: this.usageCache.latestCacheHitRate };
|
|
122
|
+
}
|
|
117
123
|
const usageTotals = createUsageTotals();
|
|
118
124
|
let latestCacheHitRate;
|
|
119
125
|
for (const entry of this.session.sessionManager.getEntries()) {
|
|
@@ -130,6 +136,20 @@ export class FooterComponent {
|
|
|
130
136
|
addUsageToTotals(usageTotals, entry.usage);
|
|
131
137
|
}
|
|
132
138
|
}
|
|
139
|
+
this.usageCache = { version, totals: usageTotals, latestCacheHitRate };
|
|
140
|
+
return { totals: usageTotals, latestCacheHitRate };
|
|
141
|
+
}
|
|
142
|
+
render(width) {
|
|
143
|
+
const state = this.session.state;
|
|
144
|
+
const symbolPreset = this.accessibilitySettings.getSymbolPreset();
|
|
145
|
+
const colorBlindMode = this.accessibilitySettings.getColorBlindMode();
|
|
146
|
+
const tokenUsageDisplay = this.accessibilitySettings.getTokenUsageDisplay();
|
|
147
|
+
const upSymbol = symbolPreset === "ascii" ? "^" : "↑";
|
|
148
|
+
const downSymbol = symbolPreset === "ascii" ? "v" : "↓";
|
|
149
|
+
const bulletSymbol = symbolPreset === "ascii" ? "-" : "•";
|
|
150
|
+
// Calculate cumulative usage from ALL session entries (not just post-compaction messages),
|
|
151
|
+
// cached against the session's entry version (see computeUsage).
|
|
152
|
+
const { totals: usageTotals, latestCacheHitRate } = this.computeUsage();
|
|
133
153
|
// Calculate context usage from session (handles compaction correctly).
|
|
134
154
|
// After compaction, tokens are unknown until the next LLM response.
|
|
135
155
|
const contextUsage = this.session.getContextUsage();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"footer.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/footer.ts"],"names":[],"mappings":"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","sourcesContent":["import { isAbsolute, relative, resolve, sep } from \"node:path\";\nimport { type Component, truncateToWidth, visibleWidth } from \"@earendil-works/pi-tui\";\nimport type { AgentSession } from \"../../../core/agent-session.ts\";\nimport { areExperimentalFeaturesEnabled } from \"../../../core/experimental.ts\";\nimport type { ReadonlyFooterDataProvider } from \"../../../core/footer-data-provider.ts\";\nimport type { PermissionMode } from \"../../../core/permissions/store.ts\";\nimport { addUsageToTotals, createUsageTotals } from \"../../../core/usage-totals.ts\";\nimport { theme } from \"../theme/theme.ts\";\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\n/**\n * Format token counts for compact footer display.\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\n/** Cells in the context gauge. Eight reads as a bar without crowding the tray. */\nconst CONTEXT_GAUGE_CELLS = 8;\n\n/**\n * A filled bar for the context window, split into its filled and empty runs so\n * the caller can colour them separately.\n *\n * This is a redundant channel and never the only one: the percentage beside it\n * states the same value as text (WCAG 1.4.1). An unknown percentage renders an\n * empty track rather than guessing a position.\n */\nexport function renderContextGauge(\n\tpercent: number | undefined,\n\tsymbolPreset: \"unicode\" | \"ascii\",\n): { filled: string; empty: string } {\n\tconst filledGlyph = symbolPreset === \"ascii\" ? \"#\" : \"█\";\n\tconst emptyGlyph = symbolPreset === \"ascii\" ? \"-\" : \"░\";\n\tif (percent === undefined) {\n\t\treturn { filled: \"\", empty: emptyGlyph.repeat(CONTEXT_GAUGE_CELLS) };\n\t}\n\tconst clamped = Math.min(100, Math.max(0, percent));\n\t// Any non-zero usage lights at least one cell, so the bar never reads empty\n\t// while the window is genuinely filling.\n\tconst cells = clamped === 0 ? 0 : Math.max(1, Math.round((clamped / 100) * CONTEXT_GAUGE_CELLS));\n\treturn { filled: filledGlyph.repeat(cells), empty: emptyGlyph.repeat(CONTEXT_GAUGE_CELLS - cells) };\n}\n\n/** Accessibility/display settings the footer reads (roadmap Phase 8, task 8.6). */\nexport interface FooterAccessibilitySettings {\n\tgetSymbolPreset(): \"unicode\" | \"ascii\";\n\tgetColorBlindMode(): boolean;\n\tgetTokenUsageDisplay(): \"off\" | \"compact\" | \"full\";\n}\n\nconst DEFAULT_ACCESSIBILITY_SETTINGS: FooterAccessibilitySettings = {\n\tgetSymbolPreset: () => \"unicode\",\n\tgetColorBlindMode: () => false,\n\tgetTokenUsageDisplay: () => \"compact\",\n};\n\nexport function formatCwdForFooter(cwd: string, home: string | undefined): string {\n\tif (!home) return cwd;\n\n\tconst resolvedCwd = resolve(cwd);\n\tconst resolvedHome = resolve(home);\n\tconst relativeToHome = relative(resolvedHome, resolvedCwd);\n\tconst isInsideHome =\n\t\trelativeToHome === \"\" ||\n\t\t(relativeToHome !== \"..\" && !relativeToHome.startsWith(`..${sep}`) && !isAbsolute(relativeToHome));\n\n\tif (!isInsideHome) return cwd;\n\treturn relativeToHome === \"\" ? \"~\" : `~/${relativeToHome.replaceAll(\"\\\\\", \"/\")}`;\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\tprivate permissionMode: PermissionMode = \"default\";\n\tprivate session: AgentSession;\n\tprivate footerData: ReadonlyFooterDataProvider;\n\tprivate accessibilitySettings: FooterAccessibilitySettings;\n\n\tconstructor(\n\t\tsession: AgentSession,\n\t\tfooterData: ReadonlyFooterDataProvider,\n\t\taccessibilitySettings: FooterAccessibilitySettings = DEFAULT_ACCESSIBILITY_SETTINGS,\n\t) {\n\t\tthis.session = session;\n\t\tthis.footerData = footerData;\n\t\tthis.accessibilitySettings = accessibilitySettings;\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\tsetPermissionMode(mode: PermissionMode): void {\n\t\tthis.permissionMode = mode;\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\t\tconst symbolPreset = this.accessibilitySettings.getSymbolPreset();\n\t\tconst colorBlindMode = this.accessibilitySettings.getColorBlindMode();\n\t\tconst tokenUsageDisplay = this.accessibilitySettings.getTokenUsageDisplay();\n\t\tconst upSymbol = symbolPreset === \"ascii\" ? \"^\" : \"↑\";\n\t\tconst downSymbol = symbolPreset === \"ascii\" ? \"v\" : \"↓\";\n\t\tconst bulletSymbol = symbolPreset === \"ascii\" ? \"-\" : \"•\";\n\n\t\t// Calculate cumulative usage from ALL session entries (not just post-compaction messages)\n\t\tconst usageTotals = createUsageTotals();\n\t\tlet latestCacheHitRate: number | undefined;\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\taddUsageToTotals(usageTotals, entry.message.usage);\n\n\t\t\t\tconst latestPromptTokens =\n\t\t\t\t\tentry.message.usage.input + entry.message.usage.cacheRead + entry.message.usage.cacheWrite;\n\t\t\t\tlatestCacheHitRate =\n\t\t\t\t\tlatestPromptTokens > 0 ? (entry.message.usage.cacheRead / latestPromptTokens) * 100 : undefined;\n\t\t\t} else if (entry.type === \"message\" && entry.message.role === \"toolResult\" && entry.message.usage) {\n\t\t\t\taddUsageToTotals(usageTotals, entry.message.usage);\n\t\t\t} else if ((entry.type === \"branch_summary\" || entry.type === \"compaction\") && entry.usage) {\n\t\t\t\taddUsageToTotals(usageTotals, entry.usage);\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 = formatCwdForFooter(this.session.sessionManager.getCwd(), process.env.HOME || process.env.USERPROFILE);\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} ${bulletSymbol} ${sessionName}`;\n\t\t}\n\n\t\t// Build stats line. tokenUsageDisplay: \"off\" hides this whole section\n\t\t// (context% and model name are separate and always shown); \"full\" shows\n\t\t// exact counts instead of formatTokens' abbreviation.\n\t\tconst formatTokenCount = tokenUsageDisplay === \"full\" ? (n: number) => n.toLocaleString() : formatTokens;\n\t\tconst statsParts = [];\n\t\tif (tokenUsageDisplay !== \"off\") {\n\t\t\tif (usageTotals.input) statsParts.push(`${upSymbol}${formatTokenCount(usageTotals.input)}`);\n\t\t\tif (usageTotals.output) statsParts.push(`${downSymbol}${formatTokenCount(usageTotals.output)}`);\n\t\t\tif (usageTotals.cacheRead) statsParts.push(`R${formatTokenCount(usageTotals.cacheRead)}`);\n\t\t\tif (usageTotals.cacheWrite) statsParts.push(`W${formatTokenCount(usageTotals.cacheWrite)}`);\n\t\t\tif ((usageTotals.cacheRead > 0 || usageTotals.cacheWrite > 0) && latestCacheHitRate !== undefined) {\n\t\t\t\tstatsParts.push(`CH${latestCacheHitRate.toFixed(1)}%`);\n\t\t\t}\n\t\t}\n\n\t\t// Kimi Coding is subscription-backed despite using API-key authentication.\n\t\tconst usingSubscription = state.model\n\t\t\t? state.model.provider === \"kimi-coding\" || this.session.modelRuntime.isUsingSubscription(state.model.provider)\n\t\t\t: false;\n\t\tif (tokenUsageDisplay !== \"off\" && (usageTotals.cost || usingSubscription)) {\n\t\t\tconst costStr = `$${usageTotals.cost.toFixed(3)}${usingSubscription ? \" (sub)\" : \"\"}`;\n\t\t\tstatsParts.push(costStr);\n\t\t}\n\n\t\t// Context pressure is signalled through text at every setting -- never\n\t\t// color alone (WCAG 1.4.1) -- with color as an additional channel. The\n\t\t// default error/warning pair (red/amber) is already distinguishable for\n\t\t// most red-green colorblindness; colorBlindMode's palette adjustment\n\t\t// moves the critical (>90%) threshold to \"accent\" -- a hue outside that\n\t\t// red/amber family entirely -- rather than relying on a style attribute\n\t\t// like bold/underline, which several terminals (and this theme's own\n\t\t// chalk-backed style methods, suppressed outside a real TTY) silently\n\t\t// drop, making it an unreliable second channel.\n\t\tlet contextPercentStr: string;\n\t\tconst autoIndicator = this.autoCompactEnabled ? \" (auto)\" : \"\";\n\t\tconst pressureMarker = contextPercentValue > 90 ? \"!!\" : contextPercentValue > 70 ? \"!\" : \"\";\n\t\tconst contextPercentDisplay =\n\t\t\tcontextPercent === \"?\"\n\t\t\t\t? `?/${formatTokens(contextWindow)}${autoIndicator}`\n\t\t\t\t: `${contextPercent}%${pressureMarker}/${formatTokens(contextWindow)}${autoIndicator}`;\n\t\tif (contextPercentValue > 90) {\n\t\t\tcontextPercentStr = theme.fg(colorBlindMode ? \"accent\" : \"error\", contextPercentDisplay);\n\t\t} else if (contextPercentValue > 70) {\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\t\tif (areExperimentalFeaturesEnabled()) {\n\t\t\tstatsParts.push(`${theme.fg(\"dim\", bulletSymbol)} ${theme.bold(theme.fg(\"warning\", \"xp\"))}`);\n\t\t}\n\n\t\t// Permission posture is always named, including `default`. It is operational\n\t\t// state rather than routine telemetry: hiding the safe default would make the\n\t\t// tray change meaning by omission, while hiding bypass would conceal risk.\n\t\t// Text carries the signal and color remains a second channel (WCAG 1.4.1).\n\t\tconst modeColor =\n\t\t\tthis.permissionMode === \"bypassPermissions\" ? \"error\" : this.permissionMode === \"default\" ? \"dim\" : \"warning\";\n\t\tstatsParts.unshift(theme.fg(modeColor, this.permissionMode));\n\n\t\tif (tokenUsageDisplay !== \"full\") {\n\t\t\tconst separator = theme.fg(\"dim\", symbolPreset === \"ascii\" ? \" - \" : \" · \");\n\t\t\tconst compactPermissionNames: Record<PermissionMode, string> = {\n\t\t\t\tdefault: \"default\",\n\t\t\t\tplan: \"plan\",\n\t\t\t\tacceptEdits: \"accept\",\n\t\t\t\tbypassPermissions: \"bypass\",\n\t\t\t\tdontAsk: \"dontAsk\",\n\t\t\t};\n\t\t\tconst permissionColor =\n\t\t\t\tthis.permissionMode === \"bypassPermissions\"\n\t\t\t\t\t? \"error\"\n\t\t\t\t\t: this.permissionMode === \"default\"\n\t\t\t\t\t\t? \"dim\"\n\t\t\t\t\t\t: \"warning\";\n\t\t\tconst permissionFull = theme.fg(permissionColor, this.permissionMode);\n\t\t\tconst permissionCompact = theme.fg(permissionColor, compactPermissionNames[this.permissionMode]);\n\t\t\tconst compactPercent = contextPercent === \"?\" ? \"?\" : contextPercentValue.toFixed(0);\n\t\t\tconst compactPressure = contextPercentValue > 90 ? \"!!\" : contextPercentValue > 70 ? \"!\" : \"\";\n\t\t\tconst contextColor =\n\t\t\t\tcontextPercentValue > 90\n\t\t\t\t\t? colorBlindMode\n\t\t\t\t\t\t? \"accent\"\n\t\t\t\t\t\t: \"error\"\n\t\t\t\t\t: contextPercentValue > 70\n\t\t\t\t\t\t? \"warning\"\n\t\t\t\t\t\t: \"dim\";\n\t\t\t// The gauge rides along only in the roomy form. The compact form drops\n\t\t\t// it, which is what lets it keep winning the fit ladder below.\n\t\t\tconst gauge = renderContextGauge(contextPercent === \"?\" ? undefined : contextPercentValue, symbolPreset);\n\t\t\t// Below the warning threshold the text is dim, which would make the bar\n\t\t\t// invisible; the fill takes the accent there and tracks the text colour\n\t\t\t// at every level that actually signals pressure.\n\t\t\tconst gaugeColor = contextColor === \"dim\" ? \"accent\" : contextColor;\n\t\t\t// An empty run would still emit an open/close SGR pair, so skip it.\n\t\t\tconst paintRun = (color: Parameters<typeof theme.fg>[0], text: string) => (text ? theme.fg(color, text) : \"\");\n\t\t\tconst contextGauged =\n\t\t\t\ttheme.fg(contextColor, \"context \") +\n\t\t\t\tpaintRun(gaugeColor, gauge.filled) +\n\t\t\t\tpaintRun(\"borderMuted\", gauge.empty) +\n\t\t\t\ttheme.fg(contextColor, ` ${contextPercent}%${pressureMarker}`);\n\t\t\tconst contextFull = theme.fg(contextColor, `context ${contextPercent}%${pressureMarker}`);\n\t\t\tconst contextCompact = theme.fg(contextColor, `ctx ${compactPercent}%${compactPressure}`);\n\n\t\t\tconst joinSegments = (segments: string[]): string => segments.join(separator);\n\t\t\tconst fits = (segments: string[]): boolean => visibleWidth(joinSegments(segments)) <= width;\n\t\t\tlet left: string[] = [permissionFull];\n\t\t\tlet right = contextGauged;\n\n\t\t\t// The gauge is decoration over a value the text already states, so it\n\t\t\t// is the first thing to go. Dropping it must not cost the tray its\n\t\t\t// spelled-out permission mode, which is why this is its own rung.\n\t\t\tif (!fits([...left, right])) {\n\t\t\t\tright = contextFull;\n\t\t\t}\n\n\t\t\tif (!fits([...left, right])) {\n\t\t\t\tleft = [permissionCompact];\n\t\t\t\tright = contextCompact;\n\t\t\t}\n\n\t\t\tif (!fits([...left, right])) {\n\t\t\t\tconst safetyOnly = left[0];\n\t\t\t\tconst line =\n\t\t\t\t\tvisibleWidth(safetyOnly) <= width ? safetyOnly : truncateToWidth(safetyOnly, Math.max(0, width), \"\");\n\t\t\t\treturn width > 0 ? [line] : [];\n\t\t\t}\n\n\t\t\tconst optional: string[] = [];\n\t\t\toptional.push(theme.fg(\"dim\", state.model?.id || \"no-model\"));\n\t\t\tif (state.model?.reasoning && state.thinkingLevel && state.thinkingLevel !== \"off\") {\n\t\t\t\toptional.push(theme.fg(\"dim\", state.thinkingLevel));\n\t\t\t}\n\t\t\tif (tokenUsageDisplay !== \"off\" && (usageTotals.input || usageTotals.output)) {\n\t\t\t\toptional.push(\n\t\t\t\t\ttheme.fg(\n\t\t\t\t\t\t\"dim\",\n\t\t\t\t\t\t`${upSymbol}${formatTokens(usageTotals.input)} ${downSymbol}${formatTokens(usageTotals.output)}`,\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (\n\t\t\t\ttokenUsageDisplay !== \"off\" &&\n\t\t\t\t(usageTotals.cacheRead > 0 || usageTotals.cacheWrite > 0) &&\n\t\t\t\tlatestCacheHitRate !== undefined\n\t\t\t) {\n\t\t\t\toptional.push(theme.fg(\"dim\", `CH${latestCacheHitRate.toFixed(1)}%`));\n\t\t\t}\n\t\t\tif (tokenUsageDisplay !== \"off\" && (usageTotals.cost || usingSubscription)) {\n\t\t\t\toptional.push(theme.fg(\"dim\", `$${usageTotals.cost.toFixed(3)}${usingSubscription ? \" (sub)\" : \"\"}`));\n\t\t\t}\n\t\t\tif (this.footerData.getAvailableProviderCount() > 1 && state.model) {\n\t\t\t\toptional.push(theme.fg(\"dim\", state.model.provider));\n\t\t\t}\n\t\t\tif (areExperimentalFeaturesEnabled()) optional.push(theme.fg(\"warning\", \"xp\"));\n\t\t\toptional.push(theme.fg(\"dim\", pwd));\n\n\t\t\tfor (const segment of optional) {\n\t\t\t\tif (fits([...left, segment, right])) left.push(segment);\n\t\t\t}\n\n\t\t\tconst leftText = joinSegments(left);\n\t\t\tconst padding = \" \".repeat(Math.max(1, width - visibleWidth(leftText) - visibleWidth(right)));\n\t\t\tconst tray = leftText ? `${leftText}${padding}${right}` : right;\n\t\t\tconst lines = [tray];\n\t\t\tconst extensionStatuses = this.footerData.getExtensionStatuses();\n\t\t\tif (extensionStatuses.size > 0) {\n\t\t\t\tconst sortedStatuses = Array.from(extensionStatuses.entries())\n\t\t\t\t\t.sort(([a], [b]) => a.localeCompare(b))\n\t\t\t\t\t.map(([, text]) => sanitizeStatusText(text));\n\t\t\t\tlines.push(truncateToWidth(sortedStatuses.join(\" \"), width, theme.fg(\"dim\", \"...\")));\n\t\t\t}\n\t\t\treturn lines;\n\t\t}\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\t// Permission mode is the first segment and must remain textual at every\n\t\t\t// positive width. An ellipsis suffix can consume the entire narrow line.\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\"\n\t\t\t\t\t? `${modelName} ${bulletSymbol} thinking off`\n\t\t\t\t\t: `${modelName} ${bulletSymbol} ${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\tconst pwdLine = truncateToWidth(theme.fg(\"dim\", pwd), width, theme.fg(\"dim\", \"...\"));\n\t\tconst lines = [pwdLine, 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 { isAbsolute, relative, resolve, sep } from \"node:path\";\nimport { type Component, truncateToWidth, visibleWidth } from \"@earendil-works/pi-tui\";\nimport type { AgentSession } from \"../../../core/agent-session.ts\";\nimport { areExperimentalFeaturesEnabled } from \"../../../core/experimental.ts\";\nimport type { ReadonlyFooterDataProvider } from \"../../../core/footer-data-provider.ts\";\nimport type { PermissionMode } from \"../../../core/permissions/store.ts\";\nimport { addUsageToTotals, createUsageTotals } from \"../../../core/usage-totals.ts\";\nimport { theme } from \"../theme/theme.ts\";\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\n/**\n * Format token counts for compact footer display.\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\n/** Cells in the context gauge. Eight reads as a bar without crowding the tray. */\nconst CONTEXT_GAUGE_CELLS = 8;\n\n/**\n * A filled bar for the context window, split into its filled and empty runs so\n * the caller can colour them separately.\n *\n * This is a redundant channel and never the only one: the percentage beside it\n * states the same value as text (WCAG 1.4.1). An unknown percentage renders an\n * empty track rather than guessing a position.\n */\nexport function renderContextGauge(\n\tpercent: number | undefined,\n\tsymbolPreset: \"unicode\" | \"ascii\",\n): { filled: string; empty: string } {\n\tconst filledGlyph = symbolPreset === \"ascii\" ? \"#\" : \"█\";\n\tconst emptyGlyph = symbolPreset === \"ascii\" ? \"-\" : \"░\";\n\tif (percent === undefined) {\n\t\treturn { filled: \"\", empty: emptyGlyph.repeat(CONTEXT_GAUGE_CELLS) };\n\t}\n\tconst clamped = Math.min(100, Math.max(0, percent));\n\t// Any non-zero usage lights at least one cell, so the bar never reads empty\n\t// while the window is genuinely filling.\n\tconst cells = clamped === 0 ? 0 : Math.max(1, Math.round((clamped / 100) * CONTEXT_GAUGE_CELLS));\n\treturn { filled: filledGlyph.repeat(cells), empty: emptyGlyph.repeat(CONTEXT_GAUGE_CELLS - cells) };\n}\n\n/** Accessibility/display settings the footer reads (roadmap Phase 8, task 8.6). */\nexport interface FooterAccessibilitySettings {\n\tgetSymbolPreset(): \"unicode\" | \"ascii\";\n\tgetColorBlindMode(): boolean;\n\tgetTokenUsageDisplay(): \"off\" | \"compact\" | \"full\";\n}\n\nconst DEFAULT_ACCESSIBILITY_SETTINGS: FooterAccessibilitySettings = {\n\tgetSymbolPreset: () => \"unicode\",\n\tgetColorBlindMode: () => false,\n\tgetTokenUsageDisplay: () => \"compact\",\n};\n\nexport function formatCwdForFooter(cwd: string, home: string | undefined): string {\n\tif (!home) return cwd;\n\n\tconst resolvedCwd = resolve(cwd);\n\tconst resolvedHome = resolve(home);\n\tconst relativeToHome = relative(resolvedHome, resolvedCwd);\n\tconst isInsideHome =\n\t\trelativeToHome === \"\" ||\n\t\t(relativeToHome !== \"..\" && !relativeToHome.startsWith(`..${sep}`) && !isAbsolute(relativeToHome));\n\n\tif (!isInsideHome) return cwd;\n\treturn relativeToHome === \"\" ? \"~\" : `~/${relativeToHome.replaceAll(\"\\\\\", \"/\")}`;\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\tprivate permissionMode: PermissionMode = \"default\";\n\tprivate session: AgentSession;\n\tprivate footerData: ReadonlyFooterDataProvider;\n\tprivate accessibilitySettings: FooterAccessibilitySettings;\n\n\tconstructor(\n\t\tsession: AgentSession,\n\t\tfooterData: ReadonlyFooterDataProvider,\n\t\taccessibilitySettings: FooterAccessibilitySettings = DEFAULT_ACCESSIBILITY_SETTINGS,\n\t) {\n\t\tthis.session = session;\n\t\tthis.footerData = footerData;\n\t\tthis.accessibilitySettings = accessibilitySettings;\n\t}\n\n\tsetSession(session: AgentSession): void {\n\t\tthis.session = session;\n\t\t// The usage totals are cached against the previous session's entry\n\t\t// version; a session switch must not reuse them.\n\t\tthis.usageCache = undefined;\n\t}\n\n\tsetAutoCompactEnabled(enabled: boolean): void {\n\t\tthis.autoCompactEnabled = enabled;\n\t}\n\n\tsetPermissionMode(mode: PermissionMode): void {\n\t\tthis.permissionMode = mode;\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\t/**\n\t * Usage totals walked from all session entries. getEntries() copies the\n\t * whole entry list, so this is cached against the session manager's\n\t * entries version: without the cache the footer re-walked the entire\n\t * transcript on every rendered frame.\n\t */\n\tprivate usageCache?: {\n\t\tversion: number;\n\t\ttotals: ReturnType<typeof createUsageTotals>;\n\t\tlatestCacheHitRate?: number;\n\t};\n\n\tprivate computeUsage(): {\n\t\ttotals: ReturnType<typeof createUsageTotals>;\n\t\tlatestCacheHitRate?: number;\n\t} {\n\t\tconst version = this.session.sessionManager.getEntriesVersion();\n\t\tif (this.usageCache && this.usageCache.version === version) {\n\t\t\treturn { totals: this.usageCache.totals, latestCacheHitRate: this.usageCache.latestCacheHitRate };\n\t\t}\n\n\t\tconst usageTotals = createUsageTotals();\n\t\tlet latestCacheHitRate: number | undefined;\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\taddUsageToTotals(usageTotals, entry.message.usage);\n\n\t\t\t\tconst latestPromptTokens =\n\t\t\t\t\tentry.message.usage.input + entry.message.usage.cacheRead + entry.message.usage.cacheWrite;\n\t\t\t\tlatestCacheHitRate =\n\t\t\t\t\tlatestPromptTokens > 0 ? (entry.message.usage.cacheRead / latestPromptTokens) * 100 : undefined;\n\t\t\t} else if (entry.type === \"message\" && entry.message.role === \"toolResult\" && entry.message.usage) {\n\t\t\t\taddUsageToTotals(usageTotals, entry.message.usage);\n\t\t\t} else if ((entry.type === \"branch_summary\" || entry.type === \"compaction\") && entry.usage) {\n\t\t\t\taddUsageToTotals(usageTotals, entry.usage);\n\t\t\t}\n\t\t}\n\n\t\tthis.usageCache = { version, totals: usageTotals, latestCacheHitRate };\n\t\treturn { totals: usageTotals, latestCacheHitRate };\n\t}\n\n\trender(width: number): string[] {\n\t\tconst state = this.session.state;\n\t\tconst symbolPreset = this.accessibilitySettings.getSymbolPreset();\n\t\tconst colorBlindMode = this.accessibilitySettings.getColorBlindMode();\n\t\tconst tokenUsageDisplay = this.accessibilitySettings.getTokenUsageDisplay();\n\t\tconst upSymbol = symbolPreset === \"ascii\" ? \"^\" : \"↑\";\n\t\tconst downSymbol = symbolPreset === \"ascii\" ? \"v\" : \"↓\";\n\t\tconst bulletSymbol = symbolPreset === \"ascii\" ? \"-\" : \"•\";\n\n\t\t// Calculate cumulative usage from ALL session entries (not just post-compaction messages),\n\t\t// cached against the session's entry version (see computeUsage).\n\t\tconst { totals: usageTotals, latestCacheHitRate } = this.computeUsage();\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 = formatCwdForFooter(this.session.sessionManager.getCwd(), process.env.HOME || process.env.USERPROFILE);\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} ${bulletSymbol} ${sessionName}`;\n\t\t}\n\n\t\t// Build stats line. tokenUsageDisplay: \"off\" hides this whole section\n\t\t// (context% and model name are separate and always shown); \"full\" shows\n\t\t// exact counts instead of formatTokens' abbreviation.\n\t\tconst formatTokenCount = tokenUsageDisplay === \"full\" ? (n: number) => n.toLocaleString() : formatTokens;\n\t\tconst statsParts = [];\n\t\tif (tokenUsageDisplay !== \"off\") {\n\t\t\tif (usageTotals.input) statsParts.push(`${upSymbol}${formatTokenCount(usageTotals.input)}`);\n\t\t\tif (usageTotals.output) statsParts.push(`${downSymbol}${formatTokenCount(usageTotals.output)}`);\n\t\t\tif (usageTotals.cacheRead) statsParts.push(`R${formatTokenCount(usageTotals.cacheRead)}`);\n\t\t\tif (usageTotals.cacheWrite) statsParts.push(`W${formatTokenCount(usageTotals.cacheWrite)}`);\n\t\t\tif ((usageTotals.cacheRead > 0 || usageTotals.cacheWrite > 0) && latestCacheHitRate !== undefined) {\n\t\t\t\tstatsParts.push(`CH${latestCacheHitRate.toFixed(1)}%`);\n\t\t\t}\n\t\t}\n\n\t\t// Kimi Coding is subscription-backed despite using API-key authentication.\n\t\tconst usingSubscription = state.model\n\t\t\t? state.model.provider === \"kimi-coding\" || this.session.modelRuntime.isUsingSubscription(state.model.provider)\n\t\t\t: false;\n\t\tif (tokenUsageDisplay !== \"off\" && (usageTotals.cost || usingSubscription)) {\n\t\t\tconst costStr = `$${usageTotals.cost.toFixed(3)}${usingSubscription ? \" (sub)\" : \"\"}`;\n\t\t\tstatsParts.push(costStr);\n\t\t}\n\n\t\t// Context pressure is signalled through text at every setting -- never\n\t\t// color alone (WCAG 1.4.1) -- with color as an additional channel. The\n\t\t// default error/warning pair (red/amber) is already distinguishable for\n\t\t// most red-green colorblindness; colorBlindMode's palette adjustment\n\t\t// moves the critical (>90%) threshold to \"accent\" -- a hue outside that\n\t\t// red/amber family entirely -- rather than relying on a style attribute\n\t\t// like bold/underline, which several terminals (and this theme's own\n\t\t// chalk-backed style methods, suppressed outside a real TTY) silently\n\t\t// drop, making it an unreliable second channel.\n\t\tlet contextPercentStr: string;\n\t\tconst autoIndicator = this.autoCompactEnabled ? \" (auto)\" : \"\";\n\t\tconst pressureMarker = contextPercentValue > 90 ? \"!!\" : contextPercentValue > 70 ? \"!\" : \"\";\n\t\tconst contextPercentDisplay =\n\t\t\tcontextPercent === \"?\"\n\t\t\t\t? `?/${formatTokens(contextWindow)}${autoIndicator}`\n\t\t\t\t: `${contextPercent}%${pressureMarker}/${formatTokens(contextWindow)}${autoIndicator}`;\n\t\tif (contextPercentValue > 90) {\n\t\t\tcontextPercentStr = theme.fg(colorBlindMode ? \"accent\" : \"error\", contextPercentDisplay);\n\t\t} else if (contextPercentValue > 70) {\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\t\tif (areExperimentalFeaturesEnabled()) {\n\t\t\tstatsParts.push(`${theme.fg(\"dim\", bulletSymbol)} ${theme.bold(theme.fg(\"warning\", \"xp\"))}`);\n\t\t}\n\n\t\t// Permission posture is always named, including `default`. It is operational\n\t\t// state rather than routine telemetry: hiding the safe default would make the\n\t\t// tray change meaning by omission, while hiding bypass would conceal risk.\n\t\t// Text carries the signal and color remains a second channel (WCAG 1.4.1).\n\t\tconst modeColor =\n\t\t\tthis.permissionMode === \"bypassPermissions\" ? \"error\" : this.permissionMode === \"default\" ? \"dim\" : \"warning\";\n\t\tstatsParts.unshift(theme.fg(modeColor, this.permissionMode));\n\n\t\tif (tokenUsageDisplay !== \"full\") {\n\t\t\tconst separator = theme.fg(\"dim\", symbolPreset === \"ascii\" ? \" - \" : \" · \");\n\t\t\tconst compactPermissionNames: Record<PermissionMode, string> = {\n\t\t\t\tdefault: \"default\",\n\t\t\t\tplan: \"plan\",\n\t\t\t\tacceptEdits: \"accept\",\n\t\t\t\tbypassPermissions: \"bypass\",\n\t\t\t\tdontAsk: \"dontAsk\",\n\t\t\t};\n\t\t\tconst permissionColor =\n\t\t\t\tthis.permissionMode === \"bypassPermissions\"\n\t\t\t\t\t? \"error\"\n\t\t\t\t\t: this.permissionMode === \"default\"\n\t\t\t\t\t\t? \"dim\"\n\t\t\t\t\t\t: \"warning\";\n\t\t\tconst permissionFull = theme.fg(permissionColor, this.permissionMode);\n\t\t\tconst permissionCompact = theme.fg(permissionColor, compactPermissionNames[this.permissionMode]);\n\t\t\tconst compactPercent = contextPercent === \"?\" ? \"?\" : contextPercentValue.toFixed(0);\n\t\t\tconst compactPressure = contextPercentValue > 90 ? \"!!\" : contextPercentValue > 70 ? \"!\" : \"\";\n\t\t\tconst contextColor =\n\t\t\t\tcontextPercentValue > 90\n\t\t\t\t\t? colorBlindMode\n\t\t\t\t\t\t? \"accent\"\n\t\t\t\t\t\t: \"error\"\n\t\t\t\t\t: contextPercentValue > 70\n\t\t\t\t\t\t? \"warning\"\n\t\t\t\t\t\t: \"dim\";\n\t\t\t// The gauge rides along only in the roomy form. The compact form drops\n\t\t\t// it, which is what lets it keep winning the fit ladder below.\n\t\t\tconst gauge = renderContextGauge(contextPercent === \"?\" ? undefined : contextPercentValue, symbolPreset);\n\t\t\t// Below the warning threshold the text is dim, which would make the bar\n\t\t\t// invisible; the fill takes the accent there and tracks the text colour\n\t\t\t// at every level that actually signals pressure.\n\t\t\tconst gaugeColor = contextColor === \"dim\" ? \"accent\" : contextColor;\n\t\t\t// An empty run would still emit an open/close SGR pair, so skip it.\n\t\t\tconst paintRun = (color: Parameters<typeof theme.fg>[0], text: string) => (text ? theme.fg(color, text) : \"\");\n\t\t\tconst contextGauged =\n\t\t\t\ttheme.fg(contextColor, \"context \") +\n\t\t\t\tpaintRun(gaugeColor, gauge.filled) +\n\t\t\t\tpaintRun(\"borderMuted\", gauge.empty) +\n\t\t\t\ttheme.fg(contextColor, ` ${contextPercent}%${pressureMarker}`);\n\t\t\tconst contextFull = theme.fg(contextColor, `context ${contextPercent}%${pressureMarker}`);\n\t\t\tconst contextCompact = theme.fg(contextColor, `ctx ${compactPercent}%${compactPressure}`);\n\n\t\t\tconst joinSegments = (segments: string[]): string => segments.join(separator);\n\t\t\tconst fits = (segments: string[]): boolean => visibleWidth(joinSegments(segments)) <= width;\n\t\t\tlet left: string[] = [permissionFull];\n\t\t\tlet right = contextGauged;\n\n\t\t\t// The gauge is decoration over a value the text already states, so it\n\t\t\t// is the first thing to go. Dropping it must not cost the tray its\n\t\t\t// spelled-out permission mode, which is why this is its own rung.\n\t\t\tif (!fits([...left, right])) {\n\t\t\t\tright = contextFull;\n\t\t\t}\n\n\t\t\tif (!fits([...left, right])) {\n\t\t\t\tleft = [permissionCompact];\n\t\t\t\tright = contextCompact;\n\t\t\t}\n\n\t\t\tif (!fits([...left, right])) {\n\t\t\t\tconst safetyOnly = left[0];\n\t\t\t\tconst line =\n\t\t\t\t\tvisibleWidth(safetyOnly) <= width ? safetyOnly : truncateToWidth(safetyOnly, Math.max(0, width), \"\");\n\t\t\t\treturn width > 0 ? [line] : [];\n\t\t\t}\n\n\t\t\tconst optional: string[] = [];\n\t\t\toptional.push(theme.fg(\"dim\", state.model?.id || \"no-model\"));\n\t\t\tif (state.model?.reasoning && state.thinkingLevel && state.thinkingLevel !== \"off\") {\n\t\t\t\toptional.push(theme.fg(\"dim\", state.thinkingLevel));\n\t\t\t}\n\t\t\tif (tokenUsageDisplay !== \"off\" && (usageTotals.input || usageTotals.output)) {\n\t\t\t\toptional.push(\n\t\t\t\t\ttheme.fg(\n\t\t\t\t\t\t\"dim\",\n\t\t\t\t\t\t`${upSymbol}${formatTokens(usageTotals.input)} ${downSymbol}${formatTokens(usageTotals.output)}`,\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (\n\t\t\t\ttokenUsageDisplay !== \"off\" &&\n\t\t\t\t(usageTotals.cacheRead > 0 || usageTotals.cacheWrite > 0) &&\n\t\t\t\tlatestCacheHitRate !== undefined\n\t\t\t) {\n\t\t\t\toptional.push(theme.fg(\"dim\", `CH${latestCacheHitRate.toFixed(1)}%`));\n\t\t\t}\n\t\t\tif (tokenUsageDisplay !== \"off\" && (usageTotals.cost || usingSubscription)) {\n\t\t\t\toptional.push(theme.fg(\"dim\", `$${usageTotals.cost.toFixed(3)}${usingSubscription ? \" (sub)\" : \"\"}`));\n\t\t\t}\n\t\t\tif (this.footerData.getAvailableProviderCount() > 1 && state.model) {\n\t\t\t\toptional.push(theme.fg(\"dim\", state.model.provider));\n\t\t\t}\n\t\t\tif (areExperimentalFeaturesEnabled()) optional.push(theme.fg(\"warning\", \"xp\"));\n\t\t\toptional.push(theme.fg(\"dim\", pwd));\n\n\t\t\tfor (const segment of optional) {\n\t\t\t\tif (fits([...left, segment, right])) left.push(segment);\n\t\t\t}\n\n\t\t\tconst leftText = joinSegments(left);\n\t\t\tconst padding = \" \".repeat(Math.max(1, width - visibleWidth(leftText) - visibleWidth(right)));\n\t\t\tconst tray = leftText ? `${leftText}${padding}${right}` : right;\n\t\t\tconst lines = [tray];\n\t\t\tconst extensionStatuses = this.footerData.getExtensionStatuses();\n\t\t\tif (extensionStatuses.size > 0) {\n\t\t\t\tconst sortedStatuses = Array.from(extensionStatuses.entries())\n\t\t\t\t\t.sort(([a], [b]) => a.localeCompare(b))\n\t\t\t\t\t.map(([, text]) => sanitizeStatusText(text));\n\t\t\t\tlines.push(truncateToWidth(sortedStatuses.join(\" \"), width, theme.fg(\"dim\", \"...\")));\n\t\t\t}\n\t\t\treturn lines;\n\t\t}\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\t// Permission mode is the first segment and must remain textual at every\n\t\t\t// positive width. An ellipsis suffix can consume the entire narrow line.\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\"\n\t\t\t\t\t? `${modelName} ${bulletSymbol} thinking off`\n\t\t\t\t\t: `${modelName} ${bulletSymbol} ${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\tconst pwdLine = truncateToWidth(theme.fg(\"dim\", pwd), width, theme.fg(\"dim\", \"...\"));\n\t\tconst lines = [pwdLine, 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"]}
|
|
@@ -33,6 +33,8 @@ export declare class ToolExecutionComponent extends Container {
|
|
|
33
33
|
private elapsedTimer?;
|
|
34
34
|
private argsComplete;
|
|
35
35
|
private symbolPreset;
|
|
36
|
+
private displayVersion;
|
|
37
|
+
private cachedRender?;
|
|
36
38
|
private result?;
|
|
37
39
|
private convertedImages;
|
|
38
40
|
private hideComponent;
|
|
@@ -71,6 +73,16 @@ export declare class ToolExecutionComponent extends Container {
|
|
|
71
73
|
private renderDisclosureHint;
|
|
72
74
|
private renderCollapsedError;
|
|
73
75
|
render(width: number): string[];
|
|
76
|
+
/**
|
|
77
|
+
* Cache key for the composed lines. Every state change funnels through
|
|
78
|
+
* updateDisplay(), which bumps displayVersion; the elapsed readout is the
|
|
79
|
+
* one input that advances without a state change, so it is bucketed at
|
|
80
|
+
* 250 ms while running (matching the 1 s elapsed timer's cadence closely
|
|
81
|
+
* enough that the readout never looks stale, while bounding recomposition
|
|
82
|
+
* to at most four per second for the one running tool).
|
|
83
|
+
*/
|
|
84
|
+
private renderCacheKey;
|
|
85
|
+
private compose;
|
|
74
86
|
private updateDisplay;
|
|
75
87
|
private getTextOutput;
|
|
76
88
|
private formatToolExecution;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tool-execution.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/tool-execution.ts"],"names":[],"mappings":"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","sourcesContent":["import { Box, type Component, Container, getCapabilities, Image, Spacer, Text, type TUI } from \"@earendil-works/pi-tui\";\nimport type { ToolDefinition, ToolRenderContext } from \"../../../core/extensions/types.ts\";\nimport type { ApexToolDefinition } from \"../../../core/tools/contract.ts\";\nimport { createAllToolDefinitions, type ToolName } from \"../../../core/tools/index.ts\";\nimport { getTextOutput as getRenderedTextOutput } from \"../../../core/tools/render-utils.ts\";\nimport { convertToPng } from \"../../../utils/image-convert.ts\";\nimport { theme } from \"../theme/theme.ts\";\nimport { keyHint } from \"./keybinding-hints.ts\";\nimport {\n\ttype ToolLifecycle,\n\tToolPanelComponent,\n\tToolStatusLineComponent,\n\ttype ToolSymbolPreset,\n} from \"./tool-panel.ts\";\n\nconst COLLAPSED_ERROR_VISUAL_LINE_LIMIT = 3;\n\nconst FALLBACK_PREVIEW_LINES = 10;\n\nexport interface ToolExecutionOptions {\n\tshowImages?: boolean;\n\timageWidthCells?: number;\n\tsymbolPreset?: ToolSymbolPreset;\n}\n\nexport class ToolExecutionComponent extends Container {\n\tprivate contentBox: Box;\n\tprivate contentText: Text;\n\tprivate selfRenderContainer: Container;\n\tprivate callRendererComponent?: Component;\n\tprivate resultRendererComponent?: Component;\n\tprivate rendererState: any = {};\n\tprivate imageComponents: Image[] = [];\n\tprivate imageSpacers: Spacer[] = [];\n\tprivate toolName: string;\n\tprivate toolCallId: string;\n\tprivate args: any;\n\tprivate expanded = false;\n\tprivate showImages: boolean;\n\tprivate imageWidthCells: number;\n\tprivate isPartial = true;\n\tprivate toolDefinition?: ToolDefinition<any, any> | ApexToolDefinition<any, any>;\n\tprivate builtInToolDefinition?: ToolDefinition<any, any> | ApexToolDefinition<any, any>;\n\tprivate ui: TUI;\n\tprivate cwd: string;\n\tprivate executionStarted = false;\n\tprivate executionStartedAt?: number;\n\tprivate executionEndedAt?: number;\n\tprivate elapsedTimer?: ReturnType<typeof setInterval>;\n\tprivate argsComplete = false;\n\tprivate symbolPreset: ToolSymbolPreset;\n\tprivate result?: {\n\t\tcontent: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;\n\t\tisError: boolean;\n\t\tdetails?: any;\n\t};\n\tprivate convertedImages: Map<number, { data: string; mimeType: string }> = new Map();\n\tprivate hideComponent = false;\n\n\tconstructor(\n\t\ttoolName: string,\n\t\ttoolCallId: string,\n\t\targs: any,\n\t\toptions: ToolExecutionOptions = {},\n\t\ttoolDefinition: ToolDefinition<any, any> | ApexToolDefinition<any, any> | undefined,\n\t\tui: TUI,\n\t\tcwd: string,\n\t) {\n\t\tsuper();\n\t\tthis.toolName = toolName;\n\t\tthis.toolCallId = toolCallId;\n\t\tthis.args = args;\n\t\tthis.toolDefinition = toolDefinition;\n\t\tthis.builtInToolDefinition = createAllToolDefinitions(cwd)[toolName as ToolName];\n\t\tthis.showImages = options.showImages ?? true;\n\t\tthis.imageWidthCells = options.imageWidthCells ?? 60;\n\t\tthis.symbolPreset = options.symbolPreset ?? \"unicode\";\n\t\tthis.ui = ui;\n\t\tthis.cwd = cwd;\n\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Always create all shell variants. contentBox is used for default renderer-based composition.\n\t\t// selfRenderContainer is used when the tool renders its own framing.\n\t\t// contentText is reserved for generic fallback rendering when no tool definition exists.\n\t\tthis.contentBox = new Box(0, 0);\n\t\tthis.contentText = new Text(\"\", 0, 0);\n\t\tthis.selfRenderContainer = new Container();\n\n\t\tif (this.hasRendererDefinition()) {\n\t\t\tthis.addChild(this.getRenderShell() === \"self\" ? this.selfRenderContainer : this.contentBox);\n\t\t} else {\n\t\t\tthis.addChild(this.contentText);\n\t\t}\n\n\t\tthis.updateDisplay();\n\t}\n\n\tprivate getCallRenderer(): ToolDefinition<any, any>[\"renderCall\"] | undefined {\n\t\tif (!this.builtInToolDefinition) {\n\t\t\treturn this.toolDefinition?.renderCall;\n\t\t}\n\t\tif (!this.toolDefinition) {\n\t\t\treturn this.builtInToolDefinition.renderCall;\n\t\t}\n\t\treturn this.toolDefinition.renderCall ?? this.builtInToolDefinition.renderCall;\n\t}\n\n\tprivate getResultRenderer(): ToolDefinition<any, any>[\"renderResult\"] | undefined {\n\t\tif (!this.builtInToolDefinition) {\n\t\t\treturn this.toolDefinition?.renderResult;\n\t\t}\n\t\tif (!this.toolDefinition) {\n\t\t\treturn this.builtInToolDefinition.renderResult;\n\t\t}\n\t\treturn this.toolDefinition.renderResult ?? this.builtInToolDefinition.renderResult;\n\t}\n\n\tprivate hasRendererDefinition(): boolean {\n\t\treturn this.builtInToolDefinition !== undefined || this.toolDefinition !== undefined;\n\t}\n\n\tprivate getRenderShell(): \"default\" | \"self\" {\n\t\tif (!this.builtInToolDefinition) {\n\t\t\treturn this.toolDefinition?.renderShell ?? \"default\";\n\t\t}\n\t\tif (!this.toolDefinition) {\n\t\t\treturn this.builtInToolDefinition.renderShell ?? \"default\";\n\t\t}\n\t\treturn this.toolDefinition.renderShell ?? this.builtInToolDefinition.renderShell ?? \"default\";\n\t}\n\n\tprivate getRenderContext(lastComponent: Component | undefined): ToolRenderContext {\n\t\treturn {\n\t\t\targs: this.args,\n\t\t\ttoolCallId: this.toolCallId,\n\t\t\tinvalidate: () => {\n\t\t\t\tthis.invalidate();\n\t\t\t\tthis.ui.requestRender();\n\t\t\t},\n\t\t\tlastComponent,\n\t\t\tstate: this.rendererState,\n\t\t\tcwd: this.cwd,\n\t\t\texecutionStarted: this.executionStarted,\n\t\t\targsComplete: this.argsComplete,\n\t\t\tisPartial: this.isPartial,\n\t\t\texpanded: this.expanded,\n\t\t\tshowImages: this.showImages,\n\t\t\tisError: this.result?.isError ?? false,\n\t\t};\n\t}\n\n\tprivate createCallFallback(): Component {\n\t\treturn new Text(theme.fg(\"toolTitle\", theme.bold(this.toolName)), 0, 0);\n\t}\n\n\tprivate createResultFallback(): Component | undefined {\n\t\tconst output = this.getTextOutput();\n\t\tif (!output) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst lines = output.split(\"\\n\");\n\t\tconst displayLines = this.expanded ? lines : lines.slice(0, FALLBACK_PREVIEW_LINES);\n\t\tconst remaining = lines.length - displayLines.length;\n\t\tlet text = displayLines.map((line) => theme.fg(\"toolOutput\", line)).join(\"\\n\");\n\t\tif (remaining > 0) {\n\t\t\ttext += `${theme.fg(\"muted\", `\\n... (${remaining} more lines,`)} ${keyHint(\"app.tools.expand\", \"to expand\")}${theme.fg(\"muted\", \")\")}`;\n\t\t}\n\t\treturn new Text(text, 0, 0);\n\t}\n\n\tupdateArgs(args: any): void {\n\t\tthis.args = args;\n\t\tthis.updateDisplay();\n\t}\n\n\tmarkExecutionStarted(): void {\n\t\tthis.executionStarted = true;\n\t\tthis.executionStartedAt ??= Date.now();\n\t\tthis.startElapsedTimer();\n\t\tthis.updateDisplay();\n\t\tthis.ui.requestRender();\n\t}\n\n\tsetArgsComplete(): void {\n\t\tthis.argsComplete = true;\n\t\tthis.updateDisplay();\n\t\tthis.ui.requestRender();\n\t}\n\n\tupdateResult(\n\t\tresult: {\n\t\t\tcontent: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;\n\t\t\tdetails?: any;\n\t\t\tisError: boolean;\n\t\t},\n\t\tisPartial = false,\n\t): void {\n\t\tthis.result = result;\n\t\tthis.isPartial = isPartial;\n\t\tif (!isPartial) {\n\t\t\tthis.executionEndedAt ??= Date.now();\n\t\t\tthis.stopElapsedTimer();\n\t\t}\n\t\tthis.updateDisplay();\n\t\tthis.maybeConvertImagesForKitty();\n\t}\n\n\tdispose(): void {\n\t\tthis.stopElapsedTimer();\n\t}\n\n\tprivate startElapsedTimer(): void {\n\t\tif (this.elapsedTimer) return;\n\t\tthis.elapsedTimer = setInterval(() => this.ui.requestRender(), 1000);\n\t}\n\n\tprivate stopElapsedTimer(): void {\n\t\tif (!this.elapsedTimer) return;\n\t\tclearInterval(this.elapsedTimer);\n\t\tthis.elapsedTimer = undefined;\n\t}\n\n\tprivate maybeConvertImagesForKitty(): void {\n\t\tconst caps = getCapabilities();\n\t\tif (caps.images !== \"kitty\") return;\n\t\tif (!this.result) return;\n\n\t\tconst imageBlocks = this.result.content.filter((c) => c.type === \"image\");\n\t\tfor (let i = 0; i < imageBlocks.length; i++) {\n\t\t\tconst img = imageBlocks[i];\n\t\t\tif (!img.data || !img.mimeType) continue;\n\t\t\tif (img.mimeType === \"image/png\") continue;\n\t\t\tif (this.convertedImages.has(i)) continue;\n\n\t\t\tconst index = i;\n\t\t\tconvertToPng(img.data, img.mimeType).then((converted) => {\n\t\t\t\tif (converted) {\n\t\t\t\t\tthis.convertedImages.set(index, converted);\n\t\t\t\t\tthis.updateDisplay();\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t}\n\n\tsetExpanded(expanded: boolean): void {\n\t\tthis.expanded = expanded;\n\t\tthis.updateDisplay();\n\t}\n\n\tsetShowImages(show: boolean): void {\n\t\tthis.showImages = show;\n\t\tthis.updateDisplay();\n\t}\n\n\tsetImageWidthCells(width: number): void {\n\t\tthis.imageWidthCells = Math.max(1, Math.floor(width));\n\t\tthis.updateDisplay();\n\t}\n\n\toverride invalidate(): void {\n\t\tsuper.invalidate();\n\t\tthis.updateDisplay();\n\t}\n\n\tprivate getLifecycle(): ToolLifecycle {\n\t\tif (this.result && !this.isPartial) return this.result.isError ? \"error\" : \"done\";\n\t\treturn this.executionStarted ? \"running\" : \"queued\";\n\t}\n\n\tprivate getDurationMs(): number | undefined {\n\t\tif (this.executionStartedAt === undefined) return undefined;\n\t\treturn (this.executionEndedAt ?? Date.now()) - this.executionStartedAt;\n\t}\n\n\tprivate renderWithStatus(component: Component, width: number): string[] {\n\t\tconst options = {\n\t\t\tlifecycle: this.getLifecycle(),\n\t\t\tsymbolPreset: this.symbolPreset,\n\t\t\tdurationMs: this.getDurationMs(),\n\t\t};\n\t\tif (this.hasRendererDefinition() && this.getRenderShell() === \"self\") {\n\t\t\treturn new ToolStatusLineComponent(component, options).render(width);\n\t\t}\n\t\treturn new ToolPanelComponent(component, options).render(width);\n\t}\n\n\tprivate renderDisclosureHint(width: number): string[] {\n\t\tif (this.expanded || !this.result || this.isPartial || width <= 0) return [];\n\t\tconst hint = keyHint(\"app.tools.expand\", \"to expand\");\n\t\treturn new Text(hint, 0, 0).render(width);\n\t}\n\n\tprivate renderCollapsedError(contentLines: string[], width: number): string[] {\n\t\tconst errorText = this.getTextOutput();\n\t\tif (!errorText || contentLines.length === 0) return contentLines;\n\n\t\tconst errorLines = new Text(theme.fg(\"toolOutput\", errorText), 0, 0).render(width);\n\t\tconst previewLines = errorLines.slice(0, COLLAPSED_ERROR_VISUAL_LINE_LIMIT);\n\t\tconst omittedLines = errorLines.length - previewLines.length;\n\t\tif (omittedLines === 0) return [...contentLines.slice(0, 1), ...previewLines];\n\n\t\treturn [...contentLines.slice(0, 1), ...previewLines, theme.fg(\"muted\", `${omittedLines} more lines omitted`)];\n\t}\n\n\toverride render(width: number): string[] {\n\t\tif (this.hideComponent || width <= 0) return [];\n\n\t\tconst shell = this.hasRendererDefinition()\n\t\t\t? this.getRenderShell() === \"self\"\n\t\t\t\t? this.selfRenderContainer\n\t\t\t\t: this.contentBox\n\t\t\t: this.contentText;\n\t\tconst selfRendered = this.hasRendererDefinition() && this.getRenderShell() === \"self\";\n\t\tconst panelContent: Component = selfRendered\n\t\t\t? shell\n\t\t\t: {\n\t\t\t\t\trender: (innerWidth: number) => {\n\t\t\t\t\t\tconst contentLines = shell.render(innerWidth);\n\t\t\t\t\t\tconst visibleLines =\n\t\t\t\t\t\t\tthis.result?.isError && !this.isPartial && !this.expanded\n\t\t\t\t\t\t\t\t? this.renderCollapsedError(contentLines, innerWidth)\n\t\t\t\t\t\t\t\t: contentLines;\n\t\t\t\t\t\treturn [...visibleLines, ...this.renderDisclosureHint(innerWidth)];\n\t\t\t\t\t},\n\t\t\t\t\tinvalidate: () => shell.invalidate?.(),\n\t\t\t\t};\n\t\tconst visibleContentLines = this.renderWithStatus(panelContent, width);\n\t\tif (visibleContentLines.length === 0 && this.imageComponents.length === 0) return [];\n\n\t\tconst lines: string[] = [];\n\t\tif (visibleContentLines.length > 0) {\n\t\t\tlines.push(\"\");\n\t\t\tlines.push(...visibleContentLines);\n\t\t\tif (selfRendered) lines.push(...this.renderDisclosureHint(width));\n\t\t}\n\t\tfor (let i = 0; i < this.imageComponents.length; i++) {\n\t\t\tconst spacer = this.imageSpacers[i];\n\t\t\tif (spacer) lines.push(...spacer.render(width));\n\t\t\tconst imageComponent = this.imageComponents[i];\n\t\t\tif (imageComponent) lines.push(...imageComponent.render(width));\n\t\t}\n\t\treturn lines;\n\t}\n\n\tprivate updateDisplay(): void {\n\t\tlet hasContent = false;\n\t\tthis.hideComponent = false;\n\t\tif (this.hasRendererDefinition()) {\n\t\t\tconst renderContainer = this.getRenderShell() === \"self\" ? this.selfRenderContainer : this.contentBox;\n\t\t\trenderContainer.clear();\n\n\t\t\tconst callRenderer = this.getCallRenderer();\n\t\t\tif (!callRenderer) {\n\t\t\t\trenderContainer.addChild(this.createCallFallback());\n\t\t\t\thasContent = true;\n\t\t\t} else {\n\t\t\t\ttry {\n\t\t\t\t\tconst component = callRenderer(this.args, theme, this.getRenderContext(this.callRendererComponent));\n\t\t\t\t\tthis.callRendererComponent = component;\n\t\t\t\t\trenderContainer.addChild(component);\n\t\t\t\t\thasContent = true;\n\t\t\t\t} catch {\n\t\t\t\t\tthis.callRendererComponent = undefined;\n\t\t\t\t\trenderContainer.addChild(this.createCallFallback());\n\t\t\t\t\thasContent = true;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (this.result) {\n\t\t\t\tconst resultRenderer = this.getResultRenderer();\n\t\t\t\tif (!resultRenderer) {\n\t\t\t\t\tconst component = this.createResultFallback();\n\t\t\t\t\tif (component) {\n\t\t\t\t\t\trenderContainer.addChild(component);\n\t\t\t\t\t\thasContent = true;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst component = resultRenderer(\n\t\t\t\t\t\t\t{ content: this.result.content as any, details: this.result.details },\n\t\t\t\t\t\t\t{ expanded: this.expanded, isPartial: this.isPartial },\n\t\t\t\t\t\t\ttheme,\n\t\t\t\t\t\t\tthis.getRenderContext(this.resultRendererComponent),\n\t\t\t\t\t\t);\n\t\t\t\t\t\tthis.resultRendererComponent = component;\n\t\t\t\t\t\trenderContainer.addChild(component);\n\t\t\t\t\t\thasContent = true;\n\t\t\t\t\t} catch {\n\t\t\t\t\t\tthis.resultRendererComponent = undefined;\n\t\t\t\t\t\tconst component = this.createResultFallback();\n\t\t\t\t\t\tif (component) {\n\t\t\t\t\t\t\trenderContainer.addChild(component);\n\t\t\t\t\t\t\thasContent = true;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tthis.contentText.setText(this.formatToolExecution());\n\t\t\thasContent = true;\n\t\t}\n\n\t\tfor (const img of this.imageComponents) {\n\t\t\tthis.removeChild(img);\n\t\t}\n\t\tthis.imageComponents = [];\n\t\tfor (const spacer of this.imageSpacers) {\n\t\t\tthis.removeChild(spacer);\n\t\t}\n\t\tthis.imageSpacers = [];\n\n\t\tif (this.result) {\n\t\t\tconst imageBlocks = this.result.content.filter((c) => c.type === \"image\");\n\t\t\tconst caps = getCapabilities();\n\t\t\tfor (let i = 0; i < imageBlocks.length; i++) {\n\t\t\t\tconst img = imageBlocks[i];\n\t\t\t\tif (caps.images && this.showImages && img.data && img.mimeType) {\n\t\t\t\t\tconst converted = this.convertedImages.get(i);\n\t\t\t\t\tconst imageData = converted?.data ?? img.data;\n\t\t\t\t\tconst imageMimeType = converted?.mimeType ?? img.mimeType;\n\t\t\t\t\tif (caps.images === \"kitty\" && imageMimeType !== \"image/png\") continue;\n\n\t\t\t\t\tconst spacer = new Spacer(1);\n\t\t\t\t\tthis.addChild(spacer);\n\t\t\t\t\tthis.imageSpacers.push(spacer);\n\t\t\t\t\tconst imageComponent = new Image(\n\t\t\t\t\t\timageData,\n\t\t\t\t\t\timageMimeType,\n\t\t\t\t\t\t{ fallbackColor: (s: string) => theme.fg(\"toolOutput\", s) },\n\t\t\t\t\t\t{ maxWidthCells: this.imageWidthCells },\n\t\t\t\t\t);\n\t\t\t\t\tthis.imageComponents.push(imageComponent);\n\t\t\t\t\tthis.addChild(imageComponent);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (this.hasRendererDefinition() && !hasContent && this.imageComponents.length === 0) {\n\t\t\tthis.hideComponent = true;\n\t\t}\n\t}\n\n\tprivate getTextOutput(): string {\n\t\treturn getRenderedTextOutput(this.result, this.showImages);\n\t}\n\n\tprivate formatToolExecution(): string {\n\t\tlet text = theme.fg(\"toolTitle\", theme.bold(this.toolName));\n\t\tconst content = JSON.stringify(this.args, null, 2);\n\t\tif (content) {\n\t\t\ttext += `\\n\\n${content}`;\n\t\t}\n\t\tconst output = this.getTextOutput();\n\t\tif (output) {\n\t\t\ttext += `\\n${output}`;\n\t\t}\n\t\treturn text;\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"tool-execution.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/tool-execution.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuB,SAAS,EAAwC,KAAK,GAAG,EAAE,MAAM,wBAAwB,CAAC;AACxH,OAAO,KAAK,EAAE,cAAc,EAAqB,MAAM,mCAAmC,CAAC;AAC3F,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;AAM1E,OAAO,EAIN,KAAK,gBAAgB,EACrB,MAAM,iBAAiB,CAAC;AAMzB,MAAM,WAAW,oBAAoB;IACpC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,gBAAgB,CAAC;CAChC;AAED,qBAAa,sBAAuB,SAAQ,SAAS;IACpD,OAAO,CAAC,UAAU,CAAM;IACxB,OAAO,CAAC,WAAW,CAAO;IAC1B,OAAO,CAAC,mBAAmB,CAAY;IACvC,OAAO,CAAC,qBAAqB,CAAC,CAAY;IAC1C,OAAO,CAAC,uBAAuB,CAAC,CAAY;IAC5C,OAAO,CAAC,aAAa,CAAW;IAChC,OAAO,CAAC,eAAe,CAAe;IACtC,OAAO,CAAC,YAAY,CAAgB;IACpC,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,IAAI,CAAM;IAClB,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,UAAU,CAAU;IAC5B,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,SAAS,CAAQ;IACzB,OAAO,CAAC,cAAc,CAAC,CAA0D;IACjF,OAAO,CAAC,qBAAqB,CAAC,CAA0D;IACxF,OAAO,CAAC,EAAE,CAAM;IAChB,OAAO,CAAC,GAAG,CAAS;IACpB,OAAO,CAAC,gBAAgB,CAAS;IACjC,OAAO,CAAC,kBAAkB,CAAC,CAAS;IACpC,OAAO,CAAC,gBAAgB,CAAC,CAAS;IAClC,OAAO,CAAC,YAAY,CAAC,CAAiC;IACtD,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,YAAY,CAAmB;IAKvC,OAAO,CAAC,cAAc,CAAK;IAC3B,OAAO,CAAC,YAAY,CAAC,CAAkD;IACvE,OAAO,CAAC,MAAM,CAAC,CAIb;IACF,OAAO,CAAC,eAAe,CAA8D;IACrF,OAAO,CAAC,aAAa,CAAS;IAE9B,YACC,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,GAAG,EACT,OAAO,kCAA2B,EAClC,cAAc,EAAE,cAAc,CAAC,GAAG,EAAE,GAAG,CAAC,GAAG,kBAAkB,CAAC,GAAG,EAAE,GAAG,CAAC,GAAG,SAAS,EACnF,EAAE,EAAE,GAAG,EACP,GAAG,EAAE,MAAM,EA8BX;IAED,OAAO,CAAC,eAAe;IAUvB,OAAO,CAAC,iBAAiB;IAUzB,OAAO,CAAC,qBAAqB;IAI7B,OAAO,CAAC,cAAc;IAUtB,OAAO,CAAC,gBAAgB;IAoBxB,OAAO,CAAC,kBAAkB;IAI1B,OAAO,CAAC,oBAAoB;IAgB5B,UAAU,CAAC,IAAI,EAAE,GAAG,GAAG,IAAI,CAG1B;IAED,oBAAoB,IAAI,IAAI,CAM3B;IAED,eAAe,IAAI,IAAI,CAItB;IAED,YAAY,CACX,MAAM,EAAE;QACP,OAAO,EAAE,KAAK,CAAC;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,IAAI,CAAC,EAAE,MAAM,CAAC;YAAC,IAAI,CAAC,EAAE,MAAM,CAAC;YAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;QAClF,OAAO,CAAC,EAAE,GAAG,CAAC;QACd,OAAO,EAAE,OAAO,CAAC;KACjB,EACD,SAAS,UAAQ,GACf,IAAI,CASN;IAED,OAAO,IAAI,IAAI,CAGd;IAED,OAAO,CAAC,iBAAiB;IAKzB,OAAO,CAAC,gBAAgB;IAMxB,OAAO,CAAC,0BAA0B;IAuBlC,WAAW,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI,CAGnC;IAED,aAAa,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,CAGjC;IAED,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAGtC;IAEQ,UAAU,IAAI,IAAI,CAG1B;IAED,OAAO,CAAC,YAAY;IAKpB,OAAO,CAAC,aAAa;IAKrB,OAAO,CAAC,gBAAgB;IAYxB,OAAO,CAAC,oBAAoB;IAM5B,OAAO,CAAC,oBAAoB;IAYnB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAQvC;IAED;;;;;;;OAOG;IACH,OAAO,CAAC,cAAc;IAOtB,OAAO,CAAC,OAAO;IAsCf,OAAO,CAAC,aAAa;IAmGrB,OAAO,CAAC,aAAa;IAIrB,OAAO,CAAC,mBAAmB;CAY3B","sourcesContent":["import { Box, type Component, Container, getCapabilities, Image, Spacer, Text, type TUI } from \"@earendil-works/pi-tui\";\nimport type { ToolDefinition, ToolRenderContext } from \"../../../core/extensions/types.ts\";\nimport type { ApexToolDefinition } from \"../../../core/tools/contract.ts\";\nimport { createAllToolDefinitions, type ToolName } from \"../../../core/tools/index.ts\";\nimport { getTextOutput as getRenderedTextOutput } from \"../../../core/tools/render-utils.ts\";\nimport { convertToPng } from \"../../../utils/image-convert.ts\";\nimport { theme } from \"../theme/theme.ts\";\nimport { keyHint } from \"./keybinding-hints.ts\";\nimport {\n\ttype ToolLifecycle,\n\tToolPanelComponent,\n\tToolStatusLineComponent,\n\ttype ToolSymbolPreset,\n} from \"./tool-panel.ts\";\n\nconst COLLAPSED_ERROR_VISUAL_LINE_LIMIT = 3;\n\nconst FALLBACK_PREVIEW_LINES = 10;\n\nexport interface ToolExecutionOptions {\n\tshowImages?: boolean;\n\timageWidthCells?: number;\n\tsymbolPreset?: ToolSymbolPreset;\n}\n\nexport class ToolExecutionComponent extends Container {\n\tprivate contentBox: Box;\n\tprivate contentText: Text;\n\tprivate selfRenderContainer: Container;\n\tprivate callRendererComponent?: Component;\n\tprivate resultRendererComponent?: Component;\n\tprivate rendererState: any = {};\n\tprivate imageComponents: Image[] = [];\n\tprivate imageSpacers: Spacer[] = [];\n\tprivate toolName: string;\n\tprivate toolCallId: string;\n\tprivate args: any;\n\tprivate expanded = false;\n\tprivate showImages: boolean;\n\tprivate imageWidthCells: number;\n\tprivate isPartial = true;\n\tprivate toolDefinition?: ToolDefinition<any, any> | ApexToolDefinition<any, any>;\n\tprivate builtInToolDefinition?: ToolDefinition<any, any> | ApexToolDefinition<any, any>;\n\tprivate ui: TUI;\n\tprivate cwd: string;\n\tprivate executionStarted = false;\n\tprivate executionStartedAt?: number;\n\tprivate executionEndedAt?: number;\n\tprivate elapsedTimer?: ReturnType<typeof setInterval>;\n\tprivate argsComplete = false;\n\tprivate symbolPreset: ToolSymbolPreset;\n\t// Composed-line cache. Recomposing the panel (ANSI-aware truncation, width\n\t// measurement, background paint per content line) on every frame made\n\t// per-frame cost grow with the number of tools on screen; see\n\t// test/tool-execution-render-cache.test.ts.\n\tprivate displayVersion = 0;\n\tprivate cachedRender?: { key: string; width: number; lines: string[] };\n\tprivate result?: {\n\t\tcontent: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;\n\t\tisError: boolean;\n\t\tdetails?: any;\n\t};\n\tprivate convertedImages: Map<number, { data: string; mimeType: string }> = new Map();\n\tprivate hideComponent = false;\n\n\tconstructor(\n\t\ttoolName: string,\n\t\ttoolCallId: string,\n\t\targs: any,\n\t\toptions: ToolExecutionOptions = {},\n\t\ttoolDefinition: ToolDefinition<any, any> | ApexToolDefinition<any, any> | undefined,\n\t\tui: TUI,\n\t\tcwd: string,\n\t) {\n\t\tsuper();\n\t\tthis.toolName = toolName;\n\t\tthis.toolCallId = toolCallId;\n\t\tthis.args = args;\n\t\tthis.toolDefinition = toolDefinition;\n\t\tthis.builtInToolDefinition = createAllToolDefinitions(cwd)[toolName as ToolName];\n\t\tthis.showImages = options.showImages ?? true;\n\t\tthis.imageWidthCells = options.imageWidthCells ?? 60;\n\t\tthis.symbolPreset = options.symbolPreset ?? \"unicode\";\n\t\tthis.ui = ui;\n\t\tthis.cwd = cwd;\n\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Always create all shell variants. contentBox is used for default renderer-based composition.\n\t\t// selfRenderContainer is used when the tool renders its own framing.\n\t\t// contentText is reserved for generic fallback rendering when no tool definition exists.\n\t\tthis.contentBox = new Box(0, 0);\n\t\tthis.contentText = new Text(\"\", 0, 0);\n\t\tthis.selfRenderContainer = new Container();\n\n\t\tif (this.hasRendererDefinition()) {\n\t\t\tthis.addChild(this.getRenderShell() === \"self\" ? this.selfRenderContainer : this.contentBox);\n\t\t} else {\n\t\t\tthis.addChild(this.contentText);\n\t\t}\n\n\t\tthis.updateDisplay();\n\t}\n\n\tprivate getCallRenderer(): ToolDefinition<any, any>[\"renderCall\"] | undefined {\n\t\tif (!this.builtInToolDefinition) {\n\t\t\treturn this.toolDefinition?.renderCall;\n\t\t}\n\t\tif (!this.toolDefinition) {\n\t\t\treturn this.builtInToolDefinition.renderCall;\n\t\t}\n\t\treturn this.toolDefinition.renderCall ?? this.builtInToolDefinition.renderCall;\n\t}\n\n\tprivate getResultRenderer(): ToolDefinition<any, any>[\"renderResult\"] | undefined {\n\t\tif (!this.builtInToolDefinition) {\n\t\t\treturn this.toolDefinition?.renderResult;\n\t\t}\n\t\tif (!this.toolDefinition) {\n\t\t\treturn this.builtInToolDefinition.renderResult;\n\t\t}\n\t\treturn this.toolDefinition.renderResult ?? this.builtInToolDefinition.renderResult;\n\t}\n\n\tprivate hasRendererDefinition(): boolean {\n\t\treturn this.builtInToolDefinition !== undefined || this.toolDefinition !== undefined;\n\t}\n\n\tprivate getRenderShell(): \"default\" | \"self\" {\n\t\tif (!this.builtInToolDefinition) {\n\t\t\treturn this.toolDefinition?.renderShell ?? \"default\";\n\t\t}\n\t\tif (!this.toolDefinition) {\n\t\t\treturn this.builtInToolDefinition.renderShell ?? \"default\";\n\t\t}\n\t\treturn this.toolDefinition.renderShell ?? this.builtInToolDefinition.renderShell ?? \"default\";\n\t}\n\n\tprivate getRenderContext(lastComponent: Component | undefined): ToolRenderContext {\n\t\treturn {\n\t\t\targs: this.args,\n\t\t\ttoolCallId: this.toolCallId,\n\t\t\tinvalidate: () => {\n\t\t\t\tthis.invalidate();\n\t\t\t\tthis.ui.requestRender();\n\t\t\t},\n\t\t\tlastComponent,\n\t\t\tstate: this.rendererState,\n\t\t\tcwd: this.cwd,\n\t\t\texecutionStarted: this.executionStarted,\n\t\t\targsComplete: this.argsComplete,\n\t\t\tisPartial: this.isPartial,\n\t\t\texpanded: this.expanded,\n\t\t\tshowImages: this.showImages,\n\t\t\tisError: this.result?.isError ?? false,\n\t\t};\n\t}\n\n\tprivate createCallFallback(): Component {\n\t\treturn new Text(theme.fg(\"toolTitle\", theme.bold(this.toolName)), 0, 0);\n\t}\n\n\tprivate createResultFallback(): Component | undefined {\n\t\tconst output = this.getTextOutput();\n\t\tif (!output) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst lines = output.split(\"\\n\");\n\t\tconst displayLines = this.expanded ? lines : lines.slice(0, FALLBACK_PREVIEW_LINES);\n\t\tconst remaining = lines.length - displayLines.length;\n\t\tlet text = displayLines.map((line) => theme.fg(\"toolOutput\", line)).join(\"\\n\");\n\t\tif (remaining > 0) {\n\t\t\ttext += `${theme.fg(\"muted\", `\\n... (${remaining} more lines,`)} ${keyHint(\"app.tools.expand\", \"to expand\")}${theme.fg(\"muted\", \")\")}`;\n\t\t}\n\t\treturn new Text(text, 0, 0);\n\t}\n\n\tupdateArgs(args: any): void {\n\t\tthis.args = args;\n\t\tthis.updateDisplay();\n\t}\n\n\tmarkExecutionStarted(): void {\n\t\tthis.executionStarted = true;\n\t\tthis.executionStartedAt ??= Date.now();\n\t\tthis.startElapsedTimer();\n\t\tthis.updateDisplay();\n\t\tthis.ui.requestRender();\n\t}\n\n\tsetArgsComplete(): void {\n\t\tthis.argsComplete = true;\n\t\tthis.updateDisplay();\n\t\tthis.ui.requestRender();\n\t}\n\n\tupdateResult(\n\t\tresult: {\n\t\t\tcontent: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;\n\t\t\tdetails?: any;\n\t\t\tisError: boolean;\n\t\t},\n\t\tisPartial = false,\n\t): void {\n\t\tthis.result = result;\n\t\tthis.isPartial = isPartial;\n\t\tif (!isPartial) {\n\t\t\tthis.executionEndedAt ??= Date.now();\n\t\t\tthis.stopElapsedTimer();\n\t\t}\n\t\tthis.updateDisplay();\n\t\tthis.maybeConvertImagesForKitty();\n\t}\n\n\tdispose(): void {\n\t\tthis.stopElapsedTimer();\n\t\tthis.cachedRender = undefined;\n\t}\n\n\tprivate startElapsedTimer(): void {\n\t\tif (this.elapsedTimer) return;\n\t\tthis.elapsedTimer = setInterval(() => this.ui.requestRender(), 1000);\n\t}\n\n\tprivate stopElapsedTimer(): void {\n\t\tif (!this.elapsedTimer) return;\n\t\tclearInterval(this.elapsedTimer);\n\t\tthis.elapsedTimer = undefined;\n\t}\n\n\tprivate maybeConvertImagesForKitty(): void {\n\t\tconst caps = getCapabilities();\n\t\tif (caps.images !== \"kitty\") return;\n\t\tif (!this.result) return;\n\n\t\tconst imageBlocks = this.result.content.filter((c) => c.type === \"image\");\n\t\tfor (let i = 0; i < imageBlocks.length; i++) {\n\t\t\tconst img = imageBlocks[i];\n\t\t\tif (!img.data || !img.mimeType) continue;\n\t\t\tif (img.mimeType === \"image/png\") continue;\n\t\t\tif (this.convertedImages.has(i)) continue;\n\n\t\t\tconst index = i;\n\t\t\tconvertToPng(img.data, img.mimeType).then((converted) => {\n\t\t\t\tif (converted) {\n\t\t\t\t\tthis.convertedImages.set(index, converted);\n\t\t\t\t\tthis.updateDisplay();\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t}\n\n\tsetExpanded(expanded: boolean): void {\n\t\tthis.expanded = expanded;\n\t\tthis.updateDisplay();\n\t}\n\n\tsetShowImages(show: boolean): void {\n\t\tthis.showImages = show;\n\t\tthis.updateDisplay();\n\t}\n\n\tsetImageWidthCells(width: number): void {\n\t\tthis.imageWidthCells = Math.max(1, Math.floor(width));\n\t\tthis.updateDisplay();\n\t}\n\n\toverride invalidate(): void {\n\t\tsuper.invalidate();\n\t\tthis.updateDisplay();\n\t}\n\n\tprivate getLifecycle(): ToolLifecycle {\n\t\tif (this.result && !this.isPartial) return this.result.isError ? \"error\" : \"done\";\n\t\treturn this.executionStarted ? \"running\" : \"queued\";\n\t}\n\n\tprivate getDurationMs(): number | undefined {\n\t\tif (this.executionStartedAt === undefined) return undefined;\n\t\treturn (this.executionEndedAt ?? Date.now()) - this.executionStartedAt;\n\t}\n\n\tprivate renderWithStatus(component: Component, width: number): string[] {\n\t\tconst options = {\n\t\t\tlifecycle: this.getLifecycle(),\n\t\t\tsymbolPreset: this.symbolPreset,\n\t\t\tdurationMs: this.getDurationMs(),\n\t\t};\n\t\tif (this.hasRendererDefinition() && this.getRenderShell() === \"self\") {\n\t\t\treturn new ToolStatusLineComponent(component, options).render(width);\n\t\t}\n\t\treturn new ToolPanelComponent(component, options).render(width);\n\t}\n\n\tprivate renderDisclosureHint(width: number): string[] {\n\t\tif (this.expanded || !this.result || this.isPartial || width <= 0) return [];\n\t\tconst hint = keyHint(\"app.tools.expand\", \"to expand\");\n\t\treturn new Text(hint, 0, 0).render(width);\n\t}\n\n\tprivate renderCollapsedError(contentLines: string[], width: number): string[] {\n\t\tconst errorText = this.getTextOutput();\n\t\tif (!errorText || contentLines.length === 0) return contentLines;\n\n\t\tconst errorLines = new Text(theme.fg(\"toolOutput\", errorText), 0, 0).render(width);\n\t\tconst previewLines = errorLines.slice(0, COLLAPSED_ERROR_VISUAL_LINE_LIMIT);\n\t\tconst omittedLines = errorLines.length - previewLines.length;\n\t\tif (omittedLines === 0) return [...contentLines.slice(0, 1), ...previewLines];\n\n\t\treturn [...contentLines.slice(0, 1), ...previewLines, theme.fg(\"muted\", `${omittedLines} more lines omitted`)];\n\t}\n\n\toverride render(width: number): string[] {\n\t\tif (this.hideComponent || width <= 0) return [];\n\t\tconst key = this.renderCacheKey();\n\t\tconst cached = this.cachedRender;\n\t\tif (cached && cached.key === key && cached.width === width) return cached.lines;\n\t\tconst lines = this.compose(width);\n\t\tthis.cachedRender = { key, width, lines };\n\t\treturn lines;\n\t}\n\n\t/**\n\t * Cache key for the composed lines. Every state change funnels through\n\t * updateDisplay(), which bumps displayVersion; the elapsed readout is the\n\t * one input that advances without a state change, so it is bucketed at\n\t * 250 ms while running (matching the 1 s elapsed timer's cadence closely\n\t * enough that the readout never looks stale, while bounding recomposition\n\t * to at most four per second for the one running tool).\n\t */\n\tprivate renderCacheKey(): string {\n\t\tconst lifecycle = this.getLifecycle();\n\t\tconst durationMs = this.getDurationMs();\n\t\tconst durationKey = lifecycle === \"running\" ? Math.floor((durationMs ?? 0) / 250) : (durationMs ?? -1);\n\t\treturn `${this.displayVersion}:${lifecycle}:${durationKey}`;\n\t}\n\n\tprivate compose(width: number): string[] {\n\t\tconst shell = this.hasRendererDefinition()\n\t\t\t? this.getRenderShell() === \"self\"\n\t\t\t\t? this.selfRenderContainer\n\t\t\t\t: this.contentBox\n\t\t\t: this.contentText;\n\t\tconst selfRendered = this.hasRendererDefinition() && this.getRenderShell() === \"self\";\n\t\tconst panelContent: Component = selfRendered\n\t\t\t? shell\n\t\t\t: {\n\t\t\t\t\trender: (innerWidth: number) => {\n\t\t\t\t\t\tconst contentLines = shell.render(innerWidth);\n\t\t\t\t\t\tconst visibleLines =\n\t\t\t\t\t\t\tthis.result?.isError && !this.isPartial && !this.expanded\n\t\t\t\t\t\t\t\t? this.renderCollapsedError(contentLines, innerWidth)\n\t\t\t\t\t\t\t\t: contentLines;\n\t\t\t\t\t\treturn [...visibleLines, ...this.renderDisclosureHint(innerWidth)];\n\t\t\t\t\t},\n\t\t\t\t\tinvalidate: () => shell.invalidate?.(),\n\t\t\t\t};\n\t\tconst visibleContentLines = this.renderWithStatus(panelContent, width);\n\t\tif (visibleContentLines.length === 0 && this.imageComponents.length === 0) return [];\n\n\t\tconst lines: string[] = [];\n\t\tif (visibleContentLines.length > 0) {\n\t\t\tlines.push(\"\");\n\t\t\tlines.push(...visibleContentLines);\n\t\t\tif (selfRendered) lines.push(...this.renderDisclosureHint(width));\n\t\t}\n\t\tfor (let i = 0; i < this.imageComponents.length; i++) {\n\t\t\tconst spacer = this.imageSpacers[i];\n\t\t\tif (spacer) lines.push(...spacer.render(width));\n\t\t\tconst imageComponent = this.imageComponents[i];\n\t\t\tif (imageComponent) lines.push(...imageComponent.render(width));\n\t\t}\n\t\treturn lines;\n\t}\n\n\tprivate updateDisplay(): void {\n\t\tthis.displayVersion += 1;\n\t\tlet hasContent = false;\n\t\tthis.hideComponent = false;\n\t\tif (this.hasRendererDefinition()) {\n\t\t\tconst renderContainer = this.getRenderShell() === \"self\" ? this.selfRenderContainer : this.contentBox;\n\t\t\trenderContainer.clear();\n\n\t\t\tconst callRenderer = this.getCallRenderer();\n\t\t\tif (!callRenderer) {\n\t\t\t\trenderContainer.addChild(this.createCallFallback());\n\t\t\t\thasContent = true;\n\t\t\t} else {\n\t\t\t\ttry {\n\t\t\t\t\tconst component = callRenderer(this.args, theme, this.getRenderContext(this.callRendererComponent));\n\t\t\t\t\tthis.callRendererComponent = component;\n\t\t\t\t\trenderContainer.addChild(component);\n\t\t\t\t\thasContent = true;\n\t\t\t\t} catch {\n\t\t\t\t\tthis.callRendererComponent = undefined;\n\t\t\t\t\trenderContainer.addChild(this.createCallFallback());\n\t\t\t\t\thasContent = true;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (this.result) {\n\t\t\t\tconst resultRenderer = this.getResultRenderer();\n\t\t\t\tif (!resultRenderer) {\n\t\t\t\t\tconst component = this.createResultFallback();\n\t\t\t\t\tif (component) {\n\t\t\t\t\t\trenderContainer.addChild(component);\n\t\t\t\t\t\thasContent = true;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst component = resultRenderer(\n\t\t\t\t\t\t\t{ content: this.result.content as any, details: this.result.details },\n\t\t\t\t\t\t\t{ expanded: this.expanded, isPartial: this.isPartial },\n\t\t\t\t\t\t\ttheme,\n\t\t\t\t\t\t\tthis.getRenderContext(this.resultRendererComponent),\n\t\t\t\t\t\t);\n\t\t\t\t\t\tthis.resultRendererComponent = component;\n\t\t\t\t\t\trenderContainer.addChild(component);\n\t\t\t\t\t\thasContent = true;\n\t\t\t\t\t} catch {\n\t\t\t\t\t\tthis.resultRendererComponent = undefined;\n\t\t\t\t\t\tconst component = this.createResultFallback();\n\t\t\t\t\t\tif (component) {\n\t\t\t\t\t\t\trenderContainer.addChild(component);\n\t\t\t\t\t\t\thasContent = true;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tthis.contentText.setText(this.formatToolExecution());\n\t\t\thasContent = true;\n\t\t}\n\n\t\tfor (const img of this.imageComponents) {\n\t\t\tthis.removeChild(img);\n\t\t}\n\t\tthis.imageComponents = [];\n\t\tfor (const spacer of this.imageSpacers) {\n\t\t\tthis.removeChild(spacer);\n\t\t}\n\t\tthis.imageSpacers = [];\n\n\t\tif (this.result) {\n\t\t\tconst imageBlocks = this.result.content.filter((c) => c.type === \"image\");\n\t\t\tconst caps = getCapabilities();\n\t\t\tfor (let i = 0; i < imageBlocks.length; i++) {\n\t\t\t\tconst img = imageBlocks[i];\n\t\t\t\tif (caps.images && this.showImages && img.data && img.mimeType) {\n\t\t\t\t\tconst converted = this.convertedImages.get(i);\n\t\t\t\t\tconst imageData = converted?.data ?? img.data;\n\t\t\t\t\tconst imageMimeType = converted?.mimeType ?? img.mimeType;\n\t\t\t\t\tif (caps.images === \"kitty\" && imageMimeType !== \"image/png\") continue;\n\n\t\t\t\t\tconst spacer = new Spacer(1);\n\t\t\t\t\tthis.addChild(spacer);\n\t\t\t\t\tthis.imageSpacers.push(spacer);\n\t\t\t\t\tconst imageComponent = new Image(\n\t\t\t\t\t\timageData,\n\t\t\t\t\t\timageMimeType,\n\t\t\t\t\t\t{ fallbackColor: (s: string) => theme.fg(\"toolOutput\", s) },\n\t\t\t\t\t\t{ maxWidthCells: this.imageWidthCells },\n\t\t\t\t\t);\n\t\t\t\t\tthis.imageComponents.push(imageComponent);\n\t\t\t\t\tthis.addChild(imageComponent);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (this.hasRendererDefinition() && !hasContent && this.imageComponents.length === 0) {\n\t\t\tthis.hideComponent = true;\n\t\t}\n\t}\n\n\tprivate getTextOutput(): string {\n\t\treturn getRenderedTextOutput(this.result, this.showImages);\n\t}\n\n\tprivate formatToolExecution(): string {\n\t\tlet text = theme.fg(\"toolTitle\", theme.bold(this.toolName));\n\t\tconst content = JSON.stringify(this.args, null, 2);\n\t\tif (content) {\n\t\t\ttext += `\\n\\n${content}`;\n\t\t}\n\t\tconst output = this.getTextOutput();\n\t\tif (output) {\n\t\t\ttext += `\\n${output}`;\n\t\t}\n\t\treturn text;\n\t}\n}\n"]}
|
|
@@ -33,6 +33,12 @@ export class ToolExecutionComponent extends Container {
|
|
|
33
33
|
elapsedTimer;
|
|
34
34
|
argsComplete = false;
|
|
35
35
|
symbolPreset;
|
|
36
|
+
// Composed-line cache. Recomposing the panel (ANSI-aware truncation, width
|
|
37
|
+
// measurement, background paint per content line) on every frame made
|
|
38
|
+
// per-frame cost grow with the number of tools on screen; see
|
|
39
|
+
// test/tool-execution-render-cache.test.ts.
|
|
40
|
+
displayVersion = 0;
|
|
41
|
+
cachedRender;
|
|
36
42
|
result;
|
|
37
43
|
convertedImages = new Map();
|
|
38
44
|
hideComponent = false;
|
|
@@ -157,6 +163,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
157
163
|
}
|
|
158
164
|
dispose() {
|
|
159
165
|
this.stopElapsedTimer();
|
|
166
|
+
this.cachedRender = undefined;
|
|
160
167
|
}
|
|
161
168
|
startElapsedTimer() {
|
|
162
169
|
if (this.elapsedTimer)
|
|
@@ -251,6 +258,29 @@ export class ToolExecutionComponent extends Container {
|
|
|
251
258
|
render(width) {
|
|
252
259
|
if (this.hideComponent || width <= 0)
|
|
253
260
|
return [];
|
|
261
|
+
const key = this.renderCacheKey();
|
|
262
|
+
const cached = this.cachedRender;
|
|
263
|
+
if (cached && cached.key === key && cached.width === width)
|
|
264
|
+
return cached.lines;
|
|
265
|
+
const lines = this.compose(width);
|
|
266
|
+
this.cachedRender = { key, width, lines };
|
|
267
|
+
return lines;
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Cache key for the composed lines. Every state change funnels through
|
|
271
|
+
* updateDisplay(), which bumps displayVersion; the elapsed readout is the
|
|
272
|
+
* one input that advances without a state change, so it is bucketed at
|
|
273
|
+
* 250 ms while running (matching the 1 s elapsed timer's cadence closely
|
|
274
|
+
* enough that the readout never looks stale, while bounding recomposition
|
|
275
|
+
* to at most four per second for the one running tool).
|
|
276
|
+
*/
|
|
277
|
+
renderCacheKey() {
|
|
278
|
+
const lifecycle = this.getLifecycle();
|
|
279
|
+
const durationMs = this.getDurationMs();
|
|
280
|
+
const durationKey = lifecycle === "running" ? Math.floor((durationMs ?? 0) / 250) : (durationMs ?? -1);
|
|
281
|
+
return `${this.displayVersion}:${lifecycle}:${durationKey}`;
|
|
282
|
+
}
|
|
283
|
+
compose(width) {
|
|
254
284
|
const shell = this.hasRendererDefinition()
|
|
255
285
|
? this.getRenderShell() === "self"
|
|
256
286
|
? this.selfRenderContainer
|
|
@@ -290,6 +320,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
290
320
|
return lines;
|
|
291
321
|
}
|
|
292
322
|
updateDisplay() {
|
|
323
|
+
this.displayVersion += 1;
|
|
293
324
|
let hasContent = false;
|
|
294
325
|
this.hideComponent = false;
|
|
295
326
|
if (this.hasRendererDefinition()) {
|