feat(app): autocomplete mcp resources (#34597)

This commit is contained in:
Brendan Allan 2026-06-30 19:05:53 +08:00 committed by GitHub
commit 06cb0de0b1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 191 additions and 23 deletions

View file

@ -664,6 +664,20 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
.map((agent): AtOption => ({ type: "agent", name: agent.name, display: agent.name })),
)
const mcpResourceList = createMemo(() =>
Object.values(sync().data.mcp_resource).map(
(resource): AtOption => ({
type: "resource",
name: resource.name,
uri: resource.uri,
client: resource.client,
display: resource.name,
description: resource.description,
mime: resource.mimeType,
}),
),
)
const handleAtSelect = (option: AtOption | undefined) => {
if (!option) return
if (option.type === "agent") {
@ -682,6 +696,25 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
})
return
}
if (option.type === "resource") {
addPart({
type: "file",
path: option.uri,
content: "@" + option.name,
start: 0,
end: 0,
mime: option.mime ?? "text/plain",
filename: option.name,
url: option.uri,
source: {
type: "resource",
text: { value: "@" + option.name, start: 0, end: 0 },
clientName: option.client,
uri: option.uri,
},
})
return
}
addPart({ type: "file", path: option.path, content: "@" + option.path, start: 0, end: 0 })
}
@ -689,6 +722,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
if (!x) return ""
if (x.type === "agent") return `agent:${x.name}`
if (x.type === "reference") return `reference:${x.name}`
if (x.type === "resource") return `resource:${x.client}:${x.uri}`
return `file:${x.path}`
}
@ -702,15 +736,16 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
items: async (query) => {
const references = referenceList()
const agents = agentList()
const mcpResources = mcpResourceList()
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 [...references, ...agents, ...pinned]
if (!query.trim()) return [...references, ...agents, ...mcpResources, ...pinned]
const paths = await files.searchFilesAndDirectories(query)
const fileOptions: AtOption[] = paths
.filter((path) => !seen.has(path))
.map((path) => ({ type: "file", path, display: path }))
return [...references, ...agents, ...pinned, ...fileOptions]
return [...references, ...agents, ...mcpResources, ...pinned, ...fileOptions]
},
key: atKey,
filterKeys: ["display"],
@ -718,6 +753,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
groupBy: (item) => {
if (item.type === "reference") return "reference"
if (item.type === "agent") return "agent"
if (item.type === "resource") return "resource"
if (item.recent) return "recent"
return "file"
},
@ -725,8 +761,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const rank = (category: string) => {
if (category === "reference") return 0
if (category === "agent") return 1
if (category === "recent") return 2
return 3
if (category === "resource") return 2
if (category === "recent") return 3
return 4
}
return rank(a.category) - rank(b.category)
},
@ -796,6 +833,12 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
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.url) pill.setAttribute("data-url", part.url)
if (part.source?.type === "resource") {
pill.setAttribute("data-source-type", part.source.type)
pill.setAttribute("data-source-client-name", part.source.clientName)
pill.setAttribute("data-source-uri", part.source.uri)
}
}
if (part.type === "agent") pill.setAttribute("data-name", part.name)
pill.setAttribute("contenteditable", "false")
@ -911,6 +954,19 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const pushFile = (file: HTMLElement) => {
const content = file.textContent ?? ""
const source =
file.dataset.sourceType === "resource" && file.dataset.sourceClientName && file.dataset.sourceUri
? {
type: "resource" as const,
text: {
value: content,
start: position,
end: position + content.length,
},
clientName: file.dataset.sourceClientName,
uri: file.dataset.sourceUri,
}
: undefined
parts.push({
type: "file",
path: file.dataset.path!,
@ -919,6 +975,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
end: position + content.length,
...(file.dataset.mime ? { mime: file.dataset.mime } : {}),
...(file.dataset.filename ? { filename: file.dataset.filename } : {}),
...(file.dataset.url ? { url: file.dataset.url } : {}),
...(source ? { source } : {}),
})
position += content.length
}