11'use client' ;
22
3- import { useApi } from '@/hooks/use-api' ;
43import { Badge } from '@trycompai/ui/badge' ;
54import { Button } from '@trycompai/ui/button' ;
65import {
@@ -14,10 +13,16 @@ import { useRealtimeRun } from '@trigger.dev/react-hooks';
1413import { AlertTriangle , ListOrdered , Loader2 , RotateCcw } from 'lucide-react' ;
1514import { useCallback , useEffect , useRef , useState } from 'react' ;
1615import { toast } from 'sonner' ;
17- import { startSingleFix } from '../actions/single-fix' ;
16+ import { startPreview , startSingleFix } from '../actions/single-fix' ;
1817import { AcknowledgmentPanel } from './AcknowledgmentPanel' ;
1918import { PermissionErrorPanel } from './PermissionErrorPanel' ;
2019
20+ interface PreviewProgress {
21+ phase : 'analyzing' | 'complete' | 'failed' ;
22+ error ?: string ;
23+ preview ?: PreviewData ;
24+ }
25+
2126interface SingleFixProgress {
2227 phase : 'executing' | 'success' | 'failed' | 'needs_permissions' ;
2328 error ?: string ;
@@ -267,7 +272,6 @@ export function RemediationDialog({
267272 description,
268273 onComplete,
269274} : RemediationDialogProps ) {
270- const api = useApi ( ) ;
271275 const [ preview , setPreview ] = useState < PreviewData | null > ( null ) ;
272276 const [ isLoadingPreview , setIsLoadingPreview ] = useState ( false ) ;
273277 const [ isExecuting , setIsExecuting ] = useState ( false ) ;
@@ -277,18 +281,53 @@ export function RemediationDialog({
277281 const [ permissionError , setPermissionError ] = useState < { missingActions : string [ ] ; fixScript ?: string } | null > ( null ) ;
278282 const [ acknowledgment , setAcknowledgment ] = useState < string | null > ( null ) ;
279283
280- // Trigger.dev state for async execution
281- const [ runId , setRunId ] = useState < string | null > ( null ) ;
282- const [ triggerAccessToken , setTriggerAccessToken ] = useState < string | null > ( null ) ;
284+ // Trigger.dev state for preview (async)
285+ const [ previewRunId , setPreviewRunId ] = useState < string | null > ( null ) ;
286+ const [ previewAccessToken , setPreviewAccessToken ] = useState < string | null > ( null ) ;
283287
284- const { run } = useRealtimeRun ( runId ?? '' , {
285- accessToken : triggerAccessToken ?? undefined ,
286- enabled : Boolean ( runId && triggerAccessToken ) ,
288+ // Trigger.dev state for execute (async)
289+ const [ executeRunId , setExecuteRunId ] = useState < string | null > ( null ) ;
290+ const [ executeAccessToken , setExecuteAccessToken ] = useState < string | null > ( null ) ;
291+
292+ const { run : previewRun } = useRealtimeRun ( previewRunId ?? '' , {
293+ accessToken : previewAccessToken ?? undefined ,
294+ enabled : Boolean ( previewRunId && previewAccessToken ) ,
287295 } ) ;
288296
289- // Watch task progress and update dialog state
297+ const { run : executeRun } = useRealtimeRun ( executeRunId ?? '' , {
298+ accessToken : executeAccessToken ?? undefined ,
299+ enabled : Boolean ( executeRunId && executeAccessToken ) ,
300+ } ) ;
301+
302+ // Ref to store permissions across rechecks (avoids stale closure in useCallback)
303+ const permissionsRef = useRef < string [ ] | undefined > ( undefined ) ;
304+
305+ // Watch preview task progress
290306 useEffect ( ( ) => {
291- const progress = ( run ?. metadata as { progress ?: SingleFixProgress } | undefined ) ?. progress ;
307+ const progress = ( previewRun ?. metadata as { progress ?: PreviewProgress } | undefined ) ?. progress ;
308+ if ( ! progress || progress . phase === 'analyzing' ) return ;
309+
310+ if ( progress . phase === 'complete' && progress . preview ) {
311+ const previewData = progress . preview as unknown as PreviewData ;
312+ setPreview ( previewData ) ;
313+ if ( previewData . allRequiredPermissions ) {
314+ permissionsRef . current = previewData . allRequiredPermissions ;
315+ }
316+ setIsLoadingPreview ( false ) ;
317+ setPreviewRunId ( null ) ;
318+ setPreviewAccessToken ( null ) ;
319+ } else if ( progress . phase === 'failed' ) {
320+ setError ( progress . error || 'Failed to load preview' ) ;
321+ setIsLoadingPreview ( false ) ;
322+ setPreviewRunId ( null ) ;
323+ setPreviewAccessToken ( null ) ;
324+ }
325+ // eslint-disable-next-line react-hooks/exhaustive-deps
326+ } , [ previewRun ?. metadata ] ) ;
327+
328+ // Watch execute task progress
329+ useEffect ( ( ) => {
330+ const progress = ( executeRun ?. metadata as { progress ?: SingleFixProgress } | undefined ) ?. progress ;
292331 if ( ! progress || progress . phase === 'executing' ) return ;
293332
294333 if ( progress . phase === 'success' ) {
@@ -301,73 +340,61 @@ export function RemediationDialog({
301340 setTimeout ( ( ) => {
302341 onOpenChange ( false ) ;
303342 setSucceeded ( false ) ;
304- setRunId ( null ) ;
305- setTriggerAccessToken ( null ) ;
343+ setExecuteRunId ( null ) ;
344+ setExecuteAccessToken ( null ) ;
306345 } , 4000 ) ;
307346 } else if ( progress . phase === 'failed' ) {
308347 setIsExecuting ( false ) ;
309348 setError ( progress . error || 'Remediation failed' ) ;
310- setRunId ( null ) ;
311- setTriggerAccessToken ( null ) ;
349+ setExecuteRunId ( null ) ;
350+ setExecuteAccessToken ( null ) ;
312351 } else if ( progress . phase === 'needs_permissions' ) {
313352 setIsExecuting ( false ) ;
314353 setError ( progress . error || 'Missing permissions' ) ;
315354 if ( progress . permissionError ) {
316355 setPermissionError ( progress . permissionError ) ;
317356 }
318- setRunId ( null ) ;
319- setTriggerAccessToken ( null ) ;
357+ setExecuteRunId ( null ) ;
358+ setExecuteAccessToken ( null ) ;
320359 }
321360 // eslint-disable-next-line react-hooks/exhaustive-deps
322- } , [ run ?. metadata ] ) ;
323-
324- // Ref to store permissions across rechecks (avoids stale closure in useCallback)
325- const permissionsRef = useRef < string [ ] | undefined > ( undefined ) ;
361+ } , [ executeRun ?. metadata ] ) ;
326362
327363 const loadPreview = useCallback ( async ( recheck = false ) => {
328364 setIsLoadingPreview ( true ) ;
329365 setError ( null ) ;
330366 try {
331- const response = await api . post (
332- '/v1/cloud-security/remediation/preview' ,
333- {
334- connectionId,
335- checkResultId,
336- remediationKey,
337- // On recheck, send the cached permissions so backend doesn't re-run AI
338- ...( recheck && permissionsRef . current && {
339- cachedPermissions : permissionsRef . current ,
340- } ) ,
341- } ,
342- ) ;
343- if ( response . error ) {
344- setError (
345- typeof response . error === 'string'
346- ? response . error
347- : 'Failed to load preview' ,
348- ) ;
367+ const result = await startPreview ( {
368+ connectionId,
369+ checkResultId,
370+ remediationKey,
371+ ...( recheck && permissionsRef . current && {
372+ cachedPermissions : permissionsRef . current ,
373+ } ) ,
374+ } ) ;
375+ if ( result . error || ! result . data ) {
376+ setError ( result . error || 'Failed to load preview' ) ;
377+ setIsLoadingPreview ( false ) ;
349378 return ;
350379 }
351- const previewData = response . data as PreviewData ;
352- setPreview ( previewData ) ;
353- // Store permissions in ref so Recheck can access them without stale closure
354- if ( previewData . allRequiredPermissions ) {
355- permissionsRef . current = previewData . allRequiredPermissions ;
356- }
380+ // Task started — preview effect handles the rest
381+ setPreviewRunId ( result . data . runId ) ;
382+ setPreviewAccessToken ( result . data . accessToken ) ;
357383 } catch {
358384 setError ( 'Failed to load preview' ) ;
359- } finally {
360385 setIsLoadingPreview ( false ) ;
361386 }
362- } , [ api , connectionId , checkResultId , remediationKey ] ) ;
387+ } , [ connectionId , checkResultId , remediationKey ] ) ;
363388
364389 useEffect ( ( ) => {
365390 if ( ! open ) return ;
366391 setError ( null ) ;
367392 setPermissionError ( null ) ;
368393 setAcknowledgment ( null ) ;
369- setRunId ( null ) ;
370- setTriggerAccessToken ( null ) ;
394+ setPreviewRunId ( null ) ;
395+ setPreviewAccessToken ( null ) ;
396+ setExecuteRunId ( null ) ;
397+ setExecuteAccessToken ( null ) ;
371398 setSucceeded ( false ) ;
372399
373400 // Guided-only: skip API call, use local data
@@ -406,9 +433,9 @@ export function RemediationDialog({
406433 setIsExecuting ( false ) ;
407434 return ;
408435 }
409- // Task started — useRealtimeRun effect handles the rest
410- setRunId ( result . data . runId ) ;
411- setTriggerAccessToken ( result . data . accessToken ) ;
436+ // Task started — execute effect handles the rest
437+ setExecuteRunId ( result . data . runId ) ;
438+ setExecuteAccessToken ( result . data . accessToken ) ;
412439 } catch {
413440 setError ( 'Failed to start fix' ) ;
414441 setIsExecuting ( false ) ;
0 commit comments