1package prompt
2
3import (
4 "fmt"
5 "os"
6 "path/filepath"
7 "strings"
8 "sync"
9
10 "github.com/opencode-ai/opencode/internal/config"
11 "github.com/opencode-ai/opencode/internal/llm/models"
12 "github.com/opencode-ai/opencode/internal/logging"
13)
14
15func GetAgentPrompt(agentName config.AgentName, provider models.ModelProvider) string {
16 basePrompt := ""
17 switch agentName {
18 case config.AgentCoder:
19 basePrompt = CoderPrompt(provider)
20 case config.AgentTitle:
21 basePrompt = TitlePrompt(provider)
22 case config.AgentTask:
23 basePrompt = TaskPrompt(provider)
24 default:
25 basePrompt = "You are a helpful assistant"
26 }
27
28 if agentName == config.AgentCoder || agentName == config.AgentTask {
29 // Add context from project-specific instruction files if they exist
30 contextContent := getContextFromPaths()
31 logging.Debug("Context content", "Context", contextContent)
32 if contextContent != "" {
33 return fmt.Sprintf("%s\n\n# Project-Specific Context\n Make sure to follow the instructions in the context below\n%s", basePrompt, contextContent)
34 }
35 }
36 return basePrompt
37}
38
39var (
40 onceContext sync.Once
41 contextContent string
42)
43
44func getContextFromPaths() string {
45 onceContext.Do(func() {
46 var (
47 cfg = config.Get()
48 workDir = cfg.WorkingDir
49 contextPaths = cfg.ContextPaths
50 )
51
52 contextContent = processContextPaths(workDir, contextPaths)
53 })
54
55 return contextContent
56}
57
58func processContextPaths(workDir string, paths []string) string {
59 var (
60 wg sync.WaitGroup
61 resultCh = make(chan string)
62 )
63
64 // Track processed files to avoid duplicates
65 processedFiles := make(map[string]bool)
66 var processedMutex sync.Mutex
67
68 for _, path := range paths {
69 wg.Add(1)
70 go func(p string) {
71 defer wg.Done()
72
73 if strings.HasSuffix(p, "/") {
74 filepath.WalkDir(filepath.Join(workDir, p), func(path string, d os.DirEntry, err error) error {
75 if err != nil {
76 return err
77 }
78 if !d.IsDir() {
79 // Check if we've already processed this file (case-insensitive)
80 processedMutex.Lock()
81 lowerPath := strings.ToLower(path)
82 if !processedFiles[lowerPath] {
83 processedFiles[lowerPath] = true
84 processedMutex.Unlock()
85
86 if result := processFile(path); result != "" {
87 resultCh <- result
88 }
89 } else {
90 processedMutex.Unlock()
91 }
92 }
93 return nil
94 })
95 } else {
96 fullPath := filepath.Join(workDir, p)
97
98 // Check if we've already processed this file (case-insensitive)
99 processedMutex.Lock()
100 lowerPath := strings.ToLower(fullPath)
101 if !processedFiles[lowerPath] {
102 processedFiles[lowerPath] = true
103 processedMutex.Unlock()
104
105 result := processFile(fullPath)
106 if result != "" {
107 resultCh <- result
108 }
109 } else {
110 processedMutex.Unlock()
111 }
112 }
113 }(path)
114 }
115
116 go func() {
117 wg.Wait()
118 close(resultCh)
119 }()
120
121 results := make([]string, 0)
122 for result := range resultCh {
123 results = append(results, result)
124 }
125
126 return strings.Join(results, "\n")
127}
128
129func processFile(filePath string) string {
130 content, err := os.ReadFile(filePath)
131 if err != nil {
132 return ""
133 }
134 return "# From:" + filePath + "\n" + string(content)
135}