import type { Metadata } from "next";
import { Label as MuLabel } from "@unlocalhosted/metalui";
import { disconnectAppAction } from "@/app/actions/oauth";
import { ConfirmButton } from "@/components/confirm-button";
import { SparkleIcon } from "@/components/icons";
import { SettingsSection } from "@/components/metal";
import { CopyIconButton } from "@/components/settings-fields";
import { ListRow, PlateBody, PlateSection, RowGlyph } from "@/components/settings-plate";
import { Blankslate, ButtonLink, Label, PageTitle } from "@/components/ui";
import { requireViewer } from "@/lib/auth";
import { timeAgo } from "@/lib/format";
import { listConnectedApps, mcpResource } from "@/lib/oauth";
import { serverOrigin } from "@/lib/request-origin";
export const metadata: Metadata = { title: "Connected apps" };
function Command({ children }: { children: string }) {
return (
<div className="flex items-center gap-2 rounded-row recipe-well-field py-1.5 pr-1.5 pl-3.5">
<code className="flex-1 overflow-x-auto font-mono text-[12px] leading-5 whitespace-pre text-ink">{children}</code>
<CopyIconButton text={children} label="Copy" toast="Copied" />
</div>
);
}
export default async function ConnectedAppsPage() {
const viewer = await requireViewer("/settings/applications");
const [apps, url, origin] = await Promise.all([listConnectedApps(viewer.id), mcpResource(), serverOrigin()]);
return (
<div>
<PageTitle
title="Connected apps"
description="AI agents and other MCP clients you’ve allowed to work with your repositories: read code, commit, manage issues and run workflows."
/>
<div className="flex flex-col gap-7">
{apps.length === 0 ? (
<PlateSection title="Your connected apps">
<Blankslate icon={<SparkleIcon />} title="No connected apps">
Connect an agent with the server address below. It will send you here to approve it.
</Blankslate>
</PlateSection>
) : (
<SettingsSection title={`Your connected apps · ${apps.length}`}>
{apps.map((app) => (
<ListRow
key={app.id}
lead={
<RowGlyph>
<SparkleIcon />
</RowGlyph>
}
title={
<>
<span className="truncate">{app.name}</span>
<Label tone={app.permission === "write" ? "accent" : "default"}>
{app.permission === "write" ? "Read & write" : "Read-only"}
</Label>
</>
}
meta={
<>
<span className="truncate type-body text-ink2">
{app.allRepos ? "All repositories" : app.repos.map((r) => r.fullName).join(", ") || "No repositories (deleted)"}
</span>
<MuLabel variant="engraved">
Connected <b>{timeAgo(app.createdAt)}</b> ·{" "}
{app.lastUsedAt ? (
<>
Last used <b>{timeAgo(app.lastUsedAt)}</b>
</>
) : (
"Never used"
)}
</MuLabel>
</>
}
>
<ButtonLink href={`/settings/activity?agent=${app.agentKey}`} variant="invisible">
Activity
</ButtonLink>
<ConfirmButton
action={disconnectAppAction.bind(null, app.id)}
message={`Disconnect “${app.name}”? It loses access right away and has to ask you again.`}
variant="default"
>
Disconnect
</ConfirmButton>
</ListRow>
))}
</SettingsSection>
)}
<PlateSection title="Connect an agent">
<PlateBody className="max-w-2xl">
<div className="flex flex-col gap-1.5">
<p className="type-title text-ink">MCP server address</p>
<p className="type-body text-ink2">
Add it to your agent as a remote MCP server (Streamable HTTP). It signs in with OAuth: you approve it here, and choose what it may
reach.
</p>
<Command>{url}</Command>
</div>
<div className="flex flex-col gap-1.5">
<p className="type-title text-ink">Claude Code</p>
<Command>{`claude mcp add --transport http vectrum ${url}`}</Command>
<p className="type-body text-ink2">
Then run <code className="font-mono text-[12px]">/mcp</code> in Claude Code and choose <i>Authenticate</i>.
</p>
</div>
<div className="flex flex-col gap-1.5">
<p className="type-title text-ink">Command line</p>
<p className="type-body text-ink2">
<code className="font-mono text-[12px]">vectrum</code> works like GitHub's <code className="font-mono text-[12px]">gh</code>, for agents in a shell and for you:
pull requests, issues, checks, events. It signs in the same way, and can be git's credential helper, so pushes count as the agent's.
</p>
<Command>{`curl -fsSLO ${origin}/vectrum.mjs && node vectrum.mjs auth login --host ${origin}`}</Command>
</div>
<div className="flex flex-col gap-1.5">
<p className="type-title text-ink">Reading pages</p>
<p className="type-body text-ink2">
Every page is also Markdown: send <code className="font-mono text-[12px]">Accept: text/markdown</code> or add{" "}
<code className="font-mono text-[12px]">?format=md</code>. <code className="font-mono text-[12px]">{origin}/llms.txt</code> describes this server for agents.
</p>
</div>
<div className="flex flex-col gap-1.5">
<p className="type-title text-ink">Agents without OAuth</p>
<p className="type-body text-ink2">
Send an API token from <i>Settings → API tokens</i> instead, as <code className="font-mono text-[12px]">Authorization: Bearer vec_…</code>.
</p>
</div>
</PlateBody>
</PlateSection>
</div>
</div>
);
}