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