1- import { useState } from 'react'
2- import { Plus , Copy , Check , Loader2 , Clock , Shield , Terminal , Zap } from 'lucide-react'
1+ import { useState , useCallback } from 'react'
2+ import { Plus , Copy , Check , Loader2 , Clock , Shield , Terminal , Zap , Wifi , CircleCheck , CircleX , ArrowRight } from 'lucide-react'
33import { useQuery , useQueryClient } from '@tanstack/react-query'
44import { useAuth } from '@/contexts/AuthContext'
55import { Button } from '@/components/ui/button'
@@ -166,36 +166,165 @@ function KeyCard({
166166 )
167167}
168168
169- function ConnectGuide ( ) {
169+ type TestStep = { label : string ; status : 'idle' | 'running' | 'pass' | 'fail' }
170+
171+ function ConnectionTest ( { apiKey } : { apiKey : string | null } ) {
172+ const [ steps , setSteps ] = useState < TestStep [ ] > ( [
173+ { label : 'MCP server reachable' , status : 'idle' } ,
174+ { label : 'API key authenticated' , status : 'idle' } ,
175+ { label : 'Repositories accessible' , status : 'idle' } ,
176+ ] )
177+ const [ running , setRunning ] = useState ( false )
178+ const [ tested , setTested ] = useState ( false )
179+
180+ const runTest = useCallback ( async ( ) => {
181+ if ( ! apiKey || running ) return
182+ setRunning ( true )
183+ setTested ( true )
184+ const update = ( idx : number , status : TestStep [ 'status' ] ) =>
185+ setSteps ( ( prev ) => prev . map ( ( s , i ) => ( i === idx ? { ...s , status } : s ) ) )
186+
187+ // Reset
188+ setSteps ( ( prev ) => prev . map ( ( s ) => ( { ...s , status : 'idle' } ) ) )
189+
190+ // Step 1: MCP health
191+ update ( 0 , 'running' )
192+ try {
193+ const res = await fetch ( 'https://mcp.opencodeintel.com/health' )
194+ update ( 0 , res . ok ? 'pass' : 'fail' )
195+ if ( ! res . ok ) { setRunning ( false ) ; return }
196+ } catch {
197+ update ( 0 , 'fail' ) ; setRunning ( false ) ; return
198+ }
199+
200+ // Step 2: Auth check
201+ update ( 1 , 'running' )
202+ try {
203+ const res = await fetch ( `${ API_URL } /keys` , {
204+ headers : { Authorization : `Bearer ${ apiKey } ` } ,
205+ } )
206+ update ( 1 , res . ok ? 'pass' : 'fail' )
207+ if ( ! res . ok ) { setRunning ( false ) ; return }
208+ } catch {
209+ update ( 1 , 'fail' ) ; setRunning ( false ) ; return
210+ }
211+
212+ // Step 3: Repos accessible
213+ update ( 2 , 'running' )
214+ try {
215+ const res = await fetch ( `${ API_URL } /repos` , {
216+ headers : { Authorization : `Bearer ${ apiKey } ` } ,
217+ } )
218+ update ( 2 , res . ok ? 'pass' : 'fail' )
219+ } catch {
220+ update ( 2 , 'fail' )
221+ }
222+
223+ setRunning ( false )
224+ } , [ apiKey , running ] )
225+
226+ const allPassed = steps . every ( ( s ) => s . status === 'pass' )
227+ const anyFailed = steps . some ( ( s ) => s . status === 'fail' )
228+
229+ return (
230+ < div className = "px-5 py-4 border-t border-border/40" >
231+ < div className = "flex items-center justify-between mb-3" >
232+ < span className = "text-xs font-medium text-muted-foreground uppercase tracking-wider" >
233+ Connection test
234+ </ span >
235+ < button
236+ onClick = { runTest }
237+ disabled = { ! apiKey || running }
238+ className = { cn (
239+ 'text-xs px-3 py-1.5 rounded-md transition-all flex items-center gap-1.5' ,
240+ running
241+ ? 'text-muted-foreground bg-muted/30'
242+ : allPassed && tested
243+ ? 'text-emerald-400 bg-emerald-500/10 hover:bg-emerald-500/15'
244+ : 'text-primary bg-primary/10 hover:bg-primary/15' ,
245+ ) }
246+ >
247+ { running ? (
248+ < > < Loader2 className = "w-3 h-3 animate-spin" /> Testing...</ >
249+ ) : allPassed && tested ? (
250+ < > < CircleCheck className = "w-3 h-3" /> Connected</ >
251+ ) : (
252+ < > < Wifi className = "w-3 h-3" /> Test connection</ >
253+ ) }
254+ </ button >
255+ </ div >
256+ < div className = "flex items-center gap-3" >
257+ { steps . map ( ( step , i ) => (
258+ < div key = { step . label } className = "flex items-center gap-1.5" >
259+ { i > 0 && (
260+ < ArrowRight className = { cn (
261+ 'w-3 h-3' ,
262+ step . status === 'pass' ? 'text-emerald-400/50' : 'text-border/60' ,
263+ ) } />
264+ ) }
265+ < div className = { cn (
266+ 'flex items-center gap-1.5 text-xs px-2 py-1 rounded-md' ,
267+ step . status === 'pass' && 'text-emerald-400 bg-emerald-500/8' ,
268+ step . status === 'fail' && 'text-destructive bg-destructive/8' ,
269+ step . status === 'running' && 'text-primary bg-primary/8' ,
270+ step . status === 'idle' && 'text-muted-foreground/60' ,
271+ ) } >
272+ { step . status === 'running' && < Loader2 className = "w-3 h-3 animate-spin" /> }
273+ { step . status === 'pass' && < CircleCheck className = "w-3 h-3" /> }
274+ { step . status === 'fail' && < CircleX className = "w-3 h-3" /> }
275+ { step . status === 'idle' && < div className = "w-3 h-3 rounded-full border border-current opacity-40" /> }
276+ { step . label }
277+ </ div >
278+ </ div >
279+ ) ) }
280+ </ div >
281+ { anyFailed && tested && ! running && (
282+ < p className = "text-[11px] text-destructive/70 mt-2" >
283+ Connection failed. Check that your API key is active and the MCP server is running.
284+ </ p >
285+ ) }
286+ </ div >
287+ )
288+ }
289+
290+ function ConnectGuide ( { activeKeyPreview } : { activeKeyPreview : string | null } ) {
170291 const [ tab , setTab ] = useState < 'desktop' | 'code' | 'cursor' > ( 'desktop' )
171292
172- const snippets : Record < string , { label : string ; config : string } > = {
293+ const keyDisplay = activeKeyPreview || 'ci_your-key-here'
294+
295+ const snippets : Record < string , { label : string ; config : string ; hint : string } > = {
173296 desktop : {
174297 label : 'Claude Desktop' ,
298+ hint : 'Settings > Developer > Edit Config' ,
175299 config : `{
176300 "mcpServers": {
177301 "codeintel": {
178302 "command": "npx",
179303 "args": ["-y", "mcp-remote", "https://mcp.opencodeintel.com/mcp"],
180304 "env": {
181- "API_KEY": "ci_your-key-here "
305+ "API_KEY": "${ keyDisplay } "
182306 }
183307 }
184308 }
185309}` ,
186310 } ,
187311 code : {
188312 label : 'Claude Code' ,
313+ hint : 'Run in terminal' ,
189314 config : `claude mcp add codeintel \\
190315 --transport http \\
191316 https://mcp.opencodeintel.com/mcp` ,
192317 } ,
193318 cursor : {
194319 label : 'Cursor' ,
320+ hint : '.cursor/mcp.json' ,
195321 config : `{
196322 "mcpServers": {
197323 "codeintel": {
198- "url": "https://mcp.opencodeintel.com/mcp"
324+ "url": "https://mcp.opencodeintel.com/mcp",
325+ "headers": {
326+ "Authorization": "Bearer ${ keyDisplay } "
327+ }
199328 }
200329 }
201330}` ,
@@ -207,7 +336,10 @@ function ConnectGuide() {
207336 return (
208337 < div className = "rounded-lg border border-border/60 bg-card/40 overflow-hidden" >
209338 < div className = "px-5 py-3 border-b border-border/40 flex items-center justify-between" >
210- < span className = "text-sm font-medium text-foreground" > Connect to your tools</ span >
339+ < div className = "flex items-center gap-2" >
340+ < Wifi className = "w-3.5 h-3.5 text-primary/70" />
341+ < span className = "text-sm font-medium text-foreground" > Connect to your tools</ span >
342+ </ div >
211343 < div className = "flex gap-1" >
212344 { Object . entries ( snippets ) . map ( ( [ key , { label } ] ) => (
213345 < button
@@ -225,14 +357,25 @@ function ConnectGuide() {
225357 ) ) }
226358 </ div >
227359 </ div >
360+
361+ { /* Hint line */ }
362+ < div className = "px-5 pt-3 pb-0" >
363+ < p className = "text-[11px] text-muted-foreground/60" >
364+ { current . hint }
365+ </ p >
366+ </ div >
367+
228368 < div className = "relative" >
229- < pre className = "px-5 py-4 text-[12px] font-mono text-muted-foreground leading-relaxed overflow-x-auto" >
369+ < pre className = "px-5 py-3 text-[12px] font-mono text-muted-foreground leading-relaxed overflow-x-auto" >
230370 { current . config }
231371 </ pre >
232- < div className = "absolute top-3 right-3" >
372+ < div className = "absolute top-2 right-3" >
233373 < CopyInline text = { current . config } label = "Config" />
234374 </ div >
235375 </ div >
376+
377+ { /* Connection test */ }
378+ < ConnectionTest apiKey = { activeKeyPreview } />
236379 </ div >
237380 )
238381}
@@ -435,7 +578,9 @@ export function APIKeysPage() {
435578 ) }
436579
437580 { /* Connect guide */ }
438- { activeKeys . length > 0 && < ConnectGuide /> }
581+ { activeKeys . length > 0 && (
582+ < ConnectGuide activeKeyPreview = { activeKeys [ 0 ] ?. key_preview || null } />
583+ ) }
439584
440585 { /* Generate dialog */ }
441586 < Dialog open = { generateOpen } onOpenChange = { ( open ) => { if ( ! open && ! generatedKey ) closeGenerateDialog ( ) } } >
0 commit comments