Skip to content

Commit e610df6

Browse files
committed
Merge branch 'dev' of github.com:simstudioai/sim into dev
2 parents c77f204 + 6f04c48 commit e610df6

File tree

17 files changed

+5380
-3227
lines changed

17 files changed

+5380
-3227
lines changed

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

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -107,11 +107,19 @@ export function ToolCallItem({ toolName, displayTitle, status, streamingArgs }:
107107
const opMatch = streamingArgs.match(/"operation"\s*:\s*"(\w+)"/)
108108
const op = opMatch?.[1] ?? ''
109109
const verb =
110-
op === 'patch' || op === 'update' || op === 'rename'
111-
? 'Editing'
112-
: op === 'delete'
113-
? 'Deleting'
114-
: 'Writing'
110+
op === 'create'
111+
? 'Creating'
112+
: op === 'append'
113+
? 'Adding'
114+
: op === 'patch'
115+
? 'Editing'
116+
: op === 'update'
117+
? 'Writing'
118+
: op === 'rename'
119+
? 'Renaming'
120+
: op === 'delete'
121+
? 'Deleting'
122+
: 'Writing'
115123
const unescaped = titleMatch[1]
116124
.replace(/\\u([0-9a-fA-F]{4})/g, (_, hex: string) =>
117125
String.fromCharCode(Number.parseInt(hex, 16))

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ const MARKDOWN_COMPONENTS = {
155155
},
156156
inlineCode({ children }: { children?: React.ReactNode }) {
157157
return (
158-
<code className='rounded bg-[var(--surface-5)] px-1.5 py-0.5 font-mono text-small font-[400] text-[var(--text-primary)] before:content-none after:content-none'>
158+
<code className='rounded bg-[var(--surface-5)] px-1.5 py-0.5 font-[400] font-mono text-[var(--text-primary)] text-small before:content-none after:content-none'>
159159
{children}
160160
</code>
161161
)

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ const TOOL_ICONS: Record<string, IconComponent> = {
4141
superagent: Blimp,
4242
user_table: TableIcon,
4343
workspace_file: File,
44+
edit_content: File,
4445
create_workflow: Layout,
4546
edit_workflow: Pencil,
4647
workflow: Hammer,

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

Lines changed: 54 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -103,20 +103,22 @@ export const ResourceContent = memo(function ResourceContent({
103103
const isUpdateStream = streamOperation === 'update'
104104

105105
const { data: allFiles = [] } = useWorkspaceFiles(workspaceId)
106-
const activeFileRecord = useMemo(() => {
107-
if (!isPatchStream || resource.type !== 'file') return undefined
108-
return allFiles.find((f) => f.id === resource.id)
109-
}, [isPatchStream, resource, allFiles])
106+
const previewFileId =
107+
streamingFile?.fileId ?? (resource.type === 'file' ? resource.id : undefined)
108+
const previewFileRecord = useMemo(() => {
109+
if (!previewFileId) return undefined
110+
return allFiles.find((f) => f.id === previewFileId)
111+
}, [previewFileId, allFiles])
110112

111113
const isSourceMime =
112-
activeFileRecord?.type === 'text/x-pptxgenjs' ||
113-
activeFileRecord?.type === 'text/x-docxjs' ||
114-
activeFileRecord?.type === 'text/x-pdflibjs'
114+
previewFileRecord?.type === 'text/x-pptxgenjs' ||
115+
previewFileRecord?.type === 'text/x-docxjs' ||
116+
previewFileRecord?.type === 'text/x-pdflibjs'
115117

116118
const { data: fetchedFileContent } = useWorkspaceFileContent(
117119
workspaceId,
118-
activeFileRecord?.id ?? '',
119-
activeFileRecord?.key ?? '',
120+
previewFileRecord?.id ?? '',
121+
previewFileRecord?.key ?? '',
120122
isSourceMime
121123
)
122124

@@ -125,15 +127,28 @@ export const ResourceContent = memo(function ResourceContent({
125127
if (!streamOperation) return undefined
126128

127129
if (isPatchStream) {
128-
if (!fetchedFileContent) return undefined
130+
if (fetchedFileContent === undefined) return undefined
131+
if (!shouldApplyPatchPreview(streamingFile)) return undefined
129132
return extractPatchPreview(streamingFile, fetchedFileContent)
130133
}
131134

132135
const extracted = streamingFile.content
133-
if (extracted.length === 0) return undefined
134136

135137
if (isUpdateStream) return extracted
136-
if (isWriteStream) return extracted
138+
139+
if (streamOperation === 'append') {
140+
if (streamingFile.targetKind === 'file_id') {
141+
if (fetchedFileContent === undefined) return undefined
142+
return buildAppendPreview(fetchedFileContent, extracted)
143+
}
144+
return extracted.length > 0 ? extracted : undefined
145+
}
146+
147+
if (streamOperation === 'create') {
148+
return extracted.length > 0 ? extracted : undefined
149+
}
150+
151+
if (isWriteStream) return extracted.length > 0 ? extracted : undefined
137152

138153
return undefined
139154
}, [
@@ -165,16 +180,11 @@ export const ResourceContent = memo(function ResourceContent({
165180
}
166181
}, [workspaceId, streamFileName])
167182

168-
// workspace_file preview events now carry whole-file snapshots, not deltas.
169-
// Treat every live preview as replace so the viewer shows the latest snapshot.
183+
// ResourceContent now reconstructs full-file preview text per operation,
184+
// so the viewer can always treat streaming content as a whole-file replace.
170185
const streamingFileMode: 'append' | 'replace' = 'replace'
171186

172-
// For existing file resources (not streaming-file), only pass streaming
173-
// content for patch operations where the preview splices new content into
174-
// the displayed file. Update operations re-stream the entire file from
175-
// scratch which causes visual duplication of already-visible content.
176-
const embeddedStreamingContent =
177-
resource.id !== 'streaming-file' && isUpdateStream ? undefined : streamingExtractedContent
187+
const embeddedStreamingContent = streamingExtractedContent
178188

179189
if (streamingFile && resource.id === 'streaming-file') {
180190
return (
@@ -700,3 +710,27 @@ function extractPatchPreview(
700710

701711
return undefined
702712
}
713+
714+
function shouldApplyPatchPreview(streamingFile: {
715+
content: string
716+
edit?: Record<string, unknown>
717+
}): boolean {
718+
const edit = streamingFile.edit ?? {}
719+
const strategy = typeof edit.strategy === 'string' ? edit.strategy : undefined
720+
const mode = typeof edit.mode === 'string' ? edit.mode : undefined
721+
722+
// delete_between is delete-only and can be previewed from intent metadata alone.
723+
if (strategy === 'anchored' && mode === 'delete_between') {
724+
return true
725+
}
726+
727+
// For all other patch modes, keep the visible file unchanged until
728+
// edit_content actually streams content into the target location.
729+
return streamingFile.content.length > 0
730+
}
731+
732+
function buildAppendPreview(existingContent: string, incomingContent: string): string {
733+
if (incomingContent.length === 0) return existingContent
734+
if (existingContent.length === 0) return incomingContent
735+
return `${existingContent}\n${incomingContent}`
736+
}

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

Lines changed: 79 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,7 @@ export function useChat(
354354
streamingFileRef.current = streamingFile
355355
const filePreviewSessionsRef = useRef<Map<string, StreamingFilePreview>>(new Map())
356356
const activeFilePreviewToolCallIdRef = useRef<string | null>(null)
357+
const editContentParentToolCallIdRef = useRef<Map<string, string>>(new Map())
357358

358359
const [messageQueue, setMessageQueue] = useState<QueuedMessage[]>([])
359360
const messageQueueRef = useRef<QueuedMessage[]>([])
@@ -368,6 +369,15 @@ export function useChat(
368369
options?: { preserveExistingState?: boolean }
369370
) => Promise<{ sawStreamError: boolean; sawComplete: boolean }>
370371
>(async () => ({ sawStreamError: false, sawComplete: false }))
372+
const attachToExistingStreamRef = useRef<
373+
(opts: {
374+
streamId: string
375+
assistantId: string
376+
expectedGen: number
377+
initialBatch?: StreamBatchResponse | null
378+
afterCursor?: string
379+
}) => Promise<{ error: boolean; aborted: boolean }>
380+
>(async () => ({ error: false, aborted: true }))
371381
const retryReconnectRef = useRef<
372382
(opts: { streamId: string; assistantId: string; gen: number }) => Promise<boolean>
373383
>(async () => false)
@@ -518,6 +528,7 @@ export function useChat(
518528
streamingFileRef.current = null
519529
filePreviewSessionsRef.current.clear()
520530
activeFilePreviewToolCallIdRef.current = null
531+
editContentParentToolCallIdRef.current.clear()
521532
setMessageQueue([])
522533
}, [initialChatId, queryClient])
523534

@@ -541,6 +552,7 @@ export function useChat(
541552
streamingFileRef.current = null
542553
filePreviewSessionsRef.current.clear()
543554
activeFilePreviewToolCallIdRef.current = null
555+
editContentParentToolCallIdRef.current.clear()
544556
setMessageQueue([])
545557
}, [isHomePage])
546558

@@ -617,7 +629,7 @@ export function useChat(
617629

618630
const reconnectResult =
619631
snapshotEvents.length > 0
620-
? await attachToExistingStream({
632+
? await attachToExistingStreamRef.current({
621633
streamId: activeStreamId,
622634
assistantId,
623635
expectedGen: gen,
@@ -1015,6 +1027,10 @@ export function useChat(
10151027
sessions.set(id, nextSession)
10161028
activeFilePreviewToolCallIdRef.current = id
10171029
streamingFileRef.current = nextSession
1030+
const previewToolIdx = toolMap.get(id)
1031+
if (previewToolIdx !== undefined && blocks[previewToolIdx].toolCall) {
1032+
blocks[previewToolIdx].toolCall!.status = 'executing'
1033+
}
10181034
setStreamingFile(nextSession)
10191035
break
10201036
}
@@ -1062,11 +1078,19 @@ export function useChat(
10621078
const opMatch = tc.streamingArgs.match(/"operation"\s*:\s*"(\w+)"/)
10631079
const op = opMatch?.[1] ?? ''
10641080
const verb =
1065-
op === 'patch' || op === 'update' || op === 'rename'
1066-
? 'Editing'
1067-
: op === 'delete'
1068-
? 'Deleting'
1069-
: 'Writing'
1081+
op === 'create'
1082+
? 'Creating'
1083+
: op === 'append'
1084+
? 'Adding'
1085+
: op === 'patch'
1086+
? 'Editing'
1087+
: op === 'update'
1088+
? 'Writing'
1089+
: op === 'rename'
1090+
? 'Renaming'
1091+
: op === 'delete'
1092+
? 'Deleting'
1093+
: 'Writing'
10701094
const titleMatch = tc.streamingArgs.match(/"title"\s*:\s*"([^"]*)"/)
10711095
if (titleMatch?.[1]) {
10721096
const unescaped = titleMatch[1]
@@ -1174,7 +1198,20 @@ export function useChat(
11741198
clientExecutionStartedRef.current.delete(id)
11751199
}
11761200

1177-
if (tc.name === WorkspaceFile.id) {
1201+
const workspaceFileOperation =
1202+
tc.name === WorkspaceFile.id && typeof tc.params?.operation === 'string'
1203+
? tc.params.operation
1204+
: undefined
1205+
const shouldKeepWorkspacePreviewOpen =
1206+
tc.name === WorkspaceFile.id &&
1207+
(workspaceFileOperation === 'append' ||
1208+
workspaceFileOperation === 'update' ||
1209+
workspaceFileOperation === 'patch')
1210+
1211+
if (
1212+
(tc.name === WorkspaceFile.id || tc.name === 'edit_content') &&
1213+
!shouldKeepWorkspacePreviewOpen
1214+
) {
11781215
filePreviewSessionsRef.current.delete(id)
11791216
if (activeFilePreviewToolCallIdRef.current === id) {
11801217
activeFilePreviewToolCallIdRef.current = null
@@ -1198,6 +1235,7 @@ export function useChat(
11981235
setResources((rs) => rs.filter((r) => r.id !== 'streaming-file'))
11991236
}
12001237
}
1238+
editContentParentToolCallIdRef.current.delete(id)
12011239
break
12021240
}
12031241

@@ -1222,11 +1260,19 @@ export function useChat(
12221260
if (name === WorkspaceFile.id) {
12231261
const operation = typeof args?.operation === 'string' ? args.operation : ''
12241262
const verb =
1225-
operation === 'patch' || operation === 'update' || operation === 'rename'
1226-
? 'Editing'
1227-
: operation === 'delete'
1228-
? 'Deleting'
1229-
: 'Writing'
1263+
operation === 'create'
1264+
? 'Creating'
1265+
: operation === 'append'
1266+
? 'Adding'
1267+
: operation === 'patch'
1268+
? 'Editing'
1269+
: operation === 'update'
1270+
? 'Writing'
1271+
: operation === 'rename'
1272+
? 'Renaming'
1273+
: operation === 'delete'
1274+
? 'Deleting'
1275+
: 'Writing'
12301276
const chunkTitle = args?.title as string | undefined
12311277
const target = args ? asPayloadRecord(args.target) : undefined
12321278
const targetFileName = target?.fileName as string | undefined
@@ -1237,6 +1283,25 @@ export function useChat(
12371283
}
12381284
}
12391285

1286+
if (name === 'edit_content') {
1287+
const parentToolCallId =
1288+
activeFilePreviewToolCallIdRef.current ?? streamingFileRef.current?.toolCallId
1289+
const parentIdx =
1290+
parentToolCallId !== null && parentToolCallId !== undefined
1291+
? toolMap.get(parentToolCallId)
1292+
: undefined
1293+
if (parentIdx !== undefined && blocks[parentIdx].toolCall) {
1294+
toolMap.set(id, parentIdx)
1295+
editContentParentToolCallIdRef.current.set(id, parentToolCallId!)
1296+
const tc = blocks[parentIdx].toolCall!
1297+
tc.status = 'executing'
1298+
tc.result = undefined
1299+
tc.error = undefined
1300+
flush()
1301+
break
1302+
}
1303+
}
1304+
12401305
if (!toolMap.has(id)) {
12411306
toolMap.set(id, blocks.length)
12421307
blocks.push({
@@ -1608,6 +1673,7 @@ export function useChat(
16081673
},
16091674
[fetchStreamBatch]
16101675
)
1676+
attachToExistingStreamRef.current = attachToExistingStream
16111677

16121678
const resumeOrFinalize = useCallback(
16131679
async (opts: {
@@ -2054,6 +2120,7 @@ export function useChat(
20542120
streamingFileRef.current = null
20552121
filePreviewSessionsRef.current.clear()
20562122
activeFilePreviewToolCallIdRef.current = null
2123+
editContentParentToolCallIdRef.current.clear()
20572124
setResources((rs) => rs.filter((resource) => resource.id !== 'streaming-file'))
20582125

20592126
const execState = useExecutionStore.getState()

apps/sim/app/workspace/[workspaceId]/home/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
CreateWorkflow,
55
Debug,
66
Deploy,
7+
EditContent,
78
EditWorkflow,
89
FunctionExecute,
910
GetPageContents,
@@ -272,6 +273,11 @@ export const TOOL_UI_METADATA: Record<string, ToolUIMetadata> = {
272273
phaseLabel: 'Resource',
273274
phase: 'resource',
274275
},
276+
[EditContent.id]: {
277+
title: 'Writing content',
278+
phaseLabel: 'Resource',
279+
phase: 'resource',
280+
},
275281
[CreateWorkflow.id]: {
276282
title: 'Creating workflow',
277283
phaseLabel: 'Resource',

0 commit comments

Comments
 (0)