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'
@@ -14,7 +14,7 @@ import {
1414 DialogTitle ,
1515} from '@/components/ui/dialog'
1616import { toast } from 'sonner'
17- import { API_URL } from '@/config/api'
17+ import { API_URL , MCP_URL } from '@/config/api'
1818import { cn } from '@/lib/utils'
1919
2020interface APIKey {
@@ -166,36 +166,166 @@ function KeyCard({
166166 )
167167}
168168
169- function ConnectGuide ( ) {
169+ type TestStep = { label : string ; status : 'idle' | 'running' | 'pass' | 'fail' }
170+
171+ function ConnectionTest ( { token } : { token : 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 ( ! token || 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 ( `${ MCP_URL } /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 (uses session JWT, not API key preview)
201+ update ( 1 , 'running' )
202+ try {
203+ const res = await fetch ( `${ API_URL } /keys` , {
204+ headers : { Authorization : `Bearer ${ token } ` } ,
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 ${ token } ` } ,
217+ } )
218+ update ( 2 , res . ok ? 'pass' : 'fail' )
219+ } catch {
220+ update ( 2 , 'fail' )
221+ }
222+
223+ setRunning ( false )
224+ } , [ token , 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 = { ! token || 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, sessionToken } : { activeKeyPreview : string | null ; sessionToken : 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 \\
316+ --header "Authorization: Bearer ${ keyDisplay } " \\
191317 https://mcp.opencodeintel.com/mcp` ,
192318 } ,
193319 cursor : {
194320 label : 'Cursor' ,
321+ hint : '.cursor/mcp.json' ,
195322 config : `{
196323 "mcpServers": {
197324 "codeintel": {
198- "url": "https://mcp.opencodeintel.com/mcp"
325+ "url": "https://mcp.opencodeintel.com/mcp",
326+ "headers": {
327+ "Authorization": "Bearer ${ keyDisplay } "
328+ }
199329 }
200330 }
201331}` ,
@@ -207,7 +337,10 @@ function ConnectGuide() {
207337 return (
208338 < div className = "rounded-lg border border-border/60 bg-card/40 overflow-hidden" >
209339 < 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 >
340+ < div className = "flex items-center gap-2" >
341+ < Wifi className = "w-3.5 h-3.5 text-primary/70" />
342+ < span className = "text-sm font-medium text-foreground" > Connect to your tools</ span >
343+ </ div >
211344 < div className = "flex gap-1" >
212345 { Object . entries ( snippets ) . map ( ( [ key , { label } ] ) => (
213346 < button
@@ -225,14 +358,25 @@ function ConnectGuide() {
225358 ) ) }
226359 </ div >
227360 </ div >
361+
362+ { /* Hint line */ }
363+ < div className = "px-5 pt-3 pb-0" >
364+ < p className = "text-[11px] text-muted-foreground/60" >
365+ { current . hint }
366+ </ p >
367+ </ div >
368+
228369 < div className = "relative" >
229- < pre className = "px-5 py-4 text-[12px] font-mono text-muted-foreground leading-relaxed overflow-x-auto" >
370+ < pre className = "px-5 py-3 text-[12px] font-mono text-muted-foreground leading-relaxed overflow-x-auto" >
230371 { current . config }
231372 </ pre >
232- < div className = "absolute top-3 right-3" >
373+ < div className = "absolute top-2 right-3" >
233374 < CopyInline text = { current . config } label = "Config" />
234375 </ div >
235376 </ div >
377+
378+ { /* Connection test */ }
379+ < ConnectionTest token = { sessionToken } />
236380 </ div >
237381 )
238382}
@@ -435,7 +579,12 @@ export function APIKeysPage() {
435579 ) }
436580
437581 { /* Connect guide */ }
438- { activeKeys . length > 0 && < ConnectGuide /> }
582+ { activeKeys . length > 0 && (
583+ < ConnectGuide
584+ activeKeyPreview = { activeKeys [ 0 ] ?. key_preview || null }
585+ sessionToken = { token }
586+ />
587+ ) }
439588
440589 { /* Generate dialog */ }
441590 < Dialog open = { generateOpen } onOpenChange = { ( open ) => { if ( ! open && ! generatedKey ) closeGenerateDialog ( ) } } >
0 commit comments