on main help sign in
fix: windows basic issues
9d75e31Raphael Amorim
internal/config/config.go+8
@@ -203,9 +203,17 @@ func Load(workingDir string, debug bool) (*Config, error) {
203func configureViper() {
203func configureViper() {
204 viper.SetConfigName(fmt.Sprintf(".%s", appName))
204 viper.SetConfigName(fmt.Sprintf(".%s", appName))
205 viper.SetConfigType("json")
205 viper.SetConfigType("json")
206+
207+ // Unix-style paths
206 viper.AddConfigPath("$HOME")
208 viper.AddConfigPath("$HOME")
207 viper.AddConfigPath(fmt.Sprintf("$XDG_CONFIG_HOME/%s", appName))
209 viper.AddConfigPath(fmt.Sprintf("$XDG_CONFIG_HOME/%s", appName))
208 viper.AddConfigPath(fmt.Sprintf("$HOME/.config/%s", appName))
210 viper.AddConfigPath(fmt.Sprintf("$HOME/.config/%s", appName))
211+
212+ // Windows-style paths
213+ viper.AddConfigPath(fmt.Sprintf("$USERPROFILE"))
214+ viper.AddConfigPath(fmt.Sprintf("$APPDATA/%s", appName))
215+ viper.AddConfigPath(fmt.Sprintf("$LOCALAPPDATA/%s", appName))
216+
209 viper.SetEnvPrefix(strings.ToUpper(appName))
217 viper.SetEnvPrefix(strings.ToUpper(appName))
210 viper.AutomaticEnv()
218 viper.AutomaticEnv()
211}
219}
internal/fsext/fileutil.go+1−1
@@ -61,7 +61,7 @@ func GetRgSearchCmd(pattern, path, include string) *exec.Cmd {
61 }
61 }
62 args = append(args, path)
62 args = append(args, path)
63
63
64− return exec.Command("rg", args...)
64+ return exec.Command(rgPath, args...)
65}
65}
66
66
67type FileInfo struct {
67type FileInfo struct {
internal/llm/tools/bash.go+8
@@ -59,6 +59,14 @@ func bashDescription() string {
59 bannedCommandsStr := strings.Join(bannedCommands, ", ")
59 bannedCommandsStr := strings.Join(bannedCommands, ", ")
60 return fmt.Sprintf(`Executes a given bash command in a persistent shell session with optional timeout, ensuring proper handling and security measures.
60 return fmt.Sprintf(`Executes a given bash command in a persistent shell session with optional timeout, ensuring proper handling and security measures.
61
61
62+IMPORTANT FOR WINDOWS USERS:
63+- This tool uses a POSIX shell emulator (mvdan.cc/sh/v3) that works cross-platform, including Windows
64+- On Windows, this provides bash-like functionality without requiring WSL or Git Bash
65+- Use forward slashes (/) in paths - they work on all platforms and are converted automatically
66+- Windows-specific commands (like 'dir', 'type', 'copy') are not available - use Unix equivalents ('ls', 'cat', 'cp')
67+- Environment variables use Unix syntax: $VAR instead of %%VAR%%
68+- File paths are automatically converted between Windows and Unix formats as needed
69+
62Before executing the command, please follow these steps:
70Before executing the command, please follow these steps:
63
71
641. Directory Verification:
721. Directory Verification:
internal/llm/tools/edit.go+5
@@ -90,6 +90,11 @@ When making edits:
90 - Do not leave the code in a broken state
90 - Do not leave the code in a broken state
91 - Always use absolute file paths (starting with /)
91 - Always use absolute file paths (starting with /)
92
92
93+WINDOWS NOTES:
94+- File paths should use forward slashes (/) for cross-platform compatibility
95+- On Windows, absolute paths start with drive letters (C:/) but forward slashes work throughout
96+- File permissions are handled automatically by the Go runtime
97+
93Remember: when making multiple file edits in a row to the same file, you should prefer to send all edits in a single message with multiple calls to this tool, rather than multiple messages with a single call each.`
98Remember: when making multiple file edits in a row to the same file, you should prefer to send all edits in a single message with multiple calls to this tool, rather than multiple messages with a single call each.`
94)
99)
95
100
internal/llm/tools/glob.go+6
@@ -47,6 +47,12 @@ LIMITATIONS:
47- Does not search file contents (use Grep tool for that)
47- Does not search file contents (use Grep tool for that)
48- Hidden files (starting with '.') are skipped
48- Hidden files (starting with '.') are skipped
49
49
50+WINDOWS NOTES:
51+- Uses ripgrep (rg) command if available, otherwise falls back to built-in Go implementation
52+- On Windows, install ripgrep via: winget install BurntSushi.ripgrep.MSVC
53+- Path separators are handled automatically (both / and \ work)
54+- Patterns should use forward slashes (/) for cross-platform compatibility
55+
50TIPS:
56TIPS:
51- For the most useful results, combine with the Grep tool: first find files with Glob, then search their contents with Grep
57- For the most useful results, combine with the Grep tool: first find files with Glob, then search their contents with Grep
52- When doing iterative exploration that may require multiple rounds of searching, consider using the Agent tool instead
58- When doing iterative exploration that may require multiple rounds of searching, consider using the Agent tool instead
internal/llm/tools/grep.go+6
@@ -124,6 +124,12 @@ LIMITATIONS:
124- Very large binary files may be skipped
124- Very large binary files may be skipped
125- Hidden files (starting with '.') are skipped
125- Hidden files (starting with '.') are skipped
126
126
127+WINDOWS NOTES:
128+- Uses ripgrep (rg) command if available for better performance
129+- On Windows, install ripgrep via: winget install BurntSushi.ripgrep.MSVC
130+- Falls back to built-in Go implementation if ripgrep is not available
131+- File paths are normalized automatically for Windows compatibility
132+
127TIPS:
133TIPS:
128- For faster, more targeted searches, first use Glob to find relevant files, then use Grep
134- For faster, more targeted searches, first use Glob to find relevant files, then use Grep
129- When doing iterative exploration that may require multiple rounds of searching, consider using the Agent tool instead
135- When doing iterative exploration that may require multiple rounds of searching, consider using the Agent tool instead
internal/llm/tools/ls.go+6
@@ -58,6 +58,12 @@ LIMITATIONS:
58- Does not show file sizes or permissions
58- Does not show file sizes or permissions
59- Cannot recursively list all directories in a large project
59- Cannot recursively list all directories in a large project
60
60
61+WINDOWS NOTES:
62+- Hidden file detection uses Unix convention (files starting with '.')
63+- Windows-specific hidden files (with hidden attribute) are not automatically skipped
64+- Common Windows directories like System32, Program Files are not in default ignore list
65+- Path separators are handled automatically (both / and \ work)
66+
61TIPS:
67TIPS:
62- Use Glob tool for finding files by name patterns instead of browsing
68- Use Glob tool for finding files by name patterns instead of browsing
63- Use Grep tool for searching file contents
69- Use Grep tool for searching file contents
internal/llm/tools/shell/shell.go+11
@@ -1,3 +1,14 @@
1+// Package shell provides cross-platform shell execution capabilities.
2+//
3+// WINDOWS COMPATIBILITY NOTE:
4+// This implementation uses mvdan.cc/sh/v3 which provides POSIX shell emulation
5+// on Windows. While this works for basic commands, it has limitations:
6+// - Windows-specific commands (dir, type, copy) are not available
7+// - PowerShell and cmd.exe specific features are not supported
8+// - Some Windows path handling may be inconsistent
9+//
10+// For full Windows compatibility, consider adding native Windows shell support
11+// using os/exec with cmd.exe or PowerShell for Windows-specific commands.
1package shell
12package shell
2
13
3import (
14import (
internal/llm/tools/view.go+5
@@ -60,6 +60,11 @@ LIMITATIONS:
60- Cannot display binary files or images
60- Cannot display binary files or images
61- Images can be identified but not displayed
61- Images can be identified but not displayed
62
62
63+WINDOWS NOTES:
64+- Handles both Windows (CRLF) and Unix (LF) line endings automatically
65+- File paths work with both forward slashes (/) and backslashes (\)
66+- Text encoding is detected automatically for most common formats
67+
63TIPS:
68TIPS:
64- Use with Glob tool to first find files you want to view
69- Use with Glob tool to first find files you want to view
65- For code exploration, first use Grep to find relevant files, then View to examine them
70- For code exploration, first use Grep to find relevant files, then View to examine them
internal/llm/tools/write.go+4
@@ -64,6 +64,10 @@ LIMITATIONS:
64- You should read a file before writing to it to avoid conflicts
64- You should read a file before writing to it to avoid conflicts
65- Cannot append to files (rewrites the entire file)
65- Cannot append to files (rewrites the entire file)
66
66
67+WINDOWS NOTES:
68+- File permissions (0o755, 0o644) are Unix-style but work on Windows with appropriate translations
69+- Use forward slashes (/) in paths for cross-platform compatibility
70+- Windows file attributes and permissions are handled automatically by the Go runtime
67
71
68TIPS:
72TIPS:
69- Use the View tool first to examine existing files before modifying them
73- Use the View tool first to examine existing files before modifying them
internal/lsp/client.go+10−11
@@ -131,7 +131,7 @@ func (c *Client) InitializeLSPClient(ctx context.Context, workspaceDir string) (
131 WorkspaceFoldersInitializeParams: protocol.WorkspaceFoldersInitializeParams{
131 WorkspaceFoldersInitializeParams: protocol.WorkspaceFoldersInitializeParams{
132 WorkspaceFolders: []protocol.WorkspaceFolder{
132 WorkspaceFolders: []protocol.WorkspaceFolder{
133 {
133 {
134− URI: protocol.URI("file://" + workspaceDir),
134+ URI: protocol.URI(protocol.URIFromPath(workspaceDir)),
135 Name: workspaceDir,
135 Name: workspaceDir,
136 },
136 },
137 },
137 },
@@ -144,7 +144,7 @@ func (c *Client) InitializeLSPClient(ctx context.Context, workspaceDir string) (
144 Version: "0.1.0",
144 Version: "0.1.0",
145 },
145 },
146 RootPath: workspaceDir,
146 RootPath: workspaceDir,
147− RootURI: protocol.DocumentUri("file://" + workspaceDir),
147+ RootURI: protocol.URIFromPath(workspaceDir),
148 Capabilities: protocol.ClientCapabilities{
148 Capabilities: protocol.ClientCapabilities{
149 Workspace: protocol.WorkspaceClientCapabilities{
149 Workspace: protocol.WorkspaceClientCapabilities{
150 Configuration: true,
150 Configuration: true,
@@ -448,7 +448,7 @@ func (c *Client) pingTypeScriptServer(ctx context.Context) error {
448
448
449 // If we have any open files, try to get document symbols for one
449 // If we have any open files, try to get document symbols for one
450 for uri := range c.openFiles {
450 for uri := range c.openFiles {
451− filePath := strings.TrimPrefix(uri, "file://")
451+ filePath := protocol.DocumentUri(uri).Path()
452 if strings.HasSuffix(filePath, ".ts") || strings.HasSuffix(filePath, ".js") ||
452 if strings.HasSuffix(filePath, ".ts") || strings.HasSuffix(filePath, ".js") ||
453 strings.HasSuffix(filePath, ".tsx") || strings.HasSuffix(filePath, ".jsx") {
453 strings.HasSuffix(filePath, ".tsx") || strings.HasSuffix(filePath, ".jsx") {
454 var symbols []protocol.DocumentSymbol
454 var symbols []protocol.DocumentSymbol
@@ -586,7 +586,7 @@ type OpenFileInfo struct {
586}
586}
587
587
588func (c *Client) OpenFile(ctx context.Context, filepath string) error {
588func (c *Client) OpenFile(ctx context.Context, filepath string) error {
589− uri := fmt.Sprintf("file://%s", filepath)
589+ uri := string(protocol.URIFromPath(filepath))
590
590
591 c.openFilesMu.Lock()
591 c.openFilesMu.Lock()
592 if _, exists := c.openFiles[uri]; exists {
592 if _, exists := c.openFiles[uri]; exists {
@@ -625,7 +625,7 @@ func (c *Client) OpenFile(ctx context.Context, filepath string) error {
625}
625}
626
626
627func (c *Client) NotifyChange(ctx context.Context, filepath string) error {
627func (c *Client) NotifyChange(ctx context.Context, filepath string) error {
628− uri := fmt.Sprintf("file://%s", filepath)
628+ uri := string(protocol.URIFromPath(filepath))
629
629
630 content, err := os.ReadFile(filepath)
630 content, err := os.ReadFile(filepath)
631 if err != nil {
631 if err != nil {
@@ -665,7 +665,7 @@ func (c *Client) NotifyChange(ctx context.Context, filepath string) error {
665
665
666func (c *Client) CloseFile(ctx context.Context, filepath string) error {
666func (c *Client) CloseFile(ctx context.Context, filepath string) error {
667 cnf := config.Get()
667 cnf := config.Get()
668− uri := fmt.Sprintf("file://%s", filepath)
668+ uri := string(protocol.URIFromPath(filepath))
669
669
670 c.openFilesMu.Lock()
670 c.openFilesMu.Lock()
671 if _, exists := c.openFiles[uri]; !exists {
671 if _, exists := c.openFiles[uri]; !exists {
@@ -695,7 +695,7 @@ func (c *Client) CloseFile(ctx context.Context, filepath string) error {
695}
695}
696
696
697func (c *Client) IsFileOpen(filepath string) bool {
697func (c *Client) IsFileOpen(filepath string) bool {
698− uri := fmt.Sprintf("file://%s", filepath)
698+ uri := string(protocol.URIFromPath(filepath))
699 c.openFilesMu.RLock()
699 c.openFilesMu.RLock()
700 defer c.openFilesMu.RUnlock()
700 defer c.openFilesMu.RUnlock()
701 _, exists := c.openFiles[uri]
701 _, exists := c.openFiles[uri]
@@ -710,8 +710,8 @@ func (c *Client) CloseAllFiles(ctx context.Context) {
710
710
711 // First collect all URIs that need to be closed
711 // First collect all URIs that need to be closed
712 for uri := range c.openFiles {
712 for uri := range c.openFiles {
713− // Convert URI back to file path by trimming "file://" prefix
713+ // Convert URI back to file path using proper URI handling
714− filePath := strings.TrimPrefix(uri, "file://")
714+ filePath := protocol.DocumentUri(uri).Path()
715 filesToClose = append(filesToClose, filePath)
715 filesToClose = append(filesToClose, filePath)
716 }
716 }
717 c.openFilesMu.Unlock()
717 c.openFilesMu.Unlock()
@@ -756,8 +756,7 @@ func (c *Client) OpenFileOnDemand(ctx context.Context, filepath string) error {
756// GetDiagnosticsForFile ensures a file is open and returns its diagnostics
756// GetDiagnosticsForFile ensures a file is open and returns its diagnostics
757// This is useful for on-demand diagnostics when using lazy loading
757// This is useful for on-demand diagnostics when using lazy loading
758func (c *Client) GetDiagnosticsForFile(ctx context.Context, filepath string) ([]protocol.Diagnostic, error) {
758func (c *Client) GetDiagnosticsForFile(ctx context.Context, filepath string) ([]protocol.Diagnostic, error) {
759− uri := fmt.Sprintf("file://%s", filepath)
760− documentUri := protocol.DocumentUri(uri)
759+ documentUri := protocol.URIFromPath(filepath)
761
760
762 // Make sure the file is open
761 // Make sure the file is open
763 if !c.IsFileOpen(filepath) {
762 if !c.IsFileOpen(filepath) {
internal/lsp/protocol/pattern_interfaces.go+2−3
@@ -2,7 +2,6 @@ package protocol
2
2
3import (
3import (
4 "fmt"
4 "fmt"
5− "strings"
6)
5)
7
6
8// PatternInfo is an interface for types that represent glob patterns
7// PatternInfo is an interface for types that represent glob patterns
@@ -45,9 +44,9 @@ func (g *GlobPattern) AsPattern() (PatternInfo, error) {
45 basePath := ""
44 basePath := ""
46 switch baseURI := v.BaseURI.Value.(type) {
45 switch baseURI := v.BaseURI.Value.(type) {
47 case string:
46 case string:
48− basePath = strings.TrimPrefix(baseURI, "file://")
47+ basePath = DocumentUri(baseURI).Path()
49 case DocumentUri:
48 case DocumentUri:
50− basePath = strings.TrimPrefix(string(baseURI), "file://")
49+ basePath = baseURI.Path()
51 default:
50 default:
52 return nil, fmt.Errorf("unknown BaseURI type: %T", v.BaseURI.Value)
51 return nil, fmt.Errorf("unknown BaseURI type: %T", v.BaseURI.Value)
53 }
52 }
internal/lsp/util/edit.go+5−5
@@ -11,7 +11,7 @@ import (
11)
11)
12
12
13func applyTextEdits(uri protocol.DocumentUri, edits []protocol.TextEdit) error {
13func applyTextEdits(uri protocol.DocumentUri, edits []protocol.TextEdit) error {
14− path := strings.TrimPrefix(string(uri), "file://")
14+ path := uri.Path()
15
15
16 // Read the file content
16 // Read the file content
17 content, err := os.ReadFile(path)
17 content, err := os.ReadFile(path)
@@ -148,7 +148,7 @@ func applyTextEdit(lines []string, edit protocol.TextEdit) ([]string, error) {
148// applyDocumentChange applies a DocumentChange (create/rename/delete operations)
148// applyDocumentChange applies a DocumentChange (create/rename/delete operations)
149func applyDocumentChange(change protocol.DocumentChange) error {
149func applyDocumentChange(change protocol.DocumentChange) error {
150 if change.CreateFile != nil {
150 if change.CreateFile != nil {
151− path := strings.TrimPrefix(string(change.CreateFile.URI), "file://")
151+ path := change.CreateFile.URI.Path()
152 if change.CreateFile.Options != nil {
152 if change.CreateFile.Options != nil {
153 if change.CreateFile.Options.Overwrite {
153 if change.CreateFile.Options.Overwrite {
154 // Proceed with overwrite
154 // Proceed with overwrite
@@ -164,7 +164,7 @@ func applyDocumentChange(change protocol.DocumentChange) error {
164 }
164 }
165
165
166 if change.DeleteFile != nil {
166 if change.DeleteFile != nil {
167− path := strings.TrimPrefix(string(change.DeleteFile.URI), "file://")
167+ path := change.DeleteFile.URI.Path()
168 if change.DeleteFile.Options != nil && change.DeleteFile.Options.Recursive {
168 if change.DeleteFile.Options != nil && change.DeleteFile.Options.Recursive {
169 if err := os.RemoveAll(path); err != nil {
169 if err := os.RemoveAll(path); err != nil {
170 return fmt.Errorf("failed to delete directory recursively: %w", err)
170 return fmt.Errorf("failed to delete directory recursively: %w", err)
@@ -177,8 +177,8 @@ func applyDocumentChange(change protocol.DocumentChange) error {
177 }
177 }
178
178
179 if change.RenameFile != nil {
179 if change.RenameFile != nil {
180− oldPath := strings.TrimPrefix(string(change.RenameFile.OldURI), "file://")
180+ oldPath := change.RenameFile.OldURI.Path()
181− newPath := strings.TrimPrefix(string(change.RenameFile.NewURI), "file://")
181+ newPath := change.RenameFile.NewURI.Path()
182 if change.RenameFile.Options != nil {
182 if change.RenameFile.Options != nil {
183 if !change.RenameFile.Options.Overwrite {
183 if !change.RenameFile.Options.Overwrite {
184 if _, err := os.Stat(newPath); err == nil {
184 if _, err := os.Stat(newPath); err == nil {
internal/lsp/watcher/watcher.go+3−3
@@ -387,7 +387,7 @@ func (w *WorkspaceWatcher) WatchWorkspace(ctx context.Context, workspacePath str
387 return
387 return
388 }
388 }
389
389
390− uri := fmt.Sprintf("file://%s", event.Name)
390+ uri := string(protocol.URIFromPath(event.Name))
391
391
392 // Add new directories to the watcher
392 // Add new directories to the watcher
393 if event.Op&fsnotify.Create != 0 {
393 if event.Op&fsnotify.Create != 0 {
@@ -614,7 +614,7 @@ func (w *WorkspaceWatcher) matchesPattern(path string, pattern protocol.GlobPatt
614 }
614 }
615
615
616 // For relative patterns
616 // For relative patterns
617− basePath = strings.TrimPrefix(basePath, "file://")
617+ basePath = protocol.DocumentUri(basePath).Path()
618 basePath = filepath.ToSlash(basePath)
618 basePath = filepath.ToSlash(basePath)
619
619
620 // Make path relative to basePath for matching
620 // Make path relative to basePath for matching
@@ -657,7 +657,7 @@ func (w *WorkspaceWatcher) debounceHandleFileEvent(ctx context.Context, uri stri
657// handleFileEvent sends file change notifications
657// handleFileEvent sends file change notifications
658func (w *WorkspaceWatcher) handleFileEvent(ctx context.Context, uri string, changeType protocol.FileChangeType) {
658func (w *WorkspaceWatcher) handleFileEvent(ctx context.Context, uri string, changeType protocol.FileChangeType) {
659 // If the file is open and it's a change event, use didChange notification
659 // If the file is open and it's a change event, use didChange notification
660− filePath := uri[7:] // Remove "file://" prefix
660+ filePath := protocol.DocumentUri(uri).Path()
661 if changeType == protocol.FileChangeType(protocol.Deleted) {
661 if changeType == protocol.FileChangeType(protocol.Deleted) {
662 w.client.ClearDiagnosticsForURI(protocol.DocumentUri(uri))
662 w.client.ClearDiagnosticsForURI(protocol.DocumentUri(uri))
663 } else if changeType == protocol.FileChangeType(protocol.Changed) && w.client.IsFileOpen(filePath) {
663 } else if changeType == protocol.FileChangeType(protocol.Changed) && w.client.IsFileOpen(filePath) {
internal/tui/components/chat/editor/editor.go+7−1
@@ -4,6 +4,7 @@ import (
4 "fmt"
4 "fmt"
5 "os"
5 "os"
6 "os/exec"
6 "os/exec"
7+ "runtime"
7 "slices"
8 "slices"
8 "strings"
9 "strings"
9 "unicode"
10 "unicode"
@@ -70,7 +71,12 @@ const (
70func (m *editorCmp) openEditor() tea.Cmd {
71func (m *editorCmp) openEditor() tea.Cmd {
71 editor := os.Getenv("EDITOR")
72 editor := os.Getenv("EDITOR")
72 if editor == "" {
73 if editor == "" {
74+ // Use platform-appropriate default editor
75+ if runtime.GOOS == "windows" {
76+ editor = "notepad"
77+ } else {
73− editor = "nvim"
78+ editor = "nvim"
79+ }
74 }
80 }
75
81
76 tmpfile, err := os.CreateTemp("", "msg_*.md")
82 tmpfile, err := os.CreateTemp("", "msg_*.md")