@code-yeongyu/senpi 2026.5.23 → 2026.5.24
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 +26 -0
- package/dist/core/extensions/builtin/history-search/filter.d.ts +3 -0
- package/dist/core/extensions/builtin/history-search/filter.d.ts.map +1 -0
- package/dist/core/extensions/builtin/history-search/filter.js +22 -0
- package/dist/core/extensions/builtin/history-search/filter.js.map +1 -0
- package/dist/core/extensions/builtin/history-search/index.d.ts +7 -0
- package/dist/core/extensions/builtin/history-search/index.d.ts.map +1 -0
- package/dist/core/extensions/builtin/history-search/index.js +45 -0
- package/dist/core/extensions/builtin/history-search/index.js.map +1 -0
- package/dist/core/extensions/builtin/history-search/indexer.d.ts +3 -0
- package/dist/core/extensions/builtin/history-search/indexer.d.ts.map +1 -0
- package/dist/core/extensions/builtin/history-search/indexer.js +161 -0
- package/dist/core/extensions/builtin/history-search/indexer.js.map +1 -0
- package/dist/core/extensions/builtin/history-search/overlay.d.ts +30 -0
- package/dist/core/extensions/builtin/history-search/overlay.d.ts.map +1 -0
- package/dist/core/extensions/builtin/history-search/overlay.js +115 -0
- package/dist/core/extensions/builtin/history-search/overlay.js.map +1 -0
- package/dist/core/extensions/builtin/history-search/types.d.ts +8 -0
- package/dist/core/extensions/builtin/history-search/types.d.ts.map +1 -0
- package/dist/core/extensions/builtin/history-search/types.js +2 -0
- package/dist/core/extensions/builtin/history-search/types.js.map +1 -0
- package/dist/core/extensions/builtin/index.d.ts.map +1 -1
- package/dist/core/extensions/builtin/index.js +4 -0
- package/dist/core/extensions/builtin/index.js.map +1 -1
- package/dist/core/extensions/builtin/session-observer/index.d.ts +5 -0
- package/dist/core/extensions/builtin/session-observer/index.d.ts.map +1 -0
- package/dist/core/extensions/builtin/session-observer/index.js +36 -0
- package/dist/core/extensions/builtin/session-observer/index.js.map +1 -0
- package/dist/core/extensions/builtin/session-observer/loader.d.ts +3 -0
- package/dist/core/extensions/builtin/session-observer/loader.d.ts.map +1 -0
- package/dist/core/extensions/builtin/session-observer/loader.js +20 -0
- package/dist/core/extensions/builtin/session-observer/loader.js.map +1 -0
- package/dist/core/extensions/builtin/session-observer/overlay-format.d.ts +7 -0
- package/dist/core/extensions/builtin/session-observer/overlay-format.d.ts.map +1 -0
- package/dist/core/extensions/builtin/session-observer/overlay-format.js +30 -0
- package/dist/core/extensions/builtin/session-observer/overlay-format.js.map +1 -0
- package/dist/core/extensions/builtin/session-observer/overlay.d.ts +51 -0
- package/dist/core/extensions/builtin/session-observer/overlay.d.ts.map +1 -0
- package/dist/core/extensions/builtin/session-observer/overlay.js +239 -0
- package/dist/core/extensions/builtin/session-observer/overlay.js.map +1 -0
- package/dist/core/extensions/builtin/session-observer/scanner.d.ts +10 -0
- package/dist/core/extensions/builtin/session-observer/scanner.d.ts.map +1 -0
- package/dist/core/extensions/builtin/session-observer/scanner.js +140 -0
- package/dist/core/extensions/builtin/session-observer/scanner.js.map +1 -0
- package/dist/core/extensions/builtin/session-observer/text.d.ts +7 -0
- package/dist/core/extensions/builtin/session-observer/text.d.ts.map +1 -0
- package/dist/core/extensions/builtin/session-observer/text.js +37 -0
- package/dist/core/extensions/builtin/session-observer/text.js.map +1 -0
- package/dist/core/extensions/builtin/session-observer/transcript-entries.d.ts +7 -0
- package/dist/core/extensions/builtin/session-observer/transcript-entries.d.ts.map +1 -0
- package/dist/core/extensions/builtin/session-observer/transcript-entries.js +71 -0
- package/dist/core/extensions/builtin/session-observer/transcript-entries.js.map +1 -0
- package/dist/core/extensions/builtin/session-observer/transcript-format.d.ts +11 -0
- package/dist/core/extensions/builtin/session-observer/transcript-format.d.ts.map +1 -0
- package/dist/core/extensions/builtin/session-observer/transcript-format.js +65 -0
- package/dist/core/extensions/builtin/session-observer/transcript-format.js.map +1 -0
- package/dist/core/extensions/builtin/session-observer/transcript.d.ts +4 -0
- package/dist/core/extensions/builtin/session-observer/transcript.d.ts.map +1 -0
- package/dist/core/extensions/builtin/session-observer/transcript.js +81 -0
- package/dist/core/extensions/builtin/session-observer/transcript.js.map +1 -0
- package/dist/core/extensions/builtin/session-observer/types.d.ts +33 -0
- package/dist/core/extensions/builtin/session-observer/types.d.ts.map +1 -0
- package/dist/core/extensions/builtin/session-observer/types.js +2 -0
- package/dist/core/extensions/builtin/session-observer/types.js.map +1 -0
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +1 -0
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/keybindings.d.ts +10 -0
- package/dist/core/keybindings.d.ts.map +1 -1
- package/dist/core/keybindings.js +3 -0
- package/dist/core/keybindings.js.map +1 -1
- package/dist/core/package-manager.d.ts.map +1 -1
- package/dist/core/package-manager.js +16 -4
- package/dist/core/package-manager.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +74 -63
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +18 -0
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/utils/paths.d.ts +1 -0
- package/dist/utils/paths.d.ts.map +1 -1
- package/dist/utils/paths.js +8 -0
- package/dist/utils/paths.js.map +1 -1
- package/docs/terminal-setup.md +6 -0
- package/node_modules/@earendil-works/pi-agent-core/package.json +2 -2
- package/node_modules/@earendil-works/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/models.generated.js +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/models.generated.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/anthropic.js +53 -11
- package/node_modules/@earendil-works/pi-ai/dist/providers/anthropic.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/package.json +1 -1
- package/node_modules/@earendil-works/pi-tui/dist/native-modifiers.d.ts +3 -0
- package/node_modules/@earendil-works/pi-tui/dist/native-modifiers.d.ts.map +1 -0
- package/node_modules/@earendil-works/pi-tui/dist/native-modifiers.js +53 -0
- package/node_modules/@earendil-works/pi-tui/dist/native-modifiers.js.map +1 -0
- package/node_modules/@earendil-works/pi-tui/dist/terminal.d.ts +2 -0
- package/node_modules/@earendil-works/pi-tui/dist/terminal.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-tui/dist/terminal.js +13 -1
- package/node_modules/@earendil-works/pi-tui/dist/terminal.js.map +1 -1
- package/node_modules/@earendil-works/pi-tui/package.json +2 -2
- package/npm-shrinkwrap.json +12 -12
- package/package.json +4 -4
|
@@ -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;AACnE,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,uCAAuC,CAAC;AAmBxF,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,OAAO,CAAe;IAC9B,OAAO,CAAC,UAAU,CAA6B;IAC/C,OAAO,CAAC,kBAAkB,CAAQ;IAElC,YAAY,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,0BAA0B,EAGxE;IAED,UAAU,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI,CAEtC;IAED,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAE5C;IAED;;;OAGG;IACH,UAAU,IAAI,IAAI,CAEjB;IAED;;;OAGG;IACH,OAAO,IAAI,IAAI,CAEd;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CA0J9B;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 type { ReadonlyFooterDataProvider } from \"../../../core/footer-data-provider.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\nfunction formatTokens(count: number): string {\n\treturn count.toLocaleString(\"en-US\");\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 === \"\" ? \"~\" : `~${sep}${relativeToHome}`;\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 session: AgentSession;\n\tprivate footerData: ReadonlyFooterDataProvider;\n\tprivate autoCompactEnabled = true;\n\n\tconstructor(session: AgentSession, footerData: ReadonlyFooterDataProvider) {\n\t\tthis.session = session;\n\t\tthis.footerData = footerData;\n\t}\n\n\tsetSession(session: AgentSession): void {\n\t\tthis.session = session;\n\t}\n\n\tsetAutoCompactEnabled(enabled: boolean): void {\n\t\tthis.autoCompactEnabled = enabled;\n\t}\n\n\t/**\n\t * No-op: git branch caching now handled by provider.\n\t * Kept for compatibility with existing call sites in interactive-mode.\n\t */\n\tinvalidate(): void {\n\t\t// No-op: git branch is cached/invalidated by provider\n\t}\n\n\t/**\n\t * Clean up resources.\n\t * Git watcher cleanup now handled by provider.\n\t */\n\tdispose(): void {\n\t\t// Git watcher cleanup handled by provider\n\t}\n\n\trender(width: number): string[] {\n\t\tconst state = this.session.state;\n\n\t\tlet totalInput = 0;\n\t\tlet totalOutput = 0;\n\t\tlet totalCacheRead = 0;\n\t\tlet totalCacheWrite = 0;\n\t\tlet totalCost = 0;\n\n\t\tfor (const entry of this.session.sessionManager.getEntries()) {\n\t\t\tif (entry.type === \"message\" && entry.message.role === \"assistant\") {\n\t\t\t\ttotalInput += entry.message.usage.input;\n\t\t\t\ttotalOutput += entry.message.usage.output;\n\t\t\t\ttotalCacheRead += entry.message.usage.cacheRead;\n\t\t\t\ttotalCacheWrite += entry.message.usage.cacheWrite;\n\t\t\t\ttotalCost += entry.message.usage.cost.total;\n\t\t\t}\n\t\t}\n\n\t\t// Calculate context usage from session (handles compaction correctly).\n\t\t// After compaction, tokens are unknown until the next LLM response.\n\t\tconst contextUsage = this.session.getContextUsage();\n\t\tconst contextWindow = contextUsage?.contextWindow ?? state.model?.contextWindow ?? 0;\n\t\tconst contextPercentValue = contextUsage?.percent ?? 0;\n\t\tconst contextPercent = contextUsage?.percent !== null ? contextPercentValue.toFixed(1) : \"?\";\n\t\tconst contextTokens =\n\t\t\ttypeof contextUsage?.tokens === \"number\"\n\t\t\t\t? formatTokens(contextUsage.tokens)\n\t\t\t\t: typeof contextUsage?.percent === \"number\"\n\t\t\t\t\t? formatTokens(Math.round((contextWindow * contextUsage.percent) / 100))\n\t\t\t\t\t: \"?\";\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} • ${sessionName}`;\n\t\t}\n\n\t\tconst statsParts: string[] = [];\n\t\tif (totalInput) statsParts.push(`↑${formatTokens(totalInput)}`);\n\t\tif (totalOutput) statsParts.push(`↓${formatTokens(totalOutput)}`);\n\t\tif (totalCacheRead || totalCacheWrite) {\n\t\t\tstatsParts.push(`cache ${formatTokens(totalCacheRead)}/${formatTokens(totalCacheWrite)}`);\n\t\t}\n\n\t\t// Show cost with \"(sub)\" indicator if using OAuth subscription\n\t\tconst usingSubscription = state.model ? this.session.modelRegistry.isUsingOAuth(state.model) : false;\n\t\tif (totalCost || usingSubscription) {\n\t\t\tconst costStr = `$${totalCost.toFixed(3)}${usingSubscription ? \" (sub)\" : \"\"}`;\n\t\t\tstatsParts.push(costStr);\n\t\t}\n\n\t\tlet contextPercentStr: string;\n\t\tconst autoIndicator = this.autoCompactEnabled ? \" (auto)\" : \"\";\n\t\tconst contextPercentDisplay =\n\t\t\tcontextPercent === \"?\"\n\t\t\t\t? `${contextTokens}/${formatTokens(contextWindow)} (?)${autoIndicator}`\n\t\t\t\t: `${contextTokens}/${formatTokens(contextWindow)} (${contextPercent}%)${autoIndicator}`;\n\t\tif (contextPercentValue > 90) {\n\t\t\tcontextPercentStr = theme.fg(\"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\n\t\tlet statsLeft = statsParts.join(\" \");\n\n\t\t// Add model name on the right side, plus thinking level if model supports it\n\t\tconst modelName = state.model?.id || \"no-model\";\n\n\t\tlet statsLeftWidth = visibleWidth(statsLeft);\n\n\t\t// If statsLeft is too wide, truncate it\n\t\tif (statsLeftWidth > width) {\n\t\t\tstatsLeft = truncateToWidth(statsLeft, width, \"...\");\n\t\t\tstatsLeftWidth = visibleWidth(statsLeft);\n\t\t}\n\n\t\t// Calculate available space for padding (minimum 2 spaces between stats and model)\n\t\tconst minPadding = 2;\n\n\t\t// Add thinking level indicator if model supports reasoning\n\t\tlet rightSideWithoutProvider = modelName;\n\t\tif (state.model?.reasoning) {\n\t\t\tconst thinkingLevel = state.thinkingLevel || \"off\";\n\t\t\trightSideWithoutProvider =\n\t\t\t\tthinkingLevel === \"off\" ? `${modelName} • thinking off` : `${modelName} • ${thinkingLevel}`;\n\t\t}\n\n\t\t// Prepend the provider in parentheses if there are multiple providers and there's enough room\n\t\tlet rightSide = rightSideWithoutProvider;\n\t\tif (this.footerData.getAvailableProviderCount() > 1 && state.model) {\n\t\t\trightSide = `(${state.model!.provider}) ${rightSideWithoutProvider}`;\n\t\t\tif (statsLeftWidth + minPadding + visibleWidth(rightSide) > width) {\n\t\t\t\t// Too wide, fall back\n\t\t\t\trightSide = rightSideWithoutProvider;\n\t\t\t}\n\t\t}\n\n\t\tconst rightSideWidth = visibleWidth(rightSide);\n\t\tconst totalNeeded = statsLeftWidth + minPadding + rightSideWidth;\n\n\t\tlet statsLine: string;\n\t\tif (totalNeeded <= width) {\n\t\t\t// Both fit - add padding to right-align model\n\t\t\tconst padding = \" \".repeat(width - statsLeftWidth - rightSideWidth);\n\t\t\tstatsLine = statsLeft + padding + rightSide;\n\t\t} else {\n\t\t\t// Need to truncate right side\n\t\t\tconst availableForRight = width - statsLeftWidth - minPadding;\n\t\t\tif (availableForRight > 0) {\n\t\t\t\tconst truncatedRight = truncateToWidth(rightSide, availableForRight, \"\");\n\t\t\t\tconst truncatedRightWidth = visibleWidth(truncatedRight);\n\t\t\t\tconst padding = \" \".repeat(Math.max(0, width - statsLeftWidth - truncatedRightWidth));\n\t\t\t\tstatsLine = statsLeft + padding + truncatedRight;\n\t\t\t} else {\n\t\t\t\t// Not enough space for right side at all\n\t\t\t\tstatsLine = statsLeft;\n\t\t\t}\n\t\t}\n\n\t\t// Apply dim to each part separately. statsLeft may contain color codes (for context %)\n\t\t// that end with a reset, which would clear an outer dim wrapper. So we dim the parts\n\t\t// before and after the colored section independently.\n\t\tconst dimStatsLeft = theme.fg(\"dim\", statsLeft);\n\t\tconst remainder = statsLine.slice(statsLeft.length); // padding + rightSide\n\t\tconst dimRemainder = theme.fg(\"dim\", remainder);\n\n\t\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;AACnE,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,uCAAuC,CAAC;AAmBxF,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAYhF;AAgBD;;;GAGG;AACH,qBAAa,eAAgB,YAAW,SAAS;IAChD,OAAO,CAAC,OAAO,CAAe;IAC9B,OAAO,CAAC,UAAU,CAA6B;IAC/C,OAAO,CAAC,kBAAkB,CAAQ;IAElC,YAAY,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,0BAA0B,EAGxE;IAED,UAAU,CAAC,OAAO,EAAE,YAAY,GAAG,IAAI,CAEtC;IAED,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAE5C;IAED;;;OAGG;IACH,UAAU,IAAI,IAAI,CAEjB;IAED;;;OAGG;IACH,OAAO,IAAI,IAAI,CAEd;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CA0J9B;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 type { ReadonlyFooterDataProvider } from \"../../../core/footer-data-provider.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\nfunction formatTokens(count: number): string {\n\treturn count.toLocaleString(\"en-US\");\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 === \"\" ? \"~\" : `~${sep}${relativeToHome}`;\n}\n\n/**\n * Color the right side of the footer: (provider) muted, model accent, :thinking dim.\n * The text is the plain (uncolored) right-aligned segment from the layout pass.\n */\nfunction colorRightSide(text: string): string {\n\tif (!text) return \"\";\n\tconst providerMatch = text.match(/^\\(([^)]+)\\) (.*)$/);\n\tconst body = providerMatch ? providerMatch[2] : text;\n\tconst providerPrefix = providerMatch ? theme.fg(\"muted\", `(${providerMatch[1]}) `) : \"\";\n\tconst thinkingMatch = body.match(/^(.+):([^:]+)$/);\n\tif (!thinkingMatch) return providerPrefix + theme.fg(\"accent\", body);\n\treturn `${providerPrefix}${theme.fg(\"accent\", thinkingMatch[1])}${theme.fg(\"dim\", `:${thinkingMatch[2]}`)}`;\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 session: AgentSession;\n\tprivate footerData: ReadonlyFooterDataProvider;\n\tprivate autoCompactEnabled = true;\n\n\tconstructor(session: AgentSession, footerData: ReadonlyFooterDataProvider) {\n\t\tthis.session = session;\n\t\tthis.footerData = footerData;\n\t}\n\n\tsetSession(session: AgentSession): void {\n\t\tthis.session = session;\n\t}\n\n\tsetAutoCompactEnabled(enabled: boolean): void {\n\t\tthis.autoCompactEnabled = enabled;\n\t}\n\n\t/**\n\t * No-op: git branch caching now handled by provider.\n\t * Kept for compatibility with existing call sites in interactive-mode.\n\t */\n\tinvalidate(): void {\n\t\t// No-op: git branch is cached/invalidated by provider\n\t}\n\n\t/**\n\t * Clean up resources.\n\t * Git watcher cleanup now handled by provider.\n\t */\n\tdispose(): void {\n\t\t// Git watcher cleanup handled by provider\n\t}\n\n\trender(width: number): string[] {\n\t\tconst state = this.session.state;\n\n\t\tlet totalInput = 0;\n\t\tlet totalOutput = 0;\n\t\tlet totalCacheRead = 0;\n\t\tlet totalCacheWrite = 0;\n\t\tlet totalCost = 0;\n\n\t\tfor (const entry of this.session.sessionManager.getEntries()) {\n\t\t\tif (entry.type === \"message\" && entry.message.role === \"assistant\") {\n\t\t\t\ttotalInput += entry.message.usage.input;\n\t\t\t\ttotalOutput += entry.message.usage.output;\n\t\t\t\ttotalCacheRead += entry.message.usage.cacheRead;\n\t\t\t\ttotalCacheWrite += entry.message.usage.cacheWrite;\n\t\t\t\ttotalCost += entry.message.usage.cost.total;\n\t\t\t}\n\t\t}\n\n\t\t// Calculate context usage from session (handles compaction correctly).\n\t\t// After compaction, tokens are unknown until the next LLM response.\n\t\tconst contextUsage = this.session.getContextUsage();\n\t\tconst contextWindow = contextUsage?.contextWindow ?? state.model?.contextWindow ?? 0;\n\t\tconst contextPercentValue = contextUsage?.percent ?? 0;\n\t\tconst contextPercent = contextUsage?.percent !== null ? contextPercentValue.toFixed(1) : \"?\";\n\t\tconst contextTokens =\n\t\t\ttypeof contextUsage?.tokens === \"number\"\n\t\t\t\t? formatTokens(contextUsage.tokens)\n\t\t\t\t: typeof contextUsage?.percent === \"number\"\n\t\t\t\t\t? formatTokens(Math.round((contextWindow * contextUsage.percent) / 100))\n\t\t\t\t\t: \"?\";\n\n\t\t// Build colored segments. Each segment carries its own theme color\n\t\t// so the HUD stays readable at a glance instead of being one dim wash.\n\t\tconst sep = theme.fg(\"borderMuted\", \" • \");\n\t\tconst pwdRaw = formatCwdForFooter(\n\t\t\tthis.session.sessionManager.getCwd(),\n\t\t\tprocess.env.HOME || process.env.USERPROFILE,\n\t\t);\n\t\tconst branch = this.footerData.getGitBranch();\n\t\tconst sessionName = this.session.sessionManager.getSessionName();\n\n\t\tconst coloredSegments: string[] = [theme.fg(\"accent\", pwdRaw)];\n\t\tconst plainSegments: string[] = [pwdRaw];\n\t\tif (branch) {\n\t\t\tcoloredSegments.push(theme.fg(\"warning\", branch));\n\t\t\tplainSegments.push(branch);\n\t\t}\n\t\tif (sessionName) {\n\t\t\tcoloredSegments.push(theme.fg(\"muted\", sessionName));\n\t\t\tplainSegments.push(sessionName);\n\t\t}\n\t\tif (totalInput) {\n\t\t\tconst text = `↑${formatTokens(totalInput)}`;\n\t\t\tcoloredSegments.push(theme.fg(\"dim\", text));\n\t\t\tplainSegments.push(text);\n\t\t}\n\t\tif (totalOutput) {\n\t\t\tconst text = `↓${formatTokens(totalOutput)}`;\n\t\t\tcoloredSegments.push(theme.fg(\"dim\", text));\n\t\t\tplainSegments.push(text);\n\t\t}\n\t\tif (totalCacheRead || totalCacheWrite) {\n\t\t\tconst text = `cache ${formatTokens(totalCacheRead)}/${formatTokens(totalCacheWrite)}`;\n\t\t\tcoloredSegments.push(theme.fg(\"dim\", text));\n\t\t\tplainSegments.push(text);\n\t\t}\n\n\t\t// Show cost with \"(sub)\" indicator if using OAuth subscription\n\t\tconst usingSubscription = state.model ? this.session.modelRegistry.isUsingOAuth(state.model) : false;\n\t\tif (totalCost || usingSubscription) {\n\t\t\tconst costStr = `$${totalCost.toFixed(3)}${usingSubscription ? \" (sub)\" : \"\"}`;\n\t\t\tcoloredSegments.push(theme.fg(\"success\", costStr));\n\t\t\tplainSegments.push(costStr);\n\t\t}\n\n\t\tconst autoIndicator = this.autoCompactEnabled ? \" (auto)\" : \"\";\n\t\tconst ctxDisplay =\n\t\t\tcontextPercent === \"?\"\n\t\t\t\t? `${contextTokens}/${formatTokens(contextWindow)} (?)${autoIndicator}`\n\t\t\t\t: `${contextTokens}/${formatTokens(contextWindow)} (${contextPercent}%)${autoIndicator}`;\n\t\tconst ctxColored =\n\t\t\tcontextPercentValue > 90\n\t\t\t\t? theme.fg(\"error\", ctxDisplay)\n\t\t\t\t: contextPercentValue > 70\n\t\t\t\t\t? theme.fg(\"warning\", ctxDisplay)\n\t\t\t\t\t: theme.fg(\"muted\", ctxDisplay);\n\t\tcoloredSegments.push(ctxColored);\n\t\tplainSegments.push(ctxDisplay);\n\n\t\tconst statsLeftPlain = plainSegments.join(\" • \");\n\t\tlet statsLeft = coloredSegments.join(sep);\n\t\tlet statsLeftWidth = visibleWidth(statsLeftPlain);\n\n\t\t// If statsLeft is too wide, truncate the plain version (color codes break truncation)\n\t\tif (statsLeftWidth > width) {\n\t\t\tconst truncated = truncateToWidth(statsLeftPlain, width, \"...\");\n\t\t\tstatsLeft = theme.fg(\"muted\", truncated);\n\t\t\tstatsLeftWidth = visibleWidth(truncated);\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\tconst modelName = state.model?.id || \"no-model\";\n\t\tlet rightSideWithoutProvider = modelName;\n\t\tif (state.model?.reasoning) {\n\t\t\tconst thinkingLevel = state.thinkingLevel || \"off\";\n\t\t\trightSideWithoutProvider = thinkingLevel === \"off\" ? `${modelName}:off` : `${modelName}:${thinkingLevel}`;\n\t\t}\n\n\t\t// Prepend the provider in parentheses if there are multiple providers and there's enough room\n\t\tlet rightSidePlain = rightSideWithoutProvider;\n\t\tif (this.footerData.getAvailableProviderCount() > 1 && state.model) {\n\t\t\tconst withProvider = `(${state.model.provider}) ${rightSideWithoutProvider}`;\n\t\t\tif (statsLeftWidth + minPadding + visibleWidth(withProvider) <= width) {\n\t\t\t\trightSidePlain = withProvider;\n\t\t\t}\n\t\t}\n\n\t\tconst rightSideWidth = visibleWidth(rightSidePlain);\n\t\tconst totalNeeded = statsLeftWidth + minPadding + rightSideWidth;\n\n\t\tlet rightSideRendered = rightSidePlain;\n\t\tlet actualRightWidth = rightSideWidth;\n\t\tif (totalNeeded > width) {\n\t\t\tconst availableForRight = width - statsLeftWidth - minPadding;\n\t\t\tif (availableForRight > 0) {\n\t\t\t\trightSideRendered = truncateToWidth(rightSidePlain, availableForRight, \"\");\n\t\t\t\tactualRightWidth = visibleWidth(rightSideRendered);\n\t\t\t} else {\n\t\t\t\trightSideRendered = \"\";\n\t\t\t\tactualRightWidth = 0;\n\t\t\t}\n\t\t}\n\n\t\t// Color the right side: provider muted, model accent, thinking dim\n\t\tconst coloredRight = colorRightSide(rightSideRendered);\n\t\tconst padding = \" \".repeat(Math.max(0, width - statsLeftWidth - actualRightWidth));\n\t\tconst lines = [statsLeft + padding + coloredRight];\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"]}
|
|
@@ -27,6 +27,21 @@ export function formatCwdForFooter(cwd, home) {
|
|
|
27
27
|
return cwd;
|
|
28
28
|
return relativeToHome === "" ? "~" : `~${sep}${relativeToHome}`;
|
|
29
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Color the right side of the footer: (provider) muted, model accent, :thinking dim.
|
|
32
|
+
* The text is the plain (uncolored) right-aligned segment from the layout pass.
|
|
33
|
+
*/
|
|
34
|
+
function colorRightSide(text) {
|
|
35
|
+
if (!text)
|
|
36
|
+
return "";
|
|
37
|
+
const providerMatch = text.match(/^\(([^)]+)\) (.*)$/);
|
|
38
|
+
const body = providerMatch ? providerMatch[2] : text;
|
|
39
|
+
const providerPrefix = providerMatch ? theme.fg("muted", `(${providerMatch[1]}) `) : "";
|
|
40
|
+
const thinkingMatch = body.match(/^(.+):([^:]+)$/);
|
|
41
|
+
if (!thinkingMatch)
|
|
42
|
+
return providerPrefix + theme.fg("accent", body);
|
|
43
|
+
return `${providerPrefix}${theme.fg("accent", thinkingMatch[1])}${theme.fg("dim", `:${thinkingMatch[2]}`)}`;
|
|
44
|
+
}
|
|
30
45
|
/**
|
|
31
46
|
* Footer component that shows pwd, token stats, and context usage.
|
|
32
47
|
* Computes token/context stats from session, gets git branch and extension statuses from provider.
|
|
@@ -86,104 +101,100 @@ export class FooterComponent {
|
|
|
86
101
|
: typeof contextUsage?.percent === "number"
|
|
87
102
|
? formatTokens(Math.round((contextWindow * contextUsage.percent) / 100))
|
|
88
103
|
: "?";
|
|
89
|
-
//
|
|
90
|
-
|
|
91
|
-
|
|
104
|
+
// Build colored segments. Each segment carries its own theme color
|
|
105
|
+
// so the HUD stays readable at a glance instead of being one dim wash.
|
|
106
|
+
const sep = theme.fg("borderMuted", " • ");
|
|
107
|
+
const pwdRaw = formatCwdForFooter(this.session.sessionManager.getCwd(), process.env.HOME || process.env.USERPROFILE);
|
|
92
108
|
const branch = this.footerData.getGitBranch();
|
|
109
|
+
const sessionName = this.session.sessionManager.getSessionName();
|
|
110
|
+
const coloredSegments = [theme.fg("accent", pwdRaw)];
|
|
111
|
+
const plainSegments = [pwdRaw];
|
|
93
112
|
if (branch) {
|
|
94
|
-
|
|
113
|
+
coloredSegments.push(theme.fg("warning", branch));
|
|
114
|
+
plainSegments.push(branch);
|
|
95
115
|
}
|
|
96
|
-
// Add session name if set
|
|
97
|
-
const sessionName = this.session.sessionManager.getSessionName();
|
|
98
116
|
if (sessionName) {
|
|
99
|
-
|
|
117
|
+
coloredSegments.push(theme.fg("muted", sessionName));
|
|
118
|
+
plainSegments.push(sessionName);
|
|
119
|
+
}
|
|
120
|
+
if (totalInput) {
|
|
121
|
+
const text = `↑${formatTokens(totalInput)}`;
|
|
122
|
+
coloredSegments.push(theme.fg("dim", text));
|
|
123
|
+
plainSegments.push(text);
|
|
124
|
+
}
|
|
125
|
+
if (totalOutput) {
|
|
126
|
+
const text = `↓${formatTokens(totalOutput)}`;
|
|
127
|
+
coloredSegments.push(theme.fg("dim", text));
|
|
128
|
+
plainSegments.push(text);
|
|
100
129
|
}
|
|
101
|
-
const statsParts = [];
|
|
102
|
-
if (totalInput)
|
|
103
|
-
statsParts.push(`↑${formatTokens(totalInput)}`);
|
|
104
|
-
if (totalOutput)
|
|
105
|
-
statsParts.push(`↓${formatTokens(totalOutput)}`);
|
|
106
130
|
if (totalCacheRead || totalCacheWrite) {
|
|
107
|
-
|
|
131
|
+
const text = `cache ${formatTokens(totalCacheRead)}/${formatTokens(totalCacheWrite)}`;
|
|
132
|
+
coloredSegments.push(theme.fg("dim", text));
|
|
133
|
+
plainSegments.push(text);
|
|
108
134
|
}
|
|
109
135
|
// Show cost with "(sub)" indicator if using OAuth subscription
|
|
110
136
|
const usingSubscription = state.model ? this.session.modelRegistry.isUsingOAuth(state.model) : false;
|
|
111
137
|
if (totalCost || usingSubscription) {
|
|
112
138
|
const costStr = `$${totalCost.toFixed(3)}${usingSubscription ? " (sub)" : ""}`;
|
|
113
|
-
|
|
139
|
+
coloredSegments.push(theme.fg("success", costStr));
|
|
140
|
+
plainSegments.push(costStr);
|
|
114
141
|
}
|
|
115
|
-
let contextPercentStr;
|
|
116
142
|
const autoIndicator = this.autoCompactEnabled ? " (auto)" : "";
|
|
117
|
-
const
|
|
143
|
+
const ctxDisplay = contextPercent === "?"
|
|
118
144
|
? `${contextTokens}/${formatTokens(contextWindow)} (?)${autoIndicator}`
|
|
119
145
|
: `${contextTokens}/${formatTokens(contextWindow)} (${contextPercent}%)${autoIndicator}`;
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
// Add model name on the right side, plus thinking level if model supports it
|
|
132
|
-
const modelName = state.model?.id || "no-model";
|
|
133
|
-
let statsLeftWidth = visibleWidth(statsLeft);
|
|
134
|
-
// If statsLeft is too wide, truncate it
|
|
146
|
+
const ctxColored = contextPercentValue > 90
|
|
147
|
+
? theme.fg("error", ctxDisplay)
|
|
148
|
+
: contextPercentValue > 70
|
|
149
|
+
? theme.fg("warning", ctxDisplay)
|
|
150
|
+
: theme.fg("muted", ctxDisplay);
|
|
151
|
+
coloredSegments.push(ctxColored);
|
|
152
|
+
plainSegments.push(ctxDisplay);
|
|
153
|
+
const statsLeftPlain = plainSegments.join(" • ");
|
|
154
|
+
let statsLeft = coloredSegments.join(sep);
|
|
155
|
+
let statsLeftWidth = visibleWidth(statsLeftPlain);
|
|
156
|
+
// If statsLeft is too wide, truncate the plain version (color codes break truncation)
|
|
135
157
|
if (statsLeftWidth > width) {
|
|
136
|
-
|
|
137
|
-
|
|
158
|
+
const truncated = truncateToWidth(statsLeftPlain, width, "...");
|
|
159
|
+
statsLeft = theme.fg("muted", truncated);
|
|
160
|
+
statsLeftWidth = visibleWidth(truncated);
|
|
138
161
|
}
|
|
139
162
|
// Calculate available space for padding (minimum 2 spaces between stats and model)
|
|
140
163
|
const minPadding = 2;
|
|
141
164
|
// Add thinking level indicator if model supports reasoning
|
|
165
|
+
const modelName = state.model?.id || "no-model";
|
|
142
166
|
let rightSideWithoutProvider = modelName;
|
|
143
167
|
if (state.model?.reasoning) {
|
|
144
168
|
const thinkingLevel = state.thinkingLevel || "off";
|
|
145
|
-
rightSideWithoutProvider =
|
|
146
|
-
thinkingLevel === "off" ? `${modelName} • thinking off` : `${modelName} • ${thinkingLevel}`;
|
|
169
|
+
rightSideWithoutProvider = thinkingLevel === "off" ? `${modelName}:off` : `${modelName}:${thinkingLevel}`;
|
|
147
170
|
}
|
|
148
171
|
// Prepend the provider in parentheses if there are multiple providers and there's enough room
|
|
149
|
-
let
|
|
172
|
+
let rightSidePlain = rightSideWithoutProvider;
|
|
150
173
|
if (this.footerData.getAvailableProviderCount() > 1 && state.model) {
|
|
151
|
-
|
|
152
|
-
if (statsLeftWidth + minPadding + visibleWidth(
|
|
153
|
-
|
|
154
|
-
rightSide = rightSideWithoutProvider;
|
|
174
|
+
const withProvider = `(${state.model.provider}) ${rightSideWithoutProvider}`;
|
|
175
|
+
if (statsLeftWidth + minPadding + visibleWidth(withProvider) <= width) {
|
|
176
|
+
rightSidePlain = withProvider;
|
|
155
177
|
}
|
|
156
178
|
}
|
|
157
|
-
const rightSideWidth = visibleWidth(
|
|
179
|
+
const rightSideWidth = visibleWidth(rightSidePlain);
|
|
158
180
|
const totalNeeded = statsLeftWidth + minPadding + rightSideWidth;
|
|
159
|
-
let
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
const padding = " ".repeat(width - statsLeftWidth - rightSideWidth);
|
|
163
|
-
statsLine = statsLeft + padding + rightSide;
|
|
164
|
-
}
|
|
165
|
-
else {
|
|
166
|
-
// Need to truncate right side
|
|
181
|
+
let rightSideRendered = rightSidePlain;
|
|
182
|
+
let actualRightWidth = rightSideWidth;
|
|
183
|
+
if (totalNeeded > width) {
|
|
167
184
|
const availableForRight = width - statsLeftWidth - minPadding;
|
|
168
185
|
if (availableForRight > 0) {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const padding = " ".repeat(Math.max(0, width - statsLeftWidth - truncatedRightWidth));
|
|
172
|
-
statsLine = statsLeft + padding + truncatedRight;
|
|
186
|
+
rightSideRendered = truncateToWidth(rightSidePlain, availableForRight, "");
|
|
187
|
+
actualRightWidth = visibleWidth(rightSideRendered);
|
|
173
188
|
}
|
|
174
189
|
else {
|
|
175
|
-
|
|
176
|
-
|
|
190
|
+
rightSideRendered = "";
|
|
191
|
+
actualRightWidth = 0;
|
|
177
192
|
}
|
|
178
193
|
}
|
|
179
|
-
//
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
const
|
|
183
|
-
const remainder = statsLine.slice(statsLeft.length); // padding + rightSide
|
|
184
|
-
const dimRemainder = theme.fg("dim", remainder);
|
|
185
|
-
const pwdLine = truncateToWidth(theme.fg("dim", pwd), width, theme.fg("dim", "..."));
|
|
186
|
-
const lines = [pwdLine, dimStatsLeft + dimRemainder];
|
|
194
|
+
// Color the right side: provider muted, model accent, thinking dim
|
|
195
|
+
const coloredRight = colorRightSide(rightSideRendered);
|
|
196
|
+
const padding = " ".repeat(Math.max(0, width - statsLeftWidth - actualRightWidth));
|
|
197
|
+
const lines = [statsLeft + padding + coloredRight];
|
|
187
198
|
// Add extension statuses on a single line, sorted by key alphabetically
|
|
188
199
|
const extensionStatuses = this.footerData.getExtensionStatuses();
|
|
189
200
|
if (extensionStatuses.size > 0) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"footer.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/footer.ts"],"names":[],"mappings":"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","sourcesContent":["import { 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 type { ReadonlyFooterDataProvider } from \"../../../core/footer-data-provider.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\nfunction formatTokens(count: number): string {\n\treturn count.toLocaleString(\"en-US\");\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 === \"\" ? \"~\" : `~${sep}${relativeToHome}`;\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 session: AgentSession;\n\tprivate footerData: ReadonlyFooterDataProvider;\n\tprivate autoCompactEnabled = true;\n\n\tconstructor(session: AgentSession, footerData: ReadonlyFooterDataProvider) {\n\t\tthis.session = session;\n\t\tthis.footerData = footerData;\n\t}\n\n\tsetSession(session: AgentSession): void {\n\t\tthis.session = session;\n\t}\n\n\tsetAutoCompactEnabled(enabled: boolean): void {\n\t\tthis.autoCompactEnabled = enabled;\n\t}\n\n\t/**\n\t * No-op: git branch caching now handled by provider.\n\t * Kept for compatibility with existing call sites in interactive-mode.\n\t */\n\tinvalidate(): void {\n\t\t// No-op: git branch is cached/invalidated by provider\n\t}\n\n\t/**\n\t * Clean up resources.\n\t * Git watcher cleanup now handled by provider.\n\t */\n\tdispose(): void {\n\t\t// Git watcher cleanup handled by provider\n\t}\n\n\trender(width: number): string[] {\n\t\tconst state = this.session.state;\n\n\t\tlet totalInput = 0;\n\t\tlet totalOutput = 0;\n\t\tlet totalCacheRead = 0;\n\t\tlet totalCacheWrite = 0;\n\t\tlet totalCost = 0;\n\n\t\tfor (const entry of this.session.sessionManager.getEntries()) {\n\t\t\tif (entry.type === \"message\" && entry.message.role === \"assistant\") {\n\t\t\t\ttotalInput += entry.message.usage.input;\n\t\t\t\ttotalOutput += entry.message.usage.output;\n\t\t\t\ttotalCacheRead += entry.message.usage.cacheRead;\n\t\t\t\ttotalCacheWrite += entry.message.usage.cacheWrite;\n\t\t\t\ttotalCost += entry.message.usage.cost.total;\n\t\t\t}\n\t\t}\n\n\t\t// Calculate context usage from session (handles compaction correctly).\n\t\t// After compaction, tokens are unknown until the next LLM response.\n\t\tconst contextUsage = this.session.getContextUsage();\n\t\tconst contextWindow = contextUsage?.contextWindow ?? state.model?.contextWindow ?? 0;\n\t\tconst contextPercentValue = contextUsage?.percent ?? 0;\n\t\tconst contextPercent = contextUsage?.percent !== null ? contextPercentValue.toFixed(1) : \"?\";\n\t\tconst contextTokens =\n\t\t\ttypeof contextUsage?.tokens === \"number\"\n\t\t\t\t? formatTokens(contextUsage.tokens)\n\t\t\t\t: typeof contextUsage?.percent === \"number\"\n\t\t\t\t\t? formatTokens(Math.round((contextWindow * contextUsage.percent) / 100))\n\t\t\t\t\t: \"?\";\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} • ${sessionName}`;\n\t\t}\n\n\t\tconst statsParts: string[] = [];\n\t\tif (totalInput) statsParts.push(`↑${formatTokens(totalInput)}`);\n\t\tif (totalOutput) statsParts.push(`↓${formatTokens(totalOutput)}`);\n\t\tif (totalCacheRead || totalCacheWrite) {\n\t\t\tstatsParts.push(`cache ${formatTokens(totalCacheRead)}/${formatTokens(totalCacheWrite)}`);\n\t\t}\n\n\t\t// Show cost with \"(sub)\" indicator if using OAuth subscription\n\t\tconst usingSubscription = state.model ? this.session.modelRegistry.isUsingOAuth(state.model) : false;\n\t\tif (totalCost || usingSubscription) {\n\t\t\tconst costStr = `$${totalCost.toFixed(3)}${usingSubscription ? \" (sub)\" : \"\"}`;\n\t\t\tstatsParts.push(costStr);\n\t\t}\n\n\t\tlet contextPercentStr: string;\n\t\tconst autoIndicator = this.autoCompactEnabled ? \" (auto)\" : \"\";\n\t\tconst contextPercentDisplay =\n\t\t\tcontextPercent === \"?\"\n\t\t\t\t? `${contextTokens}/${formatTokens(contextWindow)} (?)${autoIndicator}`\n\t\t\t\t: `${contextTokens}/${formatTokens(contextWindow)} (${contextPercent}%)${autoIndicator}`;\n\t\tif (contextPercentValue > 90) {\n\t\t\tcontextPercentStr = theme.fg(\"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\n\t\tlet statsLeft = statsParts.join(\" \");\n\n\t\t// Add model name on the right side, plus thinking level if model supports it\n\t\tconst modelName = state.model?.id || \"no-model\";\n\n\t\tlet statsLeftWidth = visibleWidth(statsLeft);\n\n\t\t// If statsLeft is too wide, truncate it\n\t\tif (statsLeftWidth > width) {\n\t\t\tstatsLeft = truncateToWidth(statsLeft, width, \"...\");\n\t\t\tstatsLeftWidth = visibleWidth(statsLeft);\n\t\t}\n\n\t\t// Calculate available space for padding (minimum 2 spaces between stats and model)\n\t\tconst minPadding = 2;\n\n\t\t// Add thinking level indicator if model supports reasoning\n\t\tlet rightSideWithoutProvider = modelName;\n\t\tif (state.model?.reasoning) {\n\t\t\tconst thinkingLevel = state.thinkingLevel || \"off\";\n\t\t\trightSideWithoutProvider =\n\t\t\t\tthinkingLevel === \"off\" ? `${modelName} • thinking off` : `${modelName} • ${thinkingLevel}`;\n\t\t}\n\n\t\t// Prepend the provider in parentheses if there are multiple providers and there's enough room\n\t\tlet rightSide = rightSideWithoutProvider;\n\t\tif (this.footerData.getAvailableProviderCount() > 1 && state.model) {\n\t\t\trightSide = `(${state.model!.provider}) ${rightSideWithoutProvider}`;\n\t\t\tif (statsLeftWidth + minPadding + visibleWidth(rightSide) > width) {\n\t\t\t\t// Too wide, fall back\n\t\t\t\trightSide = rightSideWithoutProvider;\n\t\t\t}\n\t\t}\n\n\t\tconst rightSideWidth = visibleWidth(rightSide);\n\t\tconst totalNeeded = statsLeftWidth + minPadding + rightSideWidth;\n\n\t\tlet statsLine: string;\n\t\tif (totalNeeded <= width) {\n\t\t\t// Both fit - add padding to right-align model\n\t\t\tconst padding = \" \".repeat(width - statsLeftWidth - rightSideWidth);\n\t\t\tstatsLine = statsLeft + padding + rightSide;\n\t\t} else {\n\t\t\t// Need to truncate right side\n\t\t\tconst availableForRight = width - statsLeftWidth - minPadding;\n\t\t\tif (availableForRight > 0) {\n\t\t\t\tconst truncatedRight = truncateToWidth(rightSide, availableForRight, \"\");\n\t\t\t\tconst truncatedRightWidth = visibleWidth(truncatedRight);\n\t\t\t\tconst padding = \" \".repeat(Math.max(0, width - statsLeftWidth - truncatedRightWidth));\n\t\t\t\tstatsLine = statsLeft + padding + truncatedRight;\n\t\t\t} else {\n\t\t\t\t// Not enough space for right side at all\n\t\t\t\tstatsLine = statsLeft;\n\t\t\t}\n\t\t}\n\n\t\t// Apply dim to each part separately. statsLeft may contain color codes (for context %)\n\t\t// that end with a reset, which would clear an outer dim wrapper. So we dim the parts\n\t\t// before and after the colored section independently.\n\t\tconst dimStatsLeft = theme.fg(\"dim\", statsLeft);\n\t\tconst remainder = statsLine.slice(statsLeft.length); // padding + rightSide\n\t\tconst dimRemainder = theme.fg(\"dim\", remainder);\n\n\t\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 type { ReadonlyFooterDataProvider } from \"../../../core/footer-data-provider.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\nfunction formatTokens(count: number): string {\n\treturn count.toLocaleString(\"en-US\");\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 === \"\" ? \"~\" : `~${sep}${relativeToHome}`;\n}\n\n/**\n * Color the right side of the footer: (provider) muted, model accent, :thinking dim.\n * The text is the plain (uncolored) right-aligned segment from the layout pass.\n */\nfunction colorRightSide(text: string): string {\n\tif (!text) return \"\";\n\tconst providerMatch = text.match(/^\\(([^)]+)\\) (.*)$/);\n\tconst body = providerMatch ? providerMatch[2] : text;\n\tconst providerPrefix = providerMatch ? theme.fg(\"muted\", `(${providerMatch[1]}) `) : \"\";\n\tconst thinkingMatch = body.match(/^(.+):([^:]+)$/);\n\tif (!thinkingMatch) return providerPrefix + theme.fg(\"accent\", body);\n\treturn `${providerPrefix}${theme.fg(\"accent\", thinkingMatch[1])}${theme.fg(\"dim\", `:${thinkingMatch[2]}`)}`;\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 session: AgentSession;\n\tprivate footerData: ReadonlyFooterDataProvider;\n\tprivate autoCompactEnabled = true;\n\n\tconstructor(session: AgentSession, footerData: ReadonlyFooterDataProvider) {\n\t\tthis.session = session;\n\t\tthis.footerData = footerData;\n\t}\n\n\tsetSession(session: AgentSession): void {\n\t\tthis.session = session;\n\t}\n\n\tsetAutoCompactEnabled(enabled: boolean): void {\n\t\tthis.autoCompactEnabled = enabled;\n\t}\n\n\t/**\n\t * No-op: git branch caching now handled by provider.\n\t * Kept for compatibility with existing call sites in interactive-mode.\n\t */\n\tinvalidate(): void {\n\t\t// No-op: git branch is cached/invalidated by provider\n\t}\n\n\t/**\n\t * Clean up resources.\n\t * Git watcher cleanup now handled by provider.\n\t */\n\tdispose(): void {\n\t\t// Git watcher cleanup handled by provider\n\t}\n\n\trender(width: number): string[] {\n\t\tconst state = this.session.state;\n\n\t\tlet totalInput = 0;\n\t\tlet totalOutput = 0;\n\t\tlet totalCacheRead = 0;\n\t\tlet totalCacheWrite = 0;\n\t\tlet totalCost = 0;\n\n\t\tfor (const entry of this.session.sessionManager.getEntries()) {\n\t\t\tif (entry.type === \"message\" && entry.message.role === \"assistant\") {\n\t\t\t\ttotalInput += entry.message.usage.input;\n\t\t\t\ttotalOutput += entry.message.usage.output;\n\t\t\t\ttotalCacheRead += entry.message.usage.cacheRead;\n\t\t\t\ttotalCacheWrite += entry.message.usage.cacheWrite;\n\t\t\t\ttotalCost += entry.message.usage.cost.total;\n\t\t\t}\n\t\t}\n\n\t\t// Calculate context usage from session (handles compaction correctly).\n\t\t// After compaction, tokens are unknown until the next LLM response.\n\t\tconst contextUsage = this.session.getContextUsage();\n\t\tconst contextWindow = contextUsage?.contextWindow ?? state.model?.contextWindow ?? 0;\n\t\tconst contextPercentValue = contextUsage?.percent ?? 0;\n\t\tconst contextPercent = contextUsage?.percent !== null ? contextPercentValue.toFixed(1) : \"?\";\n\t\tconst contextTokens =\n\t\t\ttypeof contextUsage?.tokens === \"number\"\n\t\t\t\t? formatTokens(contextUsage.tokens)\n\t\t\t\t: typeof contextUsage?.percent === \"number\"\n\t\t\t\t\t? formatTokens(Math.round((contextWindow * contextUsage.percent) / 100))\n\t\t\t\t\t: \"?\";\n\n\t\t// Build colored segments. Each segment carries its own theme color\n\t\t// so the HUD stays readable at a glance instead of being one dim wash.\n\t\tconst sep = theme.fg(\"borderMuted\", \" • \");\n\t\tconst pwdRaw = formatCwdForFooter(\n\t\t\tthis.session.sessionManager.getCwd(),\n\t\t\tprocess.env.HOME || process.env.USERPROFILE,\n\t\t);\n\t\tconst branch = this.footerData.getGitBranch();\n\t\tconst sessionName = this.session.sessionManager.getSessionName();\n\n\t\tconst coloredSegments: string[] = [theme.fg(\"accent\", pwdRaw)];\n\t\tconst plainSegments: string[] = [pwdRaw];\n\t\tif (branch) {\n\t\t\tcoloredSegments.push(theme.fg(\"warning\", branch));\n\t\t\tplainSegments.push(branch);\n\t\t}\n\t\tif (sessionName) {\n\t\t\tcoloredSegments.push(theme.fg(\"muted\", sessionName));\n\t\t\tplainSegments.push(sessionName);\n\t\t}\n\t\tif (totalInput) {\n\t\t\tconst text = `↑${formatTokens(totalInput)}`;\n\t\t\tcoloredSegments.push(theme.fg(\"dim\", text));\n\t\t\tplainSegments.push(text);\n\t\t}\n\t\tif (totalOutput) {\n\t\t\tconst text = `↓${formatTokens(totalOutput)}`;\n\t\t\tcoloredSegments.push(theme.fg(\"dim\", text));\n\t\t\tplainSegments.push(text);\n\t\t}\n\t\tif (totalCacheRead || totalCacheWrite) {\n\t\t\tconst text = `cache ${formatTokens(totalCacheRead)}/${formatTokens(totalCacheWrite)}`;\n\t\t\tcoloredSegments.push(theme.fg(\"dim\", text));\n\t\t\tplainSegments.push(text);\n\t\t}\n\n\t\t// Show cost with \"(sub)\" indicator if using OAuth subscription\n\t\tconst usingSubscription = state.model ? this.session.modelRegistry.isUsingOAuth(state.model) : false;\n\t\tif (totalCost || usingSubscription) {\n\t\t\tconst costStr = `$${totalCost.toFixed(3)}${usingSubscription ? \" (sub)\" : \"\"}`;\n\t\t\tcoloredSegments.push(theme.fg(\"success\", costStr));\n\t\t\tplainSegments.push(costStr);\n\t\t}\n\n\t\tconst autoIndicator = this.autoCompactEnabled ? \" (auto)\" : \"\";\n\t\tconst ctxDisplay =\n\t\t\tcontextPercent === \"?\"\n\t\t\t\t? `${contextTokens}/${formatTokens(contextWindow)} (?)${autoIndicator}`\n\t\t\t\t: `${contextTokens}/${formatTokens(contextWindow)} (${contextPercent}%)${autoIndicator}`;\n\t\tconst ctxColored =\n\t\t\tcontextPercentValue > 90\n\t\t\t\t? theme.fg(\"error\", ctxDisplay)\n\t\t\t\t: contextPercentValue > 70\n\t\t\t\t\t? theme.fg(\"warning\", ctxDisplay)\n\t\t\t\t\t: theme.fg(\"muted\", ctxDisplay);\n\t\tcoloredSegments.push(ctxColored);\n\t\tplainSegments.push(ctxDisplay);\n\n\t\tconst statsLeftPlain = plainSegments.join(\" • \");\n\t\tlet statsLeft = coloredSegments.join(sep);\n\t\tlet statsLeftWidth = visibleWidth(statsLeftPlain);\n\n\t\t// If statsLeft is too wide, truncate the plain version (color codes break truncation)\n\t\tif (statsLeftWidth > width) {\n\t\t\tconst truncated = truncateToWidth(statsLeftPlain, width, \"...\");\n\t\t\tstatsLeft = theme.fg(\"muted\", truncated);\n\t\t\tstatsLeftWidth = visibleWidth(truncated);\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\tconst modelName = state.model?.id || \"no-model\";\n\t\tlet rightSideWithoutProvider = modelName;\n\t\tif (state.model?.reasoning) {\n\t\t\tconst thinkingLevel = state.thinkingLevel || \"off\";\n\t\t\trightSideWithoutProvider = thinkingLevel === \"off\" ? `${modelName}:off` : `${modelName}:${thinkingLevel}`;\n\t\t}\n\n\t\t// Prepend the provider in parentheses if there are multiple providers and there's enough room\n\t\tlet rightSidePlain = rightSideWithoutProvider;\n\t\tif (this.footerData.getAvailableProviderCount() > 1 && state.model) {\n\t\t\tconst withProvider = `(${state.model.provider}) ${rightSideWithoutProvider}`;\n\t\t\tif (statsLeftWidth + minPadding + visibleWidth(withProvider) <= width) {\n\t\t\t\trightSidePlain = withProvider;\n\t\t\t}\n\t\t}\n\n\t\tconst rightSideWidth = visibleWidth(rightSidePlain);\n\t\tconst totalNeeded = statsLeftWidth + minPadding + rightSideWidth;\n\n\t\tlet rightSideRendered = rightSidePlain;\n\t\tlet actualRightWidth = rightSideWidth;\n\t\tif (totalNeeded > width) {\n\t\t\tconst availableForRight = width - statsLeftWidth - minPadding;\n\t\t\tif (availableForRight > 0) {\n\t\t\t\trightSideRendered = truncateToWidth(rightSidePlain, availableForRight, \"\");\n\t\t\t\tactualRightWidth = visibleWidth(rightSideRendered);\n\t\t\t} else {\n\t\t\t\trightSideRendered = \"\";\n\t\t\t\tactualRightWidth = 0;\n\t\t\t}\n\t\t}\n\n\t\t// Color the right side: provider muted, model accent, thinking dim\n\t\tconst coloredRight = colorRightSide(rightSideRendered);\n\t\tconst padding = \" \".repeat(Math.max(0, width - statsLeftWidth - actualRightWidth));\n\t\tconst lines = [statsLeft + padding + coloredRight];\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"]}
|