Merge branch 'main' into docs/cleanup-questions-readme

This commit is contained in:
Thomas Boom
2026-05-11 15:07:12 +02:00
committed by GitHub
3 changed files with 128 additions and 35 deletions
@@ -13,11 +13,11 @@ export function fmtPrice(n: number): string {
return `$${n.toFixed(2)}/1M`
}
export function fmtCachedPrice(cost: Cost): string {
export function fmtCachedPrice(cost: Cost): string | null {
const read = cost.cache.read
if (read > 0) return fmtPrice(read)
if (cost.input === 0) return fmtPrice(0)
return "N/A"
return null
}
export function avgPrice(cost: Cost): number {
@@ -31,3 +31,9 @@ export function fmtContext(n: number): string {
if (n >= 1_000) return `${(n / 1_000).toFixed(n % 1_000 === 0 ? 0 : 1)}K`
return String(n)
}
export function fmtDate(s: string): string {
const d = new Date(s)
if (isNaN(d.getTime())) return s
return d.toLocaleDateString(undefined, { year: "numeric", month: "short" })
}
@@ -1,8 +1,9 @@
import { TextAttributes } from "@opentui/core"
import { useTerminalDimensions } from "@opentui/solid"
import { createMemo, Show } from "solid-js"
import { useTheme } from "@tui/context/theme"
import type { Model } from "@kilocode/sdk/v2"
import { avgPrice, fmtCachedPrice, fmtContext, fmtPrice } from "./model-info-panel-utils"
import { Show } from "solid-js"
import { avgPrice, fmtCachedPrice, fmtContext, fmtDate, fmtPrice } from "./model-info-panel-utils"
interface Props {
model: Model
@@ -12,6 +13,40 @@ interface Props {
export function ModelInfoPanel(props: Props) {
const { theme } = useTheme()
const m = () => props.model
const dimensions = useTerminalDimensions()
const maxHeight = createMemo(() => Math.floor(dimensions().height / 2) - 3)
const cost = () => m().cost
const cached = () => (cost() ? fmtCachedPrice(cost()) : null)
const avg = () => (cost() ? avgPrice(cost()) : undefined)
const caps = () => m().capabilities
const inputs = () => caps()?.input
const outputs = () => caps()?.output
const activeInputModalities = () => {
if (!inputs()) return [] as string[]
return Object.entries(inputs())
.filter(([k, v]) => v && k !== "text")
.map(([k]) => k.charAt(0).toUpperCase() + k.slice(1))
}
const activeOutputModalities = () => {
if (!outputs()) return [] as string[]
return Object.entries(outputs())
.filter(([k, v]) => v && k !== "text")
.map(([k]) => k.charAt(0).toUpperCase() + k.slice(1))
}
const inputLine = () => {
const mods = activeInputModalities()
return mods.length > 0 ? `In: ${mods.join(", ")}` : null
}
const outputLine = () => {
const mods = activeOutputModalities()
return mods.length > 0 ? `Out: ${mods.join(", ")}` : null
}
const desc = () => {
const d = m().options?.description
return typeof d === "string" && d.trim() ? d : null
}
return (
<box
@@ -24,42 +59,94 @@ export function ModelInfoPanel(props: Props) {
gap={1}
flexShrink={0}
>
<box>
<text fg={theme.text} attributes={TextAttributes.BOLD}>
{m().name ?? m().id ?? "Model"}
</text>
<text fg={theme.textMuted}>{props.provider ?? m().providerID ?? ""}</text>
</box>
<box>
<scrollbox
maxHeight={maxHeight()}
paddingRight={1}
>
<box>
<text fg={theme.text} attributes={TextAttributes.BOLD}>
{m().name ?? m().id ?? "Model"}
</text>
<text fg={theme.textMuted}>{props.provider ?? m().providerID ?? ""}</text>
</box>
<Show when={m().isFree}>
<box flexDirection="row" justifyContent="space-between">
<box>
<text fg={theme.text}>Free</text>
</box>
</Show>
<Show when={!m().isFree}>
<Show when={m().family}>
<box flexDirection="row" justifyContent="space-between">
<text fg={theme.textMuted}>Input</text>
<text fg={theme.text}>{m() ? fmtPrice(m().cost.input) : "—"}</text>
</box>
<box flexDirection="row" justifyContent="space-between">
<text fg={theme.textMuted}>Output</text>
<text fg={theme.text}>{m() ? fmtPrice(m().cost.output) : "—"}</text>
</box>
<box flexDirection="row" justifyContent="space-between">
<text fg={theme.textMuted}>Cache Read</text>
<text fg={theme.text}>{m() ? fmtCachedPrice(m().cost) : "—"}</text>
</box>
<box flexDirection="row" justifyContent="space-between"></box>
<box flexDirection="row" justifyContent="space-between">
<text fg={theme.textMuted}>Context Size</text>
<text fg={theme.text}>{m() ? fmtContext(m().limit.context) : "—"}</text>
</box>
<box flexDirection="row" justifyContent="space-between">
<text fg={theme.textMuted}>Average Cost</text>
<text fg={theme.text}>{m() ? fmtPrice(avgPrice(m().cost)) : "—"}</text>
<text fg={theme.textMuted}>Family</text>
<text fg={theme.text}>{m().family!.charAt(0).toUpperCase() + m().family!.slice(1)}</text>
</box>
</Show>
</box>
<Show when={m().release_date}>
<box flexDirection="row" justifyContent="space-between">
<text fg={theme.textMuted}>Released</text>
<text fg={theme.text}>{fmtDate(m().release_date)}</text>
</box>
</Show>
<Show when={!m().isFree}>
<box>
<box flexDirection="row" justifyContent="space-between">
<text fg={theme.textMuted}>Input</text>
<text fg={theme.text}>{m() ? fmtPrice(m().cost.input) : "—"}</text>
</box>
<box flexDirection="row" justifyContent="space-between">
<text fg={theme.textMuted}>Output</text>
<text fg={theme.text}>{m() ? fmtPrice(m().cost.output) : "—"}</text>
</box>
<Show when={cached()}>
<box flexDirection="row" justifyContent="space-between">
<text fg={theme.textMuted}>Cached</text>
<text fg={theme.text}>{cached()}</text>
</box>
</Show>
<Show when={avg() !== undefined}>
<box flexDirection="row" justifyContent="space-between">
<text fg={theme.textMuted}>Avg Cost</text>
<text fg={theme.text}>{m() ? fmtPrice(avg()!) : "—"}</text>
</box>
</Show>
<box flexDirection="row" justifyContent="space-between">
<text fg={theme.textMuted}>Context</text>
<text fg={theme.text}>{m() ? fmtContext(m().limit.context) : "—"}</text>
</box>
</box>
</Show>
<Show when={caps()?.reasoning || inputLine() || outputLine()}>
<box flexDirection="column">
<Show when={caps()?.reasoning}>
<box flexDirection="row" justifyContent="space-between">
<text fg={theme.textMuted}>Reasoning</text>
<text fg={theme.text}>Yes</text>
</box>
</Show>
<Show when={inputLine()}>
{(line) => (
<box flexDirection="row" justifyContent="space-between">
<text fg={theme.textMuted}>Caps</text>
<text fg={theme.text}>{line().replace(/^In:\s*/, "")}</text>
</box>
)}
</Show>
<Show when={outputLine()}>
{(line) => (
<box flexDirection="row" justifyContent="space-between">
<text fg={theme.textMuted}>Out</text>
<text fg={theme.text}>{line().replace(/^Out:\s*/, "")}</text>
</box>
)}
</Show>
</box>
</Show>
<Show when={desc()}>
<text fg={theme.textMuted}>{" "}</text>
<text fg={theme.textMuted} width={23}>
{desc()}
</text>
</Show>
</scrollbox>
</box>
)
}
@@ -22,8 +22,8 @@ describe("model info panel price formatting", () => {
expect(fmtCachedPrice({ input: 0, output: 0, cache: { read: 0, write: 0 } })).toBe("Free")
})
test("fmtCachedPrice returns N/A without cache read", () => {
expect(fmtCachedPrice({ input: 3, output: 15, cache: { read: 0, write: 0 } })).toBe("N/A")
test("fmtCachedPrice returns null without cache read", () => {
expect(fmtCachedPrice({ input: 3, output: 15, cache: { read: 0, write: 0 } })).toBeNull()
})
test("avgPrice uses cache weighted formula when cache read exists", () => {