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:
5
.changeset/fn-3617-claude-oauth-copy-code.md
Normal file
5
.changeset/fn-3617-claude-oauth-copy-code.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fix Claude dashboard OAuth on remote hosts by using the pasted authorization-code flow instead of callback URL rewriting, while preserving callback proxy behavior for providers that still require it.
|
||||||
@@ -304,10 +304,10 @@ For Capacitor + PWA workflow, see [MOBILE.md](./MOBILE.md).
|
|||||||
|
|
||||||
### Provider authentication
|
### Provider authentication
|
||||||
|
|
||||||
Fusion supports OAuth-based authentication for AI providers configured via **Settings → Authentication**. When the dashboard is accessed via a non-localhost host (remote node, LAN host/IP, or reverse proxy), provider login URLs are automatically rewritten to route OAuth callbacks through a bridge endpoint (`/api/auth/oauth-callback`), ensuring the redirect reaches the active browser session.
|
Fusion supports OAuth-based authentication for AI providers configured via **Settings → Authentication**. For most OAuth providers, when the dashboard is accessed via a non-localhost host (remote node, LAN host/IP, or reverse proxy), provider login URLs are rewritten to route OAuth callbacks through a bridge endpoint (`/api/auth/oauth-callback`) so redirects reach the active browser session.
|
||||||
|
|
||||||
- **Anthropic (Claude)** — Authenticates via the same Settings/onboarding OAuth flow as other OAuth providers
|
- **Anthropic (Claude)** — Uses a pasted authorization-code flow in Settings/onboarding: sign in, then paste the final redirect URL (or code) back into Fusion to complete login
|
||||||
- **OpenAI Codex** — Authenticates via Settings OAuth flow with secure state validation
|
- **OpenAI Codex** — Uses the same pasted authorization-code flow with secure state validation
|
||||||
- **Factory AI — via Droid CLI** *(optional)* — requires local Droid CLI install + `droid auth login`; detection follows the effective runtime binary path (default `droid`, or plugin `droidBinaryPath` when configured), then enable in **Settings → Authentication** and restart Fusion
|
- **Factory AI — via Droid CLI** *(optional)* — requires local Droid CLI install + `droid auth login`; detection follows the effective runtime binary path (default `droid`, or plugin `droidBinaryPath` when configured), then enable in **Settings → Authentication** and restart Fusion
|
||||||
- **llama.cpp — via HTTP server** *(optional)* — configure your llama.cpp server URL (default `http://127.0.0.1:8080`) and optional API key, then enable in **Settings → Authentication**
|
- **llama.cpp — via HTTP server** *(optional)* — configure your llama.cpp server URL (default `http://127.0.0.1:8080`) and optional API key, then enable in **Settings → Authentication**
|
||||||
- **Other providers** — Authenticate via API key entry in Settings (including Google/Gemini API key, Google Generative AI, Vertex, and Cloud Code aliases)
|
- **Other providers** — Authenticate via API key entry in Settings (including Google/Gemini API key, Google Generative AI, Vertex, and Cloud Code aliases)
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ fn dashboard
|
|||||||
|
|
||||||
On first launch, Fusion opens an onboarding wizard with three steps:
|
On first launch, Fusion opens an onboarding wizard with three steps:
|
||||||
|
|
||||||
1. **AI Setup** — choose a provider and authenticate (you only need one to start). Anthropic/Claude and OpenAI Codex support direct OAuth login in onboarding and Settings, while **Anthropic — via Claude CLI** remains available as a separate optional path. Deprecated Google Gemini CLI / Antigravity entries are hidden; Google/Gemini API key, Google Generative AI, Vertex, and Cloud Code options remain available.
|
1. **AI Setup** — choose a provider and authenticate (you only need one to start). Anthropic/Claude and OpenAI Codex use a pasted authorization-code OAuth flow in onboarding and Settings (sign in, then paste the final redirect URL or code back into Fusion), while **Anthropic — via Claude CLI** remains available as a separate optional path. Deprecated Google Gemini CLI / Antigravity entries are hidden; Google/Gemini API key, Google Generative AI, Vertex, and Cloud Code options remain available.
|
||||||
2. **GitHub (Optional)** — connect GitHub for issue import and PR workflows
|
2. **GitHub (Optional)** — connect GitHub for issue import and PR workflows
|
||||||
3. **First Task** — create your first task or import one from GitHub
|
3. **First Task** — create your first task or import one from GitHub
|
||||||
|
|
||||||
|
|||||||
@@ -10,13 +10,13 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 12px;
|
gap: var(--space-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mailbox-title {
|
.mailbox-title {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: var(--space-sm);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
.mailbox-header-actions {
|
.mailbox-header-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: var(--space-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mailbox-tabs {
|
.mailbox-tabs {
|
||||||
@@ -77,7 +77,7 @@
|
|||||||
min-width: 16px;
|
min-width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
padding: 0 4px;
|
padding: 0 4px;
|
||||||
border-radius: 8px;
|
border-radius: var(--radius-md);
|
||||||
background: var(--color-error);
|
background: var(--color-error);
|
||||||
color: var(--fab-text);
|
color: var(--fab-text);
|
||||||
font-size: 0.65rem;
|
font-size: 0.65rem;
|
||||||
@@ -87,9 +87,9 @@
|
|||||||
.mailbox-content {
|
.mailbox-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
padding: 16px;
|
padding: var(--space-lg);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
max-height: calc(80vh - 140px);
|
max-height: calc(80vh - var(--header-height) - var(--space-2xl) - var(--space-xl));
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,10 +104,10 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 48px 16px;
|
padding: calc(var(--space-2xl) + var(--space-lg)) var(--space-lg);
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
gap: 12px;
|
gap: var(--space-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mailbox-empty p {
|
.mailbox-empty p {
|
||||||
@@ -118,11 +118,11 @@
|
|||||||
.mailbox-item {
|
.mailbox-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 12px;
|
gap: var(--space-md);
|
||||||
padding: 12px;
|
padding: var(--space-md);
|
||||||
border-radius: 8px;
|
border-radius: var(--radius-md);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background-color 0.1s;
|
transition: background-color var(--transition-instant);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mailbox-item:hover {
|
.mailbox-item:hover {
|
||||||
@@ -155,7 +155,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 8px;
|
gap: var(--space-sm);
|
||||||
margin-bottom: 2px;
|
margin-bottom: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,11 +202,11 @@
|
|||||||
.mailbox-conversation-group {
|
.mailbox-conversation-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 12px;
|
gap: var(--space-md);
|
||||||
padding: 12px;
|
padding: var(--space-md);
|
||||||
border-radius: 8px;
|
border-radius: var(--radius-md);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background-color 0.1s;
|
transition: background-color var(--transition-instant);
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,21 +239,21 @@
|
|||||||
.mailbox-message-detail {
|
.mailbox-message-detail {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 16px;
|
gap: var(--space-lg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mailbox-message-detail-header {
|
.mailbox-message-detail-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 12px;
|
gap: var(--space-md);
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mailbox-message-detail-meta {
|
.mailbox-message-detail-meta {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: var(--space-sm);
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
@@ -261,7 +261,7 @@
|
|||||||
.mailbox-message-detail-actions {
|
.mailbox-message-detail-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: var(--space-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mailbox-message-type {
|
.mailbox-message-type {
|
||||||
@@ -282,7 +282,7 @@
|
|||||||
gap: 24px;
|
gap: 24px;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
background: var(--bg-tertiary);
|
background: var(--bg-tertiary);
|
||||||
border-radius: 8px;
|
border-radius: var(--radius-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mailbox-participant {
|
.mailbox-participant {
|
||||||
@@ -304,9 +304,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mailbox-message-body {
|
.mailbox-message-body {
|
||||||
padding: 16px;
|
padding: var(--space-lg);
|
||||||
background: var(--bg-secondary);
|
background: var(--bg-secondary);
|
||||||
border-radius: 8px;
|
border-radius: var(--radius-md);
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
@@ -329,7 +329,7 @@
|
|||||||
.mailbox-conversation {
|
.mailbox-conversation {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: var(--space-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mailbox-conversation-label {
|
.mailbox-conversation-label {
|
||||||
@@ -340,7 +340,7 @@
|
|||||||
|
|
||||||
.mailbox-conversation-msg {
|
.mailbox-conversation-msg {
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
border-radius: 8px;
|
border-radius: var(--radius-md);
|
||||||
background: var(--bg-tertiary);
|
background: var(--bg-tertiary);
|
||||||
border-left: 3px solid transparent;
|
border-left: 3px solid transparent;
|
||||||
}
|
}
|
||||||
@@ -354,7 +354,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 8px;
|
gap: var(--space-sm);
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
@@ -371,7 +371,7 @@
|
|||||||
.mailbox-agents {
|
.mailbox-agents {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 12px;
|
gap: var(--space-md);
|
||||||
min-height: 300px;
|
min-height: 300px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -382,7 +382,7 @@
|
|||||||
.mailbox-agents-header {
|
.mailbox-agents-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: var(--space-md);
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -441,13 +441,13 @@
|
|||||||
.mailbox-skeleton {
|
.mailbox-skeleton {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: var(--space-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mailbox-skeleton-item {
|
.mailbox-skeleton-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 12px;
|
gap: var(--space-md);
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -557,7 +557,7 @@
|
|||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.mailbox-modal .mailbox-header {
|
.mailbox-modal .mailbox-header {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 8px;
|
gap: var(--space-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mailbox-modal .mailbox-title {
|
.mailbox-modal .mailbox-title {
|
||||||
@@ -605,13 +605,14 @@
|
|||||||
|
|
||||||
.mailbox-modal .mailbox-tab {
|
.mailbox-modal .mailbox-tab {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
padding: 8px 12px;
|
padding: var(--space-sm) var(--space-md);
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mailbox-modal .mailbox-content {
|
.mailbox-modal .mailbox-content {
|
||||||
max-height: calc(100dvh - 120px);
|
max-height: calc(100dvh - var(--header-height) - var(--space-2xl) - var(--space-xl));
|
||||||
padding: 12px;
|
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 {
|
.mailbox-modal .mailbox-message-detail-header {
|
||||||
@@ -625,11 +626,11 @@
|
|||||||
|
|
||||||
.mailbox-modal .mailbox-message-participants {
|
.mailbox-modal .mailbox-message-participants {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: var(--space-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mailbox-modal .mailbox-conversation-msg {
|
.mailbox-modal .mailbox-conversation-msg {
|
||||||
padding: 6px 10px;
|
padding: var(--space-xs) var(--space-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mailbox-modal .mailbox-agent-select {
|
.mailbox-modal .mailbox-agent-select {
|
||||||
@@ -641,7 +642,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mailbox-modal .mailbox-empty {
|
.mailbox-modal .mailbox-empty {
|
||||||
padding: 32px 12px;
|
padding: var(--space-2xl) var(--space-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mailbox-modal .mailbox-agent-subtabs {
|
.mailbox-modal .mailbox-agent-subtabs {
|
||||||
@@ -652,13 +653,13 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
min-height: 36px;
|
min-height: 36px;
|
||||||
padding: 8px 12px;
|
padding: var(--space-sm) var(--space-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Mailbox View (full-page) mobile overrides */
|
/* Mailbox View (full-page) mobile overrides */
|
||||||
.mailbox-view .mailbox-header {
|
.mailbox-view .mailbox-header {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 8px;
|
gap: var(--space-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mailbox-view .mailbox-title {
|
.mailbox-view .mailbox-title {
|
||||||
@@ -677,7 +678,7 @@
|
|||||||
|
|
||||||
.mailbox-view .mailbox-tab {
|
.mailbox-view .mailbox-tab {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
padding: 8px 12px;
|
padding: var(--space-sm) var(--space-md);
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -686,7 +687,7 @@
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: var(--space-md);
|
padding: var(--space-md);
|
||||||
/* Account for mobile nav bar at bottom */
|
/* 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 {
|
.mailbox-view .mailbox-split-layout {
|
||||||
@@ -714,11 +715,11 @@
|
|||||||
|
|
||||||
.mailbox-view .mailbox-message-participants {
|
.mailbox-view .mailbox-message-participants {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: var(--space-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mailbox-view .mailbox-conversation-msg {
|
.mailbox-view .mailbox-conversation-msg {
|
||||||
padding: 6px 10px;
|
padding: var(--space-xs) var(--space-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mailbox-view .mailbox-agent-select {
|
.mailbox-view .mailbox-agent-select {
|
||||||
@@ -730,7 +731,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mailbox-view .mailbox-empty {
|
.mailbox-view .mailbox-empty {
|
||||||
padding: 32px 12px;
|
padding: var(--space-2xl) var(--space-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mailbox-view .mailbox-agent-subtabs {
|
.mailbox-view .mailbox-agent-subtabs {
|
||||||
@@ -741,7 +742,7 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
min-height: 36px;
|
min-height: 36px;
|
||||||
padding: 8px 12px;
|
padding: var(--space-sm) var(--space-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Message Composer mobile overrides */
|
/* Message Composer mobile overrides */
|
||||||
@@ -788,13 +789,13 @@
|
|||||||
.message-composer-body {
|
.message-composer-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 12px;
|
gap: var(--space-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-composer-field {
|
.message-composer-field {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 8px;
|
gap: var(--space-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-composer-field--content {
|
.message-composer-field--content {
|
||||||
@@ -905,10 +906,10 @@
|
|||||||
.message-composer-error {
|
.message-composer-error {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: var(--space-sm);
|
||||||
padding: 8px 12px;
|
padding: var(--space-sm) var(--space-md);
|
||||||
background: color-mix(in srgb, var(--color-error) 10%, transparent);
|
background: color-mix(in srgb, var(--color-error) 10%, transparent);
|
||||||
border-radius: 6px;
|
border-radius: var(--radius-sm);
|
||||||
color: var(--color-error);
|
color: var(--color-error);
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
}
|
}
|
||||||
@@ -917,6 +918,6 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
gap: 8px;
|
gap: var(--space-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -933,23 +933,23 @@ describe("MailboxModal", () => {
|
|||||||
expect(mailboxMobileSection).toContain(".mailbox-modal .mailbox-tabs::-webkit-scrollbar");
|
expect(mailboxMobileSection).toContain(".mailbox-modal .mailbox-tabs::-webkit-scrollbar");
|
||||||
expect(mailboxMobileSection).toContain("display: none;");
|
expect(mailboxMobileSection).toContain("display: none;");
|
||||||
expect(mailboxMobileSection).toContain(".mailbox-modal .mailbox-tab");
|
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("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(".mailbox-modal .mailbox-message-detail-header");
|
||||||
expect(mailboxMobileSection).toContain("flex-direction: column;");
|
expect(mailboxMobileSection).toContain("flex-direction: column;");
|
||||||
expect(mailboxMobileSection).toContain("align-items: flex-start;");
|
expect(mailboxMobileSection).toContain("align-items: flex-start;");
|
||||||
expect(mailboxMobileSection).toContain(".mailbox-modal .mailbox-message-detail-actions");
|
expect(mailboxMobileSection).toContain(".mailbox-modal .mailbox-message-detail-actions");
|
||||||
expect(mailboxMobileSection).toContain(".mailbox-modal .mailbox-message-participants");
|
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(".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(".mailbox-modal .mailbox-agent-select");
|
||||||
expect(mailboxMobileSection).toContain("max-width: 100%;");
|
expect(mailboxMobileSection).toContain("max-width: 100%;");
|
||||||
expect(mailboxMobileSection).toContain(".mailbox-modal .mailbox-agents");
|
expect(mailboxMobileSection).toContain(".mailbox-modal .mailbox-agents");
|
||||||
expect(mailboxMobileSection).toContain("min-height: 200px;");
|
expect(mailboxMobileSection).toContain("min-height: 200px;");
|
||||||
expect(mailboxMobileSection).toContain(".mailbox-modal .mailbox-empty");
|
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 () => {
|
it("renders detail-view structural hooks targeted by mobile overrides", async () => {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const mockFetchAuthStatus = vi.fn();
|
|||||||
const mockLoginProvider = vi.fn();
|
const mockLoginProvider = vi.fn();
|
||||||
const mockLogoutProvider = vi.fn();
|
const mockLogoutProvider = vi.fn();
|
||||||
const mockCancelProviderLogin = vi.fn();
|
const mockCancelProviderLogin = vi.fn();
|
||||||
|
const mockSubmitProviderManualCode = vi.fn();
|
||||||
const mockSaveApiKey = vi.fn();
|
const mockSaveApiKey = vi.fn();
|
||||||
const mockClearApiKey = vi.fn();
|
const mockClearApiKey = vi.fn();
|
||||||
const mockFetchModels = vi.fn();
|
const mockFetchModels = vi.fn();
|
||||||
@@ -24,6 +25,7 @@ vi.mock("../../api", () => ({
|
|||||||
loginProvider: (...args: unknown[]) => mockLoginProvider(...args),
|
loginProvider: (...args: unknown[]) => mockLoginProvider(...args),
|
||||||
logoutProvider: (...args: unknown[]) => mockLogoutProvider(...args),
|
logoutProvider: (...args: unknown[]) => mockLogoutProvider(...args),
|
||||||
cancelProviderLogin: (...args: unknown[]) => mockCancelProviderLogin(...args),
|
cancelProviderLogin: (...args: unknown[]) => mockCancelProviderLogin(...args),
|
||||||
|
submitProviderManualCode: (...args: unknown[]) => mockSubmitProviderManualCode(...args),
|
||||||
saveApiKey: (...args: unknown[]) => mockSaveApiKey(...args),
|
saveApiKey: (...args: unknown[]) => mockSaveApiKey(...args),
|
||||||
clearApiKey: (...args: unknown[]) => mockClearApiKey(...args),
|
clearApiKey: (...args: unknown[]) => mockClearApiKey(...args),
|
||||||
fetchModels: (...args: unknown[]) => mockFetchModels(...args),
|
fetchModels: (...args: unknown[]) => mockFetchModels(...args),
|
||||||
@@ -183,6 +185,7 @@ beforeEach(() => {
|
|||||||
mockLoginProvider.mockResolvedValue({ url: "https://auth.example.com/login" });
|
mockLoginProvider.mockResolvedValue({ url: "https://auth.example.com/login" });
|
||||||
mockLogoutProvider.mockResolvedValue({ success: true });
|
mockLogoutProvider.mockResolvedValue({ success: true });
|
||||||
mockCancelProviderLogin.mockResolvedValue({ success: true, cancelled: true });
|
mockCancelProviderLogin.mockResolvedValue({ success: true, cancelled: true });
|
||||||
|
mockSubmitProviderManualCode.mockResolvedValue({ success: true, submitted: true });
|
||||||
mockSaveApiKey.mockResolvedValue({ success: true });
|
mockSaveApiKey.mockResolvedValue({ success: true });
|
||||||
mockClearApiKey.mockResolvedValue({ success: true });
|
mockClearApiKey.mockResolvedValue({ success: true });
|
||||||
// Default to no persisted state (start at ai-setup)
|
// 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 () => {
|
it("saves API key when Save is clicked", async () => {
|
||||||
render(<ModelOnboardingModal onComplete={vi.fn()} addToast={vi.fn()} projectId="proj_123" />);
|
render(<ModelOnboardingModal onComplete={vi.fn()} addToast={vi.fn()} projectId="proj_123" />);
|
||||||
|
|
||||||
@@ -1376,7 +1441,7 @@ describe("ModelOnboardingModal", () => {
|
|||||||
const badge = screen.getByTestId("github-status-badge");
|
const badge = screen.getByTestId("github-status-badge");
|
||||||
expect(badge).toHaveTextContent("✗ Connection failed");
|
expect(badge).toHaveTextContent("✗ Connection failed");
|
||||||
expect(badge).toHaveClass("retry");
|
expect(badge).toHaveClass("retry");
|
||||||
});
|
}, { timeout: 3000 });
|
||||||
|
|
||||||
expect(screen.getByText("Connection failed or timed out.")).toBeTruthy();
|
expect(screen.getByText("Connection failed or timed out.")).toBeTruthy();
|
||||||
expect(screen.getByRole("button", { name: "Retry" })).toBeTruthy();
|
expect(screen.getByRole("button", { name: "Retry" })).toBeTruthy();
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ const mockLoginProvider = vi.fn();
|
|||||||
const mockLogoutProvider = vi.fn();
|
const mockLogoutProvider = vi.fn();
|
||||||
const mockCancelProviderLogin = vi.fn();
|
const mockCancelProviderLogin = vi.fn();
|
||||||
const mockSaveApiKey = vi.fn();
|
const mockSaveApiKey = vi.fn();
|
||||||
|
const mockSubmitProviderManualCode = vi.fn();
|
||||||
const mockFetchModels = vi.fn();
|
const mockFetchModels = vi.fn();
|
||||||
const mockFetchCustomProviders = vi.fn();
|
const mockFetchCustomProviders = vi.fn();
|
||||||
const mockCreateCustomProvider = vi.fn();
|
const mockCreateCustomProvider = vi.fn();
|
||||||
@@ -69,6 +70,7 @@ vi.mock("../../api", async (importOriginal) => {
|
|||||||
logoutProvider: (...args: unknown[]) => mockLogoutProvider(...args),
|
logoutProvider: (...args: unknown[]) => mockLogoutProvider(...args),
|
||||||
cancelProviderLogin: (...args: unknown[]) => mockCancelProviderLogin(...args),
|
cancelProviderLogin: (...args: unknown[]) => mockCancelProviderLogin(...args),
|
||||||
saveApiKey: (...args: unknown[]) => mockSaveApiKey(...args),
|
saveApiKey: (...args: unknown[]) => mockSaveApiKey(...args),
|
||||||
|
submitProviderManualCode: (...args: unknown[]) => mockSubmitProviderManualCode(...args),
|
||||||
fetchModels: (...args: unknown[]) => mockFetchModels(...args),
|
fetchModels: (...args: unknown[]) => mockFetchModels(...args),
|
||||||
fetchCustomProviders: (...args: unknown[]) => mockFetchCustomProviders(...args),
|
fetchCustomProviders: (...args: unknown[]) => mockFetchCustomProviders(...args),
|
||||||
createCustomProvider: (...args: unknown[]) => mockCreateCustomProvider(...args),
|
createCustomProvider: (...args: unknown[]) => mockCreateCustomProvider(...args),
|
||||||
@@ -266,6 +268,7 @@ describe("SettingsModal", () => {
|
|||||||
mockDeleteCustomProvider.mockResolvedValue(undefined);
|
mockDeleteCustomProvider.mockResolvedValue(undefined);
|
||||||
mockCancelProviderLogin.mockResolvedValue({ success: true, cancelled: true });
|
mockCancelProviderLogin.mockResolvedValue({ success: true, cancelled: true });
|
||||||
mockSaveApiKey.mockResolvedValue(undefined);
|
mockSaveApiKey.mockResolvedValue(undefined);
|
||||||
|
mockSubmitProviderManualCode.mockResolvedValue({ success: true, submitted: true });
|
||||||
mockTestNotification.mockResolvedValue({ success: true });
|
mockTestNotification.mockResolvedValue({ success: true });
|
||||||
mockFetchBackups.mockResolvedValue({ backups: [], totalSize: 0 });
|
mockFetchBackups.mockResolvedValue({ backups: [], totalSize: 0 });
|
||||||
mockFetchMemoryFiles.mockResolvedValue({
|
mockFetchMemoryFiles.mockResolvedValue({
|
||||||
@@ -1048,6 +1051,36 @@ describe("SettingsModal", () => {
|
|||||||
expect(openSpy).toHaveBeenCalled();
|
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 () => {
|
it("shows cancel action for server-reported pending oauth login", async () => {
|
||||||
mockFetchAuthStatus.mockResolvedValue({
|
mockFetchAuthStatus.mockResolvedValue({
|
||||||
providers: [{ id: "github-copilot", name: "GitHub Copilot", authenticated: false, type: "oauth", loginInProgress: true }],
|
providers: [{ id: "github-copilot", name: "GitHub Copilot", authenticated: false, type: "oauth", loginInProgress: true }],
|
||||||
|
|||||||
@@ -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.",
|
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 () => {
|
it("returns 400 when provider is missing", async () => {
|
||||||
const res = await REQUEST(buildApp(), "POST", "/api/auth/login", JSON.stringify({}), {
|
const res = await REQUEST(buildApp(), "POST", "/api/auth/login", JSON.stringify({}), {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -1302,7 +1332,10 @@ describe("POST /auth/manual-code", () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
store = createMockStore();
|
store = createMockStore();
|
||||||
authStorage = createMockAuthStorage({
|
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 () => {
|
it("returns 409 when no login is in progress", async () => {
|
||||||
const res = await REQUEST(
|
const res = await REQUEST(
|
||||||
buildApp(),
|
buildApp(),
|
||||||
|
|||||||
@@ -138,12 +138,9 @@ export const registerAuthRoutes: ApiRouteRegistrar = (ctx) => {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The upstream OpenAI Codex OAuth provider is hardcoded to request and
|
// These providers rely on pasted-code UX with their own localhost callbacks,
|
||||||
// later redeem the localhost callback URI `http://localhost:1455/auth/callback`.
|
// so redirect_uri must remain untouched.
|
||||||
// Rewriting that authorize-time redirect_uri to the dashboard proxy causes
|
if (providerId === "openai-codex" || providerId === "anthropic") {
|
||||||
// OpenAI auth to fail with an upstream unknown_error. Keep the original
|
|
||||||
// localhost callback for this provider.
|
|
||||||
if (providerId === "openai-codex") {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,18 +148,29 @@ export const registerAuthRoutes: ApiRouteRegistrar = (ctx) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getManualCodeConfig(providerId: string, origin: string | undefined): ManualCodeConfig | undefined {
|
function getManualCodeConfig(providerId: string, origin: string | undefined): ManualCodeConfig | undefined {
|
||||||
if (providerId !== "openai-codex") {
|
const remoteDashboard = origin !== undefined && !isLocalhostOrigin(origin);
|
||||||
return undefined;
|
|
||||||
|
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);
|
if (providerId === "anthropic") {
|
||||||
return {
|
return {
|
||||||
prompt: "Paste the final redirect URL or authorization code",
|
prompt: "Paste the final redirect URL or authorization code",
|
||||||
placeholder: "http://localhost:1455/auth/callback?code=...&state=... or just the code",
|
placeholder: "http://localhost:*/callback?code=...&state=... or just the code",
|
||||||
helpText: remoteDashboard
|
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."
|
? "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 the browser cannot finish the localhost callback automatically, copy the full browser URL from the address bar and paste it here.",
|
: "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) {
|
async function probeDroidCliWithEffectiveBinary(req?: Request) {
|
||||||
|
|||||||
Reference in New Issue
Block a user