173 lines
4.8 KiB
TypeScript
173 lines
4.8 KiB
TypeScript
import { createStore } from "solid-js/store"
|
|
import { createSimpleContext } from "@opencode-ai/ui/context"
|
|
import { batch, createMemo } from "solid-js"
|
|
import { useParams } from "@solidjs/router"
|
|
import type { FileSelection } from "@/context/file"
|
|
import { persisted } from "@/utils/persist"
|
|
|
|
interface PartBase {
|
|
content: string
|
|
start: number
|
|
end: number
|
|
}
|
|
|
|
export interface TextPart extends PartBase {
|
|
type: "text"
|
|
}
|
|
|
|
export interface FileAttachmentPart extends PartBase {
|
|
type: "file"
|
|
path: string
|
|
selection?: FileSelection
|
|
}
|
|
|
|
export interface AgentPart extends PartBase {
|
|
type: "agent"
|
|
name: string
|
|
}
|
|
|
|
export interface ImageAttachmentPart {
|
|
type: "image"
|
|
id: string
|
|
filename: string
|
|
mime: string
|
|
dataUrl: string
|
|
}
|
|
|
|
export type ContentPart = TextPart | FileAttachmentPart | AgentPart | ImageAttachmentPart
|
|
export type Prompt = ContentPart[]
|
|
|
|
export type FileContextItem = {
|
|
type: "file"
|
|
path: string
|
|
selection?: FileSelection
|
|
}
|
|
|
|
export type ContextItem = FileContextItem
|
|
|
|
export const DEFAULT_PROMPT: Prompt = [{ type: "text", content: "", start: 0, end: 0 }]
|
|
|
|
function isSelectionEqual(a?: FileSelection, b?: FileSelection) {
|
|
if (!a && !b) return true
|
|
if (!a || !b) return false
|
|
return (
|
|
a.startLine === b.startLine && a.startChar === b.startChar && a.endLine === b.endLine && a.endChar === b.endChar
|
|
)
|
|
}
|
|
|
|
export function isPromptEqual(promptA: Prompt, promptB: Prompt): boolean {
|
|
if (promptA.length !== promptB.length) return false
|
|
for (let i = 0; i < promptA.length; i++) {
|
|
const partA = promptA[i]
|
|
const partB = promptB[i]
|
|
if (partA.type !== partB.type) return false
|
|
if (partA.type === "text" && partA.content !== (partB as TextPart).content) {
|
|
return false
|
|
}
|
|
if (partA.type === "file") {
|
|
const fileA = partA as FileAttachmentPart
|
|
const fileB = partB as FileAttachmentPart
|
|
if (fileA.path !== fileB.path) return false
|
|
if (!isSelectionEqual(fileA.selection, fileB.selection)) return false
|
|
}
|
|
if (partA.type === "agent" && partA.name !== (partB as AgentPart).name) {
|
|
return false
|
|
}
|
|
if (partA.type === "image" && partA.id !== (partB as ImageAttachmentPart).id) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
function cloneSelection(selection?: FileSelection) {
|
|
if (!selection) return undefined
|
|
return { ...selection }
|
|
}
|
|
|
|
function clonePart(part: ContentPart): ContentPart {
|
|
if (part.type === "text") return { ...part }
|
|
if (part.type === "image") return { ...part }
|
|
if (part.type === "agent") return { ...part }
|
|
return {
|
|
...part,
|
|
selection: cloneSelection(part.selection),
|
|
}
|
|
}
|
|
|
|
function clonePrompt(prompt: Prompt): Prompt {
|
|
return prompt.map(clonePart)
|
|
}
|
|
|
|
export const { use: usePrompt, provider: PromptProvider } = createSimpleContext({
|
|
name: "Prompt",
|
|
init: () => {
|
|
const params = useParams()
|
|
const name = createMemo(() => `${params.dir}/prompt${params.id ? "/" + params.id : ""}.v2`)
|
|
|
|
const [store, setStore, _, ready] = persisted(
|
|
name(),
|
|
createStore<{
|
|
prompt: Prompt
|
|
cursor?: number
|
|
context: {
|
|
activeTab: boolean
|
|
items: (ContextItem & { key: string })[]
|
|
}
|
|
}>({
|
|
prompt: clonePrompt(DEFAULT_PROMPT),
|
|
cursor: undefined,
|
|
context: {
|
|
activeTab: true,
|
|
items: [],
|
|
},
|
|
}),
|
|
)
|
|
|
|
function keyForItem(item: ContextItem) {
|
|
if (item.type !== "file") return item.type
|
|
const start = item.selection?.startLine
|
|
const end = item.selection?.endLine
|
|
return `${item.type}:${item.path}:${start}:${end}`
|
|
}
|
|
|
|
return {
|
|
ready,
|
|
current: createMemo(() => store.prompt),
|
|
cursor: createMemo(() => store.cursor),
|
|
dirty: createMemo(() => !isPromptEqual(store.prompt, DEFAULT_PROMPT)),
|
|
context: {
|
|
activeTab: createMemo(() => store.context.activeTab),
|
|
items: createMemo(() => store.context.items),
|
|
addActive() {
|
|
setStore("context", "activeTab", true)
|
|
},
|
|
removeActive() {
|
|
setStore("context", "activeTab", false)
|
|
},
|
|
add(item: ContextItem) {
|
|
const key = keyForItem(item)
|
|
if (store.context.items.find((x) => x.key === key)) return
|
|
setStore("context", "items", (items) => [...items, { key, ...item }])
|
|
},
|
|
remove(key: string) {
|
|
setStore("context", "items", (items) => items.filter((x) => x.key !== key))
|
|
},
|
|
},
|
|
set(prompt: Prompt, cursorPosition?: number) {
|
|
const next = clonePrompt(prompt)
|
|
batch(() => {
|
|
setStore("prompt", next)
|
|
if (cursorPosition !== undefined) setStore("cursor", cursorPosition)
|
|
})
|
|
},
|
|
reset() {
|
|
batch(() => {
|
|
setStore("prompt", clonePrompt(DEFAULT_PROMPT))
|
|
setStore("cursor", 0)
|
|
})
|
|
},
|
|
}
|
|
},
|
|
})
|