mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-01 15:32:11 +08:00
Merge remote-tracking branch 'origin/main' into mark/upstream-manual-prompt-zdiff
This commit is contained in:
@@ -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>
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user