@@ -7,6 +7,8 @@ import { MobileConnect } from './mobile-connect'
77import { SessionPresence } from './session-presence'
88import { CaffeinateToggle } from './caffeinate-toggle'
99import { useGateway } from '@/context/gateway-context'
10+ import { useGitHubAuth } from '@/context/github-auth-context'
11+ import { fetchAuthenticatedUser , type GitHubUser } from '@/lib/github-api'
1012import { THEME_PRESETS , useTheme , type ThemeMode , type ThemePreset } from '@/context/theme-context'
1113
1214type SettingsTab = 'connect' | 'general'
@@ -46,6 +48,18 @@ export function SettingsPanel({
4648 const [ tab , setTab ] = useState < SettingsTab > ( ( initialTab as SettingsTab ) || 'connect' )
4749 const { status, gatewayUrl } = useGateway ( )
4850 const { themeId, setThemeId, mode, setMode } = useTheme ( )
51+ const { token : ghToken , authenticated : ghAuth , setManualToken, clearToken } = useGitHubAuth ( )
52+ const [ ghUser , setGhUser ] = useState < GitHubUser | null > ( null )
53+ const [ patInput , setPatInput ] = useState ( '' )
54+ const [ showPatField , setShowPatField ] = useState ( false )
55+
56+ // Fetch GitHub user on token change
57+ const ghTokenRef = useRef ( ghToken )
58+ if ( ghTokenRef . current !== ghToken ) {
59+ ghTokenRef . current = ghToken
60+ if ( ghToken ) fetchAuthenticatedUser ( ) . then ( u => setGhUser ( u ) )
61+ else setGhUser ( null )
62+ }
4963
5064 const themeGroups = useMemo ( ( ) => groupThemes ( ) , [ ] )
5165 const isMobile = typeof window !== 'undefined' && window . innerWidth <= 768
@@ -327,6 +341,110 @@ export function SettingsPanel({
327341 < CaffeinateToggle />
328342 </ section >
329343
344+ { /* GitHub Account */ }
345+ < section className = "rounded-[24px] border border-[var(--glass-border)] bg-[color-mix(in_srgb,var(--bg-elevated)_88%,transparent)] p-4 shadow-[var(--shadow-sm)]" >
346+ < div className = "mb-3 flex items-center gap-2" >
347+ < span className = "flex h-8 w-8 items-center justify-center rounded-2xl bg-[color-mix(in_srgb,var(--brand)_12%,transparent)] text-[var(--brand)]" >
348+ < Icon icon = "lucide:github" width = { 14 } />
349+ </ span >
350+ < div >
351+ < h3 className = "text-sm font-medium text-[var(--text-primary)]" > GitHub</ h3 >
352+ < p className = "text-[11px] text-[var(--text-secondary)]" >
353+ Connect your account to browse repos.
354+ </ p >
355+ </ div >
356+ </ div >
357+
358+ { ghAuth && ghUser ? (
359+ < div className = "space-y-3" >
360+ < div className = "flex items-center gap-2.5" >
361+ { ghUser . avatar_url && (
362+ < img src = { ghUser . avatar_url } alt = "" className = "w-8 h-8 rounded-full border border-[var(--border)]" />
363+ ) }
364+ < div >
365+ < p className = "text-[13px] font-medium text-[var(--text-primary)]" > { ghUser . name ?? ghUser . login } </ p >
366+ < p className = "text-[11px] text-[var(--text-secondary)]" > @{ ghUser . login } </ p >
367+ </ div >
368+ </ div >
369+ < button
370+ onClick = { ( ) => { clearToken ( ) ; setGhUser ( null ) } }
371+ className = "text-[12px] text-[var(--color-deletions)] hover:underline cursor-pointer"
372+ >
373+ Sign out
374+ </ button >
375+ </ div >
376+ ) : ghAuth ? (
377+ < div className = "flex items-center gap-2" >
378+ < span className = "w-2 h-2 rounded-full bg-green-500" />
379+ < span className = "text-[12px] text-[var(--text-secondary)]" > Connected</ span >
380+ < button
381+ onClick = { ( ) => { clearToken ( ) ; setGhUser ( null ) } }
382+ className = "ml-auto text-[12px] text-[var(--color-deletions)] hover:underline cursor-pointer"
383+ >
384+ Sign out
385+ </ button >
386+ </ div >
387+ ) : (
388+ < div className = "space-y-3" >
389+ { ! showPatField ? (
390+ < button
391+ onClick = { ( ) => setShowPatField ( true ) }
392+ className = "inline-flex items-center gap-2 rounded-full border border-[var(--border)] bg-[color-mix(in_srgb,var(--bg)_92%,transparent)] px-3 py-2 text-xs font-medium text-[var(--text-primary)] transition hover:border-[var(--border-hover)] hover:bg-[color-mix(in_srgb,var(--text-primary)_5%,transparent)] cursor-pointer"
393+ >
394+ < Icon icon = "lucide:key-round" width = { 14 } />
395+ Add Personal Access Token
396+ </ button >
397+ ) : (
398+ < div className = "space-y-2" >
399+ < div className = "flex items-center gap-1.5" >
400+ < div className = "flex-1 relative" >
401+ < Icon icon = "lucide:key-round" width = { 13 } className = "absolute left-2.5 top-1/2 -translate-y-1/2 text-[var(--text-disabled)]" />
402+ < input
403+ type = "password"
404+ value = { patInput }
405+ onChange = { ( e ) => setPatInput ( e . target . value ) }
406+ onKeyDown = { ( e ) => {
407+ if ( e . key === 'Enter' && patInput . trim ( ) ) {
408+ setManualToken ( patInput . trim ( ) )
409+ setPatInput ( '' )
410+ setShowPatField ( false )
411+ }
412+ if ( e . key === 'Escape' ) { setShowPatField ( false ) ; setPatInput ( '' ) }
413+ } }
414+ placeholder = "ghp_xxxx..."
415+ autoCapitalize = "off"
416+ autoCorrect = "off"
417+ spellCheck = { false }
418+ className = "w-full pl-8 pr-3 py-2 rounded-lg border border-[var(--border)] bg-[color-mix(in_srgb,var(--bg)_80%,transparent)] text-[13px] text-[var(--text-primary)] placeholder:text-[var(--text-disabled)] outline-none focus:border-[var(--brand)] transition-colors"
419+ />
420+ </ div >
421+ < button
422+ onClick = { ( ) => {
423+ if ( patInput . trim ( ) ) {
424+ setManualToken ( patInput . trim ( ) )
425+ setPatInput ( '' )
426+ setShowPatField ( false )
427+ }
428+ } }
429+ disabled = { ! patInput . trim ( ) }
430+ className = "shrink-0 px-3 py-2 rounded-lg text-[12px] font-medium transition-all cursor-pointer disabled:opacity-40 disabled:cursor-default bg-[var(--brand)] text-[var(--brand-contrast,#fff)]"
431+ >
432+ Save
433+ </ button >
434+ </ div >
435+ < p className = "text-[10px] text-[var(--text-disabled)] leading-relaxed" >
436+ Create at{ ' ' }
437+ < a href = "https://github.com/settings/tokens" target = "_blank" rel = "noopener noreferrer" className = "text-[var(--brand)] underline" >
438+ github.com/settings/tokens
439+ </ a >
440+ { ' ' } with < span className = "font-mono" > repo</ span > scope.
441+ </ p >
442+ </ div >
443+ ) }
444+ </ div >
445+ ) }
446+ </ section >
447+
330448 < section className = "rounded-[24px] border border-[var(--glass-border)] bg-[color-mix(in_srgb,var(--bg-elevated)_88%,transparent)] p-4 shadow-[var(--shadow-sm)]" >
331449 < div className = "flex items-center gap-3" >
332450 < span className = "flex h-10 w-10 items-center justify-center rounded-[18px] bg-[color-mix(in_srgb,var(--brand)_12%,transparent)] text-[var(--brand)]" >
0 commit comments