chore: rm fuzzy search on references (#30931)
This commit is contained in:
@@ -271,47 +271,6 @@ export function Autocomplete(props: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createReferenceFilePart(input: {
|
|
||||||
alias: string
|
|
||||||
root: string
|
|
||||||
item: string
|
|
||||||
lineRange?: { startLine: number; endLine?: number }
|
|
||||||
}) {
|
|
||||||
const filename = `${input.alias}/${
|
|
||||||
input.lineRange && !input.item.endsWith("/")
|
|
||||||
? `${input.item}#${input.lineRange.startLine}${input.lineRange.endLine ? `-${input.lineRange.endLine}` : ""}`
|
|
||||||
: input.item
|
|
||||||
}`
|
|
||||||
const urlObj = pathToFileURL(path.join(input.root, input.item))
|
|
||||||
|
|
||||||
if (input.lineRange && !input.item.endsWith("/")) {
|
|
||||||
urlObj.searchParams.set("start", String(input.lineRange.startLine))
|
|
||||||
if (input.lineRange.endLine !== undefined) {
|
|
||||||
urlObj.searchParams.set("end", String(input.lineRange.endLine))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
filename,
|
|
||||||
url: urlObj.href,
|
|
||||||
part: {
|
|
||||||
type: "file" as const,
|
|
||||||
mime: input.item.endsWith("/") ? "application/x-directory" : "text/plain",
|
|
||||||
filename,
|
|
||||||
url: urlObj.href,
|
|
||||||
source: {
|
|
||||||
type: "file" as const,
|
|
||||||
text: {
|
|
||||||
start: 0,
|
|
||||||
end: 0,
|
|
||||||
value: "",
|
|
||||||
},
|
|
||||||
path: filename,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function referencePromptText(reference: Reference.Resolved) {
|
function referencePromptText(reference: Reference.Resolved) {
|
||||||
const problem = reference.kind === "invalid" ? reference.message : undefined
|
const problem = reference.kind === "invalid" ? reference.message : undefined
|
||||||
return [
|
return [
|
||||||
@@ -336,18 +295,12 @@ export function Autocomplete(props: {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
const referenceSearch = createMemo(() => {
|
const referenceMatch = createMemo(() => {
|
||||||
if (!store.visible || store.visible === "/") return
|
if (!store.visible || store.visible === "/") return
|
||||||
const { lineRange, baseQuery } = extractLineRange(search())
|
const { baseQuery } = extractLineRange(search())
|
||||||
const slash = baseQuery.indexOf("/")
|
const slash = baseQuery.indexOf("/")
|
||||||
if (slash === -1) return
|
const alias = slash === -1 ? baseQuery : baseQuery.slice(0, slash)
|
||||||
const reference = references().find((item) => item.name === baseQuery.slice(0, slash))
|
return references().find((item) => item.name === alias)
|
||||||
if (!reference || reference.kind === "invalid") return
|
|
||||||
return {
|
|
||||||
reference,
|
|
||||||
query: baseQuery.slice(slash + 1),
|
|
||||||
lineRange,
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
function normalizeMentionPath(filePath: string) {
|
function normalizeMentionPath(filePath: string) {
|
||||||
@@ -380,7 +333,7 @@ export function Autocomplete(props: {
|
|||||||
() => search(),
|
() => search(),
|
||||||
async (query) => {
|
async (query) => {
|
||||||
if (!store.visible || store.visible === "/") return []
|
if (!store.visible || store.visible === "/") return []
|
||||||
if (referenceSearch()) return []
|
if (referenceMatch()) return []
|
||||||
|
|
||||||
const { lineRange, baseQuery } = extractLineRange(query ?? "")
|
const { lineRange, baseQuery } = extractLineRange(query ?? "")
|
||||||
|
|
||||||
@@ -430,43 +383,6 @@ export function Autocomplete(props: {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
const [referenceFiles] = createResource(
|
|
||||||
() => referenceSearch(),
|
|
||||||
async (match) => {
|
|
||||||
if (!match) return []
|
|
||||||
|
|
||||||
const result = await sdk.client.find.files({
|
|
||||||
directory: match.reference.path,
|
|
||||||
query: match.query,
|
|
||||||
limit: 50,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (result.error || !result.data) return []
|
|
||||||
|
|
||||||
const width = props.anchor().width - 4
|
|
||||||
return result.data.map((item): AutocompleteOption => {
|
|
||||||
const { filename, part } = createReferenceFilePart({
|
|
||||||
alias: match.reference.name,
|
|
||||||
root: match.reference.path,
|
|
||||||
item,
|
|
||||||
lineRange: match.lineRange,
|
|
||||||
})
|
|
||||||
return {
|
|
||||||
display: Locale.truncateMiddle(filename, width),
|
|
||||||
value: filename,
|
|
||||||
isDirectory: item.endsWith("/"),
|
|
||||||
path: filename,
|
|
||||||
onSelect: () => {
|
|
||||||
insertPart(filename, part)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
{
|
|
||||||
initialValue: [],
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
const mcpResources = createMemo(() => {
|
const mcpResources = createMemo(() => {
|
||||||
if (!store.visible || store.visible === "/") return []
|
if (!store.visible || store.visible === "/") return []
|
||||||
|
|
||||||
@@ -529,8 +445,22 @@ export function Autocomplete(props: {
|
|||||||
references().map(
|
references().map(
|
||||||
(reference): AutocompleteOption => ({
|
(reference): AutocompleteOption => ({
|
||||||
display: "@" + reference.name,
|
display: "@" + reference.name,
|
||||||
description: reference.kind === "invalid" ? reference.message : " configured reference",
|
description: reference.kind === "invalid" ? reference.message : " dir",
|
||||||
onSelect: () => {
|
onSelect: () => {
|
||||||
|
if (reference.kind !== "invalid") {
|
||||||
|
insertPart(reference.name, {
|
||||||
|
type: "file",
|
||||||
|
mime: "application/x-directory",
|
||||||
|
filename: reference.name,
|
||||||
|
url: pathToFileURL(reference.path).href,
|
||||||
|
source: {
|
||||||
|
type: "file",
|
||||||
|
text: { start: 0, end: 0, value: "" },
|
||||||
|
path: reference.name,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
insertPart(reference.name, {
|
insertPart(reference.name, {
|
||||||
type: "text",
|
type: "text",
|
||||||
text: referencePromptText(reference),
|
text: referencePromptText(reference),
|
||||||
@@ -572,16 +502,15 @@ export function Autocomplete(props: {
|
|||||||
|
|
||||||
const options = createMemo((prev: AutocompleteOption[] | undefined) => {
|
const options = createMemo((prev: AutocompleteOption[] | undefined) => {
|
||||||
const filesValue = files()
|
const filesValue = files()
|
||||||
const referenceFilesValue = referenceFiles()
|
const referenceMatchValue = referenceMatch()
|
||||||
const referenceSearchValue = referenceSearch()
|
|
||||||
const agentsValue = agents()
|
const agentsValue = agents()
|
||||||
const referenceAliasesValue = referenceAliases()
|
const referenceAliasesValue = referenceAliases()
|
||||||
const commandsValue = commands()
|
const commandsValue = commands()
|
||||||
|
|
||||||
const mixed: AutocompleteOption[] =
|
const mixed: AutocompleteOption[] =
|
||||||
store.visible === "@"
|
store.visible === "@"
|
||||||
? referenceSearchValue
|
? referenceMatchValue
|
||||||
? referenceFilesValue || []
|
? referenceAliasesValue.filter((item) => item.display === `@${referenceMatchValue.name}`)
|
||||||
: [...referenceAliasesValue, ...agentsValue, ...(filesValue || []), ...mcpResources()]
|
: [...referenceAliasesValue, ...agentsValue, ...(filesValue || []), ...mcpResources()]
|
||||||
: [...commandsValue]
|
: [...commandsValue]
|
||||||
|
|
||||||
@@ -591,10 +520,12 @@ export function Autocomplete(props: {
|
|||||||
return mixed
|
return mixed
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((files.loading || referenceFiles.loading) && prev && prev.length > 0) {
|
if (files.loading && prev && prev.length > 0) {
|
||||||
return prev
|
return prev
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (referenceMatchValue) return mixed
|
||||||
|
|
||||||
const result = fuzzysort.go(removeLineRange(searchValue), mixed, {
|
const result = fuzzysort.go(removeLineRange(searchValue), mixed, {
|
||||||
keys: [
|
keys: [
|
||||||
(obj) => removeLineRange((obj.value ?? obj.display).trimEnd()),
|
(obj) => removeLineRange((obj.value ?? obj.display).trimEnd()),
|
||||||
|
|||||||
@@ -145,9 +145,47 @@ export const layer = Layer.effect(
|
|||||||
yield* state.cancel(sessionID)
|
yield* state.cancel(sessionID)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const resolveReferenceParts = Effect.fnUntraced(function* (template: string) {
|
||||||
|
const parts: Types.DeepMutable<PromptInput["parts"]> = []
|
||||||
|
const seen = new Set<string>()
|
||||||
|
yield* Effect.forEach(
|
||||||
|
ConfigMarkdown.files(template),
|
||||||
|
Effect.fnUntraced(function* (match) {
|
||||||
|
const name = match[1]
|
||||||
|
if (!name) return
|
||||||
|
const alias = name.split("/")[0]
|
||||||
|
if (!alias || seen.has(alias)) return
|
||||||
|
const reference = yield* references.get(alias)
|
||||||
|
if (!reference) return
|
||||||
|
seen.add(alias)
|
||||||
|
|
||||||
|
const start = match.index ?? 0
|
||||||
|
const source = { value: match[0], start, end: start + match[0].length }
|
||||||
|
if (reference.kind === "invalid") {
|
||||||
|
parts.push(referenceTextPart({ reference, source }))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
yield* references.ensure(reference.path)
|
||||||
|
parts.push({
|
||||||
|
type: "file",
|
||||||
|
url: pathToFileURL(reference.path).href,
|
||||||
|
filename: alias,
|
||||||
|
mime: "application/x-directory",
|
||||||
|
source: { type: "file", text: source, path: alias },
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
{ concurrency: 1, discard: true },
|
||||||
|
)
|
||||||
|
return parts
|
||||||
|
})
|
||||||
|
|
||||||
const resolvePromptParts = Effect.fn("SessionPrompt.resolvePromptParts")(function* (template: string) {
|
const resolvePromptParts = Effect.fn("SessionPrompt.resolvePromptParts")(function* (template: string) {
|
||||||
const ctx = yield* InstanceState.context
|
const ctx = yield* InstanceState.context
|
||||||
const parts: Types.DeepMutable<PromptInput["parts"]> = [{ type: "text", text: template }]
|
const parts: Types.DeepMutable<PromptInput["parts"]> = [
|
||||||
|
{ type: "text", text: template },
|
||||||
|
...(yield* resolveReferenceParts(template)),
|
||||||
|
]
|
||||||
const files = ConfigMarkdown.files(template)
|
const files = ConfigMarkdown.files(template)
|
||||||
const seen = new Set<string>()
|
const seen = new Set<string>()
|
||||||
yield* Effect.forEach(
|
yield* Effect.forEach(
|
||||||
@@ -160,60 +198,7 @@ export const layer = Layer.effect(
|
|||||||
|
|
||||||
const slash = name.indexOf("/")
|
const slash = name.indexOf("/")
|
||||||
const alias = slash === -1 ? name : name.slice(0, slash)
|
const alias = slash === -1 ? name : name.slice(0, slash)
|
||||||
const reference = yield* references.get(alias)
|
if (yield* references.get(alias)) return
|
||||||
if (reference) {
|
|
||||||
const start = match.index ?? 0
|
|
||||||
const source = { value: match[0], start, end: start + match[0].length }
|
|
||||||
if (reference.kind === "invalid") {
|
|
||||||
parts.push(
|
|
||||||
referenceTextPart({ reference, source, target: slash === -1 ? undefined : name.slice(slash + 1) }),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
yield* references.ensure(reference.path)
|
|
||||||
if (slash === -1) {
|
|
||||||
parts.push(referenceTextPart({ reference, source }))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const target = name.slice(slash + 1)
|
|
||||||
const targetPath = path.resolve(reference.path, target)
|
|
||||||
if (!FSUtil.contains(reference.path, targetPath)) {
|
|
||||||
parts.push(
|
|
||||||
referenceTextPart({
|
|
||||||
reference,
|
|
||||||
source,
|
|
||||||
target,
|
|
||||||
targetPath,
|
|
||||||
problem: `Path escapes configured reference @${alias}: ${target}`,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const info = yield* fsys.stat(targetPath).pipe(Effect.option)
|
|
||||||
if (Option.isNone(info)) {
|
|
||||||
parts.push(
|
|
||||||
referenceTextPart({
|
|
||||||
reference,
|
|
||||||
source,
|
|
||||||
target,
|
|
||||||
targetPath,
|
|
||||||
problem: `Path does not exist inside configured reference @${alias}: ${target}`,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
parts.push({
|
|
||||||
type: "file",
|
|
||||||
url: pathToFileURL(targetPath).href,
|
|
||||||
filename: name,
|
|
||||||
mime: info.value.type === "Directory" ? "application/x-directory" : "text/plain",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const filepath = name.startsWith("~/")
|
const filepath = name.startsWith("~/")
|
||||||
? path.join(os.homedir(), name.slice(2))
|
? path.join(os.homedir(), name.slice(2))
|
||||||
@@ -770,30 +755,6 @@ export const layer = Layer.effect(
|
|||||||
id: part.id ? PartID.make(part.id) : PartID.ascending(),
|
id: part.id ? PartID.make(part.id) : PartID.ascending(),
|
||||||
})
|
})
|
||||||
|
|
||||||
const referenceContextFromFilePart = Effect.fnUntraced(function* (
|
|
||||||
part: Extract<PromptInput["parts"][number], { type: "file" }>,
|
|
||||||
filepath: string,
|
|
||||||
) {
|
|
||||||
const name = part.filename?.replace(/#\d+(?:-\d*)?$/, "")
|
|
||||||
if (!name) return
|
|
||||||
const slash = name.indexOf("/")
|
|
||||||
if (slash === -1) return
|
|
||||||
|
|
||||||
const reference = yield* references.get(name.slice(0, slash))
|
|
||||||
if (!reference || reference.kind === "invalid") return
|
|
||||||
if (!FSUtil.contains(reference.path, filepath)) return
|
|
||||||
|
|
||||||
const target = path.relative(reference.path, filepath).split(path.sep).join("/")
|
|
||||||
if (!target || target.startsWith("../") || target === "..") return
|
|
||||||
|
|
||||||
return referenceTextPart({
|
|
||||||
reference,
|
|
||||||
source: part.source?.text ?? { value: `@${name}`, start: 0, end: name.length + 1 },
|
|
||||||
target,
|
|
||||||
targetPath: filepath,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
const resolvePart: (part: PromptInput["parts"][number]) => Effect.Effect<Draft<SessionV1.Part>[]> = Effect.fn(
|
const resolvePart: (part: PromptInput["parts"][number]) => Effect.Effect<Draft<SessionV1.Part>[]> = Effect.fn(
|
||||||
"SessionPrompt.resolveUserPart",
|
"SessionPrompt.resolveUserPart",
|
||||||
)(function* (part) {
|
)(function* (part) {
|
||||||
@@ -876,7 +837,6 @@ export const layer = Layer.effect(
|
|||||||
case "file:": {
|
case "file:": {
|
||||||
log.info("file", { mime: part.mime })
|
log.info("file", { mime: part.mime })
|
||||||
const filepath = fileURLToPath(part.url)
|
const filepath = fileURLToPath(part.url)
|
||||||
const referenceContext = yield* referenceContextFromFilePart(part, filepath)
|
|
||||||
const mime = (yield* fsys.isDir(filepath)) ? "application/x-directory" : part.mime
|
const mime = (yield* fsys.isDir(filepath)) ? "application/x-directory" : part.mime
|
||||||
|
|
||||||
const { read } = yield* registry.named()
|
const { read } = yield* registry.named()
|
||||||
@@ -922,9 +882,6 @@ export const layer = Layer.effect(
|
|||||||
}
|
}
|
||||||
const args = { filePath: filepath, offset, limit }
|
const args = { filePath: filepath, offset, limit }
|
||||||
const pieces: Draft<SessionV1.Part>[] = [
|
const pieces: Draft<SessionV1.Part>[] = [
|
||||||
...(referenceContext
|
|
||||||
? [{ ...referenceContext, messageID: info.id, sessionID: input.sessionID }]
|
|
||||||
: []),
|
|
||||||
{
|
{
|
||||||
messageID: info.id,
|
messageID: info.id,
|
||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
@@ -990,9 +947,6 @@ export const layer = Layer.effect(
|
|||||||
error: new NamedError.Unknown({ message }).toObject(),
|
error: new NamedError.Unknown({ message }).toObject(),
|
||||||
})
|
})
|
||||||
return [
|
return [
|
||||||
...(referenceContext
|
|
||||||
? [{ ...referenceContext, messageID: info.id, sessionID: input.sessionID }]
|
|
||||||
: []),
|
|
||||||
{
|
{
|
||||||
messageID: info.id,
|
messageID: info.id,
|
||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
@@ -1003,9 +957,6 @@ export const layer = Layer.effect(
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
return [
|
return [
|
||||||
...(referenceContext
|
|
||||||
? [{ ...referenceContext, messageID: info.id, sessionID: input.sessionID }]
|
|
||||||
: []),
|
|
||||||
{
|
{
|
||||||
messageID: info.id,
|
messageID: info.id,
|
||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
@@ -1025,7 +976,6 @@ export const layer = Layer.effect(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
...(referenceContext ? [{ ...referenceContext, messageID: info.id, sessionID: input.sessionID }] : []),
|
|
||||||
{
|
{
|
||||||
messageID: info.id,
|
messageID: info.id,
|
||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
@@ -1071,7 +1021,22 @@ export const layer = Layer.effect(
|
|||||||
return [{ ...part, messageID: info.id, sessionID: input.sessionID }]
|
return [{ ...part, messageID: info.id, sessionID: input.sessionID }]
|
||||||
})
|
})
|
||||||
|
|
||||||
const resolvedParts = yield* Effect.forEach(input.parts, resolvePart, { concurrency: "unbounded" }).pipe(
|
const submittedParts: Types.DeepMutable<PromptInput["parts"]> = [...input.parts]
|
||||||
|
const attachedReferences = new Set(
|
||||||
|
input.parts.flatMap((part) =>
|
||||||
|
part.type === "file" && part.mime === "application/x-directory" ? [part.url] : [],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
for (const part of input.parts) {
|
||||||
|
if (part.type !== "text" || part.synthetic) continue
|
||||||
|
for (const reference of yield* resolveReferenceParts(part.text)) {
|
||||||
|
if (reference.type === "file" && attachedReferences.has(reference.url)) continue
|
||||||
|
if (reference.type === "file") attachedReferences.add(reference.url)
|
||||||
|
submittedParts.push(reference)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvedParts = yield* Effect.forEach(submittedParts, resolvePart, { concurrency: "unbounded" }).pipe(
|
||||||
Effect.map((x) => x.flat().map(assign)),
|
Effect.map((x) => x.flat().map(assign)),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1969,7 +1969,7 @@ noLLMServer.instance(
|
|||||||
)
|
)
|
||||||
|
|
||||||
noLLMServer.instance(
|
noLLMServer.instance(
|
||||||
"resolves configured reference mentions before workspace paths and agents",
|
"resolves configured reference mentions to one root directory attachment",
|
||||||
() =>
|
() =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const { directory: dir } = yield* TestInstance
|
const { directory: dir } = yield* TestInstance
|
||||||
@@ -1984,33 +1984,18 @@ noLLMServer.instance(
|
|||||||
const parts = yield* prompt.resolvePromptParts(
|
const parts = yield* prompt.resolvePromptParts(
|
||||||
"Use @docs and @docs/README.md and @docs/guide and @docs/missing.md and @docs/README.md and @build",
|
"Use @docs and @docs/README.md and @docs/guide and @docs/missing.md and @docs/README.md and @build",
|
||||||
)
|
)
|
||||||
const references = parts.filter(
|
|
||||||
(part): part is SessionV1.TextPartInput =>
|
|
||||||
part.type === "text" && part.synthetic === true && part.text.startsWith("Referenced configured reference "),
|
|
||||||
)
|
|
||||||
const files = parts.filter((part): part is SessionV1.FilePartInput => part.type === "file")
|
const files = parts.filter((part): part is SessionV1.FilePartInput => part.type === "file")
|
||||||
const agents = parts.filter((part): part is SessionV1.AgentPartInput => part.type === "agent")
|
const agents = parts.filter((part): part is SessionV1.AgentPartInput => part.type === "agent")
|
||||||
const bare = references.find((part) => part.text.includes("@docs."))
|
const text = parts.find((part): part is SessionV1.TextPartInput => part.type === "text" && !part.synthetic)
|
||||||
const missing = references.find((part) => part.text.includes("@docs/missing.md"))
|
|
||||||
const guide = files.find((part) => part.filename === "docs/guide")
|
|
||||||
|
|
||||||
expect(references.length).toBe(2)
|
expect(text?.text).toContain("@docs")
|
||||||
expect(bare?.metadata?.reference).toMatchObject({
|
expect(files).toHaveLength(1)
|
||||||
name: "docs",
|
expect(files[0]).toMatchObject({
|
||||||
kind: "local",
|
filename: "docs",
|
||||||
path: docs,
|
mime: "application/x-directory",
|
||||||
|
source: { type: "file", path: "docs", text: { value: "@docs" } },
|
||||||
})
|
})
|
||||||
expect(missing?.text).toContain("Path does not exist inside configured reference @docs")
|
expect(fileURLToPath(files[0].url)).toBe(docs)
|
||||||
expect(missing?.metadata?.reference).toMatchObject({
|
|
||||||
target: "missing.md",
|
|
||||||
targetPath: path.join(docs, "missing.md"),
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(files.length).toBe(2)
|
|
||||||
expect(files.map((file) => fileURLToPath(file.url)).sort()).toEqual(
|
|
||||||
[path.join(docs, "README.md"), path.join(docs, "guide")].sort(),
|
|
||||||
)
|
|
||||||
expect(guide?.mime).toBe("application/x-directory")
|
|
||||||
expect(agents.map((agent) => agent.name)).toEqual(["build"])
|
expect(agents.map((agent) => agent.name)).toEqual(["build"])
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
@@ -2024,7 +2009,7 @@ noLLMServer.instance(
|
|||||||
)
|
)
|
||||||
|
|
||||||
noLLMServer.instance(
|
noLLMServer.instance(
|
||||||
"injects metadata for bare configured reference mentions",
|
"stores raw reference mentions alongside directory attachments",
|
||||||
() =>
|
() =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const { directory: dir } = yield* TestInstance
|
const { directory: dir } = yield* TestInstance
|
||||||
@@ -2037,83 +2022,25 @@ noLLMServer.instance(
|
|||||||
const message = yield* prompt.prompt({
|
const message = yield* prompt.prompt({
|
||||||
sessionID: session.id,
|
sessionID: session.id,
|
||||||
noReply: true,
|
noReply: true,
|
||||||
parts: yield* prompt.resolvePromptParts("Use @docs for context"),
|
parts: [{ type: "text", text: "Use @docs for context" }],
|
||||||
})
|
})
|
||||||
|
|
||||||
const stored = yield* MessageV2.get({ sessionID: session.id, messageID: message.info.id })
|
const stored = yield* MessageV2.get({ sessionID: session.id, messageID: message.info.id })
|
||||||
const synthetic = stored.parts.filter(
|
const synthetic = stored.parts.filter(
|
||||||
(part): part is SessionV1.TextPart => part.type === "text" && part.synthetic === true,
|
(part): part is SessionV1.TextPart => part.type === "text" && part.synthetic === true,
|
||||||
)
|
)
|
||||||
const reference = synthetic.find((part) => part.text.startsWith("Referenced configured reference @docs."))
|
const files = stored.parts.filter((part): part is SessionV1.FilePart => part.type === "file")
|
||||||
|
const text = stored.parts.find((part): part is SessionV1.TextPart => part.type === "text" && !part.synthetic)
|
||||||
|
|
||||||
expect(reference?.metadata?.reference).toMatchObject({ name: "docs", kind: "local", path: docs })
|
expect(text?.text).toBe("Use @docs for context")
|
||||||
expect(synthetic.some((part) => part.text.includes(`Reference root: ${docs}`))).toBe(true)
|
expect(synthetic.some((part) => part.text.includes(JSON.stringify({ filePath: docs })))).toBe(true)
|
||||||
expect(synthetic.some((part) => part.text.includes("Inspect the configured reference"))).toBe(true)
|
expect(files).toHaveLength(1)
|
||||||
|
expect(files[0]).toMatchObject({
|
||||||
yield* sessions.remove(session.id)
|
filename: "docs",
|
||||||
}),
|
mime: "application/x-directory",
|
||||||
{
|
source: { type: "file", path: "docs", text: { value: "@docs", start: 4, end: 9 } },
|
||||||
config: {
|
|
||||||
...cfg,
|
|
||||||
reference: {
|
|
||||||
docs: "./external-docs",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
noLLMServer.instance(
|
|
||||||
"injects metadata for configured reference file attachments",
|
|
||||||
() =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const { directory: dir } = yield* TestInstance
|
|
||||||
const docs = path.join(dir, "external-docs")
|
|
||||||
const readme = path.join(docs, "README.md")
|
|
||||||
yield* ensureDir(docs)
|
|
||||||
yield* writeText(readme, "reference readme")
|
|
||||||
|
|
||||||
const prompt = yield* SessionPrompt.Service
|
|
||||||
const sessions = yield* Session.Service
|
|
||||||
const session = yield* sessions.create({})
|
|
||||||
const message = yield* prompt.prompt({
|
|
||||||
sessionID: session.id,
|
|
||||||
agent: "build",
|
|
||||||
noReply: true,
|
|
||||||
parts: [
|
|
||||||
{ type: "text", text: "Read @docs/README.md" },
|
|
||||||
{
|
|
||||||
type: "file",
|
|
||||||
mime: "text/plain",
|
|
||||||
filename: "docs/README.md",
|
|
||||||
url: pathToFileURL(readme).href,
|
|
||||||
source: {
|
|
||||||
type: "file",
|
|
||||||
path: "docs/README.md",
|
|
||||||
text: { value: "@docs/README.md", start: 5, end: 20 },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
})
|
||||||
|
expect(fileURLToPath(files[0].url)).toBe(docs)
|
||||||
const stored = yield* MessageV2.get({ sessionID: session.id, messageID: message.info.id })
|
|
||||||
const synthetic = stored.parts.filter(
|
|
||||||
(part): part is SessionV1.TextPart => part.type === "text" && part.synthetic === true,
|
|
||||||
)
|
|
||||||
const reference = synthetic.find((part) =>
|
|
||||||
part.text.startsWith("Referenced configured reference @docs/README.md."),
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(reference?.metadata?.reference).toMatchObject({
|
|
||||||
name: "docs",
|
|
||||||
kind: "local",
|
|
||||||
path: docs,
|
|
||||||
target: "README.md",
|
|
||||||
targetPath: readme,
|
|
||||||
source: { value: "@docs/README.md", start: 5, end: 20 },
|
|
||||||
})
|
|
||||||
expect(synthetic.findIndex((part) => part === reference)).toBeLessThan(
|
|
||||||
synthetic.findIndex((part) => part.text.startsWith("Called the Read tool with the following input:")),
|
|
||||||
)
|
|
||||||
|
|
||||||
yield* sessions.remove(session.id)
|
yield* sessions.remove(session.id)
|
||||||
}),
|
}),
|
||||||
|
|||||||
Reference in New Issue
Block a user