batch4: chats-in-sessions, force-send, /compact, right-rail file browser
Session 1:N Chat data model with backfill. Workspace switches to client-side multi-tab pane management. Right-rail file browser with float-over viewer and click-drag line selection replaces FileBrowserPane. Adds /compact streaming summarizer (respects compact markers in context builder), force-send (cancels in-flight, persists partial as 'cancelled', awaits cancellation completion via deferred Promise + 5s timeout), message queue, stop generation, chat auto-rename, session archive/unarchive with Closed Sessions section on repo landing page. CHECK constraints on sessions.status, messages.role, messages.status with KEEP IN SYNC comments tying to MESSAGE_ROLES / MESSAGE_STATUSES const arrays. Deletes dead pane routes/hook and the api.panes.* client block. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
147
apps/server/src/routes/chats.ts
Normal file
147
apps/server/src/routes/chats.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import { z } from 'zod';
|
||||
import type { Sql } from '../db.js';
|
||||
import type { Broker } from '../services/broker.js';
|
||||
import type { Chat, Message } from '../types/api.js';
|
||||
|
||||
const CreateBody = z.object({
|
||||
name: z.string().min(1).max(200).optional(),
|
||||
});
|
||||
|
||||
const PatchBody = z.object({
|
||||
name: z.string().min(1).max(200).optional(),
|
||||
status: z.enum(['open', 'closed']).optional(),
|
||||
});
|
||||
|
||||
export function registerChatRoutes(
|
||||
app: FastifyInstance,
|
||||
sql: Sql,
|
||||
broker: Broker
|
||||
): void {
|
||||
app.get<{ Params: { id: string } }>(
|
||||
'/api/sessions/:id/chats',
|
||||
async (req, reply) => {
|
||||
const session = await sql`SELECT id FROM sessions WHERE id = ${req.params.id}`;
|
||||
if (session.length === 0) {
|
||||
reply.code(404);
|
||||
return { error: 'session not found' };
|
||||
}
|
||||
const rows = await sql<Chat[]>`
|
||||
SELECT id, session_id, name, status, created_at, updated_at
|
||||
FROM chats
|
||||
WHERE session_id = ${req.params.id}
|
||||
ORDER BY updated_at DESC
|
||||
`;
|
||||
return rows;
|
||||
}
|
||||
);
|
||||
|
||||
app.post<{ Params: { id: string } }>(
|
||||
'/api/sessions/:id/chats',
|
||||
async (req, reply) => {
|
||||
const parsed = CreateBody.safeParse(req.body ?? {});
|
||||
if (!parsed.success) {
|
||||
reply.code(400);
|
||||
return { error: 'invalid body', details: parsed.error.flatten() };
|
||||
}
|
||||
const session = await sql`SELECT id FROM sessions WHERE id = ${req.params.id}`;
|
||||
if (session.length === 0) {
|
||||
reply.code(404);
|
||||
return { error: 'session not found' };
|
||||
}
|
||||
const [chat] = await sql<Chat[]>`
|
||||
INSERT INTO chats (session_id, name, status)
|
||||
VALUES (${req.params.id}, ${parsed.data.name ?? null}, 'open')
|
||||
RETURNING id, session_id, name, status, created_at, updated_at
|
||||
`;
|
||||
broker.publishUser('default', {
|
||||
type: 'chat_created',
|
||||
chat: chat!,
|
||||
session_id: req.params.id,
|
||||
});
|
||||
reply.code(201);
|
||||
return chat;
|
||||
}
|
||||
);
|
||||
|
||||
app.patch<{ Params: { id: string } }>(
|
||||
'/api/chats/:id',
|
||||
async (req, reply) => {
|
||||
const parsed = PatchBody.safeParse(req.body ?? {});
|
||||
if (!parsed.success) {
|
||||
reply.code(400);
|
||||
return { error: 'invalid body', details: parsed.error.flatten() };
|
||||
}
|
||||
const { name, status } = parsed.data;
|
||||
if (name === undefined && status === undefined) {
|
||||
reply.code(400);
|
||||
return { error: 'must provide name or status' };
|
||||
}
|
||||
const rows = await sql<Chat[]>`
|
||||
UPDATE chats
|
||||
SET
|
||||
name = COALESCE(${name ?? null}, name),
|
||||
status = COALESCE(${status ?? null}, status),
|
||||
updated_at = clock_timestamp()
|
||||
WHERE id = ${req.params.id}
|
||||
RETURNING id, session_id, name, status, created_at, updated_at
|
||||
`;
|
||||
if (rows.length === 0) {
|
||||
reply.code(404);
|
||||
return { error: 'chat not found' };
|
||||
}
|
||||
const chat = rows[0]!;
|
||||
if (status === 'closed') {
|
||||
broker.publishUser('default', {
|
||||
type: 'chat_closed',
|
||||
chat_id: chat.id,
|
||||
session_id: chat.session_id,
|
||||
});
|
||||
} else {
|
||||
broker.publishUser('default', {
|
||||
type: 'chat_updated',
|
||||
chat_id: chat.id,
|
||||
session_id: chat.session_id,
|
||||
name: chat.name,
|
||||
updated_at: chat.updated_at,
|
||||
});
|
||||
}
|
||||
return chat;
|
||||
}
|
||||
);
|
||||
|
||||
app.delete<{ Params: { id: string } }>(
|
||||
'/api/chats/:id',
|
||||
async (req, reply) => {
|
||||
const result = await sql<{ id: string; session_id: string }[]>`
|
||||
DELETE FROM chats WHERE id = ${req.params.id}
|
||||
RETURNING id, session_id
|
||||
`;
|
||||
if (result.length === 0) {
|
||||
reply.code(404);
|
||||
return { error: 'chat not found' };
|
||||
}
|
||||
reply.code(204);
|
||||
return null;
|
||||
}
|
||||
);
|
||||
|
||||
app.get<{ Params: { id: string } }>(
|
||||
'/api/chats/:id/messages',
|
||||
async (req, reply) => {
|
||||
const chat = await sql`SELECT id FROM chats WHERE id = ${req.params.id}`;
|
||||
if (chat.length === 0) {
|
||||
reply.code(404);
|
||||
return { error: 'chat not found' };
|
||||
}
|
||||
const rows = await sql<Message[]>`
|
||||
SELECT id, session_id, chat_id, role, content, kind, tool_calls, tool_results, status, last_seq,
|
||||
tokens_used, ctx_used, ctx_max, started_at, finished_at, created_at
|
||||
FROM messages
|
||||
WHERE chat_id = ${req.params.id}
|
||||
ORDER BY created_at ASC, id ASC
|
||||
`;
|
||||
return rows;
|
||||
}
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user