Skip to content

Commit c867801

Browse files
TheodoreSpeaksTheodore Li
andauthored
fix(ui) Live update resources in resource main view (#3617)
* Live update resources in resource main view * Stop updating on read tool calls --------- Co-authored-by: Theodore Li <theo@sim.ai>
1 parent c090c82 commit c867801

File tree

4 files changed

+284
-239
lines changed

4 files changed

+284
-239
lines changed

apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry/resource-registry.tsx

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -123,19 +123,20 @@ const RESOURCE_INVALIDATORS: Record<
123123
MothershipResourceType,
124124
(qc: QueryClient, workspaceId: string, resourceId: string) => void
125125
> = {
126-
table: (qc, wId, id) => {
127-
qc.invalidateQueries({ queryKey: tableKeys.list(wId) })
126+
table: (qc, _wId, id) => {
127+
qc.invalidateQueries({ queryKey: tableKeys.lists() })
128128
qc.invalidateQueries({ queryKey: tableKeys.detail(id) })
129129
},
130130
file: (qc, wId, id) => {
131-
qc.invalidateQueries({ queryKey: workspaceFilesKeys.list(wId) })
131+
qc.invalidateQueries({ queryKey: workspaceFilesKeys.lists() })
132132
qc.invalidateQueries({ queryKey: workspaceFilesKeys.content(wId, id) })
133+
qc.invalidateQueries({ queryKey: workspaceFilesKeys.storageInfo() })
133134
},
134-
workflow: (qc, wId) => {
135-
qc.invalidateQueries({ queryKey: workflowKeys.list(wId) })
135+
workflow: (qc, _wId) => {
136+
qc.invalidateQueries({ queryKey: workflowKeys.lists() })
136137
},
137-
knowledgebase: (qc, wId, id) => {
138-
qc.invalidateQueries({ queryKey: knowledgeKeys.list(wId) })
138+
knowledgebase: (qc, _wId, id) => {
139+
qc.invalidateQueries({ queryKey: knowledgeKeys.lists() })
139140
qc.invalidateQueries({ queryKey: knowledgeKeys.detail(id) })
140141
},
141142
}

apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ import {
88
reportManualRunToolStop,
99
} from '@/lib/copilot/client-sse/run-tool-execution'
1010
import { MOTHERSHIP_CHAT_API_PATH } from '@/lib/copilot/constants'
11+
import {
12+
extractResourcesFromToolResult,
13+
isResourceToolName,
14+
} from '@/lib/copilot/resource-extraction'
1115
import { VFS_DIR_TO_RESOURCE } from '@/lib/copilot/resource-types'
1216
import { isWorkflowToolName } from '@/lib/copilot/workflow-tools'
1317
import { getNextWorkflowColor } from '@/lib/workflows/colors'
@@ -621,7 +625,7 @@ export function useChat(
621625
calledBy: activeSubagent,
622626
},
623627
})
624-
if (name === 'read') {
628+
if (name === 'read' || isResourceToolName(name)) {
625629
const args = (data?.arguments ?? data?.input) as
626630
| Record<string, unknown>
627631
| undefined
@@ -720,6 +724,17 @@ export function useChat(
720724
})
721725
}
722726
}
727+
728+
if (tc.status === 'success' && isResourceToolName(tc.name)) {
729+
const resources = extractResourcesFromToolResult(
730+
tc.name,
731+
toolArgsMap.get(id) as Record<string, unknown> | undefined,
732+
tc.result?.output
733+
)
734+
for (const resource of resources) {
735+
invalidateResourceQueries(queryClient, workspaceId, resource.type, resource.id)
736+
}
737+
}
723738
}
724739

725740
break
Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
import type { MothershipResource, MothershipResourceType } from '@/lib/copilot/resource-types'
2+
3+
type ChatResource = MothershipResource
4+
type ResourceType = MothershipResourceType
5+
6+
const RESOURCE_TOOL_NAMES = new Set([
7+
'user_table',
8+
'workspace_file',
9+
'create_workflow',
10+
'edit_workflow',
11+
'function_execute',
12+
'knowledge_base',
13+
'knowledge',
14+
])
15+
16+
export function isResourceToolName(toolName: string): boolean {
17+
return RESOURCE_TOOL_NAMES.has(toolName)
18+
}
19+
20+
function asRecord(value: unknown): Record<string, unknown> {
21+
return value && typeof value === 'object' ? (value as Record<string, unknown>) : {}
22+
}
23+
24+
function getOperation(params: Record<string, unknown> | undefined): string | undefined {
25+
const args = asRecord(params?.args)
26+
return (args.operation ?? params?.operation) as string | undefined
27+
}
28+
29+
const READ_ONLY_TABLE_OPS = new Set(['get', 'get_schema', 'get_row', 'query_rows'])
30+
const READ_ONLY_KB_OPS = new Set(['get', 'query', 'list_tags', 'get_tag_usage'])
31+
const READ_ONLY_KNOWLEDGE_ACTIONS = new Set(['listed', 'queried'])
32+
33+
/**
34+
* Extracts resource descriptors from a tool execution result.
35+
* Returns one or more resources for tools that create/modify workspace entities.
36+
* Read-only operations are excluded to avoid unnecessary cache invalidation.
37+
*/
38+
export function extractResourcesFromToolResult(
39+
toolName: string,
40+
params: Record<string, unknown> | undefined,
41+
output: unknown
42+
): ChatResource[] {
43+
if (!isResourceToolName(toolName)) return []
44+
45+
const result = asRecord(output)
46+
const data = asRecord(result.data)
47+
48+
switch (toolName) {
49+
case 'user_table': {
50+
if (READ_ONLY_TABLE_OPS.has(getOperation(params) ?? '')) return []
51+
52+
if (result.tableId) {
53+
return [
54+
{
55+
type: 'table',
56+
id: result.tableId as string,
57+
title: (result.tableName as string) || 'Table',
58+
},
59+
]
60+
}
61+
if (result.fileId) {
62+
return [
63+
{
64+
type: 'file',
65+
id: result.fileId as string,
66+
title: (result.fileName as string) || 'File',
67+
},
68+
]
69+
}
70+
const table = asRecord(data.table)
71+
if (table.id) {
72+
return [{ type: 'table', id: table.id as string, title: (table.name as string) || 'Table' }]
73+
}
74+
const args = asRecord(params?.args)
75+
const tableId =
76+
(data.tableId as string) ?? (args.tableId as string) ?? (params?.tableId as string)
77+
if (tableId) {
78+
return [
79+
{ type: 'table', id: tableId as string, title: (data.tableName as string) || 'Table' },
80+
]
81+
}
82+
return []
83+
}
84+
85+
case 'workspace_file': {
86+
const file = asRecord(data.file)
87+
if (file.id) {
88+
return [{ type: 'file', id: file.id as string, title: (file.name as string) || 'File' }]
89+
}
90+
const fileId = (data.fileId as string) ?? (data.id as string)
91+
if (fileId) {
92+
const fileName = (data.fileName as string) || (data.name as string) || 'File'
93+
return [{ type: 'file', id: fileId, title: fileName }]
94+
}
95+
return []
96+
}
97+
98+
case 'function_execute': {
99+
if (result.tableId) {
100+
return [
101+
{
102+
type: 'table',
103+
id: result.tableId as string,
104+
title: (result.tableName as string) || 'Table',
105+
},
106+
]
107+
}
108+
if (result.fileId) {
109+
return [
110+
{
111+
type: 'file',
112+
id: result.fileId as string,
113+
title: (result.fileName as string) || 'File',
114+
},
115+
]
116+
}
117+
return []
118+
}
119+
120+
case 'create_workflow':
121+
case 'edit_workflow': {
122+
const workflowId =
123+
(result.workflowId as string) ??
124+
(data.workflowId as string) ??
125+
(params?.workflowId as string)
126+
if (workflowId) {
127+
const workflowName =
128+
(result.workflowName as string) ??
129+
(data.workflowName as string) ??
130+
(params?.workflowName as string) ??
131+
'Workflow'
132+
return [{ type: 'workflow', id: workflowId, title: workflowName }]
133+
}
134+
return []
135+
}
136+
137+
case 'knowledge_base': {
138+
if (READ_ONLY_KB_OPS.has(getOperation(params) ?? '')) return []
139+
140+
const kbId =
141+
(data.id as string) ??
142+
(result.knowledgeBaseId as string) ??
143+
(data.knowledgeBaseId as string) ??
144+
(params?.knowledgeBaseId as string)
145+
if (kbId) {
146+
const kbName =
147+
(data.name as string) ?? (result.knowledgeBaseName as string) ?? 'Knowledge Base'
148+
return [{ type: 'knowledgebase', id: kbId, title: kbName }]
149+
}
150+
return []
151+
}
152+
153+
case 'knowledge': {
154+
const action = data.action as string | undefined
155+
if (READ_ONLY_KNOWLEDGE_ACTIONS.has(action ?? '')) return []
156+
157+
const kbArray = data.knowledge_bases as Array<Record<string, unknown>> | undefined
158+
if (!Array.isArray(kbArray)) return []
159+
const resources: ChatResource[] = []
160+
for (const kb of kbArray) {
161+
const id = kb.id as string | undefined
162+
if (id) {
163+
resources.push({
164+
type: 'knowledgebase',
165+
id,
166+
title: (kb.name as string) || 'Knowledge Base',
167+
})
168+
}
169+
}
170+
return resources
171+
}
172+
173+
default:
174+
return []
175+
}
176+
}
177+
178+
const DELETE_CAPABLE_TOOL_RESOURCE_TYPE: Record<string, ResourceType> = {
179+
delete_workflow: 'workflow',
180+
workspace_file: 'file',
181+
user_table: 'table',
182+
knowledge_base: 'knowledgebase',
183+
}
184+
185+
export function hasDeleteCapability(toolName: string): boolean {
186+
return toolName in DELETE_CAPABLE_TOOL_RESOURCE_TYPE
187+
}
188+
189+
/**
190+
* Extracts resource descriptors from a tool execution result when the tool
191+
* performed a deletion. Returns one or more deleted resources for tools that
192+
* destroy workspace entities.
193+
*/
194+
export function extractDeletedResourcesFromToolResult(
195+
toolName: string,
196+
params: Record<string, unknown> | undefined,
197+
output: unknown
198+
): ChatResource[] {
199+
const resourceType = DELETE_CAPABLE_TOOL_RESOURCE_TYPE[toolName]
200+
if (!resourceType) return []
201+
202+
const result = asRecord(output)
203+
const data = asRecord(result.data)
204+
const args = asRecord(params?.args)
205+
const operation = (args.operation ?? params?.operation) as string | undefined
206+
207+
switch (toolName) {
208+
case 'delete_workflow': {
209+
const workflowId = (result.workflowId as string) ?? (params?.workflowId as string)
210+
if (workflowId && result.deleted) {
211+
return [
212+
{ type: resourceType, id: workflowId, title: (result.name as string) || 'Workflow' },
213+
]
214+
}
215+
return []
216+
}
217+
218+
case 'workspace_file': {
219+
if (operation !== 'delete') return []
220+
const fileId = (data.id as string) ?? (args.fileId as string)
221+
if (fileId) {
222+
return [{ type: resourceType, id: fileId, title: (data.name as string) || 'File' }]
223+
}
224+
return []
225+
}
226+
227+
case 'user_table': {
228+
if (operation !== 'delete') return []
229+
const tableId = (args.tableId as string) ?? (params?.tableId as string)
230+
if (tableId) {
231+
return [{ type: resourceType, id: tableId, title: 'Table' }]
232+
}
233+
return []
234+
}
235+
236+
case 'knowledge_base': {
237+
if (operation !== 'delete') return []
238+
const kbId = (data.id as string) ?? (args.knowledgeBaseId as string)
239+
if (kbId) {
240+
return [{ type: resourceType, id: kbId, title: (data.name as string) || 'Knowledge Base' }]
241+
}
242+
return []
243+
}
244+
245+
default:
246+
return []
247+
}
248+
}

0 commit comments

Comments
 (0)