Skip to content

Commit c0434ee

Browse files
committed
Patch
1 parent 46490ae commit c0434ee

3 files changed

Lines changed: 137 additions & 21 deletions

File tree

apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-call-item.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,13 +104,16 @@ export function ToolCallItem({ toolName, displayTitle, status, streamingArgs }:
104104
if (toolName !== WorkspaceFile.id || !streamingArgs) return null
105105
const titleMatch = streamingArgs.match(/"title"\s*:\s*"([^"]+)"/)
106106
if (!titleMatch?.[1]) return null
107+
const opMatch = streamingArgs.match(/"operation"\s*:\s*"(\w+)"/)
108+
const op = opMatch?.[1] ?? ''
109+
const verb = op === 'patch' || op === 'update' ? 'Editing' : 'Writing'
107110
const unescaped = titleMatch[1]
108111
.replace(/\\u([0-9a-fA-F]{4})/g, (_, hex: string) =>
109112
String.fromCharCode(Number.parseInt(hex, 16))
110113
)
111114
.replace(/\\"/g, '"')
112115
.replace(/\\\\/g, '\\')
113-
return `Writing ${unescaped}`
116+
return `${verb} ${unescaped}`
114117
}, [toolName, streamingArgs])
115118
const extracted = useMemo(() => {
116119
if (toolName !== FunctionExecute.id || !streamingArgs) return null

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -942,6 +942,9 @@ export function useChat(
942942
tc.streamingArgs = (tc.streamingArgs ?? '') + delta
943943

944944
if (tc.name === WorkspaceFile.id) {
945+
const opMatch = tc.streamingArgs.match(/"operation"\s*:\s*"(\w+)"/)
946+
const op = opMatch?.[1] ?? ''
947+
const verb = op === 'patch' || op === 'update' ? 'Editing' : 'Writing'
945948
const titleMatch = tc.streamingArgs.match(/"title"\s*:\s*"([^"]*)"/)
946949
if (titleMatch?.[1]) {
947950
const unescaped = titleMatch[1]
@@ -950,7 +953,7 @@ export function useChat(
950953
)
951954
.replace(/\\"/g, '"')
952955
.replace(/\\\\/g, '\\')
953-
tc.displayTitle = `Writing ${unescaped}`
956+
tc.displayTitle = `${verb} ${unescaped}`
954957
}
955958
}
956959

@@ -1120,12 +1123,14 @@ export function useChat(
11201123
| undefined
11211124

11221125
if (name === WorkspaceFile.id) {
1126+
const operation = typeof args?.operation === 'string' ? args.operation : ''
1127+
const verb = operation === 'patch' || operation === 'update' ? 'Editing' : 'Writing'
11231128
const innerArgs = args ? asPayloadRecord(args.args) : undefined
11241129
const chunkTitle = innerArgs?.title as string | undefined
11251130
if (chunkTitle) {
1126-
displayTitle = `Writing ${chunkTitle}`
1131+
displayTitle = `${verb} ${chunkTitle}`
11271132
} else if (activeFileContextRef.current?.fileName) {
1128-
displayTitle = `Writing ${activeFileContextRef.current.fileName}`
1133+
displayTitle = `${verb} ${activeFileContextRef.current.fileName}`
11291134
}
11301135
}
11311136

apps/sim/lib/copilot/tools/server/files/workspace-file.ts

Lines changed: 125 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -365,16 +365,25 @@ export const workspaceFileServerTool: BaseServerTool<WorkspaceFileArgs, Workspac
365365

366366
case 'patch': {
367367
const fileId = (args as Record<string, unknown>).fileId as string | undefined
368-
const edits = (args as Record<string, unknown>).edits as
368+
const edit = (args as Record<string, unknown>).edit as
369+
| {
370+
mode: string
371+
before_anchor?: string
372+
after_anchor?: string
373+
start_anchor?: string
374+
end_anchor?: string
375+
anchor?: string
376+
content?: string
377+
occurrence?: number
378+
}
379+
| undefined
380+
const legacyEdits = (args as Record<string, unknown>).edits as
369381
| { search: string; replace: string }[]
370382
| undefined
371383

372384
if (!fileId) {
373385
return { success: false, message: 'fileId is required for patch operation' }
374386
}
375-
if (!edits || !Array.isArray(edits) || edits.length === 0) {
376-
return { success: false, message: 'edits array is required for patch operation' }
377-
}
378387

379388
const fileRecord = await getWorkspaceFile(workspaceId, fileId)
380389
if (!fileRecord) {
@@ -384,24 +393,122 @@ export const workspaceFileServerTool: BaseServerTool<WorkspaceFileArgs, Workspac
384393
const currentBuffer = await downloadWsFile(fileRecord)
385394
let content = currentBuffer.toString('utf-8')
386395

387-
for (const edit of edits) {
388-
const firstIdx = content.indexOf(edit.search)
389-
if (firstIdx === -1) {
396+
if (edit && typeof edit.mode === 'string') {
397+
const lines = content.split('\n')
398+
399+
const defaultOccurrence = edit.occurrence ?? 1
400+
401+
const findAnchorLine = (
402+
anchor: string,
403+
occurrence = defaultOccurrence,
404+
afterIndex = -1
405+
): { index: number; error?: string } => {
406+
const trimmed = anchor.trim()
407+
let count = 0
408+
for (let i = afterIndex + 1; i < lines.length; i++) {
409+
if (lines[i].trim() === trimmed) {
410+
count++
411+
if (count === occurrence) return { index: i }
412+
}
413+
}
414+
if (count === 0) {
415+
return {
416+
index: -1,
417+
error: `Anchor line not found in "${fileRecord.name}": "${anchor.slice(0, 100)}"`,
418+
}
419+
}
390420
return {
391-
success: false,
392-
message: `Patch failed: search string not found in file "${fileRecord.name}". Search: "${edit.search.slice(0, 100)}${edit.search.length > 100 ? '...' : ''}"`,
421+
index: -1,
422+
error: `Anchor line occurrence ${occurrence} not found (only ${count} match${count > 1 ? 'es' : ''}) in "${fileRecord.name}": "${anchor.slice(0, 100)}"`,
393423
}
394424
}
395-
if (content.indexOf(edit.search, firstIdx + 1) !== -1) {
425+
426+
if (edit.mode === 'replace_between') {
427+
if (!edit.before_anchor || !edit.after_anchor) {
428+
return {
429+
success: false,
430+
message: 'replace_between requires before_anchor and after_anchor',
431+
}
432+
}
433+
const before = findAnchorLine(edit.before_anchor)
434+
if (before.error) return { success: false, message: `Patch failed: ${before.error}` }
435+
const after = findAnchorLine(edit.after_anchor, defaultOccurrence, before.index)
436+
if (after.error) return { success: false, message: `Patch failed: ${after.error}` }
437+
if (after.index <= before.index) {
438+
return {
439+
success: false,
440+
message: 'Patch failed: after_anchor must appear after before_anchor in the file',
441+
}
442+
}
443+
444+
const newLines = [
445+
...lines.slice(0, before.index + 1),
446+
...(edit.content ?? '').split('\n'),
447+
...lines.slice(after.index),
448+
]
449+
content = newLines.join('\n')
450+
} else if (edit.mode === 'insert_after') {
451+
if (!edit.anchor) {
452+
return { success: false, message: 'insert_after requires anchor' }
453+
}
454+
const found = findAnchorLine(edit.anchor)
455+
if (found.error) return { success: false, message: `Patch failed: ${found.error}` }
456+
457+
const newLines = [
458+
...lines.slice(0, found.index + 1),
459+
...(edit.content ?? '').split('\n'),
460+
...lines.slice(found.index + 1),
461+
]
462+
content = newLines.join('\n')
463+
} else if (edit.mode === 'delete_between') {
464+
if (!edit.start_anchor || !edit.end_anchor) {
465+
return {
466+
success: false,
467+
message: 'delete_between requires start_anchor and end_anchor',
468+
}
469+
}
470+
const start = findAnchorLine(edit.start_anchor)
471+
if (start.error) return { success: false, message: `Patch failed: ${start.error}` }
472+
const end = findAnchorLine(edit.end_anchor, defaultOccurrence, start.index)
473+
if (end.error) return { success: false, message: `Patch failed: ${end.error}` }
474+
if (end.index <= start.index) {
475+
return {
476+
success: false,
477+
message: 'Patch failed: end_anchor must appear after start_anchor in the file',
478+
}
479+
}
480+
481+
const newLines = [...lines.slice(0, start.index), ...lines.slice(end.index)]
482+
content = newLines.join('\n')
483+
} else {
396484
return {
397485
success: false,
398-
message: `Patch failed: search string is ambiguous — found at multiple locations in "${fileRecord.name}". Use a longer, unique search string.`,
486+
message: `Unknown edit mode: "${edit.mode}". Use "replace_between", "insert_after", or "delete_between".`,
487+
}
488+
}
489+
} else if (legacyEdits && Array.isArray(legacyEdits) && legacyEdits.length > 0) {
490+
for (const le of legacyEdits) {
491+
const firstIdx = content.indexOf(le.search)
492+
if (firstIdx === -1) {
493+
return {
494+
success: false,
495+
message: `Patch failed: search string not found in file "${fileRecord.name}". Search: "${le.search.slice(0, 100)}${le.search.length > 100 ? '...' : ''}"`,
496+
}
497+
}
498+
if (content.indexOf(le.search, firstIdx + 1) !== -1) {
499+
return {
500+
success: false,
501+
message: `Patch failed: search string is ambiguous — found at multiple locations in "${fileRecord.name}". Use a longer, unique search string.`,
502+
}
399503
}
504+
content =
505+
content.slice(0, firstIdx) + le.replace + content.slice(firstIdx + le.search.length)
506+
}
507+
} else {
508+
return {
509+
success: false,
510+
message: 'patch requires either an edit object (with mode) or a legacy edits array',
400511
}
401-
content =
402-
content.slice(0, firstIdx) +
403-
edit.replace +
404-
content.slice(firstIdx + edit.search.length)
405512
}
406513

407514
const patchLowerName = fileRecord.name?.toLowerCase() ?? ''
@@ -445,16 +552,17 @@ export const workspaceFileServerTool: BaseServerTool<WorkspaceFileArgs, Workspac
445552
patchSourceMime
446553
)
447554

555+
const editMode = edit?.mode ?? 'legacy'
448556
logger.info('Workspace file patched via copilot', {
449557
fileId,
450558
name: fileRecord.name,
451-
editCount: edits.length,
559+
editMode,
452560
userId: context.userId,
453561
})
454562

455563
return {
456564
success: true,
457-
message: `File "${fileRecord.name}" patched successfully (${edits.length} edit${edits.length > 1 ? 's' : ''} applied)`,
565+
message: `File "${fileRecord.name}" patched successfully (${editMode} edit applied)`,
458566
data: {
459567
id: fileId,
460568
name: fileRecord.name,

0 commit comments

Comments
 (0)