import { randomBytes } from "crypto" import { existsSync, readFileSync, statSync } from "fs" import { fileURLToPath } from "url" import { dirname, extname, isAbsolute, relative, resolve, sep } from "path" import { ctx } from "../ctx" import type { EditableDeck } from "../edit/resolve-deck" import { buildEditPrompt, type EditCommentPayload } from "../edit/prompt" import { buildPrompt } from "../prompt-builder" import type { InspectionElementSnapshot } from "../inspection-context/match" import { buildInspectionPrompt } from "../inspect/prompt" import { projectWorkspaceElement } from "../inspect/request" import { addInspectRequestEvent, completeInspectRequest, createInspectRequest, failInspectRequest, getInspectRequest, subscribeInspectRequestEvents } from "../inspect/requests" import { saveMediaAsset } from "../media/save" import { searchRemoteImages, type ImageCandidate } from "../media/search" import type { MediaAssetRecord, MediaPurpose } from "../media/types" import { addCommentRequestEvent, completeCommentRequest, createCommentRequest, failCommentRequest, getCommentRequest, stopCommentRequest, subscribeCommentRequestEvents } from "./comment-requests" import { createOpenCodeReviewPromptBridge, type ReviewPromptBridge } from "./prompt-bridge" import { suppressReviewApplyFixArtifactQa } from "./qa-suppression" import { createReviewComment, deleteReviewComment, listReviewComments, markReviewCommentApplied, markReviewCommentApplying, markReviewCommentFailed, markReviewCommentQueued, markReviewCommentStopped, readReviewComment, type ReviewCommentRecord } from "./review-comments" import { annotateVisualEditTargets, applyVisualTargetChanges, type VisualEditTarget } from "./visual-targets" const TOKEN_BYTES = 24 const SESSION_TTL_MS = 2 * 60 * 60 * 1000 const IDLE_STOP_MS = 30 * 60 * 1000 const REVIEW_UI_ASSET_ROUTE = "/__revela_ui_asset" const REVIEW_UI_ASSETS = new Map([ ["/logo-wordmark.png", resolve(dirname(fileURLToPath(import.meta.url)), "../../assets/img/logo-wordmark.png")], ]) export const REVIEW_REF_LABEL_MAX_DISPLAY_CHARS = 32 export const LIVE_EDITOR_IDLE_MS = 10 * 1000 export function displayReviewReferenceLabel(label: string): string { const text = String(label || "") return text.length > REVIEW_REF_LABEL_MAX_DISPLAY_CHARS ? text.slice(0, REVIEW_REF_LABEL_MAX_DISPLAY_CHARS - 1) + "…" : text } interface EditAsset { id: string absoluteFile: string } interface EditSession { token: string client?: any sessionID?: string promptBridge: ReviewPromptBridge deck: string file: string absoluteFile: string workspaceRoot: string assets: Map assetKeys: Map nextAssetId: number createdAt: number lastActiveAt: number defaultMode: RefineMode visualTargets: Map visualTargetDeckVersion?: string activeApplyCommentId?: string activeApplyRequestId?: string activeApplyController?: AbortController applyQueue: string[] } export type RefineMode = "edit" | "inspect" export type ReviewShellSurface = "legacy" | "codex" function lucideIcon(name: "image" | "list" | "play" | "plus" | "refresh-cw" | "send" | "square" | "trash-2", className = "composer-icon"): string { const paths: Record = { image: '', list: '', play: '', plus: '', "refresh-cw": '', send: '', square: '', "trash-2": '', } return `` } export interface RefineServerHandle { baseUrl: string getOrCreateSession(input: { client?: any sessionID?: string workspaceRoot: string deck: EditableDeck mode?: RefineMode promptBridge?: ReviewPromptBridge }): EditServerSessionResult } export interface EditServerSessionResult { token: string reused: boolean live: boolean } let server: ReturnType | undefined let baseUrl = "" let idleTimer: Timer | undefined const sessions = new Map() export function startRefineServer(): RefineServerHandle { if (!server) { server = Bun.serve({ hostname: "127.0.0.1", port: 0, fetch: handleRequest, }) baseUrl = `http://127.0.0.1:${server.port}` scheduleIdleStop() } return { baseUrl, getOrCreateSession(input) { cleanupExpiredSessions() const existing = findSessionForDeck(input.deck.absoluteFile) if (existing) { existing.session.client = input.client existing.session.sessionID = input.sessionID existing.session.promptBridge = input.promptBridge ?? createOpenCodeReviewPromptBridge(input.client, input.sessionID ?? "") existing.session.deck = input.deck.slug existing.session.file = input.deck.file existing.session.workspaceRoot = resolve(input.workspaceRoot) existing.session.defaultMode = input.mode ?? "edit" existing.session.visualTargets = existing.session.visualTargets ?? new Map() existing.session.applyQueue = existing.session.applyQueue ?? [] return { token: existing.token, reused: true, live: isSessionLive(existing.session), } } const token = randomBytes(TOKEN_BYTES).toString("base64url") sessions.set(token, { token, client: input.client, sessionID: input.sessionID, promptBridge: input.promptBridge ?? createOpenCodeReviewPromptBridge(input.client, input.sessionID ?? ""), deck: input.deck.slug, file: input.deck.file, absoluteFile: input.deck.absoluteFile, workspaceRoot: resolve(input.workspaceRoot), assets: new Map(), assetKeys: new Map(), nextAssetId: 1, createdAt: Date.now(), lastActiveAt: Date.now(), defaultMode: input.mode ?? "edit", visualTargets: new Map(), applyQueue: [], }) return { token, reused: false, live: false } }, } } export function hasLiveEditorSession(deck: EditableDeck, maxIdleMs = LIVE_EDITOR_IDLE_MS): boolean { cleanupExpiredSessions() const existing = findSessionForDeck(deck.absoluteFile) return existing ? isSessionLive(existing.session, maxIdleMs) : false } export function hasLiveEditorSessionForFile(workspaceRoot: string, filePath: string, maxIdleMs = LIVE_EDITOR_IDLE_MS): boolean { if (!filePath) return false const root = resolve(workspaceRoot) const absoluteFile = resolve(root, filePath) if (absoluteFile !== root && !absoluteFile.startsWith(root.endsWith(sep) ? root : root + sep)) return false cleanupExpiredSessions() const existing = findSessionForDeck(absoluteFile) return existing ? isSessionLive(existing.session, maxIdleMs) : false } export function stopRefineServer(): void { if (idleTimer) clearTimeout(idleTimer) idleTimer = undefined sessions.clear() server?.stop() server = undefined baseUrl = "" } export const stopEditServer = stopRefineServer function findSessionForDeck(absoluteFile: string): { token: string; session: EditSession } | undefined { for (const [token, session] of sessions) { if (session.absoluteFile === absoluteFile) return { token, session } } return undefined } function isSessionLive(session: EditSession, maxIdleMs = LIVE_EDITOR_IDLE_MS): boolean { return Date.now() - session.lastActiveAt <= maxIdleMs } async function handleRequest(req: Request): Promise { cleanupExpiredSessions() const url = new URL(req.url) if (url.pathname === "/health") return textResponse("ok") if (url.pathname.startsWith(`${REVIEW_UI_ASSET_ROUTE}/`) && (req.method === "GET" || req.method === "HEAD")) { return handleReviewUiAsset(url.pathname, req.method) } if (url.pathname === "/refine" && req.method === "GET") { const session = validateSession(url.searchParams.get("token")) if (!session.ok) return session.response return htmlResponse(renderRefineShell(session.value.token, session.value.defaultMode)) } if (url.pathname === "/codex-review" && req.method === "GET") { const session = validateSession(url.searchParams.get("token")) if (!session.ok) return session.response return htmlResponse(renderCodexReviewShell(session.value.token, session.value.defaultMode)) } if (url.pathname === "/deck" && req.method === "GET") { const session = validateSession(url.searchParams.get("token")) if (!session.ok) return session.response return handleDeck(session.value) } if (url.pathname === "/__revela_asset" && (req.method === "GET" || req.method === "HEAD")) { const session = validateSession(url.searchParams.get("token")) if (!session.ok) return session.response return handleAsset(session.value, url.searchParams.get("id"), req.method) } if (url.pathname === "/api/comment" && req.method === "POST") { const session = validateSession(url.searchParams.get("token")) if (!session.ok) return session.response if (session.value.activeApplyCommentId) return applyLockedResponse() return handleComment(req, session.value) } if (url.pathname === "/api/comments" && req.method === "GET") { const session = validateSession(url.searchParams.get("token")) if (!session.ok) return session.response return handleReviewCommentsList(session.value) } if (url.pathname === "/api/comments" && req.method === "POST") { const session = validateSession(url.searchParams.get("token")) if (!session.ok) return session.response return handleReviewCommentCreate(req, session.value) } const applyMatch = url.pathname.match(/^\/api\/comments\/([^/]+)\/apply$/) if (applyMatch && req.method === "POST") { const session = validateSession(url.searchParams.get("token")) if (!session.ok) return session.response return handleReviewCommentApply(decodeURIComponent(applyMatch[1]), req, session.value) } const stopMatch = url.pathname.match(/^\/api\/comments\/([^/]+)\/stop$/) if (stopMatch && req.method === "POST") { const session = validateSession(url.searchParams.get("token")) if (!session.ok) return session.response return handleReviewCommentStop(decodeURIComponent(stopMatch[1]), session.value) } const deleteMatch = url.pathname.match(/^\/api\/comments\/([^/]+)$/) if (deleteMatch && req.method === "DELETE") { const session = validateSession(url.searchParams.get("token")) if (!session.ok) return session.response if (session.value.activeApplyCommentId) return applyLockedResponse() return handleReviewCommentDelete(decodeURIComponent(deleteMatch[1]), session.value) } if (url.pathname === "/api/comment-result" && req.method === "GET") { const session = validateSession(url.searchParams.get("token")) if (!session.ok) return session.response return handleCommentResult(url.searchParams.get("requestId"), session.value) } if (url.pathname === "/api/comment-events" && req.method === "GET") { const session = validateSession(url.searchParams.get("token")) if (!session.ok) return session.response return handleCommentEvents(url.searchParams.get("requestId"), session.value) } if (url.pathname === "/api/inspect" && req.method === "POST") { const session = validateSession(url.searchParams.get("token")) if (!session.ok) return session.response return jsonResponse({ ok: false, error: "Review Insight/Inspect was removed in Revela 0.18. Use Comment for deck edits." }, 410) } if (url.pathname === "/api/inspect-result" && req.method === "GET") { const session = validateSession(url.searchParams.get("token")) if (!session.ok) return session.response return jsonResponse({ ok: false, error: "Review Insight/Inspect was removed in Revela 0.18." }, 410) } if (url.pathname === "/api/inspect-events" && req.method === "GET") { const session = validateSession(url.searchParams.get("token")) if (!session.ok) return session.response return jsonResponse({ ok: false, error: "Review Insight/Inspect was removed in Revela 0.18." }, 410) } if (url.pathname === "/api/deck-version" && req.method === "GET") { const session = validateSession(url.searchParams.get("token")) if (!session.ok) return session.response return handleDeckVersion(session.value) } if (url.pathname === "/api/visual-changes" && req.method === "POST") { const session = validateSession(url.searchParams.get("token")) if (!session.ok) return session.response if (session.value.activeApplyCommentId) return applyLockedResponse() return handleVisualChanges(req, session.value) } if (url.pathname === "/api/assets/search" && req.method === "GET") { const session = validateSession(url.searchParams.get("token")) if (!session.ok) return session.response return handleAssetSearch(url, session.value) } if (url.pathname === "/api/assets/save" && req.method === "POST") { const session = validateSession(url.searchParams.get("token")) if (!session.ok) return session.response if (session.value.activeApplyCommentId) return applyLockedResponse() return handleAssetSave(req, session.value) } if (url.pathname === "/api/assets/list" && req.method === "GET") { const session = validateSession(url.searchParams.get("token")) if (!session.ok) return session.response return handleAssetList(session.value) } return textResponse("Not found", 404) } function applyLockedResponse(): Response { return jsonResponse({ ok: false, code: "apply_locked", error: "Wait for the current apply to finish before making another change.", }, 409) } async function handleAssetSearch(url: URL, session: EditSession): Promise { const query = (url.searchParams.get("query") || "").trim() if (!query) return jsonResponse({ ok: false, error: "query is required" }, 400) const purpose = normalizeMediaPurpose(url.searchParams.get("purpose")) const limit = Number(url.searchParams.get("limit") || 12) const page = Number(url.searchParams.get("page") || 1) try { const candidates = await searchRemoteImages({ query, purpose, limit, page }) session.lastActiveAt = Date.now() scheduleIdleStop() return jsonResponse({ ok: true, candidates }) } catch (error) { const message = error instanceof Error ? error.message : String(error) return jsonResponse({ ok: false, error: message }, 502) } } async function handleAssetSave(req: Request, session: EditSession): Promise { let body: any try { body = await req.json() } catch { return jsonResponse({ ok: false, error: "Invalid JSON body" }, 400) } const candidate = normalizeImageCandidate(body?.candidate ?? body) if (!candidate) return jsonResponse({ ok: false, error: "Valid image candidate is required" }, 400) const purpose = normalizeMediaPurpose(body?.purpose) || candidate.purpose || "illustration" const brief = body?.brief || `Saved from ${candidate.provider} for Review asset placement.` const saved = await saveAssetCandidateUrls({ session, candidate, id: body?.id || candidate.candidateId, purpose, brief, alt: body?.alt || candidate.alt || candidate.title, notes: body?.notes, }) session.lastActiveAt = Date.now() scheduleIdleStop() const result = saved.result if (!result.ok) return jsonResponse({ ok: false, error: result.error }, 400) if (result.status !== "success" || !result.path) { return jsonResponse({ ok: false, error: failedAssetSaveMessage(result.status, saved.failures) }, 400) } const asset = savedAssetForResult(session, result.assetId) ?? savedAssetFallback(session, { id: result.assetId, path: result.path, sourceUrl: saved.sourceUrl, purpose, brief, candidate, }) if (!asset) return jsonResponse({ ok: false, error: "Saved asset was not found in workspace assets." }, 500) return jsonResponse({ ok: true, asset, result }) } async function saveAssetCandidateUrls(input: { session: EditSession candidate: ImageCandidate id: string purpose: MediaPurpose brief: string alt?: string notes?: string }): Promise<{ result: Awaited> sourceUrl?: string failures: Array<{ url: string; status: string }> }> { const urls = uniqueAssetUrls([input.candidate.imageUrl, input.candidate.thumbnailUrl]) const failures: Array<{ url: string; status: string }> = [] let lastResult: Awaited> | undefined for (const sourceUrl of urls) { const result = await saveMediaAsset({ topic: input.session.deck, id: input.id, type: "image", purpose: input.purpose, brief: input.brief, status: "success", sourceUrl, alt: input.alt, notes: input.notes, provider: input.candidate.provider, sourcePageUrl: input.candidate.sourcePageUrl, license: input.candidate.license, attribution: input.candidate.attribution, width: input.candidate.width, height: input.candidate.height, }, input.session.workspaceRoot) lastResult = result if (result.ok && result.status === "success" && result.path) return { result, sourceUrl, failures } failures.push({ url: sourceUrl, status: result.ok ? result.failureReason ?? result.status : result.error }) } return { result: lastResult ?? { ok: false, error: "No downloadable image URL was provided" }, failures, } } function uniqueAssetUrls(values: Array): string[] { const seen = new Set() return values.flatMap((value) => { const url = value?.trim() if (!url || seen.has(url)) return [] seen.add(url) return [url] }) } function failedAssetSaveMessage(status: string, failures: Array<{ url: string; status: string }>): string { if (!failures.length) return `Failed to save asset: ${status}` const details = failures .map((failure) => `${shortUrl(failure.url)}: ${failure.status}`) .join("; ") return `Failed to save asset: ${status} (${details})` } function shortUrl(value: string): string { return value.length <= 96 ? value : `${value.slice(0, 93)}...` } function handleAssetList(session: EditSession): Response { session.lastActiveAt = Date.now() scheduleIdleStop() return jsonResponse({ ok: true, assets: listSavedAssets(session) }) } function savedAssetForResult(session: EditSession, assetId: string): (MediaAssetRecord & { previewUrl?: string; deckPath?: string }) | null { return listSavedAssets(session).find((asset) => asset.id === assetId) ?? null } function savedAssetFallback( session: EditSession, input: { id: string path: string | null sourceUrl?: string purpose: MediaPurpose brief: string candidate: ImageCandidate }, ): (MediaAssetRecord & { previewUrl?: string; deckPath?: string }) | null { if (!input.path) return null return { id: input.id, type: "image", purpose: input.purpose, brief: input.brief, status: "success", path: input.path, sourceUrl: input.sourceUrl ?? input.candidate.imageUrl, alt: input.candidate.alt || input.candidate.title, provider: input.candidate.provider, sourcePageUrl: input.candidate.sourcePageUrl, license: input.candidate.license, attribution: input.candidate.attribution, width: input.candidate.width, height: input.candidate.height, savedAt: new Date().toISOString(), previewUrl: assetUrlForRef(input.path, session, session.workspaceRoot) ?? undefined, deckPath: relative(dirname(session.absoluteFile), resolve(session.workspaceRoot, input.path)).replace(/\\/g, "/"), } } function listSavedAssets(session: EditSession): Array { const manifestPath = resolve(session.workspaceRoot, "assets", slugify(session.deck), "media-manifest.json") if (!existsSync(manifestPath)) return [] try { const parsed = JSON.parse(readFileSync(manifestPath, "utf-8")) as { assets?: MediaAssetRecord[] } return (Array.isArray(parsed.assets) ? parsed.assets : []) .filter((asset) => asset.status === "success" && asset.path) .map((asset) => ({ ...asset, previewUrl: asset.path ? assetUrlForRef(asset.path, session, session.workspaceRoot) ?? undefined : undefined, deckPath: asset.path ? relative(dirname(session.absoluteFile), resolve(session.workspaceRoot, asset.path)).replace(/\\/g, "/") : undefined, })) } catch { return [] } } function normalizeImageCandidate(input: any): ImageCandidate | null { if (!input || typeof input !== "object") return null const candidateId = typeof input.candidateId === "string" ? input.candidateId.trim() : "" const provider = typeof input.provider === "string" ? input.provider.trim() : "" const title = typeof input.title === "string" ? input.title.trim() : "" const imageUrl = typeof input.imageUrl === "string" ? input.imageUrl.trim() : "" const thumbnailUrl = typeof input.thumbnailUrl === "string" ? input.thumbnailUrl.trim() : imageUrl if (!candidateId || !provider || !title || !imageUrl) return null return { candidateId, provider, title, thumbnailUrl, imageUrl, sourcePageUrl: typeof input.sourcePageUrl === "string" ? input.sourcePageUrl : undefined, width: typeof input.width === "number" ? input.width : undefined, height: typeof input.height === "number" ? input.height : undefined, alt: typeof input.alt === "string" ? input.alt : undefined, license: typeof input.license === "string" ? input.license : undefined, attribution: typeof input.attribution === "string" ? input.attribution : undefined, purpose: normalizeMediaPurpose(input.purpose), } } function normalizeMediaPurpose(input: unknown): MediaPurpose | undefined { return input === "hero" || input === "illustration" || input === "portrait" || input === "logo" || input === "screenshot" ? input : undefined } function slugify(value: string): string { return value .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, "") } function handleDeck(session: EditSession): Response { session.assets.clear() session.assetKeys.clear() session.nextAssetId = 1 const annotated = refreshVisualTargets(session) return htmlResponse(rewriteLocalAssetRefs(annotated.html, { session, sourceFile: session.absoluteFile, contentType: "html", })) } function refreshVisualTargets(session: EditSession): { html: string; version: string } { const sourceHtml = readFileSync(session.absoluteFile, "utf-8") const version = readDeckVersion(session).version const annotated = annotateVisualEditTargets(sourceHtml) session.visualTargets = annotated.targets session.visualTargetDeckVersion = version return { html: annotated.html, version } } function handleAsset(session: EditSession, id: string | null, method: string): Response { if (!id) return textResponse("Missing asset id", 400) const asset = session.assets.get(id) if (!asset) return textResponse("Asset not found", 404) if (!existsSync(asset.absoluteFile)) return textResponse("Asset file not found", 404) if (!statSync(asset.absoluteFile).isFile()) return textResponse("Asset is not a file", 404) const mime = mimeTypeForPath(asset.absoluteFile) const headers = { "content-type": mime, "cache-control": "no-store, max-age=0", } if (method === "HEAD") return new Response(null, { status: 200, headers }) if (mime === "text/css") { const css = readFileSync(asset.absoluteFile, "utf-8") return new Response(rewriteLocalAssetRefs(css, { session, sourceFile: asset.absoluteFile, contentType: "css", }), { status: 200, headers }) } return new Response(new Uint8Array(readFileSync(asset.absoluteFile)), { status: 200, headers }) } function handleReviewUiAsset(pathname: string, method: string): Response { const assetPath = pathname.slice(REVIEW_UI_ASSET_ROUTE.length) const absoluteFile = REVIEW_UI_ASSETS.get(assetPath) if (!absoluteFile || !existsSync(absoluteFile) || !statSync(absoluteFile).isFile()) { return textResponse("Asset not found", 404) } const headers = { "content-type": mimeTypeForPath(absoluteFile), "cache-control": "no-store, max-age=0", } if (method === "HEAD") return new Response(null, { status: 200, headers }) return new Response(new Uint8Array(readFileSync(absoluteFile)), { status: 200, headers }) } function rewriteLocalAssetRefs(content: string, input: { session: EditSession; sourceFile: string; contentType: "html" | "css" }): string { const baseDir = dirname(input.sourceFile) let rewritten = rewriteCssUrls(content, input.session, baseDir) if (input.contentType === "css") return rewritten rewritten = rewriteHtmlAssetAttributes(rewritten, input.session, baseDir) rewritten = rewriteSrcsetAttributes(rewritten, input.session, baseDir) return rewritten } function rewriteHtmlAssetAttributes(html: string, session: EditSession, baseDir: string): string { const attrPattern = /\b(src|href|poster)\s*=\s*("([^"]*)"|'([^']*)'|([^\s>]+))/gi return html.replace(attrPattern, (match, name: string, raw: string, doubleQuoted?: string, singleQuoted?: string, unquoted?: string) => { const value = doubleQuoted ?? singleQuoted ?? unquoted ?? "" const assetUrl = assetUrlForRef(value, session, baseDir) if (!assetUrl) return match const quote = doubleQuoted !== undefined ? '"' : singleQuoted !== undefined ? "'" : "" const escaped = quote ? assetUrl.replace(/&/g, "&") : assetUrl return `${name}=${quote}${escaped}${quote}` }) } function rewriteSrcsetAttributes(html: string, session: EditSession, baseDir: string): string { const srcsetPattern = /\bsrcset\s*=\s*("([^"]*)"|'([^']*)'|([^\s>]+))/gi return html.replace(srcsetPattern, (match, raw: string, doubleQuoted?: string, singleQuoted?: string, unquoted?: string) => { const value = doubleQuoted ?? singleQuoted ?? unquoted ?? "" const rewritten = rewriteSrcset(value, session, baseDir) if (rewritten === value) return match const quote = doubleQuoted !== undefined ? '"' : singleQuoted !== undefined ? "'" : "" const escaped = quote ? rewritten.replace(/&/g, "&") : rewritten return `srcset=${quote}${escaped}${quote}` }) } function rewriteSrcset(value: string, session: EditSession, baseDir: string): string { return value.split(",").map((part) => { const trimmed = part.trim() if (!trimmed) return part const pieces = trimmed.split(/\s+/) const assetUrl = assetUrlForRef(pieces[0], session, baseDir) if (!assetUrl) return part return [assetUrl, ...pieces.slice(1)].join(" ") }).join(", ") } function rewriteCssUrls(content: string, session: EditSession, baseDir: string): string { const cssUrlPattern = /url\(\s*("([^"]*)"|'([^']*)'|([^\s)]+))\s*\)/gi return content.replace(cssUrlPattern, (match, raw: string, doubleQuoted?: string, singleQuoted?: string, unquoted?: string) => { const value = doubleQuoted ?? singleQuoted ?? unquoted ?? "" const assetUrl = assetUrlForRef(value, session, baseDir) if (!assetUrl) return match return `url("${assetUrl.replace(/"/g, "%22")}")` }) } function assetUrlForRef(ref: string, session: EditSession, baseDir: string): string | null { const absoluteFile = resolveLocalAssetRef(ref, session.workspaceRoot, baseDir) if (!absoluteFile || !existsSync(absoluteFile) || !statSync(absoluteFile).isFile()) return null const key = resolve(absoluteFile) let id = session.assetKeys.get(key) if (!id) { id = String(session.nextAssetId++) session.assetKeys.set(key, id) session.assets.set(id, { id, absoluteFile: key }) } return `/__revela_asset?token=${encodeURIComponent(session.token)}&id=${encodeURIComponent(id)}` } function resolveLocalAssetRef(ref: string, workspaceRoot: string, baseDir: string): string | null { const trimmed = ref.trim() if (!trimmed || isSkippedAssetRef(trimmed)) return null const pathPart = stripQueryAndHash(trimmed) if (!pathPart) return null if (pathPart.startsWith("file://")) { try { return resolve(fileURLToPath(pathPart)) } catch { return null } } const decodedPath = safeDecodeUri(pathPart) if (isWindowsAbsolutePath(decodedPath)) return resolve(decodedPath) if (isAbsolute(decodedPath)) { const absolute = resolve(decodedPath) if (existsSync(absolute)) return absolute return resolve(workspaceRoot, `.${decodedPath}`) } return resolve(baseDir, decodedPath) } function stripQueryAndHash(ref: string): string { const hashIndex = ref.indexOf("#") const withoutHash = hashIndex >= 0 ? ref.slice(0, hashIndex) : ref const queryIndex = withoutHash.indexOf("?") return queryIndex >= 0 ? withoutHash.slice(0, queryIndex) : withoutHash } function safeDecodeUri(value: string): string { try { return decodeURI(value) } catch { return value } } function isSkippedAssetRef(ref: string): boolean { return /^(?:https?:|data:|blob:|mailto:|tel:|javascript:|#)/i.test(ref) || ref.startsWith("//") } function isWindowsAbsolutePath(value: string): boolean { return /^[a-zA-Z]:[\\/]/.test(value) } function mimeTypeForPath(filePath: string): string { switch (extname(filePath).toLowerCase()) { case ".html": case ".htm": return "text/html; charset=utf-8" case ".css": return "text/css" case ".js": return "application/javascript" case ".jpg": case ".jpeg": return "image/jpeg" case ".png": return "image/png" case ".gif": return "image/gif" case ".webp": return "image/webp" case ".svg": return "image/svg+xml" case ".woff": return "font/woff" case ".woff2": return "font/woff2" case ".ttf": return "font/ttf" case ".otf": return "font/otf" case ".mp4": return "video/mp4" case ".webm": return "video/webm" case ".mp3": return "audio/mpeg" case ".wav": return "audio/wav" default: return "application/octet-stream" } } function handleDeckVersion(session: EditSession): Response { try { const version = readDeckVersion(session) session.lastActiveAt = Date.now() scheduleIdleStop() return jsonResponse({ ok: true, ...version }) } catch (error) { const message = error instanceof Error ? error.message : String(error) return jsonResponse({ ok: false, error: message }, 404) } } function readDeckVersion(session: EditSession): { mtimeMs: number; size: number; version: string } { const stat = statSync(session.absoluteFile) const version = `${stat.mtimeMs}:${stat.size}` return { mtimeMs: stat.mtimeMs, size: stat.size, version } } async function handleVisualChanges(req: Request, session: EditSession): Promise { let body: any try { body = await req.json() } catch { return jsonResponse({ ok: false, error: "Invalid JSON body" }, 400) } const changes = Array.isArray(body?.changes) ? body.changes : [] if (!changes.length) return jsonResponse({ ok: false, error: "No visual changes to save." }, 400) try { const result = applyVisualTargetChanges({ file: session.absoluteFile, deckVersion: typeof body?.deckVersion === "string" ? body.deckVersion : undefined, targetDeckVersion: session.visualTargetDeckVersion, targets: session.visualTargets, changes, }) const refreshed = refreshVisualTargets(session) session.lastActiveAt = Date.now() scheduleIdleStop() return jsonResponse({ ok: true, deckVersion: refreshed.version, changeCount: result.changeCount }) } catch (error) { const message = error instanceof Error ? error.message : String(error) return jsonResponse({ ok: false, error: message }, 400) } } async function handleComment(req: Request, session: EditSession): Promise { let body: Partial try { body = await req.json() } catch { return jsonResponse({ ok: false, error: "Invalid JSON body" }, 400) } return applyCommentPayload(body, session) } function handleReviewCommentsList(session: EditSession): Response { const deckVersion = readDeckVersion(session).version const comments = listReviewComments(session.workspaceRoot, session.file) session.lastActiveAt = Date.now() scheduleIdleStop() return jsonResponse({ ok: true, deckVersion, comments }) } async function handleReviewCommentCreate(req: Request, session: EditSession): Promise { let body: Partial try { body = await req.json() } catch { return jsonResponse({ ok: false, error: "Invalid JSON body" }, 400) } const comment = typeof body.comment === "string" ? body.comment.trim() : "" const elements = Array.isArray(body.elements) ? body.elements : [] if (!comment) return jsonResponse({ ok: false, error: "Comment is required" }, 400) try { const deckVersion = readDeckVersion(session).version const saved = createReviewComment(session.workspaceRoot, { deckFile: session.file, deckVersion, comment, elements, asset: (body as any).asset, drop: (body as any).drop, }) session.lastActiveAt = Date.now() scheduleIdleStop() if ((body as any).applyNow === true) { return enqueueOrStartPersistedReviewCommentApply(session, saved, {}) } return jsonResponse({ ok: true, comment: saved, deckVersion }) } catch (error) { const message = error instanceof Error ? error.message : String(error) return jsonResponse({ ok: false, error: message }, 400) } } async function handleReviewCommentApply(commentId: string, req: Request, session: EditSession): Promise { let body: any = {} try { const text = await req.text() body = text ? JSON.parse(text) : {} } catch { return jsonResponse({ ok: false, error: "Invalid JSON body" }, 400) } const comment = readReviewComment(session.workspaceRoot, commentId) if (!comment || comment.deckFile !== session.file) return jsonResponse({ ok: false, error: "Review comment not found" }, 404) return enqueueOrStartPersistedReviewCommentApply(session, comment, body) } function handleReviewCommentStop(commentId: string, session: EditSession): Response { const comment = readReviewComment(session.workspaceRoot, commentId) if (!comment || comment.deckFile !== session.file) return jsonResponse({ ok: false, error: "Review comment not found" }, 404) session.applyQueue = (session.applyQueue ?? []).filter((id) => id !== comment.id) const wasActive = session.activeApplyCommentId === comment.id if (wasActive) { session.activeApplyController?.abort() if (session.activeApplyRequestId) stopCommentRequest(session.activeApplyRequestId) session.activeApplyCommentId = undefined session.activeApplyRequestId = undefined session.activeApplyController = undefined } else if (comment.lastApplyRequestId) { stopCommentRequest(comment.lastApplyRequestId) } const stopped = markReviewCommentStopped(session.workspaceRoot, comment.id) ?? comment session.lastActiveAt = Date.now() scheduleIdleStop() if (comment.status === "queued" || wasActive) drainPersistedReviewCommentApplyQueue(session) return jsonResponse({ ok: true, status: "stopped", comment: stopped, deckVersion: readDeckVersion(session).version }) } function handleReviewCommentDelete(commentId: string, session: EditSession): Response { const comment = readReviewComment(session.workspaceRoot, commentId) if (!comment || comment.deckFile !== session.file) return jsonResponse({ ok: false, error: "Review comment not found" }, 404) if (comment.status === "applying") return jsonResponse({ ok: false, error: "Stop the applying comment before deleting it." }, 409) session.applyQueue = (session.applyQueue ?? []).filter((id) => id !== comment.id) const deleted = deleteReviewComment(session.workspaceRoot, comment.id) if (!deleted) return jsonResponse({ ok: false, error: "Review comment not found" }, 404) session.lastActiveAt = Date.now() scheduleIdleStop() return jsonResponse({ ok: true, deleted: true, commentId: comment.id, deckVersion: readDeckVersion(session).version }) } async function enqueueOrStartPersistedReviewCommentApply(session: EditSession, comment: ReviewCommentRecord, body: any = {}): Promise { session.applyQueue = session.applyQueue ?? [] if (session.activeApplyCommentId) { if (!session.applyQueue.includes(comment.id)) session.applyQueue.push(comment.id) const queued = markReviewCommentQueued(session.workspaceRoot, comment.id) ?? comment return jsonResponse({ ok: true, deckVersion: readDeckVersion(session).version, status: "queued", queuePosition: session.applyQueue.indexOf(comment.id) + 1, comment: queued, }) } const queuedIndex = session.applyQueue.indexOf(comment.id) if (queuedIndex >= 0) { const queued = markReviewCommentQueued(session.workspaceRoot, comment.id) ?? comment return jsonResponse({ ok: true, deckVersion: readDeckVersion(session).version, status: "queued", queuePosition: queuedIndex + 1, comment: queued, }) } return startPersistedReviewCommentApply(session, comment, body) } async function startPersistedReviewCommentApply(session: EditSession, comment: ReviewCommentRecord, body: any = {}): Promise { session.activeApplyCommentId = comment.id const requestId = typeof body.requestId === "string" && body.requestId.trim() ? body.requestId.trim() : randomBytes(10).toString("base64url") const controller = new AbortController() session.activeApplyRequestId = requestId session.activeApplyController = controller try { const response = await applyCommentPayload({ ...body, comment: comment.comment, elements: comment.elements, asset: comment.asset, drop: comment.drop, requestId, }, session, { persistedCommentId: comment.id, signal: controller.signal, onSettled: () => settlePersistedReviewCommentApply(session, comment.id), }) if (!response.ok) settlePersistedReviewCommentApply(session, comment.id) return response } catch (error) { settlePersistedReviewCommentApply(session, comment.id) throw error } } function settlePersistedReviewCommentApply(session: EditSession, commentId: string): void { if (session.activeApplyCommentId === commentId) session.activeApplyCommentId = undefined if (session.activeApplyCommentId === undefined) { session.activeApplyRequestId = undefined session.activeApplyController = undefined } drainPersistedReviewCommentApplyQueue(session) } function drainPersistedReviewCommentApplyQueue(session: EditSession): void { if (session.activeApplyCommentId) return session.applyQueue = session.applyQueue ?? [] while (session.applyQueue.length) { const nextId = session.applyQueue.shift() if (!nextId) continue const next = readReviewComment(session.workspaceRoot, nextId) if (!next || next.deckFile !== session.file) continue void startPersistedReviewCommentApply(session, next) return } } async function applyCommentPayload( body: Partial, session: EditSession, options: { persistedCommentId?: string; signal?: AbortSignal; onSettled?: () => void } = {}, ): Promise { const { persistedCommentId, signal, onSettled } = options const comments = Array.isArray(body.comments) ? body.comments .map((draft: any) => ({ comment: typeof draft?.comment === "string" ? draft.comment.trim() : "", elements: Array.isArray(draft?.elements) ? draft.elements : [], })) .filter((draft) => draft.comment && draft.elements.length > 0) : [] const comment = typeof body.comment === "string" ? body.comment.trim() : "" const elements = Array.isArray(body.elements) ? body.elements : [] if (!comment && comments.length === 0) return jsonResponse({ ok: false, error: "Comment is required" }, 400) ctx.enabled = true buildPrompt({ mode: "deck-render" }) const prompt = buildEditPrompt({ ...body, deck: session.deck, file: session.file, comment, elements, comments, suppressAutomaticArtifactQa: true, }) const deckVersion = readDeckVersion(session).version const requestId = typeof (body as any).requestId === "string" && (body as any).requestId.trim() ? (body as any).requestId.trim() : randomBytes(10).toString("base64url") createCommentRequest({ requestId, deckVersion }) if (persistedCommentId) markReviewCommentApplying(session.workspaceRoot, persistedCommentId, requestId) suppressReviewApplyFixArtifactQa({ workspaceRoot: session.workspaceRoot, file: session.file, sessionID: session.sessionID, }) void session.promptBridge.send({ action: "comment", prompt, workspaceRoot: session.workspaceRoot, file: session.file, requestId, signal, onEvent: (event) => addCommentRequestEvent(requestId, event), }).then((result) => { if (result.ok) { completeCommentRequest(requestId) if (persistedCommentId && readReviewComment(session.workspaceRoot, persistedCommentId)?.status === "applying") markReviewCommentApplied(session.workspaceRoot, persistedCommentId) } else if (result.status === "stopped") { stopCommentRequest(requestId) if (persistedCommentId && readReviewComment(session.workspaceRoot, persistedCommentId)?.status === "applying") markReviewCommentStopped(session.workspaceRoot, persistedCommentId) } else { failCommentRequest(requestId, result.error, result.raw) if (persistedCommentId && readReviewComment(session.workspaceRoot, persistedCommentId)?.status === "applying") markReviewCommentFailed(session.workspaceRoot, persistedCommentId, result.error, result.raw) } onSettled?.() }).catch((error: unknown) => { const message = error instanceof Error ? error.message : String(error) if (signal?.aborted) { stopCommentRequest(requestId) if (persistedCommentId && readReviewComment(session.workspaceRoot, persistedCommentId)?.status === "applying") markReviewCommentStopped(session.workspaceRoot, persistedCommentId) } else { failCommentRequest(requestId, message) if (persistedCommentId && readReviewComment(session.workspaceRoot, persistedCommentId)?.status === "applying") markReviewCommentFailed(session.workspaceRoot, persistedCommentId, message) } onSettled?.() }) session.lastActiveAt = Date.now() scheduleIdleStop() const persistedComment = persistedCommentId ? readReviewComment(session.workspaceRoot, persistedCommentId) : undefined return jsonResponse({ ok: true, requestId, commentRequestId: requestId, deckVersion, status: "pending", ...(persistedComment ? { comment: persistedComment } : {}) }) } function handleCommentEvents(requestId: string | null, session: EditSession): Response { if (!requestId) return jsonResponse({ ok: false, error: "Missing requestId" }, 400) const request = getCommentRequest(requestId) if (!request) return jsonResponse({ ok: false, requestId, error: "Comment request not found" }, 404) session.lastActiveAt = Date.now() scheduleIdleStop() const encoder = new TextEncoder() let unsubscribe = () => {} const stream = new ReadableStream({ start(controller) { const send = (event: unknown) => { controller.enqueue(encoder.encode(`event: progress\ndata: ${JSON.stringify(event)}\n\n`)) } for (const event of request.events) send(event) if (request.status !== "pending") { controller.close() return } unsubscribe = subscribeCommentRequestEvents(requestId, (event) => { send(event) if (event.type === "completed" || event.type === "failed" || event.type === "timeout" || event.type === "stopped") { unsubscribe() controller.close() } }) }, cancel() { unsubscribe() }, }) return new Response(stream, { headers: { "content-type": "text/event-stream; charset=utf-8", "cache-control": "no-store", connection: "keep-alive", }, }) } function handleInspectEvents(requestId: string | null, session: EditSession): Response { if (!requestId) return jsonResponse({ ok: false, error: "Missing requestId" }, 400) const request = getInspectRequest(requestId) if (!request) return jsonResponse({ ok: false, requestId, error: "Inspection request not found" }, 404) session.lastActiveAt = Date.now() scheduleIdleStop() const encoder = new TextEncoder() let unsubscribe = () => {} const stream = new ReadableStream({ start(controller) { const send = (event: unknown) => { controller.enqueue(encoder.encode(`event: progress\ndata: ${JSON.stringify(event)}\n\n`)) } for (const event of request.events) send(event) if (request.status !== "pending") { controller.close() return } unsubscribe = subscribeInspectRequestEvents(requestId, (event) => { send(event) if (event.type === "completed" || event.type === "failed" || event.type === "timeout") { unsubscribe() controller.close() } }) }, cancel() { unsubscribe() }, }) return new Response(stream, { headers: { "content-type": "text/event-stream; charset=utf-8", "cache-control": "no-store", connection: "keep-alive", }, }) } function handleCommentResult(requestId: string | null, session: EditSession): Response { if (!requestId) return jsonResponse({ ok: false, error: "Missing requestId" }, 400) const request = getCommentRequest(requestId) if (!request) return jsonResponse({ ok: false, requestId, error: "Comment request not found" }, 404) session.lastActiveAt = Date.now() scheduleIdleStop() if (request.status === "failed" || request.status === "expired" || request.status === "stopped") { return jsonResponse({ ok: true, requestId, status: request.status, deckVersion: request.deckVersion, error: request.error || "Review agent failed", raw: request.raw, }) } return jsonResponse({ ok: true, requestId, status: request.status, deckVersion: request.deckVersion }) } async function handleInspect(req: Request, session: EditSession): Promise { let body: any try { body = await req.json() } catch { return jsonResponse({ ok: false, error: "Invalid JSON body" }, 400) } const snapshot = normalizeSnapshot(body?.snapshot ?? body) const language = normalizeInspectLanguage(body?.language) const comment = typeof body?.comment === "string" ? body.comment.trim().slice(0, 2000) : "" const requestId = typeof body?.requestId === "string" && body.requestId.trim() ? body.requestId.trim() : randomBytes(10).toString("base64url") const version = readDeckVersion(session).version const staleReason = typeof body?.deckVersion === "string" && body.deckVersion !== version ? "Deck changed after the browser captured this selection. Re-select the element for the freshest inspection." : undefined try { const { projection, preprocess } = projectWorkspaceElement(session.workspaceRoot, snapshot, { requestId }) createInspectRequest({ requestId, projection, deckVersion: version }) session.lastActiveAt = Date.now() scheduleIdleStop() const prompt = buildInspectionPrompt({ requestId, file: session.file, language, comment, delivery: session.promptBridge.kind === "codex-exec" ? "json" : "tool", projection: staleReason ? { ...projection, stale: { stale: true, reason: staleReason } } as any : projection, }) void session.promptBridge.send({ action: "inspect", prompt, workspaceRoot: session.workspaceRoot, file: session.file, requestId, onEvent: (event) => addInspectRequestEvent(requestId, event), }).then((result) => { if (result.ok && result.result) { completeInspectRequest(requestId, result.result) } else if (!result.ok) { failInspectRequest(requestId, result.error, result.raw) } }).catch((error: unknown) => { const message = error instanceof Error ? error.message : String(error) failInspectRequest(requestId, message) }) return jsonResponse({ ok: true, requestId, deckVersion: version, status: "pending", language, preprocess }) } catch (error) { const message = error instanceof Error ? error.message : String(error) failInspectRequest(requestId, message) return jsonResponse({ ok: false, requestId, deckVersion: version, error: message }, 400) } } function handleInspectResult(requestId: string | null, session: EditSession): Response { if (!requestId) return jsonResponse({ ok: false, error: "Missing requestId" }, 400) const request = getInspectRequest(requestId) if (!request) return jsonResponse({ ok: false, requestId, error: "Inspection request not found" }, 404) session.lastActiveAt = Date.now() scheduleIdleStop() if (request.status === "completed") return jsonResponse({ ok: true, requestId, status: request.status, deckVersion: request.deckVersion, result: request.result }) if (request.status === "failed" || request.status === "expired") return jsonResponse({ ok: true, requestId, status: request.status, deckVersion: request.deckVersion, error: request.error || "Insight failed", raw: request.raw }) return jsonResponse({ ok: true, requestId, status: request.status, deckVersion: request.deckVersion }) } function normalizeInspectLanguage(input: unknown): string { const value = typeof input === "string" ? input.trim() : "" return value || "Auto" } function normalizeSnapshot(input: any): InspectionElementSnapshot { return { scope: input?.scope === "selection" || input?.scope === "slide" || input?.scope === "element" ? input.scope : undefined, slideIndex: typeof input?.slideIndex === "number" ? input.slideIndex : undefined, text: typeof input?.text === "string" ? input.text : undefined, selectedText: typeof input?.selectedText === "string" ? input.selectedText : undefined, tagName: typeof input?.tagName === "string" ? input.tagName : undefined, slideTitle: typeof input?.slideTitle === "string" ? input.slideTitle : undefined, selector: typeof input?.selector === "string" ? input.selector : undefined, domPath: typeof input?.domPath === "string" ? input.domPath : undefined, id: typeof input?.id === "string" ? input.id : undefined, classList: Array.isArray(input?.classList) ? input.classList.filter((item: unknown) => typeof item === "string") : [], role: typeof input?.role === "string" ? input.role : undefined, outerHTMLExcerpt: typeof input?.outerHTMLExcerpt === "string" ? input.outerHTMLExcerpt : undefined, nearbyText: typeof input?.nearbyText === "string" ? input.nearbyText : undefined, elements: Array.isArray(input?.elements) ? input.elements.map((item: any) => ({ text: typeof item?.text === "string" ? item.text : undefined, tagName: typeof item?.tagName === "string" ? item.tagName : undefined, slideIndex: typeof item?.slideIndex === "number" ? item.slideIndex : undefined, slideTitle: typeof item?.slideTitle === "string" ? item.slideTitle : undefined, selector: typeof item?.selector === "string" ? item.selector : undefined, domPath: typeof item?.domPath === "string" ? item.domPath : undefined, id: typeof item?.id === "string" ? item.id : undefined, classList: Array.isArray(item?.classList) ? item.classList.filter((className: unknown) => typeof className === "string") : [], role: typeof item?.role === "string" ? item.role : undefined, outerHTMLExcerpt: typeof item?.outerHTMLExcerpt === "string" ? item.outerHTMLExcerpt : undefined, nearbyText: typeof item?.nearbyText === "string" ? item.nearbyText : undefined, boundingBox: item?.boundingBox && typeof item.boundingBox === "object" ? item.boundingBox : undefined, viewport: item?.viewport && typeof item.viewport === "object" ? item.viewport : undefined, })) : undefined, boundingBox: input?.boundingBox && typeof input.boundingBox === "object" ? input.boundingBox : undefined, viewport: input?.viewport && typeof input.viewport === "object" ? input.viewport : undefined, } } function validateSession(token: string | null): { ok: true; value: EditSession } | { ok: false; response: Response } { if (!token) return { ok: false, response: textResponse("Missing token", 401) } const session = sessions.get(token) if (!session) return { ok: false, response: textResponse("Invalid or expired token", 401) } if (Date.now() - session.lastActiveAt > SESSION_TTL_MS) { sessions.delete(token) return { ok: false, response: textResponse("Expired token", 401) } } session.lastActiveAt = Date.now() return { ok: true, value: session } } function cleanupExpiredSessions(): void { const now = Date.now() for (const [token, session] of sessions) { if (now - session.lastActiveAt > SESSION_TTL_MS) sessions.delete(token) } } function scheduleIdleStop(): void { if (idleTimer) clearTimeout(idleTimer) idleTimer = setTimeout(() => { const now = Date.now() const active = [...sessions.values()].some((session) => now - session.lastActiveAt < IDLE_STOP_MS) if (active) { scheduleIdleStop() return } sessions.clear() server?.stop() server = undefined baseUrl = "" idleTimer = undefined }, IDLE_STOP_MS) } function htmlResponse(body: string, status = 200): Response { return new Response(body, { status, headers: { "content-type": "text/html; charset=utf-8", "cache-control": "no-store, max-age=0", }, }) } function textResponse(body: string, status = 200): Response { return new Response(body, { status, headers: { "content-type": "text/plain; charset=utf-8" }, }) } function jsonResponse(body: unknown, status = 200): Response { return new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json; charset=utf-8" }, }) } export function renderCodexReviewShell(token: string, defaultMode: RefineMode = "edit"): string { return renderRefineShell(token, defaultMode, "codex") } export function renderRefineShell(token: string, defaultMode: RefineMode = "edit", surface: ReviewShellSurface = "legacy"): string { const encodedToken = JSON.stringify(token) const encodedDefaultMode = JSON.stringify(defaultMode) const encodedSurface = JSON.stringify(surface) const sendButtonHtml = `${lucideIcon("send")}Apply` const encodedSendButtonHtml = JSON.stringify(sendButtonHtml) const activityLabel = "Comments" const bodyClass = surface === "codex" ? "codex-review" : "legacy-review" return ` Revela Review
No unsaved visual changes
` }