chore: remove logs

This commit is contained in:
Kujtim Hoxha
2025-07-05 16:34:24 +02:00
parent 7f078a6e20
commit dafbdb74cd
40 changed files with 246 additions and 1131 deletions

View File

@@ -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

View File

@@ -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)
}
}

View File

@@ -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()
}

View File

@@ -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)
}

View File

@@ -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)
})

View File

@@ -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 {

View File

@@ -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

View File

@@ -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.")
}
}

View File

@@ -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)

View File

@@ -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)...)

View File

@@ -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)
}

View File

@@ -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
}
}

View File

@@ -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 {

View File

@@ -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

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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()
}
}
}
}

View File

@@ -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))
}

View File

@@ -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
}

View File

@@ -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()
}

View File

@@ -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)
}
}

View File

@@ -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, &registerParams); 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
}

View File

@@ -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

View File

@@ -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)

View File

@@ -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)
}
}
}

View File

@@ -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...)
}

View File

@@ -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

View File

@@ -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 {

View File

@@ -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

View File

@@ -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
}

View File

@@ -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))

View File

@@ -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(),
}
}

View File

@@ -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,
}
}

View File

@@ -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...)

View File

@@ -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,
}
}

View File

@@ -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(),
}
}

View File

@@ -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(),

11
main.go
View File

@@ -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)
}
}()
}