import { db } from '@sim/db' import { copilotChats } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' import { and, eq, sql } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { deleteMothershipChatContract, getMothershipChatContract, updateMothershipChatContract, } from '@/lib/api/contracts/mothership-chats' import { parseRequest } from '@/lib/api/server' import { getLatestRunForStream } from '@/lib/copilot/async-runs/repository' import { buildEffectiveChatTranscript } from '@/lib/copilot/chat/effective-transcript' import { getAccessibleCopilotChatAuth, getAccessibleCopilotChatWithMessages, } from '@/lib/copilot/chat/lifecycle' import { normalizeMessage } from '@/lib/copilot/chat/persisted-message' import { reconcileChatStreamMarkers } from '@/lib/copilot/chat/stream-liveness' import { chatPubSub } from '@/lib/copilot/chat-status' import { authenticateCopilotRequestSessionOnly, createInternalServerErrorResponse, createUnauthorizedResponse, } from '@/lib/copilot/request/http' import type { FilePreviewSession } from '@/lib/copilot/request/session' import { readEvents } from '@/lib/copilot/request/session/buffer' import { readFilePreviewSessions } from '@/lib/copilot/request/session/file-preview-session' import { type StreamBatchEvent, toStreamBatchEvent } from '@/lib/copilot/request/session/types' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { captureServerEvent } from '@/lib/posthog/server' const logger = createLogger('MothershipChatAPI') export const GET = withRouteHandler( async (request: NextRequest, context: { params: Promise<{ chatId: string }> }) => { try { const { userId, isAuthenticated } = await authenticateCopilotRequestSessionOnly() if (!isAuthenticated || !userId) { return createUnauthorizedResponse() } const paramsResult = await parseRequest(getMothershipChatContract, request, context) if (!paramsResult.success) return paramsResult.response const { chatId } = paramsResult.data.params const chat = await getAccessibleCopilotChatWithMessages(chatId, userId) if (!chat || chat.type !== 'mothership') { return NextResponse.json({ success: false, error: 'Chat not found' }, { status: 404 }) } let streamSnapshot: { events: StreamBatchEvent[] previewSessions: FilePreviewSession[] status: string } | null = null const reconciledMarkers = await reconcileChatStreamMarkers( [{ chatId: chat.id, streamId: chat.conversationId }], { repairVerifiedStaleMarkers: true } ) const liveStreamId = reconciledMarkers.get(chat.id)?.streamId ?? null if (liveStreamId) { try { const [events, previewSessions] = await Promise.all([ readEvents(liveStreamId, '0'), readFilePreviewSessions(liveStreamId).catch((error) => { logger.warn('Failed to read preview sessions for mothership chat', { chatId, streamId: liveStreamId, error: toError(error).message, }) return [] }), ]) const run = await getLatestRunForStream(liveStreamId, userId).catch((error) => { logger.warn('Failed to fetch latest run for mothership chat snapshot', { chatId, streamId: liveStreamId, error: toError(error).message, }) return null }) streamSnapshot = { events: events.map(toStreamBatchEvent), previewSessions, status: typeof run?.status === 'string' ? run.status : events.length > 0 ? 'active' : 'unknown', } } catch (error) { logger.warn('Failed to read stream snapshot for mothership chat', { chatId, streamId: liveStreamId, error: toError(error).message, }) } } const normalizedMessages = Array.isArray(chat.messages) ? chat.messages .filter((message): message is Record => Boolean(message)) .map(normalizeMessage) : [] const effectiveMessages = buildEffectiveChatTranscript({ messages: normalizedMessages, activeStreamId: liveStreamId, ...(streamSnapshot ? { streamSnapshot } : {}), }) return NextResponse.json({ success: true, chat: { id: chat.id, title: chat.title, messages: effectiveMessages, activeStreamId: liveStreamId, resources: Array.isArray(chat.resources) ? chat.resources : [], createdAt: chat.createdAt, updatedAt: chat.updatedAt, ...(streamSnapshot ? { streamSnapshot } : {}), }, }) } catch (error) { logger.error('Error fetching mothership chat:', error) return createInternalServerErrorResponse('Failed to fetch chat') } } ) export const PATCH = withRouteHandler( async (request: NextRequest, context: { params: Promise<{ chatId: string }> }) => { try { const { userId, isAuthenticated } = await authenticateCopilotRequestSessionOnly() if (!isAuthenticated || !userId) { return createUnauthorizedResponse() } const parsed = await parseRequest(updateMothershipChatContract, request, context) if (!parsed.success) return parsed.response const { chatId } = parsed.data.params const { title, isUnread, pinned } = parsed.data.body const updates: Record = {} if (title !== undefined) { const now = new Date() updates.title = title updates.updatedAt = now if (isUnread === undefined) { updates.lastSeenAt = now } } if (isUnread !== undefined) { updates.lastSeenAt = isUnread ? null : sql`GREATEST(${copilotChats.updatedAt}, NOW())` } if (pinned !== undefined) { updates.pinned = pinned } const [updatedChat] = await db .update(copilotChats) .set(updates) .where( and( eq(copilotChats.id, chatId), eq(copilotChats.userId, userId), eq(copilotChats.type, 'mothership') ) ) .returning({ id: copilotChats.id, workspaceId: copilotChats.workspaceId, }) if (!updatedChat) { return NextResponse.json({ success: false, error: 'Chat not found' }, { status: 404 }) } if (updatedChat.workspaceId) { if (title !== undefined) { chatPubSub?.publishStatusChanged({ workspaceId: updatedChat.workspaceId, chatId, type: 'renamed', }) captureServerEvent( userId, 'task_renamed', { workspace_id: updatedChat.workspaceId }, { groups: { workspace: updatedChat.workspaceId }, } ) } if (isUnread === true) { captureServerEvent( userId, 'task_marked_unread', { workspace_id: updatedChat.workspaceId }, { groups: { workspace: updatedChat.workspaceId }, } ) } if (pinned !== undefined) { captureServerEvent( userId, pinned ? 'task_pinned' : 'task_unpinned', { workspace_id: updatedChat.workspaceId }, { groups: { workspace: updatedChat.workspaceId }, } ) } } return NextResponse.json({ success: true }) } catch (error) { logger.error('Error updating mothership chat:', error) return createInternalServerErrorResponse('Failed to update chat') } } ) export const DELETE = withRouteHandler( async (request: NextRequest, context: { params: Promise<{ chatId: string }> }) => { try { const { userId, isAuthenticated } = await authenticateCopilotRequestSessionOnly() if (!isAuthenticated || !userId) { return createUnauthorizedResponse() } const parsed = await parseRequest(deleteMothershipChatContract, request, context) if (!parsed.success) return parsed.response const { chatId } = parsed.data.params const chat = await getAccessibleCopilotChatAuth(chatId, userId) if (!chat || chat.type !== 'mothership') { return NextResponse.json({ success: true }) } const [deletedChat] = await db .delete(copilotChats) .where( and( eq(copilotChats.id, chatId), eq(copilotChats.userId, userId), eq(copilotChats.type, 'mothership') ) ) .returning({ workspaceId: copilotChats.workspaceId, }) if (!deletedChat) { return NextResponse.json({ success: false, error: 'Chat not found' }, { status: 404 }) } if (deletedChat.workspaceId) { chatPubSub?.publishStatusChanged({ workspaceId: deletedChat.workspaceId, chatId, type: 'deleted', }) captureServerEvent( userId, 'task_deleted', { workspace_id: deletedChat.workspaceId }, { groups: { workspace: deletedChat.workspaceId }, } ) } return NextResponse.json({ success: true }) } catch (error) { logger.error('Error deleting mothership chat:', error) return createInternalServerErrorResponse('Failed to delete chat') } } )