mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-24 16:02:55 +08:00
refactor: project filter / active projects on sidebar
This commit is contained in:
@@ -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(",")
|
||||
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user