Merge remote-tracking branch 'origin/main' into mark/upstream-manual-prompt-zdiff

This commit is contained in:
Mark IJbema
2026-05-05 12:48:05 +02:00
6 changed files with 237 additions and 26 deletions
@@ -1,4 +1,5 @@
import { createMemo, createSignal } from "solid-js"
import { useTerminalDimensions } from "@opentui/solid" // kilocode_change
import { createEffect, createMemo, createSignal, Show } from "solid-js" // kilocode_change
import { useLocal } from "@tui/context/local"
import { useSync } from "@tui/context/sync"
import { map, pipe, flatMap, entries, filter, sortBy, take } from "remeda"
@@ -7,8 +8,10 @@ import { useDialog } from "@tui/ui/dialog"
import { createDialogProviderOptions, DialogProvider } from "./dialog-provider"
import { DialogVariant } from "./dialog-variant"
import { useKeybind } from "../context/keybind"
import type { Model } from "@kilocode/sdk/v2" // kilocode_change
import * as fuzzysort from "fuzzysort"
import { useConnected } from "./use-connected"
import { ModelInfoPanel } from "@/kilocode/components/model-info-panel" // kilocode_change
export function DialogModel(props: { providerID?: string }) {
const local = useLocal()
@@ -16,6 +19,7 @@ export function DialogModel(props: { providerID?: string }) {
const dialog = useDialog()
const keybind = useKeybind()
const [query, setQuery] = createSignal("")
const dimensions = useTerminalDimensions() // kilocode_change
const connected = useConnected()
const providers = createDialogProviderOptions()
@@ -31,6 +35,36 @@ export function DialogModel(props: { providerID?: string }) {
const showExtra = createMemo(() => connected() && !props.providerID)
// kilocode_change start
const wide = createMemo(() => dimensions().width >= 108)
const [preview, setPreview] = createSignal<{
model: Model
provider: string
}>()
const lookup = (providerID: string, modelID: string) => {
const provider = sync.data.provider.find((x) => x.id === providerID)
const model = provider?.models[modelID]
if (!provider || !model) return
return {
model,
provider: provider.name,
}
}
createEffect(() => {
dialog.setSize(wide() ? "xlarge" : "large")
})
createEffect(() => {
const current = local.model.current()
if (!current) return
const next = lookup(current.providerID, current.modelID)
if (!next) return
setPreview(next)
})
// kilocode_change end
const options = createMemo(() => {
const needle = query().trim()
const showSections = showExtra() && needle.length === 0
@@ -168,31 +202,49 @@ export function DialogModel(props: { providerID?: string }) {
dialog.clear()
}
// kilocode_change start
return (
<DialogSelect<ReturnType<typeof options>[number]["value"]>
options={options()}
keybind={[
{
keybind: keybind.all.model_provider_list?.[0],
title: connected() ? "Connect provider" : "View all providers",
onTrigger() {
dialog.replace(() => <DialogProvider />)
},
},
{
keybind: keybind.all.model_favorite_toggle?.[0],
title: "Favorite",
disabled: !connected(),
onTrigger: (option) => {
local.model.toggleFavorite(option.value as { providerID: string; modelID: string })
},
},
]}
onFilter={setQuery}
flat={true}
skipFilter={true}
title={title()}
current={local.model.current()}
/>
<box flexDirection="row">
<box flexGrow={1} flexShrink={1}>
<DialogSelect<ReturnType<typeof options>[number]["value"]>
options={options()}
keybind={[
{
keybind: keybind.all.model_provider_list?.[0],
title: connected() ? "Connect provider" : "View all providers",
onTrigger() {
dialog.replace(() => <DialogProvider />)
},
},
{
keybind: keybind.all.model_favorite_toggle?.[0],
title: "Favorite",
disabled: !connected(),
onTrigger: (option) => {
local.model.toggleFavorite(option.value as { providerID: string; modelID: string })
},
},
]}
onFilter={setQuery}
onMove={(option) => {
if (typeof option.value === "string") {
setPreview(undefined)
return
}
const next = lookup(option.value.providerID, option.value.modelID)
if (!next) return
setPreview(next)
}}
flat={true}
skipFilter={true}
title={title()}
current={local.model.current()}
/>
</box>
<Show when={wide() && preview()}>
{(item) => <ModelInfoPanel model={item().model} provider={item().provider} />}
</Show>
</box>
)
// kilocode_change end
}
@@ -0,0 +1,33 @@
interface Cost {
input: number
output: number
cache: {
read: number
write: number
}
}
export function fmtPrice(n: number): string {
if (n === 0) return "Free"
if (n < 0.01) return `$${n.toFixed(4)}/1M`
return `$${n.toFixed(2)}/1M`
}
export function fmtCachedPrice(cost: Cost): string {
const read = cost.cache.read
if (read > 0) return fmtPrice(read)
if (cost.input === 0) return fmtPrice(0)
return "N/A"
}
export function avgPrice(cost: Cost): number {
const read = cost.cache.read
if (read > 0) return read * 0.7 + cost.input * 0.2 + cost.output * 0.1
return cost.input * 0.9 + cost.output * 0.1
}
export function fmtContext(n: number): string {
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(n % 1_000_000 === 0 ? 0 : 1)}M`
if (n >= 1_000) return `${(n / 1_000).toFixed(n % 1_000 === 0 ? 0 : 1)}K`
return String(n)
}
@@ -0,0 +1,65 @@
import { TextAttributes } from "@opentui/core"
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"
interface Props {
model: Model
provider: string
}
export function ModelInfoPanel(props: Props) {
const { theme } = useTheme()
const m = () => props.model
return (
<box
width={30}
border={["left"]}
borderColor={theme.border}
paddingLeft={2}
paddingRight={2}
paddingTop={1}
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>
<Show when={m().isFree}>
<box flexDirection="row" justifyContent="space-between">
<text fg={theme.text}>Free</text>
</box>
</Show>
<Show when={!m().isFree}>
<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>
</box>
</Show>
</box>
</box>
)
}
@@ -0,0 +1,52 @@
import { describe, expect, test } from "bun:test"
import { avgPrice, fmtCachedPrice, fmtContext, fmtPrice } from "../../src/kilocode/components/model-info-panel-utils"
describe("model info panel price formatting", () => {
test("fmtPrice returns Free for zero", () => {
expect(fmtPrice(0)).toBe("Free")
})
test("fmtPrice uses four decimals for very small prices", () => {
expect(fmtPrice(0.0095)).toBe("$0.0095/1M")
})
test("fmtPrice uses two decimals for standard prices", () => {
expect(fmtPrice(3)).toBe("$3.00/1M")
})
test("fmtCachedPrice returns cache read price when available", () => {
expect(fmtCachedPrice({ input: 3, output: 15, cache: { read: 0.3, write: 0 } })).toBe("$0.30/1M")
})
test("fmtCachedPrice returns Free for free models", () => {
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("avgPrice uses cache weighted formula when cache read exists", () => {
const val = avgPrice({ input: 3, output: 15, cache: { read: 0.3, write: 0 } })
expect(val).toBe(2.31)
})
test("avgPrice uses input and output weighted formula without cache read", () => {
const val = avgPrice({ input: 3, output: 15, cache: { read: 0, write: 0 } })
expect(val).toBe(4.2)
})
})
describe("model info panel context formatting", () => {
test("formats thousands as K", () => {
expect(fmtContext(128000)).toBe("128K")
})
test("formats millions as M", () => {
expect(fmtContext(1000000)).toBe("1M")
})
test("returns exact value for small contexts", () => {
expect(fmtContext(800)).toBe("800")
})
})
@@ -0,0 +1,7 @@
import type { Model as SDKModel } from "@kilocode/sdk/v2"
import { ModelInfoPanel } from "@/kilocode/components/model-info-panel"
type Assert<T extends true> = T
type Props = Parameters<typeof ModelInfoPanel>[0]
type _SyncModelMatchesPanel = Assert<SDKModel extends Props["model"] ? true : false>
+2
View File
@@ -99,7 +99,9 @@ if (Script.release) {
// Use an absolute path for the CHANGELOG because the imported SDK build
// script chdirs into packages/sdk/js, so a relative path would miss the file
// and fall through to the "No notable changes" default.
const kind = Script.preview ? "pre-release" : "release"
const flags = Script.preview ? ["--draft=false", "--prerelease"] : ["--draft=false"]
flags.push("--title", `v${Script.version} (${kind})`)
const changelogPath = fileURLToPath(new URL("../packages/kilo-vscode/CHANGELOG.md", import.meta.url))
const changelog = await Bun.file(changelogPath)
.text()