1package prompt
  2
  3import (
  4	"os"
  5	"path/filepath"
  6	"strings"
  7	"sync"
  8
  9	"github.com/charmbracelet/crush/internal/config"
 10	"github.com/charmbracelet/crush/internal/csync"
 11	"github.com/charmbracelet/crush/internal/env"
 12	"github.com/charmbracelet/crush/internal/home"
 13)
 14
 15type PromptID string
 16
 17const (
 18	PromptCoder      PromptID = "coder"
 19	PromptTitle      PromptID = "title"
 20	PromptTask       PromptID = "task"
 21	PromptSummarizer PromptID = "summarizer"
 22	PromptDefault    PromptID = "default"
 23)
 24
 25func GetPrompt(promptID PromptID, provider string, contextPaths ...string) string {
 26	basePrompt := ""
 27	switch promptID {
 28	case PromptCoder:
 29		basePrompt = CoderPrompt(provider, contextPaths...)
 30	case PromptTitle:
 31		basePrompt = TitlePrompt()
 32	case PromptTask:
 33		basePrompt = TaskPrompt()
 34	case PromptSummarizer:
 35		basePrompt = SummarizerPrompt()
 36	default:
 37		basePrompt = "You are a helpful assistant"
 38	}
 39	return basePrompt
 40}
 41
 42func getContextFromPaths(workingDir string, contextPaths []string) string {
 43	return processContextPaths(workingDir, contextPaths)
 44}
 45
 46// expandPath expands ~ and environment variables in file paths
 47func expandPath(path string) string {
 48	path = home.Long(path)
 49
 50	// Handle environment variable expansion using the same pattern as config
 51	if strings.HasPrefix(path, "$") {
 52		resolver := config.NewEnvironmentVariableResolver(env.New())
 53		if expanded, err := resolver.ResolveValue(path); err == nil {
 54			path = expanded
 55		}
 56	}
 57
 58	return path
 59}
 60
 61func processContextPaths(workDir string, paths []string) string {
 62	var (
 63		wg       sync.WaitGroup
 64		resultCh = make(chan string)
 65	)
 66
 67	// Track processed files to avoid duplicates
 68	processedFiles := csync.NewMap[string, bool]()
 69
 70	for _, path := range paths {
 71		wg.Add(1)
 72		go func(p string) {
 73			defer wg.Done()
 74
 75			// Expand ~ and environment variables before processing
 76			p = expandPath(p)
 77
 78			// Use absolute path if provided, otherwise join with workDir
 79			fullPath := p
 80			if !filepath.IsAbs(p) {
 81				fullPath = filepath.Join(workDir, p)
 82			}
 83
 84			// Check if the path is a directory using os.Stat
 85			info, err := os.Stat(fullPath)
 86			if err != nil {
 87				return // Skip if path doesn't exist or can't be accessed
 88			}
 89
 90			if info.IsDir() {
 91				filepath.WalkDir(fullPath, func(path string, d os.DirEntry, err error) error {
 92					if err != nil {
 93						return err
 94					}
 95					if !d.IsDir() {
 96						// Check if we've already processed this file (case-insensitive)
 97						lowerPath := strings.ToLower(path)
 98
 99						if alreadyProcessed, _ := processedFiles.Get(lowerPath); !alreadyProcessed {
100							processedFiles.Set(lowerPath, true)
101							if result := processFile(path); result != "" {
102								resultCh <- result
103							}
104						}
105					}
106					return nil
107				})
108			} else {
109				// It's a file, process it directly
110				// Check if we've already processed this file (case-insensitive)
111				lowerPath := strings.ToLower(fullPath)
112
113				if alreadyProcessed, _ := processedFiles.Get(lowerPath); !alreadyProcessed {
114					processedFiles.Set(lowerPath, true)
115					result := processFile(fullPath)
116					if result != "" {
117						resultCh <- result
118					}
119				}
120			}
121		}(path)
122	}
123
124	go func() {
125		wg.Wait()
126		close(resultCh)
127	}()
128
129	results := make([]string, 0)
130	for result := range resultCh {
131		results = append(results, result)
132	}
133
134	return strings.Join(results, "\n")
135}
136
137func processFile(filePath string) string {
138	content, err := os.ReadFile(filePath)
139	if err != nil {
140		return ""
141	}
142	return "# From:" + filePath + "\n" + string(content)
143}