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.
108 lines
3.3 KiB
TypeScript
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));
|
|
}
|
|
}
|