Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
d25c7fe
Improve
Sg312 Mar 18, 2026
e063a9d
Hide is hosted
Sg312 Mar 18, 2026
46ed61c
Remove hardcoded
Sg312 Mar 18, 2026
bd7e090
fix
Sg312 Mar 18, 2026
a36454d
Fixes
Sg312 Mar 18, 2026
28ad438
v0
Sg312 Mar 18, 2026
ba4c796
Fix bugs
Sg312 Mar 18, 2026
a521c56
Restore settings
Sg312 Mar 18, 2026
0713f48
Handle compaction event type
Mar 18, 2026
ecb63d9
Add keepalive
Sg312 Mar 19, 2026
d20a8a8
File streaming
Sg312 Mar 19, 2026
8d14a51
Error tags
Sg312 Mar 19, 2026
d3cb757
Abort defense
Sg312 Mar 19, 2026
d17f3c5
Merge remote-tracking branch 'origin/staging' into improvement/copilot-6
Sg312 Mar 19, 2026
715dfdb
Edit hashes
Sg312 Mar 19, 2026
7ee6ee4
DB backed tools
Sg312 Mar 20, 2026
62531f2
Fixes
Sg312 Mar 20, 2026
ec7a258
progress on autolayout improvements
icecrasher321 Mar 20, 2026
a0bb02f
Abort fixes
Sg312 Mar 20, 2026
1f88bd1
vertical insertion improvement
icecrasher321 Mar 20, 2026
1959edd
Merge branch 'improvement/copilot-6' of github.com:simstudioai/sim in…
icecrasher321 Mar 20, 2026
c9d6e05
Consolidate file attachments
Sg312 Mar 20, 2026
f92be2a
Fix lint
Sg312 Mar 20, 2026
6d98c9d
Manage agent result card fix
Sg312 Mar 20, 2026
a57c8cb
Remove hardcoded ff
Sg312 Mar 20, 2026
e35f0ec
Fix file streaming
Mar 20, 2026
7f05fb7
Fix persisted writing file tab
Mar 20, 2026
3cf4ba7
Fix lint
Mar 20, 2026
88000f4
Fix streaming file flash
Mar 20, 2026
3180707
Always set url to /file on file view
Mar 20, 2026
724de21
Edit perms for tables
Sg312 Mar 20, 2026
0e139ca
Fix file edit perms
Sg312 Mar 20, 2026
2a7c0c3
Merge branch 'staging' into improvement/copilot-6
Mar 20, 2026
58e04b2
remove inline tool call json dump
icecrasher321 Mar 20, 2026
e7f0768
Merge branch 'improvement/copilot-6' of github.com:simstudioai/sim in…
icecrasher321 Mar 20, 2026
abb29e5
Merge branch 'staging' into improvement/copilot-6
icecrasher321 Mar 20, 2026
b2c7c61
Enforce name uniqueness (#3679)
TheodoreSpeaks Mar 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/sim/app/_shell/providers/get-query-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ function makeQueryClient() {
retryOnMount: false,
},
mutations: {
retry: 1,
retry: false,
},
dehydrate: {
shouldDehydrateQuery: (query) =>
Expand Down
5 changes: 5 additions & 0 deletions apps/sim/app/api/copilot/chat/resources/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ export async function POST(req: NextRequest) {
const body = await req.json()
const { chatId, resource } = AddResourceSchema.parse(body)

// Ephemeral UI tab (client does not POST this; guard for old clients / bugs).
if (resource.id === 'streaming-file') {
return NextResponse.json({ success: true })
}

if (!VALID_RESOURCE_TYPES.has(resource.type)) {
return createBadRequestResponse(`Invalid resource type: ${resource.type}`)
}
Expand Down
162 changes: 134 additions & 28 deletions apps/sim/app/api/copilot/chat/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { db } from '@sim/db'
import { copilotChats } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { and, desc, eq } from 'drizzle-orm'
import { and, desc, eq, sql } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
Expand All @@ -15,6 +15,7 @@ import {
import { COPILOT_REQUEST_MODES } from '@/lib/copilot/models'
import { orchestrateCopilotStream } from '@/lib/copilot/orchestrator'
import { getStreamMeta, readStreamEvents } from '@/lib/copilot/orchestrator/stream/buffer'
import type { OrchestratorResult } from '@/lib/copilot/orchestrator/types'
import {
authenticateCopilotRequestSessionOnly,
createBadRequestResponse,
Expand All @@ -31,6 +32,8 @@ import {
getUserEntityPermissions,
} from '@/lib/workspaces/permissions/utils'

export const maxDuration = 3600

const logger = createLogger('CopilotChatAPI')

const FileAttachmentSchema = z.object({
Expand All @@ -48,7 +51,7 @@ const ChatMessageSchema = z.object({
workflowId: z.string().optional(),
workspaceId: z.string().optional(),
workflowName: z.string().optional(),
model: z.string().optional().default('claude-opus-4-5'),
model: z.string().optional().default('claude-opus-4-6'),
mode: z.enum(COPILOT_REQUEST_MODES).optional().default('agent'),
prefetch: z.boolean().optional(),
createNewChat: z.boolean().optional().default(false),
Expand Down Expand Up @@ -180,6 +183,29 @@ export async function POST(req: NextRequest) {
})
} catch {}

let currentChat: any = null
let conversationHistory: any[] = []
let actualChatId = chatId
const selectedModel = model || 'claude-opus-4-6'

if (chatId || createNewChat) {
const chatResult = await resolveOrCreateChat({
chatId,
userId: authenticatedUserId,
workflowId,
model: selectedModel,
})
currentChat = chatResult.chat
actualChatId = chatResult.chatId || chatId
conversationHistory = Array.isArray(chatResult.conversationHistory)
? chatResult.conversationHistory
: []

if (chatId && !currentChat) {
return createBadRequestResponse('Chat not found')
}
}

let agentContexts: Array<{ type: string; content: string }> = []
if (Array.isArray(normalizedContexts) && normalizedContexts.length > 0) {
try {
Expand All @@ -188,7 +214,8 @@ export async function POST(req: NextRequest) {
normalizedContexts as any,
authenticatedUserId,
message,
resolvedWorkspaceId
resolvedWorkspaceId,
actualChatId
)
agentContexts = processed
logger.info(`[${tracker.requestId}] Contexts processed for request`, {
Expand All @@ -210,29 +237,6 @@ export async function POST(req: NextRequest) {
}
}

let currentChat: any = null
let conversationHistory: any[] = []
let actualChatId = chatId
const selectedModel = model || 'claude-opus-4-5'

if (chatId || createNewChat) {
const chatResult = await resolveOrCreateChat({
chatId,
userId: authenticatedUserId,
workflowId,
model: selectedModel,
})
currentChat = chatResult.chat
actualChatId = chatResult.chatId || chatId
conversationHistory = Array.isArray(chatResult.conversationHistory)
? chatResult.conversationHistory
: []

if (chatId && !currentChat) {
return createBadRequestResponse('Chat not found')
}
}

const effectiveMode = mode === 'agent' ? 'build' : mode

const userPermission = resolvedWorkspaceId
Expand Down Expand Up @@ -283,26 +287,128 @@ export async function POST(req: NextRequest) {
})
} catch {}

if (actualChatId) {
const userMsg = {
id: userMessageIdToUse,
role: 'user' as const,
content: message,
timestamp: new Date().toISOString(),
...(fileAttachments && fileAttachments.length > 0 && { fileAttachments }),
...(Array.isArray(normalizedContexts) &&
normalizedContexts.length > 0 && {
contexts: normalizedContexts,
}),
}

const [updated] = await db
.update(copilotChats)
.set({
messages: sql`${copilotChats.messages} || ${JSON.stringify([userMsg])}::jsonb`,
conversationId: userMessageIdToUse,
updatedAt: new Date(),
})
.where(eq(copilotChats.id, actualChatId))
.returning({ messages: copilotChats.messages })

if (updated) {
const freshMessages: any[] = Array.isArray(updated.messages) ? updated.messages : []
conversationHistory = freshMessages.filter((m: any) => m.id !== userMessageIdToUse)
}
}

if (stream) {
const executionId = crypto.randomUUID()
const runId = crypto.randomUUID()
const sseStream = createSSEStream({
requestPayload,
userId: authenticatedUserId,
streamId: userMessageIdToUse,
executionId,
runId,
chatId: actualChatId,
currentChat,
isNewChat: conversationHistory.length === 0,
message,
titleModel: selectedModel,
titleProvider: provider,
requestId: tracker.requestId,
workspaceId: resolvedWorkspaceId,
orchestrateOptions: {
userId: authenticatedUserId,
workflowId,
chatId: actualChatId,
executionId,
runId,
goRoute: '/api/copilot',
autoExecuteTools: true,
interactive: true,
promptForToolApproval: true,
promptForToolApproval: false,
onComplete: async (result: OrchestratorResult) => {
if (!actualChatId) return

const assistantMessage: Record<string, unknown> = {
id: crypto.randomUUID(),
role: 'assistant' as const,
content: result.content,
timestamp: new Date().toISOString(),
...(result.requestId ? { requestId: result.requestId } : {}),
}
if (result.toolCalls.length > 0) {
assistantMessage.toolCalls = result.toolCalls
}
if (result.contentBlocks.length > 0) {
assistantMessage.contentBlocks = result.contentBlocks.map((block) => {
const stored: Record<string, unknown> = { type: block.type }
if (block.content) stored.content = block.content
if (block.type === 'tool_call' && block.toolCall) {
stored.toolCall = {
id: block.toolCall.id,
name: block.toolCall.name,
state:
block.toolCall.result?.success !== undefined
? block.toolCall.result.success
? 'success'
: 'error'
: block.toolCall.status,
result: block.toolCall.result,
...(block.calledBy ? { calledBy: block.calledBy } : {}),
}
}
return stored
})
}

try {
const [row] = await db
.select({ messages: copilotChats.messages })
.from(copilotChats)
.where(eq(copilotChats.id, actualChatId))
.limit(1)

const msgs: any[] = Array.isArray(row?.messages) ? row.messages : []
const userIdx = msgs.findIndex((m: any) => m.id === userMessageIdToUse)
const alreadyHasResponse =
userIdx >= 0 &&
userIdx + 1 < msgs.length &&
(msgs[userIdx + 1] as any)?.role === 'assistant'

if (!alreadyHasResponse) {
await db
.update(copilotChats)
.set({
messages: sql`${copilotChats.messages} || ${JSON.stringify([assistantMessage])}::jsonb`,
conversationId: sql`CASE WHEN ${copilotChats.conversationId} = ${userMessageIdToUse} THEN NULL ELSE ${copilotChats.conversationId} END`,
updatedAt: new Date(),
})
.where(eq(copilotChats.id, actualChatId))
}
} catch (error) {
logger.error(`[${tracker.requestId}] Failed to persist chat messages`, {
chatId: actualChatId,
error: error instanceof Error ? error.message : 'Unknown error',
})
}
},
},
})

Expand All @@ -316,7 +422,7 @@ export async function POST(req: NextRequest) {
goRoute: '/api/copilot',
autoExecuteTools: true,
interactive: true,
promptForToolApproval: true,
promptForToolApproval: false,
})

const responseData = {
Expand Down
10 changes: 9 additions & 1 deletion apps/sim/app/api/copilot/chat/stream/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ import {
import { authenticateCopilotRequestSessionOnly } from '@/lib/copilot/request-helpers'
import { SSE_HEADERS } from '@/lib/core/utils/sse'

export const maxDuration = 3600

const logger = createLogger('CopilotChatStreamAPI')
const POLL_INTERVAL_MS = 250
const MAX_STREAM_MS = 10 * 60 * 1000
const MAX_STREAM_MS = 60 * 60 * 1000

function encodeEvent(event: Record<string, any>): Uint8Array {
return new TextEncoder().encode(`data: ${JSON.stringify(event)}\n\n`)
Expand Down Expand Up @@ -67,6 +69,8 @@ export async function GET(request: NextRequest) {
success: true,
events: filteredEvents,
status: meta.status,
executionId: meta.executionId,
runId: meta.runId,
})
}

Expand All @@ -75,6 +79,7 @@ export async function GET(request: NextRequest) {
const stream = new ReadableStream({
async start(controller) {
let lastEventId = Number.isFinite(fromEventId) ? fromEventId : 0
let latestMeta = meta

const flushEvents = async () => {
const events = await readStreamEvents(streamId, lastEventId)
Expand All @@ -91,6 +96,8 @@ export async function GET(request: NextRequest) {
...entry.event,
eventId: entry.eventId,
streamId: entry.streamId,
executionId: latestMeta?.executionId,
runId: latestMeta?.runId,
}
controller.enqueue(encodeEvent(payload))
}
Expand All @@ -102,6 +109,7 @@ export async function GET(request: NextRequest) {
while (Date.now() - startTime < MAX_STREAM_MS) {
const currentMeta = await getStreamMeta(streamId)
if (!currentMeta) break
latestMeta = currentMeta

await flushEvents()

Expand Down
Loading
Loading