File writes

This commit is contained in:
Siddharth Ganesan
2026-04-08 16:39:02 -07:00
parent ce1f00c8a3
commit fd4fa1ce8d
13 changed files with 4786 additions and 3071 deletions

View File

@@ -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)

View File

@@ -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

View File

@@ -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'

View File

@@ -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
}

View File

@@ -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

View File

@@ -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 } : {}),
})

View File

@@ -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 {

View File

@@ -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' }]
}

View 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')

View File

@@ -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

View File

@@ -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']