@@ -56,7 +56,8 @@ import { EffectBridge } from "@/effect/bridge"
5656import { SyncEvent } from "@/sync"
5757import { SessionEvent } from "@/v2/session-event"
5858import { Modelv2 } from "@/v2/model"
59- import { AgentAttachment , FileAttachment , Source } from "@/v2/session-prompt"
59+ import { AgentAttachment , FileAttachment , ReferenceAttachment , Source } from "@/v2/session-prompt"
60+ import { Reference } from "@/reference/reference"
6061import * as DateTime from "effect/DateTime"
6162import { eq } from "@/storage/db"
6263import * as Database from "@/storage/db"
@@ -81,6 +82,45 @@ const STRUCTURED_OUTPUT_SYSTEM_PROMPT = `IMPORTANT: The user has requested struc
8182const log = Log . create ( { service : "session.prompt" } )
8283const elog = EffectLogger . create ( { service : "session.prompt" } )
8384
85+ type ReferencePromptMetadata = {
86+ name : string
87+ kind : "local" | "git" | "invalid"
88+ path ?: string
89+ repository ?: string
90+ branch ?: string
91+ target ?: string
92+ targetPath ?: string
93+ problem ?: string
94+ source : { value : string ; start : number ; end : number }
95+ }
96+
97+ function stringField ( record : Record < string , unknown > , key : string ) {
98+ return typeof record [ key ] === "string" ? record [ key ] : undefined
99+ }
100+
101+ function referencePromptMetadata ( input : unknown ) : ReferencePromptMetadata | undefined {
102+ if ( ! input || typeof input !== "object" || Array . isArray ( input ) ) return
103+ const record = input as Record < string , unknown >
104+ const name = stringField ( record , "name" )
105+ const kind = stringField ( record , "kind" )
106+ if ( ! name || ( kind !== "local" && kind !== "git" && kind !== "invalid" ) ) return
107+ if ( ! record . source || typeof record . source !== "object" || Array . isArray ( record . source ) ) return
108+ const source = record . source as Record < string , unknown >
109+ const value = stringField ( source , "value" )
110+ if ( ! value || typeof source . start !== "number" || typeof source . end !== "number" ) return
111+ return {
112+ name,
113+ kind,
114+ path : stringField ( record , "path" ) ,
115+ repository : stringField ( record , "repository" ) ,
116+ branch : stringField ( record , "branch" ) ,
117+ target : stringField ( record , "target" ) ,
118+ targetPath : stringField ( record , "targetPath" ) ,
119+ problem : stringField ( record , "problem" ) ,
120+ source : { value, start : source . start , end : source . end } ,
121+ }
122+ }
123+
84124export interface Interface {
85125 readonly cancel : ( sessionID : SessionID ) => Effect . Effect < void >
86126 readonly prompt : ( input : PromptInput ) => Effect . Effect < MessageV2 . WithParts >
@@ -119,6 +159,7 @@ export const layer = Layer.effect(
119159 const summary = yield * SessionSummary . Service
120160 const sys = yield * SystemPrompt . Service
121161 const llm = yield * LLM . Service
162+ const references = yield * Reference . Service
122163 const sync = yield * SyncEvent . Service
123164 const runner = Effect . fn ( "SessionPrompt.runner" ) ( function * ( ) {
124165 return yield * EffectBridge . make ( )
@@ -141,12 +182,116 @@ export const layer = Layer.effect(
141182 const parts : Types . DeepMutable < PromptInput [ "parts" ] > = [ { type : "text" , text : template } ]
142183 const files = ConfigMarkdown . files ( template )
143184 const seen = new Set < string > ( )
185+ const mentionSource = ( match : RegExpMatchArray ) => {
186+ const start = match . index ?? 0
187+ return { value : match [ 0 ] , start, end : start + match [ 0 ] . length }
188+ }
189+ const referenceTextPart = ( input : {
190+ reference : Reference . Resolved
191+ source : ReturnType < typeof mentionSource >
192+ target ?: string
193+ targetPath ?: string
194+ problem ?: string
195+ } ) : MessageV2 . TextPartInput => {
196+ const metadata : ReferencePromptMetadata = {
197+ name : input . reference . name ,
198+ kind : input . reference . kind ,
199+ ...( input . reference . kind === "invalid"
200+ ? { repository : input . reference . repository }
201+ : { path : input . reference . path } ) ,
202+ ...( input . reference . kind === "git"
203+ ? { repository : input . reference . repository , branch : input . reference . branch }
204+ : { } ) ,
205+ ...( input . target === undefined ? { } : { target : input . target } ) ,
206+ ...( input . targetPath ? { targetPath : input . targetPath } : { } ) ,
207+ problem : input . problem ?? ( input . reference . kind === "invalid" ? input . reference . message : undefined ) ,
208+ source : input . source ,
209+ }
210+ const label = metadata . target === undefined ? `@${ metadata . name } ` : `@${ metadata . name } /${ metadata . target } `
211+ return {
212+ type : "text" ,
213+ synthetic : true ,
214+ text : [
215+ `Referenced configured reference ${ label } .` ,
216+ ...( metadata . kind === "local" ? [ "Kind: local directory" ] : [ ] ) ,
217+ ...( metadata . kind === "git" ? [ "Kind: git repository" ] : [ ] ) ,
218+ ...( metadata . repository ? [ `Repository: ${ metadata . repository } ` ] : [ ] ) ,
219+ ...( metadata . branch ? [ `Branch/ref: ${ metadata . branch } ` ] : [ ] ) ,
220+ ...( metadata . path ? [ `Reference root: ${ metadata . path } ` ] : [ ] ) ,
221+ ...( metadata . targetPath ? [ `Resolved path: ${ metadata . targetPath } ` ] : [ ] ) ,
222+ ...( metadata . problem
223+ ? [ `Problem: ${ metadata . problem } ` ]
224+ : [
225+ "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." ,
226+ ] ) ,
227+ ] . join ( "\n" ) ,
228+ metadata : { reference : metadata } ,
229+ }
230+ }
144231 yield * Effect . forEach (
145232 files ,
146233 Effect . fnUntraced ( function * ( match ) {
147234 const name = match [ 1 ]
235+ if ( ! name ) return
148236 if ( seen . has ( name ) ) return
149237 seen . add ( name )
238+
239+ const slash = name . indexOf ( "/" )
240+ const alias = slash === - 1 ? name : name . slice ( 0 , slash )
241+ const reference = yield * references . get ( alias )
242+ if ( reference ) {
243+ const source = mentionSource ( match )
244+ if ( reference . kind === "invalid" ) {
245+ parts . push (
246+ referenceTextPart ( { reference, source, target : slash === - 1 ? undefined : name . slice ( slash + 1 ) } ) ,
247+ )
248+ return
249+ }
250+
251+ yield * references . ensure ( reference . path )
252+ if ( slash === - 1 ) {
253+ parts . push ( referenceTextPart ( { reference, source } ) )
254+ return
255+ }
256+
257+ const target = name . slice ( slash + 1 )
258+ const targetPath = path . resolve ( reference . path , target )
259+ if ( ! AppFileSystem . contains ( reference . path , targetPath ) ) {
260+ parts . push (
261+ referenceTextPart ( {
262+ reference,
263+ source,
264+ target,
265+ targetPath,
266+ problem : `Path escapes configured reference @${ alias } : ${ target } ` ,
267+ } ) ,
268+ )
269+ return
270+ }
271+
272+ const info = yield * fsys . stat ( targetPath ) . pipe ( Effect . option )
273+ if ( Option . isNone ( info ) ) {
274+ parts . push (
275+ referenceTextPart ( {
276+ reference,
277+ source,
278+ target,
279+ targetPath,
280+ problem : `Path does not exist inside configured reference @${ alias } : ${ target } ` ,
281+ } ) ,
282+ )
283+ return
284+ }
285+
286+ parts . push ( {
287+ type : "file" ,
288+ url : pathToFileURL ( targetPath ) . href ,
289+ filename : name ,
290+ mime : info . value . type === "Directory" ? "application/x-directory" : "text/plain" ,
291+ } )
292+ return
293+ }
294+
150295 const filepath = name . startsWith ( "~/" )
151296 ? path . join ( os . homedir ( ) , name . slice ( 2 ) )
152297 : path . resolve ( ctx . worktree , name )
@@ -1326,6 +1471,26 @@ NOTE: At any point in time through this workflow you should feel free to ask the
13261471 if ( part . type === "text" ) {
13271472 if ( part . synthetic ) result . synthetic . push ( part . text )
13281473 else result . text . push ( part . text )
1474+ const reference = referencePromptMetadata ( part . metadata ?. reference )
1475+ if ( reference ) {
1476+ result . references . push (
1477+ new ReferenceAttachment ( {
1478+ name : reference . name ,
1479+ kind : reference . kind ,
1480+ uri : reference . path ? pathToFileURL ( reference . path ) . href : undefined ,
1481+ repository : reference . repository ,
1482+ branch : reference . branch ,
1483+ target : reference . target ,
1484+ targetUri : reference . targetPath ? pathToFileURL ( reference . targetPath ) . href : undefined ,
1485+ problem : reference . problem ,
1486+ source : new Source ( {
1487+ start : reference . source . start ,
1488+ end : reference . source . end ,
1489+ text : reference . source . value ,
1490+ } ) ,
1491+ } ) ,
1492+ )
1493+ }
13291494 }
13301495 if ( part . type === "file" ) {
13311496 result . files . push (
@@ -1363,6 +1528,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
13631528 text : [ ] as string [ ] ,
13641529 files : [ ] as FileAttachment [ ] ,
13651530 agents : [ ] as AgentAttachment [ ] ,
1531+ references : [ ] as ReferenceAttachment [ ] ,
13661532 synthetic : [ ] as string [ ] ,
13671533 } ,
13681534 )
@@ -1375,6 +1541,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
13751541 text : nextPrompt . text . join ( "\n" ) ,
13761542 files : nextPrompt . files ,
13771543 agents : nextPrompt . agents ,
1544+ references : nextPrompt . references ,
13781545 } ,
13791546 } )
13801547 }
@@ -1817,6 +1984,7 @@ export const defaultLayer = Layer.suspend(() =>
18171984 Agent . defaultLayer ,
18181985 SystemPrompt . defaultLayer ,
18191986 LLM . defaultLayer ,
1987+ Reference . defaultLayer ,
18201988 Bus . layer ,
18211989 CrossSpawnSpawner . defaultLayer ,
18221990 SyncEvent . defaultLayer ,
0 commit comments