mirror of
https://github.com/0xWheatyz/handler.git
synced 2026-08-30 04:36:24 +00:00
07d8c3aa19
The dashboard's Claude page now manages the whole Claude Code install agents run on, not just the account login: - Skills: operator-authored SKILL.md rows, synced to each worker's user-level ~/.claude/skills at every launch. Managed dirs carry a .handler-managed marker so deletions in the UI propagate while hand-installed skills survive. - Connectors: MCP servers (stdio/http/sse) written per-launch as .claude/mcp-servers.json and passed to claude via --mcp-config, so nothing lands in the managed repo's tracked tree. - Plugins: marketplace-pinned plugins folded into generated settings as extraKnownMarketplaces + enabledPlugins, installing on boot of headless runs. - Permissions: defaultMode override plus allow/deny/ask rules merged over the env baseline into every generated settings.json. All of it is plain DB state (new claude_skills / claude_connectors / claude_plugins / claude_config tables, migration 0010) edited through the new admin-gated /claude/* API routes and applied by the control container at spawn and resume — changes reach the next launch of every agent with no redeploy. The login flow moved into the page's Account tab unchanged; /login redirects to /claude for old bookmarks. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019f42XmjtVsc3zQ9Dhn6DqZ
151 lines
5.6 KiB
TypeScript
151 lines
5.6 KiB
TypeScript
/* Claude account login panel (the Account tab of the Claude page) — drives the bundled
|
|
* `claude /login` OAuth flow on the host from the web UI.
|
|
*
|
|
* Click "Log in to Claude" → a small OAuth-style popup window opens (like "Sign in with
|
|
* Google") and the worker drives `claude /login` in the control container, selecting the
|
|
* subscription account and returning the claude.com authorization URL, which we point the
|
|
* popup at. (claude.com refuses to be embedded in an iframe, so a popup — not an inline
|
|
* frame — is the right surface.) You authorize there, copy the code, and paste it back to
|
|
* finish. All state lives in the store's `claudeLogin` machine (login_start / login_submit
|
|
* commands). */
|
|
"use client";
|
|
|
|
import { useEffect, useRef, useState } from "react";
|
|
import { useDashboard } from "@/components/store";
|
|
import { Button, Callout, Input } from "@/components/ui";
|
|
|
|
function openLoginPopup(url: string): Window | null {
|
|
const w = 520;
|
|
const h = 760;
|
|
// Center over the current window; specifying a size makes browsers open a popup window
|
|
// (the "Sign in with …" surface) rather than a new tab.
|
|
const left = window.screenX + Math.max(0, (window.outerWidth - w) / 2);
|
|
const top = window.screenY + Math.max(0, (window.outerHeight - h) / 2);
|
|
return window.open(
|
|
url,
|
|
"claude-login",
|
|
`popup=yes,width=${w},height=${h},left=${Math.round(left)},top=${Math.round(top)}`,
|
|
);
|
|
}
|
|
|
|
export function ClaudeLoginPanel() {
|
|
const s = useDashboard();
|
|
const { status, url, message } = s.claudeLogin;
|
|
const [code, setCode] = useState("");
|
|
const popupRef = useRef<Window | null>(null);
|
|
|
|
const busy = status === "starting" || status === "submitting";
|
|
const awaiting = status === "awaiting" || status === "submitting";
|
|
|
|
// Open a blank popup *within the click* (below) so browsers don't block it; once
|
|
// login_start returns the URL, navigate that same popup to it.
|
|
useEffect(() => {
|
|
if (status === "awaiting" && url && popupRef.current && !popupRef.current.closed) {
|
|
try {
|
|
popupRef.current.location.href = url;
|
|
} catch {
|
|
/* cross-origin after navigation — expected, ignore */
|
|
}
|
|
}
|
|
if (status === "done" || status === "error") {
|
|
popupRef.current?.close();
|
|
popupRef.current = null;
|
|
}
|
|
}, [status, url]);
|
|
|
|
const start = () => {
|
|
// Open the popup now, on the user gesture, to a lightweight loading page; the effect
|
|
// above redirects it to the real URL when it arrives.
|
|
popupRef.current = openLoginPopup("about:blank");
|
|
void s.startClaudeLogin();
|
|
};
|
|
|
|
const submit = async () => {
|
|
const ok = await s.submitClaudeCode(code);
|
|
if (ok) setCode("");
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<div className="faint" style={{ fontSize: "var(--text-sm)" }}>
|
|
Log Claude Code in on the host so agents can run. This drives{" "}
|
|
<span className="mono">claude /login</span> in the control container and picks the
|
|
Claude account with a subscription.
|
|
</div>
|
|
|
|
<div style={{ display: "flex", flexDirection: "column", gap: 16, marginTop: 14 }}>
|
|
{message && (
|
|
<Callout tone={status === "error" ? "danger" : status === "done" ? "success" : "info"}>
|
|
{message}
|
|
</Callout>
|
|
)}
|
|
|
|
{status === "done" ? (
|
|
<div>
|
|
<Button variant="secondary" onClick={s.resetClaudeLogin}>
|
|
Log in again
|
|
</Button>
|
|
</div>
|
|
) : !awaiting ? (
|
|
<div className="hstack" style={{ gap: 10 }}>
|
|
<Button variant="primary" disabled={busy} onClick={start}>
|
|
{status === "starting" ? "Starting…" : "Log in to Claude"}
|
|
</Button>
|
|
{status === "error" && (
|
|
<Button variant="ghost" disabled={busy} onClick={start}>
|
|
Retry
|
|
</Button>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<>
|
|
<Callout tone="info">
|
|
A Claude sign-in window should have opened. Authorize there, copy the code
|
|
Claude shows you, and paste it below. If the window didn't open (popups
|
|
blocked), use the button.
|
|
</Callout>
|
|
<div className="hstack" style={{ gap: 10, flexWrap: "wrap" }}>
|
|
<Button
|
|
variant="secondary"
|
|
disabled={!url}
|
|
onClick={() => {
|
|
if (url) popupRef.current = openLoginPopup(url);
|
|
}}
|
|
>
|
|
Open Claude sign-in window ↗
|
|
</Button>
|
|
{url && (
|
|
<a className="btn btn-ghost" href={url} target="_blank" rel="noopener noreferrer">
|
|
Open in a new tab
|
|
</a>
|
|
)}
|
|
<Button variant="ghost" disabled={busy} onClick={start}>
|
|
Restart
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="hstack" style={{ gap: 10, alignItems: "flex-end", flexWrap: "wrap" }}>
|
|
<div style={{ flex: "1 1 320px" }}>
|
|
<Input
|
|
label="Authorization code"
|
|
value={code}
|
|
onChange={setCode}
|
|
placeholder="Paste the code from claude.com"
|
|
disabled={status === "submitting"}
|
|
/>
|
|
</div>
|
|
<Button
|
|
variant="primary"
|
|
disabled={status === "submitting" || !code.trim()}
|
|
onClick={submit}
|
|
>
|
|
{status === "submitting" ? "Submitting…" : "Finish login"}
|
|
</Button>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
</>
|
|
);
|
|
}
|