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(cfg *config.Config, promptID PromptID, provider string, contextPaths ...string) string {
26 basePrompt := ""
27 switch promptID {
28 case PromptCoder:
29 basePrompt = CoderPrompt(cfg, provider, contextPaths...)
30 case PromptTitle:
31 basePrompt = TitlePrompt()
32 case PromptTask:
33 basePrompt = TaskPrompt(cfg)
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}