@@ -195,27 +390,6 @@ export function SettingsModal({ isOpen, onClose }: SettingsModalProps) {
/>
diff --git a/ui/src/hooks/useExpandChat.ts b/ui/src/hooks/useExpandChat.ts
index 91508852..be632a54 100644
--- a/ui/src/hooks/useExpandChat.ts
+++ b/ui/src/hooks/useExpandChat.ts
@@ -107,16 +107,20 @@ export function useExpandChat({
}, 30000)
}
- ws.onclose = () => {
+ ws.onclose = (event) => {
setConnectionStatus('disconnected')
if (pingIntervalRef.current) {
clearInterval(pingIntervalRef.current)
pingIntervalRef.current = null
}
+ // Don't retry on application-level errors (4xxx codes won't resolve on retry)
+ const isAppError = event.code >= 4000 && event.code <= 4999
+
// Attempt reconnection if not intentionally closed
if (
!manuallyDisconnectedRef.current &&
+ !isAppError &&
reconnectAttempts.current < maxReconnectAttempts &&
!isCompleteRef.current
) {
diff --git a/ui/src/hooks/useProjects.ts b/ui/src/hooks/useProjects.ts
index 676311cd..c2abe01a 100644
--- a/ui/src/hooks/useProjects.ts
+++ b/ui/src/hooks/useProjects.ts
@@ -4,7 +4,7 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import * as api from '../lib/api'
-import type { FeatureCreate, FeatureUpdate, ModelsResponse, ProjectSettingsUpdate, Settings, SettingsUpdate } from '../lib/types'
+import type { FeatureCreate, FeatureUpdate, ModelsResponse, ProjectSettingsUpdate, ProvidersResponse, Settings, SettingsUpdate } from '../lib/types'
// ============================================================================
// Projects
@@ -268,6 +268,27 @@ const DEFAULT_SETTINGS: Settings = {
testing_agent_ratio: 1,
playwright_headless: true,
batch_size: 3,
+ api_provider: 'claude',
+ api_base_url: null,
+ api_has_auth_token: false,
+ api_model: null,
+}
+
+const DEFAULT_PROVIDERS: ProvidersResponse = {
+ providers: [
+ { id: 'claude', name: 'Claude (Anthropic)', base_url: null, models: DEFAULT_MODELS.models, default_model: 'claude-opus-4-5-20251101', requires_auth: false },
+ ],
+ current: 'claude',
+}
+
+export function useAvailableProviders() {
+ return useQuery({
+ queryKey: ['available-providers'],
+ queryFn: api.getAvailableProviders,
+ staleTime: 300000,
+ retry: 1,
+ placeholderData: DEFAULT_PROVIDERS,
+ })
}
export function useAvailableModels() {
@@ -319,6 +340,8 @@ export function useUpdateSettings() {
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['settings'] })
+ queryClient.invalidateQueries({ queryKey: ['available-models'] })
+ queryClient.invalidateQueries({ queryKey: ['available-providers'] })
},
})
}
diff --git a/ui/src/hooks/useSpecChat.ts b/ui/src/hooks/useSpecChat.ts
index b2bac628..3bd09bb2 100644
--- a/ui/src/hooks/useSpecChat.ts
+++ b/ui/src/hooks/useSpecChat.ts
@@ -157,15 +157,18 @@ export function useSpecChat({
}, 30000)
}
- ws.onclose = () => {
+ ws.onclose = (event) => {
setConnectionStatus('disconnected')
if (pingIntervalRef.current) {
clearInterval(pingIntervalRef.current)
pingIntervalRef.current = null
}
+ // Don't retry on application-level errors (4xxx codes won't resolve on retry)
+ const isAppError = event.code >= 4000 && event.code <= 4999
+
// Attempt reconnection if not intentionally closed
- if (reconnectAttempts.current < maxReconnectAttempts && !isCompleteRef.current) {
+ if (!isAppError && reconnectAttempts.current < maxReconnectAttempts && !isCompleteRef.current) {
reconnectAttempts.current++
const delay = Math.min(1000 * Math.pow(2, reconnectAttempts.current), 10000)
reconnectTimeoutRef.current = window.setTimeout(connect, delay)
diff --git a/ui/src/hooks/useWebSocket.ts b/ui/src/hooks/useWebSocket.ts
index 1a444359..b9c0a3fe 100644
--- a/ui/src/hooks/useWebSocket.ts
+++ b/ui/src/hooks/useWebSocket.ts
@@ -335,10 +335,14 @@ export function useProjectWebSocket(projectName: string | null) {
}
}
- ws.onclose = () => {
+ ws.onclose = (event) => {
setState(prev => ({ ...prev, isConnected: false }))
wsRef.current = null
+ // Don't retry on application-level errors (4xxx codes won't resolve on retry)
+ const isAppError = event.code >= 4000 && event.code <= 4999
+ if (isAppError) return
+
// Exponential backoff reconnection
const delay = Math.min(1000 * Math.pow(2, reconnectAttempts.current), 30000)
reconnectAttempts.current++
diff --git a/ui/src/lib/api.ts b/ui/src/lib/api.ts
index 48ce30a8..10b577b4 100644
--- a/ui/src/lib/api.ts
+++ b/ui/src/lib/api.ts
@@ -24,6 +24,7 @@ import type {
Settings,
SettingsUpdate,
ModelsResponse,
+ ProvidersResponse,
DevServerStatusResponse,
DevServerConfig,
TerminalInfo,
@@ -399,6 +400,10 @@ export async function getAvailableModels(): Promise
{
return fetchJSON('/settings/models')
}
+export async function getAvailableProviders(): Promise {
+ return fetchJSON('/settings/providers')
+}
+
export async function getSettings(): Promise {
return fetchJSON('/settings')
}
diff --git a/ui/src/lib/types.ts b/ui/src/lib/types.ts
index cec91ec8..b75d6146 100644
--- a/ui/src/lib/types.ts
+++ b/ui/src/lib/types.ts
@@ -525,6 +525,20 @@ export interface ModelsResponse {
default: string
}
+export interface ProviderInfo {
+ id: string
+ name: string
+ base_url: string | null
+ models: ModelInfo[]
+ default_model: string
+ requires_auth: boolean
+}
+
+export interface ProvidersResponse {
+ providers: ProviderInfo[]
+ current: string
+}
+
export interface Settings {
yolo_mode: boolean
model: string
@@ -533,6 +547,10 @@ export interface Settings {
testing_agent_ratio: number // Regression testing agents (0-3)
playwright_headless: boolean
batch_size: number // Features per coding agent batch (1-3)
+ api_provider: string
+ api_base_url: string | null
+ api_has_auth_token: boolean
+ api_model: string | null
}
export interface SettingsUpdate {
@@ -541,6 +559,10 @@ export interface SettingsUpdate {
testing_agent_ratio?: number
playwright_headless?: boolean
batch_size?: number
+ api_provider?: string
+ api_base_url?: string
+ api_auth_token?: string
+ api_model?: string
}
export interface ProjectSettingsUpdate {