mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-31 08:13:11 +03:00
153 lines
5.2 KiB
TypeScript
153 lines
5.2 KiB
TypeScript
import { RGBA, StyledText, TextAttributes, type TextChunk } from "@opentui/core"
|
|
|
|
import type { TokenUsage } from "./protocol"
|
|
|
|
export interface FooterHint {
|
|
key: string
|
|
label: string
|
|
tone?: "normal" | "danger"
|
|
}
|
|
|
|
export interface FooterHintTheme {
|
|
accent: string
|
|
danger: string
|
|
muted: string
|
|
separator: string
|
|
}
|
|
|
|
export type FooterMode =
|
|
| "mention"
|
|
| "skill"
|
|
| "runtime"
|
|
| "active"
|
|
| "branch"
|
|
| "command"
|
|
| "session"
|
|
| "context"
|
|
| "history"
|
|
| "ready"
|
|
|
|
export function contextualFooterHints(
|
|
mode: FooterMode,
|
|
width: number,
|
|
theme: FooterHintTheme,
|
|
_platform: string = process.platform,
|
|
_shiftedEnter = false,
|
|
): StyledText {
|
|
return footerHints(hintsFor(mode, width), theme)
|
|
}
|
|
|
|
/** Last-turn model telemetry. Passive chrome reports the system, not its manual. */
|
|
export function footerTelemetry(
|
|
usage: TokenUsage | null,
|
|
width: number,
|
|
theme: FooterHintTheme,
|
|
): StyledText {
|
|
if (!usage) return new StyledText([])
|
|
const parts: string[] = []
|
|
const duration = usage.generation_ms
|
|
const measured = usage.measured_completion_tokens
|
|
if (typeof duration === "number" && duration > 0 && typeof measured === "number") {
|
|
const rate = measured * 1000 / duration
|
|
const value = rate < 10 ? rate.toFixed(1) : String(Math.round(rate))
|
|
const estimated = (usage.estimated_tokens || 0) > 0 ? "~" : ""
|
|
parts.push(`${estimated}${value} tok/s`)
|
|
}
|
|
const cacheHitRate = (
|
|
typeof usage.cached_tokens === "number"
|
|
&& typeof usage.prompt_tokens === "number"
|
|
&& usage.prompt_tokens > 0
|
|
)
|
|
? Math.min(100, Math.max(0, Math.round(
|
|
usage.cached_tokens * 100 / usage.prompt_tokens,
|
|
)))
|
|
: null
|
|
if (width >= 72) {
|
|
const prompt = usage.prompt_tokens
|
|
const completion = usage.completion_tokens
|
|
if (typeof prompt === "number" || typeof completion === "number") {
|
|
const cache = cacheHitRate === null ? "" : ` (${cacheHitRate}% cached)`
|
|
parts.push(`${formatTelemetryTokens(prompt || 0)} in${cache}`)
|
|
parts.push(`${formatTelemetryTokens(completion || 0)} out`)
|
|
}
|
|
} else if (cacheHitRate !== null) {
|
|
parts.push(`${cacheHitRate}% cached`)
|
|
}
|
|
if (width >= 128 && typeof usage.cost_usd === "number" && usage.cost_usd > 0) {
|
|
parts.push(`$${usage.cost_usd < 0.01 ? usage.cost_usd.toFixed(4) : usage.cost_usd.toFixed(2)}`)
|
|
}
|
|
return footerMetrics(parts, theme)
|
|
}
|
|
|
|
function formatTelemetryTokens(value: number): string {
|
|
const count = Math.max(0, value)
|
|
for (const [threshold, suffix] of [[1_000_000_000, "B"], [1_000_000, "M"], [1_000, "K"]] as const) {
|
|
if (count < threshold) continue
|
|
const scaled = count / threshold
|
|
const compact = scaled >= 10 ? Math.round(scaled) : Math.round(scaled * 10) / 10
|
|
return `${compact}${suffix}`
|
|
}
|
|
return String(Math.round(count))
|
|
}
|
|
|
|
function footerMetrics(parts: readonly string[], theme: FooterHintTheme): StyledText {
|
|
const chunks: TextChunk[] = []
|
|
parts.forEach((text, index) => {
|
|
if (index) chunks.push(chunk(" · ", theme.separator))
|
|
chunks.push(chunk(text, index === 0 ? theme.accent : theme.muted, index === 0))
|
|
})
|
|
return new StyledText(chunks)
|
|
}
|
|
|
|
/** Give shortcuts visual hierarchy without turning the footer into a toolbar. */
|
|
export function footerHints(hints: readonly FooterHint[], theme: FooterHintTheme): StyledText {
|
|
const chunks: TextChunk[] = []
|
|
hints.forEach((hint, index) => {
|
|
if (index) chunks.push(chunk(" · ", theme.separator))
|
|
const color = hint.tone === "danger" ? theme.danger : theme.accent
|
|
chunks.push(chunk(hint.key, color, true))
|
|
chunks.push(chunk(` ${hint.label}`, theme.muted))
|
|
})
|
|
return new StyledText(chunks)
|
|
}
|
|
|
|
function hintsFor(
|
|
mode: FooterMode,
|
|
width: number,
|
|
): FooterHint[] {
|
|
if (mode === "runtime") return width >= 64
|
|
? [hint("↑↓/click", "choose"), hint("enter", "apply"), hint("esc", "close")]
|
|
: [hint("enter", "apply"), hint("esc", "close")]
|
|
if (mode === "mention" || mode === "skill") return width >= 64
|
|
? [hint("↑↓", "choose"), hint("tab/enter", "insert"), hint("esc", "close")]
|
|
: [hint("enter", "insert"), hint("esc", "close")]
|
|
if (mode === "active") return []
|
|
if (mode === "branch") return width >= 64
|
|
? [hint("type", "filter"), hint("↑↓", "choose"), hint("enter", "branch"), hint("esc", "close")]
|
|
: [hint("enter", "branch"), hint("esc", "close")]
|
|
if (mode === "command") return width >= 72
|
|
? [hint("↑↓", "choose"), hint("tab", "complete"), hint("esc", "close")]
|
|
: [hint("tab", "complete"), hint("esc", "close")]
|
|
if (mode === "session") return width >= 64
|
|
? [hint("type", "filter"), hint("↑↓", "choose"), hint("enter", "open"), hint("esc", "close")]
|
|
: [hint("enter", "open"), hint("esc", "close")]
|
|
if (mode === "context") return [hint("esc", "close"), hint("pgup/pgdn", "scroll")]
|
|
if (mode === "history") return width >= 72
|
|
? [hint("ctrl+end", "latest"), hint("pgup/pgdn", "scroll")]
|
|
: width >= 48 ? [hint("ctrl+end", "latest")] : []
|
|
return []
|
|
}
|
|
|
|
function hint(key: string, label: string): FooterHint {
|
|
return { key, label }
|
|
}
|
|
|
|
function chunk(text: string, color: string, bold = false): TextChunk {
|
|
return {
|
|
__isChunk: true,
|
|
text,
|
|
fg: RGBA.fromHex(color),
|
|
attributes: bold ? TextAttributes.BOLD : 0,
|
|
}
|
|
}
|