fix: windows basic issues

Raphael Amorim created

Change summary

internal/config/config.go                     |  8 ++++++++
internal/fsext/fileutil.go                    |  2 +-
internal/llm/tools/bash.go                    |  8 ++++++++
internal/llm/tools/edit.go                    |  5 +++++
internal/llm/tools/glob.go                    |  6 ++++++
internal/llm/tools/grep.go                    |  6 ++++++
internal/llm/tools/ls.go                      |  6 ++++++
internal/llm/tools/shell/shell.go             | 11 +++++++++++
internal/llm/tools/view.go                    |  5 +++++
internal/llm/tools/write.go                   |  4 ++++
internal/lsp/client.go                        | 21 ++++++++++-----------
internal/lsp/protocol/pattern_interfaces.go   |  5 ++---
internal/lsp/util/edit.go                     | 10 +++++-----
internal/lsp/watcher/watcher.go               |  6 +++---
internal/tui/components/chat/editor/editor.go |  8 +++++++-
15 files changed, 87 insertions(+), 24 deletions(-)

Detailed changes

internal/config/config.go 🔗

@@ -203,9 +203,17 @@ func Load(workingDir string, debug bool) (*Config, error) {
 func configureViper() {
 	viper.SetConfigName(fmt.Sprintf(".%s", appName))
 	viper.SetConfigType("json")
+	
+	// Unix-style paths
 	viper.AddConfigPath("$HOME")
 	viper.AddConfigPath(fmt.Sprintf("$XDG_CONFIG_HOME/%s", appName))
 	viper.AddConfigPath(fmt.Sprintf("$HOME/.config/%s", appName))
+	
+	// Windows-style paths
+	viper.AddConfigPath(fmt.Sprintf("$USERPROFILE"))
+	viper.AddConfigPath(fmt.Sprintf("$APPDATA/%s", appName))
+	viper.AddConfigPath(fmt.Sprintf("$LOCALAPPDATA/%s", appName))
+	
 	viper.SetEnvPrefix(strings.ToUpper(appName))
 	viper.AutomaticEnv()
 }

internal/fsext/fileutil.go 🔗

@@ -61,7 +61,7 @@ func GetRgSearchCmd(pattern, path, include string) *exec.Cmd {
 	}
 	args = append(args, path)
 
-	return exec.Command("rg", args...)
+	return exec.Command(rgPath, args...)
 }
 
 type FileInfo struct {

internal/llm/tools/bash.go 🔗

@@ -59,6 +59,14 @@ func bashDescription() string {
 	bannedCommandsStr := strings.Join(bannedCommands, ", ")
 	return fmt.Sprintf(`Executes a given bash command in a persistent shell session with optional timeout, ensuring proper handling and security measures.
 
+IMPORTANT FOR WINDOWS USERS:
+- This tool uses a POSIX shell emulator (mvdan.cc/sh/v3) that works cross-platform, including Windows
+- On Windows, this provides bash-like functionality without requiring WSL or Git Bash
+- Use forward slashes (/) in paths - they work on all platforms and are converted automatically
+- Windows-specific commands (like 'dir', 'type', 'copy') are not available - use Unix equivalents ('ls', 'cat', 'cp')
+- Environment variables use Unix syntax: $VAR instead of %%VAR%%
+- File paths are automatically converted between Windows and Unix formats as needed
+
 Before executing the command, please follow these steps:
 
 1. Directory Verification:

internal/llm/tools/edit.go 🔗

@@ -90,6 +90,11 @@ When making edits:
    - Do not leave the code in a broken state
    - Always use absolute file paths (starting with /)
 
+WINDOWS NOTES:
+- File paths should use forward slashes (/) for cross-platform compatibility
+- On Windows, absolute paths start with drive letters (C:/) but forward slashes work throughout
+- File permissions are handled automatically by the Go runtime
+
 Remember: 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.`
 )
 

internal/llm/tools/glob.go 🔗

@@ -47,6 +47,12 @@ LIMITATIONS:
 - Does not search file contents (use Grep tool for that)
 - Hidden files (starting with '.') are skipped
 
+WINDOWS NOTES:
+- Uses ripgrep (rg) command if available, otherwise falls back to built-in Go implementation
+- On Windows, install ripgrep via: winget install BurntSushi.ripgrep.MSVC
+- Path separators are handled automatically (both / and \ work)
+- Patterns should use forward slashes (/) for cross-platform compatibility
+
 TIPS:
 - For the most useful results, combine with the Grep tool: first find files with Glob, then search their contents with Grep
 - When doing iterative exploration that may require multiple rounds of searching, consider using the Agent tool instead

internal/llm/tools/grep.go 🔗

@@ -124,6 +124,12 @@ LIMITATIONS:
 - Very large binary files may be skipped
 - Hidden files (starting with '.') are skipped
 
+WINDOWS NOTES:
+- Uses ripgrep (rg) command if available for better performance
+- On Windows, install ripgrep via: winget install BurntSushi.ripgrep.MSVC
+- Falls back to built-in Go implementation if ripgrep is not available
+- File paths are normalized automatically for Windows compatibility
+
 TIPS:
 - For faster, more targeted searches, first use Glob to find relevant files, then use Grep
 - When doing iterative exploration that may require multiple rounds of searching, consider using the Agent tool instead

internal/llm/tools/ls.go 🔗

@@ -58,6 +58,12 @@ LIMITATIONS:
 - Does not show file sizes or permissions
 - Cannot recursively list all directories in a large project
 
+WINDOWS NOTES:
+- Hidden file detection uses Unix convention (files starting with '.')
+- Windows-specific hidden files (with hidden attribute) are not automatically skipped
+- Common Windows directories like System32, Program Files are not in default ignore list
+- Path separators are handled automatically (both / and \ work)
+
 TIPS:
 - Use Glob tool for finding files by name patterns instead of browsing
 - Use Grep tool for searching file contents

internal/llm/tools/shell/shell.go 🔗

@@ -1,3 +1,14 @@
+// Package shell provides cross-platform shell execution capabilities.
+// 
+// WINDOWS COMPATIBILITY NOTE:
+// This implementation uses mvdan.cc/sh/v3 which provides POSIX shell emulation
+// on Windows. While this works for basic commands, it has limitations:
+// - Windows-specific commands (dir, type, copy) are not available
+// - PowerShell and cmd.exe specific features are not supported
+// - Some Windows path handling may be inconsistent
+// 
+// For full Windows compatibility, consider adding native Windows shell support
+// using os/exec with cmd.exe or PowerShell for Windows-specific commands.
 package shell
 
 import (

internal/llm/tools/view.go 🔗

@@ -60,6 +60,11 @@ LIMITATIONS:
 - Cannot display binary files or images
 - Images can be identified but not displayed
 
+WINDOWS NOTES:
+- Handles both Windows (CRLF) and Unix (LF) line endings automatically
+- File paths work with both forward slashes (/) and backslashes (\)
+- Text encoding is detected automatically for most common formats
+
 TIPS:
 - Use with Glob tool to first find files you want to view
 - For code exploration, first use Grep to find relevant files, then View to examine them

internal/llm/tools/write.go 🔗

@@ -64,6 +64,10 @@ LIMITATIONS:
 - You should read a file before writing to it to avoid conflicts
 - Cannot append to files (rewrites the entire file)
 
+WINDOWS NOTES:
+- File permissions (0o755, 0o644) are Unix-style but work on Windows with appropriate translations
+- Use forward slashes (/) in paths for cross-platform compatibility
+- Windows file attributes and permissions are handled automatically by the Go runtime
 
 TIPS:
 - Use the View tool first to examine existing files before modifying them

internal/lsp/client.go 🔗

@@ -131,7 +131,7 @@ func (c *Client) InitializeLSPClient(ctx context.Context, workspaceDir string) (
 		WorkspaceFoldersInitializeParams: protocol.WorkspaceFoldersInitializeParams{
 			WorkspaceFolders: []protocol.WorkspaceFolder{
 				{
-					URI:  protocol.URI("file://" + workspaceDir),
+					URI:  protocol.URI(protocol.URIFromPath(workspaceDir)),
 					Name: workspaceDir,
 				},
 			},
@@ -144,7 +144,7 @@ func (c *Client) InitializeLSPClient(ctx context.Context, workspaceDir string) (
 				Version: "0.1.0",
 			},
 			RootPath: workspaceDir,
-			RootURI:  protocol.DocumentUri("file://" + workspaceDir),
+			RootURI:  protocol.URIFromPath(workspaceDir),
 			Capabilities: protocol.ClientCapabilities{
 				Workspace: protocol.WorkspaceClientCapabilities{
 					Configuration: true,
@@ -448,7 +448,7 @@ func (c *Client) pingTypeScriptServer(ctx context.Context) error {
 
 	// If we have any open files, try to get document symbols for one
 	for uri := range c.openFiles {
-		filePath := strings.TrimPrefix(uri, "file://")
+		filePath := protocol.DocumentUri(uri).Path()
 		if strings.HasSuffix(filePath, ".ts") || strings.HasSuffix(filePath, ".js") ||
 			strings.HasSuffix(filePath, ".tsx") || strings.HasSuffix(filePath, ".jsx") {
 			var symbols []protocol.DocumentSymbol
@@ -586,7 +586,7 @@ type OpenFileInfo struct {
 }
 
 func (c *Client) OpenFile(ctx context.Context, filepath string) error {
-	uri := fmt.Sprintf("file://%s", filepath)
+	uri := string(protocol.URIFromPath(filepath))
 
 	c.openFilesMu.Lock()
 	if _, exists := c.openFiles[uri]; exists {
@@ -625,7 +625,7 @@ func (c *Client) OpenFile(ctx context.Context, filepath string) error {
 }
 
 func (c *Client) NotifyChange(ctx context.Context, filepath string) error {
-	uri := fmt.Sprintf("file://%s", filepath)
+	uri := string(protocol.URIFromPath(filepath))
 
 	content, err := os.ReadFile(filepath)
 	if err != nil {
@@ -665,7 +665,7 @@ func (c *Client) NotifyChange(ctx context.Context, filepath string) error {
 
 func (c *Client) CloseFile(ctx context.Context, filepath string) error {
 	cnf := config.Get()
-	uri := fmt.Sprintf("file://%s", filepath)
+	uri := string(protocol.URIFromPath(filepath))
 
 	c.openFilesMu.Lock()
 	if _, exists := c.openFiles[uri]; !exists {
@@ -695,7 +695,7 @@ func (c *Client) CloseFile(ctx context.Context, filepath string) error {
 }
 
 func (c *Client) IsFileOpen(filepath string) bool {
-	uri := fmt.Sprintf("file://%s", filepath)
+	uri := string(protocol.URIFromPath(filepath))
 	c.openFilesMu.RLock()
 	defer c.openFilesMu.RUnlock()
 	_, exists := c.openFiles[uri]
@@ -710,8 +710,8 @@ func (c *Client) CloseAllFiles(ctx context.Context) {
 
 	// First collect all URIs that need to be closed
 	for uri := range c.openFiles {
-		// Convert URI back to file path by trimming "file://" prefix
-		filePath := strings.TrimPrefix(uri, "file://")
+		// Convert URI back to file path using proper URI handling
+		filePath := protocol.DocumentUri(uri).Path()
 		filesToClose = append(filesToClose, filePath)
 	}
 	c.openFilesMu.Unlock()
@@ -756,8 +756,7 @@ func (c *Client) OpenFileOnDemand(ctx context.Context, filepath string) error {
 // GetDiagnosticsForFile ensures a file is open and returns its diagnostics
 // This is useful for on-demand diagnostics when using lazy loading
 func (c *Client) GetDiagnosticsForFile(ctx context.Context, filepath string) ([]protocol.Diagnostic, error) {
-	uri := fmt.Sprintf("file://%s", filepath)
-	documentUri := protocol.DocumentUri(uri)
+	documentUri := protocol.URIFromPath(filepath)
 
 	// Make sure the file is open
 	if !c.IsFileOpen(filepath) {

internal/lsp/protocol/pattern_interfaces.go 🔗

@@ -2,7 +2,6 @@ package protocol
 
 import (
 	"fmt"
-	"strings"
 )
 
 // PatternInfo is an interface for types that represent glob patterns
@@ -45,9 +44,9 @@ func (g *GlobPattern) AsPattern() (PatternInfo, error) {
 		basePath := ""
 		switch baseURI := v.BaseURI.Value.(type) {
 		case string:
-			basePath = strings.TrimPrefix(baseURI, "file://")
+			basePath = DocumentUri(baseURI).Path()
 		case DocumentUri:
-			basePath = strings.TrimPrefix(string(baseURI), "file://")
+			basePath = baseURI.Path()
 		default:
 			return nil, fmt.Errorf("unknown BaseURI type: %T", v.BaseURI.Value)
 		}

internal/lsp/util/edit.go 🔗

@@ -11,7 +11,7 @@ import (
 )
 
 func applyTextEdits(uri protocol.DocumentUri, edits []protocol.TextEdit) error {
-	path := strings.TrimPrefix(string(uri), "file://")
+	path := uri.Path()
 
 	// Read the file content
 	content, err := os.ReadFile(path)
@@ -148,7 +148,7 @@ func applyTextEdit(lines []string, edit protocol.TextEdit) ([]string, error) {
 // applyDocumentChange applies a DocumentChange (create/rename/delete operations)
 func applyDocumentChange(change protocol.DocumentChange) error {
 	if change.CreateFile != nil {
-		path := strings.TrimPrefix(string(change.CreateFile.URI), "file://")
+		path := change.CreateFile.URI.Path()
 		if change.CreateFile.Options != nil {
 			if change.CreateFile.Options.Overwrite {
 				// Proceed with overwrite
@@ -164,7 +164,7 @@ func applyDocumentChange(change protocol.DocumentChange) error {
 	}
 
 	if change.DeleteFile != nil {
-		path := strings.TrimPrefix(string(change.DeleteFile.URI), "file://")
+		path := change.DeleteFile.URI.Path()
 		if change.DeleteFile.Options != nil && change.DeleteFile.Options.Recursive {
 			if err := os.RemoveAll(path); err != nil {
 				return fmt.Errorf("failed to delete directory recursively: %w", err)
@@ -177,8 +177,8 @@ func applyDocumentChange(change protocol.DocumentChange) error {
 	}
 
 	if change.RenameFile != nil {
-		oldPath := strings.TrimPrefix(string(change.RenameFile.OldURI), "file://")
-		newPath := strings.TrimPrefix(string(change.RenameFile.NewURI), "file://")
+		oldPath := change.RenameFile.OldURI.Path()
+		newPath := change.RenameFile.NewURI.Path()
 		if change.RenameFile.Options != nil {
 			if !change.RenameFile.Options.Overwrite {
 				if _, err := os.Stat(newPath); err == nil {

internal/lsp/watcher/watcher.go 🔗

@@ -387,7 +387,7 @@ func (w *WorkspaceWatcher) WatchWorkspace(ctx context.Context, workspacePath str
 				return
 			}
 
-			uri := fmt.Sprintf("file://%s", event.Name)
+			uri := string(protocol.URIFromPath(event.Name))
 
 			// Add new directories to the watcher
 			if event.Op&fsnotify.Create != 0 {
@@ -614,7 +614,7 @@ func (w *WorkspaceWatcher) matchesPattern(path string, pattern protocol.GlobPatt
 	}
 
 	// For relative patterns
-	basePath = strings.TrimPrefix(basePath, "file://")
+	basePath = protocol.DocumentUri(basePath).Path()
 	basePath = filepath.ToSlash(basePath)
 
 	// Make path relative to basePath for matching
@@ -657,7 +657,7 @@ func (w *WorkspaceWatcher) debounceHandleFileEvent(ctx context.Context, uri stri
 // handleFileEvent sends file change notifications
 func (w *WorkspaceWatcher) handleFileEvent(ctx context.Context, uri string, changeType protocol.FileChangeType) {
 	// If the file is open and it's a change event, use didChange notification
-	filePath := uri[7:] // Remove "file://" prefix
+	filePath := protocol.DocumentUri(uri).Path()
 	if changeType == protocol.FileChangeType(protocol.Deleted) {
 		w.client.ClearDiagnosticsForURI(protocol.DocumentUri(uri))
 	} else if changeType == protocol.FileChangeType(protocol.Changed) && w.client.IsFileOpen(filePath) {

internal/tui/components/chat/editor/editor.go 🔗

@@ -4,6 +4,7 @@ import (
 	"fmt"
 	"os"
 	"os/exec"
+	"runtime"
 	"slices"
 	"strings"
 	"unicode"
@@ -70,7 +71,12 @@ const (
 func (m *editorCmp) openEditor() tea.Cmd {
 	editor := os.Getenv("EDITOR")
 	if editor == "" {
-		editor = "nvim"
+		// Use platform-appropriate default editor
+		if runtime.GOOS == "windows" {
+			editor = "notepad"
+		} else {
+			editor = "nvim"
+		}
 	}
 
 	tmpfile, err := os.CreateTemp("", "msg_*.md")