Skip to content

Commit ce1f00c

Browse files
committed
Checkpoint
1 parent 3893afd commit ce1f00c

File tree

25 files changed

+3749
-898
lines changed

25 files changed

+3749
-898
lines changed

apps/sim/app/api/mothership/chat/route.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,6 @@ export async function POST(req: NextRequest) {
189189
logger.error(`[${tracker.requestId}] Failed to process contexts`, e)
190190
}
191191
}
192-
}
193192
if (Array.isArray(resourceAttachments) && resourceAttachments.length > 0) {
194193
const results = await Promise.allSettled(
195194
resourceAttachments.map(async (r) => {

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,9 @@ function TextEditor({
254254
fetchedContent.endsWith(`\n${streamingContent}`)
255255
? fetchedContent
256256
: `${fetchedContent}\n${streamingContent}`
257+
// #region agent log
258+
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(()=>{});
259+
// #endregion
257260
setContent(nextContent)
258261
contentRef.current = nextContent
259262
initializedRef.current = true

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,12 @@ export function ToolCallItem({ toolName, displayTitle, status, streamingArgs }:
106106
if (!titleMatch?.[1]) return null
107107
const opMatch = streamingArgs.match(/"operation"\s*:\s*"(\w+)"/)
108108
const op = opMatch?.[1] ?? ''
109-
const verb = op === 'patch' || op === 'update' ? 'Editing' : 'Writing'
109+
const verb =
110+
op === 'patch' || op === 'update' || op === 'rename'
111+
? 'Editing'
112+
: op === 'delete'
113+
? 'Deleting'
114+
: 'Writing'
110115
const unescaped = titleMatch[1]
111116
.replace(/\\u([0-9a-fA-F]{4})/g, (_, hex: string) =>
112117
String.fromCharCode(Number.parseInt(hex, 16))

apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ function parseBlocks(blocks: ContentBlock[]): MessageSegment[] {
210210
if (block.type === 'tool_call') {
211211
if (!block.toolCall) continue
212212
const tc = block.toolCall
213-
if (tc.name === ToolSearchToolRegex.id || tc.name === 'set_file_context') continue
213+
if (tc.name === ToolSearchToolRegex.id) continue
214214
if (tc.name === ReadTool.id && isToolResultRead(tc.params)) continue
215215
const isDispatch = SUBAGENT_KEYS.has(tc.name) && !tc.calledBy
216216

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

Lines changed: 77 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,15 @@ interface ResourceContentProps {
6565
workspaceId: string
6666
resource: MothershipResource
6767
previewMode?: PreviewMode
68-
streamingFile?: { fileName: string; fileId?: string; content: string } | null
68+
streamingFile?: {
69+
toolCallId?: string
70+
fileName: string
71+
fileId?: string
72+
targetKind?: 'new_file' | 'file_id'
73+
operation?: string
74+
edit?: Record<string, unknown>
75+
content: string
76+
} | null
6977
genericResourceData?: GenericResourceData
7078
}
7179

@@ -87,11 +95,10 @@ export const ResourceContent = memo(function ResourceContent({
8795

8896
const streamOperation = useMemo(() => {
8997
if (!streamingFile) return undefined
90-
const m = streamingFile.content.match(/"operation"\s*:\s*"(\w+)"/)
91-
return m?.[1]
98+
return streamingFile.operation
9299
}, [streamingFile])
93100

94-
const isWriteStream = streamOperation === 'write'
101+
const isWriteStream = streamOperation === 'create' || streamOperation === 'append'
95102
const isPatchStream = streamOperation === 'patch'
96103
const isUpdateStream = streamOperation === 'update'
97104

@@ -113,24 +120,36 @@ export const ResourceContent = memo(function ResourceContent({
113120
isSourceMime
114121
)
115122

123+
// #region agent log
124+
if (streamingFile) {
125+
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(()=>{});
126+
}
127+
// #endregion
116128
const streamingExtractedContent = useMemo(() => {
117129
if (!streamingFile) return undefined
118-
const raw = streamingFile.content
119-
120-
// Do not guess. Until the operation key has streamed in, we don't know
121-
// whether the payload should append, replace, or splice into the file.
122-
// Rendering early here can show content at the end of the file and then
123-
// "snap" to the right place once the operation/mode becomes known.
124130
if (!streamOperation) return undefined
125131

126132
if (isPatchStream) {
127-
if (!fetchedFileContent) return undefined
128-
return extractPatchPreview(raw, fetchedFileContent)
133+
if (!fetchedFileContent) {
134+
// #region agent log
135+
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(()=>{});
136+
// #endregion
137+
return undefined
138+
}
139+
const patchResult = extractPatchPreview(streamingFile, fetchedFileContent)
140+
// #region agent log
141+
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(()=>{});
142+
// #endregion
143+
return patchResult
129144
}
130145

131-
const extracted = extractFileContent(raw)
146+
const extracted = streamingFile.content
132147
if (extracted.length === 0) return undefined
133148

149+
// #region agent log
150+
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(()=>{});
151+
// #endregion
152+
134153
if (isUpdateStream) return extracted
135154
if (isWriteStream) return extracted
136155

@@ -160,6 +179,15 @@ export const ResourceContent = memo(function ResourceContent({
160179
const streamingFileMode: 'append' | 'replace' =
161180
isWriteStream ? 'append' : 'replace'
162181

182+
// For existing file resources (not streaming-file), only pass streaming
183+
// content for patch operations where the preview splices new content into
184+
// the displayed file. Update operations re-stream the entire file from
185+
// scratch which causes visual duplication of already-visible content.
186+
const embeddedStreamingContent =
187+
resource.id !== 'streaming-file' && isUpdateStream
188+
? undefined
189+
: streamingExtractedContent
190+
163191
if (streamingFile && resource.id === 'streaming-file') {
164192
return (
165193
<div className='flex h-full flex-col overflow-hidden'>
@@ -192,7 +220,7 @@ export const ResourceContent = memo(function ResourceContent({
192220
workspaceId={workspaceId}
193221
fileId={resource.id}
194222
previewMode={previewMode}
195-
streamingContent={streamingExtractedContent}
223+
streamingContent={embeddedStreamingContent}
196224
streamingMode={streamingFileMode}
197225
/>
198226
)
@@ -587,65 +615,6 @@ function EmbeddedFolder({ workspaceId, folderId }: EmbeddedFolderProps) {
587615
)
588616
}
589617

590-
function extractFileContent(raw: string): string {
591-
const marker = '"content":'
592-
const idx = raw.indexOf(marker)
593-
if (idx === -1) return ''
594-
const rest = raw.slice(idx + marker.length).trimStart()
595-
if (!rest.startsWith('"')) return rest
596-
597-
// Walk the JSON string value to find the unescaped closing quote.
598-
// While streaming, the closing quote may not have arrived yet — in that
599-
// case we treat everything received so far as the content (no trim).
600-
let end = -1
601-
for (let i = 1; i < rest.length; i++) {
602-
if (rest[i] === '\\') {
603-
i++ // skip escaped character
604-
continue
605-
}
606-
if (rest[i] === '"') {
607-
end = i
608-
break
609-
}
610-
}
611-
612-
const inner = end === -1 ? rest.slice(1) : rest.slice(1, end)
613-
return inner
614-
.replace(/\\n/g, '\n')
615-
.replace(/\\t/g, '\t')
616-
.replace(/\\r/g, '\r')
617-
.replace(/\\"/g, '"')
618-
.replace(/\\u([0-9a-fA-F]{4})/g, (_, hex) => String.fromCharCode(Number.parseInt(hex, 16)))
619-
.replace(/\\\\/g, '\\')
620-
}
621-
622-
function extractJsonString(raw: string, key: string): string | undefined {
623-
const pattern = new RegExp(`"${key}"\\s*:\\s*"`)
624-
const m = pattern.exec(raw)
625-
if (!m) return undefined
626-
const start = m.index + m[0].length
627-
let end = -1
628-
for (let i = start; i < raw.length; i++) {
629-
if (raw[i] === '\\') {
630-
i++
631-
continue
632-
}
633-
if (raw[i] === '"') {
634-
end = i
635-
break
636-
}
637-
}
638-
if (end === -1) return undefined
639-
return raw
640-
.slice(start, end)
641-
.replace(/\\n/g, '\n')
642-
.replace(/\\t/g, '\t')
643-
.replace(/\\r/g, '\r')
644-
.replace(/\\"/g, '"')
645-
.replace(/\\u([0-9a-fA-F]{4})/g, (_, hex) => String.fromCharCode(Number.parseInt(hex, 16)))
646-
.replace(/\\\\/g, '\\')
647-
}
648-
649618
function findAnchorIndex(lines: string[], anchor: string, occurrence = 1, afterIndex = -1): number {
650619
const trimmed = anchor.trim()
651620
let count = 0
@@ -658,24 +627,46 @@ function findAnchorIndex(lines: string[], anchor: string, occurrence = 1, afterI
658627
return -1
659628
}
660629

661-
function extractPatchPreview(raw: string, existingContent: string): string | undefined {
662-
const mode = extractJsonString(raw, 'mode')
663-
if (!mode) return undefined
664-
630+
function extractPatchPreview(
631+
streamingFile: {
632+
content: string
633+
edit?: Record<string, unknown>
634+
},
635+
existingContent: string
636+
): string | undefined {
637+
const edit = streamingFile.edit ?? {}
638+
const strategy = typeof edit.strategy === 'string' ? edit.strategy : undefined
665639
const lines = existingContent.split('\n')
666-
const occurrenceMatch = raw.match(/"occurrence"\s*:\s*(\d+)/)
667-
const occurrence = occurrenceMatch ? Number.parseInt(occurrenceMatch[1], 10) : 1
640+
const occurrence =
641+
typeof edit.occurrence === 'number' && Number.isFinite(edit.occurrence)
642+
? edit.occurrence
643+
: 1
644+
645+
if (strategy === 'search_replace') {
646+
const search = typeof edit.search === 'string' ? edit.search : ''
647+
if (!search) return undefined
648+
const replace = streamingFile.content
649+
if ((edit.replaceAll as boolean | undefined) === true) {
650+
return existingContent.split(search).join(replace)
651+
}
652+
const firstIdx = existingContent.indexOf(search)
653+
if (firstIdx === -1) return undefined
654+
return existingContent.slice(0, firstIdx) + replace + existingContent.slice(firstIdx + search.length)
655+
}
656+
657+
const mode = typeof edit.mode === 'string' ? edit.mode : undefined
658+
if (!mode) return undefined
668659

669660
if (mode === 'replace_between') {
670-
const beforeAnchor = extractJsonString(raw, 'before_anchor')
671-
const afterAnchor = extractJsonString(raw, 'after_anchor')
661+
const beforeAnchor = typeof edit.before_anchor === 'string' ? edit.before_anchor : undefined
662+
const afterAnchor = typeof edit.after_anchor === 'string' ? edit.after_anchor : undefined
672663
if (!beforeAnchor || !afterAnchor) return undefined
673664

674665
const beforeIdx = findAnchorIndex(lines, beforeAnchor, occurrence)
675666
const afterIdx = findAnchorIndex(lines, afterAnchor, occurrence, beforeIdx)
676667
if (beforeIdx === -1 || afterIdx === -1 || afterIdx <= beforeIdx) return undefined
677668

678-
const newContent = extractFileContent(raw)
669+
const newContent = streamingFile.content
679670
const spliced = [
680671
...lines.slice(0, beforeIdx + 1),
681672
...(newContent.length > 0 ? newContent.split('\n') : []),
@@ -685,13 +676,13 @@ function extractPatchPreview(raw: string, existingContent: string): string | und
685676
}
686677

687678
if (mode === 'insert_after') {
688-
const anchor = extractJsonString(raw, 'anchor')
679+
const anchor = typeof edit.anchor === 'string' ? edit.anchor : undefined
689680
if (!anchor) return undefined
690681

691682
const anchorIdx = findAnchorIndex(lines, anchor, occurrence)
692683
if (anchorIdx === -1) return undefined
693684

694-
const newContent = extractFileContent(raw)
685+
const newContent = streamingFile.content
695686
const spliced = [
696687
...lines.slice(0, anchorIdx + 1),
697688
...(newContent.length > 0 ? newContent.split('\n') : []),
@@ -701,8 +692,8 @@ function extractPatchPreview(raw: string, existingContent: string): string | und
701692
}
702693

703694
if (mode === 'delete_between') {
704-
const startAnchor = extractJsonString(raw, 'start_anchor')
705-
const endAnchor = extractJsonString(raw, 'end_anchor')
695+
const startAnchor = typeof edit.start_anchor === 'string' ? edit.start_anchor : undefined
696+
const endAnchor = typeof edit.end_anchor === 'string' ? edit.end_anchor : undefined
706697
if (!startAnchor || !endAnchor) return undefined
707698

708699
const startIdx = findAnchorIndex(lines, startAnchor, occurrence)

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

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,30 +19,29 @@ const PREVIEW_CYCLE: Record<PreviewMode, PreviewMode> = {
1919
preview: 'editor',
2020
} as const
2121

22-
function streamFileBasename(name: string): string {
23-
const n = name.replace(/\\/g, '/').trim()
24-
const parts = n.split('/').filter(Boolean)
25-
return parts.length ? parts[parts.length - 1]! : n
26-
}
27-
28-
function fileTitlesEquivalent(streamFileName: string, resourceTitle: string): boolean {
29-
return streamFileBasename(streamFileName) === streamFileBasename(resourceTitle)
30-
}
31-
3222
/**
3323
* Whether the active resource should show the in-progress file stream.
34-
* The synthetic `streaming-file` tab always shows it; a real file tab shows it when
35-
* the streamed `fileName` matches that resource (so users who stay on the open file see live text).
24+
* The synthetic `streaming-file` tab always shows it; a real file tab only shows it
25+
* when the streamed fileId matches that exact resource.
3626
*/
3727
function shouldShowStreamingFilePanel(
38-
streamingFile: { fileName: string; fileId?: string; content: string } | null | undefined,
28+
streamingFile:
29+
| {
30+
toolCallId?: string
31+
fileName: string
32+
fileId?: string
33+
targetKind?: 'new_file' | 'file_id'
34+
operation?: string
35+
edit?: Record<string, unknown>
36+
content: string
37+
}
38+
| null
39+
| undefined,
3940
active: MothershipResource | null
4041
): boolean {
4142
if (!streamingFile || !active) return false
4243
if (active.id === 'streaming-file') return true
4344
if (active.type !== 'file') return false
44-
const fn = streamingFile.fileName.trim()
45-
if (fn && fileTitlesEquivalent(fn, active.title)) return true
4645
if (active.id && streamingFile.fileId === active.id) return true
4746
return false
4847
}
@@ -59,7 +58,17 @@ interface MothershipViewProps {
5958
onCollapse: () => void
6059
isCollapsed: boolean
6160
className?: string
62-
streamingFile?: { fileName: string; fileId?: string; content: string } | null
61+
streamingFile?:
62+
| {
63+
toolCallId?: string
64+
fileName: string
65+
fileId?: string
66+
targetKind?: 'new_file' | 'file_id'
67+
operation?: string
68+
edit?: Record<string, unknown>
69+
content: string
70+
}
71+
| null
6372
genericResourceData?: GenericResourceData
6473
}
6574

0 commit comments

Comments
 (0)