Files
nanobot/tui/src/footer-hints.ts
T

144 lines
4.9 KiB
TypeScript

import { RGBA, StyledText, TextAttributes, type TextChunk } from "@opentui/core"
import { formatTokenCount } from "./context-panel"
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"
| "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`)
}
if (
typeof usage.cached_tokens === "number"
&& typeof usage.prompt_tokens === "number"
&& usage.prompt_tokens > 0
) {
const hitRate = Math.min(100, Math.max(0, Math.round(
usage.cached_tokens * 100 / usage.prompt_tokens,
)))
parts.push(`cache ${hitRate}%`)
}
if (width >= 72) {
const prompt = usage.prompt_tokens
const completion = usage.completion_tokens
if (typeof prompt === "number" || typeof completion === "number") {
parts.push(`↑${formatTokenCount(prompt || 0)}${formatTokenCount(completion || 0)}`)
}
}
if (width >= 112 && typeof usage.ttft_ms === "number") {
const requests = Math.max(1, usage.timed_requests || 1)
const average = usage.ttft_ms / requests
parts.push(`TTFT ${average < 1000 ? `${Math.round(average)}ms` : `${(average / 1000).toFixed(1)}s`}`)
}
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 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") 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,
}
}