1- import type { Project , UserMessage } from "@opencode-ai/sdk/v2"
1+ import type { FileDiff , Project , UserMessage } from "@opencode-ai/sdk/v2"
22import { useDialog } from "@opencode-ai/ui/context/dialog"
33import {
44 batch ,
@@ -57,6 +57,9 @@ import { formatServerError } from "@/utils/server-errors"
5757const emptyUserMessages : UserMessage [ ] = [ ]
5858const emptyFollowups : ( FollowupDraft & { id : string } ) [ ] = [ ]
5959
60+ type ChangeMode = "git" | "branch" | "session" | "turn"
61+ type VcsMode = "git" | "branch"
62+
6063type SessionHistoryWindowInput = {
6164 sessionID : ( ) => string | undefined
6265 messagesReady : ( ) => boolean
@@ -415,15 +418,16 @@ export default function Page() {
415418
416419 const info = createMemo ( ( ) => ( params . id ? sync . session . get ( params . id ) : undefined ) )
417420 const diffs = createMemo ( ( ) => ( params . id ? ( sync . data . session_diff [ params . id ] ?? [ ] ) : [ ] ) )
418- const reviewCount = createMemo ( ( ) => Math . max ( info ( ) ?. summary ?. files ?? 0 , diffs ( ) . length ) )
419- const hasReview = createMemo ( ( ) => reviewCount ( ) > 0 )
421+ const sessionCount = createMemo ( ( ) => Math . max ( info ( ) ?. summary ?. files ?? 0 , diffs ( ) . length ) )
422+ const hasSessionReview = createMemo ( ( ) => sessionCount ( ) > 0 )
423+ const canReview = createMemo ( ( ) => ! ! params . id )
420424 const reviewTab = createMemo ( ( ) => isDesktop ( ) )
421425 const tabState = createSessionTabs ( {
422426 tabs,
423427 pathFromTab : file . pathFromTab ,
424428 normalizeTab,
425429 review : reviewTab ,
426- hasReview,
430+ hasReview : canReview ,
427431 } )
428432 const contextOpen = tabState . contextOpen
429433 const openedTabs = tabState . openedTabs
@@ -499,11 +503,22 @@ export default function Page() {
499503 const [ store , setStore ] = createStore ( {
500504 messageId : undefined as string | undefined ,
501505 mobileTab : "session" as "session" | "changes" ,
502- changes : "session " as "session" | "turn" ,
506+ changes : "git " as ChangeMode ,
503507 newSessionWorktree : "main" ,
504508 deferRender : false ,
505509 } )
506510
511+ const [ vcs , setVcs ] = createStore ( {
512+ diff : {
513+ git : [ ] as FileDiff [ ] ,
514+ branch : [ ] as FileDiff [ ] ,
515+ } ,
516+ ready : {
517+ git : false ,
518+ branch : false ,
519+ } ,
520+ } )
521+
507522 const [ followup , setFollowup ] = createStore ( {
508523 items : { } as Record < string , ( FollowupDraft & { id : string } ) [ ] | undefined > ,
509524 sending : { } as Record < string , string | undefined > ,
@@ -531,6 +546,40 @@ export default function Page() {
531546 let refreshTimer : number | undefined
532547 let diffFrame : number | undefined
533548 let diffTimer : number | undefined
549+ const vcsTask = new Map < VcsMode , Promise < void > > ( )
550+
551+ const resetVcs = ( ) => {
552+ vcsTask . clear ( )
553+ setVcs ( {
554+ diff : { git : [ ] , branch : [ ] } ,
555+ ready : { git : false , branch : false } ,
556+ } )
557+ }
558+
559+ const loadVcs = ( mode : VcsMode , force = false ) => {
560+ if ( sync . project ?. vcs !== "git" ) return Promise . resolve ( )
561+ if ( vcs . ready [ mode ] && ! force ) return Promise . resolve ( )
562+ const current = vcsTask . get ( mode )
563+ if ( current ) return current
564+
565+ const task = sdk . client . vcs
566+ . diff ( { mode } )
567+ . then ( ( result ) => {
568+ setVcs ( "diff" , mode , result . data ?? [ ] )
569+ setVcs ( "ready" , mode , true )
570+ } )
571+ . catch ( ( error ) => {
572+ console . debug ( "[session-review] failed to load vcs diff" , { mode, error } )
573+ setVcs ( "diff" , mode , [ ] )
574+ setVcs ( "ready" , mode , true )
575+ } )
576+ . finally ( ( ) => {
577+ vcsTask . delete ( mode )
578+ } )
579+
580+ vcsTask . set ( mode , task )
581+ return task
582+ }
534583
535584 createComputed ( ( prev ) => {
536585 const open = desktopReviewOpen ( )
@@ -546,7 +595,38 @@ export default function Page() {
546595 } , desktopReviewOpen ( ) )
547596
548597 const turnDiffs = createMemo ( ( ) => lastUserMessage ( ) ?. summary ?. diffs ?? [ ] )
549- const reviewDiffs = createMemo ( ( ) => ( store . changes === "session" ? diffs ( ) : turnDiffs ( ) ) )
598+ const changesOptions = createMemo < ChangeMode [ ] > ( ( ) => {
599+ const list : ChangeMode [ ] = [ ]
600+ const git = sync . project ?. vcs === "git"
601+ if ( git ) list . push ( "git" )
602+ if ( git && sync . data . vcs ?. branch && sync . data . vcs ?. default_branch && sync . data . vcs . branch !== sync . data . vcs . default_branch ) {
603+ list . push ( "branch" )
604+ }
605+ list . push ( "session" , "turn" )
606+ return list
607+ } )
608+ const vcsMode = createMemo < VcsMode | undefined > ( ( ) => {
609+ if ( store . changes === "git" || store . changes === "branch" ) return store . changes
610+ } )
611+ const reviewDiffs = createMemo ( ( ) => {
612+ if ( store . changes === "git" ) return vcs . diff . git
613+ if ( store . changes === "branch" ) return vcs . diff . branch
614+ if ( store . changes === "session" ) return diffs ( )
615+ return turnDiffs ( )
616+ } )
617+ const reviewCount = createMemo ( ( ) => {
618+ if ( store . changes === "git" ) return vcs . diff . git . length
619+ if ( store . changes === "branch" ) return vcs . diff . branch . length
620+ if ( store . changes === "session" ) return sessionCount ( )
621+ return turnDiffs ( ) . length
622+ } )
623+ const hasReview = createMemo ( ( ) => reviewCount ( ) > 0 )
624+ const reviewReady = createMemo ( ( ) => {
625+ if ( store . changes === "git" ) return vcs . ready . git
626+ if ( store . changes === "branch" ) return vcs . ready . branch
627+ if ( store . changes === "session" ) return ! hasSessionReview ( ) || diffsReady ( )
628+ return true
629+ } )
550630
551631 const newSessionWorktree = createMemo ( ( ) => {
552632 if ( store . newSessionWorktree === "create" ) return "create"
@@ -615,10 +695,10 @@ export default function Page() {
615695 const diffsReady = createMemo ( ( ) => {
616696 const id = params . id
617697 if ( ! id ) return true
618- if ( ! hasReview ( ) ) return true
698+ if ( ! hasSessionReview ( ) ) return true
619699 return sync . data . session_diff [ id ] !== undefined
620700 } )
621- const reviewEmptyKey = createMemo ( ( ) => {
701+ const sessionEmptyKey = createMemo ( ( ) => {
622702 const project = sync . project
623703 if ( project && ! project . vcs ) return "session.review.noVcs"
624704 if ( sync . data . config . snapshot === false ) return "session.review.noSnapshot"
@@ -741,13 +821,23 @@ export default function Page() {
741821 sessionKey ,
742822 ( ) => {
743823 setStore ( "messageId" , undefined )
744- setStore ( "changes" , "session " )
824+ setStore ( "changes" , "git " )
745825 setUi ( "pendingMessage" , undefined )
746826 } ,
747827 { defer : true } ,
748828 ) ,
749829 )
750830
831+ createEffect (
832+ on (
833+ ( ) => sdk . directory ,
834+ ( ) => {
835+ resetVcs ( )
836+ } ,
837+ { defer : true } ,
838+ ) ,
839+ )
840+
751841 createEffect (
752842 on (
753843 ( ) => params . dir ,
@@ -870,6 +960,38 @@ export default function Page() {
870960 }
871961
872962 const mobileChanges = createMemo ( ( ) => ! isDesktop ( ) && store . mobileTab === "changes" )
963+ const wantsReview = createMemo ( ( ) =>
964+ isDesktop ( ) ? desktopFileTreeOpen ( ) || ( desktopReviewOpen ( ) && activeTab ( ) === "review" ) : store . mobileTab === "changes" ,
965+ )
966+
967+ createEffect ( ( ) => {
968+ const list = changesOptions ( )
969+ if ( list . includes ( store . changes ) ) return
970+ const next = list [ 0 ]
971+ if ( ! next ) return
972+ setStore ( "changes" , next )
973+ } )
974+
975+ createEffect ( ( ) => {
976+ const mode = vcsMode ( )
977+ if ( ! mode ) return
978+ if ( ! wantsReview ( ) ) return
979+ void loadVcs ( mode )
980+ } )
981+
982+ createEffect (
983+ on (
984+ ( ) => sync . data . session_status [ params . id ?? "" ] ?. type ,
985+ ( next , prev ) => {
986+ const mode = vcsMode ( )
987+ if ( ! mode ) return
988+ if ( ! wantsReview ( ) ) return
989+ if ( next !== "idle" || prev === undefined || prev === "idle" ) return
990+ void loadVcs ( mode , true )
991+ } ,
992+ { defer : true } ,
993+ ) ,
994+ )
873995
874996 const fileTreeTab = ( ) => layout . fileTree . tab ( )
875997 const setFileTreeTab = ( value : "changes" | "all" ) => layout . fileTree . setTab ( value )
@@ -916,21 +1038,23 @@ export default function Page() {
9161038 loadFile : file . load ,
9171039 } )
9181040
919- const changesOptions = [ "session" , "turn" ] as const
920- const changesOptionsList = [ ...changesOptions ]
921-
9221041 const changesTitle = ( ) => {
923- if ( ! hasReview ( ) ) {
1042+ if ( ! canReview ( ) ) {
9241043 return null
9251044 }
9261045
1046+ const label = ( option : ChangeMode ) => {
1047+ if ( option === "git" ) return language . t ( "ui.sessionReview.title.git" )
1048+ if ( option === "branch" ) return language . t ( "ui.sessionReview.title.branch" )
1049+ if ( option === "session" ) return language . t ( "ui.sessionReview.title" )
1050+ return language . t ( "ui.sessionReview.title.lastTurn" )
1051+ }
1052+
9271053 return (
9281054 < Select
929- options = { changesOptionsList }
1055+ options = { changesOptions ( ) }
9301056 current = { store . changes }
931- label = { ( option ) =>
932- option === "session" ? language . t ( "ui.sessionReview.title" ) : language . t ( "ui.sessionReview.title.lastTurn" )
933- }
1057+ label = { label }
9341058 onSelect = { ( option ) => option && setStore ( "changes" , option ) }
9351059 variant = "ghost"
9361060 size = "small"
@@ -939,20 +1063,34 @@ export default function Page() {
9391063 )
9401064 }
9411065
942- const emptyTurn = ( ) => (
1066+ const empty = ( text : string ) => (
9431067 < div class = "h-full pb-64 -mt-4 flex flex-col items-center justify-center text-center gap-6" >
944- < div class = "text-14-regular text-text-weak max-w-56" > { language . t ( "session.review.noChanges" ) } </ div >
1068+ < div class = "text-14-regular text-text-weak max-w-56" > { text } </ div >
9451069 </ div >
9461070 )
9471071
1072+ const reviewEmptyText = createMemo ( ( ) => {
1073+ if ( store . changes === "git" ) return language . t ( "session.review.noUncommittedChanges" )
1074+ if ( store . changes === "branch" ) return language . t ( "session.review.noBranchChanges" )
1075+ if ( store . changes === "turn" ) return language . t ( "session.review.noChanges" )
1076+ return language . t ( sessionEmptyKey ( ) )
1077+ } )
1078+
9481079 const reviewEmpty = ( input : { loadingClass : string ; emptyClass : string } ) => {
949- if ( store . changes === "turn" ) return emptyTurn ( )
1080+ if ( store . changes === "git" || store . changes === "branch" ) {
1081+ if ( ! reviewReady ( ) ) return < div class = { input . loadingClass } > { language . t ( "session.review.loadingChanges" ) } </ div >
1082+ return empty ( reviewEmptyText ( ) )
1083+ }
1084+
1085+ if ( store . changes === "turn" ) {
1086+ return empty ( reviewEmptyText ( ) )
1087+ }
9501088
951- if ( hasReview ( ) && ! diffsReady ( ) ) {
1089+ if ( hasSessionReview ( ) && ! diffsReady ( ) ) {
9521090 return < div class = { input . loadingClass } > { language . t ( "session.review.loadingChanges" ) } </ div >
9531091 }
9541092
955- if ( reviewEmptyKey ( ) === "session.review.noVcs" ) {
1093+ if ( sessionEmptyKey ( ) === "session.review.noVcs" ) {
9561094 return (
9571095 < div class = { input . emptyClass } >
9581096 < div class = "flex flex-col gap-3" >
@@ -972,7 +1110,7 @@ export default function Page() {
9721110
9731111 return (
9741112 < div class = { input . emptyClass } >
975- < div class = "text-14-regular text-text-weak max-w-56" > { language . t ( reviewEmptyKey ( ) ) } </ div >
1113+ < div class = "text-14-regular text-text-weak max-w-56" > { reviewEmptyText ( ) } </ div >
9761114 </ div >
9771115 )
9781116 }
@@ -1076,7 +1214,7 @@ export default function Page() {
10761214 const pending = tree . pendingDiff
10771215 if ( ! pending ) return
10781216 if ( ! tree . reviewScroll ) return
1079- if ( ! diffsReady ( ) ) return
1217+ if ( ! reviewReady ( ) ) return
10801218
10811219 const attempt = ( count : number ) => {
10821220 if ( tree . pendingDiff !== pending ) return
@@ -1808,6 +1946,12 @@ export default function Page() {
18081946 </ div >
18091947
18101948 < SessionSidePanel
1949+ canReview = { canReview }
1950+ diffs = { reviewDiffs }
1951+ diffsReady = { reviewReady }
1952+ empty = { reviewEmptyText }
1953+ hasReview = { hasReview }
1954+ reviewCount = { reviewCount }
18111955 reviewPanel = { reviewPanel }
18121956 activeDiff = { tree . activeDiff }
18131957 focusReviewDiff = { focusReviewDiff }
0 commit comments