Skip to content

Commit 89f8842

Browse files
committed
Fix patch tool
1 parent 5be55d2 commit 89f8842

2 files changed

Lines changed: 135 additions & 4 deletions

File tree

apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,8 +240,12 @@ function TextEditor({
240240

241241
useEffect(() => {
242242
if (streamingContent !== undefined) {
243+
const isSplicedFull =
244+
fetchedContent !== undefined &&
245+
streamingContent.length > fetchedContent.length * 0.5 &&
246+
streamingContent.startsWith(fetchedContent.slice(0, Math.min(100, fetchedContent.length)))
243247
const nextContent =
244-
fetchedContent === undefined
248+
fetchedContent === undefined || isSplicedFull
245249
? streamingContent
246250
: fetchedContent.endsWith(streamingContent) ||
247251
fetchedContent.endsWith(`\n${streamingContent}`)

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

Lines changed: 130 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ import { useUsageLimits } from '@/app/workspace/[workspaceId]/w/[workflowId]/com
4646
import { useWorkflowExecution } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution'
4747
import { useFolders } from '@/hooks/queries/folders'
4848
import { useWorkflows } from '@/hooks/queries/workflows'
49-
import { useWorkspaceFiles } from '@/hooks/queries/workspace-files'
49+
import { useWorkspaceFileContent, useWorkspaceFiles } from '@/hooks/queries/workspace-files'
5050
import { useSettingsNavigation } from '@/hooks/use-settings-navigation'
5151
import { useExecutionStore } from '@/stores/execution/store'
5252
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
@@ -84,11 +84,41 @@ export const ResourceContent = memo(function ResourceContent({
8484
genericResourceData,
8585
}: ResourceContentProps) {
8686
const streamFileName = streamingFile?.fileName || 'file.md'
87+
88+
const isPatchStream = useMemo(() => {
89+
if (!streamingFile) return false
90+
return /"operation"\s*:\s*"patch"/.test(streamingFile.content)
91+
}, [streamingFile])
92+
93+
const { data: allFiles = [] } = useWorkspaceFiles(workspaceId)
94+
const activeFileRecord = useMemo(() => {
95+
if (!isPatchStream || resource.type !== 'file') return undefined
96+
return allFiles.find((f) => f.id === resource.id)
97+
}, [isPatchStream, resource, allFiles])
98+
99+
const isSourceMime =
100+
activeFileRecord?.type === 'text/x-pptxgenjs' ||
101+
activeFileRecord?.type === 'text/x-docxjs' ||
102+
activeFileRecord?.type === 'text/x-pdflibjs'
103+
104+
const { data: fetchedFileContent } = useWorkspaceFileContent(
105+
workspaceId,
106+
activeFileRecord?.id ?? '',
107+
activeFileRecord?.key ?? '',
108+
isSourceMime
109+
)
110+
87111
const streamingExtractedContent = useMemo(() => {
88112
if (!streamingFile) return undefined
89-
const extracted = extractFileContent(streamingFile.content)
113+
const raw = streamingFile.content
114+
115+
if (isPatchStream && fetchedFileContent) {
116+
return extractPatchPreview(raw, fetchedFileContent)
117+
}
118+
119+
const extracted = extractFileContent(raw)
90120
return extracted.length > 0 ? extracted : undefined
91-
}, [streamingFile])
121+
}, [streamingFile, isPatchStream, fetchedFileContent])
92122
const syntheticFile = useMemo(() => {
93123
const ext = getFileExtension(streamFileName)
94124
const SOURCE_MIME_MAP: Record<string, string> = {
@@ -564,3 +594,100 @@ function extractFileContent(raw: string): string {
564594
.replace(/\\u([0-9a-fA-F]{4})/g, (_, hex) => String.fromCharCode(Number.parseInt(hex, 16)))
565595
.replace(/\\\\/g, '\\')
566596
}
597+
598+
function extractJsonString(raw: string, key: string): string | undefined {
599+
const pattern = new RegExp(`"${key}"\\s*:\\s*"`)
600+
const m = pattern.exec(raw)
601+
if (!m) return undefined
602+
const start = m.index + m[0].length
603+
let end = -1
604+
for (let i = start; i < raw.length; i++) {
605+
if (raw[i] === '\\') {
606+
i++
607+
continue
608+
}
609+
if (raw[i] === '"') {
610+
end = i
611+
break
612+
}
613+
}
614+
if (end === -1) return undefined
615+
return raw
616+
.slice(start, end)
617+
.replace(/\\n/g, '\n')
618+
.replace(/\\t/g, '\t')
619+
.replace(/\\r/g, '\r')
620+
.replace(/\\"/g, '"')
621+
.replace(/\\u([0-9a-fA-F]{4})/g, (_, hex) => String.fromCharCode(Number.parseInt(hex, 16)))
622+
.replace(/\\\\/g, '\\')
623+
}
624+
625+
function findAnchorIndex(lines: string[], anchor: string, occurrence = 1, afterIndex = -1): number {
626+
const trimmed = anchor.trim()
627+
let count = 0
628+
for (let i = afterIndex + 1; i < lines.length; i++) {
629+
if (lines[i].trim() === trimmed) {
630+
count++
631+
if (count === occurrence) return i
632+
}
633+
}
634+
return -1
635+
}
636+
637+
function extractPatchPreview(raw: string, existingContent: string): string | undefined {
638+
const mode = extractJsonString(raw, 'mode')
639+
if (!mode) return undefined
640+
641+
const lines = existingContent.split('\n')
642+
const occurrenceMatch = raw.match(/"occurrence"\s*:\s*(\d+)/)
643+
const occurrence = occurrenceMatch ? Number.parseInt(occurrenceMatch[1], 10) : 1
644+
645+
if (mode === 'replace_between') {
646+
const beforeAnchor = extractJsonString(raw, 'before_anchor')
647+
const afterAnchor = extractJsonString(raw, 'after_anchor')
648+
if (!beforeAnchor || !afterAnchor) return undefined
649+
650+
const beforeIdx = findAnchorIndex(lines, beforeAnchor, occurrence)
651+
const afterIdx = findAnchorIndex(lines, afterAnchor, occurrence, beforeIdx)
652+
if (beforeIdx === -1 || afterIdx === -1 || afterIdx <= beforeIdx) return undefined
653+
654+
const newContent = extractFileContent(raw)
655+
const spliced = [
656+
...lines.slice(0, beforeIdx + 1),
657+
...(newContent.length > 0 ? newContent.split('\n') : []),
658+
...lines.slice(afterIdx),
659+
]
660+
return spliced.join('\n')
661+
}
662+
663+
if (mode === 'insert_after') {
664+
const anchor = extractJsonString(raw, 'anchor')
665+
if (!anchor) return undefined
666+
667+
const anchorIdx = findAnchorIndex(lines, anchor, occurrence)
668+
if (anchorIdx === -1) return undefined
669+
670+
const newContent = extractFileContent(raw)
671+
const spliced = [
672+
...lines.slice(0, anchorIdx + 1),
673+
...(newContent.length > 0 ? newContent.split('\n') : []),
674+
...lines.slice(anchorIdx + 1),
675+
]
676+
return spliced.join('\n')
677+
}
678+
679+
if (mode === 'delete_between') {
680+
const startAnchor = extractJsonString(raw, 'start_anchor')
681+
const endAnchor = extractJsonString(raw, 'end_anchor')
682+
if (!startAnchor || !endAnchor) return undefined
683+
684+
const startIdx = findAnchorIndex(lines, startAnchor, occurrence)
685+
const endIdx = findAnchorIndex(lines, endAnchor, occurrence, startIdx)
686+
if (startIdx === -1 || endIdx === -1 || endIdx <= startIdx) return undefined
687+
688+
const spliced = [...lines.slice(0, startIdx), ...lines.slice(endIdx)]
689+
return spliced.join('\n')
690+
}
691+
692+
return undefined
693+
}

0 commit comments

Comments
 (0)