refactor: project filter / active projects on sidebar

This commit is contained in:
Catriel Müller
2026-05-26 10:56:44 -03:00
parent 3648e2e132
commit ce80806bfa
12 changed files with 417 additions and 189 deletions
+68 -24
View File
@@ -6,7 +6,6 @@ import type {
AppAgentsResponse,
ConfigOverlayResponse,
ConfigModelStateResponse,
ConfigRulesResponse,
ConfigSourcesResponse,
FormatterStatusResponse,
GlobalHealthResponse,
@@ -17,6 +16,7 @@ import type {
ProviderAuthResponse,
ProviderListResponse,
ToolIdsResponse,
ToolListResponse,
TuiConfigGetResponse,
} from "@kilocode/sdk/v2/client"
@@ -31,18 +31,21 @@ export type Query = {
export type ProjectQuery = Pick<Query, "url" | "dir">
export type ProjectItem = KiloProject
export type RecentProjectItem = ProjectItem & {
sessions: number
}
export type Snapshot = {
health: GlobalHealthResponse
effective: EffectiveConfig
overlay: ConfigOverlayResponse
sources: ConfigSourcesResponse
rules?: ConfigRulesResponse
modelState: ConfigModelStateResponse
providers: ProviderListResponse
authMethods: ProviderAuthResponse
tui: TuiConfigGetResponse
tools: ToolIdsResponse
toolDetails: ToolListResponse
mcp: McpStatusResponse
lsp: LspStatusResponse
formatter: FormatterStatusResponse
@@ -75,6 +78,8 @@ type Result<T> = {
}
const ports = Array.from({ length: 20 }, (_, index) => 4097 + index)
const day = 24 * 60 * 60 * 1000
const hidden = new Set(["global"])
const key = "kilo.config.server"
const auth = `Basic ${btoa("kilo:kilo")}`
@@ -110,6 +115,28 @@ function demand<T>(label: string, result: Result<T>) {
return result.data
}
async function maybe<T>(label: string, result: Promise<Result<T>>) {
return await result
.then((value) => {
if (value.error) {
console.warn(`${label}: ${message(value.error)}`)
return undefined
}
return value.data
})
.catch((err) => {
console.warn(`${label}: ${message(err)}`)
return undefined
})
}
function model(input: unknown) {
if (typeof input !== "string") return undefined
const index = input.indexOf("/")
if (index <= 0 || index >= input.length - 1) return undefined
return { provider: input.slice(0, index), model: input.slice(index + 1) }
}
async function probe(url: string) {
const ctl = new AbortController()
const timer = window.setTimeout(() => ctl.abort(), 400)
@@ -150,34 +177,34 @@ export async function discover() {
export async function load(input: Query): Promise<Snapshot> {
const sdk = client(input)
const [health, overlay, modelState, providers, authMethods, tui, tools, mcp, lsp, formatter, agents, rules] =
await Promise.all([
sdk.global.health(),
sdk.config.overlay({ scope: input.scope }),
sdk.config.modelState(),
sdk.provider.list(),
sdk.provider.auth(),
sdk.tui.config.get(),
sdk.tool.ids(),
sdk.mcp.status(),
sdk.lsp.status(),
sdk.formatter.status(),
sdk.app.agents(),
input.scope === "project" ? sdk.config.rules() : Promise.resolve({ data: undefined }),
])
const [health, overlay, modelState, providers, authMethods, tui, tools, mcp, lsp, formatter, agents] = await Promise.all([
sdk.global.health(),
sdk.config.overlay({ scope: input.scope }),
sdk.config.modelState(),
sdk.provider.list(),
sdk.provider.auth(),
sdk.tui.config.get(),
sdk.tool.ids(),
sdk.mcp.status(),
sdk.lsp.status(),
sdk.formatter.status(),
sdk.app.agents(),
])
const resolved = demand("Config overlay", overlay)
const ref = model(resolved.effective.model)
const info = ref ? await maybe("Tool metadata", sdk.tool.list(ref)) : undefined
return {
health: demand("Health", health),
effective: resolved.effective,
overlay: resolved,
sources: { sources: resolved.sources },
rules: input.scope === "project" ? demand("Rules", rules) : undefined,
modelState: demand("Model state", modelState),
providers: demand("Providers", providers),
authMethods: demand("Provider auth methods", authMethods),
tui: demand("TUI config", tui),
tools: demand("Tools", tools),
toolDetails: info ?? [],
mcp: demand("MCP status", mcp),
lsp: demand("LSP status", lsp),
formatter: demand("Formatter status", formatter),
@@ -192,6 +219,29 @@ export async function loadProjects(input: ProjectQuery): Promise<ProjectItem[]>
return demand("Projects", result)
}
export async function loadVisibleProjects(input: ProjectQuery): Promise<ProjectItem[]> {
const items = await loadProjects(input)
return items.filter((item) => !hidden.has(item.id))
}
export async function loadRecentProjects(input: ProjectQuery): Promise<RecentProjectItem[]> {
const sdk = client(input)
const [items, sessions] = await Promise.all([
loadVisibleProjects(input),
sdk.experimental.session.list({ archived: false, start: Date.now() - day, limit: 500 }),
])
const rows = demand("Recent sessions", sessions)
const counts = new Map<string, number>()
for (const row of rows) {
counts.set(row.projectID, (counts.get(row.projectID) ?? 0) + 1)
}
return items
.map((item) => ({ ...item, sessions: counts.get(item.id) ?? 0 }))
.filter((item) => item.sessions > 0)
}
export async function saveConfig(input: Query, patch: Partial<ConfigPatch>) {
const sdk = client(input)
const result = await sdk.config.overlayUpdate({ scope: input.scope, set: patch })
@@ -204,12 +254,6 @@ export async function unsetConfig(input: Query, unset: ConfigUnset) {
return demand("Update config", result)
}
export async function saveRules(input: Query, content: string) {
const sdk = client(input)
const result = await sdk.config.rulesUpdate({ content })
return demand("Update rules", result)
}
export async function saveModelState(input: Query, favorite: ModelRef[]) {
const sdk = client(input)
const result = await sdk.config.modelStateUpdate({ favorite })
@@ -1,7 +1,62 @@
import { A, useLocation } from "@solidjs/router"
import { For } from "solid-js"
import { createEffect, createMemo, createResource, createSignal, For, onCleanup } from "solid-js"
import { IconButton } from "@kilocode/kilo-ui/icon-button"
import { projects, type Path } from "../../shared/navigation"
import {
discover,
forgetCached,
healthy,
loadRecentProjects,
loadCached,
saveCached,
type RecentProjectItem,
type ProjectQuery,
} from "../../client"
import { type Path } from "../../shared/navigation"
import { clean, friendly } from "../../shared/utils"
const ports = new Set(["3017", "3018"])
function shouldDiscover(input: URLSearchParams) {
if (input.get("server")) return false
return ports.has(window.location.port)
}
function base(input: URLSearchParams) {
const param = input.get("server")
if (param) return param
const cached = shouldDiscover(input) ? loadCached() : ""
if (cached) return cached
if (shouldDiscover(input)) return ""
return window.location.origin
}
function repo(input: string) {
const parts = input.split(/[\\/]/).filter(Boolean)
return parts.at(-1) ?? "Global"
}
function name(item: RecentProjectItem) {
return friendly(item.name?.trim() || repo(item.worktree) || item.id.slice(0, 8))
}
function mark(item: RecentProjectItem) {
const parts = name(item)
.split(/[\s._/-]+/)
.filter(Boolean)
const text = `${parts[0]?.[0] ?? ""}${parts[1]?.[0] ?? parts[0]?.[1] ?? ""}`
return text.toUpperCase() || "KG"
}
function tail(input: URLSearchParams) {
const next = new URLSearchParams(input)
next.delete("directory")
const query = next.toString()
return query ? `?${query}` : ""
}
function href(item: RecentProjectItem, input: URLSearchParams) {
return `/projects/${encodeURIComponent(item.id)}/settings${tail(input)}`
}
type Props = {
path: Path
@@ -9,28 +64,77 @@ type Props = {
export function AppSidebar(props: Props) {
const loc = useLocation()
const search = () => {
const params = new URLSearchParams(loc.search)
params.delete("directory")
const query = params.toString()
return query ? `?${query}` : ""
}
const params = createMemo(() => new URLSearchParams(loc.search))
const discoverable = () => shouldDiscover(params())
const fallback = () => base(params())
const [url, setUrl] = createSignal(fallback())
const query = createMemo<ProjectQuery | undefined>(() => {
const target = clean(url()) || fallback()
if (!target) return undefined
return { url: target, dir: "" }
})
const [items, { refetch }] = createResource(query, loadRecentProjects)
const settings = () => {
const suffix = search()
return `/settings${suffix}`
return `/settings${tail(params())}`
}
createEffect(() => {
const next = params().get("server")
if (next && next !== url()) setUrl(next)
})
createEffect(() => {
if (!discoverable()) return
const cached = loadCached()
void Promise.resolve(cached ? healthy(cached) : false)
.then((ok) => {
if (ok) return cached
forgetCached()
return discover()
})
.then((value) => {
if (!value) return
saveCached(value)
setUrl(value)
})
})
createEffect(() => {
const current = query()
if (!items() || !current || !discoverable()) return
saveCached(current.url)
})
createEffect(() => {
if (!items.error || !discoverable()) return
const cached = loadCached()
if (!cached || cached !== url()) return
forgetCached()
setUrl("")
void discover().then((value) => {
if (!value) return
saveCached(value)
setUrl(value)
})
})
createEffect(() => {
if (!query()) return
const timer = window.setInterval(() => void refetch(), 5000)
onCleanup(() => window.clearInterval(timer))
})
return (
<aside class="rail" aria-label="Primary navigation">
<A class="rail-action" classList={{ active: props.path === "/projects" }} href="/projects" aria-label="Projects">
<IconButton icon="folder" variant="ghost" tabindex={-1} />
</A>
<div class="rail-favorites" aria-label="Favorite projects">
<For each={projects}>
<div class="rail-favorites" aria-label="Projects with recent sessions">
<For each={items() ?? []}>
{(item) => (
<A class="favorite-project" href={item.path} aria-label={item.name} title={item.name}>
{item.label}
<A class="favorite-project" href={href(item, params())} aria-label={name(item)} title={name(item)}>
{mark(item)}
</A>
)}
</For>
@@ -9,7 +9,6 @@ import {
loadProjects,
saveCached,
saveConfig,
saveRules,
saveTui,
unsetConfig,
type ConfigPatch,
@@ -143,10 +142,6 @@ export function ConfigProvider(props: { children?: JSX.Element }) {
run("Saving config", () => unsetConfig(target(), paths))
}
function rules(content: string) {
run("Saving rules", () => saveRules(target(), content))
}
function tui(patch: TuiPatch) {
run("Saving TUI config", () => saveTui(target(), patch))
}
@@ -161,7 +156,6 @@ export function ConfigProvider(props: { children?: JSX.Element }) {
run,
save,
unset,
rules,
tui,
}
@@ -17,7 +17,6 @@ export type Ctx = {
run: (label: string, job: () => Promise<unknown>, task?: Task) => void
save: (patch: Partial<ConfigPatch>) => void
unset: (paths: ConfigUnset) => void
rules: (content: string) => void
tui: (patch: TuiPatch) => void
}
@@ -116,44 +116,43 @@ export function ModelsDefaultRoute() {
const field = () => state.snap()?.overlay.fields[item.key]
const model = () => state.item(field()?.value)
return (
<article class="resolved-card" classList={{ inherited: field()?.inherited }}>
<div>
<article class="resolved-card default-model-card" classList={{ inherited: field()?.inherited }}>
<header class="default-model-header">
<span>{item.label}</span>
<div class="tags default-model-actions">
<SourceBadge
source={field()?.source}
inherited={field()?.inherited}
overridden={field()?.overridden}
/>
<Show when={state.ctx.query()?.scope === "project" && field()?.overridden}>
<Button
variant="secondary"
disabled={Boolean(state.ctx.saving())}
onClick={() => state.ctx.unset([[item.key]])}
>
Revert
</Button>
</Show>
<IconButton
icon="edit"
variant="secondary"
aria-label={`Edit ${item.label}`}
disabled={Boolean(state.ctx.saving())}
onClick={() => state.edit(item.key)}
/>
</div>
</header>
<div class="default-model-value">
<Show when={model()} fallback={<strong>{text(field()?.value)}</strong>}>
{(selected) => (
<>
<strong>{selected().model.name}</strong>
<div class="default-model-meta">
<Tag>{selected().provider.name}</Tag>
<span>{selected().id}</span>
</div>
<strong>{`${selected().provider.name} / ${selected().model.name}`}</strong>
<span class="default-model-id">{selected().id}</span>
</>
)}
</Show>
</div>
<div class="tags">
<SourceBadge
source={field()?.source}
inherited={field()?.inherited}
overridden={field()?.overridden}
/>
<Show when={state.ctx.query()?.scope === "project" && field()?.overridden}>
<Button
variant="secondary"
disabled={Boolean(state.ctx.saving())}
onClick={() => state.ctx.unset([[item.key]])}
>
Revert
</Button>
</Show>
<IconButton
icon="edit"
variant="secondary"
aria-label={`Edit ${item.label}`}
disabled={Boolean(state.ctx.saving())}
onClick={() => state.edit(item.key)}
/>
</div>
</article>
)
}}
@@ -1,71 +0,0 @@
import { createEffect, createMemo, createSignal, For, Show } from "solid-js"
import { Button } from "@kilocode/kilo-ui/button"
import { Card } from "@kilocode/kilo-ui/card"
import { Tag } from "@kilocode/kilo-ui/tag"
import { useConfig } from "../../context/config"
import { ConfigPage, ConfigToolbar } from "./ConfigPage"
export function RulesRoute() {
const ctx = useConfig()
const [text, setText] = createSignal("")
const [dirty, setDirty] = createSignal(false)
const rules = createMemo(() => ctx.data()?.rules)
const target = createMemo(() => rules()?.files.find((file) => file.name === "AGENTS.md"))
createEffect(() => {
if (dirty()) return
setText(target()?.content ?? "")
})
function update(value: string) {
setDirty(true)
setText(value)
}
function save() {
ctx.rules(text())
setDirty(false)
}
return (
<ConfigPage title="Project Rules" actions={<Tag>{rules()?.files.filter((file) => file.exists).length ?? 0}</Tag>}>
<ConfigToolbar
title="AGENTS.md"
description="Project rules are stored in the repository and shared with every agent in this workspace."
meta={<Tag>{target()?.exists ? "local" : "new file"}</Tag>}
/>
<Show
when={ctx.query()?.scope === "project"}
fallback={<Card class="empty">Rules are available only in project settings.</Card>}
>
<section class="rules-editor">
<label>
<span>Editable rules file</span>
<textarea value={text()} spellcheck={false} onInput={(event) => update(event.currentTarget.value)} />
</label>
<div class="builder-actions">
<Button variant="primary" disabled={Boolean(ctx.saving())} onClick={save}>
Save Rules
</Button>
</div>
</section>
<div class="mini-list">
<For each={rules()?.files ?? []}>
{(file) => (
<article class="mini-item simple" classList={{ inherited: !file.editable }}>
<strong>{file.name}</strong>
<span>{file.path}</span>
<div class="tags">
<Tag>{file.exists ? "present" : "missing"}</Tag>
<Tag>{file.editable ? "editable" : "read only"}</Tag>
</div>
</article>
)}
</For>
</div>
</Show>
</ConfigPage>
)
}
@@ -1,11 +1,106 @@
import { For, Show } from "solid-js"
import { createMemo, For, Show } from "solid-js"
import { Tag } from "@kilocode/kilo-ui/tag"
import type { ToolListItem } from "@kilocode/sdk/v2/client"
import { useConfig } from "../../context/config"
import { ConfigPage, ConfigToolbar } from "./ConfigPage"
const labels: Record<string, string> = {
apply_patch: "Apply Patch",
bash: "Bash",
edit: "Edit File",
glob: "Find Files",
grep: "Search Content",
lsp: "Language Server",
question: "Ask User",
read: "Read File",
recall: "Local Recall",
skill: "Load Skill",
task: "Subagent Task",
todowrite: "Todo List",
webfetch: "Fetch URL",
websearch: "Web Search",
write: "Write File",
}
const copy: Record<string, string[]> = {
apply_patch: ["Apply structured patches to add, update, move, or delete files.", "Keeps edits explicit and easy to review."],
bash: ["Run shell commands in the current workspace.", "Supports timeouts, working directories, and command descriptions."],
edit: ["Replace exact text inside an existing file.", "Can update one match or all matches when needed."],
glob: ["Find files by path patterns.", "Useful for locating files before reading or editing them."],
grep: ["Search file contents with regular expressions.", "Can limit results to a file pattern such as TypeScript or CSS files."],
lsp: ["Use language-server features for code navigation.", "Supports operations such as definitions, references, and diagnostics."],
question: ["Ask the user for a decision during execution.", "Supports single-choice and multi-choice prompts."],
read: ["Read files, directories, images, and PDFs from the workspace.", "Supports line offsets and limits for large files."],
recall: ["Search and read previous project conversations.", "Useful for recovering prior decisions or implementation context."],
skill: ["Load task-specific instructions and workflows.", "Applies specialized repo guidance when a matching skill exists."],
task: ["Delegate focused exploration or implementation work to a subagent.", "Useful for parallel codebase research and complex subtasks."],
todowrite: ["Track multi-step implementation work.", "Shows progress with pending, in-progress, and completed states."],
webfetch: ["Fetch and convert web pages for analysis.", "Supports markdown, text, and HTML output formats."],
websearch: ["Search the web for external information.", "Supports fast, automatic, and deep search modes."],
write: ["Create or overwrite files with complete contents.", "Best for new files or full-file replacements."],
}
const acronyms = new Set(["api", "html", "id", "json", "lsp", "mcp", "pdf", "url"])
type Item = { id: string; detail: ToolListItem | undefined }
function word(input: string) {
const lower = input.toLowerCase()
if (acronyms.has(lower)) return lower.toUpperCase()
return input.charAt(0).toUpperCase() + input.slice(1)
}
function name(id: string) {
const known = labels[id]
if (known) return known
const clean = id
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
.replace(/[._:/-]+/g, " ")
.replace(/\s+/g, " ")
.trim()
if (!clean) return id
return clean.split(" ").map(word).join(" ")
}
function clip(input: string) {
if (input.length <= 180) return input
return `${input.slice(0, 177)}...`
}
function summary(input: string) {
const text = input
.replace(/```[\s\S]*?```/g, " ")
.replace(/[*`#>_-]+/g, " ")
.replace(/\s+/g, " ")
.trim()
const parts = text
.split(/(?:\.|\?|!)\s+/)
.map((part) => part.trim())
.filter(Boolean)
.slice(0, 2)
if (parts.length) return parts.map(clip)
return text ? [clip(text)] : []
}
function capabilities(item: Item) {
const known = copy[item.id]
if (known) return known
const desc = item.detail?.description.trim()
if (desc) return summary(desc)
return ["No capability description is available for this tool."]
}
export function ToolsRoute() {
const ctx = useConfig()
const snap = () => ctx.data()
const rows = createMemo(() => {
const data = snap()
if (!data) return []
const details = new Map(data.toolDetails.map((item) => [item.id, item]))
return data.tools
.map((id) => ({ id, detail: details.get(id) }))
.sort((a, b) => name(a.id).localeCompare(name(b.id)))
})
return (
<Show when={snap()}>
@@ -16,8 +111,26 @@ export function ToolsRoute() {
description="All registered tools and MCP server connection status."
/>
<div class="tag-cloud">
<For each={data().tools}>{(tool) => <Tag>{tool}</Tag>}</For>
<div class="tools">
<Show when={rows().length} fallback={<p class="empty">No tools registered.</p>}>
<For each={rows()}>
{(tool) => (
<article class="model tool-card">
<div class="model-main tool-main">
<div class="model-title">
<div>
<strong>{name(tool.id)}</strong>
<span>{tool.id}</span>
</div>
</div>
</div>
<ul class="tool-capabilities">
<For each={capabilities(tool)}>{(cap) => <li>{cap}</li>}</For>
</ul>
</article>
)}
</For>
</Show>
</div>
<Show when={Object.keys(data().mcp).length}>
@@ -9,7 +9,6 @@ import { ModelsAvailableRoute, ModelsDefaultRoute, ModelsRoute } from "./ModelsR
import { OverviewRoute } from "./OverviewRoute"
import { PermissionsRoute } from "./PermissionsRoute"
import { ProvidersRoute } from "./ProvidersRoute"
import { RulesRoute } from "./RulesRoute"
import { ServersRoute } from "./ServersRoute"
import { SourcesRoute } from "./SourcesRoute"
import { ToolsRoute } from "./ToolsRoute"
@@ -47,7 +46,6 @@ const permissions = {
label: "Permissions",
component: PermissionsRoute,
}
const rules = { path: "/rules", href: "/settings/rules", icon: "archive", label: "Rules", component: RulesRoute }
export const configNav: ConfigNode[] = [
{
@@ -55,7 +53,6 @@ export const configNav: ConfigNode[] = [
label: "General",
items: [
{ path: "/", href: "/settings", icon: "home", label: "Overview", component: OverviewRoute },
{ path: "/servers", href: "/settings/servers", icon: "server", label: "Servers", component: ServersRoute },
],
},
providers,
@@ -83,7 +80,6 @@ export const configNav: ConfigNode[] = [
tools,
mcp,
permissions,
rules,
{
id: "cli",
label: "CLI",
@@ -103,6 +99,7 @@ export const configNav: ConfigNode[] = [
id: "advanced",
label: "Advanced",
items: [
{ path: "/servers", href: "/settings/servers", icon: "server", label: "Servers", component: ServersRoute },
{ path: "/sources", href: "/settings/sources", icon: "archive", label: "Sources", component: SourcesRoute },
],
},
@@ -6,12 +6,12 @@ import {
forgetCached,
healthy,
loadCached,
loadProjects,
loadVisibleProjects,
saveCached,
type ProjectItem,
type ProjectQuery,
} from "../../client"
import { clean, errMsg } from "../../shared/utils"
import { clean, errMsg, friendly } from "../../shared/utils"
const search = new URLSearchParams(window.location.search)
const ui = new Set(["3017", "3018"])
@@ -36,7 +36,7 @@ function repo(input: string) {
}
function name(item: ProjectItem) {
return item.name?.trim() || repo(item.worktree) || item.id.slice(0, 8)
return friendly(item.name?.trim() || repo(item.worktree) || item.id.slice(0, 8))
}
function mark(item: ProjectItem) {
@@ -78,7 +78,7 @@ export function ProjectsRoute() {
if (!target) return undefined
return { url: target, dir: "" }
})
const [items] = createResource(query, loadProjects)
const [items] = createResource(query, loadVisibleProjects)
const rows = createMemo(() => [...(items() ?? [])].sort((a, b) => b.time.updated - a.time.updated))
createEffect(() => {
@@ -1,18 +1,5 @@
export type Path = "/projects" | "/profile" | "/settings"
export type Project = {
id: string
name: string
label: string
path: string
}
export const projects: Project[] = [
{ id: "cli", name: "Kilo CLI", label: "KC", path: "/projects" },
{ id: "vscode", name: "VS Code", label: "VS", path: "/projects" },
{ id: "gateway", name: "Gateway", label: "GW", path: "/projects" },
]
export function path(input: string): Path {
if (input === "/profile") return "/profile"
if (input.startsWith("/settings") || input.startsWith("/config") || input.includes("/settings")) return "/settings"
@@ -28,6 +28,18 @@ export function words(input: string) {
.filter(Boolean)
}
export function friendly(input: string) {
return input
.trim()
.split(/[\s._/-]+/)
.filter(Boolean)
.map((part) => {
if (/^[A-Z0-9]+$/.test(part) && part.length <= 4) return part
return `${part[0]?.toUpperCase() ?? ""}${part.slice(1).toLowerCase()}`
})
.join(" ")
}
export function csv(input: string) {
return input
.split(",")
+69 -19
View File
@@ -45,8 +45,7 @@
min-width: 0;
}
.resolved-card span,
.rules-editor span {
.resolved-card span {
color: var(--text-weaker);
font-size: var(--font-size-small);
font-weight: 700;
@@ -61,15 +60,43 @@
white-space: nowrap;
}
.default-model-meta {
display: flex;
gap: 0.5rem;
align-items: center;
flex-wrap: wrap;
.default-model-card {
display: grid;
align-items: stretch;
gap: 1rem;
grid-template-columns: minmax(0, 1fr);
justify-content: stretch;
}
.default-model-meta span {
.default-model-header {
display: flex;
gap: 1rem;
align-items: center;
justify-content: space-between;
min-width: 0;
width: 100%;
}
.default-model-actions {
flex: 0 0 auto;
align-items: center;
}
.default-model-value {
display: grid;
gap: 0.35rem;
min-width: 0;
}
.default-model-value strong {
font-family: var(--font-family-sans, Inter, system-ui, sans-serif);
}
.default-model-value .default-model-id {
overflow: hidden;
color: var(--text-weaker);
font-family: var(--font-family-mono, monospace);
font-size: var(--font-size-small);
font-weight: 600;
text-overflow: ellipsis;
text-transform: none;
@@ -80,16 +107,6 @@
opacity: 0.72;
}
.rules-editor {
display: grid;
gap: 1rem;
}
.rules-editor label {
display: grid;
gap: 0.5rem;
}
* {
box-sizing: border-box;
}
@@ -1028,7 +1045,8 @@ textarea:focus {
}
.providers,
.models {
.models,
.tools {
display: grid;
gap: 0.5rem;
}
@@ -1461,6 +1479,38 @@ select.invalid {
line-height: 1.45;
}
.tool-card .model-title {
padding-left: 0;
}
.tool-capabilities {
display: grid;
gap: 0.35rem;
max-width: 72rem;
margin: 0;
color: var(--text-weak);
font-size: var(--font-size-small);
line-height: 1.45;
list-style: none;
padding: 0;
}
.tool-capabilities li {
position: relative;
padding-left: 1rem;
}
.tool-capabilities li::before {
position: absolute;
top: 0.65em;
left: 0.15rem;
width: 0.3rem;
height: 0.3rem;
border-radius: 999px;
background: var(--text-weaker);
content: "";
}
.model-info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));