chore: remove logs

Kujtim Hoxha created

Change summary

cmd/logs.go                                              |   1 
cmd/root.go                                              |  46 +-
internal/app/app.go                                      |  14 
internal/app/lsp.go                                      |  25 
internal/config/init.go                                  |   4 
internal/config/load.go                                  |   7 
internal/db/connect.go                                   |  10 
internal/fsext/fileutil.go                               |   6 
internal/llm/agent/agent.go                              |  35 
internal/llm/agent/mcp-tools.go                          |  12 
internal/llm/prompt/coder.go                             |   4 
internal/llm/provider/anthropic.go                       |  18 
internal/llm/provider/gemini.go                          |  12 
internal/llm/provider/openai.go                          |  12 
internal/llm/provider/vertexai.go                        |   4 
internal/llm/tools/edit.go                               |  12 
internal/llm/tools/glob.go                               |   4 
internal/llm/tools/write.go                              |   6 
internal/log/log.go                                      |  27 +
internal/logging/logger.go                               | 209 ----------
internal/logging/message.go                              |  21 -
internal/logging/writer.go                               | 102 ----
internal/lsp/client.go                                   |  39 
internal/lsp/handlers.go                                 |  14 
internal/lsp/protocol/tsprotocol.go                      |   2 
internal/lsp/transport.go                                |  32 
internal/lsp/watcher/watcher.go                          |  82 +-
internal/shell/persistent.go                             |   7 
internal/tui/components/chat/editor/editor.go            |   4 
internal/tui/components/chat/sidebar/sidebar.go          |   4 
internal/tui/components/core/layout/split.go             |   4 
internal/tui/components/core/status/status.go            |  33 -
internal/tui/components/dialogs/filepicker/filepicker.go |  10 
internal/tui/components/logs/details.go                  | 176 --------
internal/tui/components/logs/table.go                    | 197 ---------
internal/tui/keys.go                                     |   6 
internal/tui/page/logs/keys.go                           |  43 --
internal/tui/page/logs/logs.go                           | 100 ----
internal/tui/tui.go                                      |  22 -
main.go                                                  |  11 
40 files changed, 246 insertions(+), 1,131 deletions(-)

Detailed changes

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

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

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

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

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

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 {

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

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

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)

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

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

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

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 {

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

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
 	}
 

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)

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)

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)

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

internal/logging/logger.go 🔗

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

internal/logging/message.go 🔗

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

internal/logging/writer.go 🔗

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

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

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

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

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)

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

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

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

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 {

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

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
 }

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

internal/tui/components/logs/details.go 🔗

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

internal/tui/components/logs/table.go 🔗

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

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

internal/tui/page/logs/keys.go 🔗

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

internal/tui/page/logs/logs.go 🔗

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

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

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