Files
simstudioai--sim/apps/sim/lib/copilot/request/go/file-preview-adapter.ts
T
wehub-resource-sync d25d482dc2
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:20:55 +08:00

762 lines
24 KiB
TypeScript

import { createLogger } from '@sim/logger'
import { toError } from '@sim/utils/errors'
import { MothershipStreamV1EventType } from '@/lib/copilot/generated/mothership-stream-v1'
import {
createFilePreviewSession,
type FilePreviewContentMode,
type FilePreviewSession,
type FilePreviewTargetKind,
isToolArgsDeltaStreamEvent,
isToolCallStreamEvent,
isToolResultStreamEvent,
type SyntheticFilePreviewPayload,
upsertFilePreviewSession,
} from '@/lib/copilot/request/session'
import type {
ActiveFileIntent,
ExecutionContext,
OrchestratorOptions,
StreamEvent,
StreamingContext,
} from '@/lib/copilot/request/types'
import { peekFileIntent } from '@/lib/copilot/tools/server/files/file-intent-store'
import {
buildFilePreviewText,
loadWorkspaceFileTextForPreview,
} from '@/lib/copilot/tools/server/files/file-preview'
import { resolveWorkspaceFileReference } from '@/lib/uploads/contexts/workspace/workspace-file-manager'
const logger = createLogger('CopilotFilePreviewAdapter')
type JsonRecord = Record<string, unknown>
type FileIntent = ActiveFileIntent
type EditContentStreamState = {
raw: string
lastContentSnapshot?: string
}
type FilePreviewStreamState = {
session: FilePreviewSession
lastEmittedPreviewText: string
lastSnapshotAt: number
}
type ParsedWorkspaceFileArgs = {
operation: string
target: FileIntent['target']
title?: string
contentType?: string
edit?: JsonRecord
}
const PATCH_PREVIEW_SNAPSHOT_INTERVAL_MS = 80
const DELTA_PREVIEW_CHECKPOINT_INTERVAL_MS = 1000
function asJsonRecord(value: unknown): JsonRecord | undefined {
return value && typeof value === 'object' && !Array.isArray(value)
? (value as JsonRecord)
: undefined
}
function toPreviewTargetKind(kind: string | undefined): FilePreviewTargetKind | undefined {
return kind === 'new_file' || kind === 'file_id' ? kind : undefined
}
async function resolvePreviewTarget(args: {
workspaceId?: string
target: FileIntent['target']
}): Promise<FileIntent['target']> {
if (args.target.kind !== 'path' || !args.workspaceId || !args.target.path) {
return args.target
}
const file = await resolveWorkspaceFileReference(args.workspaceId, args.target.path)
if (!file) {
return args.target
}
return {
kind: 'file_id',
fileId: file.id,
fileName: args.target.fileName ?? file.name,
path: args.target.path,
}
}
function parseWorkspaceFileArgs(value: unknown): ParsedWorkspaceFileArgs | undefined {
const args = asJsonRecord(value)
if (!args) {
return undefined
}
const operation = typeof args.operation === 'string' ? args.operation : undefined
const target = asJsonRecord(args.target)
const targetKind = typeof target?.kind === 'string' ? target.kind : undefined
if (!operation || !target || !targetKind) {
return undefined
}
const fileId = typeof target.fileId === 'string' ? target.fileId : undefined
const fileName = typeof target.fileName === 'string' ? target.fileName : undefined
const path = typeof target.path === 'string' ? target.path : undefined
const title = typeof args.title === 'string' ? args.title : undefined
const contentType = typeof args.contentType === 'string' ? args.contentType : undefined
const edit = asJsonRecord(args.edit)
return {
operation,
target: {
kind: targetKind,
...(fileId ? { fileId } : {}),
...(fileName ? { fileName } : {}),
...(path ? { path } : {}),
},
...(title ? { title } : {}),
...(contentType ? { contentType } : {}),
...(edit ? { edit } : {}),
}
}
function extractWorkspaceFileResult(output: unknown): { fileId?: string; fileName?: string } {
const candidates: JsonRecord[] = []
const root = asJsonRecord(output)
if (root) {
candidates.push(root)
const rootData = asJsonRecord(root.data)
if (rootData) candidates.push(rootData)
const rootOutput = asJsonRecord(root.output)
if (rootOutput) {
candidates.push(rootOutput)
const outputData = asJsonRecord(rootOutput.data)
if (outputData) candidates.push(outputData)
}
}
for (const candidate of candidates) {
const fileId =
typeof candidate.id === 'string'
? candidate.id
: typeof candidate.fileId === 'string'
? candidate.fileId
: undefined
if (!fileId) continue
const fileName =
typeof candidate.name === 'string'
? candidate.name
: typeof candidate.fileName === 'string'
? candidate.fileName
: undefined
return { fileId, fileName }
}
return {}
}
export function decodeJsonStringPrefix(input: string): string {
let output = ''
for (let i = 0; i < input.length; i++) {
const ch = input[i]
if (ch !== '\\') {
output += ch
continue
}
const next = input[i + 1]
if (!next) break
if (next === 'n') {
output += '\n'
i++
continue
}
if (next === 't') {
output += '\t'
i++
continue
}
if (next === 'r') {
output += '\r'
i++
continue
}
if (next === '"') {
output += '"'
i++
continue
}
if (next === '\\') {
output += '\\'
i++
continue
}
if (next === '/') {
output += '/'
i++
continue
}
if (next === 'b') {
output += '\b'
i++
continue
}
if (next === 'f') {
output += '\f'
i++
continue
}
if (next === 'u') {
const hex = input.slice(i + 2, i + 6)
if (hex.length < 4 || !/^[0-9a-fA-F]{4}$/.test(hex)) break
output += String.fromCharCode(Number.parseInt(hex, 16))
i += 5
continue
}
break
}
return output
}
export function extractEditContent(raw: string): string {
const marker = '"content":'
const idx = raw.indexOf(marker)
if (idx === -1) return ''
const rest = raw.slice(idx + marker.length).trimStart()
if (!rest.startsWith('"')) return rest
let end = -1
for (let i = 1; i < rest.length; i++) {
if (rest[i] === '\\') {
i++
continue
}
if (rest[i] === '"') {
end = i
break
}
}
const inner = end === -1 ? rest.slice(1) : rest.slice(1, end)
return decodeJsonStringPrefix(inner)
}
function isContentOperation(
operation: string | undefined
): operation is 'append' | 'update' | 'patch' {
return operation === 'append' || operation === 'update' || operation === 'patch'
}
function isDocFormat(fileName: string | undefined): boolean {
return /\.(pptx|docx|pdf)$/i.test(fileName ?? '')
}
function buildPreviewSessionFromIntent(
streamId: string,
intent: FileIntent,
current?: FilePreviewSession
): FilePreviewSession {
const targetKind = toPreviewTargetKind(intent.target.kind)
return createFilePreviewSession({
streamId,
toolCallId: intent.toolCallId,
fileName: intent.target.fileName ?? current?.fileName,
...(intent.target.fileId ? { fileId: intent.target.fileId } : {}),
...(targetKind ? { targetKind } : {}),
operation: intent.operation,
...(intent.edit ? { edit: intent.edit } : {}),
...(typeof current?.baseContent === 'string' ? { baseContent: current.baseContent } : {}),
previewText: current?.previewText ?? '',
previewVersion: current?.previewVersion ?? 0,
status: current?.status ?? 'pending',
completedAt: current?.completedAt,
})
}
async function persistFilePreviewSession(session: FilePreviewSession): Promise<void> {
try {
await upsertFilePreviewSession(session)
} catch (error) {
logger.warn('Failed to persist file preview session', {
streamId: session.streamId,
toolCallId: session.toolCallId,
previewVersion: session.previewVersion,
error: toError(error).message,
})
}
}
export function buildPreviewContentUpdate(
previousText: string,
nextText: string,
lastSnapshotAt: number,
now: number,
operation: string | undefined
): { content: string; contentMode: FilePreviewContentMode; lastSnapshotAt: number } {
const shouldForceSnapshot =
previousText.length === 0 ||
!nextText.startsWith(previousText) ||
operation === 'patch' ||
operation === 'append' ||
now - lastSnapshotAt >= DELTA_PREVIEW_CHECKPOINT_INTERVAL_MS
if (shouldForceSnapshot) {
return {
content: nextText,
contentMode: 'snapshot',
lastSnapshotAt: now,
}
}
return {
content: nextText.slice(previousText.length),
contentMode: 'delta',
lastSnapshotAt,
}
}
export interface FilePreviewAdapterState {
editContentState: Map<string, EditContentStreamState>
filePreviewState: Map<string, FilePreviewStreamState>
}
export function createFilePreviewAdapterState(): FilePreviewAdapterState {
return {
editContentState: new Map<string, EditContentStreamState>(),
filePreviewState: new Map<string, FilePreviewStreamState>(),
}
}
async function emitPreviewEvent(
streamEvent: StreamEvent,
options: Pick<OrchestratorOptions, 'onEvent'>,
payload: SyntheticFilePreviewPayload
): Promise<void> {
await options.onEvent?.({
type: MothershipStreamV1EventType.tool,
payload,
...(streamEvent.scope ? { scope: streamEvent.scope } : {}),
})
}
export async function processFilePreviewStreamEvent(input: {
streamId: string
streamEvent: StreamEvent
context: StreamingContext
execContext: ExecutionContext
options: Pick<OrchestratorOptions, 'onEvent'>
state: FilePreviewAdapterState
}): Promise<void> {
const { streamId, streamEvent, context, execContext, options, state } = input
const { editContentState, filePreviewState } = state
// Scope the in-flight intent to the invoking file subagent's channel (its
// outer tool_use id) so two file agents streaming concurrently never read or
// overwrite each other's intent. workspace_file and edit_content from the same
// file agent share this channel id, so they pair up; siblings stay isolated.
const channelId = streamEvent.scope?.parentToolCallId ?? ''
const getIntent = (): FileIntent | null => context.activeFileIntents.get(channelId) ?? null
const setIntent = (intent: FileIntent): void => {
context.activeFileIntents.set(channelId, intent)
}
const clearIntent = (): void => {
context.activeFileIntents.delete(channelId)
}
if (isToolCallStreamEvent(streamEvent) && streamEvent.payload.toolName === 'workspace_file') {
const toolCallId = streamEvent.payload.toolCallId
const parsedArgs = parseWorkspaceFileArgs(streamEvent.payload.arguments)
if (toolCallId && parsedArgs) {
const { operation, title, contentType, edit } = parsedArgs
const target = await resolvePreviewTarget({
workspaceId: execContext.workspaceId,
target: parsedArgs.target,
})
const previewTargetKind = toPreviewTargetKind(target.kind)
const { fileId, fileName } = target
const isContentOp = isContentOperation(operation)
// Per-channel: a re-declared workspace_file just overwrites THIS channel's
// slot. No cross-message intent clearing — that would wipe a concurrent
// sibling file agent's pending intent.
const intent: FileIntent = {
toolCallId,
operation,
target,
...(title ? { title } : {}),
...(contentType ? { contentType } : {}),
...(edit ? { edit } : {}),
}
setIntent(intent)
if (isContentOp && previewTargetKind) {
let previewBaseContent: string | undefined
if (
execContext.workspaceId &&
fileId &&
(operation === 'append' || operation === 'patch')
) {
previewBaseContent = await loadWorkspaceFileTextForPreview(
execContext.workspaceId,
fileId
)
}
let session = buildPreviewSessionFromIntent(streamId, intent)
if (previewBaseContent !== undefined) {
session = { ...session, baseContent: previewBaseContent }
}
filePreviewState.set(toolCallId, {
session,
lastEmittedPreviewText: '',
lastSnapshotAt: 0,
})
await persistFilePreviewSession(session)
await emitPreviewEvent(streamEvent, options, {
toolCallId,
toolName: 'workspace_file',
previewPhase: 'file_preview_start',
})
await emitPreviewEvent(streamEvent, options, {
toolCallId,
toolName: 'workspace_file',
previewPhase: 'file_preview_target',
operation,
target: {
kind: previewTargetKind,
...(fileId ? { fileId } : {}),
...(fileName ? { fileName } : {}),
},
...(title ? { title } : {}),
})
if (edit) {
await emitPreviewEvent(streamEvent, options, {
toolCallId,
toolName: 'workspace_file',
previewPhase: 'file_preview_edit_meta',
edit,
})
}
}
}
}
const workspaceResultIntent = getIntent()
if (
isToolResultStreamEvent(streamEvent) &&
streamEvent.payload.toolName === 'workspace_file' &&
workspaceResultIntent &&
isContentOperation(workspaceResultIntent.operation)
) {
const result = extractWorkspaceFileResult(streamEvent.payload.output)
if (result.fileId && workspaceResultIntent.target.kind === 'path') {
const intent: FileIntent = {
...workspaceResultIntent,
target: {
kind: 'file_id',
fileId: result.fileId,
fileName: result.fileName ?? workspaceResultIntent.target.fileName,
path: workspaceResultIntent.target.path,
},
}
setIntent(intent)
let previewBaseContent: string | undefined
if (
execContext.workspaceId &&
(intent.operation === 'append' || intent.operation === 'patch')
) {
previewBaseContent = await loadWorkspaceFileTextForPreview(
execContext.workspaceId,
result.fileId
)
}
let session = buildPreviewSessionFromIntent(streamId, intent)
if (previewBaseContent !== undefined) {
session = { ...session, baseContent: previewBaseContent }
}
filePreviewState.set(intent.toolCallId, {
session,
lastEmittedPreviewText: '',
lastSnapshotAt: 0,
})
await persistFilePreviewSession(session)
await emitPreviewEvent(streamEvent, options, {
toolCallId: intent.toolCallId,
toolName: 'workspace_file',
previewPhase: 'file_preview_start',
})
await emitPreviewEvent(streamEvent, options, {
toolCallId: intent.toolCallId,
toolName: 'workspace_file',
previewPhase: 'file_preview_target',
operation: intent.operation,
target: {
kind: 'file_id',
fileId: result.fileId,
...(result.fileName ? { fileName: result.fileName } : {}),
},
...(intent.title ? { title: intent.title } : {}),
})
if (intent.edit) {
await emitPreviewEvent(streamEvent, options, {
toolCallId: intent.toolCallId,
toolName: 'workspace_file',
previewPhase: 'file_preview_edit_meta',
edit: intent.edit,
})
}
}
}
const patchDeleteIntent = getIntent()
if (
isToolResultStreamEvent(streamEvent) &&
streamEvent.payload.toolName === 'workspace_file' &&
patchDeleteIntent &&
isContentOperation(patchDeleteIntent.operation) &&
patchDeleteIntent.operation === 'patch' &&
patchDeleteIntent.edit?.strategy === 'anchored' &&
patchDeleteIntent.edit?.mode === 'delete_between' &&
execContext.workspaceId &&
patchDeleteIntent.target.fileId &&
!isDocFormat(patchDeleteIntent.target.fileName)
) {
const currentPreview = filePreviewState.get(patchDeleteIntent.toolCallId)
const previewText = buildFilePreviewText({
operation: 'patch',
streamedContent: '',
existingContent: currentPreview?.session.baseContent,
edit: currentPreview?.session.edit,
})
if (previewText !== undefined) {
const baseSession = buildPreviewSessionFromIntent(
streamId,
patchDeleteIntent,
currentPreview?.session
)
const nextSession: FilePreviewSession = {
...baseSession,
status: 'streaming',
previewText,
previewVersion: (currentPreview?.session.previewVersion ?? 0) + 1,
updatedAt: new Date().toISOString(),
}
filePreviewState.set(patchDeleteIntent.toolCallId, {
session: nextSession,
lastEmittedPreviewText: previewText,
lastSnapshotAt: Date.now(),
})
await persistFilePreviewSession(nextSession)
await emitPreviewEvent(streamEvent, options, {
toolCallId: nextSession.toolCallId,
toolName: 'workspace_file',
previewPhase: 'file_preview_content',
content: previewText,
contentMode: 'snapshot',
previewVersion: nextSession.previewVersion,
fileName: nextSession.fileName,
...(nextSession.fileId ? { fileId: nextSession.fileId } : {}),
...(nextSession.targetKind ? { targetKind: nextSession.targetKind } : {}),
...(nextSession.operation ? { operation: nextSession.operation } : {}),
...(nextSession.edit ? { edit: nextSession.edit } : {}),
})
}
}
if (isToolArgsDeltaStreamEvent(streamEvent) && streamEvent.payload.toolName === 'edit_content') {
const toolCallId = streamEvent.payload.toolCallId
const delta = streamEvent.payload.argumentsDelta
const stateForTool = editContentState.get(toolCallId) ?? { raw: '' }
stateForTool.raw += delta
const editIntent = getIntent()
if (editIntent) {
const streamedContent = extractEditContent(stateForTool.raw)
if (streamedContent !== (stateForTool.lastContentSnapshot ?? '')) {
stateForTool.lastContentSnapshot = streamedContent
let currentPreview = filePreviewState.get(editIntent.toolCallId) ?? {
session: buildPreviewSessionFromIntent(streamId, editIntent),
lastEmittedPreviewText: '',
lastSnapshotAt: 0,
}
if (
currentPreview.session.baseContent === undefined &&
(editIntent.operation === 'append' || editIntent.operation === 'patch') &&
execContext.workspaceId &&
editIntent.target.fileId
) {
const intentBase = await peekFileIntent(
execContext.workspaceId,
editIntent.target.fileId,
{
chatId: execContext.chatId,
messageId: execContext.messageId,
channelId,
}
)
if (typeof intentBase?.existingContent === 'string') {
const seededSession: FilePreviewSession = {
...currentPreview.session,
baseContent: intentBase.existingContent,
...(intentBase.edit ? { edit: intentBase.edit } : {}),
}
currentPreview = {
...currentPreview,
session: seededSession,
}
filePreviewState.set(editIntent.toolCallId, currentPreview)
await persistFilePreviewSession(seededSession)
}
}
const previewText = isContentOperation(editIntent.operation)
? buildFilePreviewText({
operation: editIntent.operation,
streamedContent,
existingContent: currentPreview.session.baseContent,
edit: currentPreview.session.edit,
})
: undefined
if (previewText !== undefined) {
const baseSession = buildPreviewSessionFromIntent(
streamId,
editIntent,
currentPreview.session
)
const now = Date.now()
const nextSession: FilePreviewSession = {
...baseSession,
status: 'streaming',
previewText,
previewVersion: (currentPreview.session.previewVersion ?? 0) + 1,
updatedAt: new Date(now).toISOString(),
}
await persistFilePreviewSession(nextSession)
if (
nextSession.operation === 'patch' &&
now - currentPreview.lastSnapshotAt < PATCH_PREVIEW_SNAPSHOT_INTERVAL_MS
) {
filePreviewState.set(editIntent.toolCallId, {
session: nextSession,
lastEmittedPreviewText: currentPreview.lastEmittedPreviewText,
lastSnapshotAt: currentPreview.lastSnapshotAt,
})
} else {
const previewUpdate = buildPreviewContentUpdate(
currentPreview.lastEmittedPreviewText,
nextSession.previewText,
currentPreview.lastSnapshotAt,
now,
nextSession.operation
)
filePreviewState.set(editIntent.toolCallId, {
session: nextSession,
lastEmittedPreviewText: nextSession.previewText,
lastSnapshotAt: previewUpdate.lastSnapshotAt,
})
await emitPreviewEvent(streamEvent, options, {
toolCallId: nextSession.toolCallId,
toolName: 'workspace_file',
previewPhase: 'file_preview_content',
content: previewUpdate.content,
contentMode: previewUpdate.contentMode,
previewVersion: nextSession.previewVersion,
fileName: nextSession.fileName,
...(nextSession.fileId ? { fileId: nextSession.fileId } : {}),
...(nextSession.targetKind ? { targetKind: nextSession.targetKind } : {}),
...(nextSession.operation ? { operation: nextSession.operation } : {}),
...(nextSession.edit ? { edit: nextSession.edit } : {}),
})
}
} else {
filePreviewState.set(editIntent.toolCallId, {
session: currentPreview.session,
lastEmittedPreviewText: currentPreview.lastEmittedPreviewText,
lastSnapshotAt: currentPreview.lastSnapshotAt,
})
}
}
}
editContentState.set(toolCallId, stateForTool)
}
if (isToolCallStreamEvent(streamEvent) && streamEvent.payload.toolName === 'edit_content') {
const toolCallId = streamEvent.payload.toolCallId
if (toolCallId) {
editContentState.delete(toolCallId)
}
}
const editResultIntent = getIntent()
if (
isToolResultStreamEvent(streamEvent) &&
streamEvent.payload.toolName === 'edit_content' &&
editResultIntent
) {
const currentPreview = filePreviewState.get(editResultIntent.toolCallId)
const completedAt = new Date().toISOString()
if (
currentPreview &&
currentPreview.lastEmittedPreviewText !== currentPreview.session.previewText &&
currentPreview.session.previewText.length > 0
) {
filePreviewState.set(editResultIntent.toolCallId, {
session: currentPreview.session,
lastEmittedPreviewText: currentPreview.session.previewText,
lastSnapshotAt: Date.now(),
})
await emitPreviewEvent(streamEvent, options, {
toolCallId: currentPreview.session.toolCallId,
toolName: 'workspace_file',
previewPhase: 'file_preview_content',
content: currentPreview.session.previewText,
contentMode: 'snapshot',
previewVersion: currentPreview.session.previewVersion,
fileName: currentPreview.session.fileName,
...(currentPreview.session.fileId ? { fileId: currentPreview.session.fileId } : {}),
...(currentPreview.session.targetKind
? { targetKind: currentPreview.session.targetKind }
: {}),
...(currentPreview.session.operation
? { operation: currentPreview.session.operation }
: {}),
...(currentPreview.session.edit ? { edit: currentPreview.session.edit } : {}),
})
}
if (currentPreview) {
const completedSession: FilePreviewSession = {
...currentPreview.session,
status: 'complete',
updatedAt: completedAt,
completedAt,
}
filePreviewState.set(editResultIntent.toolCallId, {
session: completedSession,
lastEmittedPreviewText: completedSession.previewText,
lastSnapshotAt: Date.now(),
})
await persistFilePreviewSession(completedSession)
}
await emitPreviewEvent(streamEvent, options, {
toolCallId: editResultIntent.toolCallId,
toolName: 'workspace_file',
previewPhase: 'file_preview_complete',
fileId: editResultIntent.target.fileId,
output: streamEvent.payload.output,
...(currentPreview ? { previewVersion: currentPreview.session.previewVersion } : {}),
})
clearIntent()
}
}