@@ -12,9 +12,10 @@ import { PanesFSM, stripTransientTabFields } from './panes-fsm'
1212import { ActivityDeriver } from './activity-deriver'
1313import type { TerminalTab , WorkspacePane } from '../shared/state/terminals'
1414import { listWorktrees , listBranches , continueWorktree , removeWorktree , isWorktreeDirty , defaultWorktreeDir , getChangedFiles , getFileDiff , getBranchCommits , getCommitDiff , getMainWorktreeStatus , prepareMainForMerge , mergeWorktreeLocally , getBranchSha , previewMergeConflicts , getBranchDiffStats , listAllFiles , readWorktreeFile , runWorktreeScript , type MergeStrategy } from './worktree'
15- import { getPRStatus , testToken , starRepo } from './github'
15+ import { getPRStatus , testToken , starRepo , unstarRepo , isRepoStarred } from './github'
1616import { AVAILABLE_EDITORS , DEFAULT_EDITOR_ID , openInEditor } from './editor'
1717import { setSecret , hasSecret , deleteSecret } from './secrets'
18+ import { resolveGitHubToken , getTokenSource , invalidateTokenCache , getCachedToken } from './github-auth'
1819import {
1920 loadConfig ,
2021 saveConfig ,
@@ -119,11 +120,36 @@ const store = new Store({
119120 editor : config . editor || DEFAULT_EDITOR_ID ,
120121 worktreeBase : config . worktreeBase || DEFAULT_WORKTREE_BASE ,
121122 mergeStrategy : config . mergeStrategy || DEFAULT_MERGE_STRATEGY ,
122- hasGithubToken : hasSecret ( 'githubToken' )
123+ hasGithubToken : hasSecret ( 'githubToken' ) ,
124+ githubAuthSource : null
123125 }
124126} )
125127registerStateTransport ( store )
126128
129+ /** Query the harness star state, dispatch it to the slice, and auto-star
130+ * exactly once per user (sticky so manual unstars survive reboots). Safe
131+ * to call after any token resolution — boot, PAT save, etc. */
132+ async function refreshHarnessStarState ( ) : Promise < void > {
133+ const token = getCachedToken ( )
134+ if ( ! token ) {
135+ store . dispatch ( { type : 'settings/harnessStarredChanged' , payload : null } )
136+ return
137+ }
138+ const starred = await isRepoStarred ( token , 'frenchie4111' , 'harness' )
139+ if ( starred === false && ! config . harnessAutoStarred ) {
140+ const result = await starRepo ( token , 'frenchie4111' , 'harness' )
141+ if ( result . ok ) {
142+ config . harnessAutoStarred = true
143+ saveConfig ( config )
144+ store . dispatch ( { type : 'settings/harnessStarredChanged' , payload : true } )
145+ log ( 'app' , 'auto-starred harness on first GitHub connection' )
146+ return
147+ }
148+ log ( 'app' , 'auto-star failed' , result . error )
149+ }
150+ store . dispatch ( { type : 'settings/harnessStarredChanged' , payload : starred } )
151+ }
152+
127153const prPoller = new PRPoller ( store , {
128154 getRepoRoots : ( ) => config . repoRoots || [ ] ,
129155 getLocallyMerged : ( ) => config . locallyMerged || { } ,
@@ -886,36 +912,51 @@ function registerIpcHandlers(): void {
886912 return store . getSnapshot ( ) . state . settings . hasGithubToken
887913 } )
888914
889- ipcMain . handle ( 'settings:setGithubToken' , async ( _ , token : string , options ?: { starRepo ?: boolean } ) => {
915+ ipcMain . handle ( 'settings:setGithubToken' , async ( _ , token : string ) => {
890916 const trimmed = token . trim ( )
891917 if ( ! trimmed ) {
892918 deleteSecret ( 'githubToken' )
893919 store . dispatch ( { type : 'settings/hasGithubTokenChanged' , payload : false } )
920+ invalidateTokenCache ( )
921+ await resolveGitHubToken ( )
922+ store . dispatch ( { type : 'settings/githubAuthSourceChanged' , payload : getTokenSource ( ) } )
923+ await refreshHarnessStarState ( )
894924 return { ok : true }
895925 }
896926 // Validate the token first by hitting /user
897927 const test = await testToken ( trimmed )
898928 if ( ! test . ok ) return { ok : false , error : test . error }
899929 setSecret ( 'githubToken' , trimmed )
900930 store . dispatch ( { type : 'settings/hasGithubTokenChanged' , payload : true } )
901-
902- // Optionally star the repo — fire and forget, don't fail token save if this fails
903- let starred = false
904- if ( options ?. starRepo ) {
905- const result = await starRepo ( trimmed , 'frenchie4111' , 'harness' )
906- starred = result . ok
907- if ( ! result . ok ) log ( 'app' , 'failed to star repo' , result . error )
908- }
909-
910- return { ok : true , username : test . username , starred }
931+ invalidateTokenCache ( )
932+ await resolveGitHubToken ( )
933+ store . dispatch ( { type : 'settings/githubAuthSourceChanged' , payload : getTokenSource ( ) } )
934+ await refreshHarnessStarState ( )
935+ return { ok : true , username : test . username }
911936 } )
912937
913- ipcMain . handle ( 'settings:clearGithubToken' , ( ) => {
938+ ipcMain . handle ( 'settings:clearGithubToken' , async ( ) => {
914939 deleteSecret ( 'githubToken' )
915940 store . dispatch ( { type : 'settings/hasGithubTokenChanged' , payload : false } )
941+ invalidateTokenCache ( )
942+ await resolveGitHubToken ( )
943+ store . dispatch ( { type : 'settings/githubAuthSourceChanged' , payload : getTokenSource ( ) } )
944+ await refreshHarnessStarState ( )
916945 return true
917946 } )
918947
948+ ipcMain . handle ( 'settings:setHarnessStarred' , async ( _ , starred : boolean ) => {
949+ const token = getCachedToken ( )
950+ if ( ! token ) return { ok : false , error : 'No GitHub token' }
951+ const result = starred
952+ ? await starRepo ( token , 'frenchie4111' , 'harness' )
953+ : await unstarRepo ( token , 'frenchie4111' , 'harness' )
954+ if ( result . ok ) {
955+ store . dispatch ( { type : 'settings/harnessStarredChanged' , payload : starred } )
956+ }
957+ return result
958+ } )
959+
919960 // Updater
920961 ipcMain . handle ( 'updater:getVersion' , ( ) => {
921962 return app . getVersion ( )
@@ -1228,10 +1269,17 @@ app.whenReady().then(() => {
12281269 // and writes recordActivity + lastActive without renderer involvement.
12291270 activityDeriver . start ( )
12301271
1231- // Kick off the PR poller. An initial refreshAll() runs immediately and
1232- // then on a 5-minute interval.
1233- prPoller . start ( )
1234- void prPoller . refreshAll ( )
1272+ // Resolve the GitHub token (PAT → gh CLI → none) before the PR poller
1273+ // makes its first call. The poller's initial refreshAll waits on this.
1274+ void ( async ( ) => {
1275+ await resolveGitHubToken ( )
1276+ const source = getTokenSource ( )
1277+ store . dispatch ( { type : 'settings/githubAuthSourceChanged' , payload : source } )
1278+ prPoller . start ( )
1279+ void prPoller . refreshAll ( )
1280+
1281+ await refreshHarnessStarState ( )
1282+ } ) ( )
12351283
12361284 // Seed hooks.consent from disk. If any known worktree already has the
12371285 // hooks installed, the user must have accepted at some point — remember
0 commit comments