1- import { Component , Show , For , createSignal , createMemo , lazy , Suspense } from "solid-js" ;
1+ import { Component , Show , For , createSignal , createMemo , lazy , Suspense , onCleanup } from "solid-js" ;
22import { projectRoot , openFile , openDiff , refreshFileTree } from "../../stores/fileStore" ;
3- import { revealPath , renameEntry , deleteEntry , scanFilesSecurity , gitDiff , generateCommitMessage , getApiKey , aiReviewCode , type CodeReviewResult } from "../../lib/tauri" ;
3+ import { revealPath , renameEntry , deleteEntry , scanFilesSecurity , gitDiff , generateCommitMessage , getApiKey , aiReviewCode , onCodeReviewStart , onCodeReviewStream , onCodeReviewDone , onCodeReviewError } from "../../lib/tauri" ;
44import { securityEnabled , setSecurityResults , setSecurityShowModal , securityShowModal } from "../../stores/securityStore" ;
55import SecurityModal from "../security/SecurityModal" ;
66import ContextMenu , { type ContextMenuItem } from "../explorer/ContextMenu" ;
@@ -16,6 +16,7 @@ import type { GitLogEntry } from "../../types/git";
1616import { FileRow , GitGraphRow , PlusIcon } from "../git" ;
1717import { ResizeHandle , GitSyncButton , SidebarToolbarButton } from "../ui" ;
1818import { settings } from "../../stores/settingsStore" ;
19+ import type { UnlistenFn } from "@tauri-apps/api/event" ;
1920
2021const FileTree = lazy ( ( ) => import ( "../explorer/FileTree" ) ) ;
2122
@@ -78,10 +79,11 @@ const RightSidebar: Component<{ onOpenFolder?: () => void; onOpenRecent?: (path:
7879 const [ isDraggingGitSplitter , setIsDraggingGitSplitter ] = createSignal ( false ) ;
7980 const [ searchQuery , setSearchQuery ] = createSignal ( "" ) ;
8081 const [ isAiReviewing , setIsAiReviewing ] = createSignal ( false ) ;
81- const [ aiReviewResult , setAiReviewResult ] = createSignal < CodeReviewResult | null > ( null ) ;
82+ const [ aiReviewContent , setAiReviewContent ] = createSignal ( "" ) ;
8283 const [ aiReviewExpanded , setAiReviewExpanded ] = createSignal ( true ) ;
83- const [ aiReviewAbortController , setAiReviewAbortController ] = createSignal < AbortController | null > ( null ) ;
84+ const [ aiReviewFiles , setAiReviewFiles ] = createSignal < string [ ] > ( [ ] ) ;
8485 let gitSplitContainerRef : HTMLDivElement | undefined ;
86+ let aiReviewUnlisten : UnlistenFn | undefined ;
8587
8688 async function handleGenerateCommitMessage ( ) {
8789 if ( isGeneratingMsg ( ) || stagedFiles ( ) . length === 0 ) return ;
@@ -110,56 +112,75 @@ const RightSidebar: Component<{ onOpenFolder?: () => void; onOpenRecent?: (path:
110112 async function handleAiReview ( ) {
111113 if ( isAiReviewing ( ) || stagedFiles ( ) . length === 0 ) return ;
112114 setIsAiReviewing ( true ) ;
113- setAiReviewResult ( null ) ;
115+ setAiReviewContent ( "" ) ;
114116
115- const controller = new AbortController ( ) ;
116- setAiReviewAbortController ( controller ) ;
117-
118- try {
119- const s = settings ( ) ;
120- const model = s . aiModel || "anthropic/claude-sonnet-4" ;
121- const provider = s . aiProvider || "openrouter" ;
122- const apiKey = await getApiKey ( provider ) ;
117+ const s = settings ( ) ;
118+ const model = s . aiModel || "anthropic/claude-sonnet-4" ;
119+ const provider = s . aiProvider || "openrouter" ;
120+ const apiKey = await getApiKey ( provider ) ;
123121
124- const diff = await gitDiff ( projectRoot ( ) || "" , undefined ) ;
125- const stagedPaths = stagedFiles ( ) . map ( f => f . path ) ;
122+ const diff = await gitDiff ( projectRoot ( ) || "" , undefined ) ;
123+ const stagedPaths = stagedFiles ( ) . map ( f => f . path ) ;
126124
127- const result = await aiReviewCode ( diff , stagedPaths , model , apiKey , provider ) ;
128- setAiReviewResult ( result ) ;
129- setAiReviewExpanded ( true ) ;
125+ // Set up event listeners for streaming
126+ const unlistenStart = await onCodeReviewStart ( ( files ) => {
127+ setAiReviewFiles ( files ) ;
128+ } ) ;
129+
130+ const unlistenStream = await onCodeReviewStream ( ( chunk ) => {
131+ setAiReviewContent ( prev => prev + chunk ) ;
132+ } ) ;
133+
134+ const unlistenDone = await onCodeReviewDone ( ( ) => {
135+ setIsAiReviewing ( false ) ;
136+ } ) ;
137+
138+ const unlistenError = await onCodeReviewError ( ( error ) => {
139+ console . error ( "AI review error:" , error ) ;
140+ setAiReviewContent ( prev => prev + `\n\n**Error:** ${ error } ` ) ;
141+ setIsAiReviewing ( false ) ;
142+ } ) ;
143+
144+ // Store unlisteners for cleanup
145+ aiReviewUnlisten = ( ) => {
146+ unlistenStart ( ) ;
147+ unlistenStream ( ) ;
148+ unlistenDone ( ) ;
149+ unlistenError ( ) ;
150+ } ;
151+
152+ setAiReviewExpanded ( true ) ;
153+
154+ try {
155+ await aiReviewCode ( diff , stagedPaths , model , apiKey , provider ) ;
130156 } catch ( e ) {
131157 console . error ( "Failed to run AI code review:" , e ) ;
132- } finally {
133158 setIsAiReviewing ( false ) ;
134- setAiReviewAbortController ( null ) ;
135159 }
136160 }
137161
138162 function cancelAiReview ( ) {
139- const controller = aiReviewAbortController ( ) ;
140- if ( controller ) {
141- controller . abort ( ) ;
142- setIsAiReviewing ( false ) ;
143- setAiReviewAbortController ( null ) ;
163+ // Currently no backend cancel for this - just reset state
164+ if ( aiReviewUnlisten ) {
165+ aiReviewUnlisten ( ) ;
144166 }
167+ setIsAiReviewing ( false ) ;
168+ setAiReviewContent ( prev => prev + "\n\n*[Cancelled]*" ) ;
145169 }
146170
147171 function copyAiReviewToAgent ( ) {
148- if ( ! aiReviewResult ( ) ) return ;
149- const result = aiReviewResult ( ) ! ;
150- let text = `## AI Code Review\n\n**Files scanned:** ${ result . files_scanned . join ( ", " ) } \n\n` ;
151- text += `**Summary:** ${ result . summary } \n\n` ;
152- if ( result . suggestions . length > 0 ) {
153- text += `### Suggestions\n\n` ;
154- for ( const s of result . suggestions ) {
155- text += `**${ s . severity . toUpperCase ( ) } **: ${ s . file } ${ s . line ? `:${ s . line } ` : "" } - ${ s . title } \n` ;
156- text += `${ s . description } \n` ;
157- if ( s . suggestion ) text += `Suggestion: ${ s . suggestion } \n` ;
158- text += "\n" ;
159- }
160- }
172+ const content = aiReviewContent ( ) ;
173+ if ( ! content ) return ;
174+
175+ const text = `## AI Code Review\n\n**Files:** ${ aiReviewFiles ( ) . join ( ", " ) } \n\n${ content } ` ;
161176 navigator . clipboard . writeText ( text ) ;
162177 }
178+
179+ onCleanup ( ( ) => {
180+ if ( aiReviewUnlisten ) {
181+ aiReviewUnlisten ( ) ;
182+ }
183+ } ) ;
163184
164185 function handleGitSplitterMouseDown ( e : MouseEvent ) {
165186 e . preventDefault ( ) ;
@@ -941,33 +962,30 @@ const RightSidebar: Component<{ onOpenFolder?: () => void; onOpenRecent?: (path:
941962
942963 { /* Expandable content */ }
943964 < Show when = { aiReviewExpanded ( ) } >
944- < div class = "px-2 py-1.5" style = { { "max-height" : "150px " , "overflow-y" : "auto" } } >
945- < Show when = { isAiReviewing ( ) } >
965+ < div class = "px-2 py-1.5" style = { { "max-height" : "200px " , "overflow-y" : "auto" } } >
966+ < Show when = { isAiReviewing ( ) && ! aiReviewContent ( ) } >
946967 < div class = "flex items-center gap-2 text-xs" style = { { color : "var(--text-muted)" } } >
947968 < SpinnerIcon />
948969 < span > Analyzing staged changes...</ span >
949970 </ div >
950971 </ Show >
951- < Show when = { aiReviewResult ( ) } >
952- < div class = "text-xs" style = { { color : "var(--text-muted)" , "margin-bottom" : "8px" } } >
953- { aiReviewResult ( ) ! . summary }
972+ < Show when = { aiReviewContent ( ) } >
973+ < div
974+ class = "text-xs"
975+ style = { {
976+ color : "var(--text-primary)" ,
977+ "white-space" : "pre-wrap" ,
978+ "word-break" : "break-word" ,
979+ "line-height" : "1.5" ,
980+ } }
981+ >
982+ { aiReviewContent ( ) }
954983 </ div >
955- < Show when = { aiReviewResult ( ) ! . suggestions . length > 0 } >
956- < For each = { aiReviewResult ( ) ! . suggestions } >
957- { ( s ) => (
958- < div class = "mb-2 p-1.5 rounded" style = { { background : "var(--bg-hover)" , "font-size" : "0.75em" } } >
959- < div class = "flex items-center gap-1" style = { { color : s . severity === "warning" ? "var(--accent-yellow)" : "var(--text-secondary)" } } >
960- < span style = { { "font-weight" : 600 } } > { s . severity . toUpperCase ( ) } </ span >
961- < span style = { { color : "var(--text-muted)" } } > { s . file } { s . line ? `:${ s . line } ` : "" } </ span >
962- </ div >
963- < div style = { { color : "var(--text-primary)" , "margin-top" : "2px" } } > { s . title } </ div >
964- </ div >
965- ) }
966- </ For >
984+ < Show when = { ! isAiReviewing ( ) } >
967985 < button
968986 type = "button"
969987 onClick = { copyAiReviewToAgent }
970- class = "w-full py-1 rounded text-xs transition-colors"
988+ class = "w-full mt-2 py-1 rounded text-xs transition-colors"
971989 style = { {
972990 background : "var(--accent-primary)" ,
973991 color : "var(--accent-text, #fff)" ,
@@ -978,15 +996,10 @@ const RightSidebar: Component<{ onOpenFolder?: () => void; onOpenRecent?: (path:
978996 Copy to Agent
979997 </ button >
980998 </ Show >
981- < Show when = { aiReviewResult ( ) ! . suggestions . length === 0 } >
982- < div class = "text-xs" style = { { color : "var(--accent-green)" } } >
983- No issues found in staged changes.
984- </ div >
985- </ Show >
986999 </ Show >
987- < Show when = { ! isAiReviewing ( ) && ! aiReviewResult ( ) } >
1000+ < Show when = { ! isAiReviewing ( ) && ! aiReviewContent ( ) } >
9881001 < div class = "text-xs" style = { { color : "var(--text-muted)" } } >
989- Stage files and click AI scan to review
1002+ Click the AI button to analyze staged changes.
9901003 </ div >
9911004 </ Show >
9921005 </ div >
0 commit comments