@@ -4,9 +4,11 @@ import (
44 "context"
55 "encoding/json"
66 "fmt"
7+ "io/fs"
78 "os"
89 "path/filepath"
910 "regexp"
11+ "sort"
1012 "strings"
1113 "time"
1214
@@ -76,17 +78,17 @@ func startChatSession() error {
7678
7779 var conversation []sdk.Message
7880
79- inputModel := internal .NewChatInputModel ()
81+ inputModel := internal .NewChatManagerModel ()
8082 program := tea .NewProgram (inputModel , tea .WithAltScreen ())
8183
8284 var toolsManager * internal.LLMToolsManager
8385 if cfg .Tools .Enabled {
84- toolsManager = internal .NewLLMToolsManagerWithUI (cfg , program , inputModel )
86+ toolsManager = internal .NewLLMToolsManagerWithUI (cfg , program , inputModel . GetChatInput () )
8587 }
8688
8789 welcomeHistory := []string {
8890 fmt .Sprintf ("🤖 Chat session started with %s" , selectedModel ),
89- "💡 Type '/help' or '?' for commands • Use @filename for file references " ,
91+ "💡 Type '/help' or '?' for commands • Press @ to select files to reference " ,
9092 }
9193
9294 if cfg .Tools .Enabled {
@@ -119,7 +121,7 @@ func startChatSession() error {
119121 for {
120122 updateHistory (conversation )
121123
122- userInput := waitForInput (program , inputModel )
124+ userInput := waitForInput (program , inputModel . GetChatInput () )
123125 if userInput == "" {
124126 program .Quit ()
125127 fmt .Println ("\n 👋 Chat session ended!" )
@@ -135,6 +137,13 @@ func startChatSession() error {
135137 continue
136138 }
137139
140+ // Handle interactive file reference if user typed "@"
141+ userInput , err = handleFileReference (userInput )
142+ if err != nil {
143+ fmt .Printf ("❌ Error with file selection: %v\n " , err )
144+ continue
145+ }
146+
138147 processedInput , err := processFileReferences (userInput )
139148 if err != nil {
140149 program .Send (internal.SetStatusMsg {Message : fmt .Sprintf ("❌ Error processing file references: %v" , err ), Spinner : false })
@@ -164,7 +173,7 @@ func startChatSession() error {
164173 break
165174 }
166175
167- _ , assistantToolCalls , metrics , err := sendStreamingChatCompletionToUI (cfg , selectedModel , conversation , program , & conversation , inputModel )
176+ _ , assistantToolCalls , metrics , err := sendStreamingChatCompletionToUI (cfg , selectedModel , conversation , program , & conversation , inputModel . GetChatInput () )
168177
169178 if err != nil {
170179 if strings .Contains (err .Error (), "cancelled by user" ) {
@@ -739,6 +748,174 @@ func handleStreamErrorToUI(event sdk.SSEvent, result *uiStreamingResult) error {
739748 return fmt .Errorf ("stream error: %s" , errResp .Error )
740749}
741750
751+ // scanProjectFiles recursively scans the current directory for files,
752+ // excluding common directories that should not be included
753+ func scanProjectFiles () ([]string , error ) {
754+ var files []string
755+
756+ // Directories to exclude from scanning
757+ excludeDirs := map [string ]bool {
758+ ".git" : true ,
759+ ".github" : true ,
760+ "node_modules" : true ,
761+ ".infer" : true ,
762+ "vendor" : true ,
763+ ".flox" : true ,
764+ "dist" : true ,
765+ "build" : true ,
766+ "bin" : true ,
767+ ".vscode" : true ,
768+ ".idea" : true ,
769+ }
770+
771+ // File extensions to exclude
772+ excludeExts := map [string ]bool {
773+ ".exe" : true ,
774+ ".bin" : true ,
775+ ".dll" : true ,
776+ ".so" : true ,
777+ ".dylib" : true ,
778+ ".a" : true ,
779+ ".o" : true ,
780+ ".obj" : true ,
781+ ".pyc" : true ,
782+ ".class" : true ,
783+ ".jar" : true ,
784+ ".war" : true ,
785+ ".zip" : true ,
786+ ".tar" : true ,
787+ ".gz" : true ,
788+ ".rar" : true ,
789+ ".7z" : true ,
790+ ".png" : true ,
791+ ".jpg" : true ,
792+ ".jpeg" : true ,
793+ ".gif" : true ,
794+ ".bmp" : true ,
795+ ".ico" : true ,
796+ ".svg" : true ,
797+ ".pdf" : true ,
798+ ".mov" : true ,
799+ ".mp4" : true ,
800+ ".avi" : true ,
801+ ".mp3" : true ,
802+ ".wav" : true ,
803+ }
804+
805+ cwd , err := os .Getwd ()
806+ if err != nil {
807+ return nil , fmt .Errorf ("failed to get current directory: %w" , err )
808+ }
809+
810+ err = filepath .WalkDir (cwd , func (path string , d fs.DirEntry , err error ) error {
811+ if err != nil {
812+ return nil // Skip files with errors
813+ }
814+
815+ // Get relative path from current directory
816+ relPath , err := filepath .Rel (cwd , path )
817+ if err != nil {
818+ return nil // Skip if we can't get relative path
819+ }
820+
821+ // Skip directories that should be excluded
822+ if d .IsDir () {
823+ if excludeDirs [d .Name ()] || strings .HasPrefix (d .Name (), "." ) && d .Name () != "." {
824+ return filepath .SkipDir
825+ }
826+ return nil
827+ }
828+
829+ // Skip files with excluded extensions
830+ ext := strings .ToLower (filepath .Ext (relPath ))
831+ if excludeExts [ext ] {
832+ return nil
833+ }
834+
835+ // Skip very large files (over 1MB)
836+ if info , err := d .Info (); err == nil && info .Size () > 1024 * 1024 {
837+ return nil
838+ }
839+
840+ // Only include regular files
841+ if d .Type ().IsRegular () {
842+ files = append (files , relPath )
843+ }
844+
845+ return nil
846+ })
847+
848+ if err != nil {
849+ return nil , fmt .Errorf ("failed to scan directory: %w" , err )
850+ }
851+
852+ // Sort files for consistent ordering
853+ sort .Strings (files )
854+
855+ return files , nil
856+ }
857+
858+ // selectFileInteractively shows a dropdown to select a file from the project
859+ func selectFileInteractively () (string , error ) {
860+ files , err := scanProjectFiles ()
861+ if err != nil {
862+ return "" , fmt .Errorf ("failed to scan project files: %w" , err )
863+ }
864+
865+ if len (files ) == 0 {
866+ return "" , fmt .Errorf ("no files found in the current directory" )
867+ }
868+
869+ // Add a limit to prevent overwhelming dropdown
870+ maxFiles := 200
871+ if len (files ) > maxFiles {
872+ files = files [:maxFiles ]
873+ fmt .Printf ("⚠️ Showing first %d files (found %d total)\n " , maxFiles , len (files ))
874+ }
875+
876+ fileSelector := internal .NewFileSelectorModel (files )
877+ program := tea .NewProgram (fileSelector )
878+
879+ _ , err = program .Run ()
880+ if err != nil {
881+ return "" , fmt .Errorf ("file selection failed: %w" , err )
882+ }
883+
884+ if fileSelector .IsCancelled () {
885+ return "" , fmt .Errorf ("file selection cancelled" )
886+ }
887+
888+ if ! fileSelector .IsSelected () {
889+ return "" , fmt .Errorf ("no file was selected" )
890+ }
891+
892+ return fileSelector .GetSelected (), nil
893+ }
894+
895+ // handleFileReference processes "@" references in user input
896+ func handleFileReference (input string ) (string , error ) {
897+ if strings .TrimSpace (input ) == "@" {
898+ selectedFile , err := selectFileInteractively ()
899+ if err != nil {
900+ return "" , err
901+ }
902+ return "@" + selectedFile , nil
903+ }
904+
905+ if strings .HasSuffix (strings .TrimSpace (input ), "@" ) {
906+ selectedFile , err := selectFileInteractively ()
907+ if err != nil {
908+ return "" , err
909+ }
910+
911+ trimmed := strings .TrimSpace (input )
912+ prefix := trimmed [:len (trimmed )- 1 ]
913+ return prefix + "@" + selectedFile , nil
914+ }
915+
916+ return input , nil
917+ }
918+
742919func compactConversation (conversation []sdk.Message , selectedModel string ) error {
743920 cfg , err := config .LoadConfig ("" )
744921 if err != nil {
0 commit comments