feat(web): add agent management UI and improve launcher integration (#1358)

* Improve the web launcher and gateway integration across backend and frontend.

- add runtime model availability checks for local and OAuth-backed models
- support launcher-driven gateway host overrides and websocket URL resolution
- add gateway log clearing and keep incremental log sync consistent after resets
- migrate session history APIs to JSONL metadata-backed storage with legacy fallback
- expose session titles and improve chat history loading and error handling
- move shared backend runtime helpers into the web utils package
- avoid blocking web startup when automatic onboard initialization fails
- add backend tests covering gateway readiness, host resolution, models, logs, and sessions

* feat(agent): add skills and tools management APIs and UI

- add backend APIs to list, view, import, and delete skills
- add tool status and toggle endpoints with dependency-aware config updates
- add agent skills/tools pages, routes, sidebar entries, and i18n strings
- add backend tests for the new skills and tools flows

* chore(frontend): upgrade shadcn to 4.0.5 and refresh lockfile

* chore(web): keep backend dist placeholder tracked
This commit is contained in:
wenjie
2026-03-11 18:37:00 +08:00
committed by GitHub
parent 8a398988d7
commit dea06c391c
45 changed files with 4266 additions and 327 deletions
+8
View File
@@ -14,6 +14,8 @@ interface GatewayStatusResponse {
interface GatewayActionResponse {
status: string
pid?: number
log_total?: number
log_run_id?: number
}
const BASE_URL = ""
@@ -59,4 +61,10 @@ export async function restartGateway(): Promise<GatewayActionResponse> {
})
}
export async function clearGatewayLogs(): Promise<GatewayActionResponse> {
return request<GatewayActionResponse>("/api/gateway/logs/clear", {
method: "POST",
})
}
export type { GatewayStatusResponse, GatewayActionResponse }
+1
View File
@@ -2,6 +2,7 @@
export interface SessionSummary {
id: string
title: string
preview: string
message_count: number
created: string
+79
View File
@@ -0,0 +1,79 @@
export interface SkillSupportItem {
name: string
path: string
source: "workspace" | "global" | "builtin" | string
description: string
}
export interface SkillDetailResponse extends SkillSupportItem {
content: string
}
interface SkillsResponse {
skills: SkillSupportItem[]
}
interface SkillActionResponse {
status?: string
name?: string
path?: string
source?: string
description?: string
}
async function request<T>(path: string, options?: RequestInit): Promise<T> {
const res = await fetch(path, options)
if (!res.ok) {
throw new Error(await extractErrorMessage(res))
}
return res.json() as Promise<T>
}
export async function getSkills(): Promise<SkillsResponse> {
return request<SkillsResponse>("/api/skills")
}
export async function getSkill(name: string): Promise<SkillDetailResponse> {
return request<SkillDetailResponse>(`/api/skills/${encodeURIComponent(name)}`)
}
export async function importSkill(file: File): Promise<SkillActionResponse> {
const formData = new FormData()
formData.set("file", file)
const res = await fetch("/api/skills/import", {
method: "POST",
body: formData,
})
if (!res.ok) {
throw new Error(await extractErrorMessage(res))
}
return res.json() as Promise<SkillActionResponse>
}
export async function deleteSkill(name: string): Promise<SkillActionResponse> {
return request<SkillActionResponse>(
`/api/skills/${encodeURIComponent(name)}`,
{
method: "DELETE",
},
)
}
async function extractErrorMessage(res: Response): Promise<string> {
try {
const body = (await res.json()) as {
error?: string
errors?: string[]
}
if (Array.isArray(body.errors) && body.errors.length > 0) {
return body.errors.join("; ")
}
if (typeof body.error === "string" && body.error.trim() !== "") {
return body.error
}
} catch {
// ignore invalid body
}
return `API error: ${res.status} ${res.statusText}`
}
+56
View File
@@ -0,0 +1,56 @@
export interface ToolSupportItem {
name: string
description: string
category: string
config_key: string
status: "enabled" | "disabled" | "blocked"
reason_code?: string
}
interface ToolsResponse {
tools: ToolSupportItem[]
}
interface ToolActionResponse {
status: string
}
async function request<T>(path: string, options?: RequestInit): Promise<T> {
const res = await fetch(path, options)
if (!res.ok) {
let message = `API error: ${res.status} ${res.statusText}`
try {
const body = (await res.json()) as {
error?: string
errors?: string[]
}
if (Array.isArray(body.errors) && body.errors.length > 0) {
message = body.errors.join("; ")
} else if (typeof body.error === "string" && body.error.trim() !== "") {
message = body.error
}
} catch {
// ignore invalid body
}
throw new Error(message)
}
return res.json() as Promise<T>
}
export async function getTools(): Promise<ToolsResponse> {
return request<ToolsResponse>("/api/tools")
}
export async function setToolEnabled(
name: string,
enabled: boolean,
): Promise<ToolActionResponse> {
return request<ToolActionResponse>(
`/api/tools/${encodeURIComponent(name)}/state`,
{
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ enabled }),
},
)
}