@@ -132,14 +132,137 @@ export function ChatPanel({
132132 messagesEndRef . current ?. scrollIntoView ( { behavior : "smooth" } ) ;
133133 } , [ visibleMessages ] ) ;
134134
135- // When active session changes, swap visible transcript
135+ // Track which sessions we've already loaded history for
136+ const loadedHistoryRef = useRef < Set < string > > ( new Set ( ) ) ;
137+
138+ // Fetch and process session history when switching sessions
139+ const fetchSessionHistory = useCallback ( async ( sessionId : string ) => {
140+ // Skip if already loaded
141+ if ( loadedHistoryRef . current . has ( sessionId ) ) return ;
142+ if ( messagesBySession [ sessionId ] ?. length ) {
143+ // Already have messages from SSE
144+ loadedHistoryRef . current . add ( sessionId ) ;
145+ return ;
146+ }
147+
148+ try {
149+ const res = await fetch ( `/api/sessions/${ sessionId } /history` , { cache : "no-store" } ) ;
150+ const data = await res . json ( ) ;
151+ const history = Array . isArray ( data ?. history ) ? data . history as AcpSessionNotification [ ] : [ ] ;
152+
153+ if ( history . length === 0 ) {
154+ loadedHistoryRef . current . add ( sessionId ) ;
155+ return ;
156+ }
157+
158+ // Process history into messages
159+ const messages : ChatMessage [ ] = [ ] ;
160+ let streamingMsgId : string | null = null ;
161+ let streamingThoughtId : string | null = null ;
162+ let lastKind : string | null = null ;
163+
164+ for ( const notification of history ) {
165+ const update = ( notification . update ?? notification ) as Record < string , unknown > ;
166+ const kind = update . sessionUpdate as string | undefined ;
167+ if ( ! kind ) continue ;
168+
169+ const extractText = ( ) : string => {
170+ const content = update . content as { type : string ; text ?: string } | undefined ;
171+ if ( content ?. text ) return content . text ;
172+ if ( typeof update . text === "string" ) return update . text ;
173+ return "" ;
174+ } ;
175+
176+ switch ( kind ) {
177+ case "agent_message_chunk" : {
178+ const text = extractText ( ) ;
179+ if ( ! text ) break ;
180+ streamingThoughtId = null ;
181+ const shouldCreateNew = lastKind !== "agent_message_chunk" ;
182+ if ( shouldCreateNew ) streamingMsgId = null ;
183+ if ( ! streamingMsgId ) {
184+ streamingMsgId = uuidv4 ( ) ;
185+ messages . push ( { id : streamingMsgId , role : "assistant" , content : text , timestamp : new Date ( ) } ) ;
186+ } else {
187+ const idx = messages . findIndex ( ( m ) => m . id === streamingMsgId ) ;
188+ if ( idx >= 0 ) messages [ idx ] = { ...messages [ idx ] , content : messages [ idx ] . content + text } ;
189+ }
190+ break ;
191+ }
192+ case "agent_thought_chunk" : {
193+ const text = extractText ( ) ;
194+ if ( ! text ) break ;
195+ const shouldCreateNewThought = lastKind !== "agent_thought_chunk" ;
196+ if ( shouldCreateNewThought ) streamingThoughtId = null ;
197+ if ( ! streamingThoughtId ) {
198+ streamingThoughtId = uuidv4 ( ) ;
199+ messages . push ( { id : streamingThoughtId , role : "thought" , content : text , timestamp : new Date ( ) } ) ;
200+ } else {
201+ const idx = messages . findIndex ( ( m ) => m . id === streamingThoughtId ) ;
202+ if ( idx >= 0 ) messages [ idx ] = { ...messages [ idx ] , content : messages [ idx ] . content + text } ;
203+ }
204+ break ;
205+ }
206+ case "user_message" : {
207+ const text = extractText ( ) ;
208+ if ( text ) messages . push ( { id : uuidv4 ( ) , role : "user" , content : text , timestamp : new Date ( ) } ) ;
209+ streamingMsgId = null ;
210+ streamingThoughtId = null ;
211+ break ;
212+ }
213+ case "tool_call" : {
214+ const title = ( update . title as string ) ?? "tool" ;
215+ const status = ( update . status as string ) ?? "completed" ;
216+ const toolKind = update . kind as string | undefined ;
217+ const rawInput = ( typeof update . rawInput === "object" && update . rawInput !== null )
218+ ? update . rawInput as Record < string , unknown >
219+ : undefined ;
220+ const contentParts : string [ ] = [ ] ;
221+ if ( update . rawInput ) {
222+ contentParts . push ( `Input:\n${ typeof update . rawInput === "string" ? update . rawInput : JSON . stringify ( update . rawInput , null , 2 ) } ` ) ;
223+ }
224+ const toolContent = update . content as Array < { type : string ; text ?: string } > | undefined ;
225+ if ( Array . isArray ( toolContent ) ) {
226+ for ( const c of toolContent ) if ( c . text ) contentParts . push ( c . text ) ;
227+ }
228+ messages . push ( {
229+ id : uuidv4 ( ) ,
230+ role : "tool" ,
231+ content : contentParts . join ( "\n\n" ) || title ,
232+ toolName : title ,
233+ toolStatus : status ,
234+ toolKind,
235+ toolRawInput : rawInput ,
236+ timestamp : new Date ( ) ,
237+ } ) ;
238+ break ;
239+ }
240+ }
241+ lastKind = kind ;
242+ }
243+
244+ loadedHistoryRef . current . add ( sessionId ) ;
245+ if ( messages . length > 0 ) {
246+ setMessagesBySession ( ( prev ) => ( {
247+ ...prev ,
248+ [ sessionId ] : messages ,
249+ } ) ) ;
250+ }
251+ } catch {
252+ // ignore errors
253+ }
254+ } , [ messagesBySession ] ) ;
255+
256+ // When active session changes, swap visible transcript and load history
136257 useEffect ( ( ) => {
137258 if ( ! activeSessionId ) {
138259 setVisibleMessages ( [ ] ) ;
139260 return ;
140261 }
262+ // Load history if not yet loaded
263+ fetchSessionHistory ( activeSessionId ) ;
141264 setVisibleMessages ( messagesBySession [ activeSessionId ] ?? [ ] ) ;
142- } , [ activeSessionId , messagesBySession ] ) ;
265+ } , [ activeSessionId , messagesBySession , fetchSessionHistory ] ) ;
143266
144267 const fetchSessions = useCallback ( async ( ) => {
145268 try {
@@ -734,6 +857,7 @@ export function ChatPanel({
734857 < div className = "flex gap-2 items-end" >
735858 < TiptapInput
736859 onSend = { handleSend }
860+ onStop = { acp . cancel }
737861 placeholder = {
738862 connected
739863 ? activeSessionId
0 commit comments