mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-28 03:00:29 -04:00
File writes
This commit is contained in:
@@ -255,7 +255,26 @@ function TextEditor({
|
||||
? fetchedContent
|
||||
: `${fetchedContent}\n${streamingContent}`
|
||||
// #region agent log
|
||||
fetch('http://127.0.0.1:7774/ingest/b056eec6-a1ee-457f-8556-85f94314ca06',{method:'POST',headers:{'Content-Type':'application/json','X-Debug-Session-Id':'6f10b0'},body:JSON.stringify({sessionId:'6f10b0',location:'file-viewer.tsx:TextEditor-merge',message:'streaming merge',data:{streamingMode,fetchedContentLen:fetchedContent?.length,streamingContentLen:streamingContent.length,nextContentLen:nextContent.length,fetchedUndefined:fetchedContent===undefined,usedReplace:streamingMode==='replace'||fetchedContent===undefined,nextPreview:nextContent.slice(0,200)},timestamp:Date.now(),hypothesisId:'H2-H3'})}).catch(()=>{});
|
||||
fetch('http://127.0.0.1:7774/ingest/b056eec6-a1ee-457f-8556-85f94314ca06', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-Debug-Session-Id': '6f10b0' },
|
||||
body: JSON.stringify({
|
||||
sessionId: '6f10b0',
|
||||
location: 'file-viewer.tsx:TextEditor-merge',
|
||||
message: 'streaming merge',
|
||||
data: {
|
||||
streamingMode,
|
||||
fetchedContentLen: fetchedContent?.length,
|
||||
streamingContentLen: streamingContent.length,
|
||||
nextContentLen: nextContent.length,
|
||||
fetchedUndefined: fetchedContent === undefined,
|
||||
usedReplace: streamingMode === 'replace' || fetchedContent === undefined,
|
||||
nextPreview: nextContent.slice(0, 200),
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
hypothesisId: 'H2-H3',
|
||||
}),
|
||||
}).catch(() => {})
|
||||
// #endregion
|
||||
setContent(nextContent)
|
||||
contentRef.current = nextContent
|
||||
@@ -264,8 +283,8 @@ function TextEditor({
|
||||
}
|
||||
|
||||
if (wasStreamingRef.current) {
|
||||
wasStreamingRef.current = false
|
||||
if (fetchedContent !== undefined) {
|
||||
if (fetchedContent !== undefined && fetchedContent !== savedContentRef.current) {
|
||||
wasStreamingRef.current = false
|
||||
setContent(fetchedContent)
|
||||
setSavedContent(fetchedContent)
|
||||
savedContentRef.current = fetchedContent
|
||||
@@ -377,7 +396,8 @@ function TextEditor({
|
||||
)
|
||||
|
||||
const isStreaming = streamingContent !== undefined
|
||||
const revealedContent = useStreamingText(content, isStreaming)
|
||||
const shouldAnimateStreaming = isStreaming && streamingMode === 'append'
|
||||
const revealedContent = useStreamingText(content, shouldAnimateStreaming)
|
||||
|
||||
const textareaStuckRef = useRef(true)
|
||||
|
||||
|
||||
@@ -122,7 +122,30 @@ export const ResourceContent = memo(function ResourceContent({
|
||||
|
||||
// #region agent log
|
||||
if (streamingFile) {
|
||||
fetch('http://127.0.0.1:7774/ingest/b056eec6-a1ee-457f-8556-85f94314ca06',{method:'POST',headers:{'Content-Type':'application/json','X-Debug-Session-Id':'6f10b0'},body:JSON.stringify({sessionId:'6f10b0',location:'resource-content.tsx:streaming-context',message:'streaming state',data:{resourceId:resource.id,resourceType:resource.type,streamOp:streamOperation,isPatch:isPatchStream,isWrite:isWriteStream,isUpdate:isUpdateStream,hasActiveFileRecord:!!activeFileRecord,hasFetchedContent:!!fetchedFileContent,fetchedContentLen:fetchedFileContent?.length,streamingFileContentLen:streamingFile.content.length,streamingFileName:streamingFile.fileName,streamingFileMode:isWriteStream?'append':'replace'},timestamp:Date.now()})}).catch(()=>{});
|
||||
fetch('http://127.0.0.1:7774/ingest/b056eec6-a1ee-457f-8556-85f94314ca06', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-Debug-Session-Id': '6f10b0' },
|
||||
body: JSON.stringify({
|
||||
sessionId: '6f10b0',
|
||||
location: 'resource-content.tsx:streaming-context',
|
||||
message: 'streaming state',
|
||||
data: {
|
||||
resourceId: resource.id,
|
||||
resourceType: resource.type,
|
||||
streamOp: streamOperation,
|
||||
isPatch: isPatchStream,
|
||||
isWrite: isWriteStream,
|
||||
isUpdate: isUpdateStream,
|
||||
hasActiveFileRecord: !!activeFileRecord,
|
||||
hasFetchedContent: !!fetchedFileContent,
|
||||
fetchedContentLen: fetchedFileContent?.length,
|
||||
streamingFileContentLen: streamingFile.content.length,
|
||||
streamingFileName: streamingFile.fileName,
|
||||
streamingFileMode: isWriteStream ? 'append' : 'replace',
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
}),
|
||||
}).catch(() => {})
|
||||
}
|
||||
// #endregion
|
||||
const streamingExtractedContent = useMemo(() => {
|
||||
@@ -132,13 +155,41 @@ export const ResourceContent = memo(function ResourceContent({
|
||||
if (isPatchStream) {
|
||||
if (!fetchedFileContent) {
|
||||
// #region agent log
|
||||
fetch('http://127.0.0.1:7774/ingest/b056eec6-a1ee-457f-8556-85f94314ca06',{method:'POST',headers:{'Content-Type':'application/json','X-Debug-Session-Id':'6f10b0'},body:JSON.stringify({sessionId:'6f10b0',location:'resource-content.tsx:patch-no-fetched',message:'patch but no fetchedFileContent',data:{resourceId:resource.id,activeFileRecordId:activeFileRecord?.id},timestamp:Date.now(),hypothesisId:'H1'})}).catch(()=>{});
|
||||
fetch('http://127.0.0.1:7774/ingest/b056eec6-a1ee-457f-8556-85f94314ca06', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-Debug-Session-Id': '6f10b0' },
|
||||
body: JSON.stringify({
|
||||
sessionId: '6f10b0',
|
||||
location: 'resource-content.tsx:patch-no-fetched',
|
||||
message: 'patch but no fetchedFileContent',
|
||||
data: { resourceId: resource.id, activeFileRecordId: activeFileRecord?.id },
|
||||
timestamp: Date.now(),
|
||||
hypothesisId: 'H1',
|
||||
}),
|
||||
}).catch(() => {})
|
||||
// #endregion
|
||||
return undefined
|
||||
}
|
||||
const patchResult = extractPatchPreview(streamingFile, fetchedFileContent)
|
||||
// #region agent log
|
||||
fetch('http://127.0.0.1:7774/ingest/b056eec6-a1ee-457f-8556-85f94314ca06',{method:'POST',headers:{'Content-Type':'application/json','X-Debug-Session-Id':'6f10b0'},body:JSON.stringify({sessionId:'6f10b0',location:'resource-content.tsx:patch-result',message:'extractPatchPreview result',data:{hasPatchResult:!!patchResult,patchResultLen:patchResult?.length,fetchedLen:fetchedFileContent.length,contentPreview:streamingFile.content.slice(0,200),edit:streamingFile.edit},timestamp:Date.now(),hypothesisId:'H4'})}).catch(()=>{});
|
||||
fetch('http://127.0.0.1:7774/ingest/b056eec6-a1ee-457f-8556-85f94314ca06', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-Debug-Session-Id': '6f10b0' },
|
||||
body: JSON.stringify({
|
||||
sessionId: '6f10b0',
|
||||
location: 'resource-content.tsx:patch-result',
|
||||
message: 'extractPatchPreview result',
|
||||
data: {
|
||||
hasPatchResult: !!patchResult,
|
||||
patchResultLen: patchResult?.length,
|
||||
fetchedLen: fetchedFileContent.length,
|
||||
contentPreview: streamingFile.content.slice(0, 200),
|
||||
edit: streamingFile.edit,
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
hypothesisId: 'H4',
|
||||
}),
|
||||
}).catch(() => {})
|
||||
// #endregion
|
||||
return patchResult
|
||||
}
|
||||
@@ -147,14 +198,36 @@ export const ResourceContent = memo(function ResourceContent({
|
||||
if (extracted.length === 0) return undefined
|
||||
|
||||
// #region agent log
|
||||
fetch('http://127.0.0.1:7774/ingest/b056eec6-a1ee-457f-8556-85f94314ca06',{method:'POST',headers:{'Content-Type':'application/json','X-Debug-Session-Id':'6f10b0'},body:JSON.stringify({sessionId:'6f10b0',location:'resource-content.tsx:write-update-content',message:'extracted content for write/update',data:{streamOp:streamOperation,extractedLen:extracted.length,extractedPreview:extracted.slice(0,150)},timestamp:Date.now(),hypothesisId:'H2'})}).catch(()=>{});
|
||||
fetch('http://127.0.0.1:7774/ingest/b056eec6-a1ee-457f-8556-85f94314ca06', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-Debug-Session-Id': '6f10b0' },
|
||||
body: JSON.stringify({
|
||||
sessionId: '6f10b0',
|
||||
location: 'resource-content.tsx:write-update-content',
|
||||
message: 'extracted content for write/update',
|
||||
data: {
|
||||
streamOp: streamOperation,
|
||||
extractedLen: extracted.length,
|
||||
extractedPreview: extracted.slice(0, 150),
|
||||
},
|
||||
timestamp: Date.now(),
|
||||
hypothesisId: 'H2',
|
||||
}),
|
||||
}).catch(() => {})
|
||||
// #endregion
|
||||
|
||||
if (isUpdateStream) return extracted
|
||||
if (isWriteStream) return extracted
|
||||
|
||||
return undefined
|
||||
}, [streamingFile, streamOperation, isWriteStream, isPatchStream, isUpdateStream, fetchedFileContent])
|
||||
}, [
|
||||
streamingFile,
|
||||
streamOperation,
|
||||
isWriteStream,
|
||||
isPatchStream,
|
||||
isUpdateStream,
|
||||
fetchedFileContent,
|
||||
])
|
||||
const syntheticFile = useMemo(() => {
|
||||
const ext = getFileExtension(streamFileName)
|
||||
const SOURCE_MIME_MAP: Record<string, string> = {
|
||||
@@ -176,17 +249,14 @@ export const ResourceContent = memo(function ResourceContent({
|
||||
}
|
||||
}, [workspaceId, streamFileName])
|
||||
|
||||
const streamingFileMode: 'append' | 'replace' =
|
||||
isWriteStream ? 'append' : 'replace'
|
||||
const streamingFileMode: 'append' | 'replace' = isWriteStream ? 'append' : 'replace'
|
||||
|
||||
// For existing file resources (not streaming-file), only pass streaming
|
||||
// content for patch operations where the preview splices new content into
|
||||
// the displayed file. Update operations re-stream the entire file from
|
||||
// scratch which causes visual duplication of already-visible content.
|
||||
const embeddedStreamingContent =
|
||||
resource.id !== 'streaming-file' && isUpdateStream
|
||||
? undefined
|
||||
: streamingExtractedContent
|
||||
resource.id !== 'streaming-file' && isUpdateStream ? undefined : streamingExtractedContent
|
||||
|
||||
if (streamingFile && resource.id === 'streaming-file') {
|
||||
return (
|
||||
@@ -513,7 +583,13 @@ interface EmbeddedFileProps {
|
||||
streamingMode?: 'append' | 'replace'
|
||||
}
|
||||
|
||||
function EmbeddedFile({ workspaceId, fileId, previewMode, streamingContent, streamingMode }: EmbeddedFileProps) {
|
||||
function EmbeddedFile({
|
||||
workspaceId,
|
||||
fileId,
|
||||
previewMode,
|
||||
streamingContent,
|
||||
streamingMode,
|
||||
}: EmbeddedFileProps) {
|
||||
const { canEdit } = useUserPermissionsContext()
|
||||
const { data: files = [], isLoading, isFetching } = useWorkspaceFiles(workspaceId)
|
||||
const file = useMemo(() => files.find((f) => f.id === fileId), [files, fileId])
|
||||
@@ -638,9 +714,7 @@ function extractPatchPreview(
|
||||
const strategy = typeof edit.strategy === 'string' ? edit.strategy : undefined
|
||||
const lines = existingContent.split('\n')
|
||||
const occurrence =
|
||||
typeof edit.occurrence === 'number' && Number.isFinite(edit.occurrence)
|
||||
? edit.occurrence
|
||||
: 1
|
||||
typeof edit.occurrence === 'number' && Number.isFinite(edit.occurrence) ? edit.occurrence : 1
|
||||
|
||||
if (strategy === 'search_replace') {
|
||||
const search = typeof edit.search === 'string' ? edit.search : ''
|
||||
@@ -651,7 +725,9 @@ function extractPatchPreview(
|
||||
}
|
||||
const firstIdx = existingContent.indexOf(search)
|
||||
if (firstIdx === -1) return undefined
|
||||
return existingContent.slice(0, firstIdx) + replace + existingContent.slice(firstIdx + search.length)
|
||||
return (
|
||||
existingContent.slice(0, firstIdx) + replace + existingContent.slice(firstIdx + search.length)
|
||||
)
|
||||
}
|
||||
|
||||
const mode = typeof edit.mode === 'string' ? edit.mode : undefined
|
||||
|
||||
@@ -9,8 +9,8 @@ import {
|
||||
} from 'react'
|
||||
import { Button, Tooltip } from '@/components/emcn'
|
||||
import { Columns3, Eye, PanelLeft, Pencil } from '@/components/emcn/icons'
|
||||
import { isEphemeralResource } from '@/lib/copilot/resources/types'
|
||||
import { SIM_RESOURCE_DRAG_TYPE } from '@/lib/copilot/resource-types'
|
||||
import { isEphemeralResource } from '@/lib/copilot/resources/types'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import type { PreviewMode } from '@/app/workspace/[workspaceId]/files/components/file-viewer'
|
||||
import { AddResourceDropdown } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown'
|
||||
|
||||
@@ -58,17 +58,15 @@ interface MothershipViewProps {
|
||||
onCollapse: () => void
|
||||
isCollapsed: boolean
|
||||
className?: string
|
||||
streamingFile?:
|
||||
| {
|
||||
toolCallId?: string
|
||||
fileName: string
|
||||
fileId?: string
|
||||
targetKind?: 'new_file' | 'file_id'
|
||||
operation?: string
|
||||
edit?: Record<string, unknown>
|
||||
content: string
|
||||
}
|
||||
| null
|
||||
streamingFile?: {
|
||||
toolCallId?: string
|
||||
fileName: string
|
||||
fileId?: string
|
||||
targetKind?: 'new_file' | 'file_id'
|
||||
operation?: string
|
||||
edit?: Record<string, unknown>
|
||||
content: string
|
||||
} | null
|
||||
genericResourceData?: GenericResourceData
|
||||
}
|
||||
|
||||
|
||||
@@ -904,13 +904,15 @@ export function useChat(
|
||||
const target = asPayloadRecord(payload.target)
|
||||
const nextSession: StreamingFilePreview = {
|
||||
...prevSession,
|
||||
operation: typeof payload.operation === 'string' ? payload.operation : prevSession.operation,
|
||||
operation:
|
||||
typeof payload.operation === 'string'
|
||||
? payload.operation
|
||||
: prevSession.operation,
|
||||
targetKind:
|
||||
target?.kind === 'new_file' || target?.kind === 'file_id'
|
||||
? (target.kind as 'new_file' | 'file_id')
|
||||
: prevSession.targetKind,
|
||||
fileId:
|
||||
typeof target?.fileId === 'string' ? target.fileId : prevSession.fileId,
|
||||
fileId: typeof target?.fileId === 'string' ? target.fileId : prevSession.fileId,
|
||||
fileName:
|
||||
typeof target?.fileName === 'string' ? target.fileName : prevSession.fileName,
|
||||
}
|
||||
@@ -954,13 +956,11 @@ export function useChat(
|
||||
break
|
||||
}
|
||||
|
||||
if (previewPhase === 'file_preview_content_delta') {
|
||||
const delta =
|
||||
typeof payload.delta === 'string' ? payload.delta : ''
|
||||
if (!delta) break
|
||||
if (previewPhase === 'file_preview_content') {
|
||||
const content = typeof payload.content === 'string' ? payload.content : ''
|
||||
const nextSession: StreamingFilePreview = {
|
||||
...prevSession,
|
||||
content: (prevSession.content ?? '') + delta,
|
||||
content,
|
||||
}
|
||||
sessions.set(id, nextSession)
|
||||
activeFilePreviewToolCallIdRef.current = id
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -32,7 +32,7 @@ type FilePreviewServerState = {
|
||||
title?: string
|
||||
editMetaKey?: string
|
||||
targetKey?: string
|
||||
emittedContentLength: number
|
||||
lastContentSnapshot?: string
|
||||
}
|
||||
|
||||
function extractJsonString(raw: string, key: string): string | undefined {
|
||||
@@ -74,6 +74,70 @@ function extractJsonNumber(raw: string, key: string): number | undefined {
|
||||
return Number.parseInt(match[1], 10)
|
||||
}
|
||||
|
||||
function decodeJsonStringPrefix(input: string): string {
|
||||
let output = ''
|
||||
for (let i = 0; i < input.length; i++) {
|
||||
const ch = input[i]
|
||||
if (ch !== '\\') {
|
||||
output += ch
|
||||
continue
|
||||
}
|
||||
const next = input[i + 1]
|
||||
if (!next) break
|
||||
if (next === 'n') {
|
||||
output += '\n'
|
||||
i++
|
||||
continue
|
||||
}
|
||||
if (next === 't') {
|
||||
output += '\t'
|
||||
i++
|
||||
continue
|
||||
}
|
||||
if (next === 'r') {
|
||||
output += '\r'
|
||||
i++
|
||||
continue
|
||||
}
|
||||
if (next === '"') {
|
||||
output += '"'
|
||||
i++
|
||||
continue
|
||||
}
|
||||
if (next === '\\') {
|
||||
output += '\\'
|
||||
i++
|
||||
continue
|
||||
}
|
||||
if (next === '/') {
|
||||
output += '/'
|
||||
i++
|
||||
continue
|
||||
}
|
||||
if (next === 'b') {
|
||||
output += '\b'
|
||||
i++
|
||||
continue
|
||||
}
|
||||
if (next === 'f') {
|
||||
output += '\f'
|
||||
i++
|
||||
continue
|
||||
}
|
||||
if (next === 'u') {
|
||||
const hex = input.slice(i + 2, i + 6)
|
||||
if (hex.length < 4 || !/^[0-9a-fA-F]{4}$/.test(hex)) {
|
||||
break
|
||||
}
|
||||
output += String.fromCharCode(Number.parseInt(hex, 16))
|
||||
i += 5
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
function extractStreamedContent(raw: string, preferredKey: 'content' | 'replace'): string {
|
||||
const marker = `"${preferredKey}":`
|
||||
const idx = raw.indexOf(marker)
|
||||
@@ -92,13 +156,7 @@ function extractStreamedContent(raw: string, preferredKey: 'content' | 'replace'
|
||||
}
|
||||
}
|
||||
const inner = end === -1 ? rest.slice(1) : rest.slice(1, end)
|
||||
return inner
|
||||
.replace(/\\n/g, '\n')
|
||||
.replace(/\\t/g, '\t')
|
||||
.replace(/\\r/g, '\r')
|
||||
.replace(/\\"/g, '"')
|
||||
.replace(/\\u([0-9a-fA-F]{4})/g, (_, hex) => String.fromCharCode(Number.parseInt(hex, 16)))
|
||||
.replace(/\\\\/g, '\\')
|
||||
return decodeJsonStringPrefix(inner)
|
||||
}
|
||||
|
||||
function buildPreviewContent(raw: string, strategy?: string): string {
|
||||
@@ -235,7 +293,6 @@ export async function runStreamLoop(
|
||||
const state = filePreviewState.get(toolCallId) ?? {
|
||||
raw: '',
|
||||
started: false,
|
||||
emittedContentLength: 0,
|
||||
}
|
||||
state.raw += delta
|
||||
|
||||
@@ -343,16 +400,15 @@ export async function runStreamLoop(
|
||||
}
|
||||
|
||||
const streamedContent = buildPreviewContent(state.raw, strategy)
|
||||
if (streamedContent.length > state.emittedContentLength) {
|
||||
const contentDelta = streamedContent.slice(state.emittedContentLength)
|
||||
state.emittedContentLength = streamedContent.length
|
||||
if (streamedContent !== (state.lastContentSnapshot ?? '')) {
|
||||
state.lastContentSnapshot = streamedContent
|
||||
await options.onEvent?.({
|
||||
type: MothershipStreamV1EventType.tool,
|
||||
payload: {
|
||||
toolCallId,
|
||||
toolName: 'workspace_file',
|
||||
previewPhase: 'file_preview_content_delta',
|
||||
delta: contentDelta,
|
||||
previewPhase: 'file_preview_content',
|
||||
content: streamedContent,
|
||||
},
|
||||
...(streamEvent.scope ? { scope: streamEvent.scope } : {}),
|
||||
})
|
||||
|
||||
@@ -21,7 +21,7 @@ import type {
|
||||
StreamingContext,
|
||||
ToolCallState,
|
||||
} from '@/lib/copilot/request/types'
|
||||
import { isSimExecuted, getToolEntry } from '@/lib/copilot/tool-executor'
|
||||
import { getToolEntry, isSimExecuted } from '@/lib/copilot/tool-executor'
|
||||
import { isWorkflowToolName } from '@/lib/copilot/tools/workflow-tools'
|
||||
import type { ToolScope } from './types'
|
||||
import {
|
||||
|
||||
@@ -42,7 +42,9 @@ function getOperation(params: Record<string, unknown> | undefined): string | und
|
||||
return (args.operation ?? params?.operation) as string | undefined
|
||||
}
|
||||
|
||||
function getWorkspaceFileTarget(params: Record<string, unknown> | undefined): Record<string, unknown> {
|
||||
function getWorkspaceFileTarget(
|
||||
params: Record<string, unknown> | undefined
|
||||
): Record<string, unknown> {
|
||||
return asRecord(params?.target)
|
||||
}
|
||||
|
||||
@@ -255,10 +257,7 @@ export function extractDeletedResourcesFromToolResult(
|
||||
case WorkspaceFile.id: {
|
||||
if (operation !== 'delete') return []
|
||||
const target = getWorkspaceFileTarget(params)
|
||||
const fileId =
|
||||
(data.id as string) ??
|
||||
(target.fileId as string) ??
|
||||
(args.fileId as string)
|
||||
const fileId = (data.id as string) ?? (target.fileId as string) ?? (args.fileId as string)
|
||||
if (fileId) {
|
||||
return [{ type: resourceType, id: fileId, title: (data.name as string) || 'File' }]
|
||||
}
|
||||
|
||||
@@ -70,8 +70,6 @@ type WorkspaceFileArgs = {
|
||||
contentType?: string
|
||||
newName?: string
|
||||
edit?: WorkspaceFileEdit
|
||||
// Legacy nested shape kept temporarily for compatibility during migration.
|
||||
args?: Record<string, unknown>
|
||||
}
|
||||
|
||||
type WorkspaceFileResult = {
|
||||
@@ -140,102 +138,6 @@ function getDocumentFormatInfo(fileName: string): {
|
||||
return { isDoc: false }
|
||||
}
|
||||
|
||||
function normalizeWorkspaceFileParams(params: WorkspaceFileArgs): {
|
||||
operation: WorkspaceFileOperation
|
||||
target?: WorkspaceFileTarget
|
||||
title?: string
|
||||
content?: string
|
||||
contentType?: string
|
||||
newName?: string
|
||||
edit?: WorkspaceFileEdit
|
||||
} {
|
||||
if (params.target || params.edit || params.content !== undefined || params.newName !== undefined) {
|
||||
return {
|
||||
operation: params.operation,
|
||||
target: params.target,
|
||||
title: params.title,
|
||||
content: params.content,
|
||||
contentType: params.contentType,
|
||||
newName: params.newName,
|
||||
edit: params.edit,
|
||||
}
|
||||
}
|
||||
|
||||
const legacyArgs = (params.args ?? {}) as Record<string, unknown>
|
||||
const legacyOperation = params.operation
|
||||
const legacyTarget: WorkspaceFileTarget | undefined =
|
||||
legacyOperation === 'create'
|
||||
? ({
|
||||
kind: 'new_file',
|
||||
fileName: String(legacyArgs.fileName ?? ''),
|
||||
} as WorkspaceFileTarget)
|
||||
: legacyArgs.fileId
|
||||
? ({
|
||||
kind: 'file_id',
|
||||
fileId: String(legacyArgs.fileId),
|
||||
...(legacyArgs.fileName ? { fileName: String(legacyArgs.fileName) } : {}),
|
||||
} as WorkspaceFileTarget)
|
||||
: legacyArgs.fileName
|
||||
? ({
|
||||
kind: 'new_file',
|
||||
fileName: String(legacyArgs.fileName),
|
||||
} as WorkspaceFileTarget)
|
||||
: undefined
|
||||
|
||||
const legacyEdit = (() => {
|
||||
const structured = legacyArgs.edit as Record<string, unknown> | undefined
|
||||
if (structured && typeof structured.mode === 'string') {
|
||||
return {
|
||||
strategy: 'anchored',
|
||||
mode: structured.mode as 'replace_between' | 'insert_after' | 'delete_between',
|
||||
occurrence:
|
||||
typeof structured.occurrence === 'number' ? structured.occurrence : undefined,
|
||||
before_anchor:
|
||||
typeof structured.before_anchor === 'string' ? structured.before_anchor : undefined,
|
||||
after_anchor:
|
||||
typeof structured.after_anchor === 'string' ? structured.after_anchor : undefined,
|
||||
start_anchor:
|
||||
typeof structured.start_anchor === 'string' ? structured.start_anchor : undefined,
|
||||
end_anchor:
|
||||
typeof structured.end_anchor === 'string' ? structured.end_anchor : undefined,
|
||||
anchor: typeof structured.anchor === 'string' ? structured.anchor : undefined,
|
||||
content: typeof structured.content === 'string' ? structured.content : undefined,
|
||||
} satisfies WorkspaceFileEdit
|
||||
}
|
||||
|
||||
const edits = legacyArgs.edits as Array<{ search?: unknown; replace?: unknown }> | undefined
|
||||
if (Array.isArray(edits) && edits.length > 0) {
|
||||
const first = edits[0]
|
||||
if (typeof first?.search === 'string' && typeof first?.replace === 'string') {
|
||||
return {
|
||||
strategy: 'search_replace',
|
||||
search: first.search,
|
||||
replace: first.replace,
|
||||
} satisfies WorkspaceFileEdit
|
||||
}
|
||||
}
|
||||
|
||||
return undefined
|
||||
})()
|
||||
|
||||
const normalizedOperation: WorkspaceFileOperation =
|
||||
(legacyOperation as string) === 'write'
|
||||
? legacyTarget?.kind === 'new_file'
|
||||
? 'create'
|
||||
: 'append'
|
||||
: (legacyOperation as WorkspaceFileOperation)
|
||||
|
||||
return {
|
||||
operation: normalizedOperation,
|
||||
target: legacyTarget,
|
||||
title: typeof legacyArgs.title === 'string' ? legacyArgs.title : undefined,
|
||||
content: typeof legacyArgs.content === 'string' ? legacyArgs.content : undefined,
|
||||
contentType: typeof legacyArgs.contentType === 'string' ? legacyArgs.contentType : undefined,
|
||||
newName: typeof legacyArgs.newName === 'string' ? legacyArgs.newName : undefined,
|
||||
edit: legacyEdit,
|
||||
}
|
||||
}
|
||||
|
||||
export const workspaceFileServerTool: BaseServerTool<WorkspaceFileArgs, WorkspaceFileResult> = {
|
||||
name: WorkspaceFile.id,
|
||||
async execute(
|
||||
@@ -250,7 +152,7 @@ export const workspaceFileServerTool: BaseServerTool<WorkspaceFileArgs, Workspac
|
||||
throw new Error('Authentication required')
|
||||
}
|
||||
|
||||
const normalized = normalizeWorkspaceFileParams(params)
|
||||
const normalized = params
|
||||
const { operation } = normalized
|
||||
const workspaceId = context.workspaceId
|
||||
|
||||
@@ -262,7 +164,7 @@ export const workspaceFileServerTool: BaseServerTool<WorkspaceFileArgs, Workspac
|
||||
switch (operation) {
|
||||
case 'create': {
|
||||
const target = normalized.target
|
||||
if (!target || target.kind != 'new_file') {
|
||||
if (!target || target.kind !== 'new_file') {
|
||||
return {
|
||||
success: false,
|
||||
message: 'create requires target.kind=new_file with target.fileName',
|
||||
@@ -342,7 +244,7 @@ export const workspaceFileServerTool: BaseServerTool<WorkspaceFileArgs, Workspac
|
||||
if (!existingFile) {
|
||||
return { success: false, message: `File with ID "${target.fileId}" not found` }
|
||||
}
|
||||
if (target.fileName && target.fileName != existingFile.name) {
|
||||
if (target.fileName && target.fileName !== existingFile.name) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Target mismatch: fileId "${target.fileId}" is "${existingFile.name}", not "${target.fileName}"`,
|
||||
@@ -369,7 +271,8 @@ export const workspaceFileServerTool: BaseServerTool<WorkspaceFileArgs, Workspac
|
||||
|
||||
const combinedBuffer = Buffer.from(combined, 'utf-8')
|
||||
assertServerToolNotAborted(context)
|
||||
const appendMime = docInfo.sourceMime || inferContentType(existingFile.name, normalized.contentType)
|
||||
const appendMime =
|
||||
docInfo.sourceMime || inferContentType(existingFile.name, normalized.contentType)
|
||||
await updateWorkspaceFileContent(
|
||||
workspaceId,
|
||||
existingFile.id,
|
||||
@@ -414,7 +317,7 @@ export const workspaceFileServerTool: BaseServerTool<WorkspaceFileArgs, Workspac
|
||||
if (!fileRecord) {
|
||||
return { success: false, message: `File with ID "${target.fileId}" not found` }
|
||||
}
|
||||
if (target.fileName && target.fileName != fileRecord.name) {
|
||||
if (target.fileName && target.fileName !== fileRecord.name) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Target mismatch: fileId "${target.fileId}" is "${fileRecord.name}", not "${target.fileName}"`,
|
||||
@@ -436,7 +339,8 @@ export const workspaceFileServerTool: BaseServerTool<WorkspaceFileArgs, Workspac
|
||||
|
||||
const fileBuffer = Buffer.from(normalized.content, 'utf-8')
|
||||
assertServerToolNotAborted(context)
|
||||
const updateMime = docInfo.sourceMime || inferContentType(fileRecord.name, normalized.contentType)
|
||||
const updateMime =
|
||||
docInfo.sourceMime || inferContentType(fileRecord.name, normalized.contentType)
|
||||
await updateWorkspaceFileContent(
|
||||
workspaceId,
|
||||
target.fileId,
|
||||
@@ -589,7 +493,11 @@ export const workspaceFileServerTool: BaseServerTool<WorkspaceFileArgs, Workspac
|
||||
}
|
||||
const before = findAnchorLine(normalized.edit.before_anchor)
|
||||
if (before.error) return { success: false, message: `Patch failed: ${before.error}` }
|
||||
const after = findAnchorLine(normalized.edit.after_anchor, defaultOccurrence, before.index)
|
||||
const after = findAnchorLine(
|
||||
normalized.edit.after_anchor,
|
||||
defaultOccurrence,
|
||||
before.index
|
||||
)
|
||||
if (after.error) return { success: false, message: `Patch failed: ${after.error}` }
|
||||
if (after.index <= before.index) {
|
||||
return {
|
||||
@@ -599,7 +507,7 @@ export const workspaceFileServerTool: BaseServerTool<WorkspaceFileArgs, Workspac
|
||||
}
|
||||
const newLines = [
|
||||
...lines.slice(0, before.index + 1),
|
||||
...((normalized.edit.content ?? '').split('\n')),
|
||||
...(normalized.edit.content ?? '').split('\n'),
|
||||
...lines.slice(after.index),
|
||||
]
|
||||
content = newLines.join('\n')
|
||||
@@ -611,7 +519,7 @@ export const workspaceFileServerTool: BaseServerTool<WorkspaceFileArgs, Workspac
|
||||
if (found.error) return { success: false, message: `Patch failed: ${found.error}` }
|
||||
const newLines = [
|
||||
...lines.slice(0, found.index + 1),
|
||||
...((normalized.edit.content ?? '').split('\n')),
|
||||
...(normalized.edit.content ?? '').split('\n'),
|
||||
...lines.slice(found.index + 1),
|
||||
]
|
||||
content = newLines.join('\n')
|
||||
|
||||
@@ -16,7 +16,10 @@ function formatErrors(errors: ErrorObject[] | null | undefined): string {
|
||||
.join('; ')
|
||||
}
|
||||
|
||||
function getValidator(toolName: string, schemaKind: 'parameters' | 'resultSchema'): ValidateFunction | null {
|
||||
function getValidator(
|
||||
toolName: string,
|
||||
schemaKind: 'parameters' | 'resultSchema'
|
||||
): ValidateFunction | null {
|
||||
const cacheKey = `${toolName}:${schemaKind}`
|
||||
const cached = validatorCache.get(cacheKey)
|
||||
if (cached) return cached
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { z } from 'zod'
|
||||
import {
|
||||
DownloadToWorkspaceFile,
|
||||
GenerateImage,
|
||||
@@ -21,6 +22,7 @@ import { getTriggerBlocksServerTool } from '@/lib/copilot/tools/server/blocks/ge
|
||||
import { searchDocumentationServerTool } from '@/lib/copilot/tools/server/docs/search-documentation'
|
||||
import { downloadToWorkspaceFileServerTool } from '@/lib/copilot/tools/server/files/download-to-workspace-file'
|
||||
import { workspaceFileServerTool } from '@/lib/copilot/tools/server/files/workspace-file'
|
||||
import { validateGeneratedToolPayload } from '@/lib/copilot/tools/server/generated-schema'
|
||||
import { generateImageServerTool } from '@/lib/copilot/tools/server/image/generate-image'
|
||||
import { getJobLogsServerTool } from '@/lib/copilot/tools/server/jobs/get-job-logs'
|
||||
import { knowledgeBaseServerTool } from '@/lib/copilot/tools/server/knowledge/knowledge-base'
|
||||
@@ -33,8 +35,6 @@ import { generateVisualizationServerTool } from '@/lib/copilot/tools/server/visu
|
||||
import { editWorkflowServerTool } from '@/lib/copilot/tools/server/workflow/edit-workflow'
|
||||
import { getExecutionSummaryServerTool } from '@/lib/copilot/tools/server/workflow/get-execution-summary'
|
||||
import { getWorkflowLogsServerTool } from '@/lib/copilot/tools/server/workflow/get-workflow-logs'
|
||||
import { z } from 'zod'
|
||||
import { validateGeneratedToolPayload } from '@/lib/copilot/tools/server/generated-schema'
|
||||
|
||||
export { ExecuteResponseSuccessSchema }
|
||||
export type ExecuteResponseSuccess = (typeof ExecuteResponseSuccessSchema)['_type']
|
||||
|
||||
Reference in New Issue
Block a user