From 9d75e31304fa0082ac328bcd0d94810af71f7c11 Mon Sep 17 00:00:00 2001 From: Raphael Amorim Date: Wed, 25 Jun 2025 12:20:21 +0200 Subject: [PATCH] fix: windows basic issues --- 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(-) diff --git a/internal/config/config.go b/internal/config/config.go index 3554bd8e4da08ac6a95a95b3f0fe0b6e712eccff..fef8fd43b60a183ad0e161ef3d1cf6ff17156c05 100644 --- a/internal/config/config.go +++ b/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() } diff --git a/internal/fsext/fileutil.go b/internal/fsext/fileutil.go index 8abc3cebe98297b27c0e93c52881761fc971f9ec..1726f916b07ac9ac0defdf7c06dae7a8768b30c5 100644 --- a/internal/fsext/fileutil.go +++ b/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 { diff --git a/internal/llm/tools/bash.go b/internal/llm/tools/bash.go index 2a3f4e745e80243c1d5aa2ffdbdb344e007a8fcd..7963e95af24c6bb6257adc0c6341b95f453443b3 100644 --- a/internal/llm/tools/bash.go +++ b/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: diff --git a/internal/llm/tools/edit.go b/internal/llm/tools/edit.go index fdbf4042528cfe8e2e213860203a16cb5d82ecf5..52a202e32cd95869e3f76ebe6e3e01ca2f4802cc 100644 --- a/internal/llm/tools/edit.go +++ b/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.` ) diff --git a/internal/llm/tools/glob.go b/internal/llm/tools/glob.go index 3a0adf0581b3ba75f4bd86e337ab355323a4f3e5..d59cc801cb50ebbdcb7896c20a140b1fbedfdfcf 100644 --- a/internal/llm/tools/glob.go +++ b/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 diff --git a/internal/llm/tools/grep.go b/internal/llm/tools/grep.go index b004e47fe5f13caa7bc22be304fd074b19658471..fe77adf48dda0c637d63d26fe37cbd348e4ad9de 100644 --- a/internal/llm/tools/grep.go +++ b/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 diff --git a/internal/llm/tools/ls.go b/internal/llm/tools/ls.go index b31a69a6fc997edc769f53685ddb05137000f1a3..aeda758a66bc9dc796dc8d4e3722f8711aa39f67 100644 --- a/internal/llm/tools/ls.go +++ b/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 diff --git a/internal/llm/tools/shell/shell.go b/internal/llm/tools/shell/shell.go index cbf33e700c15ae06b66a4cbafa620bbc7ceb1405..7b75f04b6207fbb40a076dea2926e0846670e738 100644 --- a/internal/llm/tools/shell/shell.go +++ b/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 ( diff --git a/internal/llm/tools/view.go b/internal/llm/tools/view.go index 0c4652933c9b0a3e8be1b7b97a257433435993af..7d9cdaefcb98e0d6289396c9c44d45c4aa0b03c5 100644 --- a/internal/llm/tools/view.go +++ b/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 diff --git a/internal/llm/tools/write.go b/internal/llm/tools/write.go index 95692ba1a20489fdb91cc0618151b0d3c60a0403..4186954d03acc4768233435c71c2ac5d78606119 100644 --- a/internal/llm/tools/write.go +++ b/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 diff --git a/internal/lsp/client.go b/internal/lsp/client.go index 73310fda54c94f817467b9f2eb5439d184ca794d..f65b3dee20a3ee0264742257ca78a116661f1165 100644 --- a/internal/lsp/client.go +++ b/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) { diff --git a/internal/lsp/protocol/pattern_interfaces.go b/internal/lsp/protocol/pattern_interfaces.go index ebc7053dca6027765a3113c458a9f6204aba6443..ed68969faffd439732d34a150f6bbec9f6e0c264 100644 --- a/internal/lsp/protocol/pattern_interfaces.go +++ b/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) } diff --git a/internal/lsp/util/edit.go b/internal/lsp/util/edit.go index a67fab0a6a14e788f99a453a8488c5210f4d57d1..6cf37105bb81808130c81567ade3277ad879de4b 100644 --- a/internal/lsp/util/edit.go +++ b/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 { diff --git a/internal/lsp/watcher/watcher.go b/internal/lsp/watcher/watcher.go index 4db10e5b7163468f9d45b307be493a70028381b5..a69b3c10577d0c89ffb8aa9972a928201e2124f6 100644 --- a/internal/lsp/watcher/watcher.go +++ b/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) { diff --git a/internal/tui/components/chat/editor/editor.go b/internal/tui/components/chat/editor/editor.go index 2302fee3cf35ef99107f33a7140d70c4f2bdff48..04fbc2e22b1e49adc77b3be88c252b88b29c59a4 100644 --- a/internal/tui/components/chat/editor/editor.go +++ b/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")