feat(scripts): add sidecar icons and tags in the admin UI

Serve optional PNG/JPG next to each script and show meta tags in lists, cards, and the editor.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-15 21:21:24 +07:00
co-authored by Cursor
parent bccbf551e3
commit cae308359a
15 changed files with 256 additions and 47 deletions
@@ -0,0 +1,30 @@
import { useEffect, useState } from "react";
import { TbFileTypeJs } from "react-icons/tb";
export function scriptIconSrc(name) {
return `/api/scripts/${encodeURIComponent(name)}/icon`;
}
/** Same-name PNG/JPG next to the script, or a JS document icon. */
export function ScriptIcon({ name, hasIcon, className = "size-10" }) {
const [failed, setFailed] = useState(false);
useEffect(() => {
setFailed(false);
}, [name]);
const showFile = Boolean(name) && hasIcon !== false && !failed;
if (!showFile) {
return <TbFileTypeJs className={`${className} text-warning`} aria-hidden />;
}
return (
<img
src={scriptIconSrc(name)}
alt=""
className={`${className} object-contain`}
onError={() => setFailed(true)}
/>
);
}
@@ -1,3 +1,6 @@
import { scriptTags } from "../lib/script.js";
import { TagBadge } from "./TagBadge.jsx";
function FieldTable({ title, fields }) {
const entries = Object.entries(fields ?? {});
if (entries.length === 0) return null;
@@ -56,9 +59,18 @@ export function ScriptMetaPanel({ meta, metaError, className = "" }) {
);
}
const tags = scriptTags(meta);
return (
<div className={`space-y-3 ${className}`}>
{meta.description ? <p className="text-sm">{meta.description}</p> : null}
{tags.length ? (
<div className="flex flex-wrap gap-1">
{tags.map((tag) => (
<TagBadge key={tag} tag={tag} />
))}
</div>
) : null}
<div className="grid gap-3 md:grid-cols-3">
<FieldTable title="Config" fields={meta.config} />
<FieldTable title="Input (data)" fields={meta.input} />
+17
View File
@@ -0,0 +1,17 @@
import { tagColor } from "../lib/script.js";
export function TagBadge({ tag, className = "", onClick, title }) {
const { bg, text } = tagColor(tag);
const Comp = onClick ? "button" : "span";
return (
<Comp
type={onClick ? "button" : undefined}
className={`badge badge-sm border-0 font-medium ${className}`}
style={{ backgroundColor: bg, color: text }}
title={title}
onClick={onClick}
>
{tag}
</Comp>
);
}
@@ -1,5 +1,7 @@
import { useMemo, useState } from "react";
import { useScripts } from "../../api/hooks.js";
import { ScriptIcon } from "../ScriptIcon.jsx";
import { scriptTags } from "../../lib/script.js";
export function AddScriptDialog({ open, onClose, onPick }) {
const { data: scripts = [], isLoading } = useScripts();
@@ -12,12 +14,15 @@ export function AddScriptDialog({ open, onClose, onPick }) {
description: typeof s === "string" ? "" : s.meta?.description ?? "",
meta: typeof s === "string" ? null : s.meta,
metaError: typeof s === "string" ? null : s.metaError,
hasIcon: typeof s === "string" ? undefined : s.hasIcon,
tags: typeof s === "string" ? [] : scriptTags(s.meta),
}));
if (!term) return list;
return list.filter(
(s) =>
s.name.toLowerCase().includes(term) ||
s.description.toLowerCase().includes(term),
s.description.toLowerCase().includes(term) ||
s.tags.some((t) => t.toLowerCase().includes(term)),
);
}, [scripts, q]);
@@ -61,9 +66,12 @@ export function AddScriptDialog({ open, onClose, onPick }) {
setQ("");
}}
>
<span className="font-mono">{s.name}</span>
<span className="text-xs opacity-70 line-clamp-2">
{s.metaError ? s.metaError : s.description || "No description"}
<ScriptIcon name={s.name} hasIcon={s.hasIcon} className="size-8 shrink-0" />
<span className="min-w-0">
<span className="font-mono">{s.name}</span>
<span className="text-xs opacity-70 line-clamp-2 block">
{s.metaError ? s.metaError : s.description || "No description"}
</span>
</span>
</button>
</li>
@@ -10,6 +10,7 @@ import { StatusBadge } from "../../lib/format.jsx";
import { contextFromMeta, prettyJson } from "../../lib/script.js";
import { ConfigFields } from "./ConfigFields.jsx";
import { ConfigTooltip, configValueText, FieldLabel, previewConfigValue, SchemaTooltip } from "./FieldHelp.jsx";
import { ScriptIcon } from "../ScriptIcon.jsx";
export function ScriptCard({
step,
@@ -66,6 +67,9 @@ export function ScriptCard({
>
<LuGripVertical className="size-4 opacity-60" />
</button>
{step.kind === "script" && step.script ? (
<ScriptIcon name={step.script} hasIcon={listed?.hasIcon} className="size-8 shrink-0" />
) : null}
<button
type="button"
className="min-w-0 flex-1 text-left"
@@ -76,7 +80,7 @@ export function ScriptCard({
<span className="min-w-0 truncate">{baseName}</span>
{!expanded && preview ? (
<span
className="badge badge-secondary badge-sm max-w-[min(16rem,45%)] shrink-0 truncate font-mono font-normal"
className="badge badge-secondary badge-sm shrink-0 truncate font-mono font-normal"
title={previewFull}
>
{preview}