diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index e2af87f..2f74ea8 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -60,7 +60,7 @@ export { marketplaceEnv } from "./skill-market/ingest/env.js"; // search + the enumeration seam export { searchSkills, listUnlockable } from "./search/index.js"; export type { SearchFilters, SortBy, SearchOptions, UnlockableWorkflow, UnlockOptions } from "./search/index.js"; -export { dasSource, indexerSource, ownedSkills } from "./core/skillSource.js"; +export { dasSource, indexerSource, ownedSkills, workflowMintsAmong } from "./core/skillSource.js"; export type { SkillSource } from "./core/skillSource.js"; // reputation (derived live from supply + reviews) export { getReputation, getLeaderboard } from "./reputation/index.js"; diff --git a/surfaces/localhost/src/index.ts b/surfaces/localhost/src/index.ts index 7fbda02..8a8d525 100644 --- a/surfaces/localhost/src/index.ts +++ b/surfaces/localhost/src/index.ts @@ -68,6 +68,7 @@ import { maskedGithubToken, loadGithubToken, registerVerifiedWork, + workflowMintsAmong, } from "@iqlabs-official/agent-sdk"; const PORT = Number(process.env.AGENTNET_PORT ?? 4317); @@ -470,7 +471,13 @@ function attachMarketHandlers(c: Client) { ]); const names = cards.map((card) => card.name); const mints = Object.fromEntries(cards.map((card) => [card.name, card.id])); - c.send({ type: "ownedSkills", names, mints, disposedMints, cards }); + // Ground-truth which owned mints are workflows (not `card.type`: a mint missing from + // the indexer catalog falls back to type "skill" even when it's actually a workflow — + // see ownedSkillCards). The workflow-publish picker needs this to keep owned workflows + // out of the required-skills checklist (a workflow can't require another workflow). + const allMints = [...Object.values(mints), ...Object.values(disposedMints)]; + const workflowMints = allMints.length ? await workflowMintsAmong(allMints).catch(() => [] as string[]) : []; + c.send({ type: "ownedSkills", names, mints, disposedMints, cards, workflowMints }); } // `quiet` = expected transient (no wallet yet): answer reads with empty results and // skip every toast, so the market just shows clean empty states until the wallet lands. diff --git a/surfaces/webview/src/market/MarketScreen.tsx b/surfaces/webview/src/market/MarketScreen.tsx index 48e7484..384104b 100644 --- a/surfaces/webview/src/market/MarketScreen.tsx +++ b/surfaces/webview/src/market/MarketScreen.tsx @@ -131,11 +131,12 @@ export function MarketScreen({ tab }: { tab: ShellTab }) { ); } - // Publish form (market tab) + // Publish form (market tab) — opens pre-set to whichever kind you were browsing (skill + // or workflow tab), same as the VSCode builder. if (view === "publish") { return (
- setView("browse")} /> + setView("browse")} />
); } diff --git a/surfaces/webview/src/market/PublishForm.tsx b/surfaces/webview/src/market/PublishForm.tsx index 04767e9..f79f299 100644 --- a/surfaces/webview/src/market/PublishForm.tsx +++ b/surfaces/webview/src/market/PublishForm.tsx @@ -63,10 +63,18 @@ function PublishProgressView({ progress }: { progress: { phase: "store" | "mint" interface Props { onBack: () => void; + // Which kind the form opens to — e.g. hitting Publish from the Workflow browse tab should + // land straight in workflow mode, same as the VSCode builder. Defaults to "skill". + initialKind?: "skill" | "workflow"; } -export function PublishForm({ onBack }: Props) { +// A workflow can require at most 16 skills (MAX_REQUIRED_SKILLS in the agent-workflow-nft +// contract) and at least 1 — the on-chain gate that gives a workflow meaning. +const MAX_REQUIRED_SKILLS = 16; + +export function PublishForm({ onBack, initialKind = "skill" }: Props) { const { send, state, clearPublishResult } = useStore(); + const [kind, setKind] = useState<"skill" | "workflow">(initialKind); const [name, setName] = useState(""); const [description, setDescription] = useState(""); const [text, setText] = useState(""); @@ -75,27 +83,59 @@ export function PublishForm({ onBack }: Props) { const [priceSol, setPriceSol] = useState("0"); const [imageUrl, setImageUrl] = useState(""); // http/on-chain link only — no file upload const [submitting, setSubmitting] = useState(false); + // Required-skills picker (workflow mode only): mint -> selected. + const [reqSel, setReqSel] = useState>({}); + const t = PUBLISH_THEME[kind]; const result = state.publishResult; + // Skills the wallet actually owns and can require: must have a resolvable mint, and must + // NOT itself be a workflow (the on-chain gate rejects a workflow as a required_skill). + const ownedSkillNames = state.marketOwned.filter((n) => { + const mint = state.marketOwnedMints[n]; + return !!mint && !state.marketOwnedWorkflowMints.includes(mint); + }); + const chosenReqMints = Object.keys(reqSel).filter((m) => reqSel[m]); + // A result (success OR failure) ends the in-flight state so the form/button come back. useEffect(() => { if (result) setSubmitting(false); }, [result]); function handleSubmit() { - if (!name.trim() || !text.trim()) return; + if (!name.trim()) return; + if (kind === "skill" && !text.trim()) return; + if (kind === "workflow" && (chosenReqMints.length === 0 || chosenReqMints.length > MAX_REQUIRED_SKILLS)) return; setSubmitting(true); clearPublishResult(); + // Workflow mode: synthesize the SKILL.md frontmatter (type: workflow + requiredSkills) so + // the current backend's frontmatter sniff (env.ts) mints it as a workflow — same trick the + // VSCode webview builder uses. No SKILL.md body to author; the workflow IS its required skills. + const body = kind === "workflow" + ? [ + "---", + `name: ${name.trim()}`, + `description: ${description.trim().replace(/\s*\n\s*/g, " ")}`, + "type: workflow", + `requiredSkills: [${chosenReqMints.join(", ")}]`, + "---", + "", + `# ${name.trim()}`, + "", + description.trim(), + "", + ].join("\n") + : text.trim(); // Image is a link ONLY (http URL or on-chain address). The app can't attach/upload a // file, and a link stays tiny so the body doesn't chunk on-chain (keeps signatures low). send({ type: "publishSkill", name: name.trim(), description: description.trim(), - text: text.trim(), + text: body, category: category.trim() || undefined, hashtags: hashtags.split(",").map((h) => h.trim()).filter(Boolean), priceSol: priceSol || "0", image: imageUrl.trim() || undefined, + ...(kind === "workflow" ? { kind: "workflow" as const, requiredSkills: chosenReqMints } : {}), }); setTimeout(() => setSubmitting(false), 15000); } @@ -105,13 +145,13 @@ export function PublishForm({ onBack }: Props) { if (result?.ok) { return (
-
+
- Publish Skill + Publish {kind === "workflow" ? "Workflow" : "Skill"}
-
- -

Skill minted!

+
+ +

{kind === "workflow" ? "Workflow minted!" : "Skill minted!"}

{result.mint &&

{result.mint}

}
@@ -144,14 +184,33 @@ export function PublishForm({ onBack }: Props) { > - Publish Skill + Publish {kind === "workflow" ? "Workflow" : "Skill"}
+ {/* Skill/workflow toggle: a workflow is defined by the skills it requires (the + on-chain gate), so workflow mode swaps the SKILL.md box below for a checklist. */} +
+ {(["skill", "workflow"] as const).map((k) => { + const active = kind === k; + const kt = PUBLISH_THEME[k]; + return ( + + ); + })} +
setName(e.target.value)} /> @@ -159,20 +218,57 @@ export function PublishForm({ onBack }: Props) { setDescription(e.target.value)} /> - -