feat(FN-3617): align mailbox modal css with design tokens

Updated MailboxModal CSS to use design tokens instead of hardcoded values, and adjusted the co-located test to match.

Fusion-Task-Id: FN-3617
This commit is contained in:
Fusion
2026-05-06 16:08:21 -07:00
committed by gsxdsm
parent 2140148fe2
commit cabd6be84b
9 changed files with 264 additions and 79 deletions

View File

@@ -10,13 +10,13 @@
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
gap: var(--space-md);
}
.mailbox-title {
display: flex;
align-items: center;
gap: 8px;
gap: var(--space-sm);
font-weight: 600;
font-size: 1rem;
}
@@ -38,7 +38,7 @@
.mailbox-header-actions {
display: flex;
align-items: center;
gap: 8px;
gap: var(--space-sm);
}
.mailbox-tabs {
@@ -77,7 +77,7 @@
min-width: 16px;
height: 16px;
padding: 0 4px;
border-radius: 8px;
border-radius: var(--radius-md);
background: var(--color-error);
color: var(--fab-text);
font-size: 0.65rem;
@@ -87,9 +87,9 @@
.mailbox-content {
flex: 1;
min-height: 0;
padding: 16px;
padding: var(--space-lg);
overflow-y: auto;
max-height: calc(80vh - 140px);
max-height: calc(80vh - var(--header-height) - var(--space-2xl) - var(--space-xl));
position: relative;
}
@@ -104,10 +104,10 @@
flex-direction: column;
align-items: center;
justify-content: center;
padding: 48px 16px;
padding: calc(var(--space-2xl) + var(--space-lg)) var(--space-lg);
color: var(--text-muted);
text-align: center;
gap: 12px;
gap: var(--space-md);
}
.mailbox-empty p {
@@ -118,11 +118,11 @@
.mailbox-item {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 12px;
border-radius: 8px;
gap: var(--space-md);
padding: var(--space-md);
border-radius: var(--radius-md);
cursor: pointer;
transition: background-color 0.1s;
transition: background-color var(--transition-instant);
}
.mailbox-item:hover {
@@ -155,7 +155,7 @@
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
gap: var(--space-sm);
margin-bottom: 2px;
}
@@ -202,11 +202,11 @@
.mailbox-conversation-group {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 12px;
border-radius: 8px;
gap: var(--space-md);
padding: var(--space-md);
border-radius: var(--radius-md);
cursor: pointer;
transition: background-color 0.1s;
transition: background-color var(--transition-instant);
position: relative;
}
@@ -239,21 +239,21 @@
.mailbox-message-detail {
display: flex;
flex-direction: column;
gap: 16px;
gap: var(--space-lg);
}
.mailbox-message-detail-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
gap: var(--space-md);
flex-wrap: wrap;
}
.mailbox-message-detail-meta {
display: flex;
align-items: center;
gap: 8px;
gap: var(--space-sm);
font-size: 0.8rem;
color: var(--text-muted);
}
@@ -261,7 +261,7 @@
.mailbox-message-detail-actions {
display: flex;
align-items: center;
gap: 8px;
gap: var(--space-sm);
}
.mailbox-message-type {
@@ -282,7 +282,7 @@
gap: 24px;
padding: 12px;
background: var(--bg-tertiary);
border-radius: 8px;
border-radius: var(--radius-md);
}
.mailbox-participant {
@@ -304,9 +304,9 @@
}
.mailbox-message-body {
padding: 16px;
padding: var(--space-lg);
background: var(--bg-secondary);
border-radius: 8px;
border-radius: var(--radius-md);
font-size: 0.9rem;
line-height: 1.5;
white-space: pre-wrap;
@@ -329,7 +329,7 @@
.mailbox-conversation {
display: flex;
flex-direction: column;
gap: 8px;
gap: var(--space-sm);
}
.mailbox-conversation-label {
@@ -340,7 +340,7 @@
.mailbox-conversation-msg {
padding: 8px 12px;
border-radius: 8px;
border-radius: var(--radius-md);
background: var(--bg-tertiary);
border-left: 3px solid transparent;
}
@@ -354,7 +354,7 @@
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
gap: var(--space-sm);
margin-bottom: 4px;
font-size: 0.8rem;
color: var(--text-muted);
@@ -371,7 +371,7 @@
.mailbox-agents {
display: flex;
flex-direction: column;
gap: 12px;
gap: var(--space-md);
min-height: 300px;
}
@@ -382,7 +382,7 @@
.mailbox-agents-header {
display: flex;
align-items: center;
gap: 12px;
gap: var(--space-md);
flex-wrap: wrap;
}
@@ -441,13 +441,13 @@
.mailbox-skeleton {
display: flex;
flex-direction: column;
gap: 8px;
gap: var(--space-sm);
}
.mailbox-skeleton-item {
display: flex;
align-items: flex-start;
gap: 12px;
gap: var(--space-md);
padding: 12px;
}
@@ -557,7 +557,7 @@
@media (max-width: 768px) {
.mailbox-modal .mailbox-header {
flex-wrap: wrap;
gap: 8px;
gap: var(--space-sm);
}
.mailbox-modal .mailbox-title {
@@ -605,13 +605,14 @@
.mailbox-modal .mailbox-tab {
flex-shrink: 0;
padding: 8px 12px;
padding: var(--space-sm) var(--space-md);
font-size: 0.8rem;
}
.mailbox-modal .mailbox-content {
max-height: calc(100dvh - 120px);
padding: 12px;
max-height: calc(100dvh - var(--header-height) - var(--space-2xl) - var(--space-xl));
padding: var(--space-md);
padding-bottom: calc(var(--space-md) + env(safe-area-inset-bottom, 0px) + var(--standalone-bottom-gap));
}
.mailbox-modal .mailbox-message-detail-header {
@@ -625,11 +626,11 @@
.mailbox-modal .mailbox-message-participants {
flex-direction: column;
gap: 8px;
gap: var(--space-sm);
}
.mailbox-modal .mailbox-conversation-msg {
padding: 6px 10px;
padding: var(--space-xs) var(--space-md);
}
.mailbox-modal .mailbox-agent-select {
@@ -641,7 +642,7 @@
}
.mailbox-modal .mailbox-empty {
padding: 32px 12px;
padding: var(--space-2xl) var(--space-md);
}
.mailbox-modal .mailbox-agent-subtabs {
@@ -652,13 +653,13 @@
flex: 1;
justify-content: center;
min-height: 36px;
padding: 8px 12px;
padding: var(--space-sm) var(--space-md);
}
/* Mailbox View (full-page) mobile overrides */
.mailbox-view .mailbox-header {
flex-wrap: wrap;
gap: 8px;
gap: var(--space-sm);
}
.mailbox-view .mailbox-title {
@@ -677,7 +678,7 @@
.mailbox-view .mailbox-tab {
flex-shrink: 0;
padding: 8px 12px;
padding: var(--space-sm) var(--space-md);
font-size: 0.8rem;
}
@@ -686,7 +687,7 @@
overflow-y: auto;
padding: var(--space-md);
/* Account for mobile nav bar at bottom */
padding-bottom: calc(var(--mobile-nav-height) + var(--standalone-bottom-gap) + var(--space-lg));
padding-bottom: calc(var(--mobile-nav-height) + env(safe-area-inset-bottom, 0px) + var(--standalone-bottom-gap) + var(--space-lg));
}
.mailbox-view .mailbox-split-layout {
@@ -714,11 +715,11 @@
.mailbox-view .mailbox-message-participants {
flex-direction: column;
gap: 8px;
gap: var(--space-sm);
}
.mailbox-view .mailbox-conversation-msg {
padding: 6px 10px;
padding: var(--space-xs) var(--space-md);
}
.mailbox-view .mailbox-agent-select {
@@ -730,7 +731,7 @@
}
.mailbox-view .mailbox-empty {
padding: 32px 12px;
padding: var(--space-2xl) var(--space-md);
}
.mailbox-view .mailbox-agent-subtabs {
@@ -741,7 +742,7 @@
flex: 1;
justify-content: center;
min-height: 36px;
padding: 8px 12px;
padding: var(--space-sm) var(--space-md);
}
/* Message Composer mobile overrides */
@@ -788,13 +789,13 @@
.message-composer-body {
display: flex;
flex-direction: column;
gap: 12px;
gap: var(--space-md);
}
.message-composer-field {
display: flex;
align-items: flex-start;
gap: 8px;
gap: var(--space-sm);
}
.message-composer-field--content {
@@ -905,10 +906,10 @@
.message-composer-error {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
gap: var(--space-sm);
padding: var(--space-sm) var(--space-md);
background: color-mix(in srgb, var(--color-error) 10%, transparent);
border-radius: 6px;
border-radius: var(--radius-sm);
color: var(--color-error);
font-size: 0.85rem;
}
@@ -917,6 +918,6 @@
display: flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
gap: var(--space-sm);
}

View File

@@ -933,23 +933,23 @@ describe("MailboxModal", () => {
expect(mailboxMobileSection).toContain(".mailbox-modal .mailbox-tabs::-webkit-scrollbar");
expect(mailboxMobileSection).toContain("display: none;");
expect(mailboxMobileSection).toContain(".mailbox-modal .mailbox-tab");
expect(mailboxMobileSection).toContain("padding: 8px 12px;");
expect(mailboxMobileSection).toContain("padding: var(--space-sm) var(--space-md);");
expect(mailboxMobileSection).toContain("font-size: 0.8rem;");
expect(mailboxMobileSection).toContain("max-height: calc(100dvh - 120px);");
expect(mailboxMobileSection).toContain("max-height: calc(100dvh - var(--header-height) - var(--space-2xl) - var(--space-xl));");
expect(mailboxMobileSection).toContain(".mailbox-modal .mailbox-message-detail-header");
expect(mailboxMobileSection).toContain("flex-direction: column;");
expect(mailboxMobileSection).toContain("align-items: flex-start;");
expect(mailboxMobileSection).toContain(".mailbox-modal .mailbox-message-detail-actions");
expect(mailboxMobileSection).toContain(".mailbox-modal .mailbox-message-participants");
expect(mailboxMobileSection).toContain("gap: 8px;");
expect(mailboxMobileSection).toContain("gap: var(--space-sm);");
expect(mailboxMobileSection).toContain(".mailbox-modal .mailbox-conversation-msg");
expect(mailboxMobileSection).toContain("padding: 6px 10px;");
expect(mailboxMobileSection).toContain("padding: var(--space-xs) var(--space-md);");
expect(mailboxMobileSection).toContain(".mailbox-modal .mailbox-agent-select");
expect(mailboxMobileSection).toContain("max-width: 100%;");
expect(mailboxMobileSection).toContain(".mailbox-modal .mailbox-agents");
expect(mailboxMobileSection).toContain("min-height: 200px;");
expect(mailboxMobileSection).toContain(".mailbox-modal .mailbox-empty");
expect(mailboxMobileSection).toContain("padding: 32px 12px;");
expect(mailboxMobileSection).toContain("padding: var(--space-2xl) var(--space-md);");
});
it("renders detail-view structural hooks targeted by mobile overrides", async () => {

View File

@@ -10,6 +10,7 @@ const mockFetchAuthStatus = vi.fn();
const mockLoginProvider = vi.fn();
const mockLogoutProvider = vi.fn();
const mockCancelProviderLogin = vi.fn();
const mockSubmitProviderManualCode = vi.fn();
const mockSaveApiKey = vi.fn();
const mockClearApiKey = vi.fn();
const mockFetchModels = vi.fn();
@@ -24,6 +25,7 @@ vi.mock("../../api", () => ({
loginProvider: (...args: unknown[]) => mockLoginProvider(...args),
logoutProvider: (...args: unknown[]) => mockLogoutProvider(...args),
cancelProviderLogin: (...args: unknown[]) => mockCancelProviderLogin(...args),
submitProviderManualCode: (...args: unknown[]) => mockSubmitProviderManualCode(...args),
saveApiKey: (...args: unknown[]) => mockSaveApiKey(...args),
clearApiKey: (...args: unknown[]) => mockClearApiKey(...args),
fetchModels: (...args: unknown[]) => mockFetchModels(...args),
@@ -183,6 +185,7 @@ beforeEach(() => {
mockLoginProvider.mockResolvedValue({ url: "https://auth.example.com/login" });
mockLogoutProvider.mockResolvedValue({ success: true });
mockCancelProviderLogin.mockResolvedValue({ success: true, cancelled: true });
mockSubmitProviderManualCode.mockResolvedValue({ success: true, submitted: true });
mockSaveApiKey.mockResolvedValue({ success: true });
mockClearApiKey.mockResolvedValue({ success: true });
// Default to no persisted state (start at ai-setup)
@@ -652,6 +655,68 @@ describe("ModelOnboardingModal", () => {
});
});
it("shows Anthropic pasted-code form and submits manual code", async () => {
const mockWindowOpen = vi.fn();
vi.spyOn(window, "open").mockImplementation(mockWindowOpen);
mockLoginProvider.mockResolvedValueOnce({
url: "https://claude.ai/oauth/authorize",
manualCode: {
prompt: "Paste the final redirect URL or authorization code",
placeholder: "http://localhost:*/callback?code=...&state=... or just the code",
helpText: "After Claude sign-in, copy the full browser URL (or just the code) and paste it here to finish login from this dashboard host.",
},
});
render(<ModelOnboardingModal onComplete={vi.fn()} addToast={vi.fn()} projectId="proj_123" />);
await waitFor(() => {
expect(screen.getByText("Login")).toBeTruthy();
});
fireEvent.click(screen.getByText("Login"));
const prompt = await screen.findByText("Paste the final redirect URL or authorization code");
const card = prompt.closest(".onboarding-provider-card") as HTMLElement;
const textbox = within(card).getByRole("textbox");
fireEvent.change(textbox, { target: { value: "anthropic-code" } });
fireEvent.click(within(card).getByRole("button", { name: "Submit code" }));
await waitFor(() => {
expect(mockSubmitProviderManualCode).toHaveBeenCalledWith("anthropic", "anthropic-code");
});
expect(mockWindowOpen).toHaveBeenCalled();
});
it("keeps OpenAI Codex manual-code UX available in onboarding", async () => {
mockFetchAuthStatus.mockResolvedValueOnce({
providers: [{ id: "openai-codex", name: "OpenAI Codex", authenticated: false, type: "oauth" }],
});
mockLoginProvider.mockResolvedValueOnce({
url: "https://auth.openai.com/oauth/authorize",
manualCode: {
prompt: "Paste the final redirect URL or authorization code",
placeholder: "http://localhost:1455/auth/callback?code=...&state=... or just the code",
helpText: "After sign-in, OpenAI may redirect to a localhost callback that cannot open from this dashboard host. Copy the full browser URL from the address bar and paste it here.",
},
});
render(<ModelOnboardingModal onComplete={vi.fn()} addToast={vi.fn()} projectId="proj_123" />);
await waitFor(() => {
expect(screen.getByRole("button", { name: /Advanced provider settings/ })).toBeTruthy();
});
fireEvent.click(screen.getByRole("button", { name: /Advanced provider settings/ }));
await waitFor(() => {
expect(screen.getByText("OpenAI Codex")).toBeTruthy();
});
fireEvent.click(screen.getByText("Login"));
expect(await screen.findByText("Paste the final redirect URL or authorization code")).toBeTruthy();
expect(screen.getByText(/OpenAI may redirect to a localhost callback/)).toBeTruthy();
});
it("saves API key when Save is clicked", async () => {
render(<ModelOnboardingModal onComplete={vi.fn()} addToast={vi.fn()} projectId="proj_123" />);
@@ -1376,7 +1441,7 @@ describe("ModelOnboardingModal", () => {
const badge = screen.getByTestId("github-status-badge");
expect(badge).toHaveTextContent("✗ Connection failed");
expect(badge).toHaveClass("retry");
});
}, { timeout: 3000 });
expect(screen.getByText("Connection failed or timed out.")).toBeTruthy();
expect(screen.getByRole("button", { name: "Retry" })).toBeTruthy();

View File

@@ -16,6 +16,7 @@ const mockLoginProvider = vi.fn();
const mockLogoutProvider = vi.fn();
const mockCancelProviderLogin = vi.fn();
const mockSaveApiKey = vi.fn();
const mockSubmitProviderManualCode = vi.fn();
const mockFetchModels = vi.fn();
const mockFetchCustomProviders = vi.fn();
const mockCreateCustomProvider = vi.fn();
@@ -69,6 +70,7 @@ vi.mock("../../api", async (importOriginal) => {
logoutProvider: (...args: unknown[]) => mockLogoutProvider(...args),
cancelProviderLogin: (...args: unknown[]) => mockCancelProviderLogin(...args),
saveApiKey: (...args: unknown[]) => mockSaveApiKey(...args),
submitProviderManualCode: (...args: unknown[]) => mockSubmitProviderManualCode(...args),
fetchModels: (...args: unknown[]) => mockFetchModels(...args),
fetchCustomProviders: (...args: unknown[]) => mockFetchCustomProviders(...args),
createCustomProvider: (...args: unknown[]) => mockCreateCustomProvider(...args),
@@ -266,6 +268,7 @@ describe("SettingsModal", () => {
mockDeleteCustomProvider.mockResolvedValue(undefined);
mockCancelProviderLogin.mockResolvedValue({ success: true, cancelled: true });
mockSaveApiKey.mockResolvedValue(undefined);
mockSubmitProviderManualCode.mockResolvedValue({ success: true, submitted: true });
mockTestNotification.mockResolvedValue({ success: true });
mockFetchBackups.mockResolvedValue({ backups: [], totalSize: 0 });
mockFetchMemoryFiles.mockResolvedValue({
@@ -1048,6 +1051,36 @@ describe("SettingsModal", () => {
expect(openSpy).toHaveBeenCalled();
});
it("renders Anthropic pasted-code form when login response includes manualCode", async () => {
const openSpy = vi.spyOn(window, "open").mockImplementation(() => null);
mockFetchAuthStatus.mockResolvedValueOnce({
providers: [{ id: "anthropic", name: "Anthropic", authenticated: false, type: "oauth" }],
});
mockLoginProvider.mockResolvedValueOnce({
url: "https://claude.ai/oauth/authorize",
manualCode: {
prompt: "Paste the final redirect URL or authorization code",
placeholder: "http://localhost:*/callback?code=...&state=... or just the code",
helpText: "After Claude sign-in, copy the full browser URL (or just the code) and paste it here to finish login from this dashboard host.",
},
});
renderModal();
await waitForSettingsModalReady();
const anthropicCard = screen.getByTestId("auth-provider-icon-anthropic").closest(".auth-provider-card") as HTMLElement;
await userEvent.click(within(anthropicCard).getByRole("button", { name: "Login" }));
expect(await within(anthropicCard).findByText("Paste the final redirect URL or authorization code")).toBeInTheDocument();
await userEvent.type(within(anthropicCard).getByRole("textbox"), "anthropic-code");
await userEvent.click(within(anthropicCard).getByRole("button", { name: "Submit code" }));
await waitFor(() => {
expect(mockSubmitProviderManualCode).toHaveBeenCalledWith("anthropic", "anthropic-code");
});
expect(openSpy).toHaveBeenCalled();
});
it("shows cancel action for server-reported pending oauth login", async () => {
mockFetchAuthStatus.mockResolvedValue({
providers: [{ id: "github-copilot", name: "GitHub Copilot", authenticated: false, type: "oauth", loginInProgress: true }],

View File

@@ -1163,6 +1163,36 @@ describe("POST /auth/login", () => {
helpText: "After sign-in, OpenAI may redirect to a localhost callback that cannot open from this dashboard host. Copy the full browser URL from the address bar and paste it here.",
});
});
it("returns manual-code flow for anthropic and skips callback rewrite on remote hosts", async () => {
const unchangedUrl =
"https://claude.ai/oauth/authorize?state=anthropic-state&redirect_uri=http%3A%2F%2Flocalhost%3A3210%2Fauth%2Fcallback";
(authStorage.getOAuthProviders as ReturnType<typeof vi.fn>).mockReturnValue([
{ id: "anthropic", name: "Anthropic" },
]);
(authStorage.login as ReturnType<typeof vi.fn>).mockImplementation((_provider: string, callbacks: any) => {
callbacks.onAuth({ url: unchangedUrl, instructions: "Sign in with Claude" });
return Promise.resolve();
});
const res = await REQUEST(
buildApp(),
"POST",
"/api/auth/login",
JSON.stringify({ provider: "anthropic", origin: "https://my-host.example.com" }),
{ "Content-Type": "application/json" },
);
expect(res.status).toBe(200);
expect(res.body.url).toBe(unchangedUrl);
expect(res.body.instructions).toContain("After Claude sign-in");
expect(res.body.manualCode).toEqual({
prompt: "Paste the final redirect URL or authorization code",
placeholder: "http://localhost:*/callback?code=...&state=... or just the code",
helpText: "After Claude sign-in, copy the full browser URL (or just the code) and paste it here to finish login from this dashboard host.",
});
});
it("returns 400 when provider is missing", async () => {
const res = await REQUEST(buildApp(), "POST", "/api/auth/login", JSON.stringify({}), {
"Content-Type": "application/json",
@@ -1302,7 +1332,10 @@ describe("POST /auth/manual-code", () => {
beforeEach(() => {
store = createMockStore();
authStorage = createMockAuthStorage({
getOAuthProviders: vi.fn().mockReturnValue([{ id: "openai-codex", name: "OpenAI Codex" }]),
getOAuthProviders: vi.fn().mockReturnValue([
{ id: "openai-codex", name: "OpenAI Codex" },
{ id: "anthropic", name: "Anthropic" },
]),
});
});
@@ -1358,6 +1391,46 @@ describe("POST /auth/manual-code", () => {
});
});
it("submits pasted manual code for anthropic login", async () => {
let submittedCode: string | undefined;
(authStorage.login as ReturnType<typeof vi.fn>).mockImplementation(
async (_provider: string, callbacks: {
onAuth: (info: { url: string; instructions?: string }) => void;
onPrompt?: () => Promise<string>;
}) => {
callbacks.onAuth({
url: "https://claude.ai/oauth/authorize?state=anthropic-state&redirect_uri=http%3A%2F%2Flocalhost%3A3210%2Fauth%2Fcallback",
});
submittedCode = await callbacks.onPrompt?.();
},
);
const app = buildApp();
const loginRes = await REQUEST(
app,
"POST",
"/api/auth/login",
JSON.stringify({ provider: "anthropic", origin: "https://remote.example.com" }),
{ "Content-Type": "application/json" },
);
expect(loginRes.status).toBe(200);
const submitRes = await REQUEST(
app,
"POST",
"/api/auth/manual-code",
JSON.stringify({ provider: "anthropic", code: "anthropic-manual-code" }),
{ "Content-Type": "application/json" },
);
expect(submitRes.status).toBe(200);
expect(submitRes.body).toEqual({ success: true, submitted: true });
await vi.waitFor(() => {
expect(submittedCode).toBe("anthropic-manual-code");
});
});
it("returns 409 when no login is in progress", async () => {
const res = await REQUEST(
buildApp(),

View File

@@ -138,12 +138,9 @@ export const registerAuthRoutes: ApiRouteRegistrar = (ctx) => {
return false;
}
// The upstream OpenAI Codex OAuth provider is hardcoded to request and
// later redeem the localhost callback URI `http://localhost:1455/auth/callback`.
// Rewriting that authorize-time redirect_uri to the dashboard proxy causes
// OpenAI auth to fail with an upstream unknown_error. Keep the original
// localhost callback for this provider.
if (providerId === "openai-codex") {
// These providers rely on pasted-code UX with their own localhost callbacks,
// so redirect_uri must remain untouched.
if (providerId === "openai-codex" || providerId === "anthropic") {
return false;
}
@@ -151,18 +148,29 @@ export const registerAuthRoutes: ApiRouteRegistrar = (ctx) => {
}
function getManualCodeConfig(providerId: string, origin: string | undefined): ManualCodeConfig | undefined {
if (providerId !== "openai-codex") {
return undefined;
const remoteDashboard = origin !== undefined && !isLocalhostOrigin(origin);
if (providerId === "openai-codex") {
return {
prompt: "Paste the final redirect URL or authorization code",
placeholder: "http://localhost:1455/auth/callback?code=...&state=... or just the code",
helpText: remoteDashboard
? "After sign-in, OpenAI may redirect to a localhost callback that cannot open from this dashboard host. Copy the full browser URL from the address bar and paste it here."
: "If the browser cannot finish the localhost callback automatically, copy the full browser URL from the address bar and paste it here.",
};
}
const remoteDashboard = origin !== undefined && !isLocalhostOrigin(origin);
return {
prompt: "Paste the final redirect URL or authorization code",
placeholder: "http://localhost:1455/auth/callback?code=...&state=... or just the code",
helpText: remoteDashboard
? "After sign-in, OpenAI may redirect to a localhost callback that cannot open from this dashboard host. Copy the full browser URL from the address bar and paste it here."
: "If the browser cannot finish the localhost callback automatically, copy the full browser URL from the address bar and paste it here.",
};
if (providerId === "anthropic") {
return {
prompt: "Paste the final redirect URL or authorization code",
placeholder: "http://localhost:*/callback?code=...&state=... or just the code",
helpText: remoteDashboard
? "After Claude sign-in, copy the full browser URL (or just the code) and paste it here to finish login from this dashboard host."
: "If Claude cannot finish the localhost callback automatically, copy the full browser URL from the address bar and paste it here.",
};
}
return undefined;
}
async function probeDroidCliWithEffectiveBinary(req?: Request) {