mirror of
https://gitea.toothfairyai.com/ToothFairyAI/tf_code.git
synced 2026-04-21 16:14:45 +00:00
180 lines
5.1 KiB
TypeScript
180 lines
5.1 KiB
TypeScript
import { getFilename } from "@opencode-ai/util/path"
|
|
import { type AgentPartInput, type FilePartInput, type Part, type TextPartInput } from "@opencode-ai/sdk/v2/client"
|
|
import type { FileSelection } from "@/context/file"
|
|
import { encodeFilePath } from "@/context/file/path"
|
|
import type { AgentPart, FileAttachmentPart, ImageAttachmentPart, Prompt } from "@/context/prompt"
|
|
import { Identifier } from "@/utils/id"
|
|
|
|
type PromptRequestPart = (TextPartInput | FilePartInput | AgentPartInput) & { id: string }
|
|
|
|
type ContextFile = {
|
|
key: string
|
|
type: "file"
|
|
path: string
|
|
selection?: FileSelection
|
|
comment?: string
|
|
commentID?: string
|
|
commentOrigin?: "review" | "file"
|
|
preview?: string
|
|
}
|
|
|
|
type BuildRequestPartsInput = {
|
|
prompt: Prompt
|
|
context: ContextFile[]
|
|
images: ImageAttachmentPart[]
|
|
text: string
|
|
messageID: string
|
|
sessionID: string
|
|
sessionDirectory: string
|
|
}
|
|
|
|
const absolute = (directory: string, path: string) => {
|
|
if (path.startsWith("/")) return path
|
|
if (/^[A-Za-z]:[\\/]/.test(path) || /^[A-Za-z]:$/.test(path)) return path
|
|
if (path.startsWith("\\\\") || path.startsWith("//")) return path
|
|
return `${directory.replace(/[\\/]+$/, "")}/${path}`
|
|
}
|
|
|
|
const fileQuery = (selection: FileSelection | undefined) =>
|
|
selection ? `?start=${selection.startLine}&end=${selection.endLine}` : ""
|
|
|
|
const isFileAttachment = (part: Prompt[number]): part is FileAttachmentPart => part.type === "file"
|
|
const isAgentAttachment = (part: Prompt[number]): part is AgentPart => part.type === "agent"
|
|
|
|
const commentNote = (path: string, selection: FileSelection | undefined, comment: string) => {
|
|
const start = selection ? Math.min(selection.startLine, selection.endLine) : undefined
|
|
const end = selection ? Math.max(selection.startLine, selection.endLine) : undefined
|
|
const range =
|
|
start === undefined || end === undefined
|
|
? "this file"
|
|
: start === end
|
|
? `line ${start}`
|
|
: `lines ${start} through ${end}`
|
|
return `The user made the following comment regarding ${range} of ${path}: ${comment}`
|
|
}
|
|
|
|
const toOptimisticPart = (part: PromptRequestPart, sessionID: string, messageID: string): Part => {
|
|
if (part.type === "text") {
|
|
return {
|
|
id: part.id,
|
|
type: "text",
|
|
text: part.text,
|
|
synthetic: part.synthetic,
|
|
ignored: part.ignored,
|
|
time: part.time,
|
|
metadata: part.metadata,
|
|
sessionID,
|
|
messageID,
|
|
}
|
|
}
|
|
if (part.type === "file") {
|
|
return {
|
|
id: part.id,
|
|
type: "file",
|
|
mime: part.mime,
|
|
filename: part.filename,
|
|
url: part.url,
|
|
source: part.source,
|
|
sessionID,
|
|
messageID,
|
|
}
|
|
}
|
|
return {
|
|
id: part.id,
|
|
type: "agent",
|
|
name: part.name,
|
|
source: part.source,
|
|
sessionID,
|
|
messageID,
|
|
}
|
|
}
|
|
|
|
export function buildRequestParts(input: BuildRequestPartsInput) {
|
|
const requestParts: PromptRequestPart[] = [
|
|
{
|
|
id: Identifier.ascending("part"),
|
|
type: "text",
|
|
text: input.text,
|
|
},
|
|
]
|
|
|
|
const files = input.prompt.filter(isFileAttachment).map((attachment) => {
|
|
const path = absolute(input.sessionDirectory, attachment.path)
|
|
return {
|
|
id: Identifier.ascending("part"),
|
|
type: "file",
|
|
mime: "text/plain",
|
|
url: `file://${encodeFilePath(path)}${fileQuery(attachment.selection)}`,
|
|
filename: getFilename(attachment.path),
|
|
source: {
|
|
type: "file",
|
|
text: {
|
|
value: attachment.content,
|
|
start: attachment.start,
|
|
end: attachment.end,
|
|
},
|
|
path,
|
|
},
|
|
} satisfies PromptRequestPart
|
|
})
|
|
|
|
const agents = input.prompt.filter(isAgentAttachment).map((attachment) => {
|
|
return {
|
|
id: Identifier.ascending("part"),
|
|
type: "agent",
|
|
name: attachment.name,
|
|
source: {
|
|
value: attachment.content,
|
|
start: attachment.start,
|
|
end: attachment.end,
|
|
},
|
|
} satisfies PromptRequestPart
|
|
})
|
|
|
|
const used = new Set(files.map((part) => part.url))
|
|
const context = input.context.flatMap((item) => {
|
|
const path = absolute(input.sessionDirectory, item.path)
|
|
const url = `file://${encodeFilePath(path)}${fileQuery(item.selection)}`
|
|
const comment = item.comment?.trim()
|
|
if (!comment && used.has(url)) return []
|
|
used.add(url)
|
|
|
|
const filePart = {
|
|
id: Identifier.ascending("part"),
|
|
type: "file",
|
|
mime: "text/plain",
|
|
url,
|
|
filename: getFilename(item.path),
|
|
} satisfies PromptRequestPart
|
|
|
|
if (!comment) return [filePart]
|
|
|
|
return [
|
|
{
|
|
id: Identifier.ascending("part"),
|
|
type: "text",
|
|
text: commentNote(item.path, item.selection, comment),
|
|
synthetic: true,
|
|
} satisfies PromptRequestPart,
|
|
filePart,
|
|
]
|
|
})
|
|
|
|
const images = input.images.map((attachment) => {
|
|
return {
|
|
id: Identifier.ascending("part"),
|
|
type: "file",
|
|
mime: attachment.mime,
|
|
url: attachment.dataUrl,
|
|
filename: attachment.filename,
|
|
} satisfies PromptRequestPart
|
|
})
|
|
|
|
requestParts.push(...files, ...context, ...agents, ...images)
|
|
|
|
return {
|
|
requestParts,
|
|
optimisticParts: requestParts.map((part) => toOptimisticPart(part, input.sessionID, input.messageID)),
|
|
}
|
|
}
|