11"use client" ;
22
3- import { useState , useCallback , useRef } from "react" ;
3+ import { useState , useCallback , useRef , useEffect } from "react" ;
44
55export interface ChatMessage {
66 role : "user" | "assistant" ;
@@ -19,19 +19,39 @@ interface UseChatOptions {
1919 onFileChange ?: ( fileName : string ) => void ;
2020}
2121
22+ function log ( prefix : string , ...args : unknown [ ] ) {
23+ console . log ( `[useChat:${ prefix } ]` , ...args ) ;
24+ }
25+
2226export function useChat ( { projectId, onTitle, onFileChange } : UseChatOptions ) {
2327 const [ messages , setMessages ] = useState < ChatMessage [ ] > ( [ ] ) ;
2428 const [ isLoading , setIsLoading ] = useState ( false ) ;
2529 const [ activity , setActivity ] = useState < string > ( "" ) ;
2630 const abortRef = useRef < AbortController | null > ( null ) ;
31+ const projectIdRef = useRef ( projectId ) ;
32+
33+ // Reset state when projectId changes to prevent showing stale data
34+ useEffect ( ( ) => {
35+ if ( projectIdRef . current !== projectId ) {
36+ log ( "reset" , `project changed: ${ projectIdRef . current } -> ${ projectId } ` ) ;
37+ projectIdRef . current = projectId ;
38+ abortRef . current ?. abort ( ) ;
39+ abortRef . current = null ;
40+ setMessages ( [ ] ) ;
41+ setIsLoading ( false ) ;
42+ setActivity ( "" ) ;
43+ }
44+ } , [ projectId ] ) ;
2745
2846 const processSSEStream = useCallback (
29- async ( reader : ReadableStreamDefaultReader < Uint8Array > ) => {
47+ async ( reader : ReadableStreamDefaultReader < Uint8Array > , forProjectId : string ) => {
3048 const decoder = new TextDecoder ( ) ;
3149 let buffer = "" ;
3250 let assistantText = "" ;
3351 let gotTitle = false ;
3452
53+ log ( "stream" , `starting SSE processing for project=${ forProjectId } ` ) ;
54+
3555 // Add placeholder assistant message
3656 setMessages ( ( prev ) => [
3757 ...prev ,
@@ -40,7 +60,17 @@ export function useChat({ projectId, onTitle, onFileChange }: UseChatOptions) {
4060
4161 while ( true ) {
4262 const { done, value } = await reader . read ( ) ;
43- if ( done ) break ;
63+ if ( done ) {
64+ log ( "stream" , "reader done" ) ;
65+ break ;
66+ }
67+
68+ // Guard against stale project
69+ if ( projectIdRef . current !== forProjectId ) {
70+ log ( "stream" , `stale stream for ${ forProjectId } , current is ${ projectIdRef . current } — dropping` ) ;
71+ reader . cancel ( ) ;
72+ return ;
73+ }
4474
4575 buffer += decoder . decode ( value as BufferSource , { stream : true } ) ;
4676 const lines = buffer . split ( "\n" ) ;
@@ -50,6 +80,7 @@ export function useChat({ projectId, onTitle, onFileChange }: UseChatOptions) {
5080 if ( line . startsWith ( "data: " ) ) {
5181 try {
5282 const event = JSON . parse ( line . slice ( 6 ) ) ;
83+ log ( "event" , event . type , event . type === "text" ? `(${ ( event . content as string ) . length } chars)` : event . content || "" ) ;
5384
5485 switch ( event . type ) {
5586 case "text" : {
@@ -77,13 +108,16 @@ export function useChat({ projectId, onTitle, onFileChange }: UseChatOptions) {
77108 onTitle ?.( event . content ) ;
78109 break ;
79110 case "activity" :
111+ log ( "activity" , event . content ) ;
80112 setActivity ( event . content ) ;
81113 break ;
82114 case "file-change" :
115+ log ( "file-change" , event . content , event . detail ) ;
83116 onFileChange ?.( event . content ) ;
84117 setActivity ( `Updated ${ event . content } ` ) ;
85118 break ;
86119 case "error" :
120+ log ( "error" , event . content ) ;
87121 assistantText += `\n\nError: ${ event . content } ` ;
88122 setMessages ( ( prev ) => {
89123 const updated = [ ...prev ] ;
@@ -95,10 +129,11 @@ export function useChat({ projectId, onTitle, onFileChange }: UseChatOptions) {
95129 } ) ;
96130 break ;
97131 case "done" :
132+ log ( "done" , `total text length: ${ assistantText . length } ` ) ;
98133 break ;
99134 }
100- } catch {
101- // ignore parse errors
135+ } catch ( e ) {
136+ log ( "parse-error" , line . slice ( 0 , 200 ) , e ) ;
102137 }
103138 }
104139 }
@@ -108,51 +143,72 @@ export function useChat({ projectId, onTitle, onFileChange }: UseChatOptions) {
108143 ) ;
109144
110145 const loadHistory = useCallback ( async ( ) => {
146+ const loadForId = projectId ;
147+ log ( "loadHistory" , `starting for project=${ loadForId } ` ) ;
148+
111149 try {
112150 // First check for active session to reconnect to
113- const reconnectRes = await fetch ( `/api/projects/${ projectId } /chat?reconnect=1` ) ;
151+ const reconnectRes = await fetch ( `/api/projects/${ loadForId } /chat?reconnect=1` ) ;
152+
153+ if ( projectIdRef . current !== loadForId ) {
154+ log ( "loadHistory" , `stale after reconnect check — aborting` ) ;
155+ return ;
156+ }
114157
115158 if ( reconnectRes . headers . get ( "Content-Type" ) ?. includes ( "text/event-stream" ) ) {
159+ log ( "loadHistory" , "active session found, reconnecting..." ) ;
116160 // Active session found — load saved history first, then stream remaining
117- const historyRes = await fetch ( `/api/projects/${ projectId } /chat` ) ;
118- if ( historyRes . ok ) {
161+ const historyRes = await fetch ( `/api/projects/${ loadForId } /chat` ) ;
162+ if ( historyRes . ok && projectIdRef . current === loadForId ) {
119163 const data = await historyRes . json ( ) ;
164+ log ( "loadHistory" , `loaded ${ data . length } history messages before reconnect` ) ;
120165 setMessages ( data ) ;
121166 }
122167
123168 setIsLoading ( true ) ;
124169 setActivity ( "Reconnecting..." ) ;
125170
126171 const reader = reconnectRes . body ! . getReader ( ) ;
127- await processSSEStream ( reader ) ;
172+ await processSSEStream ( reader , loadForId ) ;
173+
174+ if ( projectIdRef . current !== loadForId ) return ;
128175
129176 setIsLoading ( false ) ;
130177 setActivity ( "" ) ;
131178
132179 // Reload history to get the final saved state
133- const finalRes = await fetch ( `/api/projects/${ projectId } /chat` ) ;
134- if ( finalRes . ok ) {
180+ const finalRes = await fetch ( `/api/projects/${ loadForId } /chat` ) ;
181+ if ( finalRes . ok && projectIdRef . current === loadForId ) {
135182 const data = await finalRes . json ( ) ;
183+ log ( "loadHistory" , `reloaded ${ data . length } messages after reconnect` ) ;
136184 setMessages ( data ) ;
137185 }
138186 return ;
139187 }
140188
141189 // No active session — just load history normally
142- const res = await fetch ( `/api/projects/${ projectId } /chat` ) ;
143- if ( res . ok ) {
190+ const res = await fetch ( `/api/projects/${ loadForId } /chat` ) ;
191+ if ( res . ok && projectIdRef . current === loadForId ) {
144192 const data = await res . json ( ) ;
193+ log ( "loadHistory" , `loaded ${ data . length } history messages` ) ;
145194 setMessages ( data ) ;
195+ } else if ( projectIdRef . current !== loadForId ) {
196+ log ( "loadHistory" , `stale after history fetch — dropping` ) ;
146197 }
147- } catch {
148- // ignore
198+ } catch ( e ) {
199+ log ( "loadHistory" , "error:" , e ) ;
200+ setIsLoading ( false ) ;
201+ setActivity ( "" ) ;
149202 }
150203 } , [ projectId , processSSEStream ] ) ;
151204
152205 const sendMessage = useCallback (
153206 async ( content : string ) => {
154207 if ( ! content . trim ( ) || isLoading ) return ;
155208
209+ const sendForId = projectId ;
210+ log ( "send" , `message to project=${ sendForId } : "${ content . slice ( 0 , 50 ) } ..."` ) ;
211+
156212 const userMsg : ChatMessage = {
157213 role : "user" ,
158214 content,
@@ -161,11 +217,11 @@ export function useChat({ projectId, onTitle, onFileChange }: UseChatOptions) {
161217
162218 setMessages ( ( prev ) => [ ...prev , userMsg ] ) ;
163219 setIsLoading ( true ) ;
164- setActivity ( "" ) ;
220+ setActivity ( "Thinking... " ) ;
165221
166222 try {
167223 abortRef . current = new AbortController ( ) ;
168- const res = await fetch ( `/api/projects/${ projectId } /chat` , {
224+ const res = await fetch ( `/api/projects/${ sendForId } /chat` , {
169225 method : "POST" ,
170226 headers : { "Content-Type" : "application/json" } ,
171227 body : JSON . stringify ( { message : content } ) ,
@@ -176,10 +232,12 @@ export function useChat({ projectId, onTitle, onFileChange }: UseChatOptions) {
176232 throw new Error ( `HTTP ${ res . status } ` ) ;
177233 }
178234
235+ log ( "send" , `POST response ok, reading stream...` ) ;
179236 const reader = res . body ! . getReader ( ) ;
180- await processSSEStream ( reader ) ;
237+ await processSSEStream ( reader , sendForId ) ;
181238 } catch ( err ) {
182239 if ( ( err as Error ) . name !== "AbortError" ) {
240+ log ( "send" , "error:" , err ) ;
183241 setMessages ( ( prev ) => {
184242 const updated = [ ...prev ] ;
185243 const last = updated [ updated . length - 1 ] ;
@@ -202,6 +260,7 @@ export function useChat({ projectId, onTitle, onFileChange }: UseChatOptions) {
202260 ) ;
203261
204262 const stop = useCallback ( ( ) => {
263+ log ( "stop" , "aborting" ) ;
205264 abortRef . current ?. abort ( ) ;
206265 } , [ ] ) ;
207266
0 commit comments