UI: improve skill install feedback

This commit is contained in:
Marc Terns
2026-01-07 21:21:32 -06:00
committed by Peter Steinberger
parent 04e0e10411
commit 965615a46c
4 changed files with 67 additions and 5 deletions

View File

@@ -60,6 +60,7 @@ import {
saveSkillApiKey,
updateSkillEdit,
updateSkillEnabled,
type SkillMessage,
} from "./controllers/skills";
import { loadNodes } from "./controllers/nodes";
import { loadChatHistory } from "./controllers/chat";
@@ -166,6 +167,7 @@ export type AppViewState = {
skillsError: string | null;
skillsFilter: string;
skillEdits: Record<string, string>;
skillMessages: Record<string, SkillMessage>;
skillsBusyKey: string | null;
debugLoading: boolean;
debugStatus: StatusSummary | null;
@@ -391,6 +393,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),

View File

@@ -78,6 +78,7 @@ import {
} from "./controllers/cron";
import {
loadSkills,
type SkillMessage,
} from "./controllers/skills";
import { loadDebug } from "./controllers/debug";
import { loadLogs } from "./controllers/logs";
@@ -356,6 +357,7 @@ export class ClawdbotApp extends LitElement {
@state() skillsFilter = "";
@state() skillEdits: Record<string, string> = {};
@state() skillsBusyKey: string | null = null;
@state() skillMessages: Record<string, SkillMessage> = {};
@state() debugLoading = false;
@state() debugStatus: StatusSummary | null = null;

View File

@@ -9,8 +9,24 @@ export type SkillsState = {
skillsError: string | null;
skillsBusyKey: string | null;
skillEdits: Record<string, string>;
skillMessages: SkillMessageMap;
};
export type SkillMessage = {
kind: "success" | "error";
message: string;
};
export type SkillMessageMap = Record<string, SkillMessage>;
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;
}
}

View File

@@ -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<string, string>;
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"}
</button>
${skill.install.length > 0
${canInstall
? html`<button
class="btn"
?disabled=${busy}
@click=${() => props.onInstall(skill.name, skill.install[0].id)}
>
${skill.install[0].label}
${busy ? "Installing…" : skill.install[0].label}
</button>`
: nothing}
</div>
${message
? html`<div
class="muted"
style="margin-top: 8px; color: ${
message.kind === "error"
? "var(--danger-color, #d14343)"
: "var(--success-color, #0a7f5a)"
};"
>
${message.message}
</div>`
: nothing}
${skill.primaryEnv
? html`
<div class="field" style="margin-top: 10px;">