源码快照 @ 2026-09-02 (source-only, 剔除非源码素材)
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Connection action</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/connection-action.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"name": "@openwork/mcp-apps",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
"./skill-created": {
|
||||
"types": "./dist/skill-created.d.ts",
|
||||
"default": "./dist/skill-created.js"
|
||||
},
|
||||
"./connection-action": {
|
||||
"types": "./dist/connection-action.d.ts",
|
||||
"default": "./dist/connection-action.js"
|
||||
},
|
||||
"./plugin-flow": {
|
||||
"types": "./dist/plugin-flow.d.ts",
|
||||
"default": "./dist/plugin-flow.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "node scripts/build-apps.mjs",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/ext-apps": "^1.7.5",
|
||||
"@openwork/types": "workspace:*",
|
||||
"react": "catalog:",
|
||||
"react-dom": "catalog:",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "19.2.14",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"@vitejs/plugin-react": "^5.0.4",
|
||||
"typescript": "^5.9.0",
|
||||
"vite": "^6.4.3",
|
||||
"vite-plugin-singlefile": "^2.3.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Plugin flow</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/plugin-flow.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,52 @@
|
||||
import { spawnSync } from "node:child_process"
|
||||
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises"
|
||||
import { fileURLToPath } from "node:url"
|
||||
|
||||
const APPS = [
|
||||
{ entry: "skill-created", exportName: "skillCreatedAppHtml" },
|
||||
{ entry: "connection-action", exportName: "connectionActionAppHtml" },
|
||||
{ entry: "plugin-flow", exportName: "pluginFlowAppHtml" },
|
||||
]
|
||||
|
||||
const packageDir = fileURLToPath(new URL("..", import.meta.url))
|
||||
const distUrl = new URL("../dist/", import.meta.url)
|
||||
// Concurrent consumers (parallel test files) import from dist while a build
|
||||
// runs, so never clear dist: stage into a scratch dir and atomically rename
|
||||
// each artifact into place.
|
||||
const scratchBase = new URL(`../dist-build-${process.pid}/`, import.meta.url)
|
||||
|
||||
await mkdir(distUrl, { recursive: true })
|
||||
|
||||
async function writeAtomic(fileName, contents) {
|
||||
const finalUrl = new URL(fileName, distUrl)
|
||||
const temporaryUrl = new URL(`${fileName}.tmp-${process.pid}`, distUrl)
|
||||
await writeFile(temporaryUrl, contents)
|
||||
await rename(temporaryUrl, finalUrl)
|
||||
}
|
||||
|
||||
try {
|
||||
for (const app of APPS) {
|
||||
const scratchDir = new URL(`${app.entry}/`, scratchBase)
|
||||
const build = spawnSync("pnpm", ["exec", "vite", "build", "--outDir", fileURLToPath(scratchDir)], {
|
||||
cwd: packageDir,
|
||||
stdio: "inherit",
|
||||
env: { ...process.env, OPENWORK_MCP_APP: app.entry },
|
||||
})
|
||||
if (build.status !== 0) {
|
||||
throw new Error(`vite build failed for ${app.entry}`)
|
||||
}
|
||||
const html = await readFile(new URL(`${app.entry}.html`, scratchDir), "utf8")
|
||||
await writeAtomic(`${app.entry}.js`, [
|
||||
`export const ${app.exportName} = ${JSON.stringify(html)}`,
|
||||
`export default ${app.exportName}`,
|
||||
"",
|
||||
].join("\n"))
|
||||
await writeAtomic(`${app.entry}.d.ts`, [
|
||||
`export declare const ${app.exportName}: string`,
|
||||
`export default ${app.exportName}`,
|
||||
"",
|
||||
].join("\n"))
|
||||
}
|
||||
} finally {
|
||||
await rm(scratchBase, { recursive: true, force: true })
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Skill created</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/skill-created.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,81 @@
|
||||
import { connectionActionPayloadSchema, type ConnectionActionPayload } from "@openwork/types/connection-action-app"
|
||||
import { mountMcpApp } from "./shared/bridge"
|
||||
import { AlertIcon, AppHeader, ArrowIcon, CardBody, CardFooter, CheckIcon, KeyValueGrid, PlugIcon, type Tone } from "./shared/ui"
|
||||
import "./shared/theme.css"
|
||||
|
||||
const STATE_PRESENTATION: Record<ConnectionActionPayload["state"], {
|
||||
tone: Tone
|
||||
badge: string
|
||||
title: string
|
||||
}> = {
|
||||
connected: { tone: "success", badge: "Connected", title: "Connection ready" },
|
||||
needs_connection: { tone: "warning", badge: "Not connected", title: "Connection needed" },
|
||||
reauth_required: { tone: "warning", badge: "Sign-in required", title: "Reconnect needed" },
|
||||
provider_error: { tone: "danger", badge: "Provider error", title: "Connection error" },
|
||||
}
|
||||
|
||||
const ACTOR_LABEL: Record<NonNullable<ConnectionActionPayload["actor"]>, string> = {
|
||||
member: "You",
|
||||
organization_admin: "An organization admin",
|
||||
provider_admin: "The provider admin",
|
||||
network_admin: "A network admin",
|
||||
openwork: "OpenWork support",
|
||||
}
|
||||
|
||||
const SURFACE_LABEL: Record<NonNullable<ConnectionActionPayload["action"]>["surface"], string> = {
|
||||
openwork_your_connections: "Your Connections",
|
||||
openwork_organization_connections: "Organization Connections",
|
||||
provider_admin_console: "Provider admin console",
|
||||
network_infrastructure: "Network infrastructure",
|
||||
openwork_support: "OpenWork support",
|
||||
}
|
||||
|
||||
mountMcpApp({
|
||||
name: "OpenWork Connection Action",
|
||||
waitingLabel: "Checking the connection...",
|
||||
schema: connectionActionPayloadSchema,
|
||||
render: (payload, app) => {
|
||||
const presentation = STATE_PRESENTATION[payload.state]
|
||||
const actionUrl = payload.action?.url
|
||||
const openAction = () => {
|
||||
if (actionUrl) void app?.openLink({ url: actionUrl })
|
||||
}
|
||||
return (
|
||||
<main className="card">
|
||||
<AppHeader
|
||||
tone={presentation.tone}
|
||||
icon={payload.state === "connected" ? <CheckIcon /> : payload.state === "provider_error" ? <AlertIcon /> : <PlugIcon />}
|
||||
title={presentation.title}
|
||||
subtitle={payload.connectionName}
|
||||
badge={{ tone: presentation.tone, label: presentation.badge }}
|
||||
/>
|
||||
<CardBody>
|
||||
<p className="name">{payload.connectionName}</p>
|
||||
<p className="description">{payload.message}</p>
|
||||
{payload.action ? (
|
||||
<KeyValueGrid
|
||||
items={[
|
||||
...(payload.actor ? [{ label: "Who acts", value: ACTOR_LABEL[payload.actor] }] : []),
|
||||
{ label: "Where", value: SURFACE_LABEL[payload.action.surface] },
|
||||
]}
|
||||
/>
|
||||
) : null}
|
||||
</CardBody>
|
||||
<CardFooter
|
||||
footnote={payload.state === "connected"
|
||||
? "Tools from this connection are available in chat right now."
|
||||
: "After it is fixed, ask again in this chat — the agent searches live."}
|
||||
action={payload.action ? (
|
||||
actionUrl ? (
|
||||
<button className="action-primary" type="button" onClick={openAction}>
|
||||
{payload.action.label} <ArrowIcon />
|
||||
</button>
|
||||
) : (
|
||||
<span className="badge" data-tone={presentation.tone}>{payload.action.label}</span>
|
||||
)
|
||||
) : undefined}
|
||||
/>
|
||||
</main>
|
||||
)
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,67 @@
|
||||
import { pluginFlowPayloadSchema, type PluginFlowPayload } from "@openwork/types/plugin-flow-app"
|
||||
import { mountMcpApp } from "./shared/bridge"
|
||||
import { AppHeader, CardBody, CardFooter, CheckIcon, KeyValueGrid, ShareIcon } from "./shared/ui"
|
||||
import "./shared/theme.css"
|
||||
|
||||
const MODE_PRESENTATION: Record<PluginFlowPayload["mode"], {
|
||||
title: string
|
||||
subtitle: string
|
||||
footnote: string
|
||||
}> = {
|
||||
marketplace_plugin_added: {
|
||||
title: "Plugin added to marketplace",
|
||||
subtitle: "The plugin is now listed for everyone with marketplace access.",
|
||||
footnote: "Members with access can install it from the marketplace.",
|
||||
},
|
||||
plugin_access_granted: {
|
||||
title: "Plugin shared",
|
||||
subtitle: "Access was granted successfully.",
|
||||
footnote: "The recipient can use this plugin's skills in chat immediately.",
|
||||
},
|
||||
marketplace_access_granted: {
|
||||
title: "Marketplace access granted",
|
||||
subtitle: "The member can now browse this marketplace.",
|
||||
footnote: "They can install plugins from it right away.",
|
||||
},
|
||||
}
|
||||
|
||||
const RECIPIENT_LABEL: Record<NonNullable<PluginFlowPayload["recipient"]>["kind"], string> = {
|
||||
member: "Member",
|
||||
team: "Team",
|
||||
org_wide: "Entire organization",
|
||||
}
|
||||
|
||||
mountMcpApp({
|
||||
name: "OpenWork Plugin Flow",
|
||||
waitingLabel: "Finishing up...",
|
||||
schema: pluginFlowPayloadSchema,
|
||||
render: (payload) => {
|
||||
const presentation = MODE_PRESENTATION[payload.mode]
|
||||
const items: Array<{ label: string; value: string; mono?: boolean }> = []
|
||||
if (payload.pluginId) items.push({ label: "Plugin", value: payload.pluginId, mono: true })
|
||||
if (payload.marketplaceId) items.push({ label: "Marketplace", value: payload.marketplaceId, mono: true })
|
||||
if (payload.recipient) {
|
||||
items.push({
|
||||
label: RECIPIENT_LABEL[payload.recipient.kind],
|
||||
value: payload.recipient.id ?? "org-wide",
|
||||
mono: payload.recipient.id !== null,
|
||||
})
|
||||
if (payload.recipient.role) items.push({ label: "Role", value: payload.recipient.role })
|
||||
}
|
||||
return (
|
||||
<main className="card">
|
||||
<AppHeader
|
||||
tone={payload.mode === "marketplace_plugin_added" ? "info" : "success"}
|
||||
icon={payload.mode === "marketplace_plugin_added" ? <ShareIcon /> : <CheckIcon />}
|
||||
title={presentation.title}
|
||||
subtitle={presentation.subtitle}
|
||||
badge={{ tone: "success", label: "Done" }}
|
||||
/>
|
||||
<CardBody>
|
||||
<KeyValueGrid items={items} />
|
||||
</CardBody>
|
||||
<CardFooter footnote={presentation.footnote} />
|
||||
</main>
|
||||
)
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,58 @@
|
||||
import { StrictMode, useState, type ReactNode } from "react"
|
||||
import { createRoot } from "react-dom/client"
|
||||
import { useApp, useHostStyles } from "@modelcontextprotocol/ext-apps/react"
|
||||
import type { App } from "@modelcontextprotocol/ext-apps"
|
||||
import type { z } from "zod"
|
||||
import { StatusCard } from "./ui"
|
||||
|
||||
/**
|
||||
* Standard bootstrap for every first-party OpenWork MCP App: connect to the
|
||||
* host over the MCP Apps bridge, validate the tool's structuredContent
|
||||
* against the app's zod contract, and hand a typed payload to the view.
|
||||
*/
|
||||
export function mountMcpApp<Schema extends z.ZodType>(config: {
|
||||
name: string
|
||||
waitingLabel: string
|
||||
schema: Schema
|
||||
render: (payload: z.infer<Schema>, app: App | null) => ReactNode
|
||||
}) {
|
||||
function AppRoot() {
|
||||
const [payload, setPayload] = useState<z.infer<Schema> | null>(null)
|
||||
const [resultError, setResultError] = useState<string | null>(null)
|
||||
const { app, error } = useApp({
|
||||
appInfo: { name: config.name, version: "1.0.0" },
|
||||
capabilities: {},
|
||||
onAppCreated: (createdApp) => {
|
||||
createdApp.ontoolresult = (result) => {
|
||||
const parsed = config.schema.safeParse(result.structuredContent)
|
||||
if (!parsed.success) {
|
||||
setResultError("The result did not match the expected data contract.")
|
||||
return
|
||||
}
|
||||
setResultError(null)
|
||||
setPayload(parsed.data)
|
||||
}
|
||||
createdApp.ontoolcancelled = ({ reason }) => {
|
||||
setResultError(reason ?? "The tool call was cancelled.")
|
||||
}
|
||||
},
|
||||
})
|
||||
useHostStyles(app, app?.getHostContext())
|
||||
|
||||
if (error || resultError) {
|
||||
return <StatusCard>{error?.message ?? resultError}</StatusCard>
|
||||
}
|
||||
if (!payload) {
|
||||
return <StatusCard>{config.waitingLabel}</StatusCard>
|
||||
}
|
||||
return <>{config.render(payload, app)}</>
|
||||
}
|
||||
|
||||
const root = document.getElementById("root")
|
||||
if (!root) throw new Error(`${config.name} root element is missing.`)
|
||||
createRoot(root).render(
|
||||
<StrictMode>
|
||||
<AppRoot />
|
||||
</StrictMode>,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,289 @@
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
color: var(--color-text-primary, #1c2024);
|
||||
background: transparent;
|
||||
font-family: var(--font-sans, ui-sans-serif, system-ui, sans-serif);
|
||||
|
||||
--app-bg: var(--color-background-primary, #fff);
|
||||
--app-bg-subtle: var(--color-background-secondary, #f1f3f5);
|
||||
--app-bg-hover: var(--color-background-tertiary, #f8f9fa);
|
||||
--app-text: var(--color-text-primary, #1c2024);
|
||||
--app-muted: var(--color-text-secondary, #60646c);
|
||||
--app-border: var(--color-border-primary, #f3f4f6);
|
||||
--app-accent: var(--color-background-inverse, #011627);
|
||||
--app-accent-fg: var(--color-text-inverse, #fff);
|
||||
--app-success: var(--color-text-success, #2a7e3b);
|
||||
--app-success-bg: var(--color-background-success, #e9f6e9);
|
||||
--app-success-border: var(--color-border-success, rgb(42 126 59 / 25%));
|
||||
--app-warning: var(--color-text-warning, #ab6400);
|
||||
--app-warning-bg: var(--color-background-warning, #fff1d6);
|
||||
--app-warning-border: var(--color-border-warning, rgb(171 100 0 / 28%));
|
||||
--app-danger: var(--color-text-danger, #ce2c31);
|
||||
--app-danger-bg: var(--color-background-danger, #feebec);
|
||||
--app-danger-border: var(--color-border-danger, rgb(206 44 49 / 25%));
|
||||
--app-info: var(--color-text-info, #0d74ce);
|
||||
--app-info-bg: var(--color-background-info, #e6f4fe);
|
||||
--app-info-border: var(--color-border-info, rgb(13 116 206 / 25%));
|
||||
--app-radius: var(--border-radius-lg, 16px);
|
||||
--app-radius-sm: 10px;
|
||||
--app-shadow: var(--shadow-sm, 0 1px 2px rgb(15 23 42 / 5%));
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-width: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
button {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.card {
|
||||
overflow: hidden;
|
||||
max-width: 42rem;
|
||||
border: 1px solid var(--app-border);
|
||||
border-radius: var(--app-radius);
|
||||
background: var(--app-bg);
|
||||
box-shadow: var(--app-shadow);
|
||||
}
|
||||
|
||||
.card-status {
|
||||
display: grid;
|
||||
min-height: 96px;
|
||||
place-items: center;
|
||||
padding: 16px;
|
||||
color: var(--app-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--app-border);
|
||||
}
|
||||
|
||||
.mark {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin-top: 2px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid var(--app-border);
|
||||
border-radius: 999px;
|
||||
background: var(--app-bg-hover);
|
||||
color: var(--app-muted);
|
||||
}
|
||||
|
||||
.mark svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.mark[data-tone="success"] {
|
||||
border-color: var(--app-success-border);
|
||||
background: var(--app-success-bg);
|
||||
color: var(--app-success);
|
||||
}
|
||||
|
||||
.mark[data-tone="warning"] {
|
||||
border-color: var(--app-warning-border);
|
||||
background: var(--app-warning-bg);
|
||||
color: var(--app-warning);
|
||||
}
|
||||
|
||||
.mark[data-tone="danger"] {
|
||||
border-color: var(--app-danger-border);
|
||||
background: var(--app-danger-bg);
|
||||
color: var(--app-danger);
|
||||
}
|
||||
|
||||
.mark[data-tone="info"] {
|
||||
border-color: var(--app-info-border);
|
||||
background: var(--app-info-bg);
|
||||
color: var(--app-info);
|
||||
}
|
||||
|
||||
.header-copy {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
h1 {
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 1.35;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
overflow: hidden;
|
||||
margin: 2px 0 0;
|
||||
color: var(--app-muted);
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.badge {
|
||||
flex-shrink: 0;
|
||||
margin-top: 4px;
|
||||
padding: 3px 9px;
|
||||
border: 1px solid var(--app-border);
|
||||
border-radius: 999px;
|
||||
color: var(--app-muted);
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.badge[data-tone="success"] {
|
||||
border-color: var(--app-success-border);
|
||||
background: var(--app-success-bg);
|
||||
color: var(--app-success);
|
||||
}
|
||||
|
||||
.badge[data-tone="warning"] {
|
||||
border-color: var(--app-warning-border);
|
||||
background: var(--app-warning-bg);
|
||||
color: var(--app-warning);
|
||||
}
|
||||
|
||||
.badge[data-tone="danger"] {
|
||||
border-color: var(--app-danger-border);
|
||||
background: var(--app-danger-bg);
|
||||
color: var(--app-danger);
|
||||
}
|
||||
|
||||
.badge[data-tone="info"] {
|
||||
border-color: var(--app-info-border);
|
||||
background: var(--app-info-bg);
|
||||
color: var(--app-info);
|
||||
}
|
||||
|
||||
.body {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.name {
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.description {
|
||||
margin: 0;
|
||||
color: var(--app-muted);
|
||||
font-size: 13px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.identifiers {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 8px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.identifiers > div {
|
||||
min-width: 0;
|
||||
padding: 8px 11px;
|
||||
border-radius: var(--app-radius-sm);
|
||||
background: var(--app-bg-subtle);
|
||||
}
|
||||
|
||||
.identifiers dt {
|
||||
margin-bottom: 3px;
|
||||
color: var(--app-muted);
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
letter-spacing: .05em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.identifiers dd {
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.identifiers dd[data-mono="true"] {
|
||||
font-family: var(--font-mono, ui-monospace, monospace);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
border-top: 1px solid var(--app-border);
|
||||
}
|
||||
|
||||
.footnote {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
color: var(--app-muted);
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.action-primary {
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
padding: 7px 12px;
|
||||
border: 0;
|
||||
border-radius: var(--app-radius-sm);
|
||||
background: var(--app-accent);
|
||||
color: var(--app-accent-fg);
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.action-primary:hover {
|
||||
opacity: .9;
|
||||
}
|
||||
|
||||
.action-primary svg {
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.badge {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.identifiers {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.footer {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import type { ReactNode } from "react"
|
||||
|
||||
export type Tone = "neutral" | "success" | "warning" | "danger" | "info"
|
||||
|
||||
export function CheckIcon() {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
<path d="M20 6 9 17l-5-5" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function PlugIcon() {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
<path d="M12 22v-5" />
|
||||
<path d="M9 8V2" />
|
||||
<path d="M15 8V2" />
|
||||
<path d="M18 8v5a4 4 0 0 1-4 4h-4a4 4 0 0 1-4-4V8Z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function AlertIcon() {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
<path d="M12 9v4" />
|
||||
<path d="M12 17h.01" />
|
||||
<path d="M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0Z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function ShareIcon() {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
<path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8" />
|
||||
<path d="m16 6-4-4-4 4" />
|
||||
<path d="M12 2v13" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function ArrowIcon() {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
<path d="M7 17 17 7" />
|
||||
<path d="M8 7h9v9" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function AppHeader(props: {
|
||||
tone: Tone
|
||||
icon: ReactNode
|
||||
title: string
|
||||
subtitle?: string
|
||||
badge?: { tone: Tone; label: string } | null
|
||||
}) {
|
||||
return (
|
||||
<header className="header">
|
||||
<span className="mark" data-tone={props.tone === "neutral" ? undefined : props.tone} aria-hidden="true">{props.icon}</span>
|
||||
<div className="header-copy">
|
||||
<h1>{props.title}</h1>
|
||||
{props.subtitle ? <p className="subtitle">{props.subtitle}</p> : null}
|
||||
</div>
|
||||
{props.badge ? (
|
||||
<span className="badge" data-tone={props.badge.tone === "neutral" ? undefined : props.badge.tone}>
|
||||
{props.badge.label}
|
||||
</span>
|
||||
) : null}
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
export function CardBody(props: { children: ReactNode }) {
|
||||
return <div className="body">{props.children}</div>
|
||||
}
|
||||
|
||||
export function CardFooter(props: { footnote: string; action?: ReactNode }) {
|
||||
return (
|
||||
<footer className="footer">
|
||||
<p className="footnote">{props.footnote}</p>
|
||||
{props.action ?? null}
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
|
||||
export function KeyValueGrid(props: { items: Array<{ label: string; value: string; mono?: boolean }> }) {
|
||||
if (props.items.length === 0) return null
|
||||
return (
|
||||
<dl className="identifiers">
|
||||
{props.items.map((item) => (
|
||||
<div key={item.label}>
|
||||
<dt>{item.label}</dt>
|
||||
<dd data-mono={item.mono === true ? "true" : undefined}>{item.value}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
)
|
||||
}
|
||||
|
||||
export function StatusCard(props: { children: ReactNode }) {
|
||||
return <main className="card card-status">{props.children}</main>
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { skillCreatedPayloadSchema } from "@openwork/types/skill-created-app"
|
||||
import { mountMcpApp } from "./shared/bridge"
|
||||
import { AppHeader, ArrowIcon, CardBody, CardFooter, CheckIcon, KeyValueGrid } from "./shared/ui"
|
||||
import "./shared/theme.css"
|
||||
|
||||
mountMcpApp({
|
||||
name: "OpenWork Skill Saved",
|
||||
waitingLabel: "Finishing your skill...",
|
||||
schema: skillCreatedPayloadSchema,
|
||||
render: (payload, app) => {
|
||||
const updated = payload.mode === "updated"
|
||||
const openLibrary = () => {
|
||||
if (payload.libraryUrl) void app?.openLink({ url: payload.libraryUrl })
|
||||
}
|
||||
return (
|
||||
<main className="card">
|
||||
<AppHeader
|
||||
tone="success"
|
||||
icon={<CheckIcon />}
|
||||
title={updated ? "Skill updated" : "Skill created"}
|
||||
subtitle={updated
|
||||
? "A new version is live for everyone with access."
|
||||
: "Private to you until you share it or add it to a marketplace."}
|
||||
badge={{ tone: "success", label: "Ready" }}
|
||||
/>
|
||||
<CardBody>
|
||||
<p className="name">{payload.name}</p>
|
||||
<p className="description">{payload.description}</p>
|
||||
<KeyValueGrid
|
||||
items={[
|
||||
{ label: "Plugin", value: payload.pluginId, mono: true },
|
||||
{ label: "Skill", value: payload.skillId, mono: true },
|
||||
]}
|
||||
/>
|
||||
</CardBody>
|
||||
<CardFooter
|
||||
footnote="Usable in this chat right now."
|
||||
action={payload.libraryUrl ? (
|
||||
<button className="action-primary" type="button" onClick={openLibrary}>
|
||||
Open in Library <ArrowIcon />
|
||||
</button>
|
||||
) : undefined}
|
||||
/>
|
||||
</main>
|
||||
)
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"noEmit": true,
|
||||
"strict": true,
|
||||
"target": "ES2022"
|
||||
},
|
||||
"include": ["src", "vite.config.ts"]
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import react from "@vitejs/plugin-react"
|
||||
import { defineConfig } from "vite"
|
||||
import { viteSingleFile } from "vite-plugin-singlefile"
|
||||
|
||||
const app = (process.env.OPENWORK_MCP_APP ?? "skill-created").trim()
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react(), viteSingleFile({ removeViteModuleLoader: true })],
|
||||
build: {
|
||||
cssCodeSplit: false,
|
||||
// The build script stages each app into its own scratch outDir and
|
||||
// renames artifacts into dist atomically, so nothing here may clear
|
||||
// shared output that concurrent consumers are importing.
|
||||
emptyOutDir: true,
|
||||
modulePreload: { polyfill: false },
|
||||
rollupOptions: {
|
||||
input: `${app}.html`,
|
||||
},
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user