Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions changelog/unreleased/browser-login-url.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
### English

- Browser login no longer opens the authorization page by itself. The URL is shown first, with buttons to open or copy it.

### 中文

- 浏览器登录不再自动打开授权页。地址先展示出来,可再选择打开或复制。
30 changes: 25 additions & 5 deletions frontend/src/components/AddAccountModal.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { useEffect, useLayoutEffect, useRef, useState } from 'react'
import gsap from 'gsap'
import { Button, Input, Modal, NumberField, Skeleton, TextArea } from '@heroui/react'
import { ArrowSquareOut, CaretLeft, CaretRight, CheckCircle, FileCode, Key, ShieldCheck, X } from '@phosphor-icons/react'
import { ArrowSquareOut, CaretLeft, CaretRight, Check, CheckCircle, Copy, FileCode, Key, ShieldCheck, X } from '@phosphor-icons/react'
import { copyText } from '@/lib/clipboard'
import { BrandMark } from '@/components/BrandMark'
import { ProviderMark } from '@/components/ProviderMark'
import { CompactSwitch } from '@/components/ui/CompactSwitch'
Expand Down Expand Up @@ -146,6 +147,7 @@ export function AddAccountModal({ isOpen, onClose, onAdded }: Props) {
const [phase, setPhase] = useState<Phase>('idle')
const [message, setMessage] = useState('')
const [authUrl, setAuthUrl] = useState('')
const [authCopied, setAuthCopied] = useState(false)
const [callbackUrl, setCallbackUrl] = useState('')
const stepBody = useRef<HTMLDivElement>(null)
const createdId = useRef<string>('')
Expand Down Expand Up @@ -252,6 +254,7 @@ export function AddAccountModal({ isOpen, onClose, onAdded }: Props) {
setPhase('idle')
setMessage('')
setAuthUrl('')
setAuthCopied(false)
setCallbackUrl('')
createdId.current = ''
}
Expand Down Expand Up @@ -317,7 +320,6 @@ export function AddAccountModal({ isOpen, onClose, onAdded }: Props) {
const output = await startDeviceLogin(id)
if (output.authUrl) {
setAuthUrl(output.authUrl)
window.open(output.authUrl, '_blank', 'noopener,noreferrer')
}
setMessage(t('wizardWaitingBrowser'))
for (let attempt = 0; attempt < POLL_ATTEMPTS; attempt++) {
Expand Down Expand Up @@ -675,6 +677,7 @@ export function AddAccountModal({ isOpen, onClose, onAdded }: Props) {
setTab(next as TabKey)
setMessage('')
setAuthUrl('')
setAuthCopied(false)
}
}}
ariaLabel={t('authentication')}
Expand All @@ -696,9 +699,26 @@ export function AddAccountModal({ isOpen, onClose, onAdded }: Props) {
<StatusIcon phase={phase} busy={busy} tab={tab} forTab="browser" />
<span className="text-muted">{message || t('loginOpenMsg')}</span>
</div>
<button onClick={() => window.open(authUrl, '_blank', 'noopener,noreferrer')} className="mt-2 inline-flex items-center gap-1.5 text-xs font-medium text-foreground hover:underline">
<ArrowSquareOut size={12} />{t('wizardOpenBrowser')}
</button>
<code className="mono mt-2 block break-all text-xs leading-5 text-foreground">{authUrl}</code>
<div className="mt-2 flex flex-wrap gap-2">
<Button size="sm" variant="secondary" onPress={() => window.open(authUrl, '_blank', 'noopener,noreferrer')}>
<ArrowSquareOut size={14} />{t('openAuthUrl')}
</Button>
<Button
size="sm"
variant="ghost"
onPress={() => {
void copyText(authUrl).then((ok) => {
if (!ok) return
setAuthCopied(true)
window.setTimeout(() => setAuthCopied(false), 1100)
})
}}
>
{authCopied ? <Check size={14} className="text-success" /> : <Copy size={14} />}
{authCopied ? t('copied') : t('copy')}
</Button>
</div>
</div>
) : null}
{message && !authUrl ? (
Expand Down
30 changes: 28 additions & 2 deletions frontend/src/components/account/AccountCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
ArrowClockwise,
ArrowSquareOut,
CalendarCheck,
Check,
Copy,
Cube,
DotsThreeVertical,
Expand All @@ -26,6 +27,7 @@ import {
type AccountRow,
} from '@/lib/account'
import { accountProviderLabel } from '@/lib/provider'
import { copyText } from '@/lib/clipboard'

export type AccountBusyKind = 'create' | 'import' | 'device' | 'pat' | 'callback' | 'rewarm' | 'refresh' | 'toggle' | 'delete' | 'export' | 'settings' | 'checkin'

Expand Down Expand Up @@ -100,6 +102,7 @@ export function AccountCard({
}: Props) {
const chipRef = useRef<HTMLSpanElement>(null)
const authRef = useRef<HTMLElement>(null)
const [authCopied, setAuthCopied] = useState(false)
const lastStateRef = useRef<string | null>(null)
const [, refreshCooldowns] = useState(0)
const state = accountState(account)
Expand Down Expand Up @@ -287,7 +290,6 @@ export function AccountCard({
<p className="mt-1.5 text-xs leading-5 text-muted">{t('qoderLoginHint')}</p>
<div className="mt-2.5 flex flex-wrap gap-2">
<Button size="sm" isPending={busyKind === 'device'} onPress={onDeviceLogin}><ShieldCheck size={14} />{t('startBrowserLogin')}</Button>
{authUrl ? <Button size="sm" variant="ghost" onPress={() => window.open(authUrl, '_blank', 'noopener,noreferrer')}><ArrowSquareOut size={14} />{t('open')}</Button> : null}
</div>
{account.provider === 'trae' && onSubmitCallback && onCallbackChange ? (
<div className="mt-3 space-y-2">
Expand Down Expand Up @@ -317,7 +319,31 @@ export function AccountCard({
</div>
{authUrl || note ? (
<div className="text-xs">
{authUrl ? <code className="mono block break-all text-muted">{authUrl}</code> : null}
{authUrl ? (
<div>
<div className="mb-1 text-[11px] font-medium text-muted">{t('authUrl')}</div>
<code className="mono block break-all text-foreground">{authUrl}</code>
<div className="mt-2 flex flex-wrap gap-2">
<Button size="sm" variant="secondary" onPress={() => window.open(authUrl, '_blank', 'noopener,noreferrer')}>
<ArrowSquareOut size={14} />{t('openAuthUrl')}
</Button>
<Button
size="sm"
variant="ghost"
onPress={() => {
void copyText(authUrl).then((ok) => {
if (!ok) return
setAuthCopied(true)
window.setTimeout(() => setAuthCopied(false), 1100)
})
}}
>
{authCopied ? <Check size={14} className="text-success" /> : <Copy size={14} />}
{authCopied ? t('copied') : t('copy')}
</Button>
</div>
</div>
) : null}
{note ? <p className="mt-1 text-muted">{note}</p> : null}
</div>
) : null}
Expand Down
8 changes: 4 additions & 4 deletions frontend/src/i18n/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,7 @@ export const messages: Record<Lang, Dict> = {
quotaLoading: 'Getting quota…',
quotaUnavailable: 'Quota unavailable',
authUrl: 'Auth URL',
openAuthUrl: 'Open link',
stepPatTitle: 'Personal access token',
stepPatDesc: 'Optional fallback when browser flow is inconvenient.',
pat: 'PAT',
Expand Down Expand Up @@ -605,9 +606,8 @@ export const messages: Record<Lang, Dict> = {
proxySettingsTitle: 'Outbound proxy',
proxySettingsHint: 'Set the default proxy for upstream requests. Global proxies support HTTP(S) only. SOCKS5 is available for WorkBuddy/Trae account-level proxies. Account-level settings override it.',
dropSystemPromptCreateHint: 'WorkBuddy only. Strip caller system prompts, then send an empty leading system slot so Global still accepts the request.',
wizardBrowserLead: 'Opens the authorization page. Finish in the browser and this wizard continues automatically.',
wizardBrowserLead: 'Shows the authorization URL. Open or copy it, finish in the browser, and this wizard continues automatically.',
wizardStartBrowser: 'Start browser login',
wizardOpenBrowser: 'Open authorization page',
wizardStartingSession: 'Starting the account session…',
wizardWaitingBrowser: 'Waiting for authorization in the browser…',
wizardCallbackLead: 'If the browser stays on 127.0.0.1 and this console never finishes, copy the full address-bar URL here.',
Expand Down Expand Up @@ -1012,6 +1012,7 @@ export const messages: Record<Lang, Dict> = {
quotaLoading: '额度获取中…',
quotaUnavailable: '额度不可用',
authUrl: '授权链接',
openAuthUrl: '打开链接',
stepPatTitle: '个人访问令牌',
stepPatDesc: '浏览器流程不方便时,可用 PAT 作为备选。',
pat: 'PAT',
Expand Down Expand Up @@ -1285,9 +1286,8 @@ export const messages: Record<Lang, Dict> = {
proxySettingsTitle: '统一代理出口',
proxySettingsHint: '设置所有上游请求的默认代理;全局代理支持 HTTP(S),SOCKS5 仅支持 WorkBuddy/Trae 的账号级代理;账号级代理优先于全局代理。',
dropSystemPromptCreateHint: '仅 WorkBuddy。剥离调用方系统提示词后仍补一条空的 system,避免国际版拒绝请求。',
wizardBrowserLead: '会打开授权页。在浏览器完成授权后,向导会自动继续。',
wizardBrowserLead: '会展示授权地址。打开或复制后在浏览器完成授权,向导会自动继续。',
wizardStartBrowser: '开始浏览器登录',
wizardOpenBrowser: '打开授权页',
wizardStartingSession: '正在启动账号会话…',
wizardWaitingBrowser: '正在等待浏览器完成授权…',
wizardCallbackLead: '如果浏览器停在 127.0.0.1,而控制台一直没有完成,把地址栏完整 URL 粘贴到这里。',
Expand Down
1 change: 0 additions & 1 deletion frontend/src/pages/AccountsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,6 @@ export function AccountsPage() {
const output = await startDeviceLogin(id)
if (output.authUrl) {
setUrlById((current) => ({ ...current, [id]: output.authUrl || '' }))
window.open(output.authUrl, '_blank', 'noopener,noreferrer')
}
for (let attempt = 0; attempt < 60; attempt++) {
await new Promise((resolve) => window.setTimeout(resolve, 2000))
Expand Down

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions internal/webui/static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=Outfit:wght@400;500;600;700&display=swap" rel="stylesheet" />
<script type="module" crossorigin src="/assets/index-DQkLTx6w.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-070RznMZ.css">
<script type="module" crossorigin src="/assets/index-BmBq_G52.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CmKR3T8N.css">
</head>
<body>
<div id="root"></div>
Expand Down
Loading