11import { useCallback , useEffect , useRef , useState } from 'react'
2+ import { createLogger } from '@sim/logger'
23import { useQueryClient } from '@tanstack/react-query'
34import { usePathname , useRouter } from 'next/navigation'
45import { MOTHERSHIP_CHAT_API_PATH } from '@/lib/copilot/constants'
6+ import { tableKeys } from '@/hooks/queries/tables'
57import {
68 type TaskChatHistory ,
79 type TaskStoredContentBlock ,
810 type TaskStoredMessage ,
911 taskKeys ,
1012 useChatHistory ,
1113} from '@/hooks/queries/tasks'
14+ import { workspaceFilesKeys } from '@/hooks/queries/workspace-files'
1215import type {
1316 ChatMessage ,
1417 ContentBlock ,
1518 ContentBlockType ,
19+ MothershipResource ,
1620 SSEPayload ,
1721 SSEPayloadData ,
1822 ToolCallStatus ,
@@ -26,6 +30,9 @@ export interface UseChatReturn {
2630 sendMessage : ( message : string ) => Promise < void >
2731 stopGeneration : ( ) => void
2832 chatBottomRef : React . RefObject < HTMLDivElement | null >
33+ resources : MothershipResource [ ]
34+ activeResourceId : string | null
35+ setActiveResourceId : ( id : string | null ) => void
2936}
3037
3138const STATE_TO_STATUS : Record < string , ToolCallStatus > = {
@@ -65,10 +72,66 @@ function mapStoredMessage(msg: TaskStoredMessage): ChatMessage {
6572 return mapped
6673}
6774
75+ const logger = createLogger ( 'useChat' )
76+
6877function getPayloadData ( payload : SSEPayload ) : SSEPayloadData | undefined {
6978 return typeof payload . data === 'object' ? payload . data : undefined
7079}
7180
81+ const RESOURCE_TOOL_NAMES = new Set ( [ 'user_table' , 'workspace_file' ] )
82+
83+ function getResultData ( parsed : SSEPayload ) : Record < string , unknown > | undefined {
84+ const topResult = parsed . result as Record < string , unknown > | undefined
85+ const nestedResult =
86+ typeof parsed . data === 'object' ? ( parsed . data ?. result as Record < string , unknown > ) : undefined
87+ const result = topResult ?? nestedResult
88+ return result ?. data as Record < string , unknown > | undefined
89+ }
90+
91+ function extractTableResource (
92+ parsed : SSEPayload ,
93+ storedArgs : Record < string , unknown > | undefined ,
94+ fallbackTableId : string | null
95+ ) : MothershipResource | null {
96+ const data = getResultData ( parsed )
97+ const storedInnerArgs = storedArgs ?. args as Record < string , unknown > | undefined
98+
99+ const table = data ?. table as Record < string , unknown > | undefined
100+ if ( table ?. id ) {
101+ return { type : 'table' , id : table . id as string , title : ( table . name as string ) || 'Table' }
102+ }
103+
104+ const tableId =
105+ ( data ?. tableId as string ) ?? storedInnerArgs ?. tableId ?? storedArgs ?. tableId ?? fallbackTableId
106+ const tableName = ( data ?. tableName as string ) || ( table ?. name as string ) || 'Table'
107+ if ( tableId ) return { type : 'table' , id : tableId as string , title : tableName }
108+
109+ return null
110+ }
111+
112+ function extractFileResource (
113+ parsed : SSEPayload ,
114+ storedArgs : Record < string , unknown > | undefined
115+ ) : MothershipResource | null {
116+ const data = getResultData ( parsed )
117+ const storedInnerArgs = storedArgs ?. args as Record < string , unknown > | undefined
118+
119+ const file = data ?. file as Record < string , unknown > | undefined
120+ if ( file ?. id ) {
121+ return { type : 'file' , id : file . id as string , title : ( file . name as string ) || 'File' }
122+ }
123+
124+ const fileId = ( data ?. fileId as string ) ?? ( data ?. id as string )
125+ const fileName =
126+ ( data ?. fileName as string ) ||
127+ ( data ?. name as string ) ||
128+ ( storedInnerArgs ?. fileName as string ) ||
129+ 'File'
130+ if ( fileId && typeof fileId === 'string' ) return { type : 'file' , id : fileId , title : fileName }
131+
132+ return null
133+ }
134+
72135export function useChat ( workspaceId : string , initialChatId ?: string ) : UseChatReturn {
73136 const pathname = usePathname ( )
74137 const queryClient = useQueryClient ( )
@@ -77,13 +140,16 @@ export function useChat(workspaceId: string, initialChatId?: string): UseChatRet
77140 const [ messages , setMessages ] = useState < ChatMessage [ ] > ( [ ] )
78141 const [ isSending , setIsSending ] = useState ( false )
79142 const [ error , setError ] = useState < string | null > ( null )
143+ const [ resources , setResources ] = useState < MothershipResource [ ] > ( [ ] )
144+ const [ activeResourceId , setActiveResourceId ] = useState < string | null > ( null )
80145 const abortControllerRef = useRef < AbortController | null > ( null )
81146 const chatIdRef = useRef < string | undefined > ( initialChatId )
82147 const chatBottomRef = useRef < HTMLDivElement > ( null )
83148 const appliedChatIdRef = useRef < string | undefined > ( undefined )
84149 const pendingUserMsgRef = useRef < { id : string ; content : string } | null > ( null )
85150 const streamIdRef = useRef < string | undefined > ( undefined )
86151 const sendingRef = useRef ( false )
152+ const toolArgsMapRef = useRef < Map < string , Record < string , unknown > > > ( new Map ( ) )
87153
88154 useEffect ( ( ) => {
89155 routerRef . current = router
@@ -93,12 +159,30 @@ export function useChat(workspaceId: string, initialChatId?: string): UseChatRet
93159
94160 const { data : chatHistory } = useChatHistory ( initialChatId )
95161
162+ const addResource = useCallback ( ( resource : MothershipResource ) => {
163+ setResources ( ( prev ) => {
164+ const existing = prev . find ( ( r ) => r . type === resource . type && r . id === resource . id )
165+ if ( existing ) {
166+ const keepOldTitle = existing . title !== 'Table' && existing . title !== 'File'
167+ const title = keepOldTitle ? existing . title : resource . title
168+ if ( title === existing . title ) return prev
169+ return prev . map ( ( r ) =>
170+ r . id === existing . id && r . type === existing . type ? { ...r , title } : r
171+ )
172+ }
173+ return [ ...prev , resource ]
174+ } )
175+ setActiveResourceId ( resource . id )
176+ } , [ ] )
177+
96178 useEffect ( ( ) => {
97179 chatIdRef . current = initialChatId
98180 appliedChatIdRef . current = undefined
99181 setMessages ( [ ] )
100182 setError ( null )
101183 setIsSending ( false )
184+ setResources ( [ ] )
185+ setActiveResourceId ( null )
102186 } , [ initialChatId ] )
103187
104188 useEffect ( ( ) => {
@@ -110,6 +194,8 @@ export function useChat(workspaceId: string, initialChatId?: string): UseChatRet
110194 setMessages ( [ ] )
111195 setError ( null )
112196 setIsSending ( false )
197+ setResources ( [ ] )
198+ setActiveResourceId ( null )
113199 } , [ isHomePage ] )
114200
115201 useEffect ( ( ) => {
@@ -124,6 +210,7 @@ export function useChat(workspaceId: string, initialChatId?: string): UseChatRet
124210 let buffer = ''
125211 const blocks : ContentBlock [ ] = [ ]
126212 const toolMap = new Map < string , number > ( )
213+ let lastTableId : string | null = null
127214
128215 const ensureTextBlock = ( ) : ContentBlock => {
129216 const last = blocks [ blocks . length - 1 ]
@@ -164,6 +251,8 @@ export function useChat(workspaceId: string, initialChatId?: string): UseChatRet
164251 continue
165252 }
166253
254+ logger . debug ( 'SSE event received' , parsed )
255+
167256 switch ( parsed . type ) {
168257 case 'chat_id' : {
169258 if ( parsed . chatId ) {
@@ -201,6 +290,14 @@ export function useChat(workspaceId: string, initialChatId?: string): UseChatRet
201290 const data = getPayloadData ( parsed )
202291 const name = parsed . toolName || data ?. name || 'unknown'
203292 if ( ! id ) break
293+
294+ if ( RESOURCE_TOOL_NAMES . has ( name ) ) {
295+ const args = data ?. arguments ?? data ?. input
296+ if ( args ) {
297+ toolArgsMapRef . current . set ( id , args )
298+ }
299+ }
300+
204301 const ui = parsed . ui || data ?. ui
205302 if ( ui ?. hidden ) break
206303 const displayTitle = ui ?. title || ui ?. phaseLabel
@@ -229,6 +326,33 @@ export function useChat(workspaceId: string, initialChatId?: string): UseChatRet
229326 blocks [ idx ] . toolCall ! . status = parsed . success ? 'success' : 'error'
230327 flush ( )
231328 }
329+
330+ const toolName = parsed . toolName || getPayloadData ( parsed ) ?. name
331+ if ( toolName && parsed . success && RESOURCE_TOOL_NAMES . has ( toolName ) ) {
332+ const storedArgs = toolArgsMapRef . current . get ( id )
333+ let resource : MothershipResource | null = null
334+
335+ if ( toolName === 'user_table' ) {
336+ resource = extractTableResource ( parsed , storedArgs , lastTableId )
337+ if ( resource ) {
338+ lastTableId = resource . id
339+ queryClient . invalidateQueries ( { queryKey : tableKeys . detail ( resource . id ) } )
340+ queryClient . invalidateQueries ( { queryKey : tableKeys . rowsRoot ( resource . id ) } )
341+ }
342+ } else if ( toolName === 'workspace_file' ) {
343+ resource = extractFileResource ( parsed , storedArgs )
344+ if ( resource ) {
345+ queryClient . invalidateQueries ( {
346+ queryKey : workspaceFilesKeys . list ( workspaceId ) ,
347+ } )
348+ queryClient . invalidateQueries ( {
349+ queryKey : workspaceFilesKeys . content ( workspaceId , resource . id ) ,
350+ } )
351+ }
352+ }
353+
354+ if ( resource ) addResource ( resource )
355+ }
232356 break
233357 }
234358 case 'tool_error' : {
@@ -265,7 +389,7 @@ export function useChat(workspaceId: string, initialChatId?: string): UseChatRet
265389 }
266390 }
267391 } ,
268- [ workspaceId , queryClient ]
392+ [ workspaceId , queryClient , addResource ]
269393 )
270394
271395 const finalize = useCallback ( ( ) => {
@@ -401,5 +525,8 @@ export function useChat(workspaceId: string, initialChatId?: string): UseChatRet
401525 sendMessage,
402526 stopGeneration,
403527 chatBottomRef,
528+ resources,
529+ activeResourceId,
530+ setActiveResourceId,
404531 }
405532}
0 commit comments