import { db } from '@sim/db' import { copilotChats, workflow } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { authorizeWorkflowByWorkspacePermission } from '@sim/platform-authz/workflow' import { and, desc, eq, inArray, isNull, or } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { createWorkflowCopilotChatContract } from '@/lib/api/contracts/copilot' import { parseRequest, validationErrorResponse } from '@/lib/api/server' import { resolveOrCreateChat } from '@/lib/copilot/chat/lifecycle' import { chatPubSub } from '@/lib/copilot/chat-status' import { authenticateCopilotRequestSessionOnly, createBadRequestResponse, createForbiddenResponse, createInternalServerErrorResponse, createUnauthorizedResponse, } from '@/lib/copilot/request/http' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { assertActiveWorkspaceAccess, isWorkspaceAccessDeniedError, } from '@/lib/workspaces/permissions/utils' import { listAccessibleWorkspaceRowsForUser } from '@/lib/workspaces/utils' const logger = createLogger('CopilotChatsListAPI') const DEFAULT_COPILOT_MODEL = 'claude-opus-4-6' export const GET = withRouteHandler(async (_request: NextRequest) => { try { const { userId, isAuthenticated } = await authenticateCopilotRequestSessionOnly() if (!isAuthenticated || !userId) { return createUnauthorizedResponse() } // Active accessible workspaces (explicit + org-derived). Using the active // scope keeps the archived-workspace exclusion the old join-based query had. const accessibleRows = await listAccessibleWorkspaceRowsForUser(userId) const accessibleWorkspaceIds = accessibleRows.map((row) => row.workspace.id) const inAccessibleWorkspace = accessibleWorkspaceIds.length > 0 ? or( inArray(workflow.workspaceId, accessibleWorkspaceIds), and( isNull(copilotChats.workflowId), inArray(copilotChats.workspaceId, accessibleWorkspaceIds) ) ) : undefined const visibleChats = await db .selectDistinctOn([copilotChats.id], { id: copilotChats.id, title: copilotChats.title, workflowId: copilotChats.workflowId, workspaceId: copilotChats.workspaceId, activeStreamId: copilotChats.conversationId, updatedAt: copilotChats.updatedAt, }) .from(copilotChats) .leftJoin(workflow, eq(copilotChats.workflowId, workflow.id)) .where( and( eq(copilotChats.userId, userId), or( and(isNull(copilotChats.workflowId), isNull(copilotChats.workspaceId)), inAccessibleWorkspace ), or(isNull(workflow.id), isNull(workflow.archivedAt)) ) ) .orderBy(copilotChats.id, desc(copilotChats.updatedAt)) const sorted = [...visibleChats].sort( (a, b) => new Date(b.updatedAt!).getTime() - new Date(a.updatedAt!).getTime() ) logger.info(`Retrieved ${sorted.length} chats for user ${userId}`) return NextResponse.json({ success: true, chats: sorted }) } catch (error) { logger.error('Error fetching user copilot chats:', error) return createInternalServerErrorResponse('Failed to fetch user chats') } }) /** * POST /api/copilot/chats * Creates an empty workflow-scoped copilot chat (same lifecycle as {@link resolveOrCreateChat}). * Matches mothership's POST /api/mothership/chats pattern so the client always selects a real row id. */ export const POST = withRouteHandler(async (request: NextRequest) => { try { const { userId, isAuthenticated } = await authenticateCopilotRequestSessionOnly() if (!isAuthenticated || !userId) { return createUnauthorizedResponse() } const parsed = await parseRequest( createWorkflowCopilotChatContract, request, {}, { validationErrorResponse: (error) => validationErrorResponse(error, 'workspaceId and workflowId are required'), } ) if (!parsed.success) return parsed.response const { workspaceId, workflowId } = parsed.data.body await assertActiveWorkspaceAccess(workspaceId, userId) const authorization = await authorizeWorkflowByWorkspacePermission({ workflowId, userId, action: 'read', }) if (!authorization.allowed || !authorization.workflow) { return NextResponse.json( { success: false, error: authorization.message ?? 'Forbidden' }, { status: authorization.status } ) } if (authorization.workflow.workspaceId !== workspaceId) { return createBadRequestResponse('workflow does not belong to this workspace') } const result = await resolveOrCreateChat({ userId, workflowId, workspaceId, model: DEFAULT_COPILOT_MODEL, type: 'copilot', }) if (!result.chatId) { return createInternalServerErrorResponse('Failed to create chat') } chatPubSub?.publishStatusChanged({ workspaceId, chatId: result.chatId, type: 'created' }) return NextResponse.json({ success: true, id: result.chatId }) } catch (error) { if (isWorkspaceAccessDeniedError(error)) { return createForbiddenResponse('Workspace access denied') } logger.error('Error creating workflow copilot chat:', error) return createInternalServerErrorResponse('Failed to create chat') } })