diff --git a/cmd/logs.go b/cmd/logs.go index e6f59384afa795541d7c6016b64551bc0488b35b..30beb89ae9d2e1ec37f368a9261828fbfdf9417a 100644 --- a/cmd/logs.go +++ b/cmd/logs.go @@ -35,6 +35,7 @@ var logsCmd = &cobra.Command{ return fmt.Errorf("failed to tail log file: %v", err) } + log.SetLevel(log.DebugLevel) // Print the text of each received line for line := range t.Lines { var data map[string]any diff --git a/cmd/root.go b/cmd/root.go index 5cd3eb5355aa3f4580b97203e65eb2953cf42d65..840bc58f8803965f71630b0bcce04e249708ee2a 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "io" + "log/slog" "os" "sync" "time" @@ -14,7 +15,7 @@ import ( "github.com/charmbracelet/crush/internal/db" "github.com/charmbracelet/crush/internal/format" "github.com/charmbracelet/crush/internal/llm/agent" - "github.com/charmbracelet/crush/internal/logging" + "github.com/charmbracelet/crush/internal/log" "github.com/charmbracelet/crush/internal/pubsub" "github.com/charmbracelet/crush/internal/tui" "github.com/charmbracelet/crush/internal/version" @@ -36,7 +37,7 @@ to assist developers in writing, debugging, and understanding code directly from # Run with debug logging crush -d - # Run with debug logging in a specific directory + # Run with debug slog.in a specific directory crush -d -c /path/to/project # Print version @@ -92,7 +93,7 @@ to assist developers in writing, debugging, and understanding code directly from app, err := app.New(ctx, conn) if err != nil { - logging.Error("Failed to create app: %v", err) + slog.Error("Failed to create app: %v", err) return err } // Defer shutdown here so it runs for both interactive and non-interactive modes @@ -103,7 +104,7 @@ to assist developers in writing, debugging, and understanding code directly from prompt, err = maybePrependStdin(prompt) if err != nil { - logging.Error("Failed to read stdin: %v", err) + slog.Error("Failed to read stdin: %v", err) return err } @@ -132,18 +133,18 @@ to assist developers in writing, debugging, and understanding code directly from // Set up message handling for the TUI go func() { defer tuiWg.Done() - defer logging.RecoverPanic("TUI-message-handler", func() { + defer log.RecoverPanic("TUI-message-handler", func() { attemptTUIRecovery(program) }) for { select { case <-tuiCtx.Done(): - logging.Info("TUI message handler shutting down") + slog.Info("TUI message handler shutting down") return case msg, ok := <-ch: if !ok { - logging.Info("TUI message channel closed") + slog.Info("TUI message channel closed") return } program.Send(msg) @@ -165,7 +166,7 @@ to assist developers in writing, debugging, and understanding code directly from // Wait for TUI message handler to finish tuiWg.Wait() - logging.Info("All goroutines cleaned up") + slog.Info("All goroutines cleaned up") } // Run the TUI @@ -173,18 +174,18 @@ to assist developers in writing, debugging, and understanding code directly from cleanup() if err != nil { - logging.Error("TUI error: %v", err) + slog.Error("TUI error: %v", err) return fmt.Errorf("TUI error: %v", err) } - logging.Info("TUI exited with result: %v", result) + slog.Info("TUI exited with result: %v", result) return nil }, } // attemptTUIRecovery tries to recover the TUI after a panic func attemptTUIRecovery(program *tea.Program) { - logging.Info("Attempting to recover TUI after panic") + slog.Info("Attempting to recover TUI after panic") // We could try to restart the TUI or gracefully exit // For now, we'll just quit the program to avoid further issues @@ -193,7 +194,7 @@ func attemptTUIRecovery(program *tea.Program) { func initMCPTools(ctx context.Context, app *app.App) { go func() { - defer logging.RecoverPanic("MCP-goroutine", nil) + defer log.RecoverPanic("MCP-goroutine", nil) // Create a context with timeout for the initial MCP tools fetch ctxWithTimeout, cancel := context.WithTimeout(ctx, 30*time.Second) @@ -201,7 +202,7 @@ func initMCPTools(ctx context.Context, app *app.App) { // Set this up once with proper error handling agent.GetMcpTools(ctxWithTimeout, app.Permissions) - logging.Info("MCP message handling goroutine exiting") + slog.Info("MCP message handling goroutine exiting") }() } @@ -215,7 +216,7 @@ func setupSubscriber[T any]( wg.Add(1) go func() { defer wg.Done() - defer logging.RecoverPanic(fmt.Sprintf("subscription-%s", name), nil) + defer log.RecoverPanic(fmt.Sprintf("subscription-%s", name), nil) subCh := subscriber(ctx) @@ -223,7 +224,7 @@ func setupSubscriber[T any]( select { case event, ok := <-subCh: if !ok { - logging.Info("subscription channel closed", "name", name) + slog.Info("subscription channel closed", "name", name) return } @@ -232,13 +233,13 @@ func setupSubscriber[T any]( select { case outputCh <- msg: case <-time.After(2 * time.Second): - logging.Warn("message dropped due to slow consumer", "name", name) + slog.Warn("message dropped due to slow consumer", "name", name) case <-ctx.Done(): - logging.Info("subscription cancelled", "name", name) + slog.Info("subscription cancelled", "name", name) return } case <-ctx.Done(): - logging.Info("subscription cancelled", "name", name) + slog.Info("subscription cancelled", "name", name) return } } @@ -251,7 +252,6 @@ func setupSubscriptions(app *app.App, parentCtx context.Context) (chan tea.Msg, wg := sync.WaitGroup{} ctx, cancel := context.WithCancel(parentCtx) // Inherit from parent context - setupSubscriber(ctx, &wg, "logging", logging.Subscribe, ch) setupSubscriber(ctx, &wg, "sessions", app.Sessions.Subscribe, ch) setupSubscriber(ctx, &wg, "messages", app.Messages.Subscribe, ch) setupSubscriber(ctx, &wg, "permissions", app.Permissions.Subscribe, ch) @@ -259,22 +259,22 @@ func setupSubscriptions(app *app.App, parentCtx context.Context) (chan tea.Msg, setupSubscriber(ctx, &wg, "history", app.History.Subscribe, ch) cleanupFunc := func() { - logging.Info("Cancelling all subscriptions") + slog.Info("Cancelling all subscriptions") cancel() // Signal all goroutines to stop waitCh := make(chan struct{}) go func() { - defer logging.RecoverPanic("subscription-cleanup", nil) + defer log.RecoverPanic("subscription-cleanup", nil) wg.Wait() close(waitCh) }() select { case <-waitCh: - logging.Info("All subscription goroutines completed successfully") + slog.Info("All subscription goroutines completed successfully") close(ch) // Only close after all writers are confirmed done case <-time.After(5 * time.Second): - logging.Warn("Timed out waiting for some subscription goroutines to complete") + slog.Warn("Timed out waiting for some subscription goroutines to complete") close(ch) } } diff --git a/internal/app/app.go b/internal/app/app.go index 85e619b5f23061accf07b644df90a409fd3cbe2d..304b0dcb73421132750d4be67564f3d95157d1e7 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -14,7 +14,7 @@ import ( "github.com/charmbracelet/crush/internal/format" "github.com/charmbracelet/crush/internal/history" "github.com/charmbracelet/crush/internal/llm/agent" - "github.com/charmbracelet/crush/internal/logging" + "log/slog" "github.com/charmbracelet/crush/internal/lsp" "github.com/charmbracelet/crush/internal/message" "github.com/charmbracelet/crush/internal/permission" @@ -73,7 +73,7 @@ func New(ctx context.Context, conn *sql.DB) (*App, error) { app.LSPClients, ) if err != nil { - logging.Error("Failed to create coder agent", err) + slog.Error("Failed to create coder agent", err) return nil, err } @@ -82,7 +82,7 @@ func New(ctx context.Context, conn *sql.DB) (*App, error) { // RunNonInteractive handles the execution flow when a prompt is provided via CLI flag. func (a *App) RunNonInteractive(ctx context.Context, prompt string, outputFormat string, quiet bool) error { - logging.Info("Running in non-interactive mode") + slog.Info("Running in non-interactive mode") // Start spinner if not in quiet mode var spinner *format.Spinner @@ -107,7 +107,7 @@ func (a *App) RunNonInteractive(ctx context.Context, prompt string, outputFormat if err != nil { return fmt.Errorf("failed to create session for non-interactive mode: %w", err) } - logging.Info("Created session for non-interactive run", "session_id", sess.ID) + slog.Info("Created session for non-interactive run", "session_id", sess.ID) // Automatically approve all permission requests for this non-interactive session a.Permissions.AutoApproveSession(sess.ID) @@ -120,7 +120,7 @@ func (a *App) RunNonInteractive(ctx context.Context, prompt string, outputFormat result := <-done if result.Error != nil { if errors.Is(result.Error, context.Canceled) || errors.Is(result.Error, agent.ErrRequestCancelled) { - logging.Info("Agent processing cancelled", "session_id", sess.ID) + slog.Info("Agent processing cancelled", "session_id", sess.ID) return nil } return fmt.Errorf("agent processing failed: %w", result.Error) @@ -139,7 +139,7 @@ func (a *App) RunNonInteractive(ctx context.Context, prompt string, outputFormat fmt.Println(format.FormatOutput(content, outputFormat)) - logging.Info("Non-interactive run completed", "session_id", sess.ID) + slog.Info("Non-interactive run completed", "session_id", sess.ID) return nil } @@ -163,7 +163,7 @@ func (app *App) Shutdown() { for name, client := range clients { shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) if err := client.Shutdown(shutdownCtx); err != nil { - logging.Error("Failed to shutdown LSP client", "name", name, "error", err) + slog.Error("Failed to shutdown LSP client", "name", name, "error", err) } cancel() } diff --git a/internal/app/lsp.go b/internal/app/lsp.go index 7b95458a61c5603df1396a7905eb70370b7adfeb..3c44b4b761124b91c7590e5ec8cf724dccd718b7 100644 --- a/internal/app/lsp.go +++ b/internal/app/lsp.go @@ -2,10 +2,11 @@ package app import ( "context" + "log/slog" "time" "github.com/charmbracelet/crush/internal/config" - "github.com/charmbracelet/crush/internal/logging" + "github.com/charmbracelet/crush/internal/log" "github.com/charmbracelet/crush/internal/lsp" "github.com/charmbracelet/crush/internal/lsp/watcher" ) @@ -18,18 +19,18 @@ func (app *App) initLSPClients(ctx context.Context) { // Start each client initialization in its own goroutine go app.createAndStartLSPClient(ctx, name, clientConfig.Command, clientConfig.Args...) } - logging.Info("LSP clients initialization started in background") + slog.Info("LSP clients initialization started in background") } // createAndStartLSPClient creates a new LSP client, initializes it, and starts its workspace watcher 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) + slog.Info("Creating LSP client", "name", name, "command", command, "args", args) // Create the LSP client lspClient, err := lsp.NewClient(ctx, command, args...) if err != nil { - logging.Error("Failed to create LSP client for", name, err) + slog.Error("Failed to create LSP client for", name, err) return } @@ -40,7 +41,7 @@ func (app *App) createAndStartLSPClient(ctx context.Context, name string, comman // Initialize with the initialization context _, err = lspClient.InitializeLSPClient(initCtx, config.Get().WorkingDir()) if err != nil { - logging.Error("Initialize failed", "name", name, "error", err) + slog.Error("Initialize failed", "name", name, "error", err) // Clean up the client to prevent resource leaks lspClient.Close() return @@ -48,15 +49,15 @@ func (app *App) createAndStartLSPClient(ctx context.Context, name string, comman // Wait for the server to be ready if err := lspClient.WaitForServerReady(initCtx); err != nil { - logging.Error("Server failed to become ready", "name", name, "error", err) + slog.Error("Server failed to become ready", "name", name, "error", err) // We'll continue anyway, as some functionality might still work lspClient.SetServerState(lsp.StateError) } else { - logging.Info("LSP server is ready", "name", name) + slog.Info("LSP server is ready", "name", name) lspClient.SetServerState(lsp.StateReady) } - logging.Info("LSP client initialized", "name", name) + slog.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) @@ -86,13 +87,13 @@ func (app *App) createAndStartLSPClient(ctx context.Context, name string, comman // runWorkspaceWatcher executes the workspace watcher for an LSP client func (app *App) runWorkspaceWatcher(ctx context.Context, name string, workspaceWatcher *watcher.WorkspaceWatcher) { defer app.watcherWG.Done() - defer logging.RecoverPanic("LSP-"+name, func() { + defer log.RecoverPanic("LSP-"+name, func() { // Try to restart the client app.restartLSPClient(ctx, name) }) workspaceWatcher.WatchWorkspace(ctx, config.Get().WorkingDir()) - logging.Info("Workspace watcher stopped", "client", name) + slog.Info("Workspace watcher stopped", "client", name) } // restartLSPClient attempts to restart a crashed or failed LSP client @@ -101,7 +102,7 @@ func (app *App) restartLSPClient(ctx context.Context, name string) { cfg := config.Get() clientConfig, exists := cfg.LSP[name] if !exists { - logging.Error("Cannot restart client, configuration not found", "client", name) + slog.Error("Cannot restart client, configuration not found", "client", name) return } @@ -122,5 +123,5 @@ func (app *App) restartLSPClient(ctx context.Context, name string) { // Create a new client using the shared function app.createAndStartLSPClient(ctx, name, clientConfig.Command, clientConfig.Args...) - logging.Info("Successfully restarted LSP client", "client", name) + slog.Info("Successfully restarted LSP client", "client", name) } diff --git a/internal/config/init.go b/internal/config/init.go index 9d4614c81ce1bb71aa0d18c2084c3ee9587ad6e1..30818f1881f79663bf7e6045d4656046f73ed99e 100644 --- a/internal/config/init.go +++ b/internal/config/init.go @@ -8,7 +8,7 @@ import ( "sync" "sync/atomic" - "github.com/charmbracelet/crush/internal/logging" + "log/slog" ) const ( @@ -32,7 +32,7 @@ func Init(workingDir string, debug bool) (*Config, error) { cwd = workingDir cfg, err := Load(cwd, debug) if err != nil { - logging.Error("Failed to load config", "error", err) + slog.Error("Failed to load config", "error", err) } instance.Store(cfg) }) diff --git a/internal/config/load.go b/internal/config/load.go index ec3d1c9e650e51629894bedafb9b15186936224d..dfacb5b775fe09b13b5fae10b21b0f3256265019 100644 --- a/internal/config/load.go +++ b/internal/config/load.go @@ -42,6 +42,11 @@ func Load(workingDir string, debug bool) (*Config, error) { filepath.Join(workingDir, fmt.Sprintf(".%s.json", appName)), } cfg, err := loadFromConfigPaths(configPaths) + if err != nil { + return nil, fmt.Errorf("failed to load config from paths %v: %w", configPaths, err) + } + + cfg.setDefaults(workingDir) if debug { cfg.Options.Debug = true @@ -57,8 +62,6 @@ func Load(workingDir string, debug bool) (*Config, error) { return nil, fmt.Errorf("failed to load config: %w", err) } - cfg.setDefaults(workingDir) - // Load known providers, this loads the config from fur providers, err := LoadProviders(client.New()) if err != nil || len(providers) == 0 { diff --git a/internal/db/connect.go b/internal/db/connect.go index 95df2e491d13918d8ea66a3f8312438a42e81b4f..24dc486d2d19bffda6e816e1b915a3f8de3a62ab 100644 --- a/internal/db/connect.go +++ b/internal/db/connect.go @@ -11,7 +11,7 @@ import ( _ "github.com/ncruces/go-sqlite3/embed" "github.com/charmbracelet/crush/internal/config" - "github.com/charmbracelet/crush/internal/logging" + "log/slog" "github.com/pressly/goose/v3" ) @@ -48,21 +48,21 @@ func Connect(ctx context.Context) (*sql.DB, error) { for _, pragma := range pragmas { if _, err = db.ExecContext(ctx, pragma); err != nil { - logging.Error("Failed to set pragma", pragma, err) + slog.Error("Failed to set pragma", pragma, err) } else { - logging.Debug("Set pragma", "pragma", pragma) + slog.Debug("Set pragma", "pragma", pragma) } } goose.SetBaseFS(FS) if err := goose.SetDialect("sqlite3"); err != nil { - logging.Error("Failed to set dialect", "error", err) + slog.Error("Failed to set dialect", "error", err) return nil, fmt.Errorf("failed to set dialect: %w", err) } if err := goose.Up(db, "migrations"); err != nil { - logging.Error("Failed to apply migrations", "error", err) + slog.Error("Failed to apply migrations", "error", err) return nil, fmt.Errorf("failed to apply migrations: %w", err) } return db, nil diff --git a/internal/fsext/fileutil.go b/internal/fsext/fileutil.go index cc430d73edb34cc5b81e1e36ecbad550bf4312fe..3a95bec4ff418eacbefb476ff334ec904eda1c39 100644 --- a/internal/fsext/fileutil.go +++ b/internal/fsext/fileutil.go @@ -11,7 +11,7 @@ import ( "github.com/bmatcuk/doublestar/v4" "github.com/charlievieth/fastwalk" - "github.com/charmbracelet/crush/internal/logging" + "log/slog" ignore "github.com/sabhiram/go-gitignore" ) @@ -24,11 +24,11 @@ func init() { var err error rgPath, err = exec.LookPath("rg") if err != nil { - logging.Warn("Ripgrep (rg) not found in $PATH. Some features might be limited or slower.") + slog.Warn("Ripgrep (rg) not found in $PATH. Some features might be limited or slower.") } fzfPath, err = exec.LookPath("fzf") if err != nil { - logging.Warn("FZF not found in $PATH. Some features might be limited or slower.") + slog.Warn("FZF not found in $PATH. Some features might be limited or slower.") } } diff --git a/internal/llm/agent/agent.go b/internal/llm/agent/agent.go index c515f4b60efb6e1a0a1ea19d848f42eee333d2a9..28044d2bc058a5e07996546eb5a260c20936de34 100644 --- a/internal/llm/agent/agent.go +++ b/internal/llm/agent/agent.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "log/slog" "slices" "strings" "sync" @@ -15,7 +16,7 @@ import ( "github.com/charmbracelet/crush/internal/llm/prompt" "github.com/charmbracelet/crush/internal/llm/provider" "github.com/charmbracelet/crush/internal/llm/tools" - "github.com/charmbracelet/crush/internal/logging" + "github.com/charmbracelet/crush/internal/log" "github.com/charmbracelet/crush/internal/lsp" "github.com/charmbracelet/crush/internal/message" "github.com/charmbracelet/crush/internal/permission" @@ -223,7 +224,7 @@ func (a *agent) Cancel(sessionID string) { // Cancel regular requests if cancelFunc, exists := a.activeRequests.LoadAndDelete(sessionID); exists { if cancel, ok := cancelFunc.(context.CancelFunc); ok { - logging.InfoPersist(fmt.Sprintf("Request cancellation initiated for session: %s", sessionID)) + slog.Info(fmt.Sprintf("Request cancellation initiated for session: %s", sessionID)) cancel() } } @@ -231,7 +232,7 @@ func (a *agent) Cancel(sessionID string) { // Also check for summarize requests if cancelFunc, exists := a.activeRequests.LoadAndDelete(sessionID + "-summarize"); exists { if cancel, ok := cancelFunc.(context.CancelFunc); ok { - logging.InfoPersist(fmt.Sprintf("Summarize cancellation initiated for session: %s", sessionID)) + slog.Info(fmt.Sprintf("Summarize cancellation initiated for session: %s", sessionID)) cancel() } } @@ -325,8 +326,8 @@ func (a *agent) Run(ctx context.Context, sessionID string, content string, attac a.activeRequests.Store(sessionID, cancel) go func() { - logging.Debug("Request started", "sessionID", sessionID) - defer logging.RecoverPanic("agent.Run", func() { + slog.Debug("Request started", "sessionID", sessionID) + defer log.RecoverPanic("agent.Run", func() { events <- a.err(fmt.Errorf("panic while running the agent")) }) var attachmentParts []message.ContentPart @@ -335,9 +336,9 @@ func (a *agent) Run(ctx context.Context, sessionID string, content string, attac } result := a.processGeneration(genCtx, sessionID, content, attachmentParts) if result.Error != nil && !errors.Is(result.Error, ErrRequestCancelled) && !errors.Is(result.Error, context.Canceled) { - logging.ErrorPersist(result.Error.Error()) + slog.Error(result.Error.Error()) } - logging.Debug("Request completed", "sessionID", sessionID) + slog.Debug("Request completed", "sessionID", sessionID) a.activeRequests.Delete(sessionID) cancel() a.Publish(pubsub.CreatedEvent, result) @@ -356,12 +357,12 @@ func (a *agent) processGeneration(ctx context.Context, sessionID, content string } if len(msgs) == 0 { go func() { - defer logging.RecoverPanic("agent.Run", func() { - logging.ErrorPersist("panic while generating title") + defer log.RecoverPanic("agent.Run", func() { + slog.Error("panic while generating title") }) titleErr := a.generateTitle(context.Background(), sessionID, content) if titleErr != nil && !errors.Is(titleErr, context.Canceled) && !errors.Is(titleErr, context.DeadlineExceeded) { - logging.ErrorPersist(fmt.Sprintf("failed to generate title: %v", titleErr)) + slog.Error(fmt.Sprintf("failed to generate title: %v", titleErr)) } }() } @@ -408,11 +409,7 @@ func (a *agent) processGeneration(ctx context.Context, sessionID, content string return a.err(fmt.Errorf("failed to process events: %w", err)) } if cfg.Options.Debug { - seqId := (len(msgHistory) + 1) / 2 - toolResultFilepath := logging.WriteToolResultsJson(sessionID, seqId, toolResults) - logging.Info("Result", "message", agentMessage.FinishReason(), "toolResults", "{}", "filepath", toolResultFilepath) - } else { - logging.Info("Result", "message", agentMessage.FinishReason(), "toolResults", toolResults) + slog.Info("Result", "message", agentMessage.FinishReason(), "toolResults", toolResults) } if (agentMessage.FinishReason() == message.FinishReasonToolUse) && toolResults != nil { // We are not done, we need to respond with the tool response @@ -571,22 +568,22 @@ func (a *agent) processEvent(ctx context.Context, sessionID string, assistantMsg assistantMsg.AppendContent(event.Content) return a.messages.Update(ctx, *assistantMsg) case provider.EventToolUseStart: - logging.Info("Tool call started", "toolCall", event.ToolCall) + slog.Info("Tool call started", "toolCall", event.ToolCall) assistantMsg.AddToolCall(*event.ToolCall) return a.messages.Update(ctx, *assistantMsg) case provider.EventToolUseDelta: assistantMsg.AppendToolCallInput(event.ToolCall.ID, event.ToolCall.Input) return a.messages.Update(ctx, *assistantMsg) case provider.EventToolUseStop: - logging.Info("Finished tool call", "toolCall", event.ToolCall) + slog.Info("Finished tool call", "toolCall", event.ToolCall) assistantMsg.FinishToolCall(event.ToolCall.ID) return a.messages.Update(ctx, *assistantMsg) case provider.EventError: if errors.Is(event.Error, context.Canceled) { - logging.InfoPersist(fmt.Sprintf("Event processing canceled for session: %s", sessionID)) + slog.Info(fmt.Sprintf("Event processing canceled for session: %s", sessionID)) return context.Canceled } - logging.ErrorPersist(event.Error.Error()) + slog.Error(event.Error.Error()) return event.Error case provider.EventComplete: assistantMsg.SetToolCalls(event.Response.ToolCalls) diff --git a/internal/llm/agent/mcp-tools.go b/internal/llm/agent/mcp-tools.go index 5d1bd44d56056051a841e610bdd31e0bf91f2183..5392e4f1e4d4f052c0df07dcbfcc008ada8d8aa4 100644 --- a/internal/llm/agent/mcp-tools.go +++ b/internal/llm/agent/mcp-tools.go @@ -7,7 +7,7 @@ import ( "github.com/charmbracelet/crush/internal/config" "github.com/charmbracelet/crush/internal/llm/tools" - "github.com/charmbracelet/crush/internal/logging" + "log/slog" "github.com/charmbracelet/crush/internal/permission" "github.com/charmbracelet/crush/internal/version" @@ -164,13 +164,13 @@ func getTools(ctx context.Context, name string, m config.MCPConfig, permissions _, err := c.Initialize(ctx, initRequest) if err != nil { - logging.Error("error initializing mcp client", "error", err) + slog.Error("error initializing mcp client", "error", err) return stdioTools } toolsRequest := mcp.ListToolsRequest{} tools, err := c.ListTools(ctx, toolsRequest) if err != nil { - logging.Error("error listing tools", "error", err) + slog.Error("error listing tools", "error", err) return stdioTools } for _, t := range tools.Tools { @@ -193,7 +193,7 @@ func GetMcpTools(ctx context.Context, permissions permission.Service) []tools.Ba m.Args..., ) if err != nil { - logging.Error("error creating mcp client", "error", err) + slog.Error("error creating mcp client", "error", err) continue } @@ -204,7 +204,7 @@ func GetMcpTools(ctx context.Context, permissions permission.Service) []tools.Ba transport.WithHTTPHeaders(m.Headers), ) if err != nil { - logging.Error("error creating mcp client", "error", err) + slog.Error("error creating mcp client", "error", err) continue } mcpTools = append(mcpTools, getTools(ctx, name, m, permissions, c)...) @@ -214,7 +214,7 @@ func GetMcpTools(ctx context.Context, permissions permission.Service) []tools.Ba client.WithHeaders(m.Headers), ) if err != nil { - logging.Error("error creating mcp client", "error", err) + slog.Error("error creating mcp client", "error", err) continue } mcpTools = append(mcpTools, getTools(ctx, name, m, permissions, c)...) diff --git a/internal/llm/prompt/coder.go b/internal/llm/prompt/coder.go index b85b084e9954ad1afa35ebf58acfcc299038db2b..2ac17247123bcf283a453bbbb8ccec8776dc5917 100644 --- a/internal/llm/prompt/coder.go +++ b/internal/llm/prompt/coder.go @@ -11,7 +11,7 @@ import ( "github.com/charmbracelet/crush/internal/config" "github.com/charmbracelet/crush/internal/fur/provider" "github.com/charmbracelet/crush/internal/llm/tools" - "github.com/charmbracelet/crush/internal/logging" + "log/slog" ) func CoderPrompt(p string, contextFiles ...string) string { @@ -29,7 +29,7 @@ func CoderPrompt(p string, contextFiles ...string) string { basePrompt = fmt.Sprintf("%s\n\n%s\n%s", basePrompt, envInfo, lspInformation()) contextContent := getContextFromPaths(contextFiles) - logging.Debug("Context content", "Context", contextContent) + slog.Debug("Context content", "Context", contextContent) if contextContent != "" { return fmt.Sprintf("%s\n\n# Project-Specific Context\n Make sure to follow the instructions in the context below\n%s", basePrompt, contextContent) } diff --git a/internal/llm/provider/anthropic.go b/internal/llm/provider/anthropic.go index 4ed18d7b6595bfd28c4b02e473a186c21c0f84eb..380d0e3083786b628d726a3310c01fd817f237fe 100644 --- a/internal/llm/provider/anthropic.go +++ b/internal/llm/provider/anthropic.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "io" + "log/slog" "regexp" "strconv" "time" @@ -16,7 +17,6 @@ import ( "github.com/charmbracelet/crush/internal/config" "github.com/charmbracelet/crush/internal/fur/provider" "github.com/charmbracelet/crush/internal/llm/tools" - "github.com/charmbracelet/crush/internal/logging" "github.com/charmbracelet/crush/internal/message" ) @@ -92,7 +92,7 @@ func (a *anthropicClient) convertMessages(messages []message.Message) (anthropic } if len(blocks) == 0 { - logging.Warn("There is a message without content, investigate, this should not happen") + slog.Warn("There is a message without content, investigate, this should not happen") continue } anthropicMessages = append(anthropicMessages, anthropic.NewAssistantMessage(blocks...)) @@ -207,7 +207,7 @@ func (a *anthropicClient) send(ctx context.Context, messages []message.Message, preparedMessages := a.preparedMessages(a.convertMessages(messages), a.convertTools(tools)) if cfg.Options.Debug { jsonData, _ := json.Marshal(preparedMessages) - logging.Debug("Prepared messages", "messages", string(jsonData)) + slog.Debug("Prepared messages", "messages", string(jsonData)) } anthropicResponse, err := a.client.Messages.New( @@ -216,13 +216,13 @@ func (a *anthropicClient) send(ctx context.Context, messages []message.Message, ) // If there is an error we are going to see if we can retry the call if err != nil { - logging.Error("Error in Anthropic API call", "error", err) + slog.Error("Error in Anthropic API call", "error", err) retry, after, retryErr := a.shouldRetry(attempts, err) if retryErr != nil { return nil, retryErr } if retry { - logging.WarnPersist(fmt.Sprintf("Retrying due to rate limit... attempt %d of %d", attempts, maxRetries), logging.PersistTimeArg, time.Millisecond*time.Duration(after+100)) + slog.Warn(fmt.Sprintf("Retrying due to rate limit... attempt %d of %d", attempts, maxRetries)) select { case <-ctx.Done(): return nil, ctx.Err() @@ -259,7 +259,7 @@ func (a *anthropicClient) stream(ctx context.Context, messages []message.Message preparedMessages := a.preparedMessages(a.convertMessages(messages), a.convertTools(tools)) if cfg.Options.Debug { jsonData, _ := json.Marshal(preparedMessages) - logging.Debug("Prepared messages", "messages", string(jsonData)) + slog.Debug("Prepared messages", "messages", string(jsonData)) } anthropicStream := a.client.Messages.NewStreaming( @@ -273,7 +273,7 @@ func (a *anthropicClient) stream(ctx context.Context, messages []message.Message event := anthropicStream.Current() err := accumulatedMessage.Accumulate(event) if err != nil { - logging.Warn("Error accumulating message", "error", err) + slog.Warn("Error accumulating message", "error", err) continue } @@ -364,7 +364,7 @@ func (a *anthropicClient) stream(ctx context.Context, messages []message.Message return } if retry { - logging.WarnPersist(fmt.Sprintf("Retrying due to rate limit... attempt %d of %d", attempts, maxRetries), logging.PersistTimeArg, time.Millisecond*time.Duration(after+100)) + slog.Warn(fmt.Sprintf("Retrying due to rate limit... attempt %d of %d", attempts, maxRetries)) select { case <-ctx.Done(): // context cancelled @@ -411,7 +411,7 @@ func (a *anthropicClient) shouldRetry(attempts int, err error) (bool, int64, err if apiErr.StatusCode == 400 { if adjusted, ok := a.handleContextLimitError(apiErr); ok { a.adjustedMaxTokens = adjusted - logging.Debug("Adjusted max_tokens due to context limit", "new_max_tokens", adjusted) + slog.Debug("Adjusted max_tokens due to context limit", "new_max_tokens", adjusted) return true, 0, nil } } diff --git a/internal/llm/provider/gemini.go b/internal/llm/provider/gemini.go index 7e9fdbd405dbf64c58873a6e1cfc108e9d4b4f7f..30da47326df114ad9d6b87f1d53712af2b463646 100644 --- a/internal/llm/provider/gemini.go +++ b/internal/llm/provider/gemini.go @@ -6,13 +6,13 @@ import ( "errors" "fmt" "io" + "log/slog" "strings" "time" "github.com/charmbracelet/crush/internal/config" "github.com/charmbracelet/crush/internal/fur/provider" "github.com/charmbracelet/crush/internal/llm/tools" - "github.com/charmbracelet/crush/internal/logging" "github.com/charmbracelet/crush/internal/message" "github.com/google/uuid" "google.golang.org/genai" @@ -28,7 +28,7 @@ type GeminiClient ProviderClient func newGeminiClient(opts providerClientOptions) GeminiClient { client, err := createGeminiClient(opts) if err != nil { - logging.Error("Failed to create Gemini client", "error", err) + slog.Error("Failed to create Gemini client", "error", err) return nil } @@ -168,7 +168,7 @@ func (g *geminiClient) send(ctx context.Context, messages []message.Message, too cfg := config.Get() if cfg.Options.Debug { jsonData, _ := json.Marshal(geminiMessages) - logging.Debug("Prepared messages", "messages", string(jsonData)) + slog.Debug("Prepared messages", "messages", string(jsonData)) } modelConfig := cfg.Models[config.SelectedModelTypeLarge] @@ -210,7 +210,7 @@ func (g *geminiClient) send(ctx context.Context, messages []message.Message, too return nil, retryErr } if retry { - logging.WarnPersist(fmt.Sprintf("Retrying due to rate limit... attempt %d of %d", attempts, maxRetries), logging.PersistTimeArg, time.Millisecond*time.Duration(after+100)) + slog.Warn(fmt.Sprintf("Retrying due to rate limit... attempt %d of %d", attempts, maxRetries)) select { case <-ctx.Done(): return nil, ctx.Err() @@ -266,7 +266,7 @@ func (g *geminiClient) stream(ctx context.Context, messages []message.Message, t cfg := config.Get() if cfg.Options.Debug { jsonData, _ := json.Marshal(geminiMessages) - logging.Debug("Prepared messages", "messages", string(jsonData)) + slog.Debug("Prepared messages", "messages", string(jsonData)) } modelConfig := cfg.Models[config.SelectedModelTypeLarge] @@ -323,7 +323,7 @@ func (g *geminiClient) stream(ctx context.Context, messages []message.Message, t return } if retry { - logging.WarnPersist(fmt.Sprintf("Retrying due to rate limit... attempt %d of %d", attempts, maxRetries), logging.PersistTimeArg, time.Millisecond*time.Duration(after+100)) + slog.Warn(fmt.Sprintf("Retrying due to rate limit... attempt %d of %d", attempts, maxRetries)) select { case <-ctx.Done(): if ctx.Err() != nil { diff --git a/internal/llm/provider/openai.go b/internal/llm/provider/openai.go index 6fcc0b25bb2f0721d3a46f0f0bcb32589e477816..c51762a868a1f8e2a14b20788ee28ae891cbb9d8 100644 --- a/internal/llm/provider/openai.go +++ b/internal/llm/provider/openai.go @@ -6,12 +6,12 @@ import ( "errors" "fmt" "io" + "log/slog" "time" "github.com/charmbracelet/crush/internal/config" "github.com/charmbracelet/crush/internal/fur/provider" "github.com/charmbracelet/crush/internal/llm/tools" - "github.com/charmbracelet/crush/internal/logging" "github.com/charmbracelet/crush/internal/message" "github.com/openai/openai-go" "github.com/openai/openai-go/option" @@ -194,7 +194,7 @@ func (o *openaiClient) send(ctx context.Context, messages []message.Message, too cfg := config.Get() if cfg.Options.Debug { jsonData, _ := json.Marshal(params) - logging.Debug("Prepared messages", "messages", string(jsonData)) + slog.Debug("Prepared messages", "messages", string(jsonData)) } attempts := 0 for { @@ -210,7 +210,7 @@ func (o *openaiClient) send(ctx context.Context, messages []message.Message, too return nil, retryErr } if retry { - logging.WarnPersist(fmt.Sprintf("Retrying due to rate limit... attempt %d of %d", attempts, maxRetries), logging.PersistTimeArg, time.Millisecond*time.Duration(after+100)) + slog.Warn(fmt.Sprintf("Retrying due to rate limit... attempt %d of %d", attempts, maxRetries)) select { case <-ctx.Done(): return nil, ctx.Err() @@ -251,7 +251,7 @@ func (o *openaiClient) stream(ctx context.Context, messages []message.Message, t cfg := config.Get() if cfg.Options.Debug { jsonData, _ := json.Marshal(params) - logging.Debug("Prepared messages", "messages", string(jsonData)) + slog.Debug("Prepared messages", "messages", string(jsonData)) } attempts := 0 @@ -288,7 +288,7 @@ func (o *openaiClient) stream(ctx context.Context, messages []message.Message, t if err == nil || errors.Is(err, io.EOF) { if cfg.Options.Debug { jsonData, _ := json.Marshal(acc.ChatCompletion) - logging.Debug("Response", "messages", string(jsonData)) + slog.Debug("Response", "messages", string(jsonData)) } resultFinishReason := acc.ChatCompletion.Choices[0].FinishReason if resultFinishReason == "" { @@ -326,7 +326,7 @@ func (o *openaiClient) stream(ctx context.Context, messages []message.Message, t return } if retry { - logging.WarnPersist(fmt.Sprintf("Retrying due to rate limit... attempt %d of %d", attempts, maxRetries), logging.PersistTimeArg, time.Millisecond*time.Duration(after+100)) + slog.Warn(fmt.Sprintf("Retrying due to rate limit... attempt %d of %d", attempts, maxRetries)) select { case <-ctx.Done(): // context cancelled diff --git a/internal/llm/provider/vertexai.go b/internal/llm/provider/vertexai.go index 2d95ad3f60db22e1338db3931b0900e83bccab52..76f21eef1c3f6ac132451d53b0852e584ef688f2 100644 --- a/internal/llm/provider/vertexai.go +++ b/internal/llm/provider/vertexai.go @@ -3,7 +3,7 @@ package provider import ( "context" - "github.com/charmbracelet/crush/internal/logging" + "log/slog" "google.golang.org/genai" ) @@ -18,7 +18,7 @@ func newVertexAIClient(opts providerClientOptions) VertexAIClient { Backend: genai.BackendVertexAI, }) if err != nil { - logging.Error("Failed to create VertexAI client", "error", err) + slog.Error("Failed to create VertexAI client", "error", err) return nil } diff --git a/internal/llm/tools/edit.go b/internal/llm/tools/edit.go index 1602f65ea109e9e7ad0468687ba24ce674fcaea9..3d8775b287da28c072ecd66aba38054581c2da4c 100644 --- a/internal/llm/tools/edit.go +++ b/internal/llm/tools/edit.go @@ -12,7 +12,7 @@ import ( "github.com/charmbracelet/crush/internal/config" "github.com/charmbracelet/crush/internal/diff" "github.com/charmbracelet/crush/internal/history" - "github.com/charmbracelet/crush/internal/logging" + "log/slog" "github.com/charmbracelet/crush/internal/lsp" "github.com/charmbracelet/crush/internal/permission" ) @@ -246,7 +246,7 @@ func (e *editTool) createNewFile(ctx context.Context, filePath, content string) _, err = e.files.CreateVersion(ctx, sessionID, filePath, content) if err != nil { // Log error but don't fail the operation - logging.Debug("Error creating file history version", "error", err) + slog.Debug("Error creating file history version", "error", err) } recordFileWrite(filePath) @@ -361,13 +361,13 @@ func (e *editTool) deleteContent(ctx context.Context, filePath, oldString string // User Manually changed the content store an intermediate version _, err = e.files.CreateVersion(ctx, sessionID, filePath, oldContent) if err != nil { - logging.Debug("Error creating file history version", "error", err) + slog.Debug("Error creating file history version", "error", err) } } // Store the new version _, err = e.files.CreateVersion(ctx, sessionID, filePath, "") if err != nil { - logging.Debug("Error creating file history version", "error", err) + slog.Debug("Error creating file history version", "error", err) } recordFileWrite(filePath) @@ -483,13 +483,13 @@ func (e *editTool) replaceContent(ctx context.Context, filePath, oldString, newS // User Manually changed the content store an intermediate version _, err = e.files.CreateVersion(ctx, sessionID, filePath, oldContent) if err != nil { - logging.Debug("Error creating file history version", "error", err) + slog.Debug("Error creating file history version", "error", err) } } // Store the new version _, err = e.files.CreateVersion(ctx, sessionID, filePath, newContent) if err != nil { - logging.Debug("Error creating file history version", "error", err) + slog.Debug("Error creating file history version", "error", err) } recordFileWrite(filePath) diff --git a/internal/llm/tools/glob.go b/internal/llm/tools/glob.go index 6a8ba40208b0d59b9034d8502ff576d7888481ca..535220bb691002d77e75fcfe405e392de86272f5 100644 --- a/internal/llm/tools/glob.go +++ b/internal/llm/tools/glob.go @@ -12,7 +12,7 @@ import ( "github.com/charmbracelet/crush/internal/config" "github.com/charmbracelet/crush/internal/fsext" - "github.com/charmbracelet/crush/internal/logging" + "log/slog" ) const ( @@ -143,7 +143,7 @@ func globFiles(pattern, searchPath string, limit int) ([]string, bool, error) { if err == nil { return matches, len(matches) >= limit && limit > 0, nil } - logging.Warn(fmt.Sprintf("Ripgrep execution failed: %v. Falling back to doublestar.", err)) + slog.Warn(fmt.Sprintf("Ripgrep execution failed: %v. Falling back to doublestar.", err)) } return fsext.GlobWithDoubleStar(pattern, searchPath, limit) diff --git a/internal/llm/tools/write.go b/internal/llm/tools/write.go index 676b6e02b7c0cdae28b2e256f664a339315b0eb9..95bd74787534cd3d3638cfc92427008f51708148 100644 --- a/internal/llm/tools/write.go +++ b/internal/llm/tools/write.go @@ -12,7 +12,7 @@ import ( "github.com/charmbracelet/crush/internal/config" "github.com/charmbracelet/crush/internal/diff" "github.com/charmbracelet/crush/internal/history" - "github.com/charmbracelet/crush/internal/logging" + "log/slog" "github.com/charmbracelet/crush/internal/lsp" "github.com/charmbracelet/crush/internal/permission" ) @@ -211,13 +211,13 @@ func (w *writeTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error // User Manually changed the content store an intermediate version _, err = w.files.CreateVersion(ctx, sessionID, filePath, oldContent) if err != nil { - logging.Debug("Error creating file history version", "error", err) + slog.Debug("Error creating file history version", "error", err) } } // Store the new version _, err = w.files.CreateVersion(ctx, sessionID, filePath, params.Content) if err != nil { - logging.Debug("Error creating file history version", "error", err) + slog.Debug("Error creating file history version", "error", err) } recordFileWrite(filePath) diff --git a/internal/log/log.go b/internal/log/log.go index 11174a5071c72b6773cec03d3c849d4faff9bc39..31c183e9d7274d35ad2d70c7c1f5418586bc5a2d 100644 --- a/internal/log/log.go +++ b/internal/log/log.go @@ -1,8 +1,12 @@ package log import ( + "fmt" "log/slog" + "os" + "runtime/debug" "sync" + "time" "gopkg.in/natefinch/lumberjack.v2" ) @@ -32,3 +36,26 @@ func Init(logFile string, debug bool) { slog.SetDefault(slog.New(logger)) }) } + +func RecoverPanic(name string, cleanup func()) { + if r := recover(); r != nil { + // Create a timestamped panic log file + timestamp := time.Now().Format("20060102-150405") + filename := fmt.Sprintf("crush-panic-%s-%s.log", name, timestamp) + + file, err := os.Create(filename) + if err == nil { + defer file.Close() + + // 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()) + + // Execute cleanup function if provided + if cleanup != nil { + cleanup() + } + } + } +} diff --git a/internal/logging/logger.go b/internal/logging/logger.go deleted file mode 100644 index ef33ba299855f24f0b48ae5af9ed80a069ba7a31..0000000000000000000000000000000000000000 --- a/internal/logging/logger.go +++ /dev/null @@ -1,209 +0,0 @@ -package logging - -import ( - "fmt" - "log/slog" - "os" - - // "path/filepath" - "encoding/json" - "runtime" - "runtime/debug" - "sync" - "time" -) - -func getCaller() string { - var caller string - if _, file, line, ok := runtime.Caller(2); ok { - // caller = fmt.Sprintf("%s:%d", filepath.Base(file), line) - caller = fmt.Sprintf("%s:%d", file, line) - } else { - caller = "unknown" - } - return caller -} - -func Info(msg string, args ...any) { - source := getCaller() - slog.Info(msg, append([]any{"source", source}, args...)...) -} - -func Debug(msg string, args ...any) { - // slog.Debug(msg, args...) - source := getCaller() - slog.Debug(msg, append([]any{"source", source}, args...)...) -} - -func Warn(msg string, args ...any) { - slog.Warn(msg, args...) -} - -func Error(msg string, args ...any) { - slog.Error(msg, args...) -} - -func InfoPersist(msg string, args ...any) { - args = append(args, persistKeyArg, true) - slog.Info(msg, args...) -} - -func DebugPersist(msg string, args ...any) { - args = append(args, persistKeyArg, true) - slog.Debug(msg, args...) -} - -func WarnPersist(msg string, args ...any) { - args = append(args, persistKeyArg, true) - slog.Warn(msg, args...) -} - -func ErrorPersist(msg string, args ...any) { - args = append(args, persistKeyArg, true) - slog.Error(msg, args...) -} - -// RecoverPanic is a common function to handle panics gracefully. -// It logs the error, creates a panic log file with stack trace, -// and executes an optional cleanup function before returning. -func RecoverPanic(name string, cleanup func()) { - if r := recover(); r != nil { - // Log the panic - ErrorPersist(fmt.Sprintf("Panic in %s: %v", name, r)) - - // Create a timestamped panic log file - timestamp := time.Now().Format("20060102-150405") - filename := fmt.Sprintf("crush-panic-%s-%s.log", name, timestamp) - - file, err := os.Create(filename) - if err != nil { - ErrorPersist(fmt.Sprintf("Failed to create panic log: %v", err)) - } else { - defer file.Close() - - // 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()) - - InfoPersist(fmt.Sprintf("Panic details written to %s", filename)) - } - - // Execute cleanup function if provided - if cleanup != nil { - cleanup() - } - } -} - -// Message Logging for Debug -var MessageDir string - -func GetSessionPrefix(sessionId string) string { - return sessionId[:8] -} - -var sessionLogMutex sync.Mutex - -func AppendToSessionLogFile(sessionId string, filename string, content string) string { - if MessageDir == "" || sessionId == "" { - return "" - } - sessionPrefix := GetSessionPrefix(sessionId) - - sessionLogMutex.Lock() - defer sessionLogMutex.Unlock() - - sessionPath := fmt.Sprintf("%s/%s", MessageDir, sessionPrefix) - if _, err := os.Stat(sessionPath); os.IsNotExist(err) { - if err := os.MkdirAll(sessionPath, 0o766); err != nil { - Error("Failed to create session directory", "dirpath", sessionPath, "error", err) - return "" - } - } - - filePath := fmt.Sprintf("%s/%s", sessionPath, filename) - - f, err := os.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) - if err != nil { - Error("Failed to open session log file", "filepath", filePath, "error", err) - return "" - } - defer f.Close() - - // Append chunk to file - _, err = f.WriteString(content) - if err != nil { - Error("Failed to write chunk to session log file", "filepath", filePath, "error", err) - return "" - } - return filePath -} - -func WriteRequestMessageJson(sessionId string, requestSeqId int, message any) string { - if MessageDir == "" || sessionId == "" || requestSeqId <= 0 { - return "" - } - msgJson, err := json.Marshal(message) - if err != nil { - Error("Failed to marshal message", "session_id", sessionId, "request_seq_id", requestSeqId, "error", err) - return "" - } - return WriteRequestMessage(sessionId, requestSeqId, string(msgJson)) -} - -func WriteRequestMessage(sessionId string, requestSeqId int, message string) string { - if MessageDir == "" || sessionId == "" || requestSeqId <= 0 { - return "" - } - filename := fmt.Sprintf("%d_request.json", requestSeqId) - - return AppendToSessionLogFile(sessionId, filename, message) -} - -func AppendToStreamSessionLogJson(sessionId string, requestSeqId int, jsonableChunk any) string { - if MessageDir == "" || sessionId == "" || requestSeqId <= 0 { - return "" - } - chunkJson, err := json.Marshal(jsonableChunk) - if err != nil { - Error("Failed to marshal message", "session_id", sessionId, "request_seq_id", requestSeqId, "error", err) - return "" - } - return AppendToStreamSessionLog(sessionId, requestSeqId, string(chunkJson)) -} - -func AppendToStreamSessionLog(sessionId string, requestSeqId int, chunk string) string { - if MessageDir == "" || sessionId == "" || requestSeqId <= 0 { - return "" - } - filename := fmt.Sprintf("%d_response_stream.log", requestSeqId) - return AppendToSessionLogFile(sessionId, filename, chunk) -} - -func WriteChatResponseJson(sessionId string, requestSeqId int, response any) string { - if MessageDir == "" || sessionId == "" || requestSeqId <= 0 { - return "" - } - responseJson, err := json.Marshal(response) - if err != nil { - Error("Failed to marshal response", "session_id", sessionId, "request_seq_id", requestSeqId, "error", err) - return "" - } - filename := fmt.Sprintf("%d_response.json", requestSeqId) - - return AppendToSessionLogFile(sessionId, filename, string(responseJson)) -} - -func WriteToolResultsJson(sessionId string, requestSeqId int, toolResults any) string { - if MessageDir == "" || sessionId == "" || requestSeqId <= 0 { - return "" - } - toolResultsJson, err := json.Marshal(toolResults) - if err != nil { - Error("Failed to marshal tool results", "session_id", sessionId, "request_seq_id", requestSeqId, "error", err) - return "" - } - filename := fmt.Sprintf("%d_tool_results.json", requestSeqId) - return AppendToSessionLogFile(sessionId, filename, string(toolResultsJson)) -} diff --git a/internal/logging/message.go b/internal/logging/message.go deleted file mode 100644 index 30ae8f379ed5a64c30a6f3a84ef25fafd59850d7..0000000000000000000000000000000000000000 --- a/internal/logging/message.go +++ /dev/null @@ -1,21 +0,0 @@ -package logging - -import ( - "time" -) - -// LogMessage is the event payload for a log message -type LogMessage struct { - ID string - Time time.Time - Level string - Persist bool // used when we want to show the mesage in the status bar - PersistTime time.Duration // used when we want to show the mesage in the status bar - Message string `json:"msg"` - Attributes []Attr -} - -type Attr struct { - Key string - Value string -} diff --git a/internal/logging/writer.go b/internal/logging/writer.go deleted file mode 100644 index e821338658a316ad0ffb6178e42c046addbfd1ab..0000000000000000000000000000000000000000 --- a/internal/logging/writer.go +++ /dev/null @@ -1,102 +0,0 @@ -package logging - -import ( - "bytes" - "context" - "fmt" - "strings" - "sync" - "time" - - "github.com/charmbracelet/crush/internal/pubsub" - "github.com/go-logfmt/logfmt" -) - -const ( - persistKeyArg = "$_persist" - PersistTimeArg = "$_persist_time" -) - -type LogData struct { - messages []LogMessage - *pubsub.Broker[LogMessage] - lock sync.Mutex -} - -func (l *LogData) Add(msg LogMessage) { - l.lock.Lock() - defer l.lock.Unlock() - l.messages = append(l.messages, msg) - l.Publish(pubsub.CreatedEvent, msg) -} - -func (l *LogData) List() []LogMessage { - l.lock.Lock() - defer l.lock.Unlock() - return l.messages -} - -var defaultLogData = &LogData{ - messages: make([]LogMessage, 0), - Broker: pubsub.NewBroker[LogMessage](), -} - -type writer struct{} - -func (w *writer) Write(p []byte) (int, error) { - d := logfmt.NewDecoder(bytes.NewReader(p)) - - for d.ScanRecord() { - msg := LogMessage{ - ID: fmt.Sprintf("%d", time.Now().UnixNano()), - Time: time.Now(), - } - for d.ScanKeyval() { - switch string(d.Key()) { - case "time": - parsed, err := time.Parse(time.RFC3339, string(d.Value())) - if err != nil { - return 0, fmt.Errorf("parsing time: %w", err) - } - msg.Time = parsed - case "level": - msg.Level = strings.ToLower(string(d.Value())) - case "msg": - msg.Message = string(d.Value()) - default: - if string(d.Key()) == persistKeyArg { - msg.Persist = true - } else if string(d.Key()) == PersistTimeArg { - parsed, err := time.ParseDuration(string(d.Value())) - if err != nil { - continue - } - msg.PersistTime = parsed - } else { - msg.Attributes = append(msg.Attributes, Attr{ - Key: string(d.Key()), - Value: string(d.Value()), - }) - } - } - } - defaultLogData.Add(msg) - } - if d.Err() != nil { - return 0, d.Err() - } - return len(p), nil -} - -func NewWriter() *writer { - w := &writer{} - return w -} - -func Subscribe(ctx context.Context) <-chan pubsub.Event[LogMessage] { - return defaultLogData.Subscribe(ctx) -} - -func List() []LogMessage { - return defaultLogData.List() -} diff --git a/internal/lsp/client.go b/internal/lsp/client.go index 0fec0c7d79fa64abba9b8c5a9650568d55334450..f0ace18e67e133bb5eb5b824c543cfc9fc07bab1 100644 --- a/internal/lsp/client.go +++ b/internal/lsp/client.go @@ -6,6 +6,7 @@ import ( "encoding/json" "fmt" "io" + "log/slog" "os" "os/exec" "path/filepath" @@ -15,7 +16,7 @@ import ( "time" "github.com/charmbracelet/crush/internal/config" - "github.com/charmbracelet/crush/internal/logging" + "github.com/charmbracelet/crush/internal/log" "github.com/charmbracelet/crush/internal/lsp/protocol" ) @@ -96,17 +97,17 @@ func NewClient(ctx context.Context, command string, args ...string) (*Client, er go func() { scanner := bufio.NewScanner(stderr) for scanner.Scan() { - logging.Error("LSP Server", "err", scanner.Text()) + slog.Error("LSP Server", "err", scanner.Text()) } if err := scanner.Err(); err != nil { - logging.Error("Error reading", "err", err) + slog.Error("Error reading", "err", err) } }() // Start message handling loop go func() { - defer logging.RecoverPanic("LSP-message-handler", func() { - logging.ErrorPersist("LSP message handler crashed, LSP functionality may be impaired") + defer log.RecoverPanic("LSP-message-handler", func() { + slog.Error("LSP message handler crashed, LSP functionality may be impaired") }) client.handleMessages() }() @@ -300,7 +301,7 @@ func (c *Client) WaitForServerReady(ctx context.Context) error { defer ticker.Stop() if cfg.Options.DebugLSP { - logging.Debug("Waiting for LSP server to be ready...") + slog.Debug("Waiting for LSP server to be ready...") } // Determine server type for specialized initialization @@ -309,7 +310,7 @@ func (c *Client) WaitForServerReady(ctx context.Context) error { // For TypeScript-like servers, we need to open some key files first if serverType == ServerTypeTypeScript { if cfg.Options.DebugLSP { - logging.Debug("TypeScript-like server detected, opening key configuration files") + slog.Debug("TypeScript-like server detected, opening key configuration files") } c.openKeyConfigFiles(ctx) } @@ -326,15 +327,15 @@ func (c *Client) WaitForServerReady(ctx context.Context) error { // Server responded successfully c.SetServerState(StateReady) if cfg.Options.DebugLSP { - logging.Debug("LSP server is ready") + slog.Debug("LSP server is ready") } return nil } else { - logging.Debug("LSP server not ready yet", "error", err, "serverType", serverType) + slog.Debug("LSP server not ready yet", "error", err, "serverType", serverType) } if cfg.Options.DebugLSP { - logging.Debug("LSP server not ready yet", "error", err, "serverType", serverType) + slog.Debug("LSP server not ready yet", "error", err, "serverType", serverType) } } } @@ -409,9 +410,9 @@ func (c *Client) openKeyConfigFiles(ctx context.Context) { if _, err := os.Stat(file); err == nil { // File exists, try to open it if err := c.OpenFile(ctx, file); err != nil { - logging.Debug("Failed to open key config file", "file", file, "error", err) + slog.Debug("Failed to open key config file", "file", file, "error", err) } else { - logging.Debug("Opened key config file for initialization", "file", file) + slog.Debug("Opened key config file for initialization", "file", file) } } } @@ -487,7 +488,7 @@ func (c *Client) pingTypeScriptServer(ctx context.Context) error { return nil }) if err != nil { - logging.Debug("Error walking directory for TypeScript files", "error", err) + slog.Debug("Error walking directory for TypeScript files", "error", err) } // Final fallback - just try a generic capability @@ -527,7 +528,7 @@ func (c *Client) openTypeScriptFiles(ctx context.Context, workDir string) { if err := c.OpenFile(ctx, path); err == nil { filesOpened++ if cfg.Options.DebugLSP { - logging.Debug("Opened TypeScript file for initialization", "file", path) + slog.Debug("Opened TypeScript file for initialization", "file", path) } } } @@ -536,11 +537,11 @@ func (c *Client) openTypeScriptFiles(ctx context.Context, workDir string) { }) if err != nil && cfg.Options.DebugLSP { - logging.Debug("Error walking directory for TypeScript files", "error", err) + slog.Debug("Error walking directory for TypeScript files", "error", err) } if cfg.Options.DebugLSP { - logging.Debug("Opened TypeScript files for initialization", "count", filesOpened) + slog.Debug("Opened TypeScript files for initialization", "count", filesOpened) } } @@ -681,7 +682,7 @@ func (c *Client) CloseFile(ctx context.Context, filepath string) error { } if cfg.Options.DebugLSP { - logging.Debug("Closing file", "file", filepath) + slog.Debug("Closing file", "file", filepath) } if err := c.Notify(ctx, "textDocument/didClose", params); err != nil { return err @@ -720,12 +721,12 @@ func (c *Client) CloseAllFiles(ctx context.Context) { for _, filePath := range filesToClose { err := c.CloseFile(ctx, filePath) if err != nil && cfg.Options.DebugLSP { - logging.Warn("Error closing file", "file", filePath, "error", err) + slog.Warn("Error closing file", "file", filePath, "error", err) } } if cfg.Options.DebugLSP { - logging.Debug("Closed all files", "files", filesToClose) + slog.Debug("Closed all files", "files", filesToClose) } } diff --git a/internal/lsp/handlers.go b/internal/lsp/handlers.go index f2fbfd0a589651590185fe9f73fc222e5bd6b08d..b85e208eb9bc9c19b02a67d01e64e9ef88a9668b 100644 --- a/internal/lsp/handlers.go +++ b/internal/lsp/handlers.go @@ -4,7 +4,7 @@ import ( "encoding/json" "github.com/charmbracelet/crush/internal/config" - "github.com/charmbracelet/crush/internal/logging" + "log/slog" "github.com/charmbracelet/crush/internal/lsp/protocol" "github.com/charmbracelet/crush/internal/lsp/util" ) @@ -18,7 +18,7 @@ func HandleWorkspaceConfiguration(params json.RawMessage) (any, error) { func HandleRegisterCapability(params json.RawMessage) (any, error) { var registerParams protocol.RegistrationParams if err := json.Unmarshal(params, ®isterParams); err != nil { - logging.Error("Error unmarshaling registration params", "error", err) + slog.Error("Error unmarshaling registration params", "error", err) return nil, err } @@ -28,13 +28,13 @@ func HandleRegisterCapability(params json.RawMessage) (any, error) { // Parse the registration options optionsJSON, err := json.Marshal(reg.RegisterOptions) if err != nil { - logging.Error("Error marshaling registration options", "error", err) + slog.Error("Error marshaling registration options", "error", err) continue } var options protocol.DidChangeWatchedFilesRegistrationOptions if err := json.Unmarshal(optionsJSON, &options); err != nil { - logging.Error("Error unmarshaling registration options", "error", err) + slog.Error("Error unmarshaling registration options", "error", err) continue } @@ -54,7 +54,7 @@ func HandleApplyEdit(params json.RawMessage) (any, error) { err := util.ApplyWorkspaceEdit(edit.Edit) if err != nil { - logging.Error("Error applying workspace edit", "error", err) + slog.Error("Error applying workspace edit", "error", err) return protocol.ApplyWorkspaceEditResult{Applied: false, FailureReason: err.Error()}, nil } @@ -89,7 +89,7 @@ func HandleServerMessage(params json.RawMessage) { } if err := json.Unmarshal(params, &msg); err == nil { if cfg.Options.DebugLSP { - logging.Debug("Server message", "type", msg.Type, "message", msg.Message) + slog.Debug("Server message", "type", msg.Type, "message", msg.Message) } } } @@ -97,7 +97,7 @@ func HandleServerMessage(params json.RawMessage) { func HandleDiagnostics(client *Client, params json.RawMessage) { var diagParams protocol.PublishDiagnosticsParams if err := json.Unmarshal(params, &diagParams); err != nil { - logging.Error("Error unmarshaling diagnostics params", "error", err) + slog.Error("Error unmarshaling diagnostics params", "error", err) return } diff --git a/internal/lsp/protocol/tsprotocol.go b/internal/lsp/protocol/tsprotocol.go index 7f60e6f1b0e6d9b40add8114d609d36d5c3188be..ce4e58d4785275337ac5bfb784b596fe2d832b28 100644 --- a/internal/lsp/protocol/tsprotocol.go +++ b/internal/lsp/protocol/tsprotocol.go @@ -55,7 +55,7 @@ type ApplyWorkspaceEditResult struct { // Indicates whether the edit was applied or not. Applied bool `json:"applied"` // An optional textual description for why the edit was not applied. - // This may be used by the server for diagnostic logging or to provide + // This may be used by the server for diagnostic slog.or to provide // a suitable error for a request that triggered the edit. FailureReason string `json:"failureReason,omitempty"` // Depending on the client's failure handling strategy `failedChange` might diff --git a/internal/lsp/transport.go b/internal/lsp/transport.go index 5433fb552d6ee3dae390dcf74e3e1d9c8b0d74f9..527c4dbd0748fdfbca75b6506d48337fed4c8bfb 100644 --- a/internal/lsp/transport.go +++ b/internal/lsp/transport.go @@ -9,7 +9,7 @@ import ( "strings" "github.com/charmbracelet/crush/internal/config" - "github.com/charmbracelet/crush/internal/logging" + "log/slog" ) // Write writes an LSP message to the given writer @@ -21,7 +21,7 @@ func WriteMessage(w io.Writer, msg *Message) error { cfg := config.Get() if cfg.Options.DebugLSP { - logging.Debug("Sending message to server", "method", msg.Method, "id", msg.ID) + slog.Debug("Sending message to server", "method", msg.Method, "id", msg.ID) } _, err = fmt.Fprintf(w, "Content-Length: %d\r\n\r\n", len(data)) @@ -50,7 +50,7 @@ func ReadMessage(r *bufio.Reader) (*Message, error) { line = strings.TrimSpace(line) if cfg.Options.DebugLSP { - logging.Debug("Received header", "line", line) + slog.Debug("Received header", "line", line) } if line == "" { @@ -66,7 +66,7 @@ func ReadMessage(r *bufio.Reader) (*Message, error) { } if cfg.Options.DebugLSP { - logging.Debug("Content-Length", "length", contentLength) + slog.Debug("Content-Length", "length", contentLength) } // Read content @@ -77,7 +77,7 @@ func ReadMessage(r *bufio.Reader) (*Message, error) { } if cfg.Options.DebugLSP { - logging.Debug("Received content", "content", string(content)) + slog.Debug("Received content", "content", string(content)) } // Parse message @@ -96,7 +96,7 @@ func (c *Client) handleMessages() { msg, err := ReadMessage(c.stdout) if err != nil { if cfg.Options.DebugLSP { - logging.Error("Error reading message", "error", err) + slog.Error("Error reading message", "error", err) } return } @@ -104,7 +104,7 @@ func (c *Client) handleMessages() { // Handle server->client request (has both Method and ID) if msg.Method != "" && msg.ID != 0 { if cfg.Options.DebugLSP { - logging.Debug("Received request from server", "method", msg.Method, "id", msg.ID) + slog.Debug("Received request from server", "method", msg.Method, "id", msg.ID) } response := &Message{ @@ -144,7 +144,7 @@ func (c *Client) handleMessages() { // Send response back to server if err := WriteMessage(c.stdin, response); err != nil { - logging.Error("Error sending response to server", "error", err) + slog.Error("Error sending response to server", "error", err) } continue @@ -158,11 +158,11 @@ func (c *Client) handleMessages() { if ok { if cfg.Options.DebugLSP { - logging.Debug("Handling notification", "method", msg.Method) + slog.Debug("Handling notification", "method", msg.Method) } go handler(msg.Params) } else if cfg.Options.DebugLSP { - logging.Debug("No handler for notification", "method", msg.Method) + slog.Debug("No handler for notification", "method", msg.Method) } continue } @@ -175,12 +175,12 @@ func (c *Client) handleMessages() { if ok { if cfg.Options.DebugLSP { - logging.Debug("Received response for request", "id", msg.ID) + slog.Debug("Received response for request", "id", msg.ID) } ch <- msg close(ch) } else if cfg.Options.DebugLSP { - logging.Debug("No handler for response", "id", msg.ID) + slog.Debug("No handler for response", "id", msg.ID) } } } @@ -192,7 +192,7 @@ func (c *Client) Call(ctx context.Context, method string, params any, result any id := c.nextID.Add(1) if cfg.Options.DebugLSP { - logging.Debug("Making call", "method", method, "id", id) + slog.Debug("Making call", "method", method, "id", id) } msg, err := NewRequest(id, method, params) @@ -218,14 +218,14 @@ func (c *Client) Call(ctx context.Context, method string, params any, result any } if cfg.Options.DebugLSP { - logging.Debug("Request sent", "method", method, "id", id) + slog.Debug("Request sent", "method", method, "id", id) } // Wait for response resp := <-ch if cfg.Options.DebugLSP { - logging.Debug("Received response", "id", id) + slog.Debug("Received response", "id", id) } if resp.Error != nil { @@ -251,7 +251,7 @@ func (c *Client) Call(ctx context.Context, method string, params any, result any func (c *Client) Notify(ctx context.Context, method string, params any) error { cfg := config.Get() if cfg.Options.DebugLSP { - logging.Debug("Sending notification", "method", method) + slog.Debug("Sending notification", "method", method) } msg, err := NewNotification(method, params) diff --git a/internal/lsp/watcher/watcher.go b/internal/lsp/watcher/watcher.go index 3c2dc05909bd8e6d473696efd09d22435f68dc10..afded8fd04f09d9c8590ea0818364b3310619e3b 100644 --- a/internal/lsp/watcher/watcher.go +++ b/internal/lsp/watcher/watcher.go @@ -11,7 +11,7 @@ import ( "github.com/bmatcuk/doublestar/v4" "github.com/charmbracelet/crush/internal/config" - "github.com/charmbracelet/crush/internal/logging" + "log/slog" "github.com/charmbracelet/crush/internal/lsp" "github.com/charmbracelet/crush/internal/lsp/protocol" "github.com/fsnotify/fsnotify" @@ -45,7 +45,7 @@ func NewWorkspaceWatcher(client *lsp.Client) *WorkspaceWatcher { func (w *WorkspaceWatcher) AddRegistrations(ctx context.Context, id string, watchers []protocol.FileSystemWatcher) { cfg := config.Get() - logging.Debug("Adding file watcher registrations") + slog.Debug("Adding file watcher registrations") w.registrationMu.Lock() defer w.registrationMu.Unlock() @@ -54,33 +54,33 @@ func (w *WorkspaceWatcher) AddRegistrations(ctx context.Context, id string, watc // Print detailed registration information for debugging if cfg.Options.DebugLSP { - logging.Debug("Adding file watcher registrations", + slog.Debug("Adding file watcher registrations", "id", id, "watchers", len(watchers), "total", len(w.registrations), ) for i, watcher := range watchers { - logging.Debug("Registration", "index", i+1) + slog.Debug("Registration", "index", i+1) // Log the GlobPattern switch v := watcher.GlobPattern.Value.(type) { case string: - logging.Debug("GlobPattern", "pattern", v) + slog.Debug("GlobPattern", "pattern", v) case protocol.RelativePattern: - logging.Debug("GlobPattern", "pattern", v.Pattern) + slog.Debug("GlobPattern", "pattern", v.Pattern) // Log BaseURI details switch u := v.BaseURI.Value.(type) { case string: - logging.Debug("BaseURI", "baseURI", u) + slog.Debug("BaseURI", "baseURI", u) case protocol.DocumentUri: - logging.Debug("BaseURI", "baseURI", u) + slog.Debug("BaseURI", "baseURI", u) default: - logging.Debug("BaseURI", "baseURI", u) + slog.Debug("BaseURI", "baseURI", u) } default: - logging.Debug("GlobPattern", "unknown type", fmt.Sprintf("%T", v)) + slog.Debug("GlobPattern", "unknown type", fmt.Sprintf("%T", v)) } // Log WatchKind @@ -89,13 +89,13 @@ func (w *WorkspaceWatcher) AddRegistrations(ctx context.Context, id string, watc watchKind = *watcher.Kind } - logging.Debug("WatchKind", "kind", watchKind) + slog.Debug("WatchKind", "kind", watchKind) } } // Determine server type for specialized handling serverName := getServerNameFromContext(ctx) - logging.Debug("Server type detected", "serverName", serverName) + slog.Debug("Server type detected", "serverName", serverName) // Check if this server has sent file watchers hasFileWatchers := len(watchers) > 0 @@ -123,7 +123,7 @@ func (w *WorkspaceWatcher) AddRegistrations(ctx context.Context, id string, watc filesOpened += highPriorityFilesOpened if cfg.Options.DebugLSP { - logging.Debug("Opened high-priority files", + slog.Debug("Opened high-priority files", "count", highPriorityFilesOpened, "serverName", serverName) } @@ -131,7 +131,7 @@ func (w *WorkspaceWatcher) AddRegistrations(ctx context.Context, id string, watc // If we've already opened enough high-priority files, we might not need more if filesOpened >= maxFilesToOpen { if cfg.Options.DebugLSP { - logging.Debug("Reached file limit with high-priority files", + slog.Debug("Reached file limit with high-priority files", "filesOpened", filesOpened, "maxFiles", maxFilesToOpen) } @@ -149,7 +149,7 @@ func (w *WorkspaceWatcher) AddRegistrations(ctx context.Context, id string, watc if d.IsDir() { if path != w.workspacePath && shouldExcludeDir(path) { if cfg.Options.DebugLSP { - logging.Debug("Skipping excluded directory", "path", path) + slog.Debug("Skipping excluded directory", "path", path) } return filepath.SkipDir } @@ -177,7 +177,7 @@ func (w *WorkspaceWatcher) AddRegistrations(ctx context.Context, id string, watc elapsedTime := time.Since(startTime) if cfg.Options.DebugLSP { - logging.Debug("Limited workspace scan complete", + slog.Debug("Limited workspace scan complete", "filesOpened", filesOpened, "maxFiles", maxFilesToOpen, "elapsedTime", elapsedTime.Seconds(), @@ -186,11 +186,11 @@ func (w *WorkspaceWatcher) AddRegistrations(ctx context.Context, id string, watc } if err != nil && cfg.Options.DebugLSP { - logging.Debug("Error scanning workspace for files to open", "error", err) + slog.Debug("Error scanning workspace for files to open", "error", err) } }() } else if cfg.Options.DebugLSP { - logging.Debug("Using on-demand file loading for server", "server", serverName) + slog.Debug("Using on-demand file loading for server", "server", serverName) } } @@ -266,7 +266,7 @@ func (w *WorkspaceWatcher) openHighPriorityFiles(ctx context.Context, serverName matches, err := doublestar.Glob(os.DirFS(w.workspacePath), pattern) if err != nil { if cfg.Options.DebugLSP { - logging.Debug("Error finding high-priority files", "pattern", pattern, "error", err) + slog.Debug("Error finding high-priority files", "pattern", pattern, "error", err) } continue } @@ -300,12 +300,12 @@ func (w *WorkspaceWatcher) openHighPriorityFiles(ctx context.Context, serverName fullPath := filesToOpen[j] if err := w.client.OpenFile(ctx, fullPath); err != nil { if cfg.Options.DebugLSP { - logging.Debug("Error opening high-priority file", "path", fullPath, "error", err) + slog.Debug("Error opening high-priority file", "path", fullPath, "error", err) } } else { filesOpened++ if cfg.Options.DebugLSP { - logging.Debug("Opened high-priority file", "path", fullPath) + slog.Debug("Opened high-priority file", "path", fullPath) } } } @@ -334,7 +334,7 @@ func (w *WorkspaceWatcher) WatchWorkspace(ctx context.Context, workspacePath str } serverName := getServerNameFromContext(ctx) - logging.Debug("Starting workspace watcher", "workspacePath", workspacePath, "serverName", serverName) + slog.Debug("Starting workspace watcher", "workspacePath", workspacePath, "serverName", serverName) // Register handler for file watcher registrations from the server lsp.RegisterFileWatchHandler(func(id string, watchers []protocol.FileSystemWatcher) { @@ -343,7 +343,7 @@ func (w *WorkspaceWatcher) WatchWorkspace(ctx context.Context, workspacePath str watcher, err := fsnotify.NewWatcher() if err != nil { - logging.Error("Error creating watcher", "error", err) + slog.Error("Error creating watcher", "error", err) } defer watcher.Close() @@ -357,7 +357,7 @@ func (w *WorkspaceWatcher) WatchWorkspace(ctx context.Context, workspacePath str if d.IsDir() && path != workspacePath { if shouldExcludeDir(path) { if cfg.Options.DebugLSP { - logging.Debug("Skipping excluded directory", "path", path) + slog.Debug("Skipping excluded directory", "path", path) } return filepath.SkipDir } @@ -367,14 +367,14 @@ func (w *WorkspaceWatcher) WatchWorkspace(ctx context.Context, workspacePath str if d.IsDir() { err = watcher.Add(path) if err != nil { - logging.Error("Error watching path", "path", path, "error", err) + slog.Error("Error watching path", "path", path, "error", err) } } return nil }) if err != nil { - logging.Error("Error walking workspace", "error", err) + slog.Error("Error walking workspace", "error", err) } // Event loop @@ -396,7 +396,7 @@ func (w *WorkspaceWatcher) WatchWorkspace(ctx context.Context, workspacePath str // Skip excluded directories if !shouldExcludeDir(event.Name) { if err := watcher.Add(event.Name); err != nil { - logging.Error("Error adding directory to watcher", "path", event.Name, "error", err) + slog.Error("Error adding directory to watcher", "path", event.Name, "error", err) } } } else { @@ -411,7 +411,7 @@ func (w *WorkspaceWatcher) WatchWorkspace(ctx context.Context, workspacePath str // Debug logging if cfg.Options.DebugLSP { matched, kind := w.isPathWatched(event.Name) - logging.Debug("File event", + slog.Debug("File event", "path", event.Name, "operation", event.Op.String(), "watched", matched, @@ -431,7 +431,7 @@ func (w *WorkspaceWatcher) WatchWorkspace(ctx context.Context, workspacePath str // Just send the notification if needed info, err := os.Stat(event.Name) if err != nil { - logging.Error("Error getting file info", "path", event.Name, "error", err) + slog.Error("Error getting file info", "path", event.Name, "error", err) return } if !info.IsDir() && watchKind&protocol.WatchCreate != 0 { @@ -459,7 +459,7 @@ func (w *WorkspaceWatcher) WatchWorkspace(ctx context.Context, workspacePath str if !ok { return } - logging.Error("Error watching file", "error", err) + slog.Error("Error watching file", "error", err) } } } @@ -584,7 +584,7 @@ func matchesSimpleGlob(pattern, path string) bool { // Fall back to simple matching for simpler patterns matched, err := filepath.Match(pattern, path) if err != nil { - logging.Error("Error matching pattern", "pattern", pattern, "path", path, "error", err) + slog.Error("Error matching pattern", "pattern", pattern, "path", path, "error", err) return false } @@ -595,7 +595,7 @@ func matchesSimpleGlob(pattern, path string) bool { func (w *WorkspaceWatcher) matchesPattern(path string, pattern protocol.GlobPattern) bool { patternInfo, err := pattern.AsPattern() if err != nil { - logging.Error("Error parsing pattern", "pattern", pattern, "error", err) + slog.Error("Error parsing pattern", "pattern", pattern, "error", err) return false } @@ -620,7 +620,7 @@ func (w *WorkspaceWatcher) matchesPattern(path string, pattern protocol.GlobPatt // Make path relative to basePath for matching relPath, err := filepath.Rel(basePath, path) if err != nil { - logging.Error("Error getting relative path", "path", path, "basePath", basePath, "error", err) + slog.Error("Error getting relative path", "path", path, "basePath", basePath, "error", err) return false } relPath = filepath.ToSlash(relPath) @@ -663,14 +663,14 @@ func (w *WorkspaceWatcher) handleFileEvent(ctx context.Context, uri string, chan } else if changeType == protocol.FileChangeType(protocol.Changed) && w.client.IsFileOpen(filePath) { err := w.client.NotifyChange(ctx, filePath) if err != nil { - logging.Error("Error notifying change", "error", err) + slog.Error("Error notifying change", "error", err) } return } // Notify LSP server about the file event using didChangeWatchedFiles if err := w.notifyFileEvent(ctx, uri, changeType); err != nil { - logging.Error("Error notifying LSP server about file event", "error", err) + slog.Error("Error notifying LSP server about file event", "error", err) } } @@ -678,7 +678,7 @@ func (w *WorkspaceWatcher) handleFileEvent(ctx context.Context, uri string, chan func (w *WorkspaceWatcher) notifyFileEvent(ctx context.Context, uri string, changeType protocol.FileChangeType) error { cfg := config.Get() if cfg.Options.DebugLSP { - logging.Debug("Notifying file event", + slog.Debug("Notifying file event", "uri", uri, "changeType", changeType, ) @@ -853,7 +853,7 @@ func shouldExcludeFile(filePath string) bool { // Skip large files if info.Size() > maxFileSize { if cfg.Options.DebugLSP { - logging.Debug("Skipping large file", + slog.Debug("Skipping large file", "path", filePath, "size", info.Size(), "maxSize", maxFileSize, @@ -891,10 +891,10 @@ func (w *WorkspaceWatcher) openMatchingFile(ctx context.Context, path string) { // This helps with project initialization for certain language servers if isHighPriorityFile(path, serverName) { if cfg.Options.DebugLSP { - logging.Debug("Opening high-priority file", "path", path, "serverName", serverName) + slog.Debug("Opening high-priority file", "path", path, "serverName", serverName) } if err := w.client.OpenFile(ctx, path); err != nil && cfg.Options.DebugLSP { - logging.Error("Error opening high-priority file", "path", path, "error", err) + slog.Error("Error opening high-priority file", "path", path, "error", err) } return } @@ -906,7 +906,7 @@ func (w *WorkspaceWatcher) openMatchingFile(ctx context.Context, path string) { // Check file size - for preloading we're more conservative if info.Size() > (1 * 1024 * 1024) { // 1MB limit for preloaded files if cfg.Options.DebugLSP { - logging.Debug("Skipping large file for preloading", "path", path, "size", info.Size()) + slog.Debug("Skipping large file for preloading", "path", path, "size", info.Size()) } return } @@ -938,7 +938,7 @@ func (w *WorkspaceWatcher) openMatchingFile(ctx context.Context, path string) { if shouldOpen { // Don't need to check if it's already open - the client.OpenFile handles that if err := w.client.OpenFile(ctx, path); err != nil && cfg.Options.DebugLSP { - logging.Error("Error opening file", "path", path, "error", err) + slog.Error("Error opening file", "path", path, "error", err) } } } diff --git a/internal/shell/persistent.go b/internal/shell/persistent.go index 33f80e7b3f109d67ed8a3ef039acf089f793e0ea..806261d0bbd8b4bb3471383bcd7afe3b1fd50fc4 100644 --- a/internal/shell/persistent.go +++ b/internal/shell/persistent.go @@ -1,9 +1,8 @@ package shell import ( + "log/slog" "sync" - - "github.com/charmbracelet/crush/internal/logging" ) // PersistentShell is a singleton shell instance that maintains state across the application @@ -30,9 +29,9 @@ func GetPersistentShell(cwd string) *PersistentShell { return shellInstance } -// loggingAdapter adapts the internal logging package to the Logger interface +// slog.dapter adapts the internal slog.package to the Logger interface type loggingAdapter struct{} func (l *loggingAdapter) InfoPersist(msg string, keysAndValues ...interface{}) { - logging.InfoPersist(msg, keysAndValues...) + slog.Info(msg, keysAndValues...) } diff --git a/internal/tui/components/chat/editor/editor.go b/internal/tui/components/chat/editor/editor.go index 04fbc2e22b1e49adc77b3be88c252b88b29c59a4..ca8624e9342857d436f0ec0995c5e5c3023680e7 100644 --- a/internal/tui/components/chat/editor/editor.go +++ b/internal/tui/components/chat/editor/editor.go @@ -14,7 +14,6 @@ import ( tea "github.com/charmbracelet/bubbletea/v2" "github.com/charmbracelet/crush/internal/app" "github.com/charmbracelet/crush/internal/fsext" - "github.com/charmbracelet/crush/internal/logging" "github.com/charmbracelet/crush/internal/message" "github.com/charmbracelet/crush/internal/session" "github.com/charmbracelet/crush/internal/tui/components/chat" @@ -153,8 +152,7 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil case filepicker.FilePickedMsg: if len(m.attachments) >= maxAttachments { - logging.ErrorPersist(fmt.Sprintf("cannot add more than %d images", maxAttachments)) - return m, cmd + return m, util.ReportError(fmt.Errorf("cannot add more than %d images", maxAttachments)) } m.attachments = append(m.attachments, msg.Attachment) return m, nil diff --git a/internal/tui/components/chat/sidebar/sidebar.go b/internal/tui/components/chat/sidebar/sidebar.go index 1f01a4b228ab0e538aa1b68effcabc4ad1e03ac7..1a43f86efc9fff264379d08c35fabf2a195a8d27 100644 --- a/internal/tui/components/chat/sidebar/sidebar.go +++ b/internal/tui/components/chat/sidebar/sidebar.go @@ -13,7 +13,7 @@ import ( "github.com/charmbracelet/crush/internal/diff" "github.com/charmbracelet/crush/internal/fsext" "github.com/charmbracelet/crush/internal/history" - "github.com/charmbracelet/crush/internal/logging" + "log/slog" "github.com/charmbracelet/crush/internal/lsp" "github.com/charmbracelet/crush/internal/lsp/protocol" "github.com/charmbracelet/crush/internal/pubsub" @@ -94,7 +94,7 @@ func (m *sidebarCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case chat.SessionClearedMsg: m.session = session.Session{} case pubsub.Event[history.File]: - logging.Info("sidebar", "Received file history event", "file", msg.Payload.Path, "session", msg.Payload.SessionID) + slog.Info("sidebar", "Received file history event", "file", msg.Payload.Path, "session", msg.Payload.SessionID) return m, m.handleFileHistoryEvent(msg) case pubsub.Event[session.Session]: if msg.Type == pubsub.UpdatedEvent { diff --git a/internal/tui/components/core/layout/split.go b/internal/tui/components/core/layout/split.go index 5309091a96c8ba29d31f5432b3a761e588e698d4..17622fb8d2c3545ea53890ee0487c198436c5e08 100644 --- a/internal/tui/components/core/layout/split.go +++ b/internal/tui/components/core/layout/split.go @@ -3,7 +3,7 @@ package layout import ( "github.com/charmbracelet/bubbles/v2/key" tea "github.com/charmbracelet/bubbletea/v2" - "github.com/charmbracelet/crush/internal/logging" + "log/slog" "github.com/charmbracelet/crush/internal/tui/styles" "github.com/charmbracelet/crush/internal/tui/util" "github.com/charmbracelet/lipgloss/v2" @@ -154,7 +154,7 @@ func (s *splitPaneLayout) View() tea.View { func (s *splitPaneLayout) SetSize(width, height int) tea.Cmd { s.width = width s.height = height - logging.Info("Setting split pane size", "width", width, "height", height) + slog.Info("Setting split pane size", "width", width, "height", height) var topHeight, bottomHeight int var cmds []tea.Cmd diff --git a/internal/tui/components/core/status/status.go b/internal/tui/components/core/status/status.go index bded453e78ecdfd85d6d182b4785a55d641dfd44..4411ed517169509b714a007a77a0c917d769ecd9 100644 --- a/internal/tui/components/core/status/status.go +++ b/internal/tui/components/core/status/status.go @@ -6,8 +6,6 @@ import ( "github.com/charmbracelet/bubbles/v2/help" tea "github.com/charmbracelet/bubbletea/v2" - "github.com/charmbracelet/crush/internal/logging" - "github.com/charmbracelet/crush/internal/pubsub" "github.com/charmbracelet/crush/internal/session" "github.com/charmbracelet/crush/internal/tui/styles" "github.com/charmbracelet/crush/internal/tui/util" @@ -59,37 +57,6 @@ func (m *statusCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case util.ClearStatusMsg: m.info = util.InfoMsg{} - // Handle persistent logs - case pubsub.Event[logging.LogMessage]: - if msg.Payload.Persist { - switch msg.Payload.Level { - case "error": - m.info = util.InfoMsg{ - Type: util.InfoTypeError, - Msg: msg.Payload.Message, - TTL: msg.Payload.PersistTime, - } - case "info": - m.info = util.InfoMsg{ - Type: util.InfoTypeInfo, - Msg: msg.Payload.Message, - TTL: msg.Payload.PersistTime, - } - case "warn": - m.info = util.InfoMsg{ - Type: util.InfoTypeWarn, - Msg: msg.Payload.Message, - TTL: msg.Payload.PersistTime, - } - default: - m.info = util.InfoMsg{ - Type: util.InfoTypeInfo, - Msg: msg.Payload.Message, - TTL: msg.Payload.PersistTime, - } - } - return m, m.clearMessageCmd(m.info.TTL) - } } return m, nil } diff --git a/internal/tui/components/dialogs/filepicker/filepicker.go b/internal/tui/components/dialogs/filepicker/filepicker.go index 916209b6f6371b7c5961f9fbc507f9c680f9e59b..4982dbfd605031403e33178b8ee7ae803aae8019 100644 --- a/internal/tui/components/dialogs/filepicker/filepicker.go +++ b/internal/tui/components/dialogs/filepicker/filepicker.go @@ -11,7 +11,6 @@ import ( "github.com/charmbracelet/bubbles/v2/help" "github.com/charmbracelet/bubbles/v2/key" tea "github.com/charmbracelet/bubbletea/v2" - "github.com/charmbracelet/crush/internal/logging" "github.com/charmbracelet/crush/internal/message" "github.com/charmbracelet/crush/internal/tui/components/core" "github.com/charmbracelet/crush/internal/tui/components/dialogs" @@ -119,18 +118,15 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func() tea.Msg { isFileLarge, err := ValidateFileSize(path, maxAttachmentSize) if err != nil { - logging.ErrorPersist("unable to read the image") - return nil + return util.ReportError(fmt.Errorf("unable to read the image: %w", err)) } if isFileLarge { - logging.ErrorPersist("file too large, max 5MB") - return nil + return util.ReportError(fmt.Errorf("file too large, max 5MB")) } content, err := os.ReadFile(path) if err != nil { - logging.ErrorPersist("Unable read selected file") - return nil + return util.ReportError(fmt.Errorf("unable to read the image: %w", err)) } mimeBufferSize := min(512, len(content)) diff --git a/internal/tui/components/logs/details.go b/internal/tui/components/logs/details.go deleted file mode 100644 index c73876aff8a6898809a170fec176def30af81314..0000000000000000000000000000000000000000 --- a/internal/tui/components/logs/details.go +++ /dev/null @@ -1,176 +0,0 @@ -package logs - -import ( - "fmt" - "strings" - "time" - - "github.com/charmbracelet/bubbles/v2/viewport" - tea "github.com/charmbracelet/bubbletea/v2" - "github.com/charmbracelet/crush/internal/logging" - "github.com/charmbracelet/crush/internal/tui/components/core/layout" - "github.com/charmbracelet/crush/internal/tui/styles" - "github.com/charmbracelet/crush/internal/tui/util" - "github.com/charmbracelet/lipgloss/v2" -) - -type DetailComponent interface { - util.Model - layout.Sizeable -} - -type detailCmp struct { - width, height int - currentLog logging.LogMessage - viewport viewport.Model -} - -func (i *detailCmp) Init() tea.Cmd { - messages := logging.List() - if len(messages) == 0 { - return nil - } - i.currentLog = messages[0] - return nil -} - -func (i *detailCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - switch msg := msg.(type) { - case selectedLogMsg: - if msg.ID != i.currentLog.ID { - i.currentLog = logging.LogMessage(msg) - i.updateContent() - } - } - - return i, nil -} - -func (i *detailCmp) updateContent() { - var content strings.Builder - t := styles.CurrentTheme() - - if i.currentLog.ID == "" { - content.WriteString(t.S().Muted.Render("No log selected")) - i.viewport.SetContent(content.String()) - return - } - - // Level badge with background color - levelStyle := getLevelStyle(i.currentLog.Level) - levelBadge := levelStyle.Padding(0, 1).Render(strings.ToUpper(i.currentLog.Level)) - - // Timestamp with relative time - timeStr := i.currentLog.Time.Format("2006-01-05 15:04:05 UTC") - relativeTime := getRelativeTime(i.currentLog.Time) - timeStyle := t.S().Muted - - // Header line - header := lipgloss.JoinHorizontal( - lipgloss.Left, - timeStr, - " ", - timeStyle.Render(relativeTime), - ) - - content.WriteString(levelBadge) - content.WriteString("\n\n") - content.WriteString(header) - content.WriteString("\n\n") - - // Message section - messageHeaderStyle := t.S().Base.Foreground(t.Blue).Bold(true) - content.WriteString(messageHeaderStyle.Render("Message")) - content.WriteString("\n") - content.WriteString(i.currentLog.Message) - content.WriteString("\n\n") - - // Attributes section - if len(i.currentLog.Attributes) > 0 { - attrHeaderStyle := t.S().Base.Foreground(t.Blue).Bold(true) - content.WriteString(attrHeaderStyle.Render("Attributes")) - content.WriteString("\n") - - for _, attr := range i.currentLog.Attributes { - keyStyle := t.S().Base.Foreground(t.Accent) - valueStyle := t.S().Text - attrLine := fmt.Sprintf("%s: %s", - keyStyle.Render(attr.Key), - valueStyle.Render(attr.Value), - ) - content.WriteString(attrLine) - content.WriteString("\n") - } - } - - i.viewport.SetContent(content.String()) -} - -func getLevelStyle(level string) lipgloss.Style { - t := styles.CurrentTheme() - style := t.S().Base.Bold(true) - - switch strings.ToLower(level) { - case "info": - return style.Foreground(t.White).Background(t.Info) - case "warn", "warning": - return style.Foreground(t.White).Background(t.Warning) - case "error", "err": - return style.Foreground(t.White).Background(t.Error) - case "debug": - return style.Foreground(t.White).Background(t.Success) - case "fatal": - return style.Foreground(t.White).Background(t.Error) - default: - return style.Foreground(t.FgBase) - } -} - -func getRelativeTime(logTime time.Time) string { - now := time.Now() - diff := now.Sub(logTime) - - if diff < time.Minute { - return fmt.Sprintf("%ds ago", int(diff.Seconds())) - } else if diff < time.Hour { - return fmt.Sprintf("%dm ago", int(diff.Minutes())) - } else if diff < 24*time.Hour { - return fmt.Sprintf("%dh ago", int(diff.Hours())) - } else if diff < 30*24*time.Hour { - return fmt.Sprintf("%dd ago", int(diff.Hours()/24)) - } else if diff < 365*24*time.Hour { - return fmt.Sprintf("%dmo ago", int(diff.Hours()/(24*30))) - } else { - return fmt.Sprintf("%dy ago", int(diff.Hours()/(24*365))) - } -} - -func (i *detailCmp) View() tea.View { - t := styles.CurrentTheme() - style := t.S().Base. - BorderStyle(lipgloss.RoundedBorder()). - BorderForeground(t.BorderFocus). - Width(i.width - 2). // Adjust width for border - Height(i.height - 2). // Adjust height for border - Padding(1) - return tea.NewView(style.Render(i.viewport.View())) -} - -func (i *detailCmp) GetSize() (int, int) { - return i.width, i.height -} - -func (i *detailCmp) SetSize(width int, height int) tea.Cmd { - i.width = width - i.height = height - i.viewport.SetWidth(i.width - 4) - i.viewport.SetHeight(i.height - 4) - i.updateContent() - return nil -} - -func NewLogsDetails() DetailComponent { - return &detailCmp{ - viewport: viewport.New(), - } -} diff --git a/internal/tui/components/logs/table.go b/internal/tui/components/logs/table.go deleted file mode 100644 index 88160dc875d896a61ecd09560b74d7993f11f020..0000000000000000000000000000000000000000 --- a/internal/tui/components/logs/table.go +++ /dev/null @@ -1,197 +0,0 @@ -package logs - -import ( - "fmt" - "slices" - "strings" - - "github.com/charmbracelet/bubbles/v2/table" - tea "github.com/charmbracelet/bubbletea/v2" - "github.com/charmbracelet/crush/internal/logging" - "github.com/charmbracelet/crush/internal/pubsub" - "github.com/charmbracelet/crush/internal/tui/components/core/layout" - "github.com/charmbracelet/crush/internal/tui/styles" - "github.com/charmbracelet/crush/internal/tui/util" - "github.com/charmbracelet/lipgloss/v2" -) - -type TableComponent interface { - util.Model - layout.Sizeable -} - -type tableCmp struct { - table table.Model - logs []logging.LogMessage -} - -type selectedLogMsg logging.LogMessage - -func (i *tableCmp) Init() tea.Cmd { - i.logs = logging.List() - i.setRows() - return nil -} - -func (i *tableCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - var cmds []tea.Cmd - switch msg := msg.(type) { - case pubsub.Event[logging.LogMessage]: - return i, func() tea.Msg { - if msg.Type == pubsub.CreatedEvent { - rows := i.table.Rows() - for _, row := range rows { - if row[1] == msg.Payload.ID { - return nil // If the log already exists, do not add it again - } - } - i.logs = append(i.logs, msg.Payload) - i.table.SetRows( - append( - []table.Row{ - logToRow(msg.Payload), - }, - i.table.Rows()..., - ), - ) - } - return selectedLogMsg(msg.Payload) - } - } - t, cmd := i.table.Update(msg) - cmds = append(cmds, cmd) - i.table = t - - cmds = append(cmds, func() tea.Msg { - for _, log := range logging.List() { - if log.ID == i.table.SelectedRow()[1] { - // If the selected row matches the log ID, return the selected log message - return selectedLogMsg(log) - } - } - return nil - }) - return i, tea.Batch(cmds...) -} - -func (i *tableCmp) View() tea.View { - t := styles.CurrentTheme() - defaultStyles := table.DefaultStyles() - - // Header styling - defaultStyles.Header = defaultStyles.Header. - Foreground(t.Primary). - Bold(true). - BorderStyle(lipgloss.NormalBorder()). - BorderBottom(true). - BorderForeground(t.Border) - - // Selected row styling - defaultStyles.Selected = defaultStyles.Selected. - Foreground(t.FgSelected). - Background(t.Primary). - Bold(false) - - // Cell styling - defaultStyles.Cell = defaultStyles.Cell. - Foreground(t.FgBase) - - i.table.SetStyles(defaultStyles) - return tea.NewView(i.table.View()) -} - -func (i *tableCmp) GetSize() (int, int) { - return i.table.Width(), i.table.Height() -} - -func (i *tableCmp) SetSize(width int, height int) tea.Cmd { - i.table.SetWidth(width) - i.table.SetHeight(height) - - columnWidth := (width - 10) / 4 - i.table.SetColumns([]table.Column{ - { - Title: "Level", - Width: 10, - }, - { - Title: "ID", - Width: columnWidth, - }, - { - Title: "Time", - Width: columnWidth, - }, - { - Title: "Message", - Width: columnWidth, - }, - { - Title: "Attributes", - Width: columnWidth, - }, - }) - return nil -} - -func (i *tableCmp) setRows() { - rows := []table.Row{} - - slices.SortFunc(i.logs, func(a, b logging.LogMessage) int { - if a.Time.Before(b.Time) { - return -1 - } - if a.Time.After(b.Time) { - return 1 - } - return 0 - }) - - for _, log := range i.logs { - rows = append(rows, logToRow(log)) - } - i.table.SetRows(rows) -} - -func logToRow(log logging.LogMessage) table.Row { - // Format attributes as JSON string - var attrStr string - if len(log.Attributes) > 0 { - var parts []string - for _, attr := range log.Attributes { - parts = append(parts, fmt.Sprintf(`{"Key":"%s","Value":"%s"}`, attr.Key, attr.Value)) - } - attrStr = "[" + strings.Join(parts, ",") + "]" - } - - // Format time with relative time - timeStr := log.Time.Format("2006-01-05 15:04:05 UTC") - relativeTime := getRelativeTime(log.Time) - fullTimeStr := timeStr + " " + relativeTime - - return table.Row{ - strings.ToUpper(log.Level), - log.ID, - fullTimeStr, - log.Message, - attrStr, - } -} - -func NewLogsTable() TableComponent { - columns := []table.Column{ - {Title: "Level"}, - {Title: "ID"}, - {Title: "Time"}, - {Title: "Message"}, - {Title: "Attributes"}, - } - - tableModel := table.New( - table.WithColumns(columns), - ) - tableModel.Focus() - return &tableCmp{ - table: tableModel, - } -} diff --git a/internal/tui/keys.go b/internal/tui/keys.go index 8af028cd10338eba2108f94156035b8f58f342e2..22c029dd355845b3ac7e2066a8a93bfb335c1d53 100644 --- a/internal/tui/keys.go +++ b/internal/tui/keys.go @@ -5,7 +5,6 @@ import ( ) type KeyMap struct { - Logs key.Binding Quit key.Binding Help key.Binding Commands key.Binding @@ -16,10 +15,6 @@ type KeyMap struct { func DefaultKeyMap() KeyMap { return KeyMap{ - Logs: key.NewBinding( - key.WithKeys("ctrl+l"), - key.WithHelp("ctrl+l", "logs"), - ), Quit: key.NewBinding( key.WithKeys("ctrl+c"), key.WithHelp("ctrl+c", "quit"), @@ -47,7 +42,6 @@ func (k KeyMap) FullHelp() [][]key.Binding { k.Sessions, k.Quit, k.Help, - k.Logs, } slice = k.prependEscAndTab(slice) slice = append(slice, k.pageBindings...) diff --git a/internal/tui/page/logs/keys.go b/internal/tui/page/logs/keys.go deleted file mode 100644 index 3c612b949183b4180494bdbb8224dd8eedbbb156..0000000000000000000000000000000000000000 --- a/internal/tui/page/logs/keys.go +++ /dev/null @@ -1,43 +0,0 @@ -package logs - -import ( - "github.com/charmbracelet/bubbles/v2/key" -) - -type KeyMap struct { - Back key.Binding -} - -func DefaultKeyMap() KeyMap { - return KeyMap{ - Back: key.NewBinding( - key.WithKeys("esc", "backspace"), - key.WithHelp("esc/backspace", "back to chat"), - ), - } -} - -// KeyBindings implements layout.KeyMapProvider -func (k KeyMap) KeyBindings() []key.Binding { - return []key.Binding{ - k.Back, - } -} - -// FullHelp implements help.KeyMap. -func (k KeyMap) FullHelp() [][]key.Binding { - m := [][]key.Binding{} - slice := k.KeyBindings() - for i := 0; i < len(slice); i += 4 { - end := min(i+4, len(slice)) - m = append(m, slice[i:end]) - } - return m -} - -// ShortHelp implements help.KeyMap. -func (k KeyMap) ShortHelp() []key.Binding { - return []key.Binding{ - k.Back, - } -} diff --git a/internal/tui/page/logs/logs.go b/internal/tui/page/logs/logs.go deleted file mode 100644 index e8b1077d80094ee5974debc5c09f1f82562e334c..0000000000000000000000000000000000000000 --- a/internal/tui/page/logs/logs.go +++ /dev/null @@ -1,100 +0,0 @@ -package logs - -import ( - "github.com/charmbracelet/bubbles/v2/key" - tea "github.com/charmbracelet/bubbletea/v2" - "github.com/charmbracelet/crush/internal/tui/components/core" - "github.com/charmbracelet/crush/internal/tui/components/core/layout" - logsComponents "github.com/charmbracelet/crush/internal/tui/components/logs" - "github.com/charmbracelet/crush/internal/tui/page" - "github.com/charmbracelet/crush/internal/tui/page/chat" - "github.com/charmbracelet/crush/internal/tui/styles" - "github.com/charmbracelet/crush/internal/tui/util" - "github.com/charmbracelet/lipgloss/v2" -) - -var LogsPage page.PageID = "logs" - -type LogPage interface { - util.Model - layout.Sizeable -} - -type logsPage struct { - width, height int - table logsComponents.TableComponent - details logsComponents.DetailComponent - keyMap KeyMap -} - -func (p *logsPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - var cmds []tea.Cmd - switch msg := msg.(type) { - case tea.WindowSizeMsg: - p.width = msg.Width - p.height = msg.Height - return p, p.SetSize(msg.Width, msg.Height) - case tea.KeyMsg: - switch { - case key.Matches(msg, p.keyMap.Back): - return p, util.CmdHandler(page.PageChangeMsg{ID: chat.ChatPageID}) - } - } - - table, cmd := p.table.Update(msg) - cmds = append(cmds, cmd) - p.table = table.(logsComponents.TableComponent) - details, cmd := p.details.Update(msg) - cmds = append(cmds, cmd) - p.details = details.(logsComponents.DetailComponent) - - return p, tea.Batch(cmds...) -} - -func (p *logsPage) View() tea.View { - baseStyle := styles.CurrentTheme().S().Base - style := baseStyle.Width(p.width).Height(p.height).Padding(1) - title := core.Title("Logs", p.width-2) - - return tea.NewView( - style.Render( - lipgloss.JoinVertical(lipgloss.Top, - title, - p.details.View().String(), - p.table.View().String(), - ), - ), - ) -} - -// GetSize implements LogPage. -func (p *logsPage) GetSize() (int, int) { - return p.width, p.height -} - -// SetSize implements LogPage. -func (p *logsPage) SetSize(width int, height int) tea.Cmd { - p.width = width - p.height = height - availableHeight := height - 2 // Padding for top and bottom - availableHeight -= 1 // title height - return tea.Batch( - p.table.SetSize(width-2, availableHeight/2), - p.details.SetSize(width-2, availableHeight/2), - ) -} - -func (p *logsPage) Init() tea.Cmd { - return tea.Batch( - p.table.Init(), - p.details.Init(), - ) -} - -func NewLogsPage() LogPage { - return &logsPage{ - details: logsComponents.NewLogsDetails(), - table: logsComponents.NewLogsTable(), - keyMap: DefaultKeyMap(), - } -} diff --git a/internal/tui/tui.go b/internal/tui/tui.go index fcef527c812e9f5d74801b009050c69daec7fd20..3f46fdaad7a14fc0f481a71030a5173307477375 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -9,7 +9,6 @@ import ( "github.com/charmbracelet/crush/internal/app" "github.com/charmbracelet/crush/internal/config" "github.com/charmbracelet/crush/internal/llm/agent" - "github.com/charmbracelet/crush/internal/logging" "github.com/charmbracelet/crush/internal/permission" "github.com/charmbracelet/crush/internal/pubsub" cmpChat "github.com/charmbracelet/crush/internal/tui/components/chat" @@ -27,7 +26,6 @@ import ( "github.com/charmbracelet/crush/internal/tui/components/dialogs/sessions" "github.com/charmbracelet/crush/internal/tui/page" "github.com/charmbracelet/crush/internal/tui/page/chat" - "github.com/charmbracelet/crush/internal/tui/page/logs" "github.com/charmbracelet/crush/internal/tui/styles" "github.com/charmbracelet/crush/internal/tui/util" "github.com/charmbracelet/lipgloss/v2" @@ -135,20 +133,6 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { a.selectedSessionID = msg.ID case cmpChat.SessionClearedMsg: a.selectedSessionID = "" - // Logs - case pubsub.Event[logging.LogMessage]: - // Send to the status component - s, statusCmd := a.status.Update(msg) - a.status = s.(status.StatusCmp) - cmds = append(cmds, statusCmd) - - // If the current page is logs, update the logs view - if a.currentPage == logs.LogsPage { - updated, pageCmd := a.pages[a.currentPage].Update(msg) - a.pages[a.currentPage] = updated.(util.Model) - cmds = append(cmds, pageCmd) - } - return a, tea.Batch(cmds...) // Commands case commands.SwitchSessionsMsg: return a, func() tea.Msg { @@ -176,7 +160,6 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // Update the agent with the new model/provider configuration if err := a.app.UpdateAgentModel(); err != nil { - logging.ErrorPersist(fmt.Sprintf("Failed to update agent model: %v", err)) return a, util.ReportError(fmt.Errorf("model changed to %s but failed to update agent: %v", msg.Model.Model, err)) } @@ -348,10 +331,6 @@ func (a *appModel) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd { }, ) return tea.Sequence(cmds...) - // Page navigation - case key.Matches(msg, a.keyMap.Logs): - return a.moveToPage(logs.LogsPage) - default: if a.dialog.HasDialogs() { u, dialogCmd := a.dialog.Update(msg) @@ -453,7 +432,6 @@ func New(app *app.App) tea.Model { pages: map[page.PageID]util.Model{ chat.ChatPageID: chatPage, - logs.LogsPage: logs.NewLogsPage(), }, dialog: dialogs.NewDialogCmp(), diff --git a/main.go b/main.go index ce145401f8828beeae7bb3b7747bdc9339e30405..be1c9f94267b40727f22c5d513bd2c1defe517e1 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,7 @@ package main import ( + "log/slog" "net/http" "os" @@ -9,19 +10,19 @@ import ( _ "github.com/joho/godotenv/autoload" // automatically load .env files "github.com/charmbracelet/crush/cmd" - "github.com/charmbracelet/crush/internal/logging" + "github.com/charmbracelet/crush/internal/log" ) func main() { - defer logging.RecoverPanic("main", func() { - logging.ErrorPersist("Application terminated due to unhandled panic") + defer log.RecoverPanic("main", func() { + slog.Error("Application terminated due to unhandled panic") }) if os.Getenv("CRUSH_PROFILE") != "" { go func() { - logging.Info("Serving pprof at localhost:6060") + slog.Info("Serving pprof at localhost:6060") if httpErr := http.ListenAndServe("localhost:6060", nil); httpErr != nil { - logging.Error("Failed to pprof listen: %v", httpErr) + slog.Error("Failed to pprof listen: %v", httpErr) } }() }