源码快照 @ 2026-09-02 (source-only, 剔除非源码素材)
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
export const OpenWorkConnectInstaller = () => {
|
||||
const MCP_SERVER_URL = "https://api.openworklabs.com/mcp/agent";
|
||||
const CODEX_CONNECTIONS_DEEPLINK = "codex://settings/connections";
|
||||
const CHATGPT_SETTINGS_URL = "https://chatgpt.com/#settings/Connectors";
|
||||
const CODEX_LOGIN_COMMAND = "codex mcp login openwork";
|
||||
const CODEX_RECONNECT_COMMAND = `codex mcp logout openwork
|
||||
codex mcp login openwork`;
|
||||
const OPENCODE_AUTH_COMMAND = "opencode mcp auth openwork";
|
||||
const OPENCODE_RECONNECT_COMMAND = `opencode mcp logout openwork
|
||||
opencode mcp auth openwork`;
|
||||
const installs = [
|
||||
{
|
||||
id: "cursor",
|
||||
label: "Cursor",
|
||||
eyebrow: "Cursor Desktop and Web/Agents",
|
||||
helper: "Setup-only for Cursor Desktop and Cursor Web/Agents. Cursor Desktop's OAuth callback cursor://anysphere.cursor-mcp/oauth/callback is accepted with PKCE S256 enforced.",
|
||||
supportStatus: "Setup only",
|
||||
supportExplanation: "Setup guide only: paste the server URL into Cursor and start OAuth. Cursor Desktop's cursor://anysphere.cursor-mcp/oauth/callback callback is accepted through an exact allowlist with PKCE S256 enforced. Native proof is not complete.",
|
||||
copyText: MCP_SERVER_URL,
|
||||
},
|
||||
{
|
||||
id: "codex",
|
||||
label: "Codex",
|
||||
eyebrow: "Codex desktop, CLI, and IDE",
|
||||
helper: "Add OpenWork once, then sign in with Codex's MCP login command.",
|
||||
supportStatus: "Setup only",
|
||||
supportExplanation: "Setup guide only: add OpenWork, run codex mcp login openwork, and reconnect with logout then login. Native proof must be rerun on this exact branch.",
|
||||
copyText: `codex mcp add openwork --url ${MCP_SERVER_URL}`,
|
||||
authText: CODEX_LOGIN_COMMAND,
|
||||
reconnectText: CODEX_RECONNECT_COMMAND,
|
||||
},
|
||||
{
|
||||
id: "chatgpt-desktop",
|
||||
label: "ChatGPT Desktop",
|
||||
eyebrow: "Guided desktop setup",
|
||||
helper: "Open ChatGPT Settings > MCP servers, paste this URL, then start OAuth from ChatGPT's connection prompt.",
|
||||
supportStatus: "Setup only",
|
||||
supportExplanation: "Setup guide only: paste the URL in ChatGPT Settings > MCP servers and start OAuth there. Native proof is not complete.",
|
||||
copyText: MCP_SERVER_URL,
|
||||
},
|
||||
{
|
||||
id: "claude-code",
|
||||
label: "Claude Code",
|
||||
eyebrow: "One terminal command",
|
||||
helper: "Add the remote HTTP server, then use /mcp in Claude Code and follow the client auth flow.",
|
||||
supportStatus: "Setup only",
|
||||
supportExplanation: "Setup guide only: add the server, then use /mcp in Claude Code to run the client auth flow. Native proof is not complete.",
|
||||
copyText: `claude mcp add --transport http openwork ${MCP_SERVER_URL}`,
|
||||
},
|
||||
{
|
||||
id: "opencode",
|
||||
label: "OpenCode",
|
||||
eyebrow: "opencode.json MCP config",
|
||||
helper: "Add this remote MCP server entry to your OpenCode config, then authenticate.",
|
||||
supportStatus: "Verified",
|
||||
supportExplanation: "Verified with OpenCode native remote MCP OAuth flow.",
|
||||
copyText: `{
|
||||
"mcp": {
|
||||
"openwork": {
|
||||
"type": "remote",
|
||||
"enabled": true,
|
||||
"url": "${MCP_SERVER_URL}",
|
||||
"oauth": {}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
authText: OPENCODE_AUTH_COMMAND,
|
||||
reconnectText: OPENCODE_RECONNECT_COMMAND,
|
||||
},
|
||||
{
|
||||
id: "vs-code",
|
||||
label: "VS Code",
|
||||
eyebrow: "VS Code MCP command",
|
||||
helper: "Run this from a shell with the VS Code CLI on your path, then start OAuth from VS Code's MCP server prompt.",
|
||||
supportStatus: "Setup only",
|
||||
supportExplanation: "Setup guide only: add the server with the VS Code CLI, then start OAuth from VS Code's MCP server prompt. Native proof is not complete.",
|
||||
copyText: `code --add-mcp '{"name":"openwork","type":"http","url":"${MCP_SERVER_URL}"}'`,
|
||||
},
|
||||
{
|
||||
id: "any-client",
|
||||
label: "Any client",
|
||||
eyebrow: "Bring your own MCP client",
|
||||
helper: "Paste this URL only into clients that support remote Streamable HTTP MCP servers and OAuth.",
|
||||
supportStatus: "Setup only",
|
||||
supportExplanation: "Setup guide only: use clients that support remote Streamable HTTP MCP servers and OAuth. Native proof depends on the client.",
|
||||
copyText: MCP_SERVER_URL,
|
||||
},
|
||||
];
|
||||
const clientFromHash = () => {
|
||||
if (typeof window === "undefined") return "cursor";
|
||||
const requested = window.location.hash.slice(1).replace("connect-mcp-install-", "");
|
||||
return installs.some((install) => install.id === requested) ? requested : "cursor";
|
||||
};
|
||||
const copyText = async (value) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(value);
|
||||
return true;
|
||||
} catch {
|
||||
const textarea = document.createElement("textarea");
|
||||
textarea.value = value;
|
||||
textarea.setAttribute("readonly", "");
|
||||
textarea.style.cssText = "position:absolute;left:-9999px;top:-9999px";
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
const didCopy = document.execCommand("copy");
|
||||
textarea.remove();
|
||||
return didCopy;
|
||||
}
|
||||
};
|
||||
const [activeClient, setActiveClient] = useState(clientFromHash);
|
||||
const [copied, setCopied] = useState("");
|
||||
const activeInstall = installs.find((install) => install.id === activeClient) || installs[0];
|
||||
|
||||
useEffect(() => {
|
||||
const selectHashClient = () => setActiveClient(clientFromHash());
|
||||
window.addEventListener("hashchange", selectHashClient);
|
||||
selectHashClient();
|
||||
return () => window.removeEventListener("hashchange", selectHashClient);
|
||||
}, []);
|
||||
|
||||
const copy = async (id, value) => {
|
||||
const didCopy = await copyText(value);
|
||||
setCopied(didCopy ? id : "error");
|
||||
window.setTimeout(() => setCopied(""), 2500);
|
||||
};
|
||||
|
||||
return (
|
||||
<div id="connect-mcp-install" className="not-prose my-8 overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-sm dark:border-white/10 dark:bg-gray-950">
|
||||
<div className="border-b border-gray-100 px-5 py-5 dark:border-white/10">
|
||||
<div className="text-sm text-gray-600 dark:text-gray-300">
|
||||
Developers: point your own agent at your org — verified clients and setup guides.
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-gray-400">
|
||||
Verified for OpenCode only; setup guides for Codex, Cursor, ChatGPT, Claude Code, VS Code, and more
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto border-b border-gray-100 px-4 pt-4 dark:border-white/10" role="tablist" aria-label="OpenWork MCP client install options">
|
||||
<div className="flex min-w-max gap-1">
|
||||
{installs.map((install) => (
|
||||
<button
|
||||
key={install.id}
|
||||
id={`connect-mcp-install-${install.id}`}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={install.id === activeClient}
|
||||
aria-controls={`connect-mcp-panel-${install.id}`}
|
||||
onClick={() => {
|
||||
setActiveClient(install.id);
|
||||
window.history.replaceState(null, "", `#connect-mcp-install-${install.id}`);
|
||||
}}
|
||||
className={`rounded-t-lg border-b-2 px-3 py-2 text-sm font-medium ${install.id === activeClient ? "border-gray-950 text-gray-950 dark:border-white dark:text-white" : "border-transparent text-gray-500 hover:text-gray-900 dark:hover:text-white"}`}
|
||||
>
|
||||
{install.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id={`connect-mcp-panel-${activeInstall.id}`} role="tabpanel" aria-labelledby={`connect-mcp-install-${activeInstall.id}`} className="p-5">
|
||||
<p className="m-0 text-xs font-semibold uppercase tracking-wider text-gray-400">{activeInstall.eyebrow}</p>
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<h3 className="mb-0 mt-2 text-xl font-semibold text-gray-950 dark:text-white">{activeInstall.label}</h3>
|
||||
<p className="mb-4 mt-1 text-sm text-gray-500 dark:text-gray-400">{activeInstall.helper}</p>
|
||||
</div>
|
||||
{activeInstall.id === "codex" ? (
|
||||
<a href={CODEX_CONNECTIONS_DEEPLINK} onClick={() => void copyText(MCP_SERVER_URL)} className="shrink-0 rounded-full bg-[#011627] px-5 py-2.5 text-sm font-medium text-white">
|
||||
Open settings + copy URL
|
||||
</a>
|
||||
) : activeInstall.id === "chatgpt-desktop" ? (
|
||||
<a href={CHATGPT_SETTINGS_URL} target="_blank" rel="noreferrer" onClick={() => copy("chatgpt-url", MCP_SERVER_URL)} className="shrink-0 rounded-full bg-[#011627] px-5 py-2.5 text-sm font-medium text-white">
|
||||
{copied === "chatgpt-url" ? "Copied URL" : "Open settings + copy URL"}
|
||||
</a>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mb-4 mt-2 flex flex-col gap-1 sm:flex-row sm:items-center sm:gap-2">
|
||||
<span className={`w-fit rounded-full border px-2.5 py-1 text-[11px] font-semibold uppercase tracking-wider ${activeInstall.supportStatus === "Verified" ? "border-green-200 bg-green-50 text-green-700" : "border-amber-200 bg-amber-50 text-amber-700"}`}>{activeInstall.supportStatus}</span>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">{activeInstall.supportExplanation}</span>
|
||||
</div>
|
||||
<pre className="m-0 max-h-80 overflow-auto whitespace-pre-wrap rounded-xl bg-[#011627] p-4 text-xs leading-6 text-white"><code>{activeInstall.copyText}</code></pre>
|
||||
{activeInstall.id === "any-client" ? (
|
||||
<p className="mb-0 mt-3 text-sm text-gray-500 dark:text-gray-400">Use this URL only with MCP clients that support remote Streamable HTTP servers with OAuth.</p>
|
||||
) : null}
|
||||
{activeInstall.authText ? (
|
||||
<div className="mt-3">
|
||||
<p className="mb-1.5 mt-0 text-xs font-semibold uppercase tracking-wider text-gray-400">Authenticate</p>
|
||||
<pre className="m-0 overflow-auto whitespace-pre-wrap rounded-xl bg-[#011627] p-4 text-xs leading-6 text-white"><code>{activeInstall.authText}</code></pre>
|
||||
</div>
|
||||
) : null}
|
||||
{activeInstall.reconnectText ? (
|
||||
<div className="mt-3">
|
||||
<p className="mb-1.5 mt-0 text-xs font-semibold uppercase tracking-wider text-gray-400">Reconnect or switch org</p>
|
||||
<pre className="m-0 overflow-auto whitespace-pre-wrap rounded-xl bg-[#011627] p-4 text-xs leading-6 text-white"><code>{activeInstall.reconnectText}</code></pre>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="mt-4 flex items-center justify-between gap-3">
|
||||
<p className="m-0 text-xs text-gray-500">Works with your OpenWork account — <a href="https://app.openworklabs.com?mode=sign-up" className="font-medium underline">create one free</a>.</p>
|
||||
<button type="button" aria-label="Copy the OpenWork MCP install command" onClick={() => copy(activeInstall.id, activeInstall.copyText)} className="shrink-0 rounded-lg bg-[#011627] px-4 py-2 text-xs font-medium text-white">
|
||||
{copied === activeInstall.id ? "Copied" : copied === "error" ? "Couldn't copy" : "Copy"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 border-t border-gray-100 px-5 py-4 text-xs sm:flex-row sm:items-center sm:justify-between dark:border-white/10">
|
||||
<span className="font-semibold uppercase tracking-wider text-gray-400">Server URL</span>
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<code className="break-all text-gray-950 dark:text-white">{MCP_SERVER_URL}</code>
|
||||
<button type="button" aria-label="Copy the OpenWork MCP server URL" onClick={() => copy("server-url", MCP_SERVER_URL)} className="shrink-0 font-medium underline">
|
||||
{copied === "server-url" ? "Copied" : "Copy URL"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,282 @@
|
||||
export const OpenWorkDownloads = () => {
|
||||
const STABLE_RELEASES_URL = "https://api.github.com/repos/different-ai/openwork/releases?per_page=30";
|
||||
const ALPHA_RELEASE_URL = "https://api.github.com/repos/different-ai/openwork/releases/tags/alpha-macos-latest";
|
||||
const GITHUB_HEADERS = { Accept: "application/vnd.github+json" };
|
||||
const dateFormatter = new Intl.DateTimeFormat("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
});
|
||||
const [activeChannel, setActiveChannel] = useState("stable");
|
||||
const [stableReleases, setStableReleases] = useState([]);
|
||||
const [stableLoading, setStableLoading] = useState(true);
|
||||
const [stableError, setStableError] = useState("");
|
||||
const [stableVisibleCount, setStableVisibleCount] = useState(8);
|
||||
const [alphaBuilds, setAlphaBuilds] = useState([]);
|
||||
const [alphaLoading, setAlphaLoading] = useState(false);
|
||||
const [alphaError, setAlphaError] = useState("");
|
||||
const [alphaRequested, setAlphaRequested] = useState(false);
|
||||
const [alphaVisibleCount, setAlphaVisibleCount] = useState(12);
|
||||
|
||||
const formatSize = (bytes) => `${(bytes / (1024 * 1024)).toFixed(0)} MB`;
|
||||
const formatDate = (date) => dateFormatter.format(new Date(date));
|
||||
const githubError = (response, channel) => {
|
||||
const rateLimitMessage = response.status === 403
|
||||
? " The unauthenticated GitHub API rate limit is 60 requests per hour per IP."
|
||||
: "";
|
||||
return `Could not load ${channel}: GitHub returned HTTP ${response.status}.${rateLimitMessage}`;
|
||||
};
|
||||
const getStableGroups = (release) => {
|
||||
const version = release.tag_name.slice(1);
|
||||
const findAsset = (name) => release.assets.find((asset) => asset.name === name);
|
||||
return [
|
||||
{
|
||||
label: "macOS",
|
||||
downloads: [
|
||||
{ label: "Mac (Apple Silicon)", asset: findAsset(`openwork-mac-arm64-${version}.dmg`) },
|
||||
{ label: "Mac (Intel)", asset: findAsset(`openwork-mac-x64-${version}.dmg`) },
|
||||
].filter((download) => download.asset),
|
||||
},
|
||||
{
|
||||
label: "Windows",
|
||||
downloads: [
|
||||
{ label: "Windows (x64)", asset: findAsset(`openwork-win-x64-${version}.exe`) },
|
||||
{ label: "Windows (ARM64)", asset: findAsset(`openwork-win-arm64-${version}.exe`) },
|
||||
].filter((download) => download.asset),
|
||||
},
|
||||
{
|
||||
label: "Linux",
|
||||
downloads: [
|
||||
{ label: "AppImage (x64)", asset: findAsset(`openwork-linux-x86_64-${version}.AppImage`) },
|
||||
{ label: "AppImage (ARM64)", asset: findAsset(`openwork-linux-arm64-${version}.AppImage`) },
|
||||
{ label: "tar.gz (x64)", asset: findAsset(`openwork-linux-x64-${version}.tar.gz`) },
|
||||
{ label: "tar.gz (ARM64)", asset: findAsset(`openwork-linux-arm64-${version}.tar.gz`) },
|
||||
].filter((download) => download.asset),
|
||||
},
|
||||
];
|
||||
};
|
||||
const detectPlatform = () => {
|
||||
if (typeof navigator === "undefined") {
|
||||
return { label: "macOS", assetName: (version) => `openwork-mac-arm64-${version}.dmg` };
|
||||
}
|
||||
const platform = `${navigator.userAgent} ${navigator.platform}`.toLowerCase();
|
||||
if (platform.includes("mac")) {
|
||||
return { label: "macOS", assetName: (version) => `openwork-mac-arm64-${version}.dmg` };
|
||||
}
|
||||
if (platform.includes("win")) {
|
||||
return { label: "Windows", assetName: (version) => `openwork-win-x64-${version}.exe` };
|
||||
}
|
||||
if (platform.includes("linux")) {
|
||||
return { label: "Linux", assetName: (version) => `openwork-linux-x86_64-${version}.AppImage` };
|
||||
}
|
||||
return { label: "macOS", assetName: (version) => `openwork-mac-arm64-${version}.dmg` };
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
const loadStable = async () => {
|
||||
try {
|
||||
const response = await fetch(STABLE_RELEASES_URL, { headers: GITHUB_HEADERS });
|
||||
if (!response.ok) throw new Error(githubError(response, "stable releases"));
|
||||
const releases = await response.json();
|
||||
if (active) {
|
||||
setStableReleases(releases.filter((release) => !release.draft && !release.prerelease && release.tag_name.startsWith("v")));
|
||||
}
|
||||
} catch (error) {
|
||||
if (active) setStableError(error instanceof Error ? error.message : "Could not load stable releases.");
|
||||
} finally {
|
||||
if (active) setStableLoading(false);
|
||||
}
|
||||
};
|
||||
void loadStable();
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const loadAlpha = async () => {
|
||||
if (alphaRequested) return;
|
||||
setAlphaRequested(true);
|
||||
setAlphaLoading(true);
|
||||
setAlphaError("");
|
||||
try {
|
||||
const response = await fetch(ALPHA_RELEASE_URL, { headers: GITHUB_HEADERS });
|
||||
if (!response.ok) throw new Error(githubError(response, "alpha builds"));
|
||||
const release = await response.json();
|
||||
const buildsByRun = {};
|
||||
release.assets.forEach((asset) => {
|
||||
if (!asset.name.startsWith("openwork-mac-arm64-") || (!asset.name.endsWith(".dmg") && !asset.name.endsWith(".zip"))) return;
|
||||
const runMatch = asset.name.match(/-alpha\.(\d+)(?=[^0-9]|$)/);
|
||||
if (!runMatch) return;
|
||||
const run = runMatch[1];
|
||||
if (!buildsByRun[run]) buildsByRun[run] = { run: Number(run), dmg: null, zip: null };
|
||||
if (asset.name.endsWith(".dmg")) buildsByRun[run].dmg = asset;
|
||||
if (asset.name.endsWith(".zip")) buildsByRun[run].zip = asset;
|
||||
});
|
||||
setAlphaBuilds(
|
||||
Object.values(buildsByRun)
|
||||
.filter((build) => build.dmg && build.zip)
|
||||
.map((build) => ({
|
||||
...build,
|
||||
version: build.dmg.name.replace("openwork-mac-arm64-", "").replace(/\.dmg$/, ""),
|
||||
}))
|
||||
.sort((first, second) => second.run - first.run),
|
||||
);
|
||||
} catch (error) {
|
||||
setAlphaError(error instanceof Error ? error.message : "Could not load alpha builds.");
|
||||
} finally {
|
||||
setAlphaLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const latestStable = stableReleases[0];
|
||||
const detectedPlatform = detectPlatform();
|
||||
const latestVersion = latestStable ? latestStable.tag_name.slice(1) : "";
|
||||
const detectedAsset = latestStable
|
||||
? latestStable.assets.find((asset) => asset.name === detectedPlatform.assetName(latestVersion))
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div id="openwork-downloads" className="not-prose my-8">
|
||||
{latestStable ? (
|
||||
<div className="mb-8 flex flex-col gap-3 sm:flex-row sm:items-center">
|
||||
{detectedAsset ? (
|
||||
<a download href={detectedAsset.browser_download_url} className="w-fit rounded-full bg-[#011627] px-5 py-3 text-sm font-medium text-white transition-colors hover:bg-gray-800">
|
||||
Download for {detectedPlatform.label} <span aria-hidden="true">⤓</span>
|
||||
</a>
|
||||
) : null}
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Version {latestVersion} · {formatDate(latestStable.published_at)}
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="mb-5 overflow-x-auto border-b border-gray-200 dark:border-white/10" role="tablist" aria-label="OpenWork download channels">
|
||||
<div className="flex min-w-max gap-1">
|
||||
<button
|
||||
id="downloads-channel-stable"
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={activeChannel === "stable"}
|
||||
aria-controls="downloads-stable-list"
|
||||
tabIndex={activeChannel === "stable" ? 0 : -1}
|
||||
onClick={() => setActiveChannel("stable")}
|
||||
className={`border-b-2 px-4 py-2.5 text-sm font-medium ${activeChannel === "stable" ? "border-gray-950 text-gray-950 dark:border-white dark:text-white" : "border-transparent text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"}`}
|
||||
>
|
||||
Stable
|
||||
</button>
|
||||
<button
|
||||
id="downloads-channel-alpha"
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={activeChannel === "alpha"}
|
||||
aria-controls="downloads-alpha-list"
|
||||
tabIndex={activeChannel === "alpha" ? 0 : -1}
|
||||
onClick={() => {
|
||||
setActiveChannel("alpha");
|
||||
void loadAlpha();
|
||||
}}
|
||||
className={`border-b-2 px-4 py-2.5 text-sm font-medium ${activeChannel === "alpha" ? "border-gray-950 text-gray-950 dark:border-white dark:text-white" : "border-transparent text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"}`}
|
||||
>
|
||||
Alpha
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="downloads-stable-list" role="tabpanel" aria-labelledby="downloads-channel-stable" hidden={activeChannel !== "stable"}>
|
||||
{stableLoading ? <p className="m-0 text-sm text-gray-500 dark:text-gray-400">Loading stable releases…</p> : null}
|
||||
{stableError ? <p className="m-0 rounded-xl border border-red-200 bg-red-50 p-4 text-sm text-red-700 dark:border-red-900 dark:bg-red-950 dark:text-red-300">{stableError}</p> : null}
|
||||
{!stableLoading && !stableError ? (
|
||||
<div className="space-y-4">
|
||||
{stableReleases.slice(0, stableVisibleCount).map((release, index) => {
|
||||
const version = release.tag_name.slice(1);
|
||||
const groups = getStableGroups(release);
|
||||
return (
|
||||
<div key={release.id} className="rounded-2xl border border-gray-200 bg-white p-5 dark:border-white/10 dark:bg-gray-950">
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<h2 className="m-0 font-mono text-lg font-semibold text-gray-950 dark:text-white">{version}</h2>
|
||||
{index === 0 ? <span className="rounded-full border border-green-200 bg-green-50 px-2.5 py-1 text-xs font-semibold text-green-700 dark:border-green-800 dark:bg-green-950 dark:text-green-300">Latest</span> : null}
|
||||
</div>
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400">{formatDate(release.published_at)}</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 grid grid-cols-1 gap-4 sm:grid-cols-3">
|
||||
{groups.map((group) => (
|
||||
<div key={group.label}>
|
||||
<div className="mb-2 text-xs font-semibold uppercase tracking-wider text-gray-400">{group.label}</div>
|
||||
<div className="space-y-2">
|
||||
{group.downloads.map((download) => (
|
||||
<a key={download.asset.name} download href={download.asset.browser_download_url} className="flex items-center justify-between gap-3 rounded-lg border border-gray-200 px-3 py-2 text-sm font-medium text-gray-900 transition-colors hover:border-gray-400 dark:border-white/10 dark:text-white dark:hover:border-white/30">
|
||||
<span>{download.label}</span>
|
||||
<span className="flex shrink-0 items-center gap-2 text-xs font-normal text-gray-500 dark:text-gray-400">
|
||||
{formatSize(download.asset.size)} <span aria-hidden="true">⤓</span>
|
||||
</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-5 border-t border-gray-100 pt-4 dark:border-white/10">
|
||||
<a href={`https://github.com/different-ai/openwork/releases/tag/${release.tag_name}`} className="text-sm font-medium text-gray-600 hover:text-gray-950 dark:text-gray-400 dark:hover:text-white">Release notes →</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
{stableVisibleCount < stableReleases.length ? (
|
||||
<div className="mt-6 text-center">
|
||||
<button type="button" onClick={() => setStableVisibleCount((count) => count + 8)} className="rounded-full border border-gray-300 bg-white px-5 py-2.5 text-sm font-medium text-gray-900 transition-colors hover:border-gray-500 dark:border-white/20 dark:bg-gray-950 dark:text-white dark:hover:border-white/40">
|
||||
Show more versions
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div id="downloads-alpha-list" role="tabpanel" aria-labelledby="downloads-channel-alpha" hidden={activeChannel !== "alpha"}>
|
||||
<p className="mb-4 mt-0 text-sm text-gray-500 dark:text-gray-400">Pre-release builds from every merge to dev. macOS Apple Silicon only, signed and notarized. Newest 100 builds are kept.</p>
|
||||
{alphaLoading ? <p className="m-0 text-sm text-gray-500 dark:text-gray-400">Loading alpha builds…</p> : null}
|
||||
{alphaError ? <p className="m-0 rounded-xl border border-red-200 bg-red-50 p-4 text-sm text-red-700 dark:border-red-900 dark:bg-red-950 dark:text-red-300">{alphaError}</p> : null}
|
||||
{!alphaLoading && !alphaError ? (
|
||||
<div className="space-y-4">
|
||||
{alphaBuilds.slice(0, alphaVisibleCount).map((build, index) => (
|
||||
<div key={build.run} className="rounded-2xl border border-gray-200 bg-white p-5 dark:border-white/10 dark:bg-gray-950">
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<h2 className="m-0 font-mono text-lg font-semibold text-gray-950 dark:text-white">{build.version}</h2>
|
||||
{index === 0 ? <span className="rounded-full border border-green-200 bg-green-50 px-2.5 py-1 text-xs font-semibold text-green-700 dark:border-green-800 dark:bg-green-950 dark:text-green-300">Latest</span> : null}
|
||||
</div>
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400">{formatDate(build.dmg.updated_at)}</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 grid grid-cols-2 gap-3">
|
||||
{[
|
||||
{ label: "DMG", asset: build.dmg },
|
||||
{ label: "ZIP", asset: build.zip },
|
||||
].map((download) => (
|
||||
<a key={download.label} download href={download.asset.browser_download_url} className="flex items-center justify-between gap-3 rounded-lg border border-gray-200 px-3 py-2 text-sm font-medium text-gray-900 transition-colors hover:border-gray-400 dark:border-white/10 dark:text-white dark:hover:border-white/30">
|
||||
<span>{download.label}</span>
|
||||
<span className="flex shrink-0 items-center gap-2 text-xs font-normal text-gray-500 dark:text-gray-400">
|
||||
{formatSize(download.asset.size)} <span aria-hidden="true">⤓</span>
|
||||
</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
{alphaVisibleCount < alphaBuilds.length ? (
|
||||
<div className="mt-6 text-center">
|
||||
<button type="button" onClick={() => setAlphaVisibleCount((count) => count + 20)} className="rounded-full border border-gray-300 bg-white px-5 py-2.5 text-sm font-medium text-gray-900 transition-colors hover:border-gray-500 dark:border-white/20 dark:bg-gray-950 dark:text-white dark:hover:border-white/40">
|
||||
Show more versions
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user