import type { ContentBlock, ContentChunk, ResourceLink, Role } from "@agentclientprotocol/sdk" import path from "node:path" import { fileURLToPath, pathToFileURL } from "node:url" import { SessionV1 } from "@opencode-ai/core/v1/session" export type PromptPart = SessionV1.TextPartInput | SessionV1.FilePartInput export type ReplayPart = | { type: "text" text: string synthetic?: boolean ignored?: boolean } | { type: "file" url: string mime: string filename?: string } | { type: "reasoning" text: string } export function promptContentToParts(content: readonly ContentBlock[]): PromptPart[] { return content.flatMap(contentBlockToParts) } export function contentBlockToParts(block: ContentBlock): PromptPart[] { switch (block.type) { case "text": return [ { type: "text", text: block.text, ...audienceFlags(block.annotations?.audience ?? undefined), }, ] case "image": if (block.data) { return [ { type: "file", url: `data:${block.mimeType};base64,${block.data}`, filename: filenameFromUri(block.uri ?? undefined) ?? "image", mime: block.mimeType, }, ] } if (block.uri?.startsWith("data:")) { return [ { type: "file", url: block.uri, filename: filenameFromUri(block.uri) ?? "image", mime: block.mimeType, }, ] } if (block.uri?.startsWith("http://") || block.uri?.startsWith("https://")) { return [ { type: "file", url: block.uri, filename: filenameFromUri(block.uri) ?? "image", mime: block.mimeType, }, ] } return [] case "resource_link": return [resourceLinkToPart(block)] case "resource": if ("text" in block.resource) { try { const parsed = new URL(block.resource.uri) if (parsed.protocol === "file:") { const line = parsed.hash.match(/^#L(\d+)/)?.[1] let filepath: string try { filepath = fileURLToPath(parsed) } catch { filepath = decodeURIComponent(parsed.pathname) } if (path.sep === "\\") filepath = filepath.replace(/\\/g, "/") return [ { type: "text", text: `[${filepath}${line ? `:${line}` : ""}]\n${block.resource.text}`, }, ] } } catch {} return [{ type: "text", text: `[${block.resource.uri}]\n${block.resource.text}` }] } if (block.resource.mimeType) { return [ { type: "file", url: block.resource.uri.startsWith("data:") ? block.resource.uri : `data:${block.resource.mimeType};base64,${block.resource.blob}`, filename: filenameFromUri(block.resource.uri) ?? "file", mime: block.resource.mimeType, }, ] } return [] default: return [] } } export function partsToContentChunks(parts: readonly ReplayPart[]): ContentChunk[] { return parts.flatMap(partToContentChunks) } export function partToContentChunks(part: ReplayPart): ContentChunk[] { switch (part.type) { case "text": if (!part.text) return [] return [ { content: { type: "text", text: part.text, ...partAudience(part), }, }, ] case "file": return filePartToContentChunks(part) case "reasoning": if (!part.text) return [] return [ { content: { type: "text", text: part.text, }, }, ] } } function resourceLinkToPart(link: ResourceLink): PromptPart { const parsed = uriToFilePart(link.uri, link.mimeType ?? "text/plain", link.name) if (parsed.type === "file") return parsed return { type: "text", text: parsed.text } } function uriToFilePart( uri: string, mime: string, filename?: string, ): SessionV1.FilePartInput | SessionV1.TextPartInput { try { if (uri.startsWith("file://")) { return { type: "file", url: uri, filename: filename ?? filenameFromUri(uri) ?? "file", mime, } } if (uri.startsWith("zed://")) { const pathname = new URL(uri).searchParams.get("path") if (pathname) { return { type: "file", url: pathToFileURL(pathname).href, filename: filename ?? (path.basename(pathname) || "file"), mime, } } } return { type: "text", text: uri } } catch { return { type: "text", text: uri } } } function filePartToContentChunks(part: Extract): ContentChunk[] { if (part.url.startsWith("file://")) { return [ { content: { type: "resource_link", uri: part.url, name: part.filename ?? "file", mimeType: part.mime, }, }, ] } if (!part.url.startsWith("data:")) return [] const data = decodeDataUrl(part.url) if (!data) return [] if (data.mime.startsWith("image/")) { return [ { content: { type: "image", mimeType: data.mime, data: data.base64, uri: pathToFileURL(part.filename ?? "image").href, }, }, ] } return [ { content: { type: "resource", resource: data.mime.startsWith("text/") || data.mime === "application/json" ? { uri: pathToFileURL(part.filename ?? "file").href, mimeType: data.mime, text: Buffer.from(data.base64, "base64").toString("utf8"), } : { uri: pathToFileURL(part.filename ?? "file").href, mimeType: data.mime, blob: data.base64, }, }, }, ] } function decodeDataUrl(url: string) { const match = /^data:([^;]+);base64,(.*)$/.exec(url) if (!match) return return { mime: match[1], base64: match[2] } } function audienceFlags(audience: readonly Role[] | null | undefined) { if (audience?.length === 1 && audience[0] === "assistant") return { synthetic: true } if (audience?.length === 1 && audience[0] === "user") return { ignored: true } return {} } function partAudience(part: Extract) { const audience: Role[] | undefined = part.synthetic ? ["assistant"] : part.ignored ? ["user"] : undefined if (!audience) return {} return { annotations: { audience } } } function filenameFromUri(uri: string | undefined) { if (!uri) return if (uri.startsWith("data:")) return try { const parsed = new URL(uri) const name = path.basename(parsed.pathname) return name || undefined } catch { return path.basename(uri) || undefined } }