11package editor
22
33import (
4+ "fmt"
5+ "log/slog"
6+ "os"
47 "regexp"
58 "strings"
69
@@ -24,7 +27,8 @@ var ansiRegexp = regexp.MustCompile(`\x1b\[[0-9;]*[A-Za-z]`)
2427
2528// SendMsg represents a message to send
2629type SendMsg struct {
27- Content string
30+ Content string // Full content sent to the agent (with file contents expanded)
31+ DisplayContent string // Compact version for UI display (with @filename placeholders)
2832}
2933
3034// Editor represents an input editor component
@@ -57,6 +61,11 @@ type editor struct {
5761 userTyped bool
5862 // keyboardEnhancementsSupported tracks whether the terminal supports keyboard enhancements
5963 keyboardEnhancementsSupported bool
64+ // fileRefs tracks @filename placeholders inserted via completion (handles spaces in filenames).
65+ fileRefs []string
66+ // pendingFileRef tracks the current @word being typed (for manual file ref detection).
67+ // Only set when cursor is in a word starting with @, cleared when cursor leaves.
68+ pendingFileRef string
6069}
6170
6271// New creates a new editor component
@@ -284,6 +293,13 @@ func (e *editor) Update(msg tea.Msg) (layout.Model, tea.Cmd) {
284293 e .textarea .SetValue (newValue )
285294 e .textarea .MoveToEnd ()
286295 }
296+ // Track file references when using @ completion, so we can distinguish from
297+ // normal user input that may contain @smth as literal text to send (not a file reference)
298+ if e .currentCompletion != nil && e .currentCompletion .Trigger () == "@" {
299+ e .fileRefs = append (e .fileRefs , msg .Value )
300+ }
301+ // Clear history suggestion after selecting a completion
302+ e .clearSuggestion ()
287303 return e , nil
288304 }
289305 return e , nil
@@ -316,22 +332,30 @@ func (e *editor) Update(msg tea.Msg) (layout.Model, tea.Cmd) {
316332 // If plain enter and textarea inserted a newline, submit the previous value
317333 if value != prev && msg .String () == "enter" {
318334 if prev != "" && ! e .working {
335+ displayContent := prev
336+ e .tryAddFileRef (e .pendingFileRef ) // Add any pending @filepath before send
337+ e .pendingFileRef = ""
338+ sendContent := e .appendFileAttachments (prev )
319339 e .textarea .SetValue (prev )
320340 e .textarea .MoveToEnd ()
321341 e .textarea .Reset ()
322342 e .userTyped = false
323343 e .refreshSuggestion ()
324- return e , core .CmdHandler (SendMsg {Content : prev })
344+ return e , core .CmdHandler (SendMsg {Content : sendContent , DisplayContent : displayContent })
325345 }
326346 return e , nil
327347 }
328348
329349 // Normal enter submit: send current value
330350 if value != "" && ! e .working {
351+ displayContent := value
352+ e .tryAddFileRef (e .pendingFileRef ) // Add any pending @filepath before send
353+ e .pendingFileRef = ""
354+ sendContent := e .appendFileAttachments (value )
331355 e .textarea .Reset ()
332356 e .userTyped = false
333357 e .refreshSuggestion ()
334- return e , core .CmdHandler (SendMsg {Content : value })
358+ return e , core .CmdHandler (SendMsg {Content : sendContent , DisplayContent : displayContent })
335359 }
336360
337361 return e , nil
@@ -383,13 +407,29 @@ func (e *editor) Update(msg tea.Msg) (layout.Model, tea.Cmd) {
383407 if e .textarea .Value () == "" {
384408 e .userTyped = false
385409 }
410+
411+ currentWord := e .textarea .Word ()
412+
413+ // Track manual @filepath refs - only runs when we're in/leaving an @ word
414+ if e .pendingFileRef != "" && currentWord != e .pendingFileRef {
415+ // Left the @ word - try to add it as file ref
416+ e .tryAddFileRef (e .pendingFileRef )
417+ e .pendingFileRef = ""
418+ }
419+ if e .pendingFileRef == "" && strings .HasPrefix (currentWord , "@" ) && len (currentWord ) > 1 {
420+ // Entered an @ word - start tracking
421+ e .pendingFileRef = currentWord
422+ } else if e .pendingFileRef != "" && strings .HasPrefix (currentWord , "@" ) {
423+ // Still in @ word but it changed (user typing more) - update tracking
424+ e .pendingFileRef = currentWord
425+ }
426+
386427 if keyMsg .String () == "space" {
387428 e .completionWord = ""
388429 e .currentCompletion = nil
389430 cmds = append (cmds , core .CmdHandler (completion.CloseMsg {}))
390431 }
391432
392- currentWord := e .textarea .Word ()
393433 if e .currentCompletion != nil && strings .HasPrefix (currentWord , e .currentCompletion .Trigger ()) {
394434 e .completionWord = currentWord [1 :]
395435 cmds = append (cmds , core .CmdHandler (completion.QueryMsg {Query : e .completionWord }))
@@ -457,3 +497,69 @@ func (e *editor) SetWorking(working bool) tea.Cmd {
457497 e .working = working
458498 return nil
459499}
500+
501+ // tryAddFileRef checks if word is a valid @filepath and adds it to fileRefs.
502+ // Called when cursor leaves a word to detect manually-typed file references.
503+ func (e * editor ) tryAddFileRef (word string ) {
504+ // Must start with @ and look like a path (contains / or .)
505+ if ! strings .HasPrefix (word , "@" ) || len (word ) < 2 {
506+ return
507+ }
508+
509+ path := word [1 :] // strip @
510+ if ! strings .ContainsAny (path , "/." ) {
511+ return // not a path-like reference (e.g., @username)
512+ }
513+
514+ // Check if it's an existing file (not directory)
515+ info , err := os .Stat (path )
516+ if err != nil || info .IsDir () {
517+ return
518+ }
519+
520+ // Avoid duplicates
521+ for _ , existing := range e .fileRefs {
522+ if existing == word {
523+ return
524+ }
525+ }
526+
527+ e .fileRefs = append (e .fileRefs , word )
528+ }
529+
530+ // appendFileAttachments appends file contents as a structured attachments section.
531+ // Returns the original content unchanged if no valid file references exist.
532+ func (e * editor ) appendFileAttachments (content string ) string {
533+ if len (e .fileRefs ) == 0 {
534+ return content
535+ }
536+
537+ var attachments strings.Builder
538+ for _ , ref := range e .fileRefs {
539+ if ! strings .Contains (content , ref ) {
540+ continue
541+ }
542+
543+ filename := strings .TrimPrefix (ref , "@" )
544+ info , err := os .Stat (filename )
545+ if err != nil || info .IsDir () {
546+ continue
547+ }
548+
549+ data , err := os .ReadFile (filename )
550+ if err != nil {
551+ slog .Warn ("failed to read file attachment" , "path" , filename , "error" , err )
552+ continue
553+ }
554+
555+ attachments .WriteString (fmt .Sprintf ("\n %s:\n ```\n %s\n ```\n " , ref , string (data )))
556+ }
557+
558+ e .fileRefs = nil
559+
560+ if attachments .Len () == 0 {
561+ return content
562+ }
563+
564+ return content + "\n \n <attachments>" + attachments .String () + "</attachments>"
565+ }
0 commit comments