Skip to content

Commit c09a2c9

Browse files
authored
v0.6.78: file block get
v0.6.78: file block get
2 parents ab156b5 + 1c111ff commit c09a2c9

7 files changed

Lines changed: 310 additions & 30 deletions

File tree

apps/sim/app/api/tools/file/manage/route.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@ import { ensureAbsoluteUrl } from '@/lib/core/utils/urls'
88
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
99
import {
1010
fetchWorkspaceFileBuffer,
11+
getWorkspaceFile,
1112
getWorkspaceFileByName,
1213
updateWorkspaceFileContent,
1314
uploadWorkspaceFile,
1415
} from '@/lib/uploads/contexts/workspace/workspace-file-manager'
1516
import { getFileExtension, getMimeTypeFromExtension } from '@/lib/uploads/utils/file-utils'
17+
import { assertActiveWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
1618

1719
export const dynamic = 'force-dynamic'
1820

@@ -39,7 +41,54 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
3941
}
4042

4143
try {
44+
await assertActiveWorkspaceAccess(workspaceId, userId)
45+
4246
switch (body.operation) {
47+
case 'get': {
48+
const { fileId, fileInput } = body
49+
const selectedFileId =
50+
fileId ||
51+
(fileInput && typeof fileInput === 'object' && !Array.isArray(fileInput)
52+
? typeof fileInput.id === 'string'
53+
? fileInput.id
54+
: typeof fileInput.fileId === 'string'
55+
? fileInput.fileId
56+
: ''
57+
: '')
58+
59+
if (!selectedFileId) {
60+
return NextResponse.json({ success: false, error: 'File is required' }, { status: 400 })
61+
}
62+
63+
const file = await getWorkspaceFile(workspaceId, selectedFileId)
64+
if (!file) {
65+
return NextResponse.json(
66+
{ success: false, error: `File not found: "${selectedFileId}"` },
67+
{ status: 404 }
68+
)
69+
}
70+
71+
logger.info('File retrieved', {
72+
fileId: file.id,
73+
name: file.name,
74+
})
75+
76+
return NextResponse.json({
77+
success: true,
78+
data: {
79+
file: {
80+
id: file.id,
81+
name: file.name,
82+
url: ensureAbsoluteUrl(file.path),
83+
size: file.size,
84+
type: file.type,
85+
key: file.key,
86+
context: 'workspace',
87+
},
88+
},
89+
})
90+
}
91+
4392
case 'write': {
4493
const { fileName, content, contentType } = body
4594
const mimeType = contentType || getMimeTypeFromExtension(getFileExtension(fileName))

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

Lines changed: 139 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@ import { sleep } from '@sim/utils/helpers'
55
import { generateId } from '@sim/utils/id'
66
import { useQueryClient } from '@tanstack/react-query'
77
import { usePathname, useRouter } from 'next/navigation'
8+
import { requestJson } from '@/lib/api/client/request'
9+
import {
10+
addMothershipChatResourceContract,
11+
removeMothershipChatResourceContract,
12+
reorderMothershipChatResourcesContract,
13+
} from '@/lib/api/contracts/mothership-tasks'
14+
import { cancelWorkflowExecutionContract } from '@/lib/api/contracts/workflows'
815
import { getMothershipAttachmentPreviewUrl } from '@/lib/copilot/chat/attachment-preview'
916
import { toDisplayMessage } from '@/lib/copilot/chat/display-message'
1017
import { getLiveAssistantMessageId } from '@/lib/copilot/chat/effective-transcript'
@@ -1536,6 +1543,9 @@ export function useChat(
15361543
}, [])
15371544
const resourcesRef = useRef(resources)
15381545
resourcesRef.current = resources
1546+
const pendingPersistResourceKeysRef = useRef<Set<string>>(new Set())
1547+
const inFlightResourceAddsRef = useRef<Map<string, Promise<unknown>>>(new Map())
1548+
const reorderNeededAfterFlushRef = useRef(false)
15391549

15401550
// Derive the effective active resource ID — auto-selects the last resource when the stored ID is
15411551
// absent or no longer in the list, avoiding a separate Effect-based state correction loop.
@@ -1962,6 +1972,9 @@ export function useChat(
19621972
setTransportIdle()
19631973
setResources([])
19641974
setActiveResourceId(null)
1975+
pendingPersistResourceKeysRef.current.clear()
1976+
inFlightResourceAddsRef.current.clear()
1977+
reorderNeededAfterFlushRef.current = false
19651978
resetEphemeralPreviewState()
19661979
setMessageQueue([])
19671980
clearQueueDispatchState()
@@ -1974,6 +1987,44 @@ export function useChat(
19741987
setTransportIdle,
19751988
])
19761989

1990+
const flushPendingResources = useCallback(async (chatId: string) => {
1991+
const pendingKeys = pendingPersistResourceKeysRef.current
1992+
if (pendingKeys.size === 0) return
1993+
const flushPromises: Array<Promise<unknown>> = []
1994+
for (const resource of resourcesRef.current) {
1995+
if (resource.id === 'streaming-file') continue
1996+
const key = `${resource.type}:${resource.id}`
1997+
if (!pendingKeys.has(key)) continue
1998+
pendingKeys.delete(key)
1999+
const promise = requestJson(addMothershipChatResourceContract, {
2000+
body: { chatId, resource },
2001+
})
2002+
.catch((err) => {
2003+
pendingPersistResourceKeysRef.current.add(key)
2004+
logger.warn('Failed to flush pending resource; will retry on next hydration', err)
2005+
})
2006+
.finally(() => {
2007+
inFlightResourceAddsRef.current.delete(key)
2008+
})
2009+
inFlightResourceAddsRef.current.set(key, promise)
2010+
flushPromises.push(promise)
2011+
}
2012+
if (flushPromises.length === 0) return
2013+
await Promise.allSettled(flushPromises)
2014+
if (!reorderNeededAfterFlushRef.current) return
2015+
reorderNeededAfterFlushRef.current = false
2016+
const localOrder = resourcesRef.current.filter(
2017+
(r) =>
2018+
r.id !== 'streaming-file' && !pendingPersistResourceKeysRef.current.has(`${r.type}:${r.id}`)
2019+
)
2020+
if (localOrder.length === 0) return
2021+
requestJson(reorderMothershipChatResourcesContract, {
2022+
body: { chatId, resources: localOrder },
2023+
}).catch((err) => {
2024+
logger.warn('Failed to sync resource order after flush', err)
2025+
})
2026+
}, [])
2027+
19772028
const adoptResolvedChatId = useCallback(
19782029
(chatId: string, options?: { replaceHomeHistory?: boolean; invalidateList?: boolean }) => {
19792030
const selectedChatId = selectedChatIdRef.current
@@ -1992,8 +2043,9 @@ export function useChat(
19922043
if (options?.invalidateList) {
19932044
queryClient.invalidateQueries({ queryKey: taskKeys.list(workspaceId) })
19942045
}
2046+
flushPendingResources(chatId)
19952047
},
1996-
[queryClient, workspaceId]
2048+
[flushPendingResources, queryClient, workspaceId]
19972049
)
19982050

19992051
const { data: chatHistory } = useChatHistory(resolvedChatId)
@@ -2018,15 +2070,21 @@ export function useChat(
20182070
}
20192071

20202072
const persistChatId = chatIdRef.current ?? selectedChatIdRef.current
2073+
const key = `${resource.type}:${resource.id}`
20212074
if (persistChatId) {
2022-
// boundary-raw-fetch: fire-and-forget side-effect during stream lifecycle; intentionally avoids requestJson's response parsing/throw semantics so a failure here cannot interrupt the active turn
2023-
fetch('/api/mothership/chat/resources', {
2024-
method: 'POST',
2025-
headers: { 'Content-Type': 'application/json' },
2026-
body: JSON.stringify({ chatId: persistChatId, resource }),
2027-
}).catch((err) => {
2028-
logger.warn('Failed to persist resource', err)
2075+
const promise = requestJson(addMothershipChatResourceContract, {
2076+
body: { chatId: persistChatId, resource },
20292077
})
2078+
.catch((err) => {
2079+
pendingPersistResourceKeysRef.current.add(key)
2080+
logger.warn('Failed to persist resource; will retry on next hydration', err)
2081+
})
2082+
.finally(() => {
2083+
inFlightResourceAddsRef.current.delete(key)
2084+
})
2085+
inFlightResourceAddsRef.current.set(key, promise)
2086+
} else {
2087+
pendingPersistResourceKeysRef.current.add(key)
20302088
}
20312089
return true
20322090
}, [])
@@ -2035,21 +2093,67 @@ export function useChat(
20352093
setResources((prev) => prev.filter((r) => !(r.type === resourceType && r.id === resourceId)))
20362094
setActiveResourceId((prev) => (prev === resourceId ? null : prev))
20372095

2096+
const key = `${resourceType}:${resourceId}`
2097+
const wasPending = pendingPersistResourceKeysRef.current.delete(key)
2098+
const inFlightAdd = inFlightResourceAddsRef.current.get(key)
2099+
if (wasPending && !inFlightAdd) return
2100+
20382101
const persistChatId = chatIdRef.current ?? selectedChatIdRef.current
2039-
if (persistChatId) {
2040-
// boundary-raw-fetch: fire-and-forget side-effect; intentionally avoids requestJson's response parsing/throw semantics so a transient failure cannot interrupt the caller
2041-
fetch('/api/mothership/chat/resources', {
2042-
method: 'DELETE',
2043-
headers: { 'Content-Type': 'application/json' },
2044-
body: JSON.stringify({ chatId: persistChatId, resourceType, resourceId }),
2102+
if (!persistChatId) return
2103+
const fireDelete = () => {
2104+
requestJson(removeMothershipChatResourceContract, {
2105+
body: { chatId: persistChatId, resourceType, resourceId },
20452106
}).catch((err) => {
20462107
logger.warn('Failed to persist resource removal', err)
20472108
})
20482109
}
2110+
if (inFlightAdd) {
2111+
inFlightAdd.finally(fireDelete)
2112+
} else {
2113+
fireDelete()
2114+
}
20492115
}, [])
20502116

20512117
const reorderResources = useCallback((newOrder: MothershipResource[]) => {
20522118
setResources(newOrder)
2119+
const persistChatId = chatIdRef.current ?? selectedChatIdRef.current
2120+
if (!persistChatId) return
2121+
const pendingKeys = pendingPersistResourceKeysRef.current
2122+
const inFlightAdds = inFlightResourceAddsRef.current
2123+
const hasUnsyncedAdds = newOrder.some((r) => {
2124+
const key = `${r.type}:${r.id}`
2125+
return pendingKeys.has(key) || inFlightAdds.has(key)
2126+
})
2127+
if (hasUnsyncedAdds) {
2128+
reorderNeededAfterFlushRef.current = true
2129+
if (pendingKeys.size === 0 && inFlightAdds.size > 0) {
2130+
Promise.allSettled(Array.from(inFlightAdds.values())).then(() => {
2131+
if (!reorderNeededAfterFlushRef.current) return
2132+
reorderNeededAfterFlushRef.current = false
2133+
const chatId = chatIdRef.current ?? selectedChatIdRef.current
2134+
if (!chatId) return
2135+
const order = resourcesRef.current.filter(
2136+
(r) =>
2137+
r.id !== 'streaming-file' &&
2138+
!pendingPersistResourceKeysRef.current.has(`${r.type}:${r.id}`)
2139+
)
2140+
if (order.length === 0) return
2141+
requestJson(reorderMothershipChatResourcesContract, {
2142+
body: { chatId, resources: order },
2143+
}).catch((err) => {
2144+
logger.warn('Failed to sync resource order after in-flight ADDs', err)
2145+
})
2146+
})
2147+
}
2148+
return
2149+
}
2150+
const persistableResources = newOrder.filter((r) => r.id !== 'streaming-file')
2151+
if (persistableResources.length === 0) return
2152+
requestJson(reorderMothershipChatResourcesContract, {
2153+
body: { chatId: persistChatId, resources: persistableResources },
2154+
}).catch((err) => {
2155+
logger.warn('Failed to persist resource reorder', err)
2156+
})
20532157
}, [])
20542158

20552159
const ensureWorkflowToolResource = useCallback(
@@ -2179,6 +2283,9 @@ export function useChat(
21792283
setTransportIdle()
21802284
setResources([])
21812285
setActiveResourceId(null)
2286+
pendingPersistResourceKeysRef.current.clear()
2287+
inFlightResourceAddsRef.current.clear()
2288+
reorderNeededAfterFlushRef.current = false
21822289
resetEphemeralPreviewState()
21832290
setMessageQueue([])
21842291
clearQueueDispatchState()
@@ -2229,27 +2336,32 @@ export function useChat(
22292336

22302337
const hasPersistedStreamingFile = chatHistory.resources.some((r) => r.id === 'streaming-file')
22312338
if (hasPersistedStreamingFile) {
2232-
// boundary-raw-fetch: fire-and-forget cleanup during chat-history hydration; failures are silently swallowed to keep hydration non-blocking
2233-
fetch('/api/mothership/chat/resources', {
2234-
method: 'DELETE',
2235-
headers: { 'Content-Type': 'application/json' },
2236-
body: JSON.stringify({
2339+
requestJson(removeMothershipChatResourceContract, {
2340+
body: {
22372341
chatId: chatHistory.id,
22382342
resourceType: 'file',
22392343
resourceId: 'streaming-file',
2240-
}),
2344+
},
22412345
}).catch(() => {})
22422346
}
22432347

2348+
flushPendingResources(chatHistory.id)
2349+
22442350
const persistedResources = chatHistory.resources.filter((r) => r.id !== 'streaming-file')
2245-
if (persistedResources.length > 0) {
2351+
const serverKeys = new Set(persistedResources.map((r) => `${r.type}:${r.id}`))
2352+
const localOnly = resourcesRef.current.filter(
2353+
(r) => r.id !== 'streaming-file' && !serverKeys.has(`${r.type}:${r.id}`)
2354+
)
2355+
const mergedResources = [...persistedResources, ...localOnly]
2356+
2357+
if (mergedResources.length > 0) {
22462358
const hydratedActiveResourceId =
22472359
activeResourceIdRef.current &&
2248-
persistedResources.some((resource) => resource.id === activeResourceIdRef.current)
2360+
mergedResources.some((resource) => resource.id === activeResourceIdRef.current)
22492361
? activeResourceIdRef.current
2250-
: persistedResources[persistedResources.length - 1].id
2362+
: mergedResources[mergedResources.length - 1].id
22512363
activeResourceIdRef.current = hydratedActiveResourceId
2252-
setResources(persistedResources)
2364+
setResources(mergedResources)
22532365
setActiveResourceId(hydratedActiveResourceId)
22542366

22552367
for (const resource of persistedResources) {
@@ -2373,6 +2485,7 @@ export function useChat(
23732485
workspaceId,
23742486
cancelActiveStreamReader,
23752487
cancelActiveStreamRecovery,
2488+
flushPendingResources,
23762489
queryClient,
23772490
recoverPendingClientWorkflowTools,
23782491
seedPreviewSessions,
@@ -5003,9 +5116,8 @@ export function useChat(
50035116
const executionId = execState.getCurrentExecutionId(workflowId)
50045117
if (executionId) {
50055118
execState.setCurrentExecutionId(workflowId, null)
5006-
// boundary-raw-fetch: fire-and-forget execution cancellation invoked from a stop-generation barrier; failures are silently swallowed so the stop teardown cannot stall on a contract-validation throw
5007-
fetch(`/api/workflows/${workflowId}/executions/${executionId}/cancel`, {
5008-
method: 'POST',
5119+
requestJson(cancelWorkflowExecutionContract, {
5120+
params: { id: workflowId, executionId },
50095121
}).catch(() => {})
50105122
}
50115123

0 commit comments

Comments
 (0)