refactor: improve models config and explore pages

This commit is contained in:
Catriel Müller
2026-05-27 19:11:46 -03:00
parent e4e1c253e9
commit fb4f5cc322
8 changed files with 598 additions and 234 deletions
@@ -443,7 +443,7 @@ export function AgentBuilderRoute() {
</div>
<footer class="drawer-footer">
<Button variant="secondary" onClick={state.close}>
<Button variant="ghost" onClick={state.close}>
Cancel
</Button>
<Button
@@ -512,7 +512,7 @@ export function AgentBuilderRoute() {
</div>
<footer class="drawer-footer">
<Button variant="secondary" onClick={state.close}>
<Button variant="ghost" onClick={state.close}>
Cancel
</Button>
<Button variant="primary" disabled={Boolean(state.ctx.saving()) || state.locked()} onClick={state.saveTools}>
@@ -5,24 +5,13 @@ import { Tag } from "@kilocode/kilo-web-ui/tag"
import type { Model } from "@kilocode/sdk/v2/client"
import { SearchField } from "../../components/SearchField"
import { text } from "../../shared/utils"
import { ConfigPage, ConfigToolbar, SourceBadge } from "./ConfigPage"
import { ConfigPage, SourceBadge } from "./ConfigPage"
import { type Capability, type ModelField, useModelSettings } from "./state/models"
function fmtPrice(n: number) {
if (n === 0) return "Free"
if (n < 0.01) return `$${n.toFixed(4)}/1M`
return `$${n.toFixed(2)}/1M`
}
function fmtCachedPrice(cost: Model["cost"]) {
if (cost.cache.read > 0) return fmtPrice(cost.cache.read)
if (cost.input === 0) return fmtPrice(0)
return null
}
function avgPrice(cost: Model["cost"]) {
if (cost.cache.read > 0) return cost.cache.read * 0.7 + cost.input * 0.2 + cost.output * 0.1
return cost.input * 0.9 + cost.output * 0.1
function money(n: number) {
if (n === 0) return "$0.00"
if (n < 0.01) return `$${n.toFixed(4)}`
return `$${n.toFixed(2)}`
}
function fmtContext(n: number) {
@@ -41,32 +30,19 @@ function title(input: string) {
return input.charAt(0).toUpperCase() + input.slice(1)
}
function mods(input: Record<string, boolean>) {
const values = Object.entries(input)
.filter(([key, value]) => value && key !== "text")
.map(([key]) => title(key))
return values.length > 0 ? values.join(", ") : "Text only"
}
function desc(model: Model) {
const value = model.options?.description
if (typeof value === "string" && value.trim()) return value
return null
}
function capLabel(cap: Capability) {
if (cap === "toolcall") return "Tools"
if (cap === "attachment") return "Attachments"
if (cap === "temperature") return "Temperature"
if (cap === "interleaved") return "Interleaved"
if (cap === "input:audio") return "Input audio"
if (cap === "input:image") return "Input image"
if (cap === "input:video") return "Input video"
if (cap === "input:pdf") return "Input PDF"
if (cap === "output:audio") return "Output audio"
if (cap === "output:image") return "Output image"
if (cap === "output:video") return "Output video"
return "Output PDF"
function chips(model: Model) {
const list: string[] = []
if (model.capabilities.toolcall) list.push("toolcall")
if (model.capabilities.attachment) list.push("attachment")
if (model.capabilities.input.image) list.push("vision")
if (model.capabilities.input.audio || model.capabilities.output.audio) list.push("audio")
return list
}
function step(max: number) {
@@ -96,9 +72,63 @@ function point(event: PointerEvent, root: HTMLElement, max: number) {
return snap(ratio * max, max)
}
function rangeLabel(low: number, high: number, max: number) {
const top = high >= max && max > 0 ? `${fmtContext(high)}+` : fmtContext(high)
return `${fmtContext(low)} - ${top}`
function Stat(props: { label: string; value: string; sub?: string; mono?: boolean }) {
return (
<div class="model-stat">
<span>{props.label}</span>
<strong classList={{ mono: props.mono }}>
{props.value}
<Show when={props.sub}>{(sub) => <small>{sub()}</small>}</Show>
</strong>
</div>
)
}
const caps = [
{ key: "toolcall", label: "toolcall" },
{ key: "attachment", label: "attachment" },
{ key: "input:image", label: "vision" },
{ key: "input:audio", label: "audio" },
] satisfies { key: Capability; label: string }[]
type Option = { value: string; label: string }
function FilterSelect(props: { label: string; value: string; options: Option[]; onSelect: (value: string) => void }) {
const current = () => props.options.find((option) => option.value === props.value)?.label ?? props.value
function choose(value: string, event: MouseEvent & { currentTarget: HTMLButtonElement }) {
props.onSelect(value)
event.currentTarget.closest("details")?.removeAttribute("open")
}
function toggle(event: Event & { currentTarget: HTMLDetailsElement }) {
if (!event.currentTarget.open) return
event.currentTarget.parentElement?.querySelectorAll(".models-select[open]").forEach((node) => {
if (node !== event.currentTarget) node.removeAttribute("open")
})
}
return (
<details class="models-select" onToggle={toggle}>
<summary aria-label={props.label}>{current()}</summary>
<div class="models-select-menu" role="listbox" aria-label={props.label}>
<For each={props.options}>
{(option) => (
<button
class="models-select-option"
classList={{ selected: option.value === props.value }}
type="button"
role="option"
aria-selected={option.value === props.value}
onClick={(event) => choose(option.value, event)}
>
{option.label}
</button>
)}
</For>
</div>
</details>
)
}
export function ModelsRoute() {
@@ -209,7 +239,7 @@ export function ModelsDefaultRoute() {
</div>
<footer class="drawer-footer">
<Button variant="secondary" onClick={state.close}>
<Button variant="ghost" onClick={state.close}>
Cancel
</Button>
<Button variant="primary" disabled={Boolean(state.ctx.saving()) || !state.choice()} onClick={state.save}>
@@ -261,104 +291,113 @@ export function ModelsAvailableRoute() {
return (
<Show when={state.snap()}>
<ConfigPage title="Explore" actions={<Tag>{state.models().length}</Tag>}>
<ConfigToolbar title="Model Filters" description="Search and filter configured provider models.">
<label>
<span>Search</span>
<input
value={state.search()}
placeholder="Model name"
onInput={(event) => state.setSearch(event.currentTarget.value)}
<ConfigPage
title={
<span class="config-title-count">
Explore models
<Tag>{state.models().length}</Tag>
</span>
}
description="All models exposed by configured providers. Set defaults under Models > Defaults."
>
<section class="models-filter-card">
<div class="models-filter-row models-filter-primary">
<label class="models-filter-field models-search-field">
<span>Search</span>
<input
value={state.search()}
placeholder="Search by name or ID..."
onInput={(event) => state.setSearch(event.currentTarget.value)}
/>
</label>
<FilterSelect
label="Provider"
value={state.filter()}
options={[{ value: "all", label: "All providers" }, ...state.providers().map((provider) => ({ value: provider.id, label: provider.name }))]}
onSelect={state.setFilter}
/>
</label>
<label>
<span>Provider</span>
<select value={state.filter()} onChange={(event) => state.setFilter(event.currentTarget.value)}>
<option value="all">All providers</option>
<For each={state.providers()}>{(provider) => <option value={provider.id}>{provider.name}</option>}</For>
</select>
</label>
<label>
<span>Cost</span>
<select value={state.price()} onChange={(event) => state.setPrice(event.currentTarget.value)}>
<option value="all">Free and paid</option>
<option value="free">Free only</option>
<option value="paid">Paid only</option>
</select>
</label>
<label>
<span>Reasoning</span>
<select value={state.reason()} onChange={(event) => state.setReason(event.currentTarget.value)}>
<option value="all">Any reasoning</option>
<option value="reasoning">Reasoning only</option>
<option value="standard">No reasoning</option>
</select>
</label>
<label>
<span>Favorites</span>
<select
value={state.starred() ? "favorites" : "all"}
onChange={(event) => state.setStarred(event.currentTarget.value === "favorites")}
<FilterSelect
label="Cost"
value={state.price()}
options={[
{ value: "all", label: "Any cost" },
{ value: "free", label: "Free" },
{ value: "paid", label: "Paid" },
]}
onSelect={state.setPrice}
/>
<FilterSelect
label="Reasoning"
value={state.reason()}
options={[
{ value: "all", label: "Reasoning · any" },
{ value: "reasoning", label: "Reasoning · yes" },
{ value: "standard", label: "Reasoning · no" },
]}
onSelect={state.setReason}
/>
<button
class="models-favorite-filter"
classList={{ selected: state.starred() }}
type="button"
aria-pressed={state.starred()}
onClick={() => state.setStarred(!state.starred())}
>
<option value="all">All models</option>
<option value="favorites">Favorites only</option>
</select>
</label>
<label class="context-filter">
<span>Context</span>
<div
class="context-range"
style={`--context-min: ${pct(state.low(), state.max())}%; --context-max: ${pct(state.top(), state.max())}%;`}
>
<span class="context-track" aria-hidden="true" />
<span class="context-fill" aria-hidden="true" />
<button
class="context-min"
aria-label="Minimum context"
aria-valuemin="0"
aria-valuemax={state.top()}
aria-valuenow={state.low()}
disabled={state.max() === 0}
role="slider"
type="button"
onKeyDown={(event) => key("min", event)}
onPointerDown={(event) => drag("min", event)}
/>
<button
class="context-max"
aria-label="Maximum context"
aria-valuemin={state.low()}
aria-valuemax={state.max()}
aria-valuenow={state.top()}
disabled={state.max() === 0}
role="slider"
type="button"
onKeyDown={(event) => key("max", event)}
onPointerDown={(event) => drag("max", event)}
/>
</div>
<small>{rangeLabel(state.low(), state.top(), state.max())}</small>
</label>
<details class="multi-check">
<summary>{state.caps().length ? `${state.caps().length} capabilities` : "Capabilities"}</summary>
<div class="multi-check-menu">
<For each={state.capabilities}>
{(cap) => (
<label>
<input type="checkbox" checked={state.caps().includes(cap)} onChange={() => state.toggle(cap)} />
<span>{capLabel(cap)}</span>
</label>
)}
</For>
</div>
</details>
</ConfigToolbar>
<span class="model-star" classList={{ active: state.starred() }} aria-hidden="true" />
Favorites
</button>
</div>
<div class="models">
<div class="models-filter-row models-filter-secondary">
<div class="models-context-filter">
<span>Context max</span>
<div
class="context-range"
style={`--context-min: 0%; --context-max: ${pct(state.top(), state.max())}%;`}
>
<span class="context-track" aria-hidden="true" />
<span class="context-fill" aria-hidden="true" />
<button
class="context-max"
aria-label="Maximum context"
aria-valuemin="0"
aria-valuemax={state.max()}
aria-valuenow={state.top()}
disabled={state.max() === 0}
role="slider"
type="button"
onKeyDown={(event) => key("max", event)}
onPointerDown={(event) => drag("max", event)}
/>
</div>
<strong class="models-context-value mono">{fmtContext(state.top())}</strong>
</div>
<div class="models-capabilities-filter">
<span>Capabilities</span>
<div class="models-capabilities-list">
<For each={caps}>
{(cap) => (
<button
class="models-capability-toggle"
classList={{ selected: state.caps().includes(cap.key) }}
type="button"
aria-pressed={state.caps().includes(cap.key)}
onClick={() => state.toggle(cap.key)}
>
{cap.label}
</button>
)}
</For>
</div>
</div>
</div>
</section>
<div class="models explore-models">
<Show when={state.models().length} fallback={<p class="empty">No models match the current filters.</p>}>
<For each={state.models()}>
{(item) => (
<article class="model">
<article class="model explore-model-card">
<div class="model-main">
<div class="model-title">
<button
@@ -374,62 +413,25 @@ export function ModelsAvailableRoute() {
<span>{item.id}</span>
</div>
</div>
<div class="tags">
<Tag>{item.provider.name}</Tag>
<Tag>{item.model.isFree ? "free" : "paid"}</Tag>
<div class="tags model-tags">
<Tag tone="brand">{item.provider.id}</Tag>
<Tag tone={item.model.isFree ? "success" : "neutral"}>{item.model.isFree ? "free" : "paid"}</Tag>
</div>
</div>
<Show when={desc(item.model)}>{(value) => <p class="model-description">{value()}</p>}</Show>
<div class="model-info-grid">
<Show when={item.model.family}>
<div class="model-stat">
<span>Family</span>
<strong>{title(item.model.family!)}</strong>
</div>
</Show>
<Show when={item.model.release_date}>
<div class="model-stat">
<span>Released</span>
<strong>{fmtDate(item.model.release_date)}</strong>
</div>
</Show>
<div class="model-stat">
<span>Input</span>
<strong>{fmtPrice(item.model.cost.input)}</strong>
</div>
<div class="model-stat">
<span>Output</span>
<strong>{fmtPrice(item.model.cost.output)}</strong>
</div>
<Show when={fmtCachedPrice(item.model.cost)}>
{(value) => (
<div class="model-stat">
<span>Cached</span>
<strong>{value()}</strong>
</div>
)}
</Show>
<div class="model-stat">
<span>Avg Cost</span>
<strong>{fmtPrice(avgPrice(item.model.cost))}</strong>
</div>
<div class="model-stat">
<span>Context</span>
<strong>{fmtContext(item.model.limit.context)}</strong>
</div>
<div class="model-stat">
<span>Reasoning</span>
<strong>{item.model.capabilities.reasoning ? "Yes" : "No"}</strong>
</div>
<div class="model-stat">
<span>Input caps</span>
<strong>{mods(item.model.capabilities.input)}</strong>
</div>
<div class="model-stat">
<span>Output caps</span>
<strong>{mods(item.model.capabilities.output)}</strong>
</div>
<Stat label="Family" value={item.model.family ? title(item.model.family) : "Unknown"} />
<Stat label="Released" value={item.model.release_date ? fmtDate(item.model.release_date) : "Unknown"} />
<Stat label="Context" value={fmtContext(item.model.limit.context)} mono />
<Stat label="Input" value={money(item.model.cost.input)} sub="/ 1M tok" mono />
<Stat label="Output" value={money(item.model.cost.output)} sub="/ 1M tok" mono />
<Stat label="Reasoning" value={item.model.capabilities.reasoning ? "Yes" : "No"} />
</div>
<Show when={chips(item.model).length}>
<div class="tags model-capabilities">
<For each={chips(item.model)}>{(cap) => <Tag>{cap}</Tag>}</For>
</div>
</Show>
</article>
)}
</For>
@@ -96,7 +96,7 @@ export function ProvidersRoute() {
when={state.mode() === "form"}
fallback={
<>
<header class="drawer-header add-provider-header">
<header class="drawer-header">
<div>
<h2>Add provider</h2>
<span>Choose a provider, then enter credentials.</span>
@@ -155,7 +155,7 @@ export function ProvidersRoute() {
</Show>
</Card>
</div>
<footer class="drawer-footer add-provider-footer">
<footer class="drawer-footer">
<Button variant="ghost" onClick={state.close}>
Cancel
</Button>
@@ -280,7 +280,7 @@ export function ProvidersRoute() {
</div>
<footer class="drawer-footer">
<Button variant="secondary" onClick={state.close}>
<Button variant="ghost" onClick={state.close}>
Cancel
</Button>
<Button variant="primary" disabled={Boolean(state.ctx.saving())} onClick={state.save}>
@@ -387,7 +387,7 @@ export function ProvidersRoute() {
</div>
<footer class="drawer-footer">
<Button variant="secondary" onClick={state.close}>
<Button variant="ghost" onClick={state.close}>
Cancel
</Button>
<Show when={state.method()?.type === "api"}>
+10
View File
@@ -53,6 +53,16 @@ body {
padding-right: 2.5rem;
}
.kilo-console select option {
background: var(--popover);
color: var(--popover-foreground);
}
.kilo-console select option:checked {
background: var(--primary);
color: var(--primary-foreground);
}
.kilo-console input:focus,
.kilo-console select:focus,
.kilo-console textarea:focus {
+331 -14
View File
@@ -3,6 +3,266 @@
gap: 0.5rem;
}
.kilo-console .models-filter-card {
display: grid;
gap: 0.75rem;
border: 1px solid var(--border);
border-radius: var(--radius-lg);
background: var(--card);
padding: 0.75rem;
}
.kilo-console .models-filter-row {
display: grid;
gap: 0.5rem;
align-items: center;
}
.kilo-console .models-filter-primary {
grid-template-columns: minmax(14rem, 1.6fr) repeat(3, minmax(8.75rem, 1fr)) auto;
}
.kilo-console .models-filter-field {
position: relative;
display: grid;
min-width: 0;
}
.kilo-console .models-filter-field > span {
position: absolute;
overflow: hidden;
width: 1px;
height: 1px;
clip: rect(0 0 0 0);
white-space: nowrap;
}
.kilo-console .models-filter-field input,
.kilo-console .models-select summary,
.kilo-console .models-favorite-filter,
.kilo-console .models-capability-toggle {
height: 2rem;
min-height: 2rem;
border-radius: var(--radius-md);
font-size: 0.75rem;
line-height: 1.45;
}
.kilo-console .models-filter-field input {
padding: 0 0.75rem;
}
.kilo-console .models-select {
position: relative;
min-width: 0;
}
.kilo-console .models-select summary {
display: flex;
gap: 0.5rem;
align-items: center;
justify-content: space-between;
overflow: hidden;
border: 1px solid var(--border);
background: color-mix(in oklab, var(--input) 35%, transparent);
color: var(--foreground);
cursor: pointer;
list-style: none;
padding: 0 0.75rem;
white-space: nowrap;
}
.kilo-console .models-select summary::-webkit-details-marker {
display: none;
}
.kilo-console .models-select summary::marker {
content: "";
}
.kilo-console .models-select summary::after {
flex: 0 0 auto;
width: 1rem;
height: 1rem;
background-image: url("data:image/svg+xml,%3Csvg width='16' height='16' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M4 6L8 10L12 6' stroke='%23a1a1aa' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-size: 1rem;
content: "";
}
.kilo-console .models-select[open] summary,
.kilo-console .models-select summary:focus-visible {
border-color: var(--primary);
box-shadow: 0 0 0 2px color-mix(in oklab, var(--primary) 25%, transparent);
outline: 0;
}
.kilo-console .models-select-menu {
position: absolute;
z-index: 35;
top: calc(100% + 0.25rem);
left: 0;
display: grid;
width: 100%;
max-height: 14rem;
overflow-y: auto;
border: 1px solid var(--border);
border-radius: var(--radius-md);
background: var(--popover);
box-shadow: 0 1rem 2rem color-mix(in oklab, black 24%, transparent);
padding: 0.25rem;
}
.kilo-console .models-select-option {
display: block;
width: 100%;
min-height: 1.75rem;
border: 0;
border-radius: calc(var(--radius-md) - 0.125rem);
background: transparent;
color: var(--popover-foreground);
cursor: pointer;
font: inherit;
font-size: 0.75rem;
line-height: 1.45;
padding: 0.25rem 0.5rem;
text-align: left;
}
.kilo-console .models-select-option:hover,
.kilo-console .models-select-option:focus-visible {
background: color-mix(in oklab, var(--primary) 12%, transparent);
color: var(--foreground);
outline: 0;
}
.kilo-console .models-select-option.selected {
background: color-mix(in oklab, var(--primary) 20%, transparent);
color: var(--foreground);
}
.kilo-console .models-favorite-filter,
.kilo-console .models-capability-toggle {
display: inline-flex;
gap: 0.375rem;
align-items: center;
justify-content: center;
border: 1px solid var(--border);
background: color-mix(in oklab, var(--input) 35%, transparent);
color: var(--muted-foreground);
cursor: pointer;
font: inherit;
font-size: 0.75rem;
font-weight: 500;
padding: 0 0.75rem;
}
.kilo-console .models-favorite-filter:hover,
.kilo-console .models-capability-toggle:hover,
.kilo-console .models-favorite-filter.selected,
.kilo-console .models-capability-toggle.selected {
border-color: var(--primary);
background: color-mix(in oklab, var(--primary) 10%, transparent);
color: var(--foreground);
}
.kilo-console .models-favorite-filter .model-star {
width: 1rem;
height: 1rem;
cursor: inherit;
padding: 0;
}
.kilo-console .models-filter-secondary {
display: flex;
gap: 1rem;
align-items: center;
}
.kilo-console .models-context-filter {
flex: 1 1 auto;
display: flex;
gap: 0.5rem;
align-items: center;
min-width: 16rem;
}
.kilo-console .models-context-filter > span,
.kilo-console .models-capabilities-filter > span {
flex: 0 0 auto;
color: var(--muted-foreground);
font-size: 0.6875rem;
line-height: 1.45;
white-space: nowrap;
}
.kilo-console .models-context-filter .context-range {
flex: 1 1 auto;
height: 1rem;
min-height: 1rem;
border: 0;
background: transparent;
padding: 0;
}
.kilo-console .models-context-filter .context-track,
.kilo-console .models-context-filter .context-fill {
top: 50%;
right: 0;
left: 0;
height: 0.25rem;
transform: translateY(-50%);
}
.kilo-console .models-context-filter .context-track {
background: var(--border);
}
.kilo-console .models-context-filter .context-fill {
right: calc(100% - var(--context-max));
background: var(--primary);
}
.kilo-console .models-context-filter .context-max {
top: 50%;
left: var(--context-max);
width: 0.875rem;
height: 0.875rem;
min-height: 0.875rem;
border: 1px solid var(--border);
background: var(--foreground);
box-shadow: 0 0 0 1px color-mix(in oklab, var(--background) 70%, transparent);
transform: translate(-50%, -50%);
}
.kilo-console .models-context-value {
flex: 0 0 3.5rem;
color: var(--foreground);
font-size: 0.6875rem;
font-weight: 500;
line-height: 1.45;
text-align: right;
}
.kilo-console .models-capabilities-filter {
flex: 0 0 auto;
display: flex;
gap: 0.5rem;
align-items: center;
}
.kilo-console .models-capabilities-list {
display: flex;
gap: 0.375rem;
align-items: center;
flex-wrap: wrap;
}
.kilo-console .explore-models {
grid-template-columns: repeat(auto-fill, minmax(22.5rem, 1fr));
gap: 0.625rem;
}
.kilo-console .model {
display: grid;
gap: 0.65rem;
@@ -12,11 +272,11 @@
}
.kilo-console .model strong,
.kilo-console .model span {
.kilo-console .model span:where(:not([data-component="tag"])) {
display: block;
}
.kilo-console .model span {
.kilo-console .model span:where(:not([data-component="tag"])) {
color: var(--text-weaker);
font-size: var(--font-size-small);
}
@@ -38,6 +298,7 @@
.kilo-console .model-star {
flex: 0 0 auto;
display: block;
width: 1.4rem;
height: 1.4rem;
border: 0;
@@ -116,21 +377,67 @@
.kilo-console .model-description {
margin: 0;
color: var(--text-weak);
font-size: var(--font-size-small);
font-size: 0.75rem;
line-height: 1.45;
}
.kilo-console .explore-model-card {
gap: 0.625rem;
border-color: var(--border);
background: var(--card);
padding: 0.875rem;
}
.kilo-console .explore-model-card .model-main {
align-items: flex-start;
flex-wrap: nowrap;
}
.kilo-console .explore-model-card .model-title {
flex: 1 1 auto;
}
.kilo-console .explore-model-card .model-title strong {
color: var(--foreground);
font-size: 0.875rem;
font-weight: 500;
line-height: 1.25;
}
.kilo-console .explore-model-card .model-title span {
margin-top: 0.125rem;
font-family: var(--font-family-mono);
font-size: 0.6875rem;
line-height: 1.45;
}
.kilo-console .explore-model-card .model-tags {
flex: 0 0 auto;
flex-direction: column;
align-items: flex-end;
gap: 0.25rem;
}
.kilo-console .model-capabilities {
justify-content: flex-start;
gap: 0.25rem;
}
.kilo-console .model-info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));
gap: 0.5rem;
grid-template-columns: repeat(3, minmax(0, 1fr));
overflow: hidden;
gap: 1px;
border: 1px solid var(--border);
border-radius: var(--radius-md);
background: var(--border);
}
.kilo-console .model-stat {
border: 1px solid var(--border-weak-base);
border-radius: var(--radius-md);
background: color-mix(in srgb, var(--background-stronger) 42%, transparent);
padding: 0.55rem 0.65rem;
border: 0;
border-radius: 0;
background: var(--card);
padding: 0.625rem;
}
.kilo-console .model-stat span,
@@ -140,15 +447,25 @@
.kilo-console .model-stat span {
color: var(--text-weaker);
font-size: 0.72rem;
font-weight: 700;
font-size: 0.625rem;
font-weight: 500;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.kilo-console .model-stat strong {
margin-top: 0.15rem;
display: flex;
gap: 0.25rem;
align-items: baseline;
margin-top: 0.25rem;
color: var(--text-strong);
font-size: var(--font-size-small);
font-weight: 700;
font-size: 0.75rem;
font-weight: 400;
line-height: 1.2;
}
.kilo-console .model-stat small {
color: var(--muted-foreground);
font-size: 0.625rem;
font-weight: 400;
}
+42 -30
View File
@@ -116,42 +116,38 @@
box-shadow: -1.5rem 0 4rem color-mix(in srgb, black 22%, transparent);
}
.kilo-console .drawer-header,
.kilo-console .drawer-footer {
.kilo-console .drawer-header {
display: flex;
gap: 1rem;
align-items: flex-start;
justify-content: space-between;
border-bottom: 1px solid var(--border-weak-base);
padding: 1rem;
}
.kilo-console .add-provider-header {
padding: 1.25rem 1rem 1.5rem;
}
.kilo-console .add-provider-header h2 {
font-size: 1.05rem;
line-height: 1.2;
}
.kilo-console .add-provider-header span {
font-size: 0.875rem;
padding: 0.75rem 1.25rem;
}
.kilo-console .drawer-header h2 {
margin: 0 0 0.25rem;
color: var(--text-strong);
font-size: 0.875rem;
font-weight: 500;
line-height: 1.45;
}
.kilo-console .drawer-header span {
color: var(--muted-foreground);
font-size: 0.75rem;
line-height: 1.45;
}
.kilo-console .drawer-footer {
display: flex;
gap: 0.5rem;
align-items: center;
justify-content: flex-end;
border-top: 1px solid var(--border-weak-base);
border-bottom: 0;
margin-top: auto;
padding: 0.75rem 1.25rem;
}
.kilo-console .drawer-search {
@@ -182,9 +178,9 @@
.kilo-console .add-provider-field h3 {
padding: 0 16px;
color: var(--foreground);
font-size: 0.875rem;
font-weight: 600;
line-height: 1.25rem;
font-size: 0.75rem;
font-weight: 500;
line-height: 1.45;
}
.kilo-console .add-provider-search {
@@ -216,6 +212,8 @@
.kilo-console .add-provider-picker {
flex: 1 1 auto;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.375rem;
min-height: 0;
overflow: auto;
padding-right: 0;
@@ -241,11 +239,27 @@
.kilo-console .add-provider-option {
width: auto;
min-height: 4.75rem;
min-height: 3rem;
border-color: var(--border);
border-radius: var(--radius-lg);
border-radius: var(--radius-md);
background: var(--card);
padding: 0.625rem 0.75rem;
margin-right: 0;
padding: 0.625rem;
}
.kilo-console .provider-option > div:not(.tags) > strong,
.kilo-console .provider-option .model-title strong {
color: var(--foreground);
font-size: 0.75rem;
font-weight: 400;
line-height: 1.45;
}
.kilo-console .provider-option > div:not(.tags) > span,
.kilo-console .provider-option .model-title span {
font-family: var(--font-family-mono);
font-size: 0.6875rem;
line-height: 1.45;
}
.kilo-console .add-provider-option.selected {
@@ -261,15 +275,17 @@
outline: 0;
}
.kilo-console .provider-option strong,
.kilo-console .provider-option span {
.kilo-console .provider-option > div:not(.tags) > strong,
.kilo-console .provider-option > div:not(.tags) > span,
.kilo-console .provider-option .model-title strong,
.kilo-console .provider-option .model-title span {
display: block;
}
.kilo-console .provider-option span {
.kilo-console .provider-option > div:not(.tags) > span,
.kilo-console .provider-option .model-title span {
overflow: hidden;
color: var(--text-weaker);
font-size: var(--font-size-small);
text-overflow: ellipsis;
white-space: nowrap;
}
@@ -287,10 +303,6 @@
font-weight: 400;
}
.kilo-console .add-provider-footer {
margin-top: auto;
}
.kilo-console .provider-form {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -5,7 +5,7 @@
}
.kilo-console .model-defaults {
grid-template-columns: 1fr;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.kilo-console .model-default-fields {
@@ -1,6 +1,7 @@
@media (max-width: 980px) {
.kilo-console .config-page-header,
.kilo-console .config-toolbar,
.kilo-console .models-filter-primary,
.kilo-console .details,
.kilo-console .model-controls,
.kilo-console .builder,
@@ -42,6 +43,7 @@
}
.kilo-console .compact,
.kilo-console .model-defaults,
.kilo-console .mini-list.columns,
.kilo-console .mini-item,
.kilo-console .mini-item.simple {
@@ -51,9 +53,30 @@
.kilo-console .metrics {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.kilo-console .models-filter-secondary {
align-items: stretch;
flex-direction: column;
}
.kilo-console .models-context-filter,
.kilo-console .models-capabilities-filter {
flex: 1 1 auto;
min-width: 0;
}
}
@media (max-width: 560px) {
.kilo-console .explore-models,
.kilo-console .model-info-grid {
grid-template-columns: 1fr;
}
.kilo-console .models-capabilities-filter {
align-items: flex-start;
flex-direction: column;
}
.kilo-console .metrics {
grid-template-columns: 1fr;
}