POST /api/sessions/:sessionId/pending/create queues a pending_changes create via queueCreate (WriteGuardError -> 422 with the guard message). RightRail gains a 'New file from pasted text' modal (path + content) wired through api.coder.createPendingFile; sessionId is threaded down from App.tsx. The staged change shows in the CoderPane DiffPanel for explicit apply. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
173 lines
5.1 KiB
TypeScript
173 lines
5.1 KiB
TypeScript
import type { FastifyInstance } from 'fastify';
|
|
import { z } from 'zod';
|
|
import type { Sql } from '../db.js';
|
|
import {
|
|
listPending,
|
|
applyOne,
|
|
applyAll,
|
|
rejectOne,
|
|
rewindOne,
|
|
queueCreate,
|
|
} from '../services/pending_changes.js';
|
|
import { WriteGuardError } from '../services/write_guard.js';
|
|
|
|
const CreateBody = z.object({
|
|
file_path: z.string().min(1),
|
|
content: z.string(),
|
|
});
|
|
|
|
/**
|
|
* Resolve project root from a session's project path.
|
|
*/
|
|
async function resolveProjectRoot(sql: Sql, sessionId: string): Promise<string | null> {
|
|
const rows = await sql<{ path: string }[]>`
|
|
SELECT p.path FROM sessions s
|
|
JOIN projects p ON s.project_id = p.id
|
|
WHERE s.id = ${sessionId}
|
|
`;
|
|
return rows.length > 0 ? rows[0]!.path : null;
|
|
}
|
|
|
|
/**
|
|
* Resolve project root from a pending change's session.
|
|
*/
|
|
async function resolveProjectRootForChange(sql: Sql, changeId: string): Promise<string | null> {
|
|
const rows = await sql<{ path: string }[]>`
|
|
SELECT p.path FROM pending_changes pc
|
|
JOIN sessions s ON pc.session_id = s.id
|
|
JOIN projects p ON s.project_id = p.id
|
|
WHERE pc.id = ${changeId}
|
|
`;
|
|
return rows.length > 0 ? rows[0]!.path : null;
|
|
}
|
|
|
|
export function registerPendingRoutes(app: FastifyInstance, sql: Sql): void {
|
|
// GET /api/sessions/:sessionId/pending — list pending changes for a session
|
|
app.get<{ Params: { sessionId: string } }>(
|
|
'/api/sessions/:sessionId/pending',
|
|
async (req, reply) => {
|
|
const sessionId = req.params.sessionId;
|
|
|
|
const session = await sql<{ id: string }[]>`SELECT id FROM sessions WHERE id = ${sessionId}`;
|
|
if (session.length === 0) {
|
|
reply.code(404);
|
|
return { error: 'session not found' };
|
|
}
|
|
|
|
const pending = await listPending(sql, sessionId);
|
|
return pending;
|
|
},
|
|
);
|
|
|
|
// POST /api/sessions/:sessionId/pending/create — queue a new-file create
|
|
// (manual create from the RightRail file browser; no inference involved).
|
|
// queueCreate runs resolveWritePath internally, so a path that escapes the
|
|
// project root or hits a secret file throws WriteGuardError → 422 with the
|
|
// guard message. Mirrors the { error } 404 shape used by the other routes
|
|
// and the 422 status used by apply/rewind on failure.
|
|
app.post<{ Params: { sessionId: string } }>(
|
|
'/api/sessions/:sessionId/pending/create',
|
|
async (req, reply) => {
|
|
const sessionId = req.params.sessionId;
|
|
|
|
const parsed = CreateBody.safeParse(req.body);
|
|
if (!parsed.success) {
|
|
reply.code(400);
|
|
return { error: 'invalid body', details: parsed.error.flatten() };
|
|
}
|
|
|
|
const projectRoot = await resolveProjectRoot(sql, sessionId);
|
|
if (!projectRoot) {
|
|
reply.code(404);
|
|
return { error: 'session or project not found' };
|
|
}
|
|
|
|
try {
|
|
const change = await queueCreate(
|
|
sql,
|
|
sessionId,
|
|
null,
|
|
parsed.data.file_path,
|
|
parsed.data.content,
|
|
projectRoot,
|
|
);
|
|
return change;
|
|
} catch (err) {
|
|
if (err instanceof WriteGuardError) {
|
|
reply.code(422);
|
|
return { error: err.message };
|
|
}
|
|
throw err;
|
|
}
|
|
},
|
|
);
|
|
|
|
// POST /api/sessions/:sessionId/pending/apply — apply all pending changes
|
|
app.post<{ Params: { sessionId: string } }>(
|
|
'/api/sessions/:sessionId/pending/apply',
|
|
async (req, reply) => {
|
|
const sessionId = req.params.sessionId;
|
|
|
|
const projectRoot = await resolveProjectRoot(sql, sessionId);
|
|
if (!projectRoot) {
|
|
reply.code(404);
|
|
return { error: 'session or project not found' };
|
|
}
|
|
|
|
const results = await applyAll(sql, sessionId, projectRoot);
|
|
return { results };
|
|
},
|
|
);
|
|
|
|
// POST /api/pending/:id/apply — apply a single pending change
|
|
app.post<{ Params: { id: string } }>(
|
|
'/api/pending/:id/apply',
|
|
async (req, reply) => {
|
|
const changeId = req.params.id;
|
|
|
|
const projectRoot = await resolveProjectRootForChange(sql, changeId);
|
|
if (!projectRoot) {
|
|
reply.code(404);
|
|
return { error: 'pending change or project not found' };
|
|
}
|
|
|
|
const result = await applyOne(sql, changeId, projectRoot);
|
|
if (!result.success) {
|
|
reply.code(422);
|
|
}
|
|
return result;
|
|
},
|
|
);
|
|
|
|
// POST /api/pending/:id/reject — reject a single pending change
|
|
app.post<{ Params: { id: string } }>(
|
|
'/api/pending/:id/reject',
|
|
async (req, reply) => {
|
|
const changeId = req.params.id;
|
|
|
|
await rejectOne(sql, changeId);
|
|
return { ok: true };
|
|
},
|
|
);
|
|
|
|
// POST /api/pending/:id/rewind — rewind (undo) an applied change
|
|
app.post<{ Params: { id: string } }>(
|
|
'/api/pending/:id/rewind',
|
|
async (req, reply) => {
|
|
const changeId = req.params.id;
|
|
|
|
const projectRoot = await resolveProjectRootForChange(sql, changeId);
|
|
if (!projectRoot) {
|
|
reply.code(404);
|
|
return { error: 'pending change or project not found' };
|
|
}
|
|
|
|
const result = await rewindOne(sql, changeId, projectRoot);
|
|
if (!result.success) {
|
|
reply.code(422);
|
|
}
|
|
return result;
|
|
},
|
|
);
|
|
}
|