diff --git a/packages/core/src/chat/marketMessages.ts b/packages/core/src/chat/marketMessages.ts index ff08ac7..d04527a 100644 --- a/packages/core/src/chat/marketMessages.ts +++ b/packages/core/src/chat/marketMessages.ts @@ -10,15 +10,15 @@ // // Direction is in the name: *Request = UI -> host; *Event = host -> UI. -import type { Note, Reputation } from "../core/types.js"; +import type { MarketItemType, Note, PluginEngine, PluginManifest, Reputation } from "../core/types.js"; /** One item row as the UI renders it (cards + detail). Mirrors the subset of `Skill` - * the UI needs — kept here so host (env callbacks) and UI agree. Covers both kinds: - * `type` splits the Skills / Workflows tabs; `requiredSkills` (workflows only) are - * the prerequisite skill mint ids the detail view renders as clickable links. */ + * the UI needs — kept here so host (env callbacks) and UI agree. Covers all marketplace + * kinds: `type` identifies skills, workflows, and future plugin cards. `requiredSkills` + * is workflow-only; plugin cards use engine/provenance fields for later badges/details. */ export interface SkillCard { id: string; // mint address - type?: "skill" | "workflow"; + type?: MarketItemType; name: string; description?: string; category?: string; @@ -28,6 +28,12 @@ export interface SkillCard { price?: string; // lamports as decimal string (for buy-all cost summing) creator?: string; // wallet (paid on a priced buy) requiredSkills?: string[]; // workflows only: prerequisite skill mint ids + engines?: string[]; // plugins only: engine badges such as "claude", "codex", "mcp" + iqGitPda?: string; // plugins only: canonical IQ Git provenance anchor + version?: string; + capabilities?: string[]; + permissions?: string[]; + pluginManifest?: PluginManifest; } /** Full agent profile payload sent to the UI on getAgentProfile. */ @@ -71,10 +77,11 @@ export interface RpcStatus { // ── UI -> host (requests) ─────────────────────────────────────────────────── export type MarketRequest = - | { type: "searchSkills"; query: string; kind?: "skill" | "workflow" } // kind = the active tab + | { type: "searchSkills"; query: string; kind?: MarketItemType } // kind = the active tab / future plugin tab | { type: "getSkillDetail"; mint: string } // open the detail view for one item | { type: "getSkillDoc"; name: string } // read an installed skill's local SKILL.md by name | { type: "buySkill"; skillId: string; creatorWallet?: string } + | { type: "installPlugin"; pluginId: string; engine: PluginEngine } | { type: "disposeSkill"; skillId: string } // un-equip an owned skill (local + sticky) | { type: "reEquipSkill"; skillId: string } // undo a dispose (re-install, no re-buy) | { type: "ownedSkills" } // ask the host to (re)send the owned list @@ -83,7 +90,7 @@ export type MarketRequest = | { type: "useDefaultRpc" } // clear any key, fall back to the default | { type: "getRpcStatus" } // ask the host to (re)send rpcStatus // issue #34: post a comment on a skill (holder-gated client-side) - | { type: "postNote"; skillId: string; skillType?: "skill" | "workflow"; text: string; gitLink?: string } + | { type: "postNote"; skillId: string; skillType?: MarketItemType; text: string; gitLink?: string } // issue #35: agent directory + profile | { type: "listAgents" } | { type: "getAgentProfile"; wallet: string } @@ -110,6 +117,7 @@ export type MarketEvent = | { type: "skillDetail"; detail: SkillDetail } // full detail for the opened item (includes notes) | { type: "skillDoc"; name: string; text: string | null } // installed skill's SKILL.md (null = not found) | { type: "buyResult"; skillId: string; ok: boolean; slug?: string; error?: string } + | { type: "pluginInstallResult"; pluginId: string; engine: PluginEngine; ok: boolean; error?: string } | { type: "disposeResult"; skillId: string; ok: boolean; slug?: string; error?: string } | { type: "reEquipResult"; skillId: string; ok: boolean; slug?: string; error?: string } // installed skill names (panel fill) + slug->mint for bought NFTs (reuse market detail) + diff --git a/packages/core/src/chat/session.ts b/packages/core/src/chat/session.ts index 7e16d32..cb6f5c3 100644 --- a/packages/core/src/chat/session.ts +++ b/packages/core/src/chat/session.ts @@ -13,6 +13,7 @@ import type { AgentRuntime, SessionHandle } from "../runtime/contract.js"; import type { ApprovalChannel } from "../runtime/approval/channel.js"; +import type { MarketItemType, PluginEngine } from "../core/types.js"; import type { SkillCard, MarketRequest } from "./marketMessages.js"; // The two-way pipe to ONE chat UI (one panel / one socket). Messages both ways are @@ -45,10 +46,11 @@ export interface ChatEnv { // are host-held (the extension owns them), so they're delegated like wallet/cloud. // buySkill installs the bought skill's SKILL.md into the runtime skills dir as part // of the buy (the host calls SkillSync.installBought), returning the installed slug. - searchSkills?(query: string, kind?: "skill" | "workflow"): Promise; + searchSkills?(query: string, kind?: MarketItemType): Promise; getSkillDetail?(mint: string): Promise; getSkillDoc?(name: string): Promise; buySkill?(skillId: string, creatorWallet?: string): Promise<{ ok: boolean; slug?: string; error?: string }>; + installPlugin?(pluginId: string, engine: PluginEngine): Promise<{ ok: boolean; error?: string }>; // dispose (un-equip) an owned skill: local + sticky (soulbound NFT stays owned, no refund). disposeSkill?(skillId: string): Promise<{ ok: boolean; slug?: string; error?: string }>; // re-equip a previously-disposed skill the wallet still owns (undo, no re-buy). @@ -56,7 +58,7 @@ export interface ChatEnv { // slug -> mint for disposed (un-pinned) skills — the UI greys these + offers Re-equip. disposedSkillMints?(): Promise>; // issue #34: post a comment on a skill (holder-gated), returns refreshed notes on success - postNote?(skillId: string, skillType: "skill" | "workflow" | undefined, text: string, gitLink?: string): Promise<{ ok: boolean; notes?: import("./marketMessages.js").Note[]; error?: string }>; + postNote?(skillId: string, skillType: MarketItemType | undefined, text: string, gitLink?: string): Promise<{ ok: boolean; notes?: import("./marketMessages.js").Note[]; error?: string }>; ownedSkills?(): Promise; // skill names already installed (panel fill) // The wallet's soulbound NFT skills, by display name (catalog ∩ holdings). This is // what the "Equipped skills" panel shows: skills you OWN on-chain, not local dirs. @@ -399,6 +401,12 @@ export function createChatSession( } break; } + case "installPlugin": { + const req = m as Extract; + const res = env.installPlugin ? await env.installPlugin(req.pluginId, req.engine) : { ok: false, error: "plugin install unavailable" }; + sendMarket({ type: "pluginInstallResult", pluginId: req.pluginId, engine: req.engine, ...res }); + break; + } case "disposeSkill": { const req = m as Extract; const res = env.disposeSkill ? await env.disposeSkill(req.skillId) : { ok: false, error: "dispose unavailable" }; diff --git a/packages/core/src/chat/ui/webview.ts b/packages/core/src/chat/ui/webview.ts index 66a2a59..902f061 100644 --- a/packages/core/src/chat/ui/webview.ts +++ b/packages/core/src/chat/ui/webview.ts @@ -679,7 +679,7 @@ export function chatHtml(): string { /* a card body is clickable (opens detail); the Buy button stops propagation */ .mktCard .mc-main { cursor: pointer; } .mktCard .mc-main:hover .mc-name { color: var(--an-green); } - /* Skills / Workflows segmented tabs */ + /* Skills / Workflows / Plugins segmented tabs */ .mktTabs { display: inline-flex; gap: 2px; padding: 2px; margin-bottom: 12px; background: var(--an-bg); border: 1px solid var(--an-line); border-radius: 999px; } .mktTab { background: transparent; border: none; color: var(--vscode-foreground); opacity: 0.6; @@ -1472,24 +1472,25 @@ export function chatHtml(): string { -