feat(scout): autocomplete configured mentions (#26843)
This commit is contained in:
@@ -18,6 +18,8 @@ import { Locale } from "@/util/locale"
|
|||||||
import type { PromptInfo } from "./history"
|
import type { PromptInfo } from "./history"
|
||||||
import { useFrecency } from "./frecency"
|
import { useFrecency } from "./frecency"
|
||||||
import { useBindings } from "../../keymap"
|
import { useBindings } from "../../keymap"
|
||||||
|
import { Reference } from "@/reference/reference"
|
||||||
|
import type { Config } from "@/config/config"
|
||||||
|
|
||||||
function removeLineRange(input: string) {
|
function removeLineRange(input: string) {
|
||||||
const hashIndex = input.lastIndexOf("#")
|
const hashIndex = input.lastIndexOf("#")
|
||||||
@@ -260,6 +262,87 @@ 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) {
|
||||||
|
const problem = reference.kind === "invalid" ? reference.message : undefined
|
||||||
|
return [
|
||||||
|
`Referenced configured reference @${reference.name}.`,
|
||||||
|
...(reference.kind === "local" ? ["Kind: local directory"] : []),
|
||||||
|
...(reference.kind === "git" ? ["Kind: git repository"] : []),
|
||||||
|
...(reference.kind === "invalid" ? [`Repository: ${reference.repository}`] : []),
|
||||||
|
...(reference.kind === "git" ? [`Repository: ${reference.repository}`] : []),
|
||||||
|
...(reference.kind === "git" && reference.branch ? [`Branch/ref: ${reference.branch}`] : []),
|
||||||
|
...(reference.kind === "invalid" ? [] : [`Reference root: ${reference.path}`]),
|
||||||
|
...(problem
|
||||||
|
? [`Problem: ${problem}`]
|
||||||
|
: [
|
||||||
|
"For targeted context, inspect the reference path directly with Read, Glob, and Grep. For broader research, call the task tool with subagent scout and include this reference path.",
|
||||||
|
]),
|
||||||
|
].join("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
const references = createMemo(() =>
|
||||||
|
Reference.resolveAll({
|
||||||
|
references: (sync.data.config.reference ?? {}) as NonNullable<Config.Info["reference"]>,
|
||||||
|
directory: sync.path.directory || process.cwd(),
|
||||||
|
worktree: sync.path.worktree || sync.path.directory || process.cwd(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const referenceSearch = createMemo(() => {
|
||||||
|
if (!store.visible || store.visible === "/") return
|
||||||
|
const { lineRange, baseQuery } = extractLineRange(search())
|
||||||
|
const slash = baseQuery.indexOf("/")
|
||||||
|
if (slash === -1) return
|
||||||
|
const reference = references().find((item) => item.name === baseQuery.slice(0, slash))
|
||||||
|
if (!reference || reference.kind === "invalid") return
|
||||||
|
return {
|
||||||
|
reference,
|
||||||
|
query: baseQuery.slice(slash + 1),
|
||||||
|
lineRange,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
function normalizeMentionPath(filePath: string) {
|
function normalizeMentionPath(filePath: string) {
|
||||||
const baseDir = sync.path.directory || process.cwd()
|
const baseDir = sync.path.directory || process.cwd()
|
||||||
const absolute = path.resolve(filePath)
|
const absolute = path.resolve(filePath)
|
||||||
@@ -291,6 +374,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 []
|
||||||
|
|
||||||
const { lineRange, baseQuery } = extractLineRange(query ?? "")
|
const { lineRange, baseQuery } = extractLineRange(query ?? "")
|
||||||
|
|
||||||
@@ -339,6 +423,43 @@ 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 []
|
||||||
|
|
||||||
@@ -397,6 +518,22 @@ export function Autocomplete(props: {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const referenceAliases = createMemo(() =>
|
||||||
|
references().map(
|
||||||
|
(reference): AutocompleteOption => ({
|
||||||
|
display: "@" + reference.name,
|
||||||
|
description: reference.kind === "invalid" ? reference.message : " configured reference",
|
||||||
|
onSelect: () => {
|
||||||
|
insertPart(reference.name, {
|
||||||
|
type: "text",
|
||||||
|
text: referencePromptText(reference),
|
||||||
|
synthetic: true,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
const commands = createMemo((): AutocompleteOption[] => {
|
const commands = createMemo((): AutocompleteOption[] => {
|
||||||
const results: AutocompleteOption[] = [...command.slashes()]
|
const results: AutocompleteOption[] = [...command.slashes()]
|
||||||
|
|
||||||
@@ -428,11 +565,18 @@ 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 referenceSearchValue = referenceSearch()
|
||||||
const agentsValue = agents()
|
const agentsValue = agents()
|
||||||
|
const referenceAliasesValue = referenceAliases()
|
||||||
const commandsValue = commands()
|
const commandsValue = commands()
|
||||||
|
|
||||||
const mixed: AutocompleteOption[] =
|
const mixed: AutocompleteOption[] =
|
||||||
store.visible === "@" ? [...agentsValue, ...(filesValue || []), ...mcpResources()] : [...commandsValue]
|
store.visible === "@"
|
||||||
|
? referenceSearchValue
|
||||||
|
? referenceFilesValue || []
|
||||||
|
: [...referenceAliasesValue, ...agentsValue, ...(filesValue || []), ...mcpResources()]
|
||||||
|
: [...commandsValue]
|
||||||
|
|
||||||
const searchValue = search()
|
const searchValue = search()
|
||||||
|
|
||||||
@@ -440,7 +584,7 @@ export function Autocomplete(props: {
|
|||||||
return mixed
|
return mixed
|
||||||
}
|
}
|
||||||
|
|
||||||
if (files.loading && prev && prev.length > 0) {
|
if ((files.loading || referenceFiles.loading) && prev && prev.length > 0) {
|
||||||
return prev
|
return prev
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -505,7 +649,7 @@ export function Autocomplete(props: {
|
|||||||
const input = props.input()
|
const input = props.input()
|
||||||
const currentCursorOffset = input.cursorOffset
|
const currentCursorOffset = input.cursorOffset
|
||||||
|
|
||||||
const displayText = selected.display.trimEnd()
|
const displayText = (selected.value ?? selected.display).trimEnd()
|
||||||
const path = displayText.startsWith("@") ? displayText.slice(1) : displayText
|
const path = displayText.startsWith("@") ? displayText.slice(1) : displayText
|
||||||
|
|
||||||
input.cursorOffset = store.index
|
input.cursorOffset = store.index
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ export const Info = Schema.Struct({
|
|||||||
}),
|
}),
|
||||||
skills: Schema.optional(ConfigSkills.Info).annotate({ description: "Additional skill folder paths" }),
|
skills: Schema.optional(ConfigSkills.Info).annotate({ description: "Additional skill folder paths" }),
|
||||||
reference: Schema.optional(ConfigReference.Info).annotate({
|
reference: Schema.optional(ConfigReference.Info).annotate({
|
||||||
description: "Named git or local directory references that can be @ mentioned as Scout-backed subagents",
|
description: "Named git or local directory references that can be mentioned as @alias or @alias/path",
|
||||||
}),
|
}),
|
||||||
watcher: Schema.optional(
|
watcher: Schema.optional(
|
||||||
Schema.Struct({
|
Schema.Struct({
|
||||||
|
|||||||
@@ -121,6 +121,45 @@ function referencePromptMetadata(input: unknown): ReferencePromptMetadata | unde
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function referenceTextPart(input: {
|
||||||
|
reference: Reference.Resolved
|
||||||
|
source: ReferencePromptMetadata["source"]
|
||||||
|
target?: string
|
||||||
|
targetPath?: string
|
||||||
|
problem?: string
|
||||||
|
}): MessageV2.TextPartInput {
|
||||||
|
const metadata: ReferencePromptMetadata = {
|
||||||
|
name: input.reference.name,
|
||||||
|
kind: input.reference.kind,
|
||||||
|
...(input.reference.kind === "invalid" ? { repository: input.reference.repository } : { path: input.reference.path }),
|
||||||
|
...(input.reference.kind === "git" ? { repository: input.reference.repository, branch: input.reference.branch } : {}),
|
||||||
|
...(input.target === undefined ? {} : { target: input.target }),
|
||||||
|
...(input.targetPath ? { targetPath: input.targetPath } : {}),
|
||||||
|
problem: input.problem ?? (input.reference.kind === "invalid" ? input.reference.message : undefined),
|
||||||
|
source: input.source,
|
||||||
|
}
|
||||||
|
const label = metadata.target === undefined ? `@${metadata.name}` : `@${metadata.name}/${metadata.target}`
|
||||||
|
return {
|
||||||
|
type: "text",
|
||||||
|
synthetic: true,
|
||||||
|
text: [
|
||||||
|
`Referenced configured reference ${label}.`,
|
||||||
|
...(metadata.kind === "local" ? ["Kind: local directory"] : []),
|
||||||
|
...(metadata.kind === "git" ? ["Kind: git repository"] : []),
|
||||||
|
...(metadata.repository ? [`Repository: ${metadata.repository}`] : []),
|
||||||
|
...(metadata.branch ? [`Branch/ref: ${metadata.branch}`] : []),
|
||||||
|
...(metadata.path ? [`Reference root: ${metadata.path}`] : []),
|
||||||
|
...(metadata.targetPath ? [`Resolved path: ${metadata.targetPath}`] : []),
|
||||||
|
...(metadata.problem
|
||||||
|
? [`Problem: ${metadata.problem}`]
|
||||||
|
: [
|
||||||
|
"For targeted context, inspect the reference path directly with Read, Glob, and Grep. For broader research, call the task tool with subagent scout and include this reference path.",
|
||||||
|
]),
|
||||||
|
].join("\n"),
|
||||||
|
metadata: { reference: metadata },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export interface Interface {
|
export interface Interface {
|
||||||
readonly cancel: (sessionID: SessionID) => Effect.Effect<void>
|
readonly cancel: (sessionID: SessionID) => Effect.Effect<void>
|
||||||
readonly prompt: (input: PromptInput) => Effect.Effect<MessageV2.WithParts>
|
readonly prompt: (input: PromptInput) => Effect.Effect<MessageV2.WithParts>
|
||||||
@@ -186,48 +225,6 @@ export const layer = Layer.effect(
|
|||||||
const start = match.index ?? 0
|
const start = match.index ?? 0
|
||||||
return { value: match[0], start, end: start + match[0].length }
|
return { value: match[0], start, end: start + match[0].length }
|
||||||
}
|
}
|
||||||
const referenceTextPart = (input: {
|
|
||||||
reference: Reference.Resolved
|
|
||||||
source: ReturnType<typeof mentionSource>
|
|
||||||
target?: string
|
|
||||||
targetPath?: string
|
|
||||||
problem?: string
|
|
||||||
}): MessageV2.TextPartInput => {
|
|
||||||
const metadata: ReferencePromptMetadata = {
|
|
||||||
name: input.reference.name,
|
|
||||||
kind: input.reference.kind,
|
|
||||||
...(input.reference.kind === "invalid"
|
|
||||||
? { repository: input.reference.repository }
|
|
||||||
: { path: input.reference.path }),
|
|
||||||
...(input.reference.kind === "git"
|
|
||||||
? { repository: input.reference.repository, branch: input.reference.branch }
|
|
||||||
: {}),
|
|
||||||
...(input.target === undefined ? {} : { target: input.target }),
|
|
||||||
...(input.targetPath ? { targetPath: input.targetPath } : {}),
|
|
||||||
problem: input.problem ?? (input.reference.kind === "invalid" ? input.reference.message : undefined),
|
|
||||||
source: input.source,
|
|
||||||
}
|
|
||||||
const label = metadata.target === undefined ? `@${metadata.name}` : `@${metadata.name}/${metadata.target}`
|
|
||||||
return {
|
|
||||||
type: "text",
|
|
||||||
synthetic: true,
|
|
||||||
text: [
|
|
||||||
`Referenced configured reference ${label}.`,
|
|
||||||
...(metadata.kind === "local" ? ["Kind: local directory"] : []),
|
|
||||||
...(metadata.kind === "git" ? ["Kind: git repository"] : []),
|
|
||||||
...(metadata.repository ? [`Repository: ${metadata.repository}`] : []),
|
|
||||||
...(metadata.branch ? [`Branch/ref: ${metadata.branch}`] : []),
|
|
||||||
...(metadata.path ? [`Reference root: ${metadata.path}`] : []),
|
|
||||||
...(metadata.targetPath ? [`Resolved path: ${metadata.targetPath}`] : []),
|
|
||||||
...(metadata.problem
|
|
||||||
? [`Problem: ${metadata.problem}`]
|
|
||||||
: [
|
|
||||||
"For targeted context, inspect the reference path directly with Read, Glob, and Grep. For broader research, call the task tool with subagent scout and include this reference path.",
|
|
||||||
]),
|
|
||||||
].join("\n"),
|
|
||||||
metadata: { reference: metadata },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
yield* Effect.forEach(
|
yield* Effect.forEach(
|
||||||
files,
|
files,
|
||||||
Effect.fnUntraced(function* (match) {
|
Effect.fnUntraced(function* (match) {
|
||||||
@@ -1156,6 +1153,30 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
|||||||
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 (!AppFileSystem.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<MessageV2.Part>[]> = Effect.fn(
|
const resolvePart: (part: PromptInput["parts"][number]) => Effect.Effect<Draft<MessageV2.Part>[]> = Effect.fn(
|
||||||
"SessionPrompt.resolveUserPart",
|
"SessionPrompt.resolveUserPart",
|
||||||
)(function* (part) {
|
)(function* (part) {
|
||||||
@@ -1238,6 +1259,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
|||||||
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()
|
||||||
@@ -1283,6 +1305,9 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
|||||||
}
|
}
|
||||||
const args = { filePath: filepath, offset, limit }
|
const args = { filePath: filepath, offset, limit }
|
||||||
const pieces: Draft<MessageV2.Part>[] = [
|
const pieces: Draft<MessageV2.Part>[] = [
|
||||||
|
...(referenceContext
|
||||||
|
? [{ ...referenceContext, messageID: info.id, sessionID: input.sessionID }]
|
||||||
|
: []),
|
||||||
{
|
{
|
||||||
messageID: info.id,
|
messageID: info.id,
|
||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
@@ -1348,6 +1373,9 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
|||||||
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,
|
||||||
@@ -1358,6 +1386,9 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
return [
|
return [
|
||||||
|
...(referenceContext
|
||||||
|
? [{ ...referenceContext, messageID: info.id, sessionID: input.sessionID }]
|
||||||
|
: []),
|
||||||
{
|
{
|
||||||
messageID: info.id,
|
messageID: info.id,
|
||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
@@ -1377,6 +1408,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
|||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
...(referenceContext ? [{ ...referenceContext, messageID: info.id, sessionID: input.sessionID }] : []),
|
||||||
{
|
{
|
||||||
messageID: info.id,
|
messageID: info.id,
|
||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { expect } from "bun:test"
|
|||||||
import { Cause, Effect, Exit, Fiber, Layer } from "effect"
|
import { Cause, Effect, Exit, Fiber, Layer } from "effect"
|
||||||
import fs from "fs/promises"
|
import fs from "fs/promises"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { fileURLToPath } from "url"
|
import { fileURLToPath, pathToFileURL } from "url"
|
||||||
import { NamedError } from "@opencode-ai/core/util/error"
|
import { NamedError } from "@opencode-ai/core/util/error"
|
||||||
import { Agent as AgentSvc } from "../../src/agent/agent"
|
import { Agent as AgentSvc } from "../../src/agent/agent"
|
||||||
import { Bus } from "../../src/bus"
|
import { Bus } from "../../src/bus"
|
||||||
@@ -1889,6 +1889,70 @@ it.live("injects metadata for bare configured reference mentions", () =>
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
it.live("injects metadata for configured reference file attachments", () =>
|
||||||
|
provideTmpdirInstance(
|
||||||
|
(dir) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const docs = path.join(dir, "external-docs")
|
||||||
|
const readme = path.join(docs, "README.md")
|
||||||
|
yield* Effect.promise(() => fs.mkdir(docs, { recursive: true }))
|
||||||
|
yield* Effect.promise(() => Bun.write(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 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
const stored = MessageV2.get({ sessionID: session.id, messageID: message.info.id })
|
||||||
|
const synthetic = stored.parts.filter(
|
||||||
|
(part): part is MessageV2.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)
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
git: true,
|
||||||
|
config: {
|
||||||
|
...cfg,
|
||||||
|
reference: {
|
||||||
|
docs: "./external-docs",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
// Special characters in filenames
|
// Special characters in filenames
|
||||||
|
|
||||||
it.live("handles filenames with # character", () =>
|
it.live("handles filenames with # character", () =>
|
||||||
|
|||||||
@@ -41,6 +41,12 @@ How is auth handled in @packages/functions/src/api/index.ts?
|
|||||||
|
|
||||||
The content of the file is added to the conversation automatically.
|
The content of the file is added to the conversation automatically.
|
||||||
|
|
||||||
|
Configured references also appear in `@` autocomplete. Type `@alias` to add the reference root as context, or type `@alias/` to autocomplete files inside that reference.
|
||||||
|
|
||||||
|
```text "@docs/README.md"
|
||||||
|
Compare our setup with @docs/README.md
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Bash commands
|
## Bash commands
|
||||||
|
|||||||
Reference in New Issue
Block a user