|
|
@@ -68,6 +68,7 @@ import { promptPlaceholder } from "./prompt-input/placeholder"
|
|
|
import { createPromptInputTransientState } from "./prompt-input/transient-state"
|
|
|
import { showToast } from "@/utils/toast"
|
|
|
import { ImagePreview } from "@opencode-ai/ui/image-preview"
|
|
|
+import type { ReferenceInfo } from "@opencode-ai/sdk/v2/client"
|
|
|
|
|
|
export type PromptInputState = ReturnType<typeof usePrompt>
|
|
|
|
|
|
@@ -640,6 +641,23 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|
|
})
|
|
|
}
|
|
|
|
|
|
+ const referenceDescription = (reference: ReferenceInfo) =>
|
|
|
+ reference.source.type === "git" ? reference.source.repository : reference.source.path
|
|
|
+
|
|
|
+ const referenceList = createMemo(() =>
|
|
|
+ sync()
|
|
|
+ .data.reference.filter((reference) => !reference.hidden)
|
|
|
+ .map(
|
|
|
+ (reference): AtOption => ({
|
|
|
+ type: "reference",
|
|
|
+ name: reference.name,
|
|
|
+ path: reference.path,
|
|
|
+ display: reference.name,
|
|
|
+ description: reference.description ?? referenceDescription(reference),
|
|
|
+ }),
|
|
|
+ ),
|
|
|
+ )
|
|
|
+
|
|
|
const agentList = createMemo(() =>
|
|
|
props.controls.agents.available
|
|
|
.filter((agent) => !agent.hidden && agent.mode !== "primary")
|
|
|
@@ -650,14 +668,28 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|
|
if (!option) return
|
|
|
if (option.type === "agent") {
|
|
|
addPart({ type: "agent", name: option.name, content: "@" + option.name, start: 0, end: 0 })
|
|
|
- } else {
|
|
|
- addPart({ type: "file", path: option.path, content: "@" + option.path, start: 0, end: 0 })
|
|
|
+ return
|
|
|
}
|
|
|
+ if (option.type === "reference") {
|
|
|
+ addPart({
|
|
|
+ type: "file",
|
|
|
+ path: option.path,
|
|
|
+ content: "@" + option.name,
|
|
|
+ start: 0,
|
|
|
+ end: 0,
|
|
|
+ mime: "application/x-directory",
|
|
|
+ filename: option.name,
|
|
|
+ })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ addPart({ type: "file", path: option.path, content: "@" + option.path, start: 0, end: 0 })
|
|
|
}
|
|
|
|
|
|
const atKey = (x: AtOption | undefined) => {
|
|
|
if (!x) return ""
|
|
|
- return x.type === "agent" ? `agent:${x.name}` : `file:${x.path}`
|
|
|
+ if (x.type === "agent") return `agent:${x.name}`
|
|
|
+ if (x.type === "reference") return `reference:${x.name}`
|
|
|
+ return `file:${x.path}`
|
|
|
}
|
|
|
|
|
|
const {
|
|
|
@@ -668,30 +700,33 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|
|
onKeyDown: atOnKeyDown,
|
|
|
} = useFilteredList<AtOption>({
|
|
|
items: async (query) => {
|
|
|
+ const references = referenceList()
|
|
|
const agents = agentList()
|
|
|
const open = recent()
|
|
|
const seen = new Set(open)
|
|
|
const pinned: AtOption[] = open.map((path) => ({ type: "file", path, display: path, recent: true }))
|
|
|
- if (!query.trim()) return [...agents, ...pinned]
|
|
|
+ if (!query.trim()) return [...references, ...agents, ...pinned]
|
|
|
const paths = await files.searchFilesAndDirectories(query)
|
|
|
const fileOptions: AtOption[] = paths
|
|
|
.filter((path) => !seen.has(path))
|
|
|
.map((path) => ({ type: "file", path, display: path }))
|
|
|
- return [...agents, ...pinned, ...fileOptions]
|
|
|
+ return [...references, ...agents, ...pinned, ...fileOptions]
|
|
|
},
|
|
|
key: atKey,
|
|
|
filterKeys: ["display"],
|
|
|
skipFilter: (item) => item.type === "file" && !item.recent,
|
|
|
groupBy: (item) => {
|
|
|
+ if (item.type === "reference") return "reference"
|
|
|
if (item.type === "agent") return "agent"
|
|
|
if (item.recent) return "recent"
|
|
|
return "file"
|
|
|
},
|
|
|
sortGroupsBy: (a, b) => {
|
|
|
const rank = (category: string) => {
|
|
|
- if (category === "agent") return 0
|
|
|
- if (category === "recent") return 1
|
|
|
- return 2
|
|
|
+ if (category === "reference") return 0
|
|
|
+ if (category === "agent") return 1
|
|
|
+ if (category === "recent") return 2
|
|
|
+ return 3
|
|
|
}
|
|
|
return rank(a.category) - rank(b.category)
|
|
|
},
|
|
|
@@ -757,7 +792,11 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|
|
const pill = document.createElement("span")
|
|
|
pill.textContent = part.content
|
|
|
pill.setAttribute("data-type", part.type)
|
|
|
- if (part.type === "file") pill.setAttribute("data-path", part.path)
|
|
|
+ if (part.type === "file") {
|
|
|
+ pill.setAttribute("data-path", part.path)
|
|
|
+ if (part.mime) pill.setAttribute("data-mime", part.mime)
|
|
|
+ if (part.filename) pill.setAttribute("data-filename", part.filename)
|
|
|
+ }
|
|
|
if (part.type === "agent") pill.setAttribute("data-name", part.name)
|
|
|
pill.setAttribute("contenteditable", "false")
|
|
|
pill.style.userSelect = "text"
|
|
|
@@ -878,6 +917,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|
|
content,
|
|
|
start: position,
|
|
|
end: position + content.length,
|
|
|
+ ...(file.dataset.mime ? { mime: file.dataset.mime } : {}),
|
|
|
+ ...(file.dataset.filename ? { filename: file.dataset.filename } : {}),
|
|
|
})
|
|
|
position += content.length
|
|
|
}
|