diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 939ebb46..f2bc8f7a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,7 +16,7 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 00000000..265dc194 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,33 @@ +name: Lint + +on: + workflow_dispatch: + pull_request: + push: + branches: + - main + +permissions: + contents: read + pull-requests: read + +jobs: + scan: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Required for merge_group events with only-new-issues + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + + - name: golangci-lint + uses: golangci/golangci-lint-action@v8.0.0 + timeout-minutes: 15 + with: + version: v2.1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 542000fb..45e958f0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,7 +16,7 @@ jobs: goreleaser: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 diff --git a/cmd/root.go b/cmd/root.go index 3a58cec4..c130fe62 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -49,7 +49,7 @@ to assist developers in writing, debugging, and understanding code directly from RunE: func(cmd *cobra.Command, args []string) error { // If the help flag is set, show the help message if cmd.Flag("help").Changed { - cmd.Help() + _ = cmd.Help() return nil } if cmd.Flag("version").Changed { @@ -303,7 +303,7 @@ func init() { rootCmd.Flags().BoolP("quiet", "q", false, "Hide spinner in non-interactive mode") // Register custom validation for the format flag - rootCmd.RegisterFlagCompletionFunc("output-format", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + _ = rootCmd.RegisterFlagCompletionFunc("output-format", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return format.SupportedFormats, cobra.ShellCompDirectiveNoFileComp }) } diff --git a/internal/app/lsp.go b/internal/app/lsp.go index 872532fd..efe2aaa7 100644 --- a/internal/app/lsp.go +++ b/internal/app/lsp.go @@ -10,6 +10,13 @@ import ( "github.com/opencode-ai/opencode/internal/lsp/watcher" ) +// contextKey is a custom type for context keys to avoid collisions +type contextKey string + +const ( + serverNameKey contextKey = "serverName" +) + func (app *App) initLSPClients(ctx context.Context) { cfg := config.Get() @@ -25,7 +32,7 @@ func (app *App) initLSPClients(ctx context.Context) { func (app *App) createAndStartLSPClient(ctx context.Context, name string, command string, args ...string) { // Create a specific context for initialization with a timeout logging.Info("Creating LSP client", "name", name, "command", command, "args", args) - + // Create the LSP client lspClient, err := lsp.NewClient(ctx, command, args...) if err != nil { @@ -36,13 +43,15 @@ func (app *App) createAndStartLSPClient(ctx context.Context, name string, comman // Create a longer timeout for initialization (some servers take time to start) initCtx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() - + // Initialize with the initialization context _, err = lspClient.InitializeLSPClient(initCtx, config.WorkingDirectory()) if err != nil { logging.Error("Initialize failed", "name", name, "error", err) // Clean up the client to prevent resource leaks - lspClient.Close() + if closeErr := lspClient.Close(); closeErr != nil { + logging.Error("Failed to close LSP client after initialization failure", "name", name, "error", closeErr) + } return } @@ -57,13 +66,13 @@ func (app *App) createAndStartLSPClient(ctx context.Context, name string, comman } logging.Info("LSP client initialized", "name", name) - + // Create a child context that can be canceled when the app is shutting down watchCtx, cancelFunc := context.WithCancel(ctx) - + // Create a context with the server name for better identification - watchCtx = context.WithValue(watchCtx, "serverName", name) - + watchCtx = context.WithValue(watchCtx, serverNameKey, name) + // Create the workspace watcher workspaceWatcher := watcher.NewWorkspaceWatcher(lspClient) diff --git a/internal/completions/files-folders.go b/internal/completions/files-folders.go index af1b5a87..666ce8f6 100644 --- a/internal/completions/files-folders.go +++ b/internal/completions/files-folders.go @@ -66,7 +66,11 @@ func (cg *filesAndFoldersContextGroup) getFiles(query string) ([]string, error) if err != nil { return nil, fmt.Errorf("failed to get rg stdout pipe: %w", err) } - defer rgPipe.Close() + defer func() { + if closeErr := rgPipe.Close(); closeErr != nil { + logging.Warn("Failed to close rg pipe", "error", closeErr) + } + }() cmdFzf.Stdin = rgPipe var fzfOut bytes.Buffer diff --git a/internal/config/config.go b/internal/config/config.go index 5a0905bb..b7822e4d 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -416,13 +416,15 @@ func readConfig(err error) error { // mergeLocalConfig loads and merges configuration from the local directory. func mergeLocalConfig(workingDir string) { local := viper.New() - local.SetConfigName(fmt.Sprintf(".%s", appName)) + local.SetConfigName(".opencode") local.SetConfigType("json") local.AddConfigPath(workingDir) // Merge local config if it exists if err := local.ReadInConfig(); err == nil { - viper.MergeConfigMap(local.AllSettings()) + if mergeErr := viper.MergeConfigMap(local.AllSettings()); mergeErr != nil { + logging.Warn("Failed to merge local config", "error", mergeErr) + } } } diff --git a/internal/config/init.go b/internal/config/init.go index e0a1c6da..25ee9c13 100644 --- a/internal/config/init.go +++ b/internal/config/init.go @@ -54,8 +54,12 @@ func MarkProjectInitialized() error { if err != nil { return fmt.Errorf("failed to create init flag file: %w", err) } - defer file.Close() + defer func() { + if closeErr := file.Close(); closeErr != nil { + // Log the error but don't fail the function since the main operation succeeded + fmt.Fprintf(os.Stderr, "Warning: failed to close init flag file: %v\n", closeErr) + } + }() return nil } - diff --git a/internal/db/connect.go b/internal/db/connect.go index b8fcb736..e6a557dd 100644 --- a/internal/db/connect.go +++ b/internal/db/connect.go @@ -32,7 +32,9 @@ func Connect() (*sql.DB, error) { // Verify connection if err = db.Ping(); err != nil { - db.Close() + if closeErr := db.Close(); closeErr != nil { + logging.Error("Failed to close database after ping failure", "error", closeErr) + } return nil, fmt.Errorf("failed to connect to database: %w", err) } diff --git a/internal/diff/diff.go b/internal/diff/diff.go index 8f5e669d..8a4c0721 100644 --- a/internal/diff/diff.go +++ b/internal/diff/diff.go @@ -563,18 +563,6 @@ func createStyles(t theme.Theme) (removedLineStyle, addedLineStyle, contextLineS // Rendering Functions // ------------------------------------------------------------------------- -func lipglossToHex(color lipgloss.Color) string { - r, g, b, a := color.RGBA() - - // Scale uint32 values (0-65535) to uint8 (0-255). - r8 := uint8(r >> 8) - g8 := uint8(g >> 8) - b8 := uint8(b >> 8) - a8 := uint8(a >> 8) - - return fmt.Sprintf("#%02x%02x%02x%02x", r8, g8, b8, a8) -} - // applyHighlighting applies intra-line highlighting to a piece of text func applyHighlighting(content string, segments []Segment, segmentType LineType, highlightBg lipgloss.AdaptiveColor) string { // Find all ANSI sequences in the content diff --git a/internal/format/format.go b/internal/format/format.go index 3d91ba05..f21da240 100644 --- a/internal/format/format.go +++ b/internal/format/format.go @@ -86,11 +86,11 @@ func formatAsJSON(content string) string { jsonBytes, err := json.MarshalIndent(response, "", " ") if err != nil { // In case of an error, return a manually formatted JSON - jsonEscaped := strings.Replace(content, "\\", "\\\\", -1) - jsonEscaped = strings.Replace(jsonEscaped, "\"", "\\\"", -1) - jsonEscaped = strings.Replace(jsonEscaped, "\n", "\\n", -1) - jsonEscaped = strings.Replace(jsonEscaped, "\r", "\\r", -1) - jsonEscaped = strings.Replace(jsonEscaped, "\t", "\\t", -1) + jsonEscaped := strings.ReplaceAll(content, "\\", "\\\\") + jsonEscaped = strings.ReplaceAll(jsonEscaped, "\"", "\\\"") + jsonEscaped = strings.ReplaceAll(jsonEscaped, "\n", "\\n") + jsonEscaped = strings.ReplaceAll(jsonEscaped, "\r", "\\r") + jsonEscaped = strings.ReplaceAll(jsonEscaped, "\t", "\\t") return fmt.Sprintf("{\n \"response\": \"%s\"\n}", jsonEscaped) } diff --git a/internal/history/file.go b/internal/history/file.go index 9cdb2e47..77a4e9d7 100644 --- a/internal/history/file.go +++ b/internal/history/file.go @@ -10,6 +10,7 @@ import ( "github.com/google/uuid" "github.com/opencode-ai/opencode/internal/db" + "github.com/opencode-ai/opencode/internal/logging" "github.com/opencode-ai/opencode/internal/pubsub" ) @@ -121,7 +122,9 @@ func (s *service) createWithVersion(ctx context.Context, sessionID, path, conten }) if txErr != nil { // Rollback the transaction - tx.Rollback() + if rollbackErr := tx.Rollback(); rollbackErr != nil { + logging.Warn("Failed to rollback transaction", "error", rollbackErr) + } // Check if this is a uniqueness constraint violation if strings.Contains(txErr.Error(), "UNIQUE constraint failed") { diff --git a/internal/llm/agent/agent.go b/internal/llm/agent/agent.go index 4f31fe75..de76bcff 100644 --- a/internal/llm/agent/agent.go +++ b/internal/llm/agent/agent.go @@ -283,7 +283,9 @@ func (a *agent) processGeneration(ctx context.Context, sessionID, content string if err != nil { if errors.Is(err, context.Canceled) { agentMessage.AddFinish(message.FinishReasonCanceled) - a.messages.Update(context.Background(), agentMessage) + if updateErr := a.messages.Update(context.Background(), agentMessage); updateErr != nil { + logging.Warn("Failed to update agent message on cancellation", "error", updateErr) + } return a.err(ErrRequestCancelled) } return a.err(fmt.Errorf("failed to process events: %w", err)) diff --git a/internal/llm/agent/mcp-tools.go b/internal/llm/agent/mcp-tools.go index 23756064..cb312090 100644 --- a/internal/llm/agent/mcp-tools.go +++ b/internal/llm/agent/mcp-tools.go @@ -42,7 +42,11 @@ func (b *mcpTool) Info() tools.ToolInfo { } func runTool(ctx context.Context, c MCPClient, toolName string, input string) (tools.ToolResponse, error) { - defer c.Close() + defer func() { + if closeErr := c.Close(); closeErr != nil { + logging.Warn("Failed to close MCP client", "error", closeErr) + } + }() initRequest := mcp.InitializeRequest{} initRequest.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION initRequest.Params.ClientInfo = mcp.Implementation{ @@ -158,7 +162,11 @@ func getTools(ctx context.Context, name string, m config.MCPServer, permissions for _, t := range tools.Tools { stdioTools = append(stdioTools, NewMcpTool(name, t, permissions, m)) } - defer c.Close() + defer func() { + if closeErr := c.Close(); closeErr != nil { + logging.Warn("Failed to close MCP client", "error", closeErr) + } + }() return stdioTools } diff --git a/internal/llm/models/local.go b/internal/llm/models/local.go index 5d8412c8..5a71d2b9 100644 --- a/internal/llm/models/local.go +++ b/internal/llm/models/local.go @@ -1,7 +1,6 @@ package models import ( - "cmp" "encoding/json" "net/http" "net/url" @@ -82,7 +81,11 @@ func listLocalModels(modelsEndpoint string) []localModel { "endpoint", modelsEndpoint, ) } - defer res.Body.Close() + defer func() { + if closeErr := res.Body.Close(); closeErr != nil { + logging.Debug("Failed to close response body", "error", closeErr) + } + }() if res.StatusCode != http.StatusOK { logging.Debug("Failed to list local models", @@ -135,13 +138,17 @@ func loadLocalModels(models []localModel) { } func convertLocalModel(model localModel) Model { + contextWindow := model.LoadedContextLength + if contextWindow == 0 { + contextWindow = 4096 + } return Model{ ID: ModelID("local." + model.ID), Name: friendlyModelName(model.ID), Provider: ProviderLocal, APIModel: model.ID, - ContextWindow: cmp.Or(model.LoadedContextLength, 4096), - DefaultMaxTokens: cmp.Or(model.LoadedContextLength, 4096), + ContextWindow: contextWindow, + DefaultMaxTokens: contextWindow, CanReason: true, SupportsAttachments: true, } diff --git a/internal/llm/prompt/prompt.go b/internal/llm/prompt/prompt.go index 8cdbdfc2..d213eefc 100644 --- a/internal/llm/prompt/prompt.go +++ b/internal/llm/prompt/prompt.go @@ -73,7 +73,7 @@ func processContextPaths(workDir string, paths []string) string { defer wg.Done() if strings.HasSuffix(p, "/") { - filepath.WalkDir(filepath.Join(workDir, p), func(path string, d os.DirEntry, err error) error { + if walkErr := filepath.WalkDir(filepath.Join(workDir, p), func(path string, d os.DirEntry, err error) error { if err != nil { return err } @@ -93,7 +93,9 @@ func processContextPaths(workDir string, paths []string) string { } } return nil - }) + }); walkErr != nil { + logging.Warn("Failed to walk directory", "error", walkErr, "path", p) + } } else { fullPath := filepath.Join(workDir, p) diff --git a/internal/llm/provider/anthropic.go b/internal/llm/provider/anthropic.go index badf6a3a..fcbc4412 100644 --- a/internal/llm/provider/anthropic.go +++ b/internal/llm/provider/anthropic.go @@ -248,8 +248,8 @@ func (a *anthropicClient) stream(ctx context.Context, messages []message.Message preparedMessages := a.preparedMessages(a.convertMessages(messages), a.convertTools(tools)) cfg := config.Get() if cfg.Debug { - // jsonData, _ := json.Marshal(preparedMessages) - // logging.Debug("Prepared messages", "messages", string(jsonData)) + jsonData, _ := json.Marshal(preparedMessages) + logging.Debug("Prepared messages", "messages", string(jsonData)) } attempts := 0 eventChan := make(chan ProviderEvent) @@ -273,9 +273,10 @@ func (a *anthropicClient) stream(ctx context.Context, messages []message.Message switch event := event.AsAny().(type) { case anthropic.ContentBlockStartEvent: - if event.ContentBlock.Type == "text" { + switch event.ContentBlock.Type { + case "text": eventChan <- ProviderEvent{Type: EventContentStart} - } else if event.ContentBlock.Type == "tool_use" { + case "tool_use": currentToolCallID = event.ContentBlock.ID eventChan <- ProviderEvent{ Type: EventToolUseStart, diff --git a/internal/llm/provider/bedrock.go b/internal/llm/provider/bedrock.go index 9f42e5b1..9fa3ca87 100644 --- a/internal/llm/provider/bedrock.go +++ b/internal/llm/provider/bedrock.go @@ -98,4 +98,3 @@ func (b *bedrockClient) stream(ctx context.Context, messages []message.Message, return b.childProvider.stream(ctx, messages, tools) } - diff --git a/internal/llm/provider/gemini.go b/internal/llm/provider/gemini.go index ebc36119..6a70a093 100644 --- a/internal/llm/provider/gemini.go +++ b/internal/llm/provider/gemini.go @@ -155,10 +155,10 @@ func (g *geminiClient) convertTools(tools []tools.BaseTool) []*genai.Tool { } func (g *geminiClient) finishReason(reason genai.FinishReason) message.FinishReason { - switch { - case reason == genai.FinishReasonStop: + switch reason { + case genai.FinishReasonStop: return message.FinishReasonEndTurn - case reason == genai.FinishReasonMaxTokens: + case genai.FinishReasonMaxTokens: return message.FinishReasonMaxTokens default: return message.FinishReasonUnknown @@ -404,12 +404,8 @@ func (g *geminiClient) shouldRetry(attempts int, err error) (bool, int64, error) } errMsg := err.Error() - isRateLimit := false - // Check for common rate limit error messages - if contains(errMsg, "rate limit", "quota exceeded", "too many requests") { - isRateLimit = true - } + isRateLimit := contains(errMsg, "rate limit", "quota exceeded", "too many requests") if !isRateLimit { return false, 0, err @@ -423,27 +419,6 @@ func (g *geminiClient) shouldRetry(attempts int, err error) (bool, int64, error) return true, int64(retryMs), nil } -func (g *geminiClient) toolCalls(resp *genai.GenerateContentResponse) []message.ToolCall { - var toolCalls []message.ToolCall - - if len(resp.Candidates) > 0 && resp.Candidates[0].Content != nil { - for _, part := range resp.Candidates[0].Content.Parts { - if part.FunctionCall != nil { - id := "call_" + uuid.New().String() - args, _ := json.Marshal(part.FunctionCall.Args) - toolCalls = append(toolCalls, message.ToolCall{ - ID: id, - Name: part.FunctionCall.Name, - Input: string(args), - Type: "function", - }) - } - } - } - - return toolCalls -} - func (g *geminiClient) usage(resp *genai.GenerateContentResponse) TokenUsage { if resp == nil || resp.UsageMetadata == nil { return TokenUsage{} diff --git a/internal/llm/provider/openai.go b/internal/llm/provider/openai.go index 8a561c77..dcc7f80b 100644 --- a/internal/llm/provider/openai.go +++ b/internal/llm/provider/openai.go @@ -166,7 +166,7 @@ func (o *openaiClient) preparedParams(messages []openai.ChatCompletionMessagePar Tools: tools, } - if o.providerOptions.model.CanReason == true { + if o.providerOptions.model.CanReason { params.MaxCompletionTokens = openai.Int(o.providerOptions.maxTokens) switch o.options.reasoningEffort { case "low": @@ -283,8 +283,8 @@ func (o *openaiClient) stream(ctx context.Context, messages []message.Message, t err := openaiStream.Err() if err == nil || errors.Is(err, io.EOF) { // Stream completed successfully - finishReason := o.finishReason(string(acc.ChatCompletion.Choices[0].FinishReason)) - if len(acc.ChatCompletion.Choices[0].Message.ToolCalls) > 0 { + finishReason := o.finishReason(string(acc.Choices[0].FinishReason)) + if len(acc.Choices[0].Message.ToolCalls) > 0 { toolCalls = append(toolCalls, o.toolCalls(acc.ChatCompletion)...) } if len(toolCalls) > 0 { diff --git a/internal/llm/tools/fetch.go b/internal/llm/tools/fetch.go index 863532a0..870f96b6 100644 --- a/internal/llm/tools/fetch.go +++ b/internal/llm/tools/fetch.go @@ -12,6 +12,7 @@ import ( md "github.com/JohannesKaufmann/html-to-markdown" "github.com/PuerkitoBio/goquery" "github.com/opencode-ai/opencode/internal/config" + "github.com/opencode-ai/opencode/internal/logging" "github.com/opencode-ai/opencode/internal/permission" ) @@ -158,7 +159,11 @@ func (t *fetchTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error if err != nil { return ToolResponse{}, fmt.Errorf("failed to fetch URL: %w", err) } - defer resp.Body.Close() + defer func() { + if closeErr := resp.Body.Close(); closeErr != nil { + logging.Warn("Failed to close response body", "error", closeErr) + } + }() if resp.StatusCode != http.StatusOK { return NewTextErrorResponse(fmt.Sprintf("Request failed with status code: %d", resp.StatusCode)), nil diff --git a/internal/llm/tools/grep.go b/internal/llm/tools/grep.go index f20d61ef..c9d461c0 100644 --- a/internal/llm/tools/grep.go +++ b/internal/llm/tools/grep.go @@ -16,6 +16,7 @@ import ( "github.com/opencode-ai/opencode/internal/config" "github.com/opencode-ai/opencode/internal/fileutil" + "github.com/opencode-ai/opencode/internal/logging" ) type GrepParams struct { @@ -329,7 +330,12 @@ func fileContainsPattern(filePath string, pattern *regexp.Regexp) (bool, int, st if err != nil { return false, 0, "", err } - defer file.Close() + defer func() { + if closeErr := file.Close(); closeErr != nil { + // Log at debug level since this is in a utility function + logging.Debug("Failed to close file", "error", closeErr, "path", filePath) + } + }() scanner := bufio.NewScanner(file) lineNum := 0 diff --git a/internal/llm/tools/ls_test.go b/internal/llm/tools/ls_test.go index 508cb98d..dd4488be 100644 --- a/internal/llm/tools/ls_test.go +++ b/internal/llm/tools/ls_test.go @@ -8,6 +8,7 @@ import ( "strings" "testing" + "github.com/opencode-ai/opencode/internal/config" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -27,7 +28,11 @@ func TestLsTool_Run(t *testing.T) { // Create a temporary directory for testing tempDir, err := os.MkdirTemp("", "ls_tool_test") require.NoError(t, err) - defer os.RemoveAll(tempDir) + defer func() { + if rmErr := os.RemoveAll(tempDir); rmErr != nil { + t.Logf("Warning: failed to clean up temp directory: %v", rmErr) + } + }() // Create a test directory structure testDirs := []string{ @@ -83,19 +88,19 @@ func TestLsTool_Run(t *testing.T) { response, err := tool.Run(context.Background(), call) require.NoError(t, err) - + // Check that visible directories and files are included assert.Contains(t, response.Content, "dir1") assert.Contains(t, response.Content, "dir2") assert.Contains(t, response.Content, "dir3") assert.Contains(t, response.Content, "file1.txt") assert.Contains(t, response.Content, "file2.txt") - + // Check that hidden files and directories are not included assert.NotContains(t, response.Content, ".hidden_dir") assert.NotContains(t, response.Content, ".hidden_file.txt") assert.NotContains(t, response.Content, ".hidden_root_file.txt") - + // Check that __pycache__ is not included assert.NotContains(t, response.Content, "__pycache__") }) @@ -120,9 +125,15 @@ func TestLsTool_Run(t *testing.T) { }) t.Run("handles empty path parameter", func(t *testing.T) { - // For this test, we need to mock the config.WorkingDirectory function - // Since we can't easily do that, we'll just check that the response doesn't contain an error message - + // Initialize config for this test to avoid panic + tmpConfigDir := t.TempDir() + _, err := config.Load(tmpConfigDir, false) + require.NoError(t, err) + + // Set the working directory to our test directory + cfg := config.Get() + cfg.WorkingDir = tempDir + tool := NewLsTool() params := LSParams{ Path: "", @@ -138,10 +149,11 @@ func TestLsTool_Run(t *testing.T) { response, err := tool.Run(context.Background(), call) require.NoError(t, err) - - // The response should either contain a valid directory listing or an error - // We'll just check that it's not empty + + // The response should contain a valid directory listing assert.NotEmpty(t, response.Content) + // Should list some of our test files/directories + assert.Contains(t, response.Content, "dir1") }) t.Run("handles invalid parameters", func(t *testing.T) { @@ -173,28 +185,39 @@ func TestLsTool_Run(t *testing.T) { response, err := tool.Run(context.Background(), call) require.NoError(t, err) - + // The output format is a tree, so we need to check for specific patterns // Check that file1.txt is not directly mentioned assert.NotContains(t, response.Content, "- file1.txt") - + // Check that dir1/ is not directly mentioned assert.NotContains(t, response.Content, "- dir1/") }) t.Run("handles relative path", func(t *testing.T) { + // Initialize config for this test + tmpConfigDir := t.TempDir() + _, err := config.Load(tmpConfigDir, false) + require.NoError(t, err) + // Save original working directory origWd, err := os.Getwd() require.NoError(t, err) defer func() { - os.Chdir(origWd) + if chErr := os.Chdir(origWd); chErr != nil { + t.Logf("Warning: failed to restore working directory: %v", chErr) + } }() - + // Change to a directory above the temp directory parentDir := filepath.Dir(tempDir) err = os.Chdir(parentDir) require.NoError(t, err) - + + // Set the working directory in config to match the current directory + cfg := config.Get() + cfg.WorkingDir = parentDir + tool := NewLsTool() params := LSParams{ Path: filepath.Base(tempDir), @@ -210,7 +233,7 @@ func TestLsTool_Run(t *testing.T) { response, err := tool.Run(context.Background(), call) require.NoError(t, err) - + // Should list the temp directory contents assert.Contains(t, response.Content, "dir1") assert.Contains(t, response.Content, "file1.txt") @@ -291,22 +314,22 @@ func TestCreateFileTree(t *testing.T) { } tree := createFileTree(paths) - + // Check the structure of the tree assert.Len(t, tree, 1) // Should have one root node - + // Check the root node rootNode := tree[0] assert.Equal(t, "path", rootNode.Name) assert.Equal(t, "directory", rootNode.Type) assert.Len(t, rootNode.Children, 1) - + // Check the "to" node toNode := rootNode.Children[0] assert.Equal(t, "to", toNode.Name) assert.Equal(t, "directory", toNode.Type) assert.Len(t, toNode.Children, 3) // file1.txt, dir1, dir2 - + // Find the dir1 node var dir1Node *TreeNode for _, child := range toNode.Children { @@ -315,7 +338,7 @@ func TestCreateFileTree(t *testing.T) { break } } - + require.NotNil(t, dir1Node) assert.Equal(t, "directory", dir1Node.Type) assert.Len(t, dir1Node.Children, 2) // file2.txt and subdir @@ -354,9 +377,9 @@ func TestPrintTree(t *testing.T) { Type: "file", }, } - + result := printTree(tree, "/root") - + // Check the output format assert.Contains(t, result, "- /root/") assert.Contains(t, result, " - dir1/") @@ -370,7 +393,11 @@ func TestListDirectory(t *testing.T) { // Create a temporary directory for testing tempDir, err := os.MkdirTemp("", "list_directory_test") require.NoError(t, err) - defer os.RemoveAll(tempDir) + defer func() { + if rmErr := os.RemoveAll(tempDir); rmErr != nil { + t.Logf("Warning: failed to clean up temp directory: %v", rmErr) + } + }() // Create a test directory structure testDirs := []string{ @@ -405,7 +432,7 @@ func TestListDirectory(t *testing.T) { files, truncated, err := listDirectory(tempDir, []string{}, 1000) require.NoError(t, err) assert.False(t, truncated) - + // Check that visible files and directories are included containsPath := func(paths []string, target string) bool { targetPath := filepath.Join(tempDir, target) @@ -416,12 +443,12 @@ func TestListDirectory(t *testing.T) { } return false } - + assert.True(t, containsPath(files, "dir1")) assert.True(t, containsPath(files, "file1.txt")) assert.True(t, containsPath(files, "file2.txt")) assert.True(t, containsPath(files, "dir1/file3.txt")) - + // Check that hidden files and directories are not included assert.False(t, containsPath(files, ".hidden_dir")) assert.False(t, containsPath(files, ".hidden_file.txt")) @@ -438,12 +465,12 @@ func TestListDirectory(t *testing.T) { files, truncated, err := listDirectory(tempDir, []string{"*.txt"}, 1000) require.NoError(t, err) assert.False(t, truncated) - + // Check that no .txt files are included for _, file := range files { assert.False(t, strings.HasSuffix(file, ".txt"), "Found .txt file: %s", file) } - + // But directories should still be included containsDir := false for _, file := range files { @@ -454,4 +481,4 @@ func TestListDirectory(t *testing.T) { } assert.True(t, containsDir) }) -} \ No newline at end of file +} diff --git a/internal/llm/tools/shell/shell.go b/internal/llm/tools/shell/shell.go index 7d3b87e4..8929fd7f 100644 --- a/internal/llm/tools/shell/shell.go +++ b/internal/llm/tools/shell/shell.go @@ -13,6 +13,7 @@ import ( "time" "github.com/opencode-ai/opencode/internal/config" + "github.com/opencode-ai/opencode/internal/logging" ) type PersistentShell struct { @@ -61,23 +62,23 @@ func GetPersistentShell(workingDir string) *PersistentShell { func newPersistentShell(cwd string) *PersistentShell { // Get shell configuration from config cfg := config.Get() - + // Default to environment variable if config is not set or nil var shellPath string var shellArgs []string - + if cfg != nil { shellPath = cfg.Shell.Path shellArgs = cfg.Shell.Args } - + if shellPath == "" { shellPath = os.Getenv("SHELL") if shellPath == "" { shellPath = "/bin/bash" } } - + // Default shell args if len(shellArgs) == 0 { shellArgs = []string{"-l"} @@ -120,7 +121,7 @@ func newPersistentShell(cwd string) *PersistentShell { go func() { err := cmd.Wait() if err != nil { - // Log the error if needed + logging.Debug("Shell process ended", "error", err) } shell.isAlive = false close(shell.commandQueue) @@ -155,10 +156,11 @@ func (s *PersistentShell) execCommand(command string, timeout time.Duration, ctx cwdFile := filepath.Join(tempDir, fmt.Sprintf("opencode-cwd-%d", time.Now().UnixNano())) defer func() { - os.Remove(stdoutFile) - os.Remove(stderrFile) - os.Remove(statusFile) - os.Remove(cwdFile) + // Clean up temporary files, but don't fail if they can't be removed + _ = os.Remove(stdoutFile) + _ = os.Remove(stderrFile) + _ = os.Remove(statusFile) + _ = os.Remove(cwdFile) }() fullCommand := fmt.Sprintf(` @@ -225,7 +227,10 @@ echo $EXEC_EXIT_CODE > %s exitCode := 0 if exitCodeStr != "" { - fmt.Sscanf(exitCodeStr, "%d", &exitCode) + // Parse exit code, ignore error as we have a fallback + if _, err := fmt.Sscanf(exitCodeStr, "%d", &exitCode); err != nil { + exitCode = 1 // fallback to error code + } } else if interrupted { exitCode = 143 stderr += "\nCommand execution timed out or was interrupted" @@ -254,14 +259,18 @@ func (s *PersistentShell) killChildren() { return } - for pidStr := range strings.SplitSeq(string(output), "\n") { + for _, pidStr := range strings.Split(string(output), "\n") { if pidStr = strings.TrimSpace(pidStr); pidStr != "" { var pid int - fmt.Sscanf(pidStr, "%d", &pid) + // Parse PID, ignore error as we have validation below + if _, err := fmt.Sscanf(pidStr, "%d", &pid); err != nil { + continue + } if pid > 0 { proc, err := os.FindProcess(pid) if err == nil { - proc.Signal(syscall.SIGTERM) + // Send signal, ignore error as process may already be dead + _ = proc.Signal(syscall.SIGTERM) } } } @@ -295,9 +304,13 @@ func (s *PersistentShell) Close() { return } - s.stdin.Write([]byte("exit\n")) + // Try to gracefully exit first + _, _ = s.stdin.Write([]byte("exit\n")) - s.cmd.Process.Kill() + // Force kill if needed + if s.cmd != nil && s.cmd.Process != nil { + _ = s.cmd.Process.Kill() + } s.isAlive = false } diff --git a/internal/llm/tools/sourcegraph.go b/internal/llm/tools/sourcegraph.go index 0d38c975..a1a670f1 100644 --- a/internal/llm/tools/sourcegraph.go +++ b/internal/llm/tools/sourcegraph.go @@ -9,6 +9,8 @@ import ( "net/http" "strings" "time" + + "github.com/opencode-ai/opencode/internal/logging" ) type SourcegraphParams struct { @@ -224,7 +226,11 @@ func (t *sourcegraphTool) Run(ctx context.Context, call ToolCall) (ToolResponse, if err != nil { return ToolResponse{}, fmt.Errorf("failed to fetch URL: %w", err) } - defer resp.Body.Close() + defer func() { + if closeErr := resp.Body.Close(); closeErr != nil { + logging.Warn("Failed to close response body", "error", closeErr) + } + }() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) diff --git a/internal/llm/tools/view.go b/internal/llm/tools/view.go index 6d800ce6..375cd8ea 100644 --- a/internal/llm/tools/view.go +++ b/internal/llm/tools/view.go @@ -11,6 +11,7 @@ import ( "strings" "github.com/opencode-ai/opencode/internal/config" + "github.com/opencode-ai/opencode/internal/logging" "github.com/opencode-ai/opencode/internal/lsp" ) @@ -224,7 +225,11 @@ func readTextFile(filePath string, offset, limit int) (string, int, error) { if err != nil { return "", 0, err } - defer file.Close() + defer func() { + if closeErr := file.Close(); closeErr != nil { + logging.Debug("Failed to close file", "error", closeErr, "path", filePath) + } + }() lineCount := 0 diff --git a/internal/logging/logger.go b/internal/logging/logger.go index 7ae2e7b8..11794f85 100644 --- a/internal/logging/logger.go +++ b/internal/logging/logger.go @@ -60,12 +60,22 @@ func RecoverPanic(name string, cleanup func()) { if err != nil { ErrorPersist(fmt.Sprintf("Failed to create panic log: %v", err)) } else { - defer file.Close() + defer func() { + if closeErr := file.Close(); closeErr != nil { + ErrorPersist(fmt.Sprintf("Failed to close panic log file: %v", closeErr)) + } + }() // Write panic information and stack trace - fmt.Fprintf(file, "Panic in %s: %v\n\n", name, r) - fmt.Fprintf(file, "Time: %s\n\n", time.Now().Format(time.RFC3339)) - fmt.Fprintf(file, "Stack Trace:\n%s\n", debug.Stack()) + if _, writeErr := fmt.Fprintf(file, "Panic in %s: %v\n\n", name, r); writeErr != nil { + ErrorPersist(fmt.Sprintf("Failed to write panic info: %v", writeErr)) + } + if _, writeErr := fmt.Fprintf(file, "Time: %s\n\n", time.Now().Format(time.RFC3339)); writeErr != nil { + ErrorPersist(fmt.Sprintf("Failed to write time info: %v", writeErr)) + } + if _, writeErr := fmt.Fprintf(file, "Stack Trace:\n%s\n", debug.Stack()); writeErr != nil { + ErrorPersist(fmt.Sprintf("Failed to write stack trace: %v", writeErr)) + } InfoPersist(fmt.Sprintf("Panic details written to %s", filename)) } diff --git a/internal/lsp/watcher/watcher.go b/internal/lsp/watcher/watcher.go index fd7e0483..7b6c0cc6 100644 --- a/internal/lsp/watcher/watcher.go +++ b/internal/lsp/watcher/watcher.go @@ -17,6 +17,14 @@ import ( "github.com/opencode-ai/opencode/internal/lsp/protocol" ) +// contextKey is a custom type for context keys to avoid collisions +type contextKey string + +const ( + workspaceWatcherKey contextKey = "workspaceWatcher" + serverNameKey contextKey = "serverName" +) + // WorkspaceWatcher manages LSP file watching type WorkspaceWatcher struct { client *lsp.Client @@ -309,12 +317,12 @@ func (w *WorkspaceWatcher) WatchWorkspace(ctx context.Context, workspacePath str w.workspacePath = workspacePath // Store the watcher in the context for later use - ctx = context.WithValue(ctx, "workspaceWatcher", w) + ctx = context.WithValue(ctx, workspaceWatcherKey, w) // If the server name isn't already in the context, try to detect it - if _, ok := ctx.Value("serverName").(string); !ok { + if _, ok := ctx.Value(serverNameKey).(string); !ok { serverName := getServerNameFromContext(ctx) - ctx = context.WithValue(ctx, "serverName", serverName) + ctx = context.WithValue(ctx, serverNameKey, serverName) } serverName := getServerNameFromContext(ctx) @@ -328,8 +336,13 @@ func (w *WorkspaceWatcher) WatchWorkspace(ctx context.Context, workspacePath str watcher, err := fsnotify.NewWatcher() if err != nil { logging.Error("Error creating watcher", "error", err) + return } - defer watcher.Close() + defer func() { + if closeErr := watcher.Close(); closeErr != nil { + logging.Error("Error closing file watcher", "error", closeErr) + } + }() // Watch the workspace recursively err = filepath.WalkDir(workspacePath, func(path string, d os.DirEntry, err error) error { @@ -685,12 +698,12 @@ func (w *WorkspaceWatcher) notifyFileEvent(ctx context.Context, uri string, chan // This is a best-effort function that tries to identify which LSP server we're dealing with func getServerNameFromContext(ctx context.Context) string { // First check if the server name is directly stored in the context - if serverName, ok := ctx.Value("serverName").(string); ok && serverName != "" { + if serverName, ok := ctx.Value(serverNameKey).(string); ok && serverName != "" { return strings.ToLower(serverName) } // Otherwise, try to extract server name from the client command path - if w, ok := ctx.Value("workspaceWatcher").(*WorkspaceWatcher); ok && w != nil && w.client != nil && w.client.Cmd != nil { + if w, ok := ctx.Value(workspaceWatcherKey).(*WorkspaceWatcher); ok && w != nil && w.client != nil && w.client.Cmd != nil { path := strings.ToLower(w.client.Cmd.Path) // Extract server name from path diff --git a/internal/tui/components/chat/chat.go b/internal/tui/components/chat/chat.go index 1ad3f683..4f6d2758 100644 --- a/internal/tui/components/chat/chat.go +++ b/internal/tui/components/chat/chat.go @@ -138,4 +138,3 @@ func cwd(width int) string { Width(width). Render(cwd) } - diff --git a/internal/tui/components/chat/editor.go b/internal/tui/components/chat/editor.go index a6c5a44e..03b0e6ea 100644 --- a/internal/tui/components/chat/editor.go +++ b/internal/tui/components/chat/editor.go @@ -38,11 +38,6 @@ type EditorKeyMaps struct { OpenEditor key.Binding } -type bluredEditorKeyMaps struct { - Send key.Binding - Focus key.Binding - OpenEditor key.Binding -} type DeleteAttachmentKeyMaps struct { AttachmentDeleteMode key.Binding Escape key.Binding @@ -89,7 +84,9 @@ func (m *editorCmp) openEditor() tea.Cmd { if err != nil { return util.ReportError(err) } - tmpfile.Close() + if closeErr := tmpfile.Close(); closeErr != nil { + return util.ReportError(fmt.Errorf("failed to close temporary file: %w", closeErr)) + } c := exec.Command(editor, tmpfile.Name()) //nolint:gosec c.Stdin = os.Stdin c.Stdout = os.Stdout @@ -105,7 +102,7 @@ func (m *editorCmp) openEditor() tea.Cmd { if len(content) == 0 { return util.ReportWarn("Message is empty") } - os.Remove(tmpfile.Name()) + _ = os.Remove(tmpfile.Name()) // Clean up temporary file attachments := m.attachments m.attachments = nil return SendMsg{ diff --git a/internal/tui/components/chat/list.go b/internal/tui/components/chat/list.go index 40d5b962..cc552c2e 100644 --- a/internal/tui/components/chat/list.go +++ b/internal/tui/components/chat/list.go @@ -3,7 +3,6 @@ package chat import ( "context" "fmt" - "math" "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/spinner" @@ -172,18 +171,6 @@ func (m *messagesCmp) IsAgentWorking() bool { return m.app.CoderAgent.IsSessionBusy(m.session.ID) } -func formatTimeDifference(unixTime1, unixTime2 int64) string { - diffSeconds := float64(math.Abs(float64(unixTime2 - unixTime1))) - - if diffSeconds < 60 { - return fmt.Sprintf("%.1fs", diffSeconds) - } - - minutes := int(diffSeconds / 60) - seconds := int(diffSeconds) % 60 - return fmt.Sprintf("%dm%ds", minutes, seconds) -} - func (m *messagesCmp) renderView() { m.uiMessages = make([]uiMessage, 0) pos := 0 diff --git a/internal/tui/components/chat/message.go b/internal/tui/components/chat/message.go index 0732366d..ee24b957 100644 --- a/internal/tui/components/chat/message.go +++ b/internal/tui/components/chat/message.go @@ -185,7 +185,7 @@ func renderAssistantMessage( position++ // for the space } else if thinking && thinkingContent != "" { // Render the thinking content - content = renderMessage(thinkingContent, false, msg.ID == focusedUIMessageId, width) + renderMessage(thinkingContent, false, msg.ID == focusedUIMessageId, width) } for i, toolCall := range msg.ToolCalls() { @@ -317,42 +317,41 @@ func renderParams(paramsWidth int, params ...string) string { func removeWorkingDirPrefix(path string) string { wd := config.WorkingDirectory() - if strings.HasPrefix(path, wd) { - path = strings.TrimPrefix(path, wd) - } - if strings.HasPrefix(path, "/") { - path = strings.TrimPrefix(path, "/") - } - if strings.HasPrefix(path, "./") { - path = strings.TrimPrefix(path, "./") - } - if strings.HasPrefix(path, "../") { - path = strings.TrimPrefix(path, "../") - } + path = strings.TrimPrefix(path, wd) + path = strings.TrimPrefix(path, "/") + path = strings.TrimPrefix(path, "./") + path = strings.TrimPrefix(path, "../") return path } func renderToolParams(paramWidth int, toolCall message.ToolCall) string { - params := "" switch toolCall.Name { case agent.AgentToolName: var params agent.AgentParams - json.Unmarshal([]byte(toolCall.Input), ¶ms) + if err := json.Unmarshal([]byte(toolCall.Input), ¶ms); err != nil { + return renderParams(paramWidth, "invalid parameters") + } prompt := strings.ReplaceAll(params.Prompt, "\n", " ") return renderParams(paramWidth, prompt) case tools.BashToolName: var params tools.BashParams - json.Unmarshal([]byte(toolCall.Input), ¶ms) + if err := json.Unmarshal([]byte(toolCall.Input), ¶ms); err != nil { + return renderParams(paramWidth, "invalid parameters") + } command := strings.ReplaceAll(params.Command, "\n", " ") return renderParams(paramWidth, command) case tools.EditToolName: var params tools.EditParams - json.Unmarshal([]byte(toolCall.Input), ¶ms) + if err := json.Unmarshal([]byte(toolCall.Input), ¶ms); err != nil { + return renderParams(paramWidth, "invalid parameters") + } filePath := removeWorkingDirPrefix(params.FilePath) return renderParams(paramWidth, filePath) case tools.FetchToolName: var params tools.FetchParams - json.Unmarshal([]byte(toolCall.Input), ¶ms) + if err := json.Unmarshal([]byte(toolCall.Input), ¶ms); err != nil { + return renderParams(paramWidth, "invalid parameters") + } url := params.URL toolParams := []string{ url, @@ -366,7 +365,9 @@ func renderToolParams(paramWidth int, toolCall message.ToolCall) string { return renderParams(paramWidth, toolParams...) case tools.GlobToolName: var params tools.GlobParams - json.Unmarshal([]byte(toolCall.Input), ¶ms) + if err := json.Unmarshal([]byte(toolCall.Input), ¶ms); err != nil { + return renderParams(paramWidth, "invalid parameters") + } pattern := params.Pattern toolParams := []string{ pattern, @@ -377,7 +378,9 @@ func renderToolParams(paramWidth int, toolCall message.ToolCall) string { return renderParams(paramWidth, toolParams...) case tools.GrepToolName: var params tools.GrepParams - json.Unmarshal([]byte(toolCall.Input), ¶ms) + if err := json.Unmarshal([]byte(toolCall.Input), ¶ms); err != nil { + return renderParams(paramWidth, "invalid parameters") + } pattern := params.Pattern toolParams := []string{ pattern, @@ -394,7 +397,9 @@ func renderToolParams(paramWidth int, toolCall message.ToolCall) string { return renderParams(paramWidth, toolParams...) case tools.LSToolName: var params tools.LSParams - json.Unmarshal([]byte(toolCall.Input), ¶ms) + if err := json.Unmarshal([]byte(toolCall.Input), ¶ms); err != nil { + return renderParams(paramWidth, "invalid parameters") + } path := params.Path if path == "" { path = "." @@ -402,11 +407,15 @@ func renderToolParams(paramWidth int, toolCall message.ToolCall) string { return renderParams(paramWidth, path) case tools.SourcegraphToolName: var params tools.SourcegraphParams - json.Unmarshal([]byte(toolCall.Input), ¶ms) + if err := json.Unmarshal([]byte(toolCall.Input), ¶ms); err != nil { + return renderParams(paramWidth, "invalid parameters") + } return renderParams(paramWidth, params.Query) case tools.ViewToolName: var params tools.ViewParams - json.Unmarshal([]byte(toolCall.Input), ¶ms) + if err := json.Unmarshal([]byte(toolCall.Input), ¶ms); err != nil { + return renderParams(paramWidth, "invalid parameters") + } filePath := removeWorkingDirPrefix(params.FilePath) toolParams := []string{ filePath, @@ -420,14 +429,15 @@ func renderToolParams(paramWidth int, toolCall message.ToolCall) string { return renderParams(paramWidth, toolParams...) case tools.WriteToolName: var params tools.WriteParams - json.Unmarshal([]byte(toolCall.Input), ¶ms) + if err := json.Unmarshal([]byte(toolCall.Input), ¶ms); err != nil { + return renderParams(paramWidth, "invalid parameters") + } filePath := removeWorkingDirPrefix(params.FilePath) return renderParams(paramWidth, filePath) default: input := strings.ReplaceAll(toolCall.Input, "\n", " ") - params = renderParams(paramWidth, input) + return renderParams(paramWidth, input) } - return params } func truncateHeight(content string, height int) string { @@ -466,13 +476,17 @@ func renderToolResponse(toolCall message.ToolCall, response message.ToolResult, ) case tools.EditToolName: metadata := tools.EditResponseMetadata{} - json.Unmarshal([]byte(response.Metadata), &metadata) + if err := json.Unmarshal([]byte(response.Metadata), &metadata); err != nil { + return baseStyle.Width(width).Foreground(t.Error()).Render("Error parsing metadata") + } truncDiff := truncateHeight(metadata.Diff, maxResultHeight) formattedDiff, _ := diff.FormatDiff(truncDiff, diff.WithTotalWidth(width)) return formattedDiff case tools.FetchToolName: var params tools.FetchParams - json.Unmarshal([]byte(toolCall.Input), ¶ms) + if err := json.Unmarshal([]byte(toolCall.Input), ¶ms); err != nil { + return baseStyle.Width(width).Foreground(t.Error()).Render("Error parsing parameters") + } mdFormat := "markdown" switch params.Format { case "text": @@ -495,7 +509,9 @@ func renderToolResponse(toolCall message.ToolCall, response message.ToolResult, return baseStyle.Width(width).Foreground(t.TextMuted()).Render(resultContent) case tools.ViewToolName: metadata := tools.ViewResponseMetadata{} - json.Unmarshal([]byte(response.Metadata), &metadata) + if err := json.Unmarshal([]byte(response.Metadata), &metadata); err != nil { + return baseStyle.Width(width).Foreground(t.Error()).Render("Error parsing metadata") + } ext := filepath.Ext(metadata.FilePath) if ext == "" { ext = "" @@ -509,9 +525,13 @@ func renderToolResponse(toolCall message.ToolCall, response message.ToolResult, ) case tools.WriteToolName: params := tools.WriteParams{} - json.Unmarshal([]byte(toolCall.Input), ¶ms) + if err := json.Unmarshal([]byte(toolCall.Input), ¶ms); err != nil { + return baseStyle.Width(width).Foreground(t.Error()).Render("Error parsing parameters") + } metadata := tools.WriteResponseMetadata{} - json.Unmarshal([]byte(response.Metadata), &metadata) + if err := json.Unmarshal([]byte(response.Metadata), &metadata); err != nil { + return baseStyle.Width(width).Foreground(t.Error()).Render("Error parsing metadata") + } ext := filepath.Ext(params.FilePath) if ext == "" { ext = "" @@ -566,7 +586,7 @@ func renderToolMessage( progressText := baseStyle. Width(width - 2 - lipgloss.Width(toolNameText)). Foreground(t.TextMuted()). - Render(fmt.Sprintf("%s", toolAction)) + Render(toolAction) content := style.Render(lipgloss.JoinHorizontal(lipgloss.Left, toolNameText, progressText)) toolMsg := uiMessage{ diff --git a/internal/tui/components/chat/sidebar.go b/internal/tui/components/chat/sidebar.go index a66249b3..e7f15f86 100644 --- a/internal/tui/components/chat/sidebar.go +++ b/internal/tui/components/chat/sidebar.go @@ -180,7 +180,7 @@ func (m *sidebarCmp) modifiedFiles() string { Render("Modified Files:") // If no modified files, show a placeholder message - if m.modFiles == nil || len(m.modFiles) == 0 { + if len(m.modFiles) == 0 { message := "No modified files" remainingWidth := m.width - lipgloss.Width(message) if remainingWidth > 0 { diff --git a/internal/tui/components/core/status.go b/internal/tui/components/core/status.go index 0dc227a8..f97ead55 100644 --- a/internal/tui/components/core/status.go +++ b/internal/tui/components/core/status.go @@ -258,14 +258,6 @@ func (m *statusCmp) projectDiagnostics() string { return strings.Join(diagnostics, " ") } -func (m statusCmp) availableFooterMsgWidth(diagnostics, tokenInfo string) int { - tokensWidth := 0 - if m.session.ID != "" { - tokensWidth = lipgloss.Width(tokenInfo) + 2 - } - return max(0, m.width-lipgloss.Width(helpWidget)-lipgloss.Width(m.model())-lipgloss.Width(diagnostics)-tokensWidth) -} - func (m statusCmp) model() string { t := theme.CurrentTheme() diff --git a/internal/tui/components/dialog/arguments.go b/internal/tui/components/dialog/arguments.go index 684d8662..9b01f134 100644 --- a/internal/tui/components/dialog/arguments.go +++ b/internal/tui/components/dialog/arguments.go @@ -2,6 +2,7 @@ package dialog import ( "fmt" + "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" @@ -75,7 +76,7 @@ func NewMultiArgumentsDialogCmp(commandID, content string, argNames []string) Mu ti.PlaceholderStyle = ti.PlaceholderStyle.Background(t.Background()) ti.PromptStyle = ti.PromptStyle.Background(t.Background()) ti.TextStyle = ti.TextStyle.Background(t.Background()) - + // Only focus the first input initially if i == 0 { ti.Focus() @@ -89,11 +90,11 @@ func NewMultiArgumentsDialogCmp(commandID, content string, argNames []string) Mu } return MultiArgumentsDialogCmp{ - inputs: inputs, - keys: argumentsDialogKeyMap{}, - commandID: commandID, - content: content, - argNames: argNames, + inputs: inputs, + keys: argumentsDialogKeyMap{}, + commandID: commandID, + content: content, + argNames: argNames, focusIndex: 0, } } @@ -108,7 +109,7 @@ func (m MultiArgumentsDialogCmp) Init() tea.Cmd { m.inputs[i].Blur() } } - + return textinput.Blink } @@ -181,7 +182,10 @@ func (m MultiArgumentsDialogCmp) View() string { baseStyle := styles.BaseStyle() // Calculate width needed for content - maxWidth := 60 // Width for explanation text + maxWidth := 80 + if m.width > 100 { + maxWidth = 120 + } title := lipgloss.NewStyle(). Foreground(t.Primary()). @@ -206,13 +210,13 @@ func (m MultiArgumentsDialogCmp) View() string { Width(maxWidth). Padding(1, 1, 0, 1). Background(t.Background()) - + if i == m.focusIndex { labelStyle = labelStyle.Foreground(t.Primary()).Bold(true) } else { labelStyle = labelStyle.Foreground(t.TextMuted()) } - + label := labelStyle.Render(m.argNames[i] + ":") field := lipgloss.NewStyle(). @@ -225,8 +229,6 @@ func (m MultiArgumentsDialogCmp) View() string { inputFields[i] = lipgloss.JoinVertical(lipgloss.Left, label, field) } - maxWidth = min(maxWidth, m.width-10) - // Join all elements vertically elements := []string{title, explanation} elements = append(elements, inputFields...) @@ -254,4 +256,4 @@ func (m *MultiArgumentsDialogCmp) SetSize(width, height int) { // Bindings implements layout.Bindings. func (m MultiArgumentsDialogCmp) Bindings() []key.Binding { return m.keys.ShortHelp() -} \ No newline at end of file +} diff --git a/internal/tui/components/dialog/complete.go b/internal/tui/components/dialog/complete.go index 1ce66e12..518c9f95 100644 --- a/internal/tui/components/dialog/complete.go +++ b/internal/tui/components/dialog/complete.go @@ -14,7 +14,6 @@ import ( ) type CompletionItem struct { - title string Title string Value string } diff --git a/internal/tui/components/dialog/custom_commands_test.go b/internal/tui/components/dialog/custom_commands_test.go index 3468ac3b..c21eaaa5 100644 --- a/internal/tui/components/dialog/custom_commands_test.go +++ b/internal/tui/components/dialog/custom_commands_test.go @@ -1,8 +1,8 @@ package dialog import ( - "testing" "regexp" + "testing" ) func TestNamedArgPattern(t *testing.T) { @@ -38,11 +38,11 @@ func TestNamedArgPattern(t *testing.T) { for _, tc := range testCases { matches := namedArgPattern.FindAllStringSubmatch(tc.input, -1) - + // Extract unique argument names argNames := make([]string, 0) argMap := make(map[string]bool) - + for _, match := range matches { argName := match[1] // Group 1 is the name without $ if !argMap[argName] { @@ -50,13 +50,13 @@ func TestNamedArgPattern(t *testing.T) { argNames = append(argNames, argName) } } - + // Check if we got the expected number of arguments if len(argNames) != len(tc.expected) { t.Errorf("Expected %d arguments, got %d for input: %s", len(tc.expected), len(argNames), tc.input) continue } - + // Check if we got the expected argument names for _, expectedArg := range tc.expected { found := false @@ -75,7 +75,7 @@ func TestNamedArgPattern(t *testing.T) { func TestRegexPattern(t *testing.T) { pattern := regexp.MustCompile(`\$([A-Z][A-Z0-9_]*)`) - + validMatches := []string{ "$FOO", "$BAR", @@ -83,7 +83,7 @@ func TestRegexPattern(t *testing.T) { "$BAZ123", "$ARGUMENTS", } - + invalidMatches := []string{ "$foo", "$1BAR", @@ -91,16 +91,16 @@ func TestRegexPattern(t *testing.T) { "FOO", "$", } - + for _, valid := range validMatches { if !pattern.MatchString(valid) { t.Errorf("Expected %s to match, but it didn't", valid) } } - + for _, invalid := range invalidMatches { if pattern.MatchString(invalid) { t.Errorf("Expected %s not to match, but it did", invalid) } } -} \ No newline at end of file +} diff --git a/internal/tui/components/dialog/filepicker.go b/internal/tui/components/dialog/filepicker.go index 3b9a0dc6..663522f5 100644 --- a/internal/tui/components/dialog/filepicker.go +++ b/internal/tui/components/dialog/filepicker.go @@ -77,11 +77,9 @@ var filePickerKeyMap = FilePrickerKeyMap{ } type filepickerCmp struct { - basePath string width int height int cursor int - err error cursorChain stack viewport viewport.Model dirs []os.DirEntry diff --git a/internal/tui/components/dialog/init.go b/internal/tui/components/dialog/init.go index 77c76584..b3f506e6 100644 --- a/internal/tui/components/dialog/init.go +++ b/internal/tui/components/dialog/init.go @@ -95,7 +95,7 @@ func (m InitDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (m InitDialogCmp) View() string { t := theme.CurrentTheme() baseStyle := styles.BaseStyle() - + // Calculate width needed for content maxWidth := 60 // Width for explanation text diff --git a/internal/tui/components/dialog/quit.go b/internal/tui/components/dialog/quit.go index f755fa27..bf109006 100644 --- a/internal/tui/components/dialog/quit.go +++ b/internal/tui/components/dialog/quit.go @@ -84,7 +84,7 @@ func (q *quitDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (q *quitDialogCmp) View() string { t := theme.CurrentTheme() baseStyle := styles.BaseStyle() - + yesStyle := baseStyle noStyle := baseStyle spacerStyle := baseStyle.Background(t.Background()) diff --git a/internal/tui/components/dialog/session.go b/internal/tui/components/dialog/session.go index a29fa713..4154a354 100644 --- a/internal/tui/components/dialog/session.go +++ b/internal/tui/components/dialog/session.go @@ -108,7 +108,7 @@ func (s *sessionDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (s *sessionDialogCmp) View() string { t := theme.CurrentTheme() baseStyle := styles.BaseStyle() - + if len(s.sessions) == 0 { return baseStyle.Padding(1, 2). Border(lipgloss.RoundedBorder()). diff --git a/internal/tui/components/dialog/theme.go b/internal/tui/components/dialog/theme.go index d35d3e2b..752f85e6 100644 --- a/internal/tui/components/dialog/theme.go +++ b/internal/tui/components/dialog/theme.go @@ -195,4 +195,3 @@ func NewThemeDialogCmp() ThemeDialog { currentTheme: "", } } - diff --git a/internal/tui/components/logs/details.go b/internal/tui/components/logs/details.go index 9d7713bb..04d0cb9c 100644 --- a/internal/tui/components/logs/details.go +++ b/internal/tui/components/logs/details.go @@ -99,7 +99,7 @@ func (i *detailCmp) updateContent() { func getLevelStyle(level string) lipgloss.Style { style := lipgloss.NewStyle().Bold(true) t := theme.CurrentTheme() - + switch strings.ToLower(level) { case "info": return style.Foreground(t.Info()) diff --git a/internal/tui/components/util/simple-list.go b/internal/tui/components/util/simple-list.go index 7aad2494..e733e989 100644 --- a/internal/tui/components/util/simple-list.go +++ b/internal/tui/components/util/simple-list.go @@ -29,8 +29,6 @@ type simpleListCmp[T SimpleListItem] struct { maxWidth int maxVisibleItems int useAlphaNumericKeys bool - width int - height int } type simpleListKeyMap struct { diff --git a/internal/tui/image/images.go b/internal/tui/image/images.go index d10a169f..48725dbd 100644 --- a/internal/tui/image/images.go +++ b/internal/tui/image/images.go @@ -59,7 +59,12 @@ func ImagePreview(width int, filename string) (string, error) { if err != nil { return "", err } - defer imageContent.Close() + defer func() { + if closeErr := imageContent.Close(); closeErr != nil { + // Log the error but don't fail the function since the main operation may have succeeded + fmt.Fprintf(os.Stderr, "Warning: failed to close image file: %v\n", closeErr) + } + }() img, _, err := image.Decode(imageContent) if err != nil { diff --git a/internal/tui/layout/overlay.go b/internal/tui/layout/overlay.go index 3a14dbc5..850c5382 100644 --- a/internal/tui/layout/overlay.go +++ b/internal/tui/layout/overlay.go @@ -47,7 +47,7 @@ func PlaceOverlay( t := theme.CurrentTheme() baseStyle := styles.BaseStyle() - var shadowbg string = "" + var shadowbg = "" shadowchar := lipgloss.NewStyle(). Background(t.BackgroundDarker()). Foreground(t.Background()). diff --git a/internal/tui/page/chat.go b/internal/tui/page/chat.go index d297a34c..a1023ffc 100644 --- a/internal/tui/page/chat.go +++ b/internal/tui/page/chat.go @@ -76,7 +76,7 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if p.app.CoderAgent.IsBusy() { return p, util.ReportWarn("Agent is busy, please wait before executing a command...") } - + // Process the command content with arguments if any content := msg.Content if msg.Args != nil { @@ -86,7 +86,7 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) { content = strings.ReplaceAll(content, placeholder, value) } } - + // Handle custom command execution cmd := p.sendMessage(content, nil) if cmd != nil { diff --git a/internal/tui/theme/catppuccin.go b/internal/tui/theme/catppuccin.go index a843100a..c3c32501 100644 --- a/internal/tui/theme/catppuccin.go +++ b/internal/tui/theme/catppuccin.go @@ -245,4 +245,4 @@ func NewCatppuccinTheme() *CatppuccinTheme { func init() { // Register the Catppuccin theme with the theme manager RegisterTheme("catppuccin", NewCatppuccinTheme()) -} \ No newline at end of file +} diff --git a/internal/tui/theme/dracula.go b/internal/tui/theme/dracula.go index e625206a..29a1457d 100644 --- a/internal/tui/theme/dracula.go +++ b/internal/tui/theme/dracula.go @@ -271,4 +271,4 @@ func NewDraculaTheme() *DraculaTheme { func init() { // Register the Dracula theme with the theme manager RegisterTheme("dracula", NewDraculaTheme()) -} \ No newline at end of file +} diff --git a/internal/tui/theme/flexoki.go b/internal/tui/theme/flexoki.go index 49d94beb..5da5683c 100644 --- a/internal/tui/theme/flexoki.go +++ b/internal/tui/theme/flexoki.go @@ -7,20 +7,20 @@ import ( // Flexoki color palette constants const ( // Base colors - flexokiPaper = "#FFFCF0" // Paper (lightest) - flexokiBase50 = "#F2F0E5" // bg-2 (light) - flexokiBase100 = "#E6E4D9" // ui (light) - flexokiBase150 = "#DAD8CE" // ui-2 (light) - flexokiBase200 = "#CECDC3" // ui-3 (light) - flexokiBase300 = "#B7B5AC" // tx-3 (light) - flexokiBase500 = "#878580" // tx-2 (light) - flexokiBase600 = "#6F6E69" // tx (light) - flexokiBase700 = "#575653" // tx-3 (dark) - flexokiBase800 = "#403E3C" // ui-3 (dark) - flexokiBase850 = "#343331" // ui-2 (dark) - flexokiBase900 = "#282726" // ui (dark) - flexokiBase950 = "#1C1B1A" // bg-2 (dark) - flexokiBlack = "#100F0F" // bg (darkest) + flexokiPaper = "#FFFCF0" // Paper (lightest) + flexokiBase50 = "#F2F0E5" // bg-2 (light) + flexokiBase100 = "#E6E4D9" // ui (light) + flexokiBase150 = "#DAD8CE" // ui-2 (light) + flexokiBase200 = "#CECDC3" // ui-3 (light) + flexokiBase300 = "#B7B5AC" // tx-3 (light) + flexokiBase500 = "#878580" // tx-2 (light) + flexokiBase600 = "#6F6E69" // tx (light) + flexokiBase700 = "#575653" // tx-3 (dark) + flexokiBase800 = "#403E3C" // ui-3 (dark) + flexokiBase850 = "#343331" // ui-2 (dark) + flexokiBase900 = "#282726" // ui (dark) + flexokiBase950 = "#1C1B1A" // bg-2 (dark) + flexokiBlack = "#100F0F" // bg (darkest) // Accent colors - Light theme (600) flexokiRed600 = "#AF3029" @@ -279,4 +279,4 @@ func NewFlexokiTheme() *FlexokiTheme { func init() { // Register the Flexoki theme with the theme manager RegisterTheme("flexoki", NewFlexokiTheme()) -} \ No newline at end of file +} diff --git a/internal/tui/theme/gruvbox.go b/internal/tui/theme/gruvbox.go index ed544b84..51719faa 100644 --- a/internal/tui/theme/gruvbox.go +++ b/internal/tui/theme/gruvbox.go @@ -173,11 +173,11 @@ func NewGruvboxTheme() *GruvboxTheme { Light: gruvboxLightRedBright, } theme.DiffAddedBgColor = lipgloss.AdaptiveColor{ - Dark: "#3C4C3C", // Darker green background + Dark: "#3C4C3C", // Darker green background Light: "#E8F5E9", // Light green background } theme.DiffRemovedBgColor = lipgloss.AdaptiveColor{ - Dark: "#4C3C3C", // Darker red background + Dark: "#4C3C3C", // Darker red background Light: "#FFEBEE", // Light red background } theme.DiffContextBgColor = lipgloss.AdaptiveColor{ @@ -189,11 +189,11 @@ func NewGruvboxTheme() *GruvboxTheme { Light: gruvboxLightFg4, } theme.DiffAddedLineNumberBgColor = lipgloss.AdaptiveColor{ - Dark: "#32432F", // Slightly darker green + Dark: "#32432F", // Slightly darker green Light: "#C8E6C9", // Light green } theme.DiffRemovedLineNumberBgColor = lipgloss.AdaptiveColor{ - Dark: "#43322F", // Slightly darker red + Dark: "#43322F", // Slightly darker red Light: "#FFCDD2", // Light red } @@ -299,4 +299,4 @@ func NewGruvboxTheme() *GruvboxTheme { func init() { // Register the Gruvbox theme with the theme manager RegisterTheme("gruvbox", NewGruvboxTheme()) -} \ No newline at end of file +} diff --git a/internal/tui/theme/monokai.go b/internal/tui/theme/monokai.go index 4695fefa..7511d333 100644 --- a/internal/tui/theme/monokai.go +++ b/internal/tui/theme/monokai.go @@ -270,4 +270,4 @@ func NewMonokaiProTheme() *MonokaiProTheme { func init() { // Register the Monokai Pro theme with the theme manager RegisterTheme("monokai", NewMonokaiProTheme()) -} \ No newline at end of file +} diff --git a/internal/tui/theme/onedark.go b/internal/tui/theme/onedark.go index 2b4dee50..a2c1447c 100644 --- a/internal/tui/theme/onedark.go +++ b/internal/tui/theme/onedark.go @@ -271,4 +271,4 @@ func NewOneDarkTheme() *OneDarkTheme { func init() { // Register the One Dark theme with the theme manager RegisterTheme("onedark", NewOneDarkTheme()) -} \ No newline at end of file +} diff --git a/internal/tui/theme/opencode.go b/internal/tui/theme/opencode.go index efec8615..7ee6f15e 100644 --- a/internal/tui/theme/opencode.go +++ b/internal/tui/theme/opencode.go @@ -274,4 +274,3 @@ func init() { // Register the OpenCode theme with the theme manager RegisterTheme("opencode", NewOpenCodeTheme()) } - diff --git a/internal/tui/theme/theme.go b/internal/tui/theme/theme.go index 4ee14a07..e39cf893 100644 --- a/internal/tui/theme/theme.go +++ b/internal/tui/theme/theme.go @@ -80,25 +80,25 @@ type Theme interface { // that can be embedded in concrete theme implementations. type BaseTheme struct { // Base colors - PrimaryColor lipgloss.AdaptiveColor - SecondaryColor lipgloss.AdaptiveColor - AccentColor lipgloss.AdaptiveColor + PrimaryColor lipgloss.AdaptiveColor + SecondaryColor lipgloss.AdaptiveColor + AccentColor lipgloss.AdaptiveColor // Status colors - ErrorColor lipgloss.AdaptiveColor - WarningColor lipgloss.AdaptiveColor - SuccessColor lipgloss.AdaptiveColor - InfoColor lipgloss.AdaptiveColor + ErrorColor lipgloss.AdaptiveColor + WarningColor lipgloss.AdaptiveColor + SuccessColor lipgloss.AdaptiveColor + InfoColor lipgloss.AdaptiveColor // Text colors - TextColor lipgloss.AdaptiveColor - TextMutedColor lipgloss.AdaptiveColor + TextColor lipgloss.AdaptiveColor + TextMutedColor lipgloss.AdaptiveColor TextEmphasizedColor lipgloss.AdaptiveColor // Background colors - BackgroundColor lipgloss.AdaptiveColor + BackgroundColor lipgloss.AdaptiveColor BackgroundSecondaryColor lipgloss.AdaptiveColor - BackgroundDarkerColor lipgloss.AdaptiveColor + BackgroundDarkerColor lipgloss.AdaptiveColor // Border colors BorderNormalColor lipgloss.AdaptiveColor @@ -106,103 +106,111 @@ type BaseTheme struct { BorderDimColor lipgloss.AdaptiveColor // Diff view colors - DiffAddedColor lipgloss.AdaptiveColor - DiffRemovedColor lipgloss.AdaptiveColor - DiffContextColor lipgloss.AdaptiveColor - DiffHunkHeaderColor lipgloss.AdaptiveColor - DiffHighlightAddedColor lipgloss.AdaptiveColor - DiffHighlightRemovedColor lipgloss.AdaptiveColor - DiffAddedBgColor lipgloss.AdaptiveColor - DiffRemovedBgColor lipgloss.AdaptiveColor - DiffContextBgColor lipgloss.AdaptiveColor - DiffLineNumberColor lipgloss.AdaptiveColor - DiffAddedLineNumberBgColor lipgloss.AdaptiveColor + DiffAddedColor lipgloss.AdaptiveColor + DiffRemovedColor lipgloss.AdaptiveColor + DiffContextColor lipgloss.AdaptiveColor + DiffHunkHeaderColor lipgloss.AdaptiveColor + DiffHighlightAddedColor lipgloss.AdaptiveColor + DiffHighlightRemovedColor lipgloss.AdaptiveColor + DiffAddedBgColor lipgloss.AdaptiveColor + DiffRemovedBgColor lipgloss.AdaptiveColor + DiffContextBgColor lipgloss.AdaptiveColor + DiffLineNumberColor lipgloss.AdaptiveColor + DiffAddedLineNumberBgColor lipgloss.AdaptiveColor DiffRemovedLineNumberBgColor lipgloss.AdaptiveColor // Markdown colors - MarkdownTextColor lipgloss.AdaptiveColor - MarkdownHeadingColor lipgloss.AdaptiveColor - MarkdownLinkColor lipgloss.AdaptiveColor - MarkdownLinkTextColor lipgloss.AdaptiveColor - MarkdownCodeColor lipgloss.AdaptiveColor - MarkdownBlockQuoteColor lipgloss.AdaptiveColor - MarkdownEmphColor lipgloss.AdaptiveColor - MarkdownStrongColor lipgloss.AdaptiveColor - MarkdownHorizontalRuleColor lipgloss.AdaptiveColor - MarkdownListItemColor lipgloss.AdaptiveColor + MarkdownTextColor lipgloss.AdaptiveColor + MarkdownHeadingColor lipgloss.AdaptiveColor + MarkdownLinkColor lipgloss.AdaptiveColor + MarkdownLinkTextColor lipgloss.AdaptiveColor + MarkdownCodeColor lipgloss.AdaptiveColor + MarkdownBlockQuoteColor lipgloss.AdaptiveColor + MarkdownEmphColor lipgloss.AdaptiveColor + MarkdownStrongColor lipgloss.AdaptiveColor + MarkdownHorizontalRuleColor lipgloss.AdaptiveColor + MarkdownListItemColor lipgloss.AdaptiveColor MarkdownListEnumerationColor lipgloss.AdaptiveColor - MarkdownImageColor lipgloss.AdaptiveColor - MarkdownImageTextColor lipgloss.AdaptiveColor - MarkdownCodeBlockColor lipgloss.AdaptiveColor + MarkdownImageColor lipgloss.AdaptiveColor + MarkdownImageTextColor lipgloss.AdaptiveColor + MarkdownCodeBlockColor lipgloss.AdaptiveColor // Syntax highlighting colors - SyntaxCommentColor lipgloss.AdaptiveColor - SyntaxKeywordColor lipgloss.AdaptiveColor - SyntaxFunctionColor lipgloss.AdaptiveColor - SyntaxVariableColor lipgloss.AdaptiveColor - SyntaxStringColor lipgloss.AdaptiveColor - SyntaxNumberColor lipgloss.AdaptiveColor - SyntaxTypeColor lipgloss.AdaptiveColor - SyntaxOperatorColor lipgloss.AdaptiveColor + SyntaxCommentColor lipgloss.AdaptiveColor + SyntaxKeywordColor lipgloss.AdaptiveColor + SyntaxFunctionColor lipgloss.AdaptiveColor + SyntaxVariableColor lipgloss.AdaptiveColor + SyntaxStringColor lipgloss.AdaptiveColor + SyntaxNumberColor lipgloss.AdaptiveColor + SyntaxTypeColor lipgloss.AdaptiveColor + SyntaxOperatorColor lipgloss.AdaptiveColor SyntaxPunctuationColor lipgloss.AdaptiveColor } // Implement the Theme interface for BaseTheme -func (t *BaseTheme) Primary() lipgloss.AdaptiveColor { return t.PrimaryColor } +func (t *BaseTheme) Primary() lipgloss.AdaptiveColor { return t.PrimaryColor } func (t *BaseTheme) Secondary() lipgloss.AdaptiveColor { return t.SecondaryColor } -func (t *BaseTheme) Accent() lipgloss.AdaptiveColor { return t.AccentColor } +func (t *BaseTheme) Accent() lipgloss.AdaptiveColor { return t.AccentColor } -func (t *BaseTheme) Error() lipgloss.AdaptiveColor { return t.ErrorColor } +func (t *BaseTheme) Error() lipgloss.AdaptiveColor { return t.ErrorColor } func (t *BaseTheme) Warning() lipgloss.AdaptiveColor { return t.WarningColor } func (t *BaseTheme) Success() lipgloss.AdaptiveColor { return t.SuccessColor } -func (t *BaseTheme) Info() lipgloss.AdaptiveColor { return t.InfoColor } +func (t *BaseTheme) Info() lipgloss.AdaptiveColor { return t.InfoColor } -func (t *BaseTheme) Text() lipgloss.AdaptiveColor { return t.TextColor } -func (t *BaseTheme) TextMuted() lipgloss.AdaptiveColor { return t.TextMutedColor } +func (t *BaseTheme) Text() lipgloss.AdaptiveColor { return t.TextColor } +func (t *BaseTheme) TextMuted() lipgloss.AdaptiveColor { return t.TextMutedColor } func (t *BaseTheme) TextEmphasized() lipgloss.AdaptiveColor { return t.TextEmphasizedColor } -func (t *BaseTheme) Background() lipgloss.AdaptiveColor { return t.BackgroundColor } +func (t *BaseTheme) Background() lipgloss.AdaptiveColor { return t.BackgroundColor } func (t *BaseTheme) BackgroundSecondary() lipgloss.AdaptiveColor { return t.BackgroundSecondaryColor } -func (t *BaseTheme) BackgroundDarker() lipgloss.AdaptiveColor { return t.BackgroundDarkerColor } +func (t *BaseTheme) BackgroundDarker() lipgloss.AdaptiveColor { return t.BackgroundDarkerColor } -func (t *BaseTheme) BorderNormal() lipgloss.AdaptiveColor { return t.BorderNormalColor } +func (t *BaseTheme) BorderNormal() lipgloss.AdaptiveColor { return t.BorderNormalColor } func (t *BaseTheme) BorderFocused() lipgloss.AdaptiveColor { return t.BorderFocusedColor } -func (t *BaseTheme) BorderDim() lipgloss.AdaptiveColor { return t.BorderDimColor } +func (t *BaseTheme) BorderDim() lipgloss.AdaptiveColor { return t.BorderDimColor } -func (t *BaseTheme) DiffAdded() lipgloss.AdaptiveColor { return t.DiffAddedColor } -func (t *BaseTheme) DiffRemoved() lipgloss.AdaptiveColor { return t.DiffRemovedColor } -func (t *BaseTheme) DiffContext() lipgloss.AdaptiveColor { return t.DiffContextColor } -func (t *BaseTheme) DiffHunkHeader() lipgloss.AdaptiveColor { return t.DiffHunkHeaderColor } -func (t *BaseTheme) DiffHighlightAdded() lipgloss.AdaptiveColor { return t.DiffHighlightAddedColor } +func (t *BaseTheme) DiffAdded() lipgloss.AdaptiveColor { return t.DiffAddedColor } +func (t *BaseTheme) DiffRemoved() lipgloss.AdaptiveColor { return t.DiffRemovedColor } +func (t *BaseTheme) DiffContext() lipgloss.AdaptiveColor { return t.DiffContextColor } +func (t *BaseTheme) DiffHunkHeader() lipgloss.AdaptiveColor { return t.DiffHunkHeaderColor } +func (t *BaseTheme) DiffHighlightAdded() lipgloss.AdaptiveColor { return t.DiffHighlightAddedColor } func (t *BaseTheme) DiffHighlightRemoved() lipgloss.AdaptiveColor { return t.DiffHighlightRemovedColor } -func (t *BaseTheme) DiffAddedBg() lipgloss.AdaptiveColor { return t.DiffAddedBgColor } -func (t *BaseTheme) DiffRemovedBg() lipgloss.AdaptiveColor { return t.DiffRemovedBgColor } -func (t *BaseTheme) DiffContextBg() lipgloss.AdaptiveColor { return t.DiffContextBgColor } -func (t *BaseTheme) DiffLineNumber() lipgloss.AdaptiveColor { return t.DiffLineNumberColor } -func (t *BaseTheme) DiffAddedLineNumberBg() lipgloss.AdaptiveColor { return t.DiffAddedLineNumberBgColor } -func (t *BaseTheme) DiffRemovedLineNumberBg() lipgloss.AdaptiveColor { return t.DiffRemovedLineNumberBgColor } - -func (t *BaseTheme) MarkdownText() lipgloss.AdaptiveColor { return t.MarkdownTextColor } -func (t *BaseTheme) MarkdownHeading() lipgloss.AdaptiveColor { return t.MarkdownHeadingColor } -func (t *BaseTheme) MarkdownLink() lipgloss.AdaptiveColor { return t.MarkdownLinkColor } -func (t *BaseTheme) MarkdownLinkText() lipgloss.AdaptiveColor { return t.MarkdownLinkTextColor } -func (t *BaseTheme) MarkdownCode() lipgloss.AdaptiveColor { return t.MarkdownCodeColor } +func (t *BaseTheme) DiffAddedBg() lipgloss.AdaptiveColor { return t.DiffAddedBgColor } +func (t *BaseTheme) DiffRemovedBg() lipgloss.AdaptiveColor { return t.DiffRemovedBgColor } +func (t *BaseTheme) DiffContextBg() lipgloss.AdaptiveColor { return t.DiffContextBgColor } +func (t *BaseTheme) DiffLineNumber() lipgloss.AdaptiveColor { return t.DiffLineNumberColor } +func (t *BaseTheme) DiffAddedLineNumberBg() lipgloss.AdaptiveColor { + return t.DiffAddedLineNumberBgColor +} +func (t *BaseTheme) DiffRemovedLineNumberBg() lipgloss.AdaptiveColor { + return t.DiffRemovedLineNumberBgColor +} + +func (t *BaseTheme) MarkdownText() lipgloss.AdaptiveColor { return t.MarkdownTextColor } +func (t *BaseTheme) MarkdownHeading() lipgloss.AdaptiveColor { return t.MarkdownHeadingColor } +func (t *BaseTheme) MarkdownLink() lipgloss.AdaptiveColor { return t.MarkdownLinkColor } +func (t *BaseTheme) MarkdownLinkText() lipgloss.AdaptiveColor { return t.MarkdownLinkTextColor } +func (t *BaseTheme) MarkdownCode() lipgloss.AdaptiveColor { return t.MarkdownCodeColor } func (t *BaseTheme) MarkdownBlockQuote() lipgloss.AdaptiveColor { return t.MarkdownBlockQuoteColor } -func (t *BaseTheme) MarkdownEmph() lipgloss.AdaptiveColor { return t.MarkdownEmphColor } -func (t *BaseTheme) MarkdownStrong() lipgloss.AdaptiveColor { return t.MarkdownStrongColor } -func (t *BaseTheme) MarkdownHorizontalRule() lipgloss.AdaptiveColor { return t.MarkdownHorizontalRuleColor } +func (t *BaseTheme) MarkdownEmph() lipgloss.AdaptiveColor { return t.MarkdownEmphColor } +func (t *BaseTheme) MarkdownStrong() lipgloss.AdaptiveColor { return t.MarkdownStrongColor } +func (t *BaseTheme) MarkdownHorizontalRule() lipgloss.AdaptiveColor { + return t.MarkdownHorizontalRuleColor +} func (t *BaseTheme) MarkdownListItem() lipgloss.AdaptiveColor { return t.MarkdownListItemColor } -func (t *BaseTheme) MarkdownListEnumeration() lipgloss.AdaptiveColor { return t.MarkdownListEnumerationColor } -func (t *BaseTheme) MarkdownImage() lipgloss.AdaptiveColor { return t.MarkdownImageColor } +func (t *BaseTheme) MarkdownListEnumeration() lipgloss.AdaptiveColor { + return t.MarkdownListEnumerationColor +} +func (t *BaseTheme) MarkdownImage() lipgloss.AdaptiveColor { return t.MarkdownImageColor } func (t *BaseTheme) MarkdownImageText() lipgloss.AdaptiveColor { return t.MarkdownImageTextColor } func (t *BaseTheme) MarkdownCodeBlock() lipgloss.AdaptiveColor { return t.MarkdownCodeBlockColor } -func (t *BaseTheme) SyntaxComment() lipgloss.AdaptiveColor { return t.SyntaxCommentColor } -func (t *BaseTheme) SyntaxKeyword() lipgloss.AdaptiveColor { return t.SyntaxKeywordColor } -func (t *BaseTheme) SyntaxFunction() lipgloss.AdaptiveColor { return t.SyntaxFunctionColor } -func (t *BaseTheme) SyntaxVariable() lipgloss.AdaptiveColor { return t.SyntaxVariableColor } -func (t *BaseTheme) SyntaxString() lipgloss.AdaptiveColor { return t.SyntaxStringColor } -func (t *BaseTheme) SyntaxNumber() lipgloss.AdaptiveColor { return t.SyntaxNumberColor } -func (t *BaseTheme) SyntaxType() lipgloss.AdaptiveColor { return t.SyntaxTypeColor } -func (t *BaseTheme) SyntaxOperator() lipgloss.AdaptiveColor { return t.SyntaxOperatorColor } -func (t *BaseTheme) SyntaxPunctuation() lipgloss.AdaptiveColor { return t.SyntaxPunctuationColor } \ No newline at end of file +func (t *BaseTheme) SyntaxComment() lipgloss.AdaptiveColor { return t.SyntaxCommentColor } +func (t *BaseTheme) SyntaxKeyword() lipgloss.AdaptiveColor { return t.SyntaxKeywordColor } +func (t *BaseTheme) SyntaxFunction() lipgloss.AdaptiveColor { return t.SyntaxFunctionColor } +func (t *BaseTheme) SyntaxVariable() lipgloss.AdaptiveColor { return t.SyntaxVariableColor } +func (t *BaseTheme) SyntaxString() lipgloss.AdaptiveColor { return t.SyntaxStringColor } +func (t *BaseTheme) SyntaxNumber() lipgloss.AdaptiveColor { return t.SyntaxNumberColor } +func (t *BaseTheme) SyntaxType() lipgloss.AdaptiveColor { return t.SyntaxTypeColor } +func (t *BaseTheme) SyntaxOperator() lipgloss.AdaptiveColor { return t.SyntaxOperatorColor } +func (t *BaseTheme) SyntaxPunctuation() lipgloss.AdaptiveColor { return t.SyntaxPunctuationColor } diff --git a/internal/tui/theme/theme_test.go b/internal/tui/theme/theme_test.go index 5ec810e3..790ee3aa 100644 --- a/internal/tui/theme/theme_test.go +++ b/internal/tui/theme/theme_test.go @@ -7,7 +7,7 @@ import ( func TestThemeRegistration(t *testing.T) { // Get list of available themes availableThemes := AvailableThemes() - + // Check if "catppuccin" theme is registered catppuccinFound := false for _, themeName := range availableThemes { @@ -16,11 +16,11 @@ func TestThemeRegistration(t *testing.T) { break } } - + if !catppuccinFound { t.Errorf("Catppuccin theme is not registered") } - + // Check if "gruvbox" theme is registered gruvboxFound := false for _, themeName := range availableThemes { @@ -29,11 +29,11 @@ func TestThemeRegistration(t *testing.T) { break } } - + if !gruvboxFound { t.Errorf("Gruvbox theme is not registered") } - + // Check if "monokai" theme is registered monokaiFound := false for _, themeName := range availableThemes { @@ -42,48 +42,48 @@ func TestThemeRegistration(t *testing.T) { break } } - + if !monokaiFound { t.Errorf("Monokai theme is not registered") } - + // Try to get the themes and make sure they're not nil catppuccin := GetTheme("catppuccin") if catppuccin == nil { t.Errorf("Catppuccin theme is nil") } - + gruvbox := GetTheme("gruvbox") if gruvbox == nil { t.Errorf("Gruvbox theme is nil") } - + monokai := GetTheme("monokai") if monokai == nil { t.Errorf("Monokai theme is nil") } - + // Test switching theme originalTheme := CurrentThemeName() - + err := SetTheme("gruvbox") if err != nil { t.Errorf("Failed to set theme to gruvbox: %v", err) } - + if CurrentThemeName() != "gruvbox" { t.Errorf("Theme not properly switched to gruvbox") } - + err = SetTheme("monokai") if err != nil { t.Errorf("Failed to set theme to monokai: %v", err) } - + if CurrentThemeName() != "monokai" { t.Errorf("Theme not properly switched to monokai") } - + // Switch back to original theme _ = SetTheme(originalTheme) -} \ No newline at end of file +} diff --git a/internal/tui/theme/tokyonight.go b/internal/tui/theme/tokyonight.go index acd9dbf6..a6499a25 100644 --- a/internal/tui/theme/tokyonight.go +++ b/internal/tui/theme/tokyonight.go @@ -271,4 +271,4 @@ func NewTokyoNightTheme() *TokyoNightTheme { func init() { // Register the Tokyo Night theme with the theme manager RegisterTheme("tokyonight", NewTokyoNightTheme()) -} \ No newline at end of file +} diff --git a/internal/tui/theme/tron.go b/internal/tui/theme/tron.go index 5f1bdfb0..c4997a6d 100644 --- a/internal/tui/theme/tron.go +++ b/internal/tui/theme/tron.go @@ -273,4 +273,4 @@ func NewTronTheme() *TronTheme { func init() { // Register the Tron theme with the theme manager RegisterTheme("tron", NewTronTheme()) -} \ No newline at end of file +} diff --git a/internal/tui/tui.go b/internal/tui/tui.go index 1c9c2f03..5530971b 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -316,7 +316,9 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // Start the summarization process return a, func() tea.Msg { ctx := context.Background() - a.app.CoderAgent.Summarize(ctx, a.selectedSession.ID) + if err := a.app.CoderAgent.Summarize(ctx, a.selectedSession.ID); err != nil { + logging.Warn("Failed to summarize session", "error", err, "sessionID", a.selectedSession.ID) + } return nil } @@ -668,15 +670,6 @@ func (a *appModel) RegisterCommand(cmd dialog.Command) { a.commands = append(a.commands, cmd) } -func (a *appModel) findCommand(id string) (dialog.Command, bool) { - for _, cmd := range a.commands { - if cmd.ID == id { - return cmd, true - } - } - return dialog.Command{}, false -} - func (a *appModel) moveToPage(pageID page.PageID) tea.Cmd { if a.app.CoderAgent.IsBusy() { // For now we don't move to any page if the agent is busy