@@ -19,6 +19,22 @@ enum ShortcutStatus {
1919
2020const MOVE_STEP = 200
2121const shortcuts : Record < string , Shortcut > = { }
22+
23+ type AbortReason = 'user' | 'new-request'
24+
25+ interface StreamContext {
26+ controller : AbortController
27+ reason : AbortReason | null
28+ }
29+
30+ let currentStreamContext : StreamContext | null = null
31+
32+ function abortCurrentStream ( reason : AbortReason ) {
33+ if ( ! currentStreamContext ) return
34+ currentStreamContext . reason = reason
35+ currentStreamContext . controller . abort ( )
36+ }
37+
2238const callbacks : Record < string , ( ) => void > = {
2339 hideOrShowMainWindow : async ( ) => {
2440 const mainWindow = global . mainWindow
@@ -33,16 +49,74 @@ const callbacks: Record<string, () => void> = {
3349 takeScreenshot : async ( ) => {
3450 const mainWindow = global . mainWindow
3551 if ( ! mainWindow || mainWindow . isDestroyed ( ) || ! state . inCoderPage || ! settings . apiKey ) return
52+
53+ abortCurrentStream ( 'new-request' )
3654 const screenshotData = await takeScreenshot ( )
3755 if ( screenshotData && mainWindow && ! mainWindow . isDestroyed ( ) ) {
56+ const streamContext : StreamContext = {
57+ controller : new AbortController ( ) ,
58+ reason : null
59+ }
60+ currentStreamContext = streamContext
3861 mainWindow . webContents . send ( 'screenshot-taken' , screenshotData )
39- const solutionStream = getSolutionStream ( screenshotData )
40- for await ( const chunk of solutionStream ) {
41- mainWindow . webContents . send ( 'solution-chunk' , chunk )
62+ let endedNaturally = true
63+ let streamStarted = false
64+ try {
65+ const solutionStream = getSolutionStream ( screenshotData , streamContext . controller . signal )
66+ streamStarted = true
67+ try {
68+ for await ( const chunk of solutionStream ) {
69+ if ( streamContext . controller . signal . aborted ) {
70+ endedNaturally = false
71+ break
72+ }
73+ mainWindow . webContents . send ( 'solution-chunk' , chunk )
74+ }
75+ } catch ( error ) {
76+ if ( ! streamContext . controller . signal . aborted ) {
77+ endedNaturally = false
78+ console . error ( 'Error streaming solution:' , error )
79+ const errorMessage = error instanceof Error ? error . message : 'Unknown error'
80+ mainWindow . webContents . send ( 'solution-error' , errorMessage )
81+ } else {
82+ endedNaturally = false
83+ }
84+ }
85+
86+ if ( streamContext . controller . signal . aborted ) {
87+ if ( streamContext . reason === 'user' ) {
88+ mainWindow . webContents . send ( 'solution-stopped' )
89+ }
90+ } else if ( endedNaturally ) {
91+ mainWindow . webContents . send ( 'solution-complete' )
92+ }
93+ } catch ( error ) {
94+ if ( streamContext . controller . signal . aborted ) {
95+ if ( streamContext . reason === 'user' ) {
96+ mainWindow . webContents . send ( 'solution-stopped' )
97+ }
98+ } else {
99+ endedNaturally = false
100+ const errorMessage = error instanceof Error ? error . message : 'Unknown error'
101+ console . error ( 'Error streaming solution:' , error )
102+ mainWindow . webContents . send ( 'solution-error' , errorMessage )
103+ }
104+ } finally {
105+ if ( currentStreamContext === streamContext ) {
106+ currentStreamContext = null
107+ }
108+ if ( ! streamStarted && streamContext . reason === 'user' ) {
109+ mainWindow . webContents . send ( 'solution-stopped' )
110+ }
42111 }
43112 }
44113 } ,
45114
115+ // Stop current AI solution stream
116+ stopSolutionStream : ( ) => {
117+ abortCurrentStream ( 'user' )
118+ } ,
119+
46120 ignoreOrEnableMouse : ( ) => {
47121 const mainWindow = global . mainWindow
48122 if ( ! mainWindow || mainWindow . isDestroyed ( ) || ! state . inCoderPage ) return
@@ -122,3 +196,9 @@ ipcMain.handle('updateShortcuts', (_event, _shortcuts: { action: string; key: st
122196 }
123197 } )
124198} )
199+
200+ ipcMain . handle ( 'stopSolutionStream' , ( ) => {
201+ if ( ! currentStreamContext ) return false
202+ abortCurrentStream ( 'user' )
203+ return true
204+ } )
0 commit comments