fix: route sign-in links by what can actually catch the callback
`isAnthropicSignInUrl` made the container the default action for every Anthropic sign-in link, justified by "the host has nothing to catch it with". That was wrong in both directions. The host does have something -- the auth bridge -- and the container side is not a general browser at all but Playwright's dashboard, whose packages and chromium are deliberately not baked into the image. So the default pointed at the one path that is uninstalled on a fresh project, on every platform, while the path that works sat behind a switch. The decision now lives in `useSignInOpenTarget`: a live auth bridge picks the host, otherwise a container that can actually launch a browser picks the container, otherwise the host. It resolves at mount rather than when a URL arrives, so the buttons do not swap under a moving mouse, and it re-decides on `auth-bridge-changed` so flipping the switch during a hanging login takes effect. A bridge with port conflicts reads as not live; an empty `active_ports` does not, since there is nothing to bridge until the CLI binds its listener and that races the URL. Both buttons still render either way -- this changes which one leads. `sanitizeRelayUrl` is byte-for-byte unchanged, so the embedded copy in web_terminal/terminal.html needs no matching edit. The host "Open" path also failed silently: `dismissUrlPrompt()` ran before `openUrl`, so the toast vanished and a rejected promise reached only the devtools console. Dismissal now happens on success only, leaving "In container" one click away after a failure, and the error surfaces through the same toast the container path already used. On Linux this catch will not fire for the common case -- `xdg-open` routinely exits 0 having done nothing -- so it complements the AppImage environment fix rather than replacing it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -105,6 +105,7 @@ describe("UrlToast", () => {
|
||||
url={SIGN_IN}
|
||||
onOpen={noop}
|
||||
onOpenInContainer={noop}
|
||||
signInDefault="container"
|
||||
onDismiss={noop}
|
||||
/>,
|
||||
);
|
||||
@@ -150,6 +151,7 @@ describe("UrlToast", () => {
|
||||
url={SIGN_IN}
|
||||
onOpen={noop}
|
||||
onOpenInContainer={noop}
|
||||
signInDefault="container"
|
||||
onDismiss={noop}
|
||||
/>,
|
||||
);
|
||||
@@ -166,10 +168,11 @@ describe("UrlToast", () => {
|
||||
|
||||
describe("Anthropic sign-in links", () => {
|
||||
// The callback listener a `claude login` is waiting on is *inside* the
|
||||
// container. Sending the user to their host browser completes the sign-in
|
||||
// and then posts the result where nothing is listening, and the terminal
|
||||
// hangs to its timeout — so for these, and only these, the container-side
|
||||
// browser leads.
|
||||
// container, so a sign-in is the one case where the host browser may be the
|
||||
// wrong lead. Whether it actually is depends on the project — a live auth
|
||||
// bridge carries the callback back, and the container-side alternative is
|
||||
// not installed on a fresh project — so the owner decides and passes
|
||||
// `signInDefault`. This component only renders the decision.
|
||||
const SIGN_IN =
|
||||
"https://claude.ai/oauth/authorize?code=true&client_id=abc&response_type=code";
|
||||
|
||||
@@ -180,12 +183,13 @@ describe("UrlToast", () => {
|
||||
.filter((t) => t === "Open" || t === "In container");
|
||||
}
|
||||
|
||||
it("puts the container browser first", () => {
|
||||
it("puts the container browser first when the caller asks for it", () => {
|
||||
render(
|
||||
<UrlToast
|
||||
url={SIGN_IN}
|
||||
onOpen={noop}
|
||||
onOpenInContainer={noop}
|
||||
signInDefault="container"
|
||||
onDismiss={noop}
|
||||
/>,
|
||||
);
|
||||
@@ -195,6 +199,42 @@ describe("UrlToast", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("leads with the host when the caller says so, without hiding the other", () => {
|
||||
// A live auth bridge, or a container with no browser installed. The pair
|
||||
// is unchanged; only the order and which one is filled.
|
||||
render(
|
||||
<UrlToast
|
||||
url={SIGN_IN}
|
||||
onOpen={noop}
|
||||
onOpenInContainer={noop}
|
||||
signInDefault="host"
|
||||
onDismiss={noop}
|
||||
/>,
|
||||
);
|
||||
expect(actions()).toEqual(["Open", "In container"]);
|
||||
expect(
|
||||
document.querySelector(URL_TOAST_PRIMARY_SELECTOR),
|
||||
).toHaveTextContent("Open");
|
||||
// Still recognised as a sign-in, so the explanation stays.
|
||||
expect(screen.getByTestId("url-toast-signin-hint")).toHaveTextContent(
|
||||
/auth bridge/i,
|
||||
);
|
||||
});
|
||||
|
||||
it("defaults to the host when the caller passes nothing", () => {
|
||||
// The safe fallback: the answer more likely to work, and the one that
|
||||
// reports its own failure.
|
||||
render(
|
||||
<UrlToast
|
||||
url={SIGN_IN}
|
||||
onOpen={noop}
|
||||
onOpenInContainer={noop}
|
||||
onDismiss={noop}
|
||||
/>,
|
||||
);
|
||||
expect(actions()).toEqual(["Open", "In container"]);
|
||||
});
|
||||
|
||||
it("keeps the host browser available as a fallback", () => {
|
||||
const onOpen = vi.fn();
|
||||
render(
|
||||
@@ -202,6 +242,7 @@ describe("UrlToast", () => {
|
||||
url={SIGN_IN}
|
||||
onOpen={onOpen}
|
||||
onOpenInContainer={noop}
|
||||
signInDefault="container"
|
||||
onDismiss={noop}
|
||||
/>,
|
||||
);
|
||||
@@ -211,12 +252,14 @@ describe("UrlToast", () => {
|
||||
|
||||
it("leaves an ordinary URL alone", () => {
|
||||
// A `gh auth login` device code, a docs page, a preview build — the host
|
||||
// browser is the right answer for all of them and stays the default.
|
||||
// browser is the right answer for all of them and stays the default,
|
||||
// whatever the project's sign-in preference happens to be.
|
||||
render(
|
||||
<UrlToast
|
||||
url="https://github.com/login/device?code=ABCD-EFGH"
|
||||
onOpen={noop}
|
||||
onOpenInContainer={noop}
|
||||
signInDefault="container"
|
||||
onDismiss={noop}
|
||||
/>,
|
||||
);
|
||||
@@ -232,6 +275,7 @@ describe("UrlToast", () => {
|
||||
url="https://claude.ai.evil.tld/oauth/authorize?x=1"
|
||||
onOpen={noop}
|
||||
onOpenInContainer={noop}
|
||||
signInDefault="container"
|
||||
onDismiss={noop}
|
||||
/>,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user