diff --git a/components/repo-selector.tsx b/components/repo-selector.tsx index aee07d91..c898638b 100644 --- a/components/repo-selector.tsx +++ b/components/repo-selector.tsx @@ -5,8 +5,9 @@ import Image from 'next/image' import { Input } from '@/components/ui/input' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { Lock, Loader2 } from 'lucide-react' -import { useAtomValue, useSetAtom } from 'jotai' +import { useAtomValue, useSetAtom, useAtom } from 'jotai' import { githubConnectionAtom } from '@/lib/atoms/github-connection' +import { githubOwnersAtom, githubReposAtomFamily } from '@/lib/atoms/github-cache' interface GitHubOwner { login: string @@ -42,19 +43,8 @@ export function RepoSelector({ }: RepoSelectorProps) { const [repoFilter, setRepoFilter] = useState('') // Initialize with selected owner to prevent flash - const [owners, setOwners] = useState(() => { - if (selectedOwner) { - return [ - { - login: selectedOwner, - name: selectedOwner, - avatar_url: `https://github.com/${selectedOwner}.png`, - }, - ] - } - return [] - }) - const [repos, setRepos] = useState([]) + const [owners, setOwners] = useAtom(githubOwnersAtom) + const [repos, setRepos] = useAtom(githubReposAtomFamily(selectedOwner)) const [loadingOwners, setLoadingOwners] = useState(true) const [loadingRepos, setLoadingRepos] = useState(false) const [repoDropdownOpen, setRepoDropdownOpen] = useState(false) @@ -72,17 +62,13 @@ export function RepoSelector({ useEffect(() => { // If GitHub was disconnected, clear data and cache if (githubConnectionRef.current && !githubConnection.connected) { - // Clear cache - localStorage.removeItem('github-owners') - Object.keys(localStorage).forEach((key) => { - if (key.startsWith('github-repos-')) { - localStorage.removeItem(key) - } - }) + // Clear cache using atoms + setOwners(null) + // Clear all repos - we need to iterate through all possible owners + // Since we can't clear all atomFamily members easily, we'll just clear the current one + setRepos(null) // Clear state - setOwners([]) - setRepos([]) onOwnerChange('') onRepoChange('') } @@ -90,12 +76,12 @@ export function RepoSelector({ // If GitHub was reconnected, reload owners if (!githubConnectionRef.current && githubConnection.connected) { setLoadingOwners(true) - setOwners([]) - setRepos([]) + setOwners(null) + setRepos(null) } githubConnectionRef.current = githubConnection.connected - }, [githubConnection.connected, onOwnerChange, onRepoChange]) + }, [githubConnection.connected, onOwnerChange, onRepoChange, setOwners, setRepos]) // Load owners on component mount and when GitHub is connected useEffect(() => { @@ -107,17 +93,14 @@ export function RepoSelector({ const loadOwners = async () => { try { // Only show loading state if we don't have owners yet - if (owners.length === 0) { + if (!owners || owners.length === 0) { setLoadingOwners(true) } else { setIsRefreshing(true) } // Check cache first - but only use it if we're not forcing a refresh - const cachedOwners = localStorage.getItem('github-owners') - if (cachedOwners && owners.length === 0) { - const parsedOwners = JSON.parse(cachedOwners) - setOwners(parsedOwners) + if (owners && owners.length > 0) { setLoadingOwners(false) // Continue fetching in background to update } @@ -128,13 +111,8 @@ export function RepoSelector({ // Check for authentication errors - disconnect GitHub if auth fails if (!userResponse.ok) { if (userResponse.status === 401 || userResponse.status === 403) { - // Clear cache - localStorage.removeItem('github-owners') - Object.keys(localStorage).forEach((key) => { - if (key.startsWith('github-repos-')) { - localStorage.removeItem(key) - } - }) + // Clear cache using atoms + setOwners(null) // Call backend to disconnect GitHub try { @@ -183,8 +161,7 @@ export function RepoSelector({ sortedOwners.push(...organizations) setOwners(sortedOwners) - // Cache the owners - localStorage.setItem('github-owners', JSON.stringify(sortedOwners)) + // Cache is automatic with atomWithStorage } catch (error) { console.error('Error loading owners:', error) @@ -208,15 +185,15 @@ export function RepoSelector({ loadOwners() // eslint-disable-next-line react-hooks/exhaustive-deps - }, [githubConnection.connected, setGitHubConnection]) + }, [githubConnection.connected, setGitHubConnection, setOwners]) // Auto-select user's personal account if no owner is selected and no saved owner exists useEffect(() => { - if (owners.length > 0 && !selectedOwner) { + if (owners && owners.length > 0 && !selectedOwner) { // Only auto-select if we have owners loaded and no owner is currently selected // This allows the parent component to set a saved owner from cookies first const timer = setTimeout(() => { - if (!selectedOwner && owners.length > 0) { + if (!selectedOwner && owners && owners.length > 0) { // Auto-select the first owner (user's personal account) // Since we add the user first in the loadOwners function, owners[0] will be the personal account onOwnerChange(owners[0].login) @@ -233,32 +210,20 @@ export function RepoSelector({ const loadRepos = async () => { try { // Check cache first - show cached data immediately if available - const cacheKey = `github-repos-${selectedOwner}` - const cachedRepos = localStorage.getItem(cacheKey) - if (cachedRepos && repos.length === 0) { - const parsedRepos = JSON.parse(cachedRepos) - setRepos(parsedRepos) + if (repos && repos.length > 0) { setLoadingRepos(false) // Continue fetching in background to update - } else if (!cachedRepos && repos.length === 0) { + } else { // Only show loading if we don't have cached data or existing repos setLoadingRepos(true) - } else if (repos.length > 0) { - // If we have repos, just refresh in background - setIsRefreshing(true) } const response = await fetch(`/api/github/repos?owner=${selectedOwner}`) if (!response.ok) { if (response.status === 401 || response.status === 403) { - // Clear cache - localStorage.removeItem('github-owners') - Object.keys(localStorage).forEach((key) => { - if (key.startsWith('github-repos-')) { - localStorage.removeItem(key) - } - }) + // Clear cache using atoms + setOwners(null) // Call backend to disconnect GitHub try { @@ -281,8 +246,7 @@ export function RepoSelector({ const reposList = await response.json() setRepos(reposList) - // Cache the repos - localStorage.setItem(cacheKey, JSON.stringify(reposList)) + // Cache is automatic with atomWithStorage } catch (error) { console.error('Error loading repos:', error) @@ -306,11 +270,11 @@ export function RepoSelector({ loadRepos() } else { - setRepos([]) + setRepos(null) setLoadingRepos(false) } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selectedOwner, setGitHubConnection]) + }, [selectedOwner, setGitHubConnection, setOwners, setRepos]) // Focus filter input when dropdown opens (but not on mobile to prevent keyboard popup) useEffect(() => { @@ -343,7 +307,7 @@ export function RepoSelector({ const hasMoreRepos = filteredRepos.length > 50 // Ensure selected repo is in the displayed list (if it matches current filter) - if (selectedRepo && repos.length > 0) { + if (selectedRepo && repos && repos.length > 0) { const isInFilteredRepos = filteredRepos.find((repo) => repo.name === selectedRepo) const isInDisplayedRepos = displayedRepos.find((repo) => repo.name === selectedRepo) @@ -357,7 +321,7 @@ export function RepoSelector({ onOwnerChange(value) onRepoChange('') // Reset repo when owner changes setRepoFilter('') // Reset filter when owner changes - setRepos([]) // Clear repos to trigger loading state for new owner + setRepos(null) // Clear repos to trigger loading state for new owner } const handleRepoChange = (value: string) => { @@ -375,11 +339,11 @@ export function RepoSelector({ : 'w-auto min-w-[160px] border-0 bg-transparent shadow-none focus:ring-0 h-8' // Find the selected owner for avatar display - const selectedOwnerData = owners.find((owner) => owner.login === selectedOwner) + const selectedOwnerData = owners?.find((owner) => owner.login === selectedOwner) // Determine if we should show loading indicators - const showOwnersLoading = loadingOwners && owners.length === 0 - const showReposLoading = loadingRepos && repos.length === 0 + const showOwnersLoading = loadingOwners && (!owners || owners.length === 0) + const showReposLoading = loadingRepos && (!repos || repos.length === 0) return (
@@ -409,20 +373,21 @@ export function RepoSelector({ )} - {owners.map((owner) => ( - -
- {owner.login} - {owner.login} -
-
- ))} + {owners && + owners.map((owner) => ( + +
+ {owner.login} + {owner.login} +
+
+ ))}
diff --git a/components/task-form.tsx b/components/task-form.tsx index ad86500c..79e9b587 100644 --- a/components/task-form.tsx +++ b/components/task-form.tsx @@ -22,8 +22,10 @@ import { setInstallDependencies, setMaxDuration, setKeepAlive } from '@/lib/util import { useConnectors } from '@/components/connectors-provider' import { ConnectorDialog } from '@/components/connectors/manage-connectors' import { toast } from 'sonner' -import { useAtom } from 'jotai' +import { useAtom, useAtomValue, useSetAtom } from 'jotai' import { taskPromptAtom } from '@/lib/atoms/task' +import { lastSelectedAgentAtom, lastSelectedModelAtomFamily } from '@/lib/atoms/agent-selection' +import { githubReposAtomFamily } from '@/lib/atoms/github-cache' import { useSearchParams } from 'next/navigation' interface GitHubRepo { @@ -159,10 +161,11 @@ export function TaskForm({ maxSandboxDuration = 300, }: TaskFormProps) { const [prompt, setPrompt] = useAtom(taskPromptAtom) - const [selectedAgent, setSelectedAgent] = useState('claude') + const [savedAgent, setSavedAgent] = useAtom(lastSelectedAgentAtom) + const [selectedAgent, setSelectedAgent] = useState(savedAgent || 'claude') const [selectedModel, setSelectedModel] = useState(DEFAULT_MODELS.claude) const [selectedModels, setSelectedModels] = useState([]) - const [repos, setRepos] = useState([]) + const [repos, setRepos] = useAtom(githubReposAtomFamily(selectedOwner)) const [, setLoadingRepos] = useState(false) // Options state - initialize with server values @@ -234,26 +237,10 @@ export function TaskForm({ setSelectedModel(urlModel) } } - } else { - // Fall back to localStorage - const savedAgent = localStorage.getItem('last-selected-agent') - if ( - savedAgent && - CODING_AGENTS.some((agent) => agent.value === savedAgent && !('isDivider' in agent && agent.isDivider)) - ) { + } else if (savedAgent) { + // Fall back to saved agent from Jotai atom + if (CODING_AGENTS.some((agent) => agent.value === savedAgent && !('isDivider' in agent && agent.isDivider))) { setSelectedAgent(savedAgent) - - // Load saved model for this agent - const savedModel = localStorage.getItem(`last-selected-model-${savedAgent}`) - const agentModels = AGENT_MODELS[savedAgent as keyof typeof AGENT_MODELS] - if (savedModel && agentModels?.some((model) => model.value === savedModel)) { - setSelectedModel(savedModel) - } else { - const defaultModel = DEFAULT_MODELS[savedAgent as keyof typeof DEFAULT_MODELS] - if (defaultModel) { - setSelectedModel(defaultModel) - } - } } } @@ -266,6 +253,11 @@ export function TaskForm({ // eslint-disable-next-line react-hooks/exhaustive-deps }, []) + // Get saved model atom for current agent + const savedModelAtom = lastSelectedModelAtomFamily(selectedAgent) + const savedModel = useAtomValue(savedModelAtom) + const setSavedModel = useSetAtom(savedModelAtom) + // Update model when agent changes useEffect(() => { if (selectedAgent) { @@ -275,7 +267,6 @@ export function TaskForm({ } // Load saved model for this agent or use default - const savedModel = localStorage.getItem(`last-selected-model-${selectedAgent}`) const agentModels = AGENT_MODELS[selectedAgent as keyof typeof AGENT_MODELS] if (savedModel && agentModels?.some((model) => model.value === savedModel)) { setSelectedModel(savedModel) @@ -286,41 +277,28 @@ export function TaskForm({ } } } - }, [selectedAgent]) + }, [selectedAgent, savedModel]) // Fetch repositories when owner changes useEffect(() => { if (!selectedOwner) { - setRepos([]) + setRepos(null) return } const fetchRepos = async () => { setLoadingRepos(true) try { - // Check cache first - const cacheKey = `github-repos-${selectedOwner}` - const cachedRepos = localStorage.getItem(cacheKey) - - if (cachedRepos) { - try { - const parsedRepos = JSON.parse(cachedRepos) - setRepos(parsedRepos) - setLoadingRepos(false) - return - } catch { - console.warn('Failed to parse cached repos, fetching fresh data') - localStorage.removeItem(cacheKey) - } + // Check cache first (repos is from the atom) + if (repos && repos.length > 0) { + setLoadingRepos(false) + return } const response = await fetch(`/api/github/repos?owner=${selectedOwner}`) if (response.ok) { const reposList = await response.json() setRepos(reposList) - - // Cache the results - localStorage.setItem(cacheKey, JSON.stringify(reposList)) } } catch (error) { console.error('Error fetching repositories:', error) @@ -330,7 +308,7 @@ export function TaskForm({ } fetchRepos() - }, [selectedOwner]) + }, [selectedOwner, repos, setRepos]) const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() @@ -362,7 +340,7 @@ export function TaskForm({ // Check if API key is required and available for the selected agent and model // Skip this check if we don't have repo data (likely not signed in) or if multi-agent mode - const selectedRepoData = repos.find((repo) => repo.name === selectedRepo) + const selectedRepoData = repos?.find((repo) => repo.name === selectedRepo) if (selectedRepoData && selectedAgent !== 'multi-agent') { try { @@ -457,8 +435,8 @@ export function TaskForm({ value={selectedAgent} onValueChange={(value) => { setSelectedAgent(value) - // Save to localStorage immediately - localStorage.setItem('last-selected-agent', value) + // Save to Jotai atom immediately + setSavedAgent(value) }} disabled={isSubmitting} > @@ -544,8 +522,8 @@ export function TaskForm({ value={selectedModel} onValueChange={(value) => { setSelectedModel(value) - // Save to localStorage immediately - localStorage.setItem(`last-selected-model-${selectedAgent}`, value) + // Save to Jotai atom immediately + setSavedModel(value) }} disabled={isSubmitting} > diff --git a/lib/atoms/agent-selection.ts b/lib/atoms/agent-selection.ts new file mode 100644 index 00000000..a0d2577e --- /dev/null +++ b/lib/atoms/agent-selection.ts @@ -0,0 +1,10 @@ +import { atomWithStorage } from 'jotai/utils' +import { atomFamily } from 'jotai/utils' + +// Last selected agent +export const lastSelectedAgentAtom = atomWithStorage('last-selected-agent', null) + +// Per-agent last selected model using atom family +export const lastSelectedModelAtomFamily = atomFamily((agent: string) => + atomWithStorage(`last-selected-model-${agent}`, null), +) diff --git a/lib/atoms/github-cache.ts b/lib/atoms/github-cache.ts new file mode 100644 index 00000000..1662cb46 --- /dev/null +++ b/lib/atoms/github-cache.ts @@ -0,0 +1,25 @@ +import { atomWithStorage } from 'jotai/utils' +import { atomFamily } from 'jotai/utils' + +interface GitHubOwner { + login: string + name: string + avatar_url: string +} + +interface GitHubRepo { + name: string + full_name: string + description: string + private: boolean + clone_url: string + language: string +} + +// GitHub owners cache +export const githubOwnersAtom = atomWithStorage('github-owners', null) + +// Per-owner repos cache using atom family +export const githubReposAtomFamily = atomFamily((owner: string) => + atomWithStorage(`github-repos-${owner}`, null), +) diff --git a/lib/atoms/newly-created-repo.ts b/lib/atoms/newly-created-repo.ts new file mode 100644 index 00000000..c14c3b09 --- /dev/null +++ b/lib/atoms/newly-created-repo.ts @@ -0,0 +1,9 @@ +import { atomWithStorage } from 'jotai/utils' + +interface NewlyCreatedRepo { + owner: string + repo: string +} + +// Newly created repo tracking +export const newlyCreatedRepoAtom = atomWithStorage('newly-created-repo', null)