From 871e88828396b310bd89b06370d8ac19ada66bdc Mon Sep 17 00:00:00 2001 From: Marc Terns Date: Wed, 7 Jan 2026 21:21:32 -0600 Subject: [PATCH] UI: improve skill install feedback --- ui/src/ui/app-render.ts | 3 +++ ui/src/ui/app.ts | 2 ++ ui/src/ui/controllers/skills.ts | 45 ++++++++++++++++++++++++++++++--- ui/src/ui/views/skills.ts | 22 ++++++++++++++-- 4 files changed, 67 insertions(+), 5 deletions(-) diff --git a/ui/src/ui/app-render.ts b/ui/src/ui/app-render.ts index 9cc9e5b17..eadc0cc52 100644 --- a/ui/src/ui/app-render.ts +++ b/ui/src/ui/app-render.ts @@ -57,6 +57,7 @@ import { saveSkillApiKey, updateSkillEdit, updateSkillEnabled, + type SkillMessage, } from "./controllers/skills"; import { loadNodes } from "./controllers/nodes"; import { loadChatHistory } from "./controllers/chat"; @@ -162,6 +163,7 @@ export type AppViewState = { skillsError: string | null; skillsFilter: string; skillEdits: Record; + skillMessages: Record; skillsBusyKey: string | null; debugLoading: boolean; debugStatus: StatusSummary | null; @@ -377,6 +379,7 @@ export function renderApp(state: AppViewState) { error: state.skillsError, filter: state.skillsFilter, edits: state.skillEdits, + messages: state.skillMessages, busyKey: state.skillsBusyKey, onFilterChange: (next) => (state.skillsFilter = next), onRefresh: () => loadSkills(state), diff --git a/ui/src/ui/app.ts b/ui/src/ui/app.ts index 702cf69ee..1a13da3a4 100644 --- a/ui/src/ui/app.ts +++ b/ui/src/ui/app.ts @@ -75,6 +75,7 @@ import { } from "./controllers/cron"; import { loadSkills, + type SkillMessage, } from "./controllers/skills"; import { loadDebug } from "./controllers/debug"; @@ -332,6 +333,7 @@ export class ClawdbotApp extends LitElement { @state() skillsFilter = ""; @state() skillEdits: Record = {}; @state() skillsBusyKey: string | null = null; + @state() skillMessages: Record = {}; @state() debugLoading = false; @state() debugStatus: StatusSummary | null = null; diff --git a/ui/src/ui/controllers/skills.ts b/ui/src/ui/controllers/skills.ts index fe3329956..688c54fb4 100644 --- a/ui/src/ui/controllers/skills.ts +++ b/ui/src/ui/controllers/skills.ts @@ -9,8 +9,24 @@ export type SkillsState = { skillsError: string | null; skillsBusyKey: string | null; skillEdits: Record; + skillMessages: SkillMessageMap; }; +export type SkillMessage = { + kind: "success" | "error"; + message: string; +}; + +export type SkillMessageMap = Record; + +function setSkillMessage(state: SkillsState, key: string, message?: SkillMessage) { + if (!key.trim()) return; + const next = { ...state.skillMessages }; + if (message) next[key] = message; + else delete next[key]; + state.skillMessages = next; +} + export async function loadSkills(state: SkillsState) { if (!state.client || !state.connected) return; if (state.skillsLoading) return; @@ -47,8 +63,16 @@ export async function updateSkillEnabled( try { await state.client.request("skills.update", { skillKey, enabled }); await loadSkills(state); + setSkillMessage(state, skillKey, { + kind: "success", + message: enabled ? "Skill enabled" : "Skill disabled", + }); } catch (err) { state.skillsError = String(err); + setSkillMessage(state, skillKey, { + kind: "error", + message: String(err), + }); } finally { state.skillsBusyKey = null; } @@ -62,8 +86,16 @@ export async function saveSkillApiKey(state: SkillsState, skillKey: string) { const apiKey = state.skillEdits[skillKey] ?? ""; await state.client.request("skills.update", { skillKey, apiKey }); await loadSkills(state); + setSkillMessage(state, skillKey, { + kind: "success", + message: "API key saved", + }); } catch (err) { state.skillsError = String(err); + setSkillMessage(state, skillKey, { + kind: "error", + message: String(err), + }); } finally { state.skillsBusyKey = null; } @@ -78,16 +110,23 @@ export async function installSkill( state.skillsBusyKey = name; state.skillsError = null; try { - await state.client.request("skills.install", { + const result = (await state.client.request("skills.install", { name, installId, timeoutMs: 120000, - }); + })) as { ok?: boolean; message?: string }; await loadSkills(state); + setSkillMessage(state, name, { + kind: "success", + message: result?.message ?? "Installed", + }); } catch (err) { state.skillsError = String(err); + setSkillMessage(state, name, { + kind: "error", + message: String(err), + }); } finally { state.skillsBusyKey = null; } } - diff --git a/ui/src/ui/views/skills.ts b/ui/src/ui/views/skills.ts index 50861fee1..f10aae6dc 100644 --- a/ui/src/ui/views/skills.ts +++ b/ui/src/ui/views/skills.ts @@ -2,6 +2,7 @@ import { html, nothing } from "lit"; import { clampText } from "../format"; import type { SkillStatusEntry, SkillStatusReport } from "../types"; +import type { SkillMessageMap } from "../controllers/skills"; export type SkillsProps = { loading: boolean; @@ -10,6 +11,7 @@ export type SkillsProps = { filter: string; edits: Record; busyKey: string | null; + messages: SkillMessageMap; onFilterChange: (next: string) => void; onRefresh: () => void; onToggle: (skillKey: string, enabled: boolean) => void; @@ -73,6 +75,10 @@ export function renderSkills(props: SkillsProps) { function renderSkill(skill: SkillStatusEntry, props: SkillsProps) { const busy = props.busyKey === skill.skillKey || props.busyKey === skill.name; const apiKey = props.edits[skill.skillKey] ?? ""; + const message = + props.messages[skill.skillKey] ?? props.messages[skill.name] ?? null; + const canInstall = + skill.install.length > 0 && skill.missing.bins.length > 0; const missing = [ ...skill.missing.bins.map((b) => `bin:${b}`), ...skill.missing.env.map((e) => `env:${e}`), @@ -120,16 +126,28 @@ function renderSkill(skill: SkillStatusEntry, props: SkillsProps) { > ${skill.disabled ? "Enable" : "Disable"} - ${skill.install.length > 0 + ${canInstall ? html`` : nothing} + ${message + ? html`
+ ${message.message} +
` + : nothing} ${skill.primaryEnv ? html`