on main help sign in
allow for custom contextFiles outside of workingDir path
f8d8ce3Tai Groot
internal/llm/agent/agent.go+2−2
@@ -149,7 +149,7 @@ func NewAgent(
149 }
149 }
150 opts := []provider.ProviderClientOption{
150 opts := []provider.ProviderClientOption{
151 provider.WithModel(agentCfg.Model),
151 provider.WithModel(agentCfg.Model),
152− provider.WithSystemMessage(prompt.GetPrompt(promptID, providerCfg.ID)),
152+ provider.WithSystemMessage(prompt.GetPrompt(promptID, providerCfg.ID, config.Get().Options.ContextPaths...)),
153 }
153 }
154 agentProvider, err := provider.NewProvider(*providerCfg, opts...)
154 agentProvider, err := provider.NewProvider(*providerCfg, opts...)
155 if err != nil {
155 if err != nil {
@@ -827,7 +827,7 @@ func (a *agent) UpdateModel() error {
827
827
828 opts := []provider.ProviderClientOption{
828 opts := []provider.ProviderClientOption{
829 provider.WithModel(a.agentCfg.Model),
829 provider.WithModel(a.agentCfg.Model),
830− provider.WithSystemMessage(prompt.GetPrompt(promptID, currentProviderCfg.ID)),
830+ provider.WithSystemMessage(prompt.GetPrompt(promptID, currentProviderCfg.ID, cfg.Options.ContextPaths...)),
831 }
831 }
832
832
833 newProvider, err := provider.NewProvider(*currentProviderCfg, opts...)
833 newProvider, err := provider.NewProvider(*currentProviderCfg, opts...)
internal/llm/prompt/prompt.go+45−3
@@ -5,6 +5,9 @@ import (
5 "path/filepath"
5 "path/filepath"
6 "strings"
6 "strings"
7 "sync"
7 "sync"
8+
9+ "github.com/charmbracelet/crush/internal/config"
10+ "github.com/charmbracelet/crush/internal/env"
8)
11)
9
12
10type PromptID string
13type PromptID string
@@ -21,7 +24,7 @@ func GetPrompt(promptID PromptID, provider string, contextPaths ...string) strin
21 basePrompt := ""
24 basePrompt := ""
22 switch promptID {
25 switch promptID {
23 case PromptCoder:
26 case PromptCoder:
24− basePrompt = CoderPrompt(provider)
27+ basePrompt = CoderPrompt(provider, contextPaths...)
25 case PromptTitle:
28 case PromptTitle:
26 basePrompt = TitlePrompt()
29 basePrompt = TitlePrompt()
27 case PromptTask:
30 case PromptTask:
@@ -38,6 +41,32 @@ func getContextFromPaths(workingDir string, contextPaths []string) string {
38 return processContextPaths(workingDir, contextPaths)
41 return processContextPaths(workingDir, contextPaths)
39}
42}
40
43
44+// expandPath expands ~ and environment variables in file paths
45+func 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+
41func processContextPaths(workDir string, paths []string) string {
70func processContextPaths(workDir string, paths []string) string {
42 var (
71 var (
43 wg sync.WaitGroup
72 wg sync.WaitGroup
@@ -53,8 +82,16 @@ func processContextPaths(workDir string, paths []string) string {
53 go func(p string) {
82 go func(p string) {
54 defer wg.Done()
83 defer wg.Done()
55
84
85+ // Expand ~ and environment variables before processing
86+ p = expandPath(p)
87+
56 if strings.HasSuffix(p, "/") {
88 if strings.HasSuffix(p, "/") {
89+ // Use absolute path if provided, otherwise join with workDir
90+ dirPath := p
91+ if !filepath.IsAbs(p) {
92+ dirPath = filepath.Join(workDir, p)
93+ }
57− filepath.WalkDir(filepath.Join(workDir, p), func(path string, d os.DirEntry, err error) error {
94+ filepath.WalkDir(dirPath, func(path string, d os.DirEntry, err error) error {
58 if err != nil {
95 if err != nil {
59 return err
96 return err
60 }
97 }
@@ -78,7 +115,12 @@ func processContextPaths(workDir string, paths []string) string {
78 return nil
115 return nil
79 })
116 })
80 } else {
117 } else {
118+ // Expand ~ and environment variables before processing
119+ // Use absolute path if provided, otherwise join with workDir
120+ fullPath := p
121+ if !filepath.IsAbs(p) {
81− fullPath := filepath.Join(workDir, p)
122+ fullPath = filepath.Join(workDir, p)
123+ }
82
124
83 // Check if we've already processed this file (case-insensitive)
125 // Check if we've already processed this file (case-insensitive)
84 lowerPath := strings.ToLower(fullPath)
126 lowerPath := strings.ToLower(fullPath)
internal/llm/prompt/prompt_test.gonew file+108
@@ -0,0 +1,108 @@
1+package prompt
2+
3+import (
4+ "os"
5+ "path/filepath"
6+ "strings"
7+ "testing"
8+)
9+
10+func TestExpandPath(t *testing.T) {
11+ tests := []struct {
12+ name string
13+ input string
14+ expected func() string
15+ }{
16+ {
17+ name: "regular path unchanged",
18+ input: "/absolute/path",
19+ expected: func() string {
20+ return "/absolute/path"
21+ },
22+ },
23+ {
24+ name: "tilde expansion",
25+ input: "~/documents",
26+ expected: func() string {
27+ home, _ := os.UserHomeDir()
28+ return filepath.Join(home, "documents")
29+ },
30+ },
31+ {
32+ name: "tilde only",
33+ input: "~",
34+ expected: func() string {
35+ home, _ := os.UserHomeDir()
36+ return home
37+ },
38+ },
39+ {
40+ name: "environment variable expansion",
41+ input: "$HOME",
42+ expected: func() string {
43+ return os.Getenv("HOME")
44+ },
45+ },
46+ {
47+ name: "relative path unchanged",
48+ input: "relative/path",
49+ expected: func() string {
50+ return "relative/path"
51+ },
52+ },
53+ }
54+
55+ for _, tt := range tests {
56+ t.Run(tt.name, func(t *testing.T) {
57+ result := expandPath(tt.input)
58+ expected := tt.expected()
59+
60+ // Skip test if environment variable is not set
61+ if strings.HasPrefix(tt.input, "$") && expected == "" {
62+ t.Skip("Environment variable not set")
63+ }
64+
65+ if result != expected {
66+ t.Errorf("expandPath(%q) = %q, want %q", tt.input, result, expected)
67+ }
68+ })
69+ }
70+}
71+
72+func TestProcessContextPaths(t *testing.T) {
73+ // Create a temporary directory and file for testing
74+ tmpDir := t.TempDir()
75+ testFile := filepath.Join(tmpDir, "test.txt")
76+ testContent := "test content"
77+
78+ err := os.WriteFile(testFile, []byte(testContent), 0o644)
79+ if err != nil {
80+ t.Fatalf("Failed to create test file: %v", err)
81+ }
82+
83+ // Test with absolute path
84+ result := processContextPaths("", []string{testFile})
85+ expected := "# From:" + testFile + "\n" + testContent
86+
87+ if result != expected {
88+ t.Errorf("processContextPaths with absolute path failed.\nGot: %q\nWant: %q", result, expected)
89+ }
90+
91+ // Test with tilde expansion (if we can create a file in home directory)
92+ home, err := os.UserHomeDir()
93+ if err == nil {
94+ homeTestFile := filepath.Join(home, "crush_test_file.txt")
95+ err = os.WriteFile(homeTestFile, []byte(testContent), 0o644)
96+ if err == nil {
97+ defer os.Remove(homeTestFile) // Clean up
98+
99+ tildeFile := "~/crush_test_file.txt"
100+ result = processContextPaths("", []string{tildeFile})
101+ expected = "# From:" + homeTestFile + "\n" + testContent
102+
103+ if result != expected {
104+ t.Errorf("processContextPaths with tilde expansion failed.\nGot: %q\nWant: %q", result, expected)
105+ }
106+ }
107+ }
108+}