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, saveSkillApiKey,
updateSkillEdit, updateSkillEdit,
updateSkillEnabled, updateSkillEnabled,
type SkillMessage,
} from "./controllers/skills"; } from "./controllers/skills";
import { loadNodes } from "./controllers/nodes"; import { loadNodes } from "./controllers/nodes";
import { loadChatHistory } from "./controllers/chat"; import { loadChatHistory } from "./controllers/chat";
@@ -166,6 +167,7 @@ export type AppViewState = {
skillsError: string | null; skillsError: string | null;
skillsFilter: string; skillsFilter: string;
skillEdits: Record<string, string>; skillEdits: Record<string, string>;
skillMessages: Record<string, SkillMessage>;
skillsBusyKey: string | null; skillsBusyKey: string | null;
debugLoading: boolean; debugLoading: boolean;
debugStatus: StatusSummary | null; debugStatus: StatusSummary | null;
@@ -391,6 +393,7 @@ export function renderApp(state: AppViewState) {
error: state.skillsError, error: state.skillsError,
filter: state.skillsFilter, filter: state.skillsFilter,
edits: state.skillEdits, edits: state.skillEdits,
messages: state.skillMessages,
busyKey: state.skillsBusyKey, busyKey: state.skillsBusyKey,
onFilterChange: (next) => (state.skillsFilter = next), onFilterChange: (next) => (state.skillsFilter = next),
onRefresh: () => loadSkills(state), onRefresh: () => loadSkills(state),

View File

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

View File

@@ -9,8 +9,24 @@ export type SkillsState = {
skillsError: string | null; skillsError: string | null;
skillsBusyKey: string | null; skillsBusyKey: string | null;
skillEdits: Record<string, string>; 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) { export async function loadSkills(state: SkillsState) {
if (!state.client || !state.connected) return; if (!state.client || !state.connected) return;
if (state.skillsLoading) return; if (state.skillsLoading) return;
@@ -47,8 +63,16 @@ export async function updateSkillEnabled(
try { try {
await state.client.request("skills.update", { skillKey, enabled }); await state.client.request("skills.update", { skillKey, enabled });
await loadSkills(state); await loadSkills(state);
setSkillMessage(state, skillKey, {
kind: "success",
message: enabled ? "Skill enabled" : "Skill disabled",
});
} catch (err) { } catch (err) {
state.skillsError = String(err); state.skillsError = String(err);
setSkillMessage(state, skillKey, {
kind: "error",
message: String(err),
});
} finally { } finally {
state.skillsBusyKey = null; state.skillsBusyKey = null;
} }
@@ -62,8 +86,16 @@ export async function saveSkillApiKey(state: SkillsState, skillKey: string) {
const apiKey = state.skillEdits[skillKey] ?? ""; const apiKey = state.skillEdits[skillKey] ?? "";
await state.client.request("skills.update", { skillKey, apiKey }); await state.client.request("skills.update", { skillKey, apiKey });
await loadSkills(state); await loadSkills(state);
setSkillMessage(state, skillKey, {
kind: "success",
message: "API key saved",
});
} catch (err) { } catch (err) {
state.skillsError = String(err); state.skillsError = String(err);
setSkillMessage(state, skillKey, {
kind: "error",
message: String(err),
});
} finally { } finally {
state.skillsBusyKey = null; state.skillsBusyKey = null;
} }
@@ -78,16 +110,23 @@ export async function installSkill(
state.skillsBusyKey = name; state.skillsBusyKey = name;
state.skillsError = null; state.skillsError = null;
try { try {
await state.client.request("skills.install", { const result = (await state.client.request("skills.install", {
name, name,
installId, installId,
timeoutMs: 120000, timeoutMs: 120000,
}); })) as { ok?: boolean; message?: string };
await loadSkills(state); await loadSkills(state);
setSkillMessage(state, name, {
kind: "success",
message: result?.message ?? "Installed",
});
} catch (err) { } catch (err) {
state.skillsError = String(err); state.skillsError = String(err);
setSkillMessage(state, name, {
kind: "error",
message: String(err),
});
} finally { } finally {
state.skillsBusyKey = null; state.skillsBusyKey = null;
} }
} }

View File

@@ -2,6 +2,7 @@ import { html, nothing } from "lit";
import { clampText } from "../format"; import { clampText } from "../format";
import type { SkillStatusEntry, SkillStatusReport } from "../types"; import type { SkillStatusEntry, SkillStatusReport } from "../types";
import type { SkillMessageMap } from "../controllers/skills";
export type SkillsProps = { export type SkillsProps = {
loading: boolean; loading: boolean;
@@ -10,6 +11,7 @@ export type SkillsProps = {
filter: string; filter: string;
edits: Record<string, string>; edits: Record<string, string>;
busyKey: string | null; busyKey: string | null;
messages: SkillMessageMap;
onFilterChange: (next: string) => void; onFilterChange: (next: string) => void;
onRefresh: () => void; onRefresh: () => void;
onToggle: (skillKey: string, enabled: boolean) => void; onToggle: (skillKey: string, enabled: boolean) => void;
@@ -73,6 +75,10 @@ export function renderSkills(props: SkillsProps) {
function renderSkill(skill: SkillStatusEntry, props: SkillsProps) { function renderSkill(skill: SkillStatusEntry, props: SkillsProps) {
const busy = props.busyKey === skill.skillKey || props.busyKey === skill.name; const busy = props.busyKey === skill.skillKey || props.busyKey === skill.name;
const apiKey = props.edits[skill.skillKey] ?? ""; 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 = [ const missing = [
...skill.missing.bins.map((b) => `bin:${b}`), ...skill.missing.bins.map((b) => `bin:${b}`),
...skill.missing.env.map((e) => `env:${e}`), ...skill.missing.env.map((e) => `env:${e}`),
@@ -120,16 +126,28 @@ function renderSkill(skill: SkillStatusEntry, props: SkillsProps) {
> >
${skill.disabled ? "Enable" : "Disable"} ${skill.disabled ? "Enable" : "Disable"}
</button> </button>
${skill.install.length > 0 ${canInstall
? html`<button ? html`<button
class="btn" class="btn"
?disabled=${busy} ?disabled=${busy}
@click=${() => props.onInstall(skill.name, skill.install[0].id)} @click=${() => props.onInstall(skill.name, skill.install[0].id)}
> >
${skill.install[0].label} ${busy ? "Installing…" : skill.install[0].label}
</button>` </button>`
: nothing} : nothing}
</div> </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 ${skill.primaryEnv
? html` ? html`
<div class="field" style="margin-top: 10px;"> <div class="field" style="margin-top: 10px;">