From cd49dab5a3a4cbea6c1f47cb6bdc87b75a0bd974 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 15 Jul 2026 16:09:30 +0000 Subject: [PATCH] feat(login): open the login URL in an OAuth-style popup, not an iframe claude.com refuses to be embedded in an iframe (X-Frame-Options), so the inline frame just showed a blocked page. Replace it with a small popup window, like a "Sign in with Google" flow: the "Log in to Claude" click opens a blank popup (within the user gesture, so it isn't popup-blocked) and, once login_start returns the URL, the popup is navigated to it. Buttons to reopen the window or open the URL in a new tab remain as fallbacks, and the popup is closed on success/error. README updated to match. Rebuilt static export. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01YKVyBmKvWDVgrFC9WER2f2 --- README.md | 18 ++-- frontend/components/sections/LoginSection.tsx | 102 ++++++++++++------ src/handler/api/static/404.html | 2 +- .../_buildManifest.js | 0 .../_ssgManifest.js | 0 ...3ffe4c78c3.js => page-5b19e394a5d5460f.js} | 2 +- src/handler/api/static/index.html | 2 +- src/handler/api/static/index.txt | 4 +- 8 files changed, 84 insertions(+), 46 deletions(-) rename src/handler/api/static/_next/static/{J55muUx2ya8M2SQERVlYt => UFaQ4h6X_9jNfr8PwK2IS}/_buildManifest.js (100%) rename src/handler/api/static/_next/static/{J55muUx2ya8M2SQERVlYt => UFaQ4h6X_9jNfr8PwK2IS}/_ssgManifest.js (100%) rename src/handler/api/static/_next/static/chunks/app/{page-aaee823ffe4c78c3.js => page-5b19e394a5d5460f.js} (77%) diff --git a/README.md b/README.md index 0fe4532..51d3321 100644 --- a/README.md +++ b/README.md @@ -243,13 +243,17 @@ pane logs it in from the browser — the same command-queue handoff every other action uses: 1. **Log in to Claude** enqueues a `login_start` command. The worker opens `claude` in a - dedicated tmux session in the control container, sends `/login`, selects the **Claude - account with subscription** option, and scrapes the pane for the `claude.com` - authorization URL — returned in the command result. -2. The UI opens that URL in an embedded frame (with a new-tab link as a fallback, since - claude.com may refuse to be framed). You authorize and Claude gives you a code. -3. **Finish login** enqueues a `login_submit` command carrying the code; the worker feeds - it into the still-open session, waits for claude to exchange it, and reports success. + dedicated (wide) tmux session in the control container, navigates whatever onboarding a + fresh `claude` shows (theme picker, folder-trust) to the **Claude account with + subscription** login, and scrapes the pane for the `claude.com` authorization URL — + returned in the command result. +2. The UI opens that URL in a small **OAuth-style popup window** (like "Sign in with …"; + claude.com refuses to be embedded in an iframe, so a popup is the right surface), with + a new-tab link as a fallback. You authorize there and Claude gives you a code. +3. **Finish login** enqueues a `login_submit` command carrying the code; the worker pastes + it into the still-open session and presses Enter separately (a long code plus an + immediate Enter races the TUI and never submits), then confirms by watching claude write + its credentials. The login session lives in the control container, and Claude's credentials land under the `handler` user's home on the `/var/lib/handler` volume — so the login **persists** across diff --git a/frontend/components/sections/LoginSection.tsx b/frontend/components/sections/LoginSection.tsx index 8dfc582..f6c1ae4 100644 --- a/frontend/components/sections/LoginSection.tsx +++ b/frontend/components/sections/LoginSection.tsx @@ -1,24 +1,64 @@ /* Claude Login — drive the bundled `claude /login` OAuth flow on the host from the web UI. * - * Click "Log in to Claude" → the worker opens `claude /login` in the control container, - * selects the subscription account, and returns the claude.com authorization URL. That URL - * is shown in an embedded frame (and as a new-tab link, since claude.com may refuse to be - * framed); after authorizing, paste the code back to finish. All state lives in the store's - * `claudeLogin` machine (login_start / login_submit commands). */ + * 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 { useState } from "react"; +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 LoginSection() { const s = useDashboard(); const { status, url, message } = s.claudeLogin; const [code, setCode] = useState(""); + const popupRef = useRef(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(""); @@ -50,48 +90,42 @@ export function LoginSection() { ) : !awaiting ? (
- {status === "error" && ( - )}
) : ( <> + + 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. +
- - Open login page in a new tab ↗ - - + {url && ( + + Open in a new tab + + )} +
-
-