Skip to content

feat(providers): add Anthropic OAuth subscription support with CLI detection#430

Open
TheDarkSkyXD wants to merge 2 commits intospacedriveapp:mainfrom
TheDarkSkyXD:feat/anthropic-oauth-cli-detection
Open

feat(providers): add Anthropic OAuth subscription support with CLI detection#430
TheDarkSkyXD wants to merge 2 commits intospacedriveapp:mainfrom
TheDarkSkyXD:feat/anthropic-oauth-cli-detection

Conversation

@TheDarkSkyXD
Copy link
Contributor

@TheDarkSkyXD TheDarkSkyXD commented Mar 14, 2026

Summary

Adds the ability for users to authenticate with their Anthropic Claude Pro/Max subscription via OAuth (PKCE flow) instead of requiring a separate API key. This brings Anthropic OAuth support on par with the existing ChatGPT Plus OAuth integration.

Key capabilities:

  • Claude Code CLI detection — automatically detects if the user has Claude Code CLI installed and authenticated on their local machine by inspecting ~/.claude/ and running claude auth status. When detected, the UI surfaces the user's email and indicates they can sign in quickly since they're already authenticated with Anthropic.
  • OAuth PKCE flow — users can sign in with their Anthropic account (Claude Pro, Max, or API console) through a browser-based authorization flow. The backend generates a PKCE challenge, opens the Anthropic authorization page, and exchanges the returned code for access/refresh tokens.
  • Model selector — during sign-in, users choose which Anthropic model to apply to all routing roles (channel, branch, worker, compactor, cortex) rather than being locked to a hardcoded default. This is important because OAuth tokens require fully versioned model IDs (e.g., claude-sonnet-4-20250514) rather than short aliases.
  • Provider switching — under Settings > Providers, a new "Claude Code CLI (OAuth)" card appears directly below the Anthropic API key card, allowing users to switch between API key and OAuth authentication methods. OAuth credentials can be independently added or removed.

Changes

Backend (src/)

File Changes
src/api/providers.rs Added anthropic_oauth to ProviderStatus. Added ClaudeCliStatusResponse, AnthropicOAuthStartRequest/Response, AnthropicOAuthExchangeRequest/Response types. Added ANTHROPIC_OAUTH_SESSIONS static for PKCE session tracking. Implemented claude_cli_status() handler (CLI binary discovery via PATH + fallback paths, version verification, auth status parsing). Implemented start_anthropic_oauth() handler (PKCE flow initiation with session management). Implemented exchange_anthropic_oauth() handler (code exchange, credential persistence, model routing update). Added anthropic-oauth case to delete_provider().
src/api/server.rs Added 3 new routes: GET /providers/anthropic/oauth/cli-status, POST /providers/anthropic/oauth/start, POST /providers/anthropic/oauth/exchange.
src/llm/manager.rs Added set_anthropic_oauth_credentials() and clear_anthropic_oauth_credentials() methods to LlmManager for runtime credential management.

Frontend (interface/src/)

File Changes
interface/src/api/client.ts Added anthropic_oauth to ProviderStatus. Added ClaudeCliStatusResponse, AnthropicOAuthStartResponse, AnthropicOAuthExchangeResponse types. Added claudeCliStatus(), startAnthropicOAuth(), exchangeAnthropicOAuth() API functions.
interface/src/routes/Settings.tsx Added AnthropicOAuthProviderCard component with CLI detection status indicator. Added AnthropicOAuthDialog component with model selector (ModelSelect), PKCE flow step indicators, and authorization code input. Integrated both into the providers section alongside the existing Anthropic API key card. Added supporting state and mutation hooks.

How it works

  1. User navigates to Settings > Providers
  2. Below the Anthropic API key card, they see "Claude Code CLI (OAuth)" with:
    • If CLI is detected: green indicator showing their email and a note that sign-in will be quick
    • If CLI is not detected: note that they can still sign in via browser
  3. User clicks "Sign in", selects a model from the dropdown
  4. Clicks "Open authorization page" which opens an Anthropic consent page in a popup
  5. After approving, user copies the authorization code and pastes it into the dialog
  6. Backend exchanges the code for OAuth tokens, saves them to anthropic_oauth.json, updates the LLM manager, and applies the selected model to all routing roles
  7. The OAuth token is automatically refreshed when it expires (5-minute buffer)

Test plan

  • Verify CLI detection endpoint returns correct status when Claude Code CLI is/isn't installed
  • Verify OAuth sign-in flow completes successfully with a Claude Pro/Max account
  • Verify the selected model is applied to all routing roles in config.toml
  • Verify the bot responds correctly when using OAuth credentials
  • Verify removing the OAuth provider clears credentials and updates provider status
  • Verify OAuth and API key can coexist (OAuth takes precedence per existing get_anthropic_provider() logic)
  • Verify token refresh works when the access token expires

🤖 Generated with Claude Code


Note

Implementation Summary: This PR adds Anthropic OAuth authentication with smart CLI detection. The backend implements PKCE-based OAuth flow (10-minute session TTL) with CLI binary detection via which command and fallback paths, parses claude auth status JSON output for email extraction, and persists OAuth credentials separately from API keys. The frontend provides two new components: a provider card with CLI status indication and a multi-step dialog for PKCE authorization with model selection. The implementation integrates seamlessly with existing provider management—OAuth is stored independently and takes precedence over API keys when both exist. Key additions: 3 API routes, 290 lines of backend logic for OAuth flow + CLI detection, 220 lines of frontend UI components with proper state management and polling.
Written by Tembo for commit eedc001.

…tection

Add the ability for users to authenticate with their Anthropic Claude
Pro/Max subscription via OAuth instead of requiring a separate API key.
The feature detects existing Claude Code CLI installations and surfaces
that status in the UI for a streamlined sign-in experience.

Backend:
- Add GET /api/providers/anthropic/oauth/cli-status endpoint that
  detects Claude Code CLI installation, version, and auth status by
  inspecting ~/.claude/ and running `claude auth status`
- Add POST /api/providers/anthropic/oauth/start endpoint to initiate
  the PKCE authorization flow and return the browser authorize URL
- Add POST /api/providers/anthropic/oauth/exchange endpoint to exchange
  the authorization code for access/refresh tokens
- Add anthropic_oauth field to ProviderStatus for tracking OAuth state
- Add set/clear_anthropic_oauth_credentials methods to LlmManager
- Handle DELETE /api/providers/anthropic-oauth for credential removal

Frontend:
- Add AnthropicOAuthProviderCard below the Anthropic API key card with
  CLI detection status indicator and sign-in/remove actions
- Add AnthropicOAuthDialog with model selector (user picks their model),
  PKCE flow steps, and authorization code input
- Add ClaudeCliStatusResponse, AnthropicOAuthStartResponse, and
  AnthropicOAuthExchangeResponse types to the API client
- Add claudeCliStatus(), startAnthropicOAuth(), and
  exchangeAnthropicOAuth() API functions

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 14, 2026

Walkthrough

Adds Anthropic OAuth PKCE support across frontend and backend: new client API types/methods, Settings UI components and flows (including Claude CLI detection and Tauri openExternal), backend PKCE session handling and endpoints, credential persistence in the LLM manager, and route registrations.

Changes

Cohort / File(s) Summary
API client
interface/src/api/client.ts
Added ClaudeCliStatusResponse, AnthropicOAuthStartResponse, AnthropicOAuthExchangeResponse, extended ProviderStatus with anthropic_oauth, and new API methods claudeCliStatus, startAnthropicOAuth, exchangeAnthropicOAuth.
Frontend — Settings & UI
interface/src/routes/Settings.tsx
Added Anthropic OAuth UI (AnthropicOAuthProviderCard, AnthropicOAuthDialog), local state and react-query flows for CLI status, start/exchange mutations, cache invalidation logic, and openExternal helper (replaces direct window.open for OpenAI device-login usage).
Backend — Providers logic
src/api/providers.rs
Introduced in-memory PKCE session store (ANTHROPIC_OAUTH_SESSIONS), AnthropicOAuthSession type, Claude CLI detection helpers, claude_cli_status, start_anthropic_oauth, exchange_anthropic_oauth handlers, updated ProviderStatus with anthropic_oauth, credential persistence, and provider delete handling for Anthropic OAuth.
Backend — Router
src/api/server.rs
Registered three new routes under /providers/anthropic/oauth: cli-status (GET), start (POST), and exchange (POST).
LLM Manager
src/llm/manager.rs
Added async methods to set/clear Anthropic OAuth credentials and to clear OpenAI OAuth credentials in-memory (set_anthropic_oauth_credentials, clear_anthropic_oauth_credentials, clear_openai_oauth_credentials).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: adding Anthropic OAuth authentication with CLI detection support, which matches the primary focus of all file changes.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, covering the OAuth implementation, CLI detection, model selection, and UI components across all modified files.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

Important

Merge conflicts detected (Beta)

  • Resolve merge conflict in branch feat/anthropic-oauth-cli-detection
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Tip

CodeRabbit can suggest fixes for GitHub Check annotations.

Configure the reviews.tools.github-checks setting to adjust the time to wait for GitHub Checks to complete.

@TheDarkSkyXD
Copy link
Contributor Author

I tested it and it fullly worked for me.

Comment on lines +1048 to +1051
let home = dirs::home_dir().unwrap_or_default();
let claude_dir = home.join(".claude");
let claude_folder_exists = claude_dir.is_dir();
let credentials_file_exists = claude_dir.join("credentials.json").is_file();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dirs::home_dir() can be None; unwrap_or_default() makes .claude relative to CWD and can give false positives. Handling the None case avoids surprising detection.

Suggested change
let home = dirs::home_dir().unwrap_or_default();
let claude_dir = home.join(".claude");
let claude_folder_exists = claude_dir.is_dir();
let credentials_file_exists = claude_dir.join("credentials.json").is_file();
let (claude_folder_exists, credentials_file_exists) = match dirs::home_dir() {
Some(home) => {
let claude_dir = home.join(".claude");
(
claude_dir.is_dir(),
claude_dir.join("credentials.json").is_file(),
)
}
None => (false, false),
};

Comment on lines +1135 to +1138
Err(_) => {
// Command failed to run but binary exists — assume authed (older CLI).
(true, None)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If claude auth status fails to run, returning authenticated=true can produce a pretty confusing false-positive. I’d keep this conservative.

Suggested change
Err(_) => {
// Command failed to run but binary exists — assume authed (older CLI).
(true, None)
}
Err(_) => (false, None),

Comment on lines +596 to +600
window.open(
result.authorize_url,
"spacebot-anthropic-oauth",
"popup=true,width=780,height=960,noopener,noreferrer",
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

window.open can return null (popup blocked), which leaves the dialog in the “paste code” step without ever opening the auth page. Surfacing that case makes the flow less confusing.

Suggested change
window.open(
result.authorize_url,
"spacebot-anthropic-oauth",
"popup=true,width=780,height=960,noopener,noreferrer",
);
const popup = window.open(
result.authorize_url,
"spacebot-anthropic-oauth",
"popup=true,width=780,height=960,noopener,noreferrer",
);
if (!popup) {
setAnthropicOAuthMessage({
text: "Popup was blocked by the browser. Please allow popups and try again.",
type: "error",
});
}

Comment on lines +606 to +613
const handleExchangeAnthropicOAuth = async () => {
if (!anthropicOAuthState || !anthropicOAuthCodeInput.trim()) return;
setAnthropicOAuthMessage(null);
try {
const result = await exchangeAnthropicOAuthMutation.mutateAsync({
code: anthropicOAuthCodeInput.trim(),
state: anthropicOAuthState,
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the backend expects the code in the <code>#<state> format, validating the paste upfront gives a clearer error than “token exchange failed”.

Suggested change
const handleExchangeAnthropicOAuth = async () => {
if (!anthropicOAuthState || !anthropicOAuthCodeInput.trim()) return;
setAnthropicOAuthMessage(null);
try {
const result = await exchangeAnthropicOAuthMutation.mutateAsync({
code: anthropicOAuthCodeInput.trim(),
state: anthropicOAuthState,
});
const handleExchangeAnthropicOAuth = async () => {
const code = anthropicOAuthCodeInput.trim();
if (!anthropicOAuthState || !code) return;
if (!code.includes("#")) {
setAnthropicOAuthMessage({
text: "Paste the full authorization code (it includes a '#...' suffix).",
type: "error",
});
return;
}
setAnthropicOAuthMessage(null);
try {
const result = await exchangeAnthropicOAuthMutation.mutateAsync({
code,
state: anthropicOAuthState,
});

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@interface/src/routes/Settings.tsx`:
- Around line 576-603: Open the popup synchronously before awaiting the network
call to avoid browser popup blocking: in handleStartAnthropicOAuth, call
window.open with an empty URL (e.g., window.open("", "spacebot-anthropic-oauth",
"popup=true,width=780,height=960")) and keep the returned popup reference (do
NOT include "noopener" or "noreferrer" so the reference is usable); then call
startAnthropicOAuthMutation.mutateAsync({...}); after the response, if success
setAnthropicOAuthState(result.state) and navigate the popup by assigning
popup.location.href = result.authorize_url; if the request fails or the response
is invalid, close the popup (popup.close()) and setAnthropicOAuthMessage
appropriately (use existing error messaging logic); ensure you still clear/set
anthropicOAuthCodeInput and anthropicOAuthMessage as before.

In `@src/api/providers.rs`:
- Around line 1284-1323: The handler currently saves credentials and updates the
LLM manager before attempting to parse/write config.toml, then proceeds to
refresh_defaults_config and emit provider_setup_tx even if the TOML write
failed; fix by making the routing write an explicit success gate: perform
apply_model_routing and the tokio::fs::write(&config_path, ...) first (using
content.parse::<toml_edit::DocumentMut>() and writing doc.to_string()), and only
if that parse+write succeeds then call crate::auth::save_credentials,
llm_manager.set_anthropic_oauth_credentials,
refresh_defaults_config(&state).await, and provider_setup_tx.try_send(...); if
the TOML write fails return an error response immediately (and do not call
refresh_defaults_config or emit ProvidersConfigured). This reordering ensures
config routing is actually applied before committing credentials/manager
updates.
- Around line 963-977: After removing the Anthropic OAuth file and clearing the
cached token in the branch that handles provider == "anthropic-oauth" (where you
call mgr.clear_anthropic_oauth_credentials()), also notify the runtime by
sending the same ProvidersConfigured broadcast used in the add/update/exchange
paths so live agents reload their config; locate the existing code that sends
the ProvidersConfigured event and invoke that same broadcast/send logic
immediately before returning the ProviderUpdateResponse to ensure agents observe
the fallback from OAuth to API key/no provider.
- Around line 1199-1212: The endpoint currently only checks for an empty model
but must also reject models that don't belong to Anthropic; update the
validation in the handler that reads request.model (the same block using
model.trim()) to verify provider/model alignment and return an error
Json(AnthropicOAuthStartResponse { success: false, message: "...", ... }) when
the model is not an Anthropic model. Mirror the same provider/model check logic
used by update_provider and test_provider_model (reuse or call their validation
helper if available) so non-Anthropic model strings are rejected before
proceeding to create the OAuth session.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 97f79c12-6703-4827-a697-be89d5781a57

📥 Commits

Reviewing files that changed from the base of the PR and between ed3aebe and eedc001.

📒 Files selected for processing (5)
  • interface/src/api/client.ts
  • interface/src/routes/Settings.tsx
  • src/api/providers.rs
  • src/api/server.rs
  • src/llm/manager.rs

Comment on lines +576 to +603
const handleStartAnthropicOAuth = async () => {
if (!anthropicOAuthModel.trim()) {
setAnthropicOAuthMessage({ text: "Please select a model first", type: "error" });
return;
}
setAnthropicOAuthMessage(null);
setAnthropicOAuthState(null);
setAnthropicOAuthCodeInput("");
try {
const result = await startAnthropicOAuthMutation.mutateAsync({
model: anthropicOAuthModel.trim(),
});
if (!result.success || !result.authorize_url || !result.state) {
setAnthropicOAuthMessage({
text: result.message || "Failed to start OAuth flow",
type: "error",
});
return;
}
setAnthropicOAuthState(result.state);
window.open(
result.authorize_url,
"spacebot-anthropic-oauth",
"popup=true,width=780,height=960,noopener,noreferrer",
);
} catch (error: any) {
setAnthropicOAuthMessage({ text: `Failed: ${error.message}`, type: "error" });
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

Do browsers preserve user activation for window.open() after awaiting a network request in a click handler, or is the popup typically blocked?

💡 Result:

Browsers don’t reliably preserve “user activation” across an awaited network request for window.open()—so the popup is typically blocked, especially in Firefox and Safari.

  • The HTML spec defines transient activation as lasting only a few seconds (user-agent-defined) and expiring by timeout or being consumed by certain APIs. [1]
  • MDN documents window.open() as requiring transient activation and notes it must be called “inside a user interaction … within five seconds,” otherwise it may return null due to popup blocking. [2]
  • In practice, browsers often treat “must be opened in direct response to user input” as meaning synchronous with the click handler, and async work (like a fetch) breaks that expectation. This is a long-standing pain point and leads to workarounds like opening a blank window immediately, then navigating it later. [3][4]

Practical takeaway: if you await fetch(...) and then call window.open(...), expect it to be blocked in many cases. The common cross-browser pattern is:

  1. const w = window.open('about:blank', '_blank') synchronously in the click, then
  2. after the await, set w.location = finalUrl (or close it if the request fails). [4]

Sources:
[1] HTML Standard – transient activation duration/expiry & consumption: https://html.spec.whatwg.org/multipage/interaction.html
[2] MDN – Window.open() user activation requirement / popup blocking: https://developer.mozilla.org/en-US/docs/Web/API/Window/open
[3] WICG discussion – async breaks user-gesture-gated APIs like window.open: https://discourse.wicg.io/t/user-gesture-restrictions-and-async-code/1640
[4] Stack Overflow (example) – works in Chrome but blocked in Firefox/Safari after await, with workaround: https://stackoverflow.com/questions/74128322/using-window-open-in-an-async-function-in-firefox-and-safari


🏁 Script executed:

# Check the code around lines 3053-3074 in Settings.tsx
sed -n '3045,3080p' interface/src/routes/Settings.tsx

Repository: spacedriveapp/spacebot

Length of output: 1052


Open the auth window before awaiting the network request to avoid popup blocking.

The code at line 596 calls window.open() after mutateAsync() resolves. Browsers don't reliably preserve transient user activation across awaited network requests—the popup will be blocked in Firefox, Safari, and others. Because the dialog doesn't offer a manual fallback URL in the pre-auth state, a blocked popup leaves the user stranded in the sign-in flow.

Open a blank popup window synchronously before the request, then navigate it to the authorization URL after the response resolves. Close the popup if the request fails.

Suggested fix
 const handleStartAnthropicOAuth = async () => {
 	if (!anthropicOAuthModel.trim()) {
 		setAnthropicOAuthMessage({ text: "Please select a model first", type: "error" });
 		return;
 	}
 	setAnthropicOAuthMessage(null);
 	setAnthropicOAuthState(null);
 	setAnthropicOAuthCodeInput("");
+	const popup = window.open(
+		"",
+		"spacebot-anthropic-oauth",
+		"popup=true,width=780,height=960,noopener,noreferrer",
+	);
 	try {
 		const result = await startAnthropicOAuthMutation.mutateAsync({
 			model: anthropicOAuthModel.trim(),
 		});
 		if (!result.success || !result.authorize_url || !result.state) {
+			popup?.close();
 			setAnthropicOAuthMessage({
 				text: result.message || "Failed to start OAuth flow",
 				type: "error",
 			});
 			return;
 		}
 		setAnthropicOAuthState(result.state);
-		window.open(
-			result.authorize_url,
-			"spacebot-anthropic-oauth",
-			"popup=true,width=780,height=960,noopener,noreferrer",
-		);
+		if (popup) {
+			popup.location.href = result.authorize_url;
+		} else {
+			window.location.assign(result.authorize_url);
+		}
 	} catch (error: any) {
+		popup?.close();
 		setAnthropicOAuthMessage({ text: `Failed: ${error.message}`, type: "error" });
 	}
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleStartAnthropicOAuth = async () => {
if (!anthropicOAuthModel.trim()) {
setAnthropicOAuthMessage({ text: "Please select a model first", type: "error" });
return;
}
setAnthropicOAuthMessage(null);
setAnthropicOAuthState(null);
setAnthropicOAuthCodeInput("");
try {
const result = await startAnthropicOAuthMutation.mutateAsync({
model: anthropicOAuthModel.trim(),
});
if (!result.success || !result.authorize_url || !result.state) {
setAnthropicOAuthMessage({
text: result.message || "Failed to start OAuth flow",
type: "error",
});
return;
}
setAnthropicOAuthState(result.state);
window.open(
result.authorize_url,
"spacebot-anthropic-oauth",
"popup=true,width=780,height=960,noopener,noreferrer",
);
} catch (error: any) {
setAnthropicOAuthMessage({ text: `Failed: ${error.message}`, type: "error" });
}
const handleStartAnthropicOAuth = async () => {
if (!anthropicOAuthModel.trim()) {
setAnthropicOAuthMessage({ text: "Please select a model first", type: "error" });
return;
}
setAnthropicOAuthMessage(null);
setAnthropicOAuthState(null);
setAnthropicOAuthCodeInput("");
const popup = window.open(
"",
"spacebot-anthropic-oauth",
"popup=true,width=780,height=960,noopener,noreferrer",
);
try {
const result = await startAnthropicOAuthMutation.mutateAsync({
model: anthropicOAuthModel.trim(),
});
if (!result.success || !result.authorize_url || !result.state) {
popup?.close();
setAnthropicOAuthMessage({
text: result.message || "Failed to start OAuth flow",
type: "error",
});
return;
}
setAnthropicOAuthState(result.state);
if (popup) {
popup.location.href = result.authorize_url;
} else {
window.location.assign(result.authorize_url);
}
} catch (error: any) {
popup?.close();
setAnthropicOAuthMessage({ text: `Failed: ${error.message}`, type: "error" });
}
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@interface/src/routes/Settings.tsx` around lines 576 - 603, Open the popup
synchronously before awaiting the network call to avoid browser popup blocking:
in handleStartAnthropicOAuth, call window.open with an empty URL (e.g.,
window.open("", "spacebot-anthropic-oauth", "popup=true,width=780,height=960"))
and keep the returned popup reference (do NOT include "noopener" or "noreferrer"
so the reference is usable); then call
startAnthropicOAuthMutation.mutateAsync({...}); after the response, if success
setAnthropicOAuthState(result.state) and navigate the popup by assigning
popup.location.href = result.authorize_url; if the request fails or the response
is invalid, close the popup (popup.close()) and setAnthropicOAuthMessage
appropriately (use existing error messaging logic); ensure you still clear/set
anthropicOAuthCodeInput and anthropicOAuthMessage as before.

Comment on lines +963 to +977
if provider == "anthropic-oauth" {
let instance_dir = (**state.instance_dir.load()).clone();
let cred_path = crate::auth::credentials_path(&instance_dir);
if cred_path.exists() {
tokio::fs::remove_file(&cred_path)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
}
if let Some(mgr) = state.llm_manager.read().await.as_ref() {
mgr.clear_anthropic_oauth_credentials().await;
}
return Ok(Json(ProviderUpdateResponse {
success: true,
message: "Anthropic OAuth credentials removed".into(),
}));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Notify the runtime after removing Anthropic OAuth.

This branch clears the file and cached token, then returns without sending ProvidersConfigured. The add/update/exchange paths in this file all broadcast after credential changes, so live agents won't observe the fallback from OAuth to API key (or no provider) until something else reloads them.

Suggested fix
         if let Some(mgr) = state.llm_manager.read().await.as_ref() {
             mgr.clear_anthropic_oauth_credentials().await;
         }
+        state
+            .provider_setup_tx
+            .try_send(crate::ProviderSetupEvent::ProvidersConfigured)
+            .ok();
         return Ok(Json(ProviderUpdateResponse {
             success: true,
             message: "Anthropic OAuth credentials removed".into(),
         }));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if provider == "anthropic-oauth" {
let instance_dir = (**state.instance_dir.load()).clone();
let cred_path = crate::auth::credentials_path(&instance_dir);
if cred_path.exists() {
tokio::fs::remove_file(&cred_path)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
}
if let Some(mgr) = state.llm_manager.read().await.as_ref() {
mgr.clear_anthropic_oauth_credentials().await;
}
return Ok(Json(ProviderUpdateResponse {
success: true,
message: "Anthropic OAuth credentials removed".into(),
}));
if provider == "anthropic-oauth" {
let instance_dir = (**state.instance_dir.load()).clone();
let cred_path = crate::auth::credentials_path(&instance_dir);
if cred_path.exists() {
tokio::fs::remove_file(&cred_path)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
}
if let Some(mgr) = state.llm_manager.read().await.as_ref() {
mgr.clear_anthropic_oauth_credentials().await;
}
state
.provider_setup_tx
.try_send(crate::ProviderSetupEvent::ProvidersConfigured)
.ok();
return Ok(Json(ProviderUpdateResponse {
success: true,
message: "Anthropic OAuth credentials removed".into(),
}));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/api/providers.rs` around lines 963 - 977, After removing the Anthropic
OAuth file and clearing the cached token in the branch that handles provider ==
"anthropic-oauth" (where you call mgr.clear_anthropic_oauth_credentials()), also
notify the runtime by sending the same ProvidersConfigured broadcast used in the
add/update/exchange paths so live agents reload their config; locate the
existing code that sends the ProvidersConfigured event and invoke that same
broadcast/send logic immediately before returning the ProviderUpdateResponse to
ensure agents observe the fallback from OAuth to API key/no provider.

Comment on lines +1199 to +1212
let model = request.model.trim().to_string();
if model.is_empty() {
return Ok(Json(AnthropicOAuthStartResponse {
success: false,
message: "Model cannot be empty".to_string(),
authorize_url: None,
state: None,
}));
}

let mode = match request.mode.as_deref() {
Some("console") => crate::auth::AuthMode::Console,
_ => crate::auth::AuthMode::Max,
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Reject cross-provider models here too.

This endpoint only checks for empty input. A caller can start an Anthropic OAuth session with a non-Anthropic model string, and Line 1312 will later write that value into routing. update_provider and test_provider_model already enforce provider/model alignment; this path should do the same.

Suggested fix
     if model.is_empty() {
         return Ok(Json(AnthropicOAuthStartResponse {
             success: false,
             message: "Model cannot be empty".to_string(),
             authorize_url: None,
             state: None,
         }));
     }
+    if !model_matches_provider("anthropic", &model) {
+        return Ok(Json(AnthropicOAuthStartResponse {
+            success: false,
+            message: format!("Model '{}' must use provider 'anthropic'.", request.model),
+            authorize_url: None,
+            state: None,
+        }));
+    }
 
     let mode = match request.mode.as_deref() {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/api/providers.rs` around lines 1199 - 1212, The endpoint currently only
checks for an empty model but must also reject models that don't belong to
Anthropic; update the validation in the handler that reads request.model (the
same block using model.trim()) to verify provider/model alignment and return an
error Json(AnthropicOAuthStartResponse { success: false, message: "...", ... })
when the model is not an Anthropic model. Mirror the same provider/model check
logic used by update_provider and test_provider_model (reuse or call their
validation helper if available) so non-Anthropic model strings are rejected
before proceeding to create the OAuth session.

Comment on lines +1284 to +1323
// Save credentials to disk
let instance_dir = (**state.instance_dir.load()).clone();
if let Err(error) = crate::auth::save_credentials(&instance_dir, &credentials) {
tracing::warn!(%error, "failed to save Anthropic OAuth credentials");
return Ok(Json(AnthropicOAuthExchangeResponse {
success: false,
message: format!("Failed to save credentials: {error}"),
}));
}

// Update the LLM manager
if let Some(llm_manager) = state.llm_manager.read().await.as_ref() {
llm_manager
.set_anthropic_oauth_credentials(credentials)
.await;
}

// Update model routing in config.toml
let config_path = state.config_path.read().await.clone();
let content = if config_path.exists() {
tokio::fs::read_to_string(&config_path)
.await
.unwrap_or_default()
} else {
String::new()
};

if let Ok(mut doc) = content.parse::<toml_edit::DocumentMut>() {
apply_model_routing(&mut doc, &session.model);
if let Err(error) = tokio::fs::write(&config_path, doc.to_string()).await {
tracing::warn!(%error, "failed to write config.toml after Anthropic OAuth");
}
}

refresh_defaults_config(&state).await;

state
.provider_setup_tx
.try_send(crate::ProviderSetupEvent::ProvidersConfigured)
.ok();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't claim routing was applied when the config update failed.

Lines 1311-1315 currently log and continue on TOML parse/write errors, but the handler still refreshes defaults, emits ProvidersConfigured, and returns a success message saying the model was applied. That leaves the instance half-configured: OAuth credentials are live, but routing may still point at the old model. Make the routing write part of the success path, or roll the saved credentials back if it fails.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/api/providers.rs` around lines 1284 - 1323, The handler currently saves
credentials and updates the LLM manager before attempting to parse/write
config.toml, then proceeds to refresh_defaults_config and emit provider_setup_tx
even if the TOML write failed; fix by making the routing write an explicit
success gate: perform apply_model_routing and the tokio::fs::write(&config_path,
...) first (using content.parse::<toml_edit::DocumentMut>() and writing
doc.to_string()), and only if that parse+write succeeds then call
crate::auth::save_credentials, llm_manager.set_anthropic_oauth_credentials,
refresh_defaults_config(&state).await, and provider_setup_tx.try_send(...); if
the TOML write fails return an error response immediately (and do not call
refresh_defaults_config or emit ProvidersConfigured). This reordering ensures
config routing is actually applied before committing credentials/manager
updates.

- Replace window.open() with openExternal() helper that uses the Tauri
  shell plugin to open OAuth URLs in the system browser. window.open()
  in Tauri opens a webview instead of the default browser, breaking the
  OAuth flow in the desktop app. This also fixes the same latent bug in
  the existing OpenAI OAuth flow.
- Prefix unused `state` parameter in start_anthropic_oauth with
  underscore to suppress compiler warning.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (5)
src/api/providers.rs (4)

1112-1138: ⚠️ Potential issue | 🟡 Minor

Keep CLI auth detection conservative when auth status fails.

The Err(_) => (true, None) fallback reports an authenticated CLI even when the command could not run, so the UI can advertise a streamlined sign-in that does not actually exist.

Suggested fix
-        Err(_) => {
-            // Command failed to run but binary exists — assume authed (older CLI).
-            (true, None)
-        }
+        Err(error) => {
+            tracing::debug!(%error, "claude auth status failed");
+            (false, None)
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/api/providers.rs` around lines 1112 - 1138, The match over auth_result is
too permissive: the Err(_) arm currently returns (true, None) which falsely
reports the CLI as authenticated when the auth status command failed to run;
update the Err(_) arm in the auth_result match to return (false, None) (and
optionally log the error) so that failures to execute/authenticate are treated
conservatively as unauthenticated; refer to the auth_result match block and the
logged_in/email extraction logic to locate the change.

963-977: ⚠️ Potential issue | 🟠 Major

Emit ProvidersConfigured after removing Anthropic OAuth.

This branch clears the persisted token and in-memory OAuth override, then returns without the broadcast used by the add/update/exchange paths. Live agents will keep using the stale provider state until some other reload happens.

Suggested fix
         if let Some(mgr) = state.llm_manager.read().await.as_ref() {
             mgr.clear_anthropic_oauth_credentials().await;
         }
+        state
+            .provider_setup_tx
+            .try_send(crate::ProviderSetupEvent::ProvidersConfigured)
+            .ok();
         return Ok(Json(ProviderUpdateResponse {
             success: true,
             message: "Anthropic OAuth credentials removed".into(),
         }));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/api/providers.rs` around lines 963 - 977, In the "anthropic-oauth"
removal branch in providers.rs (the block handling provider ==
"anthropic-oauth"), after removing the credential file and calling
mgr.clear_anthropic_oauth_credentials().await, emit the same ProvidersConfigured
broadcast used by the add/update/exchange paths so live agents reload;
specifically construct/send the ProvidersConfigured event via the existing
provider broadcaster (e.g., call
state.provider_broadcaster.send(ProvidersConfigured) or use the same helper used
elsewhere), handle/ignore the send result as other branches do, then return the
existing Ok(Json(ProviderUpdateResponse { ... })) response.

1199-1212: ⚠️ Potential issue | 🟠 Major

Reject non-Anthropic models before creating the PKCE session.

update_provider and test_provider_model already enforce provider/model alignment, but this path still accepts any non-empty model string. A caller can start Anthropic OAuth with a different provider model and have it written into routing during exchange.

Suggested fix
     if model.is_empty() {
         return Ok(Json(AnthropicOAuthStartResponse {
             success: false,
             message: "Model cannot be empty".to_string(),
             authorize_url: None,
             state: None,
         }));
     }
+    if !model_matches_provider("anthropic", &model) {
+        return Ok(Json(AnthropicOAuthStartResponse {
+            success: false,
+            message: format!("Model '{}' must use provider 'anthropic'.", request.model),
+            authorize_url: None,
+            state: None,
+        }));
+    }
 
     let mode = match request.mode.as_deref() {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/api/providers.rs` around lines 1199 - 1212, This path currently accepts
any non-empty request.model; before creating the PKCE session, validate that the
requested model is an Anthropic model and reject others. Use the same validation
used by update_provider/test_provider_model (or call test_provider_model) to
verify provider/model alignment; if the model is not valid for Anthropic return
the existing error Json response (success: false, message). Update the block
that sets let model = request.model.trim().to_string(); to perform this check
and only proceed to compute mode (crate::auth::AuthMode) and create the PKCE
session when the model is confirmed Anthropic.

1284-1329: ⚠️ Potential issue | 🟠 Major

Don't report “model applied” unless the routing write succeeded.

This handler saves OAuth credentials and updates llm_manager before the config read/parse/write, then treats routing changes as best-effort (unwrap_or_default(), ignored parse failure, warning-only write failure) while still returning success. That leaves the instance half-configured: OAuth is live, but routing may still point at the old model. As per coding guidelines, "Don't silently discard errors. No let _ = on Results. Handle them, log them, or propagate them."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/api/providers.rs` around lines 1284 - 1329, The handler currently saves
credentials (crate::auth::save_credentials) and updates the LLM manager
(llm_manager.set_anthropic_oauth_credentials) but treats model routing changes
as best-effort (content.parse::<toml_edit::DocumentMut>(), apply_model_routing,
tokio::fs::write) while still returning a success response that claims the model
was applied; change this so routing failures are not silently ignored: check the
parse and write Results, log detailed errors, and if applying routing fails set
the AnthropicOAuthExchangeResponse to success: false (and include the error in
message) or otherwise only report "Model '...' applied to routing" when
tokio::fs::write succeeded; do not use unwrap_or_default/ignored errors—handle
or propagate them and ensure the response accurately reflects whether
apply_model_routing and the config write completed.
interface/src/routes/Settings.tsx (1)

591-605: ⚠️ Potential issue | 🔴 Critical

Open the Anthropic auth window before the await in web builds, and await the launcher.

In non-Tauri builds openExternal() falls back to window.open(), but this handler only calls it after mutateAsync() resolves. Browsers can block that popup, and because the promise is not awaited, a Tauri shell-open failure will not hit this catch either. The dialog can advance to code-entry even though no authorization page actually opened.

Do browsers preserve user activation for `window.open()` after awaiting a network request in a click handler, and does a surrounding `try/catch` catch errors from an async function if that promise is not awaited?
Suggested fix
 	const handleStartAnthropicOAuth = async () => {
 		if (!anthropicOAuthModel.trim()) {
 			setAnthropicOAuthMessage({ text: "Please select a model first", type: "error" });
 			return;
 		}
 		setAnthropicOAuthMessage(null);
 		setAnthropicOAuthState(null);
 		setAnthropicOAuthCodeInput("");
+		const popup = !IS_TAURI
+			? window.open("", "spacebot-anthropic-oauth", "popup=true,width=780,height=960")
+			: null;
 		try {
 			const result = await startAnthropicOAuthMutation.mutateAsync({
 				model: anthropicOAuthModel.trim(),
 			});
 			if (!result.success || !result.authorize_url || !result.state) {
+				popup?.close();
 				setAnthropicOAuthMessage({
 					text: result.message || "Failed to start OAuth flow",
 					type: "error",
 				});
 				return;
 			}
-			setAnthropicOAuthState(result.state);
-			openExternal(result.authorize_url);
+			if (IS_TAURI) {
+				await openExternal(result.authorize_url);
+				setAnthropicOAuthState(result.state);
+			} else if (popup) {
+				popup.location.href = result.authorize_url;
+				setAnthropicOAuthState(result.state);
+			} else {
+				setAnthropicOAuthMessage({
+					text: "Popup was blocked. Please allow popups and try again.",
+					type: "error",
+				});
+				return;
+			}
 		} catch (error: any) {
+			popup?.close();
 			setAnthropicOAuthMessage({ text: `Failed: ${error.message}`, type: "error" });
 		}
 	};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@interface/src/routes/Settings.tsx` around lines 591 - 605, The click handler
currently awaits startAnthropicOAuthMutation.mutateAsync before calling
openExternal, which can cause browsers to block window.open and also won't
surface Tauri open errors; change the flow in the handler so you open the auth
window via openExternal (or call a web-specific window.open) before awaiting
startAnthropicOAuthMutation.mutateAsync, capture the returned window or promise
(so you can await the launcher for Tauri), then await mutateAsync, and only
after validate result call setAnthropicOAuthState and setAnthropicOAuthMessage
as needed; ensure you await the openExternal/launcher and wrap both operations
in the existing try/catch so any shell-open or window-open errors are caught and
shown via setAnthropicOAuthMessage.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@interface/src/routes/Settings.tsx`:
- Around line 3045-3077: The start form disappears when a start attempt fails
because the JSX only shows the ModelSelect/Button when !hasState && !message;
change the rendering so the error message does not hide the start form: render
the {message} block independently (remove the !hasState guard or move it above
the form) and keep the ModelSelect, onModelChange, modelValue, onStartOAuth
Button and isRequesting controls visible when hasState is false so users can
retry without closing the dialog.

---

Duplicate comments:
In `@interface/src/routes/Settings.tsx`:
- Around line 591-605: The click handler currently awaits
startAnthropicOAuthMutation.mutateAsync before calling openExternal, which can
cause browsers to block window.open and also won't surface Tauri open errors;
change the flow in the handler so you open the auth window via openExternal (or
call a web-specific window.open) before awaiting
startAnthropicOAuthMutation.mutateAsync, capture the returned window or promise
(so you can await the launcher for Tauri), then await mutateAsync, and only
after validate result call setAnthropicOAuthState and setAnthropicOAuthMessage
as needed; ensure you await the openExternal/launcher and wrap both operations
in the existing try/catch so any shell-open or window-open errors are caught and
shown via setAnthropicOAuthMessage.

In `@src/api/providers.rs`:
- Around line 1112-1138: The match over auth_result is too permissive: the
Err(_) arm currently returns (true, None) which falsely reports the CLI as
authenticated when the auth status command failed to run; update the Err(_) arm
in the auth_result match to return (false, None) (and optionally log the error)
so that failures to execute/authenticate are treated conservatively as
unauthenticated; refer to the auth_result match block and the logged_in/email
extraction logic to locate the change.
- Around line 963-977: In the "anthropic-oauth" removal branch in providers.rs
(the block handling provider == "anthropic-oauth"), after removing the
credential file and calling mgr.clear_anthropic_oauth_credentials().await, emit
the same ProvidersConfigured broadcast used by the add/update/exchange paths so
live agents reload; specifically construct/send the ProvidersConfigured event
via the existing provider broadcaster (e.g., call
state.provider_broadcaster.send(ProvidersConfigured) or use the same helper used
elsewhere), handle/ignore the send result as other branches do, then return the
existing Ok(Json(ProviderUpdateResponse { ... })) response.
- Around line 1199-1212: This path currently accepts any non-empty
request.model; before creating the PKCE session, validate that the requested
model is an Anthropic model and reject others. Use the same validation used by
update_provider/test_provider_model (or call test_provider_model) to verify
provider/model alignment; if the model is not valid for Anthropic return the
existing error Json response (success: false, message). Update the block that
sets let model = request.model.trim().to_string(); to perform this check and
only proceed to compute mode (crate::auth::AuthMode) and create the PKCE session
when the model is confirmed Anthropic.
- Around line 1284-1329: The handler currently saves credentials
(crate::auth::save_credentials) and updates the LLM manager
(llm_manager.set_anthropic_oauth_credentials) but treats model routing changes
as best-effort (content.parse::<toml_edit::DocumentMut>(), apply_model_routing,
tokio::fs::write) while still returning a success response that claims the model
was applied; change this so routing failures are not silently ignored: check the
parse and write Results, log detailed errors, and if applying routing fails set
the AnthropicOAuthExchangeResponse to success: false (and include the error in
message) or otherwise only report "Model '...' applied to routing" when
tokio::fs::write succeeded; do not use unwrap_or_default/ignored errors—handle
or propagate them and ensure the response accurately reflects whether
apply_model_routing and the config write completed.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f3c9b01c-2433-4f46-ac08-735de1159825

📥 Commits

Reviewing files that changed from the base of the PR and between eedc001 and ec7f86e.

📒 Files selected for processing (2)
  • interface/src/routes/Settings.tsx
  • src/api/providers.rs

Comment on lines +3045 to +3077
{message && !hasState && (
<div
className={`rounded-md border px-3 py-2 text-sm ${message.type === "success"
? "border-green-500/20 bg-green-500/10 text-green-400"
: "border-red-500/20 bg-red-500/10 text-red-400"
}`}
>
{message.text}
</div>
)}

{!hasState && !message && (
<div className="space-y-3">
<p className="text-sm text-ink-dull">
Choose the model to use, then authorize access in a browser window.
</p>
<ModelSelect
label="Model"
description="Pick the model to apply to routing"
value={modelValue}
onChange={onModelChange}
provider="anthropic"
/>
<Button
onClick={onStartOAuth}
disabled={!modelValue.trim()}
loading={isRequesting}
variant="outline"
size="sm"
>
Open authorization page
</Button>
</div>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Keep the start form visible after a start error.

When the start call fails, hasState stays false but message becomes truthy, so the model picker and “Open authorization page” button disappear. Users have to close and reopen the dialog just to retry.

Suggested fix
-					{message && !hasState && (
-						<div
-							className={`rounded-md border px-3 py-2 text-sm ${message.type === "success"
-								? "border-green-500/20 bg-green-500/10 text-green-400"
-								: "border-red-500/20 bg-red-500/10 text-red-400"
-							}`}
-						>
-							{message.text}
-						</div>
-					)}
-
-					{!hasState && !message && (
+					{!hasState && (
 						<div className="space-y-3">
+							{message && (
+								<div
+									className={`rounded-md border px-3 py-2 text-sm ${message.type === "success"
+										? "border-green-500/20 bg-green-500/10 text-green-400"
+										: "border-red-500/20 bg-red-500/10 text-red-400"
+									}`}
+								>
+									{message.text}
+								</div>
+							)}
 							<p className="text-sm text-ink-dull">
 								Choose the model to use, then authorize access in a browser window.
 							</p>
 							<ModelSelect
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{message && !hasState && (
<div
className={`rounded-md border px-3 py-2 text-sm ${message.type === "success"
? "border-green-500/20 bg-green-500/10 text-green-400"
: "border-red-500/20 bg-red-500/10 text-red-400"
}`}
>
{message.text}
</div>
)}
{!hasState && !message && (
<div className="space-y-3">
<p className="text-sm text-ink-dull">
Choose the model to use, then authorize access in a browser window.
</p>
<ModelSelect
label="Model"
description="Pick the model to apply to routing"
value={modelValue}
onChange={onModelChange}
provider="anthropic"
/>
<Button
onClick={onStartOAuth}
disabled={!modelValue.trim()}
loading={isRequesting}
variant="outline"
size="sm"
>
Open authorization page
</Button>
</div>
{!hasState && (
<div className="space-y-3">
{message && (
<div
className={`rounded-md border px-3 py-2 text-sm ${message.type === "success"
? "border-green-500/20 bg-green-500/10 text-green-400"
: "border-red-500/20 bg-red-500/10 text-red-400"
}`}
>
{message.text}
</div>
)}
<p className="text-sm text-ink-dull">
Choose the model to use, then authorize access in a browser window.
</p>
<ModelSelect
label="Model"
description="Pick the model to apply to routing"
value={modelValue}
onChange={onModelChange}
provider="anthropic"
/>
<Button
onClick={onStartOAuth}
disabled={!modelValue.trim()}
loading={isRequesting}
variant="outline"
size="sm"
>
Open authorization page
</Button>
</div>
)}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@interface/src/routes/Settings.tsx` around lines 3045 - 3077, The start form
disappears when a start attempt fails because the JSX only shows the
ModelSelect/Button when !hasState && !message; change the rendering so the error
message does not hide the start form: render the {message} block independently
(remove the !hasState guard or move it above the form) and keep the ModelSelect,
onModelChange, modelValue, onStartOAuth Button and isRequesting controls visible
when hasState is false so users can retry without closing the dialog.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant