Merge origin/main with local UI refactor integration

This commit is contained in:
2026-02-25 00:38:19 -05:00
42 changed files with 4886 additions and 188 deletions

View File

@@ -5,10 +5,17 @@ import express from "express";
import cors from "cors";
import { z } from "zod";
import { buildSessionGraphInsight, buildSessionSummaries } from "../telemetry/session-insights.js";
import { UiConfigStore, type LimitSettings, type RuntimeNotificationSettings, type SecurityPolicySettings } from "../store/config-store.js";
import {
UiConfigStore,
type LimitSettings,
type RuntimeNotificationSettings,
type SecurityPolicySettings,
} from "../store/config-store.js";
import { ManifestStore } from "../agents/manifest-store.js";
import { filterRuntimeEvents, readRuntimeEvents } from "../telemetry/runtime-events-store.js";
import { readRunMetaBySession, UiRunService } from "../runs/run-service.js";
import { filterClaudeTraceEvents, readClaudeTraceEvents } from "./claude-trace-store.js";
import { readRunMetaBySession, UiRunService, type RunExecutionMode } from "../runs/run-service.js";
import type { RunProvider } from "../agents/provider-executor.js";
function parsePort(value: string | undefined): number {
const parsed = Number(value ?? "4317");
@@ -29,14 +36,23 @@ function parseLimit(value: string | null | undefined, fallback: number): number
return parsed;
}
function ensureNonEmptyString(value: unknown, field: string): string {
if (typeof value !== "string" || value.trim().length === 0) {
throw new Error(`Field "${field}" is required.`);
}
return value.trim();
}
async function readRuntimePaths(configStore: UiConfigStore, workspaceRoot: string): Promise<{
stateRoot: string;
runtimeEventLogPath: string;
claudeTraceLogPath: string;
}> {
const snapshot = await configStore.readSnapshot();
return {
stateRoot: resolve(workspaceRoot, snapshot.paths.stateRoot),
runtimeEventLogPath: resolve(workspaceRoot, snapshot.paths.runtimeEventLogPath),
claudeTraceLogPath: resolve(workspaceRoot, snapshot.paths.claudeTraceLogPath),
};
}
@@ -53,6 +69,21 @@ const StartRunSchema = z.object({
provider: z.enum(["claude", "codex"]).optional(),
});
type StartRunBody = z.infer<typeof StartRunSchema>;
async function resolveManifestFromRunRequest(input: {
body: StartRunBody;
manifestStore: ManifestStore;
}): Promise<unknown> {
if (input.body.manifest !== undefined) {
return input.body.manifest;
}
if (input.body.manifestPath) {
return (await input.manifestStore.read(input.body.manifestPath)).source;
}
throw new Error("A manifest or manifestPath is required to start a run.");
}
export async function startUiServer(input: {
workspaceRoot: string;
port?: number;
@@ -79,66 +110,82 @@ export async function startUiServer(input: {
try {
const snapshot = await configStore.readSnapshot();
res.json({ ok: true, config: snapshot });
} catch (error) { next(error); }
} catch (error) {
next(error);
}
});
app.put("/api/config/runtime-events", async (req, res, next) => {
try {
const snapshot = await configStore.updateRuntimeEvents(req.body as RuntimeNotificationSettings);
res.json({ ok: true, config: snapshot });
} catch (error) { next(error); }
} catch (error) {
next(error);
}
});
app.put("/api/config/security", async (req, res, next) => {
try {
const snapshot = await configStore.updateSecurityPolicy(req.body as SecurityPolicySettings);
res.json({ ok: true, config: snapshot });
} catch (error) { next(error); }
} catch (error) {
next(error);
}
});
app.put("/api/config/limits", async (req, res, next) => {
try {
const snapshot = await configStore.updateLimits(req.body as LimitSettings);
res.json({ ok: true, config: snapshot });
} catch (error) { next(error); }
} catch (error) {
next(error);
}
});
app.get("/api/manifests", async (req, res, next) => {
try {
const listing = await manifestStore.list();
res.json({ ok: true, manifests: listing.paths });
} catch (error) { next(error); }
} catch (error) {
next(error);
}
});
app.get("/api/manifests/read", async (req, res, next) => {
try {
const manifestPath = req.query.path as string;
const manifestPath = req.query.path as string | undefined;
if (!manifestPath) {
res.status(400).json({ ok: false, error: 'Query parameter "path" is required.' });
return;
}
const record = await manifestStore.read(manifestPath);
res.json({ ok: true, manifest: record });
} catch (error) { next(error); }
} catch (error) {
next(error);
}
});
app.post("/api/manifests/validate", async (req, res, next) => {
try {
const manifest = await manifestStore.validate(req.body.manifest);
const manifest = await manifestStore.validate((req.body as { manifest?: unknown }).manifest);
res.json({ ok: true, manifest });
} catch (error) { next(error); }
} catch (error) {
next(error);
}
});
app.put("/api/manifests/save", async (req, res, next) => {
try {
const { path, manifest } = req.body;
const { path, manifest } = req.body as { path?: unknown; manifest?: unknown };
if (!path || typeof path !== "string") {
res.status(400).json({ ok: false, error: 'Field "path" is required.' });
return;
}
const record = await manifestStore.save(path, manifest);
res.json({ ok: true, manifest: record });
} catch (error) { next(error); }
} catch (error) {
next(error);
}
});
app.get("/api/runtime-events", async (req, res, next) => {
@@ -151,15 +198,45 @@ export async function startUiServer(input: {
limit,
});
res.json({ ok: true, events });
} catch (error) { next(error); }
} catch (error) {
next(error);
}
});
app.get("/api/claude-trace", async (req, res, next) => {
try {
const { claudeTraceLogPath } = await readRuntimePaths(configStore, workspaceRoot);
const limit = parseLimit(req.query.limit as string | undefined, 200);
const sessionId = (req.query.sessionId as string) || undefined;
const events = filterClaudeTraceEvents(await readClaudeTraceEvents(claudeTraceLogPath), {
...(sessionId ? { sessionId } : {}),
limit,
});
res.json({ ok: true, events });
} catch (error) {
next(error);
}
});
app.post("/api/sessions", async (req, res, next) => {
try {
const projectPath = ensureNonEmptyString((req.body as { projectPath?: unknown }).projectPath, "projectPath");
const session = await runService.createSession({ projectPath });
res.status(201).json({ ok: true, session });
} catch (error) {
next(error);
}
});
app.get("/api/sessions", async (req, res, next) => {
try {
const { stateRoot, runtimeEventLogPath } = await readRuntimePaths(configStore, workspaceRoot);
const sessions = await buildSessionSummaries({ stateRoot, runtimeEventLogPath });
res.json({ ok: true, sessions, runs: runService.listRuns() });
} catch (error) { next(error); }
const metadata = await runService.listSessions();
res.json({ ok: true, sessions, sessionMetadata: metadata, runs: runService.listRuns() });
} catch (error) {
next(error);
}
});
app.get("/api/sessions/graph", async (req, res, next) => {
@@ -176,7 +253,10 @@ export async function startUiServer(input: {
const manifestPath = explicitManifestPath ?? runMeta?.manifestPath;
if (!manifestPath) {
res.status(400).json({ ok: false, error: "No manifestPath available for this session. Provide one in query string." });
res.status(400).json({
ok: false,
error: "No manifestPath available for this session. Provide one in query string.",
});
return;
}
@@ -189,7 +269,68 @@ export async function startUiServer(input: {
});
res.json({ ok: true, graph, manifestPath: manifestRecord.path });
} catch (error) { next(error); }
} catch (error) {
next(error);
}
});
app.post("/api/sessions/:sessionId/run", async (req, res, next) => {
try {
const sessionId = req.params.sessionId;
if (!sessionId) {
res.status(400).json({ ok: false, error: "Session id is required." });
return;
}
const parseResult = StartRunSchema.safeParse(req.body);
if (!parseResult.success) {
res.status(400).json({ ok: false, error: parseResult.error.issues[0]?.message ?? "Invalid body" });
return;
}
const body = parseResult.data;
const manifest = await resolveManifestFromRunRequest({ body, manifestStore });
const record = await runService.startRun({
prompt: body.prompt,
manifest,
manifestPath: body.manifestPath,
sessionId,
topologyHint: body.topologyHint,
initialFlags: body.initialFlags ?? {},
runtimeContextOverrides: body.runtimeContextOverrides ?? {},
simulateValidationNodeIds: body.simulateValidationNodeIds ?? [],
executionMode: (body.executionMode ?? "mock") as RunExecutionMode,
provider: (body.provider ?? "codex") as RunProvider,
});
res.status(202).json({ ok: true, run: record });
} catch (error) {
next(error);
}
});
app.post("/api/sessions/:sessionId/close", async (req, res, next) => {
try {
const sessionId = req.params.sessionId;
if (!sessionId) {
res.status(400).json({ ok: false, error: "Session id is required." });
return;
}
const mergeToProject =
typeof (req.body as { mergeToProject?: unknown } | undefined)?.mergeToProject === "boolean"
? ((req.body as { mergeToProject: boolean }).mergeToProject)
: false;
const session = await runService.closeSession({
sessionId,
mergeToProject,
});
res.json({ ok: true, session });
} catch (error) {
next(error);
}
});
app.get("/api/runs", (req, res) => {
@@ -200,23 +341,12 @@ export async function startUiServer(input: {
try {
const parseResult = StartRunSchema.safeParse(req.body);
if (!parseResult.success) {
res.status(400).json({ ok: false, error: parseResult.error.issues[0]?.message || 'Invalid body' });
res.status(400).json({ ok: false, error: parseResult.error.issues[0]?.message ?? "Invalid body" });
return;
}
const body = parseResult.data;
let manifest: unknown;
if (body.manifest !== undefined) {
manifest = body.manifest;
} else if (body.manifestPath) {
manifest = (await manifestStore.read(body.manifestPath)).source;
}
if (!manifest) {
res.status(400).json({ ok: false, error: "A manifest or manifestPath is required to start a run." });
return;
}
const manifest = await resolveManifestFromRunRequest({ body, manifestStore });
const record = await runService.startRun({
prompt: body.prompt,
manifest,
@@ -226,12 +356,14 @@ export async function startUiServer(input: {
initialFlags: body.initialFlags ?? {},
runtimeContextOverrides: body.runtimeContextOverrides ?? {},
simulateValidationNodeIds: body.simulateValidationNodeIds ?? [],
executionMode: body.executionMode ?? "mock",
provider: body.provider ?? "codex",
executionMode: (body.executionMode ?? "mock") as RunExecutionMode,
provider: (body.provider ?? "codex") as RunProvider,
});
res.status(202).json({ ok: true, run: record });
} catch (error) { next(error); }
} catch (error) {
next(error);
}
});
app.post("/api/runs/:runId/cancel", async (req, res, next) => {
@@ -243,23 +375,23 @@ export async function startUiServer(input: {
}
const run = await runService.cancelRun(runId);
res.json({ ok: true, run });
} catch (error) { next(error); }
} catch (error) {
next(error);
}
});
app.get("/api/runs/:runId", async (req, res, next) => {
try {
const runId = req.params.runId;
if (!runId) {
res.status(400).json({ ok: false, error: "runId required" });
return;
}
const run = runService.getRun(runId);
if (!run) {
res.status(404).json({ ok: false, error: `Run "${runId}" was not found.` });
return;
}
res.json({ ok: true, run });
} catch (error) { next(error); }
app.get("/api/runs/:runId", (req, res) => {
const runId = req.params.runId;
if (!runId) {
res.status(400).json({ ok: false, error: "runId required" });
return;
}
const run = runService.getRun(runId);
if (!run) {
res.status(404).json({ ok: false, error: `Run "${runId}" was not found.` });
return;
}
res.json({ ok: true, run });
});
app.use("/api", (req, res) => {
@@ -269,15 +401,13 @@ export async function startUiServer(input: {
});
});
// Default error handler
app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => {
app.use((err: unknown, req: express.Request, res: express.Response, next: express.NextFunction) => {
res.status(400).json({
ok: false,
error: err instanceof Error ? err.message : String(err),
});
});
// Serve static files
app.use(express.static(staticRoot));
app.get(/(.*)/, (req, res) => {
res.sendFile(resolve(staticRoot, "index.html"));