@@ -95,9 +95,35 @@ export default function WorkbenchDrawer(props: WorkbenchDrawerProps) {
9595 const [ chatHistoryLoading , setChatHistoryLoading ] = useState ( false ) ;
9696 const [ chatHistoryError , setChatHistoryError ] = useState < string | null > ( null ) ;
9797 const [ copiedStatePath , setCopiedStatePath ] = useState ( false ) ;
98+ const [ copiedCodexLoginCommand , setCopiedCodexLoginCommand ] = useState ( false ) ;
99+ const [ codexTrustPending , setCodexTrustPending ] = useState ( false ) ;
100+ const [ codexTrustError , setCodexTrustError ] = useState < string | null > ( null ) ;
101+ const [ codexTrustSuccess , setCodexTrustSuccess ] = useState < string | null > (
102+ null ,
103+ ) ;
104+ const [ codexWorkspaceWriteEnabled , setCodexWorkspaceWriteEnabled ] = useState <
105+ boolean | null
106+ > ( null ) ;
107+ const [ codexWorkspaceLoggedIn , setCodexWorkspaceLoggedIn ] = useState <
108+ boolean | null
109+ > ( null ) ;
110+ const [ codexLoginStatusText , setCodexLoginStatusText ] = useState <
111+ string | null
112+ > ( null ) ;
113+ const [ codexTrustedPath , setCodexTrustedPath ] = useState < string | null > ( null ) ;
114+ const [ codexTrustOverlayDismissed , setCodexTrustOverlayDismissed ] = useState (
115+ false ,
116+ ) ;
98117 const initializedChipTrackingRef = useRef ( false ) ;
99118 const seenRatingChipIdsRef = useRef ( new Set < string > ( ) ) ;
100119 const seenFlagChipIdsRef = useRef ( new Set < string > ( ) ) ;
120+ const showCodexTrustOverlay = ( codexWorkspaceWriteEnabled === false ||
121+ codexWorkspaceLoggedIn === false ) &&
122+ ! codexTrustOverlayDismissed || Boolean ( codexTrustError ) ;
123+ const workspaceIdForTrust = ( sessionId ?? run . id ) || undefined ;
124+ const codexLoginCommand = codexTrustedPath
125+ ? `CODEX_HOME="${ codexTrustedPath } /.codex" codex login`
126+ : 'CODEX_HOME="<workspace>/.codex" codex login' ;
101127 const resolvedStatePath = useMemo ( ( ) => {
102128 if ( statePath ) return statePath ;
103129 const meta = sessionDetail ?. meta ;
@@ -327,8 +353,67 @@ export default function WorkbenchDrawer(props: WorkbenchDrawerProps) {
327353 initializedChipTrackingRef . current = false ;
328354 seenRatingChipIdsRef . current . clear ( ) ;
329355 seenFlagChipIdsRef . current . clear ( ) ;
356+ setCodexTrustPending ( false ) ;
357+ setCodexTrustError ( null ) ;
358+ setCodexTrustSuccess ( null ) ;
359+ setCodexWorkspaceWriteEnabled ( null ) ;
360+ setCodexWorkspaceLoggedIn ( null ) ;
361+ setCodexLoginStatusText ( null ) ;
362+ setCodexTrustedPath ( null ) ;
363+ setCopiedCodexLoginCommand ( false ) ;
364+ setCodexTrustOverlayDismissed ( false ) ;
330365 } , [ sessionId ] ) ;
331366
367+ useEffect ( ( ) => {
368+ if ( ! open ) return ;
369+ if ( ! workspaceIdForTrust ) return ;
370+ let canceled = false ;
371+ setCodexTrustError ( null ) ;
372+ fetch (
373+ `/api/codex/trust-workspace?workspaceId=${
374+ encodeURIComponent ( workspaceIdForTrust )
375+ } `,
376+ )
377+ . then ( async ( response ) => {
378+ const payload = await response . json ( ) as {
379+ ok ?: boolean ;
380+ trusted ?: boolean ;
381+ writeEnabled ?: boolean ;
382+ codexLoggedIn ?: boolean ;
383+ codexLoginStatus ?: string ;
384+ trustedPath ?: string ;
385+ error ?: string ;
386+ } ;
387+ if ( ! response . ok || payload . ok === false ) {
388+ throw new Error ( payload . error || response . statusText ) ;
389+ }
390+ if ( canceled ) return ;
391+ setCodexWorkspaceWriteEnabled ( payload . writeEnabled === true ) ;
392+ setCodexWorkspaceLoggedIn ( payload . codexLoggedIn === true ) ;
393+ setCodexLoginStatusText (
394+ typeof payload . codexLoginStatus === "string"
395+ ? payload . codexLoginStatus
396+ : null ,
397+ ) ;
398+ setCodexTrustedPath (
399+ typeof payload . trustedPath === "string" ? payload . trustedPath : null ,
400+ ) ;
401+ } )
402+ . catch ( ( err ) => {
403+ if ( canceled ) return ;
404+ setCodexWorkspaceWriteEnabled ( null ) ;
405+ setCodexWorkspaceLoggedIn ( null ) ;
406+ setCodexLoginStatusText ( null ) ;
407+ setCodexTrustError ( err instanceof Error ? err . message : String ( err ) ) ;
408+ } )
409+ . finally ( ( ) => {
410+ if ( canceled ) return ;
411+ } ) ;
412+ return ( ) => {
413+ canceled = true ;
414+ } ;
415+ } , [ open , workspaceIdForTrust ] ) ;
416+
332417 useEffect ( ( ) => {
333418 if ( loading ) return ;
334419 const currentRatingChipIds = new Set (
@@ -482,6 +567,11 @@ export default function WorkbenchDrawer(props: WorkbenchDrawerProps) {
482567 window . setTimeout ( ( ) => setCopiedStatePath ( false ) , 1200 ) ;
483568 } ;
484569 } , [ resolvedStatePath ] ) ;
570+ const handleCopyCodexLoginCommand = useCallback ( ( ) => {
571+ navigator . clipboard ?. writeText ( codexLoginCommand ) ;
572+ setCopiedCodexLoginCommand ( true ) ;
573+ window . setTimeout ( ( ) => setCopiedCodexLoginCommand ( false ) , 1200 ) ;
574+ } , [ codexLoginCommand ] ) ;
485575 useEffect ( ( ) => {
486576 if ( ! open ) return ;
487577 if ( ! onClose ) return ;
@@ -493,6 +583,90 @@ export default function WorkbenchDrawer(props: WorkbenchDrawerProps) {
493583 window . addEventListener ( "keydown" , handler ) ;
494584 return ( ) => window . removeEventListener ( "keydown" , handler ) ;
495585 } , [ onClose , open ] ) ;
586+ const trustWorkspaceInCodex = useCallback ( async ( ) => {
587+ setCodexTrustPending ( true ) ;
588+ setCodexTrustError ( null ) ;
589+ setCodexTrustSuccess ( null ) ;
590+ try {
591+ const statusResponse = await fetch (
592+ `/api/codex/trust-workspace?workspaceId=${
593+ encodeURIComponent ( workspaceIdForTrust ?? "" )
594+ } `,
595+ ) ;
596+ const statusPayload = await statusResponse . json ( ) as {
597+ ok ?: boolean ;
598+ trusted ?: boolean ;
599+ writeEnabled ?: boolean ;
600+ codexLoggedIn ?: boolean ;
601+ codexLoginStatus ?: string ;
602+ trustedPath ?: string ;
603+ error ?: string ;
604+ } ;
605+ if ( ! statusResponse . ok || statusPayload . ok === false ) {
606+ throw new Error ( statusPayload . error || statusResponse . statusText ) ;
607+ }
608+ if (
609+ statusPayload . writeEnabled === true &&
610+ statusPayload . codexLoggedIn === true
611+ ) {
612+ setCodexWorkspaceWriteEnabled ( true ) ;
613+ setCodexWorkspaceLoggedIn ( true ) ;
614+ setCodexTrustSuccess (
615+ "Workspace is already configured for Codex writes." ,
616+ ) ;
617+ setCodexTrustOverlayDismissed ( true ) ;
618+ return ;
619+ }
620+ setCodexWorkspaceWriteEnabled ( statusPayload . writeEnabled === true ) ;
621+ setCodexWorkspaceLoggedIn ( statusPayload . codexLoggedIn === true ) ;
622+ setCodexLoginStatusText (
623+ typeof statusPayload . codexLoginStatus === "string"
624+ ? statusPayload . codexLoginStatus
625+ : null ,
626+ ) ;
627+ setCodexTrustedPath (
628+ typeof statusPayload . trustedPath === "string"
629+ ? statusPayload . trustedPath
630+ : null ,
631+ ) ;
632+
633+ const response = await fetch ( "/api/codex/trust-workspace" , {
634+ method : "POST" ,
635+ headers : { "content-type" : "application/json" } ,
636+ body : JSON . stringify ( { workspaceId : workspaceIdForTrust } ) ,
637+ } ) ;
638+ const payload = await response . json ( ) as {
639+ ok ?: boolean ;
640+ error ?: string ;
641+ trustedPath ?: string ;
642+ writeEnabled ?: boolean ;
643+ codexLoggedIn ?: boolean ;
644+ codexLoginStatus ?: string ;
645+ } ;
646+ if ( ! response . ok || payload . ok === false ) {
647+ throw new Error ( payload . error || response . statusText ) ;
648+ }
649+ const trustedPath = typeof payload . trustedPath === "string"
650+ ? payload . trustedPath
651+ : "workspace" ;
652+ setCodexTrustSuccess ( `Codex write enabled for: ${ trustedPath } ` ) ;
653+ setCodexWorkspaceWriteEnabled ( payload . writeEnabled === true ) ;
654+ setCodexWorkspaceLoggedIn ( payload . codexLoggedIn === true ) ;
655+ setCodexLoginStatusText (
656+ typeof payload . codexLoginStatus === "string"
657+ ? payload . codexLoginStatus
658+ : null ,
659+ ) ;
660+ setCodexTrustedPath (
661+ typeof payload . trustedPath === "string" ? payload . trustedPath : null ,
662+ ) ;
663+ setCodexTrustOverlayDismissed ( payload . codexLoggedIn === true ) ;
664+ } catch ( err ) {
665+ setCodexTrustError ( err instanceof Error ? err . message : String ( err ) ) ;
666+ } finally {
667+ setCodexTrustPending ( false ) ;
668+ }
669+ } , [ workspaceIdForTrust ] ) ;
496670 if ( ! open ) return null ;
497671 return (
498672 < aside className = "workbench-drawer-docked" role = "dialog" >
@@ -605,6 +779,81 @@ export default function WorkbenchDrawer(props: WorkbenchDrawerProps) {
605779 chatHistoryOpen ? " is-history" : ""
606780 } `}
607781 >
782+ { showCodexTrustOverlay && (
783+ < div className = "workbench-chat-readonly-overlay" >
784+ < div className = "workbench-chat-readonly-card" >
785+ < h3 className = "workbench-chat-readonly-title" >
786+ Codex setup required
787+ </ h3 >
788+ { codexWorkspaceWriteEnabled === false && (
789+ < p className = "workbench-chat-readonly-copy" >
790+ Codex write access is disabled for this
791+ workspace. Trust this workspace to enable file
792+ edits.
793+ </ p >
794+ ) }
795+ < div className = "workbench-chat-readonly-actions" >
796+ { codexWorkspaceWriteEnabled === false && (
797+ < Button
798+ variant = "primary"
799+ onClick = { ( ) => trustWorkspaceInCodex ( ) }
800+ disabled = { codexTrustPending }
801+ >
802+ { codexTrustPending
803+ ? "Trusting..."
804+ : "Trust workspace" }
805+ </ Button >
806+ ) }
807+ </ div >
808+ { codexWorkspaceLoggedIn === false && (
809+ < >
810+ < p className = "workbench-chat-readonly-copy" >
811+ Codex login is required for this workspace.
812+ </ p >
813+ < p className = "workbench-chat-readonly-copy" >
814+ Run this in this workspace to authenticate
815+ Codex, then restart Gambit.
816+ </ p >
817+ < div className = "workbench-chat-command-row" >
818+ < pre className = "workbench-chat-command-code" >
819+ < code > { codexLoginCommand } </ code >
820+ </ pre >
821+ < Button
822+ variant = "secondary"
823+ size = "small"
824+ onClick = { handleCopyCodexLoginCommand }
825+ >
826+ < Icon
827+ name = { copiedCodexLoginCommand
828+ ? "copied"
829+ : "copy" }
830+ size = { 14 }
831+ />
832+ { copiedCodexLoginCommand
833+ ? "Copied"
834+ : "Copy" }
835+ </ Button >
836+ </ div >
837+ </ >
838+ ) }
839+ { codexLoginStatusText &&
840+ ! / ^ n o t l o g g e d i n $ / i. test (
841+ codexLoginStatusText . trim ( ) ,
842+ ) && < Callout > { codexLoginStatusText } </ Callout > }
843+ { codexTrustError && (
844+ < div className = "error" > { codexTrustError } </ div >
845+ ) }
846+ < Button
847+ variant = "secondary"
848+ onClick = { ( ) =>
849+ setCodexTrustOverlayDismissed ( true ) }
850+ disabled = { codexTrustPending }
851+ >
852+ Dismiss
853+ </ Button >
854+ </ div >
855+ </ div >
856+ ) }
608857 < Chat
609858 composerChips = { composerChips }
610859 onComposerChipsChange = { onComposerChipsChange }
@@ -621,6 +870,7 @@ export default function WorkbenchDrawer(props: WorkbenchDrawerProps) {
621870 defaultOpen : false ,
622871 content : (
623872 < div className = "workbench-ratings" >
873+ { codexTrustSuccess && < Callout > { codexTrustSuccess } </ Callout > }
624874 { showCopyStatePath && handleCopyStatePath && (
625875 < >
626876 < Button variant = "secondary" onClick = { handleCopyStatePath } >
0 commit comments