Files
Codewalkers/apps/server/db/repositories/drizzle/chat-session.ts
Lukas May fcf822363c feat: Add persistent chat sessions for iterative phase/task refinement
Introduces a chat loop where users send instructions to an agent that
applies changes (create/update/delete phases, tasks, pages) and stays
alive for follow-up messages. Includes schema + migration, repository
layer, chat prompt, file-io action field extension, output handler chat
mode, revert support for deletes, tRPC procedures, events, frontend
slide-over UI with inline changeset display and revert, and docs.
2026-03-04 10:14:28 +01:00

108 lines
3.3 KiB
TypeScript

/**
* Drizzle Chat Session Repository Adapter
*
* Implements ChatSessionRepository interface using Drizzle ORM.
*/
import { eq, and, asc } from 'drizzle-orm';
import { nanoid } from 'nanoid';
import type { DrizzleDatabase } from '../../index.js';
import { chatSessions, chatMessages, type ChatSession, type ChatMessage } from '../../schema.js';
import type { ChatSessionRepository, CreateChatSessionData, CreateChatMessageData } from '../chat-session-repository.js';
export class DrizzleChatSessionRepository implements ChatSessionRepository {
constructor(private db: DrizzleDatabase) {}
async createSession(data: CreateChatSessionData): Promise<ChatSession> {
const now = new Date();
const id = nanoid();
await this.db.insert(chatSessions).values({
id,
targetType: data.targetType,
targetId: data.targetId,
initiativeId: data.initiativeId,
agentId: data.agentId ?? null,
status: 'active',
createdAt: now,
updatedAt: now,
});
return this.findSessionById(id) as Promise<ChatSession>;
}
async findSessionById(id: string): Promise<ChatSession | null> {
const rows = await this.db
.select()
.from(chatSessions)
.where(eq(chatSessions.id, id))
.limit(1);
return rows[0] ?? null;
}
async findActiveSession(targetType: 'phase' | 'task', targetId: string): Promise<ChatSession | null> {
const rows = await this.db
.select()
.from(chatSessions)
.where(
and(
eq(chatSessions.targetType, targetType),
eq(chatSessions.targetId, targetId),
eq(chatSessions.status, 'active' as 'active' | 'closed'),
),
)
.limit(1);
return rows[0] ?? null;
}
async findActiveSessionByAgentId(agentId: string): Promise<ChatSession | null> {
const rows = await this.db
.select()
.from(chatSessions)
.where(
and(
eq(chatSessions.agentId, agentId),
eq(chatSessions.status, 'active' as 'active' | 'closed'),
),
)
.limit(1);
return rows[0] ?? null;
}
async updateSession(id: string, data: { agentId?: string | null; status?: 'active' | 'closed' }): Promise<ChatSession> {
const updates: Record<string, unknown> = { updatedAt: new Date() };
if (data.agentId !== undefined) updates.agentId = data.agentId;
if (data.status !== undefined) updates.status = data.status;
await this.db
.update(chatSessions)
.set(updates)
.where(eq(chatSessions.id, id));
return this.findSessionById(id) as Promise<ChatSession>;
}
async createMessage(data: CreateChatMessageData): Promise<ChatMessage> {
const id = nanoid();
const now = new Date();
await this.db.insert(chatMessages).values({
id,
chatSessionId: data.chatSessionId,
role: data.role,
content: data.content,
changeSetId: data.changeSetId ?? null,
createdAt: now,
});
const rows = await this.db
.select()
.from(chatMessages)
.where(eq(chatMessages.id, id))
.limit(1);
return rows[0]!;
}
async findMessagesBySessionId(sessionId: string): Promise<ChatMessage[]> {
return this.db
.select()
.from(chatMessages)
.where(eq(chatMessages.chatSessionId, sessionId))
.orderBy(asc(chatMessages.createdAt));
}
}