/login can hand you the URL instead of opening it here

Opening the link opens it in THIS browser — already signed into whichever account you are trying to move
away from. Copying it is how you authorise as somebody else: another browser profile, another browser, or
your phone. This dialog exists for a machine with two Anthropic accounts on it, so that is the common case
rather than the awkward one.

Uses helpers/clipboard, which falls back to execCommand when navigator.clipboard is absent, and reports a
failure rather than showing "Copied" over an empty clipboard.

Corrected a claim in that helper's own comment while touching it: it said Officer is reached over the
tailnet on plain http, as though that were the whole story. The owner browses over https and did when it
was written. The http origins are the other ways in, and an install with no certificate has no other way
in at all — which is why the fallback stays.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-21 21:02:57 +00:00
co-authored by Claude Opus 5
parent 31cdb93691
commit 58615ee5e7
2 changed files with 41 additions and 15 deletions
+6 -3
View File
@@ -7,9 +7,12 @@
* is not localhost the whole object is undefined, so `navigator.clipboard.writeText`
* throws `TypeError: Cannot read properties of undefined`.
*
* Officer is reached over the tailnet — `http://officer-dev:9000` — which is
* neither https nor localhost. Twenty call sites were affected, in three states
* that all looked fine in review:
* Officer is reachable over plain http — `http://officer-dev:9000` on the tailnet —
* which is neither https nor localhost. The owner's own browsing is over https
* (`platform.officer.dev`) and was already https when this was written; the http
* origins are the other ways in, and an install with no certificate has no other
* way in at all. Twenty call sites were affected, in three states that all looked
* fine in review:
*
* bare `navigator.clipboard.writeText(x)` threw, killing the handler
* `navigator.clipboard?.writeText(x)` did nothing, silently
@@ -1,9 +1,10 @@
import { useEffect, useRef, useState } from 'react';
import { Check, ExternalLink, Loader2 } from 'lucide-react';
import { Check, Copy, ExternalLink, Loader2 } from 'lucide-react';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { useClient } from 'hooks/useClient';
import { errorText } from 'helpers/error-text';
import { copyToClipboard } from 'helpers/clipboard';
/*
`/login` — the same flow as the terminal, without the terminal.
@@ -29,6 +30,7 @@ export const ClaudeLoginDialog = ({ open, onOpenChange }: Props) => {
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
const [done, setDone] = useState(false);
const [copied, setCopied] = useState(false);
const codeRef = useRef<HTMLInputElement>(null);
// Start on open, and abandon on close. The server holds a live `claude` process per pending login, so a
@@ -101,17 +103,38 @@ export const ClaudeLoginDialog = ({ open, onOpenChange }: Props) => {
<p className="text-sm text-muted-foreground">
Open this, authorise, and paste back the code it gives you.
</p>
{/* A real anchor. Every link here opens a new tab, which is what you want for this one —
losing the dialog would lose the process holding the other half of the exchange. */}
<a
href={started.url}
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-1.5 rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:opacity-90"
>
Authorise in claude.com
<ExternalLink className="h-3.5 w-3.5" />
</a>
<div className="flex items-center gap-2">
{/* A real anchor. Every link here opens a new tab, which is what you want for this one —
losing the dialog would lose the process holding the other half of the exchange. */}
<a
href={started.url}
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-1.5 rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:opacity-90"
>
Authorise in claude.com
<ExternalLink className="h-3.5 w-3.5" />
</a>
{/* Copy, because opening here opens in THIS browser — already signed into whichever account
you are trying to move away from. Pasting into another profile or another device is how
you sign in as somebody else, and this dialog exists precisely for the machine with two
Anthropic accounts on it. */}
<button
type="button"
onClick={() => {
void copyToClipboard(started.url).then((ok) => {
setCopied(ok);
if (ok) setTimeout(() => setCopied(false), 2_000);
else setError('could not reach the clipboard — select the link and copy it by hand');
});
}}
title="Copy the link, to open it in another browser or on your phone"
className="inline-flex items-center gap-1.5 rounded-md border border-border px-3 py-1.5 text-sm font-medium hover:bg-foreground/5"
>
{copied ? <Check className="h-3.5 w-3.5 text-success" /> : <Copy className="h-3.5 w-3.5" />}
{copied ? 'Copied' : 'Copy link'}
</button>
</div>
</div>
<div className="space-y-1">