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:
2026-05-15 20:39:48 +00:00
parent 6d9515b8a5
commit c35ec65fc4
37 changed files with 3290 additions and 1012 deletions

View File

@@ -5,7 +5,6 @@ import type { Config } from '../config.js';
import type { Broker } from '../services/broker.js';
import type { Session } from '../types/api.js';
import { getSetting } from './settings.js';
import { requireUser } from '../auth.js';
const CreateBody = z.object({
name: z.string().min(1).max(200).optional(),
@@ -31,7 +30,7 @@ export function registerSessionRoutes(
config: Config,
broker: Broker
): void {
app.get<{ Params: { id: string } }>(
app.get<{ Params: { id: string }; Querystring: { status?: string } }>(
'/api/projects/:id/sessions',
async (req, reply) => {
const project = await sql`SELECT id FROM projects WHERE id = ${req.params.id}`;
@@ -39,10 +38,11 @@ export function registerSessionRoutes(
reply.code(404);
return { error: 'project not found' };
}
const status = req.query.status === 'archived' ? 'archived' : 'open';
const rows = await sql<Session[]>`
SELECT id, project_id, name, model, system_prompt, created_at, updated_at
SELECT id, project_id, name, model, system_prompt, status, created_at, updated_at
FROM sessions
WHERE project_id = ${req.params.id}
WHERE project_id = ${req.params.id} AND status = ${status}
ORDER BY updated_at DESC
`;
return rows;
@@ -81,15 +81,15 @@ export function registerSessionRoutes(
const [session] = await tx<Session[]>`
INSERT INTO sessions (project_id, name, model, system_prompt)
VALUES (${req.params.id}, ${name}, ${model}, ${systemPrompt})
RETURNING id, project_id, name, model, system_prompt, created_at, updated_at
RETURNING id, project_id, name, model, system_prompt, status, created_at, updated_at
`;
await tx`
INSERT INTO session_panes (session_id, position, kind, state)
VALUES (${session!.id}, 0, 'chat', '{}'::jsonb)
INSERT INTO chats (session_id, name, status)
VALUES (${session!.id}, NULL, 'open')
`;
return session!;
});
broker.publishUser(requireUser(req), {
broker.publishUser('default', {
type: 'session_created',
session: row,
project_id: row.project_id,
@@ -101,7 +101,7 @@ export function registerSessionRoutes(
app.get<{ Params: { id: string } }>('/api/sessions/:id', async (req, reply) => {
const rows = await sql<Session[]>`
SELECT id, project_id, name, model, system_prompt, created_at, updated_at
SELECT id, project_id, name, model, system_prompt, status, created_at, updated_at
FROM sessions WHERE id = ${req.params.id}
`;
if (rows.length === 0) {
@@ -128,7 +128,7 @@ export function registerSessionRoutes(
system_prompt = COALESCE(${system_prompt ?? null}, system_prompt),
updated_at = clock_timestamp()
WHERE id = ${req.params.id}
RETURNING id, project_id, name, model, system_prompt, created_at, updated_at
RETURNING id, project_id, name, model, system_prompt, status, created_at, updated_at
`;
if (rows.length === 0) {
reply.code(404);
@@ -138,6 +138,51 @@ export function registerSessionRoutes(
}
);
app.post<{ Params: { id: string } }>(
'/api/sessions/:id/archive',
async (req, reply) => {
const rows = await sql<{ id: string; project_id: string }[]>`
UPDATE sessions SET status = 'archived', updated_at = clock_timestamp()
WHERE id = ${req.params.id} AND status = 'open'
RETURNING id, project_id
`;
if (rows.length === 0) {
reply.code(404);
return { error: 'session not found or already archived' };
}
broker.publishUser('default', {
type: 'session_archived',
session_id: rows[0]!.id,
project_id: rows[0]!.project_id,
});
reply.code(204);
return null;
}
);
app.post<{ Params: { id: string } }>(
'/api/sessions/:id/unarchive',
async (req, reply) => {
const rows = await sql<Session[]>`
UPDATE sessions SET status = 'open', updated_at = clock_timestamp()
WHERE id = ${req.params.id} AND status = 'archived'
RETURNING id, project_id, name, model, system_prompt, status, created_at, updated_at
`;
if (rows.length === 0) {
reply.code(404);
return { error: 'session not found or not archived' };
}
const session = rows[0]!;
broker.publishUser('default', {
type: 'session_created',
session: session,
project_id: session.project_id,
});
reply.code(200);
return session;
}
);
app.delete<{ Params: { id: string } }>(
'/api/sessions/:id',
async (req, reply) => {
@@ -150,7 +195,7 @@ export function registerSessionRoutes(
return { error: 'not found' };
}
const project_id = deleted[0]!.project_id;
broker.publishUser(requireUser(req), { type: 'session_deleted', session_id: id, project_id });
broker.publishUser('default', { type: 'session_deleted', session_id: id, project_id });
reply.code(204);
return null;
}