@@ -15,13 +15,8 @@ import { useEditor } from '@/context/editor-context'
1515import { emit } from '@/lib/events'
1616import { getRecentFolders } from '@/context/local-context'
1717import { getAgentConfig } from '@/lib/agent-session'
18- import {
19- fetchRepoByName ,
20- fetchAuthenticatedUser ,
21- startDeviceFlow ,
22- pollDeviceFlow ,
23- type GitHubUser ,
24- } from '@/lib/github-api'
18+ import { fetchRepoByName , fetchAuthenticatedUser , type GitHubUser } from '@/lib/github-api'
19+ import { getFavorites , getRecents , addRecent , type SavedRepo } from '@/lib/github-repos-store'
2520
2621const STATIC_SUGGESTIONS = [
2722 {
@@ -99,7 +94,7 @@ export const ChatHome = memo(function ChatHome({
9994 const local = useLocal ( )
10095 const { status } = useGateway ( )
10196 const { files : openFiles } = useEditor ( )
102- const { token : ghToken , setManualToken , authenticated : ghAuthenticated } = useGitHubAuth ( )
97+ const { token : ghToken , authenticated : ghAuthenticated } = useGitHubAuth ( )
10398
10499 const repoShort = useMemo (
105100 ( ) => repo ?. fullName ?. split ( '/' ) . pop ( ) ?? local . rootPath ?. split ( '/' ) . pop ( ) ?? null ,
@@ -115,69 +110,22 @@ export const ChatHome = memo(function ChatHome({
115110 const [ repoError , setRepoError ] = useState < string | null > ( null )
116111 const repoInputRef = useRef < HTMLInputElement > ( null )
117112 const [ ghUser , setGhUser ] = useState < GitHubUser | null > ( null )
118- const [ deviceFlow , setDeviceFlow ] = useState < {
119- userCode : string
120- verificationUri : string
121- } | null > ( null )
122- const [ authLoading , setAuthLoading ] = useState ( false )
123- const [ showPatInput , setShowPatInput ] = useState ( false )
124- const [ patInput , setPatInput ] = useState ( '' )
125- const patInputRef = useRef < HTMLInputElement > ( null )
126- const deviceFlowAbort = useRef < AbortController | null > ( null )
113+ const [ savedFavorites , setSavedFavorites ] = useState < SavedRepo [ ] > ( [ ] )
114+ const [ savedRecents , setSavedRecents ] = useState < SavedRepo [ ] > ( [ ] )
127115
128116 const isMobile = typeof window !== 'undefined' && window . innerWidth <= 768
129117
130118 // Fetch GitHub user when token is available
131119 useEffect ( ( ) => {
132- if ( ! ghToken ) {
133- setGhUser ( null )
134- return
135- }
120+ if ( ! ghToken ) { setGhUser ( null ) ; return }
136121 fetchAuthenticatedUser ( ) . then ( ( u ) => setGhUser ( u ) )
137122 } , [ ghToken ] )
138123
139- // GitHub Device Flow sign-in
140- const startGitHubSignIn = useCallback ( async ( ) => {
141- setAuthLoading ( true )
142- setRepoError ( null )
143- try {
144- const flow = await startDeviceFlow ( )
145- setDeviceFlow ( { userCode : flow . user_code , verificationUri : flow . verification_uri } )
146- deviceFlowAbort . current = new AbortController ( )
147- const token = await pollDeviceFlow (
148- flow . device_code ,
149- flow . interval ,
150- deviceFlowAbort . current . signal ,
151- )
152- setManualToken ( token )
153- setDeviceFlow ( null )
154- } catch ( err ) {
155- if ( ( err as Error ) . message !== 'Cancelled' ) {
156- setRepoError ( err instanceof Error ? err . message : 'Sign-in failed' )
157- }
158- setDeviceFlow ( null )
159- } finally {
160- setAuthLoading ( false )
161- }
162- } , [ setManualToken ] )
163-
164- const cancelGitHubSignIn = useCallback ( ( ) => {
165- deviceFlowAbort . current ?. abort ( )
166- setDeviceFlow ( null )
167- setAuthLoading ( false )
168- } , [ ] )
169-
170- const handlePatSubmit = useCallback ( ( ) => {
171- const t = patInput . trim ( )
172- if ( ! t ) return
173- setManualToken ( t )
174- setPatInput ( '' )
175- setShowPatInput ( false )
176- } , [ patInput , setManualToken ] )
177-
124+ // Load favorites + recents
178125 useEffect ( ( ) => {
179- if ( showPatInput ) setTimeout ( ( ) => patInputRef . current ?. focus ( ) , 100 )
180- } , [ showPatInput ] )
126+ setSavedFavorites ( getFavorites ( ) )
127+ setSavedRecents ( getRecents ( ) )
128+ } , [ ghToken ] )
181129
182130 const handleRepoConnect = useCallback ( async ( ) => {
183131 const val = repoInput
@@ -200,6 +148,13 @@ export const ChatHome = memo(function ChatHome({
200148 fullName : ghRepo . full_name ,
201149 }
202150 setRepo ( info )
151+ setSavedRecents ( addRecent ( {
152+ fullName : ghRepo . full_name ,
153+ name : ghRepo . name ,
154+ owner : ghRepo . owner . login ,
155+ defaultBranch : ghRepo . default_branch ,
156+ addedAt : Date . now ( ) ,
157+ } ) )
203158 setShowRepoInput ( false )
204159 setRepoInput ( '' )
205160 } catch ( err ) {
@@ -209,6 +164,17 @@ export const ChatHome = memo(function ChatHome({
209164 }
210165 } , [ repoInput , setRepo ] )
211166
167+ const selectSavedRepo = useCallback ( ( saved : SavedRepo ) => {
168+ const info : RepoInfo = {
169+ owner : saved . owner ,
170+ repo : saved . name ,
171+ branch : saved . defaultBranch ,
172+ fullName : saved . fullName ,
173+ }
174+ setRepo ( info )
175+ setSavedRecents ( addRecent ( saved ) )
176+ } , [ setRepo ] )
177+
212178 useEffect ( ( ) => {
213179 if ( showRepoInput ) {
214180 setTimeout ( ( ) => repoInputRef . current ?. focus ( ) , 100 )
@@ -334,181 +300,111 @@ export const ChatHome = memo(function ChatHome({
334300 { /* Mobile project selector */ }
335301 { isMobile && (
336302 < div className = "mt-3 flex flex-col items-center gap-2 w-full max-w-[320px]" >
337- { /* GitHub user badge */ }
338- { ghAuthenticated && ghUser && (
339- < span className = "inline-flex items-center gap-1.5 text-[11px] text-[var(--text-secondary)]" >
340- { ghUser . avatar_url && (
341- < img src = { ghUser . avatar_url } alt = "" className = "w-4 h-4 rounded-full" />
342- ) }
343- { ghUser . login }
344- </ span >
345- ) }
346-
347- { /* Sign in with GitHub — when no token */ }
348- { ! ghAuthenticated && ! deviceFlow && ! showPatInput && (
349- < div className = "flex flex-col items-center gap-2" >
350- < button
351- onClick = { startGitHubSignIn }
352- disabled = { authLoading }
353- className = "inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg border border-[var(--border)] bg-[color-mix(in_srgb,var(--bg-elevated)_80%,transparent)] text-[12px] text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:border-[var(--text-disabled)] transition-all cursor-pointer disabled:opacity-50"
354- >
355- < Icon icon = "lucide:github" width = { 14 } height = { 14 } />
356- { authLoading ? 'Signing in…' : 'Sign in with GitHub' }
357- </ button >
358- < button
359- onClick = { ( ) => setShowPatInput ( true ) }
360- className = "text-[11px] text-[var(--text-disabled)] hover:text-[var(--text-secondary)] transition-colors cursor-pointer"
361- >
362- or use a personal access token
363- </ button >
364- </ div >
303+ { /* Not signed in — link to Settings */ }
304+ { ! ghAuthenticated && (
305+ < button
306+ onClick = { ( ) => emit ( 'open-settings' ) }
307+ className = "inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg border border-dashed border-[var(--border)] text-[12px] text-[var(--text-disabled)] hover:text-[var(--text-secondary)] hover:border-[var(--text-disabled)] transition-all cursor-pointer"
308+ >
309+ < Icon icon = "lucide:github" width = { 14 } height = { 14 } />
310+ Sign in to GitHub via Settings
311+ </ button >
365312 ) }
366313
367- { /* PAT input */ }
368- { ! ghAuthenticated && showPatInput && ! deviceFlow && (
314+ { /* Signed in — favorites + recents picker */ }
315+ { ghAuthenticated && ! hasWorkspace && ! showRepoInput && (
369316 < div className = "w-full space-y-2" >
370- < div className = "flex items-center gap-1.5" >
371- < div className = "flex-1 relative" >
372- < Icon icon = "lucide:key-round" width = { 14 } height = { 14 } className = "absolute left-2.5 top-1/2 -translate-y-1/2 text-[var(--text-disabled)]" />
373- < input
374- ref = { patInputRef }
375- type = "password"
376- value = { patInput }
377- onChange = { ( e ) => setPatInput ( e . target . value ) }
378- onKeyDown = { ( e ) => { if ( e . key === 'Enter' ) handlePatSubmit ( ) ; if ( e . key === 'Escape' ) setShowPatInput ( false ) } }
379- placeholder = "ghp_xxxx..."
380- autoCapitalize = "off"
381- autoCorrect = "off"
382- spellCheck = { false }
383- className = "w-full pl-8 pr-3 py-2 rounded-lg border border-[var(--border)] bg-[color-mix(in_srgb,var(--bg-elevated)_80%,transparent)] text-[13px] text-[var(--text-primary)] placeholder:text-[var(--text-disabled)] outline-none focus:border-[var(--brand)] transition-colors"
384- />
317+ { /* Favorites */ }
318+ { savedFavorites . length > 0 && (
319+ < div >
320+ < p className = "text-[10px] uppercase tracking-wider text-[var(--text-disabled)] font-medium mb-1.5 px-1" > Favorites</ p >
321+ < div className = "space-y-0.5" >
322+ { savedFavorites . map ( r => (
323+ < button
324+ key = { r . fullName }
325+ onClick = { ( ) => selectSavedRepo ( r ) }
326+ className = "w-full flex items-center gap-2 px-2.5 py-2 rounded-lg hover:bg-[color-mix(in_srgb,var(--text-primary)_6%,transparent)] transition-colors cursor-pointer text-left"
327+ >
328+ < Icon icon = "lucide:star" width = { 12 } className = "text-amber-400 shrink-0" />
329+ < span className = "text-[12px] text-[var(--text-primary)] truncate flex-1" > { r . fullName } </ span >
330+ < Icon icon = "lucide:chevron-right" width = { 12 } className = "text-[var(--text-disabled)] shrink-0" />
331+ </ button >
332+ ) ) }
333+ </ div >
385334 </ div >
386- < button
387- onClick = { handlePatSubmit }
388- disabled = { ! patInput . trim ( ) }
389- 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)]"
390- >
391- Save
392- </ button >
393- </ div >
394- < p className = "text-[10px] text-[var(--text-disabled)] leading-relaxed" >
395- Create a token at{ ' ' }
396- < a href = "https://github.com/settings/tokens" target = "_blank" rel = "noopener noreferrer" className = "text-[var(--brand)] underline" >
397- github.com/settings/tokens
398- </ a >
399- { ' ' } with < span className = "font-mono" > repo</ span > scope. Stored securely on device.
400- </ p >
401- < button
402- onClick = { ( ) => { setShowPatInput ( false ) ; setPatInput ( '' ) } }
403- className = "text-[11px] text-[var(--text-disabled)] hover:text-[var(--text-secondary)] cursor-pointer"
404- >
405- ← back to sign in
406- </ button >
407- </ div >
408- ) }
335+ ) }
409336
410- { /* Device flow — show code */ }
411- { deviceFlow && (
412- < div className = "text-center space-y-2" >
413- < p className = "text-[11px] text-[var(--text-disabled)]" >
414- Enter this code at{ ' ' }
415- < a
416- href = { deviceFlow . verificationUri }
417- target = "_blank"
418- rel = "noopener noreferrer"
419- className = "text-[var(--brand)] underline"
420- >
421- github.com/login/device
422- </ a >
423- </ p >
424- < p className = "text-[20px] font-mono font-bold tracking-[0.15em] text-[var(--text-primary)]" >
425- { deviceFlow . userCode }
426- </ p >
337+ { /* Recents */ }
338+ { savedRecents . length > 0 && (
339+ < div >
340+ < p className = "text-[10px] uppercase tracking-wider text-[var(--text-disabled)] font-medium mb-1.5 px-1" > Recent</ p >
341+ < div className = "space-y-0.5" >
342+ { savedRecents . filter ( r => ! savedFavorites . some ( f => f . fullName === r . fullName ) ) . slice ( 0 , 5 ) . map ( r => (
343+ < button
344+ key = { r . fullName }
345+ onClick = { ( ) => selectSavedRepo ( r ) }
346+ className = "w-full flex items-center gap-2 px-2.5 py-2 rounded-lg hover:bg-[color-mix(in_srgb,var(--text-primary)_6%,transparent)] transition-colors cursor-pointer text-left"
347+ >
348+ < Icon icon = "lucide:clock" width = { 12 } className = "text-[var(--text-disabled)] shrink-0" />
349+ < span className = "text-[12px] text-[var(--text-primary)] truncate flex-1" > { r . fullName } </ span >
350+ < Icon icon = "lucide:chevron-right" width = { 12 } className = "text-[var(--text-disabled)] shrink-0" />
351+ </ button >
352+ ) ) }
353+ </ div >
354+ </ div >
355+ ) }
356+
357+ { /* Manual input fallback */ }
427358 < button
428- onClick = { cancelGitHubSignIn }
429- className = "text-[11px ] text-[var(--text-disabled)] hover:text-[var(--text-secondary)] cursor-pointer"
359+ onClick = { ( ) => setShowRepoInput ( true ) }
360+ className = "w-full flex items-center justify-center gap-1.5 px-3 py-2 rounded-lg border border-dashed border-[var(--border)] text-[12px ] text-[var(--text-disabled)] hover:text-[var(--text-secondary)] hover:border-[var(--text-disabled)] transition-all cursor-pointer"
430361 >
431- Cancel
362+ < Icon icon = "lucide:plus" width = { 13 } />
363+ Open a repository
432364 </ button >
433365 </ div >
434366 ) }
435367
436- { /* Connect repo — when authenticated */ }
437- { ghAuthenticated && ! hasWorkspace && ! showRepoInput && (
438- < button
439- onClick = { ( ) => setShowRepoInput ( true ) }
440- className = "inline-flex items-center gap-2 px-4 py-2 rounded-xl border border-[color-mix(in_srgb,var(--brand)_28%,var(--border))] bg-[color-mix(in_srgb,var(--brand)_12%,transparent)] text-[12px] font-medium text-[var(--text-primary)] shadow-[0_8px_24px_color-mix(in_srgb,var(--brand)_10%,transparent)] hover:border-[color-mix(in_srgb,var(--brand)_40%,var(--border))] hover:bg-[color-mix(in_srgb,var(--brand)_16%,transparent)] transition-all cursor-pointer"
441- >
442- < Icon
443- icon = "lucide:folder-git-2"
444- width = { 15 }
445- height = { 15 }
446- className = "text-[var(--brand)]"
447- />
448- Connect a repository
449- </ button >
450- ) }
451-
452368 { /* Repo input */ }
453369 { ghAuthenticated && ! hasWorkspace && showRepoInput && (
454370 < div className = "w-full" >
455371 < div className = "flex items-center gap-1.5" >
456372 < div className = "flex-1 relative" >
457- < Icon
458- icon = "lucide:github"
459- width = { 14 }
460- height = { 14 }
461- className = "absolute left-2.5 top-1/2 -translate-y-1/2 text-[var(--text-disabled)]"
462- />
373+ < Icon icon = "lucide:github" width = { 14 } height = { 14 } className = "absolute left-2.5 top-1/2 -translate-y-1/2 text-[var(--text-disabled)]" />
463374 < input
464375 ref = { repoInputRef }
465376 type = "text"
466377 value = { repoInput }
467- onChange = { ( e ) => {
468- setRepoInput ( e . target . value )
469- setRepoError ( null )
470- } }
471- onKeyDown = { ( e ) => {
472- if ( e . key === 'Enter' ) handleRepoConnect ( )
473- if ( e . key === 'Escape' ) setShowRepoInput ( false )
474- } }
378+ onChange = { ( e ) => { setRepoInput ( e . target . value ) ; setRepoError ( null ) } }
379+ onKeyDown = { ( e ) => { if ( e . key === 'Enter' ) handleRepoConnect ( ) ; if ( e . key === 'Escape' ) setShowRepoInput ( false ) } }
475380 placeholder = "owner/repo"
476- autoCapitalize = "off"
477- autoCorrect = "off"
478- spellCheck = { false }
381+ autoCapitalize = "off" autoCorrect = "off" spellCheck = { false }
479382 className = "w-full pl-8 pr-3 py-2 rounded-lg border border-[var(--border)] bg-[color-mix(in_srgb,var(--bg-elevated)_80%,transparent)] text-[13px] text-[var(--text-primary)] placeholder:text-[var(--text-disabled)] outline-none focus:border-[var(--brand)] transition-colors"
480383 />
481384 </ div >
482385 < button
483386 onClick = { handleRepoConnect }
484387 disabled = { repoLoading || ! repoInput . trim ( ) }
485- 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)]"
388+ className = "shrink-0 px-3 py-2 rounded-lg text-[12px] font-medium cursor-pointer disabled:opacity-40 bg-[var(--brand)] text-[var(--brand-contrast,#fff)]"
486389 >
487390 { repoLoading ? '…' : 'Go' }
488391 </ button >
489392 </ div >
393+ { repoError && < p className = "mt-1.5 text-[11px] text-[var(--color-deletions)]" > { repoError } </ p > }
490394 </ div >
491395 ) }
492396
493- { /* Connected repo */ }
397+ { /* Connected repo — tap to switch */ }
494398 { hasWorkspace && (
495399 < button
496- onClick = { ( ) => {
497- setRepo ( null )
498- setShowRepoInput ( true )
499- } }
400+ onClick = { ( ) => { setRepo ( null ) ; setShowRepoInput ( false ) } }
500401 className = "inline-flex items-center gap-1.5 text-[13px] text-[var(--text-secondary)] hover:text-[var(--text-primary)] transition-colors cursor-pointer"
501402 >
502403 < Icon icon = "lucide:folder-git-2" width = { 13 } height = { 13 } className = "opacity-60" />
503404 { repo ?. fullName ?? repoShort }
504405 < Icon icon = "lucide:chevron-down" width = { 12 } height = { 12 } className = "opacity-40" />
505406 </ button >
506407 ) }
507-
508- { /* Error */ }
509- { repoError && (
510- < p className = "text-[11px] text-[var(--color-deletions)]" > { repoError } </ p >
511- ) }
512408 </ div >
513409 ) }
514410 </ div >
0 commit comments