Skip to content

Commit 35aae77

Browse files
committed
Fix patch tool
1 parent c0434ee commit 35aae77

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
@@ -38,7 +38,7 @@ import { Table } from '@/app/workspace/[workspaceId]/tables/[tableId]/components
3838
import { useUsageLimits } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/hooks'
3939
import { useWorkflowExecution } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution'
4040
import { useWorkflows } from '@/hooks/queries/workflows'
41-
import { useWorkspaceFiles } from '@/hooks/queries/workspace-files'
41+
import { useWorkspaceFileContent, useWorkspaceFiles } from '@/hooks/queries/workspace-files'
4242
import { useSettingsNavigation } from '@/hooks/use-settings-navigation'
4343
import { useExecutionStore } from '@/stores/execution/store'
4444
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
@@ -76,11 +76,41 @@ export const ResourceContent = memo(function ResourceContent({
7676
genericResourceData,
7777
}: ResourceContentProps) {
7878
const streamFileName = streamingFile?.fileName || 'file.md'
79+
80+
const isPatchStream = useMemo(() => {
81+
if (!streamingFile) return false
82+
return /"operation"\s*:\s*"patch"/.test(streamingFile.content)
83+
}, [streamingFile])
84+
85+
const { data: allFiles = [] } = useWorkspaceFiles(workspaceId)
86+
const activeFileRecord = useMemo(() => {
87+
if (!isPatchStream || resource.type !== 'file') return undefined
88+
return allFiles.find((f) => f.id === resource.id)
89+
}, [isPatchStream, resource, allFiles])
90+
91+
const isSourceMime =
92+
activeFileRecord?.type === 'text/x-pptxgenjs' ||
93+
activeFileRecord?.type === 'text/x-docxjs' ||
94+
activeFileRecord?.type === 'text/x-pdflibjs'
95+
96+
const { data: fetchedFileContent } = useWorkspaceFileContent(
97+
workspaceId,
98+
activeFileRecord?.id ?? '',
99+
activeFileRecord?.key ?? '',
100+
isSourceMime
101+
)
102+
79103
const streamingExtractedContent = useMemo(() => {
80104
if (!streamingFile) return undefined
81-
const extracted = extractFileContent(streamingFile.content)
105+
const raw = streamingFile.content
106+
107+
if (isPatchStream && fetchedFileContent) {
108+
return extractPatchPreview(raw, fetchedFileContent)
109+
}
110+
111+
const extracted = extractFileContent(raw)
82112
return extracted.length > 0 ? extracted : undefined
83-
}, [streamingFile])
113+
}, [streamingFile, isPatchStream, fetchedFileContent])
84114
const syntheticFile = useMemo(() => {
85115
const ext = getFileExtension(streamFileName)
86116
const SOURCE_MIME_MAP: Record<string, string> = {
@@ -486,3 +516,100 @@ function extractFileContent(raw: string): string {
486516
.replace(/\\u([0-9a-fA-F]{4})/g, (_, hex) => String.fromCharCode(Number.parseInt(hex, 16)))
487517
.replace(/\\\\/g, '\\')
488518
}
519+
520+
function extractJsonString(raw: string, key: string): string | undefined {
521+
const pattern = new RegExp(`"${key}"\\s*:\\s*"`)
522+
const m = pattern.exec(raw)
523+
if (!m) return undefined
524+
const start = m.index + m[0].length
525+
let end = -1
526+
for (let i = start; i < raw.length; i++) {
527+
if (raw[i] === '\\') {
528+
i++
529+
continue
530+
}
531+
if (raw[i] === '"') {
532+
end = i
533+
break
534+
}
535+
}
536+
if (end === -1) return undefined
537+
return raw
538+
.slice(start, end)
539+
.replace(/\\n/g, '\n')
540+
.replace(/\\t/g, '\t')
541+
.replace(/\\r/g, '\r')
542+
.replace(/\\"/g, '"')
543+
.replace(/\\u([0-9a-fA-F]{4})/g, (_, hex) => String.fromCharCode(Number.parseInt(hex, 16)))
544+
.replace(/\\\\/g, '\\')
545+
}
546+
547+
function findAnchorIndex(lines: string[], anchor: string, occurrence = 1, afterIndex = -1): number {
548+
const trimmed = anchor.trim()
549+
let count = 0
550+
for (let i = afterIndex + 1; i < lines.length; i++) {
551+
if (lines[i].trim() === trimmed) {
552+
count++
553+
if (count === occurrence) return i
554+
}
555+
}
556+
return -1
557+
}
558+
559+
function extractPatchPreview(raw: string, existingContent: string): string | undefined {
560+
const mode = extractJsonString(raw, 'mode')
561+
if (!mode) return undefined
562+
563+
const lines = existingContent.split('\n')
564+
const occurrenceMatch = raw.match(/"occurrence"\s*:\s*(\d+)/)
565+
const occurrence = occurrenceMatch ? Number.parseInt(occurrenceMatch[1], 10) : 1
566+
567+
if (mode === 'replace_between') {
568+
const beforeAnchor = extractJsonString(raw, 'before_anchor')
569+
const afterAnchor = extractJsonString(raw, 'after_anchor')
570+
if (!beforeAnchor || !afterAnchor) return undefined
571+
572+
const beforeIdx = findAnchorIndex(lines, beforeAnchor, occurrence)
573+
const afterIdx = findAnchorIndex(lines, afterAnchor, occurrence, beforeIdx)
574+
if (beforeIdx === -1 || afterIdx === -1 || afterIdx <= beforeIdx) return undefined
575+
576+
const newContent = extractFileContent(raw)
577+
const spliced = [
578+
...lines.slice(0, beforeIdx + 1),
579+
...(newContent.length > 0 ? newContent.split('\n') : []),
580+
...lines.slice(afterIdx),
581+
]
582+
return spliced.join('\n')
583+
}
584+
585+
if (mode === 'insert_after') {
586+
const anchor = extractJsonString(raw, 'anchor')
587+
if (!anchor) return undefined
588+
589+
const anchorIdx = findAnchorIndex(lines, anchor, occurrence)
590+
if (anchorIdx === -1) return undefined
591+
592+
const newContent = extractFileContent(raw)
593+
const spliced = [
594+
...lines.slice(0, anchorIdx + 1),
595+
...(newContent.length > 0 ? newContent.split('\n') : []),
596+
...lines.slice(anchorIdx + 1),
597+
]
598+
return spliced.join('\n')
599+
}
600+
601+
if (mode === 'delete_between') {
602+
const startAnchor = extractJsonString(raw, 'start_anchor')
603+
const endAnchor = extractJsonString(raw, 'end_anchor')
604+
if (!startAnchor || !endAnchor) return undefined
605+
606+
const startIdx = findAnchorIndex(lines, startAnchor, occurrence)
607+
const endIdx = findAnchorIndex(lines, endAnchor, occurrence, startIdx)
608+
if (startIdx === -1 || endIdx === -1 || endIdx <= startIdx) return undefined
609+
610+
const spliced = [...lines.slice(0, startIdx), ...lines.slice(endIdx)]
611+
return spliced.join('\n')
612+
}
613+
614+
return undefined
615+
}

0 commit comments

Comments
 (0)