-
Notifications
You must be signed in to change notification settings - Fork 598
Expand file tree
/
Copy pathApp.tsx
More file actions
220 lines (199 loc) · 7.63 KB
/
App.tsx
File metadata and controls
220 lines (199 loc) · 7.63 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
import { Provider as JotaiProvider, useAtomValue, useSetAtom } from "jotai"
import { ThemeProvider, useTheme } from "next-themes"
import { useEffect, useMemo } from "react"
import { Toaster } from "sonner"
import { TooltipProvider } from "./components/ui/tooltip"
import { TRPCProvider } from "./contexts/TRPCProvider"
import { WindowProvider, getInitialWindowParams } from "./contexts/WindowContext"
import { selectedProjectAtom, selectedAgentChatIdAtom } from "./features/agents/atoms"
import { getFreshSelectedProject } from "./features/agents/lib/selected-project"
import { useAgentSubChatStore } from "./features/agents/stores/sub-chat-store"
import { AgentsLayout } from "./features/layout/agents-layout"
import {
AnthropicOnboardingPage,
ApiKeyOnboardingPage,
BillingMethodPage,
CodexOnboardingPage,
SelectRepoPage,
} from "./features/onboarding"
import { identify, initAnalytics, shutdown } from "./lib/analytics"
import {
anthropicOnboardingCompletedAtom,
apiKeyOnboardingCompletedAtom,
billingMethodAtom,
codexOnboardingCompletedAtom,
} from "./lib/atoms"
import { appStore } from "./lib/jotai-store"
import { VSCodeThemeProvider } from "./lib/themes/theme-provider"
import { trpc } from "./lib/trpc"
/**
* Custom Toaster that adapts to theme
*/
function ThemedToaster() {
const { resolvedTheme } = useTheme()
return (
<Toaster
position="bottom-right"
theme={resolvedTheme as "light" | "dark" | "system"}
closeButton
/>
)
}
/**
* Main content router - decides which page to show based on onboarding state
*/
function AppContent() {
const billingMethod = useAtomValue(billingMethodAtom)
const setBillingMethod = useSetAtom(billingMethodAtom)
const anthropicOnboardingCompleted = useAtomValue(
anthropicOnboardingCompletedAtom
)
const setAnthropicOnboardingCompleted = useSetAtom(anthropicOnboardingCompletedAtom)
const apiKeyOnboardingCompleted = useAtomValue(apiKeyOnboardingCompletedAtom)
const setApiKeyOnboardingCompleted = useSetAtom(apiKeyOnboardingCompletedAtom)
const codexOnboardingCompleted = useAtomValue(codexOnboardingCompletedAtom)
const selectedProject = useAtomValue(selectedProjectAtom)
const setSelectedChatId = useSetAtom(selectedAgentChatIdAtom)
const { setActiveSubChat, addToOpenSubChats, setChatId } = useAgentSubChatStore()
// Apply initial window params (chatId/subChatId) when opening via "Open in new window"
useEffect(() => {
const params = getInitialWindowParams()
if (params.chatId) {
console.log("[App] Opening chat from window params:", params.chatId, params.subChatId)
setSelectedChatId(params.chatId)
setChatId(params.chatId)
if (params.subChatId) {
addToOpenSubChats(params.subChatId)
setActiveSubChat(params.subChatId)
}
}
}, [setSelectedChatId, setChatId, addToOpenSubChats, setActiveSubChat])
// Claim the initially selected chat to prevent duplicate windows.
// For new windows opened via "Open in new window", the chat is pre-claimed by main process.
// For restored windows (persisted localStorage), we need to claim here.
// Read atom directly from store to avoid stale closure with empty deps.
useEffect(() => {
if (!window.desktopApi?.claimChat) return
const currentChatId = appStore.get(selectedAgentChatIdAtom)
if (!currentChatId) return
window.desktopApi.claimChat(currentChatId).then((result) => {
if (!result.ok) {
// Another window already has this chat — clear our selection
setSelectedChatId(null)
}
})
// Only run on mount
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
// Check if user has existing CLI config (API key or proxy)
// Based on PR #29 by @sa4hnd
const { data: cliConfig, isLoading: isLoadingCliConfig } =
trpc.claudeCode.hasExistingCliConfig.useQuery()
// Migration: If user already completed Anthropic onboarding but has no billing method set,
// automatically set it to "claude-subscription" (legacy users before billing method was added)
useEffect(() => {
if (!billingMethod && anthropicOnboardingCompleted) {
setBillingMethod("claude-subscription")
}
}, [billingMethod, anthropicOnboardingCompleted, setBillingMethod])
// Auto-skip onboarding if user has existing CLI config (API key or proxy)
// This allows users with ANTHROPIC_API_KEY to use the app without OAuth
useEffect(() => {
if (cliConfig?.hasConfig && !billingMethod) {
console.log("[App] Detected existing CLI config, auto-completing onboarding")
setBillingMethod("api-key")
setApiKeyOnboardingCompleted(true)
}
}, [cliConfig?.hasConfig, billingMethod, setBillingMethod, setApiKeyOnboardingCompleted])
// Fetch projects to validate selectedProject exists
const { data: projects, isLoading: isLoadingProjects } =
trpc.projects.list.useQuery()
// Validated project - only valid if exists in DB
const validatedProject = useMemo(() => {
return getFreshSelectedProject(selectedProject, projects, isLoadingProjects)
}, [selectedProject, projects, isLoadingProjects])
// Determine which page to show:
// 1. No billing method selected -> BillingMethodPage
// 2. Claude subscription selected but not completed -> AnthropicOnboardingPage
// 3. Codex selected but not completed -> CodexOnboardingPage
// 4. API key or custom model selected but not completed -> ApiKeyOnboardingPage
// 5. No valid project selected -> SelectRepoPage
// 6. Otherwise -> AgentsLayout
if (!billingMethod) {
return <BillingMethodPage />
}
if (billingMethod === "claude-subscription" && !anthropicOnboardingCompleted) {
return <AnthropicOnboardingPage />
}
if (
(billingMethod === "codex-subscription" ||
billingMethod === "codex-api-key") &&
!codexOnboardingCompleted
) {
return <CodexOnboardingPage />
}
if (
(billingMethod === "api-key" || billingMethod === "custom-model") &&
!apiKeyOnboardingCompleted
) {
return <ApiKeyOnboardingPage />
}
if (!validatedProject && !isLoadingProjects) {
return <SelectRepoPage />
}
return <AgentsLayout />
}
export function App() {
// Initialize analytics on mount
useEffect(() => {
initAnalytics()
// Sync analytics opt-out status to main process
const syncOptOutStatus = async () => {
try {
const optOut =
localStorage.getItem("preferences:analytics-opt-out") === "true"
await window.desktopApi?.setAnalyticsOptOut(optOut)
} catch (error) {
console.warn("[Analytics] Failed to sync opt-out status:", error)
}
}
syncOptOutStatus()
// Identify user if already authenticated
const identifyUser = async () => {
try {
const user = await window.desktopApi?.getUser()
if (user?.id) {
identify(user.id, { email: user.email, name: user.name })
}
} catch (error) {
console.warn("[Analytics] Failed to identify user:", error)
}
}
identifyUser()
// Cleanup on unmount
return () => {
shutdown()
}
}, [])
return (
<WindowProvider>
<JotaiProvider store={appStore}>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<VSCodeThemeProvider>
<TooltipProvider delayDuration={100}>
<TRPCProvider>
<div
data-agents-page
className="h-screen w-screen bg-background text-foreground overflow-hidden"
>
<AppContent />
</div>
<ThemedToaster />
</TRPCProvider>
</TooltipProvider>
</VSCodeThemeProvider>
</ThemeProvider>
</JotaiProvider>
</WindowProvider>
)
}