on main help sign in
chore: remove logs
commitdafbdb74cd53593bcabbd2def29206c4be040825 authorKujtim Hoxha <kujtimii.h@gmail.com> dateSat 5 Jul 2025, 16:34 (1 year ago) parent7f078a6
40 files changed+246−1131
cmd/logs.go+1 cmd/root.go+23−23 internal/app/app.go+7−7 internal/app/lsp.go+13−12 internal/config/init.go+2−2 internal/config/load.go+5−2 internal/db/connect.go+5−5 internal/fsext/fileutil.go+3−3 internal/llm/agent/agent.go+16−19 internal/llm/agent/mcp-tools.go+6−6 internal/llm/prompt/coder.go+2−2 internal/llm/provider/anthropic.go+9−9 internal/llm/provider/gemini.go+6−6 internal/llm/provider/openai.go+6−6 internal/llm/provider/vertexai.go+2−2 internal/llm/tools/edit.go+6−6 internal/llm/tools/glob.go+2−2 internal/llm/tools/write.go+3−3 internal/log/log.go+27 internal/logging/logger.go−209 internal/logging/message.go−21 internal/logging/writer.go−102 internal/lsp/client.go+20−19 internal/lsp/handlers.go+7−7 internal/lsp/protocol/tsprotocol.go+1−1 internal/lsp/transport.go+16−16 internal/lsp/watcher/watcher.go+41−41 internal/shell/persistent.go+3−4 internal/tui/components/chat/editor/editor.go+1−3 internal/tui/components/chat/sidebar/sidebar.go+2−2 internal/tui/components/core/layout/split.go+2−2 internal/tui/components/core/status/status.go−33 internal/tui/components/dialogs/filepicker/filepicker.go+3−7 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+6−5
cmd/logs.go+1
@@ -35,6 +35,7 @@ var logsCmd = &cobra.Command{
3535 return fmt.Errorf("failed to tail log file: %v", err)
3636 }
3737
38+ log.SetLevel(log.DebugLevel)
3839 // Print the text of each received line
3940 for line := range t.Lines {
4041 var data map[string]any
cmd/root.go+23−23
@@ -4,6 +4,7 @@ import (
44 "context"
55 "fmt"
66 "io"
7+ "log/slog"
78 "os"
89 "sync"
910 "time"
@@ -14,7 +15,7 @@ import (
1415 "github.com/charmbracelet/crush/internal/db"
1516 "github.com/charmbracelet/crush/internal/format"
1617 "github.com/charmbracelet/crush/internal/llm/agent"
17− "github.com/charmbracelet/crush/internal/logging"
18+ "github.com/charmbracelet/crush/internal/log"
1819 "github.com/charmbracelet/crush/internal/pubsub"
1920 "github.com/charmbracelet/crush/internal/tui"
2021 "github.com/charmbracelet/crush/internal/version"
@@ -36,7 +37,7 @@ to assist developers in writing, debugging, and understanding code directly from
3637 # Run with debug logging
3738 crush -d
3839
39− # Run with debug logging in a specific directory
40+ # Run with debug slog.in a specific directory
4041 crush -d -c /path/to/project
4142
4243 # Print version
@@ -92,7 +93,7 @@ to assist developers in writing, debugging, and understanding code directly from
9293
9394 app, err := app.New(ctx, conn)
9495 if err != nil {
95− logging.Error("Failed to create app: %v", err)
96+ slog.Error("Failed to create app: %v", err)
9697 return err
9798 }
9899 // 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
103104
104105 prompt, err = maybePrependStdin(prompt)
105106 if err != nil {
106− logging.Error("Failed to read stdin: %v", err)
107+ slog.Error("Failed to read stdin: %v", err)
107108 return err
108109 }
109110
@@ -132,18 +133,18 @@ to assist developers in writing, debugging, and understanding code directly from
132133 // Set up message handling for the TUI
133134 go func() {
134135 defer tuiWg.Done()
135− defer logging.RecoverPanic("TUI-message-handler", func() {
136+ defer log.RecoverPanic("TUI-message-handler", func() {
136137 attemptTUIRecovery(program)
137138 })
138139
139140 for {
140141 select {
141142 case <-tuiCtx.Done():
142− logging.Info("TUI message handler shutting down")
143+ slog.Info("TUI message handler shutting down")
143144 return
144145 case msg, ok := <-ch:
145146 if !ok {
146− logging.Info("TUI message channel closed")
147+ slog.Info("TUI message channel closed")
147148 return
148149 }
149150 program.Send(msg)
@@ -165,7 +166,7 @@ to assist developers in writing, debugging, and understanding code directly from
165166 // Wait for TUI message handler to finish
166167 tuiWg.Wait()
167168
168− logging.Info("All goroutines cleaned up")
169+ slog.Info("All goroutines cleaned up")
169170 }
170171
171172 // Run the TUI
@@ -173,18 +174,18 @@ to assist developers in writing, debugging, and understanding code directly from
173174 cleanup()
174175
175176 if err != nil {
176− logging.Error("TUI error: %v", err)
177+ slog.Error("TUI error: %v", err)
177178 return fmt.Errorf("TUI error: %v", err)
178179 }
179180
180− logging.Info("TUI exited with result: %v", result)
181+ slog.Info("TUI exited with result: %v", result)
181182 return nil
182183 },
183184}
184185
185186// attemptTUIRecovery tries to recover the TUI after a panic
186187func attemptTUIRecovery(program *tea.Program) {
187− logging.Info("Attempting to recover TUI after panic")
188+ slog.Info("Attempting to recover TUI after panic")
188189
189190 // We could try to restart the TUI or gracefully exit
190191 // For now, we'll just quit the program to avoid further issues
@@ -193,7 +194,7 @@ func attemptTUIRecovery(program *tea.Program) {
193194
194195func initMCPTools(ctx context.Context, app *app.App) {
195196 go func() {
196− defer logging.RecoverPanic("MCP-goroutine", nil)
197+ defer log.RecoverPanic("MCP-goroutine", nil)
197198
198199 // Create a context with timeout for the initial MCP tools fetch
199200 ctxWithTimeout, cancel := context.WithTimeout(ctx, 30*time.Second)
@@ -201,7 +202,7 @@ func initMCPTools(ctx context.Context, app *app.App) {
201202
202203 // Set this up once with proper error handling
203204 agent.GetMcpTools(ctxWithTimeout, app.Permissions)
204− logging.Info("MCP message handling goroutine exiting")
205+ slog.Info("MCP message handling goroutine exiting")
205206 }()
206207}
207208
@@ -215,7 +216,7 @@ func setupSubscriber[T any](
215216 wg.Add(1)
216217 go func() {
217218 defer wg.Done()
218− defer logging.RecoverPanic(fmt.Sprintf("subscription-%s", name), nil)
219+ defer log.RecoverPanic(fmt.Sprintf("subscription-%s", name), nil)
219220
220221 subCh := subscriber(ctx)
221222
@@ -223,7 +224,7 @@ func setupSubscriber[T any](
223224 select {
224225 case event, ok := <-subCh:
225226 if !ok {
226− logging.Info("subscription channel closed", "name", name)
227+ slog.Info("subscription channel closed", "name", name)
227228 return
228229 }
229230
@@ -232,13 +233,13 @@ func setupSubscriber[T any](
232233 select {
233234 case outputCh <- msg:
234235 case <-time.After(2 * time.Second):
235− logging.Warn("message dropped due to slow consumer", "name", name)
236+ slog.Warn("message dropped due to slow consumer", "name", name)
236237 case <-ctx.Done():
237− logging.Info("subscription cancelled", "name", name)
238+ slog.Info("subscription cancelled", "name", name)
238239 return
239240 }
240241 case <-ctx.Done():
241− logging.Info("subscription cancelled", "name", name)
242+ slog.Info("subscription cancelled", "name", name)
242243 return
243244 }
244245 }
@@ -251,7 +252,6 @@ func setupSubscriptions(app *app.App, parentCtx context.Context) (chan tea.Msg,
251252 wg := sync.WaitGroup{}
252253 ctx, cancel := context.WithCancel(parentCtx) // Inherit from parent context
253254
254− setupSubscriber(ctx, &wg, "logging", logging.Subscribe, ch)
255255 setupSubscriber(ctx, &wg, "sessions", app.Sessions.Subscribe, ch)
256256 setupSubscriber(ctx, &wg, "messages", app.Messages.Subscribe, ch)
257257 setupSubscriber(ctx, &wg, "permissions", app.Permissions.Subscribe, ch)
@@ -259,22 +259,22 @@ func setupSubscriptions(app *app.App, parentCtx context.Context) (chan tea.Msg,
259259 setupSubscriber(ctx, &wg, "history", app.History.Subscribe, ch)
260260
261261 cleanupFunc := func() {
262− logging.Info("Cancelling all subscriptions")
262+ slog.Info("Cancelling all subscriptions")
263263 cancel() // Signal all goroutines to stop
264264
265265 waitCh := make(chan struct{})
266266 go func() {
267− defer logging.RecoverPanic("subscription-cleanup", nil)
267+ defer log.RecoverPanic("subscription-cleanup", nil)
268268 wg.Wait()
269269 close(waitCh)
270270 }()
271271
272272 select {
273273 case <-waitCh:
274− logging.Info("All subscription goroutines completed successfully")
274+ slog.Info("All subscription goroutines completed successfully")
275275 close(ch) // Only close after all writers are confirmed done
276276 case <-time.After(5 * time.Second):
277− logging.Warn("Timed out waiting for some subscription goroutines to complete")
277+ slog.Warn("Timed out waiting for some subscription goroutines to complete")
278278 close(ch)
279279 }
280280 }
internal/app/app.go+7−7
@@ -14,7 +14,7 @@ import (
1414 "github.com/charmbracelet/crush/internal/format"
1515 "github.com/charmbracelet/crush/internal/history"
1616 "github.com/charmbracelet/crush/internal/llm/agent"
17− "github.com/charmbracelet/crush/internal/logging"
17+ "log/slog"
1818 "github.com/charmbracelet/crush/internal/lsp"
1919 "github.com/charmbracelet/crush/internal/message"
2020 "github.com/charmbracelet/crush/internal/permission"
@@ -73,7 +73,7 @@ func New(ctx context.Context, conn *sql.DB) (*App, error) {
7373 app.LSPClients,
7474 )
7575 if err != nil {
76− logging.Error("Failed to create coder agent", err)
76+ slog.Error("Failed to create coder agent", err)
7777 return nil, err
7878 }
7979
@@ -82,7 +82,7 @@ func New(ctx context.Context, conn *sql.DB) (*App, error) {
8282
8383// RunNonInteractive handles the execution flow when a prompt is provided via CLI flag.
8484func (a *App) RunNonInteractive(ctx context.Context, prompt string, outputFormat string, quiet bool) error {
85− logging.Info("Running in non-interactive mode")
85+ slog.Info("Running in non-interactive mode")
8686
8787 // Start spinner if not in quiet mode
8888 var spinner *format.Spinner
@@ -107,7 +107,7 @@ func (a *App) RunNonInteractive(ctx context.Context, prompt string, outputFormat
107107 if err != nil {
108108 return fmt.Errorf("failed to create session for non-interactive mode: %w", err)
109109 }
110− logging.Info("Created session for non-interactive run", "session_id", sess.ID)
110+ slog.Info("Created session for non-interactive run", "session_id", sess.ID)
111111
112112 // Automatically approve all permission requests for this non-interactive session
113113 a.Permissions.AutoApproveSession(sess.ID)
@@ -120,7 +120,7 @@ func (a *App) RunNonInteractive(ctx context.Context, prompt string, outputFormat
120120 result := <-done
121121 if result.Error != nil {
122122 if errors.Is(result.Error, context.Canceled) || errors.Is(result.Error, agent.ErrRequestCancelled) {
123− logging.Info("Agent processing cancelled", "session_id", sess.ID)
123+ slog.Info("Agent processing cancelled", "session_id", sess.ID)
124124 return nil
125125 }
126126 return fmt.Errorf("agent processing failed: %w", result.Error)
@@ -139,7 +139,7 @@ func (a *App) RunNonInteractive(ctx context.Context, prompt string, outputFormat
139139
140140 fmt.Println(format.FormatOutput(content, outputFormat))
141141
142− logging.Info("Non-interactive run completed", "session_id", sess.ID)
142+ slog.Info("Non-interactive run completed", "session_id", sess.ID)
143143
144144 return nil
145145}
@@ -163,7 +163,7 @@ func (app *App) Shutdown() {
163163 for name, client := range clients {
164164 shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
165165 if err := client.Shutdown(shutdownCtx); err != nil {
166− logging.Error("Failed to shutdown LSP client", "name", name, "error", err)
166+ slog.Error("Failed to shutdown LSP client", "name", name, "error", err)
167167 }
168168 cancel()
169169 }
internal/app/lsp.go+13−12
@@ -2,10 +2,11 @@ package app
22
33import (
44 "context"
5+ "log/slog"
56 "time"
67
78 "github.com/charmbracelet/crush/internal/config"
8− "github.com/charmbracelet/crush/internal/logging"
9+ "github.com/charmbracelet/crush/internal/log"
910 "github.com/charmbracelet/crush/internal/lsp"
1011 "github.com/charmbracelet/crush/internal/lsp/watcher"
1112)
@@ -18,18 +19,18 @@ func (app *App) initLSPClients(ctx context.Context) {
1819 // Start each client initialization in its own goroutine
1920 go app.createAndStartLSPClient(ctx, name, clientConfig.Command, clientConfig.Args...)
2021 }
21− logging.Info("LSP clients initialization started in background")
22+ slog.Info("LSP clients initialization started in background")
2223}
2324
2425// createAndStartLSPClient creates a new LSP client, initializes it, and starts its workspace watcher
2526func (app *App) createAndStartLSPClient(ctx context.Context, name string, command string, args ...string) {
2627 // Create a specific context for initialization with a timeout
27− logging.Info("Creating LSP client", "name", name, "command", command, "args", args)
28+ slog.Info("Creating LSP client", "name", name, "command", command, "args", args)
2829
2930 // Create the LSP client
3031 lspClient, err := lsp.NewClient(ctx, command, args...)
3132 if err != nil {
32− logging.Error("Failed to create LSP client for", name, err)
33+ slog.Error("Failed to create LSP client for", name, err)
3334 return
3435 }
3536
@@ -40,7 +41,7 @@ func (app *App) createAndStartLSPClient(ctx context.Context, name string, comman
4041 // Initialize with the initialization context
4142 _, err = lspClient.InitializeLSPClient(initCtx, config.Get().WorkingDir())
4243 if err != nil {
43− logging.Error("Initialize failed", "name", name, "error", err)
44+ slog.Error("Initialize failed", "name", name, "error", err)
4445 // Clean up the client to prevent resource leaks
4546 lspClient.Close()
4647 return
@@ -48,15 +49,15 @@ func (app *App) createAndStartLSPClient(ctx context.Context, name string, comman
4849
4950 // Wait for the server to be ready
5051 if err := lspClient.WaitForServerReady(initCtx); err != nil {
51− logging.Error("Server failed to become ready", "name", name, "error", err)
52+ slog.Error("Server failed to become ready", "name", name, "error", err)
5253 // We'll continue anyway, as some functionality might still work
5354 lspClient.SetServerState(lsp.StateError)
5455 } else {
55− logging.Info("LSP server is ready", "name", name)
56+ slog.Info("LSP server is ready", "name", name)
5657 lspClient.SetServerState(lsp.StateReady)
5758 }
5859
59− logging.Info("LSP client initialized", "name", name)
60+ slog.Info("LSP client initialized", "name", name)
6061
6162 // Create a child context that can be canceled when the app is shutting down
6263 watchCtx, cancelFunc := context.WithCancel(ctx)
@@ -86,13 +87,13 @@ func (app *App) createAndStartLSPClient(ctx context.Context, name string, comman
8687// runWorkspaceWatcher executes the workspace watcher for an LSP client
8788func (app *App) runWorkspaceWatcher(ctx context.Context, name string, workspaceWatcher *watcher.WorkspaceWatcher) {
8889 defer app.watcherWG.Done()
89− defer logging.RecoverPanic("LSP-"+name, func() {
90+ defer log.RecoverPanic("LSP-"+name, func() {
9091 // Try to restart the client
9192 app.restartLSPClient(ctx, name)
9293 })
9394
9495 workspaceWatcher.WatchWorkspace(ctx, config.Get().WorkingDir())
95− logging.Info("Workspace watcher stopped", "client", name)
96+ slog.Info("Workspace watcher stopped", "client", name)
9697}
9798
9899// restartLSPClient attempts to restart a crashed or failed LSP client
@@ -101,7 +102,7 @@ func (app *App) restartLSPClient(ctx context.Context, name string) {
101102 cfg := config.Get()
102103 clientConfig, exists := cfg.LSP[name]
103104 if !exists {
104− logging.Error("Cannot restart client, configuration not found", "client", name)
105+ slog.Error("Cannot restart client, configuration not found", "client", name)
105106 return
106107 }
107108
@@ -122,5 +123,5 @@ func (app *App) restartLSPClient(ctx context.Context, name string) {
122123
123124 // Create a new client using the shared function
124125 app.createAndStartLSPClient(ctx, name, clientConfig.Command, clientConfig.Args...)
125− logging.Info("Successfully restarted LSP client", "client", name)
126+ slog.Info("Successfully restarted LSP client", "client", name)
126127}
internal/config/init.go+2−2
@@ -8,7 +8,7 @@ import (
88 "sync"
99 "sync/atomic"
1010
11− "github.com/charmbracelet/crush/internal/logging"
11+ "log/slog"
1212)
1313
1414const (
@@ -32,7 +32,7 @@ func Init(workingDir string, debug bool) (*Config, error) {
3232 cwd = workingDir
3333 cfg, err := Load(cwd, debug)
3434 if err != nil {
35− logging.Error("Failed to load config", "error", err)
35+ slog.Error("Failed to load config", "error", err)
3636 }
3737 instance.Store(cfg)
3838 })
internal/config/load.go+5−2
@@ -42,6 +42,11 @@ func Load(workingDir string, debug bool) (*Config, error) {
4242 filepath.Join(workingDir, fmt.Sprintf(".%s.json", appName)),
4343 }
4444 cfg, err := loadFromConfigPaths(configPaths)
45+ if err != nil {
46+ return nil, fmt.Errorf("failed to load config from paths %v: %w", configPaths, err)
47+ }
48+
49+ cfg.setDefaults(workingDir)
4550
4651 if debug {
4752 cfg.Options.Debug = true
@@ -57,8 +62,6 @@ func Load(workingDir string, debug bool) (*Config, error) {
5762 return nil, fmt.Errorf("failed to load config: %w", err)
5863 }
5964
60− cfg.setDefaults(workingDir)
61−
6265 // Load known providers, this loads the config from fur
6366 providers, err := LoadProviders(client.New())
6467 if err != nil || len(providers) == 0 {
internal/db/connect.go+5−5
@@ -11,7 +11,7 @@ import (
1111 _ "github.com/ncruces/go-sqlite3/embed"
1212
1313 "github.com/charmbracelet/crush/internal/config"
14− "github.com/charmbracelet/crush/internal/logging"
14+ "log/slog"
1515
1616 "github.com/pressly/goose/v3"
1717)
@@ -48,21 +48,21 @@ func Connect(ctx context.Context) (*sql.DB, error) {
4848
4949 for _, pragma := range pragmas {
5050 if _, err = db.ExecContext(ctx, pragma); err != nil {
51− logging.Error("Failed to set pragma", pragma, err)
51+ slog.Error("Failed to set pragma", pragma, err)
5252 } else {
53− logging.Debug("Set pragma", "pragma", pragma)
53+ slog.Debug("Set pragma", "pragma", pragma)
5454 }
5555 }
5656
5757 goose.SetBaseFS(FS)
5858
5959 if err := goose.SetDialect("sqlite3"); err != nil {
60− logging.Error("Failed to set dialect", "error", err)
60+ slog.Error("Failed to set dialect", "error", err)
6161 return nil, fmt.Errorf("failed to set dialect: %w", err)
6262 }
6363
6464 if err := goose.Up(db, "migrations"); err != nil {
65− logging.Error("Failed to apply migrations", "error", err)
65+ slog.Error("Failed to apply migrations", "error", err)
6666 return nil, fmt.Errorf("failed to apply migrations: %w", err)
6767 }
6868 return db, nil
internal/fsext/fileutil.go+3−3
@@ -11,7 +11,7 @@ import (
1111
1212 "github.com/bmatcuk/doublestar/v4"
1313 "github.com/charlievieth/fastwalk"
14− "github.com/charmbracelet/crush/internal/logging"
14+ "log/slog"
1515 ignore "github.com/sabhiram/go-gitignore"
1616)
1717
@@ -24,11 +24,11 @@ func init() {
2424 var err error
2525 rgPath, err = exec.LookPath("rg")
2626 if err != nil {
27− logging.Warn("Ripgrep (rg) not found in $PATH. Some features might be limited or slower.")
27+ slog.Warn("Ripgrep (rg) not found in $PATH. Some features might be limited or slower.")
2828 }
2929 fzfPath, err = exec.LookPath("fzf")
3030 if err != nil {
31− logging.Warn("FZF not found in $PATH. Some features might be limited or slower.")
31+ slog.Warn("FZF not found in $PATH. Some features might be limited or slower.")
3232 }
3333}
3434
internal/llm/agent/agent.go+16−19
@@ -4,6 +4,7 @@ import (
44 "context"
55 "errors"
66 "fmt"
7+ "log/slog"
78 "slices"
89 "strings"
910 "sync"
@@ -15,7 +16,7 @@ import (
1516 "github.com/charmbracelet/crush/internal/llm/prompt"
1617 "github.com/charmbracelet/crush/internal/llm/provider"
1718 "github.com/charmbracelet/crush/internal/llm/tools"
18− "github.com/charmbracelet/crush/internal/logging"
19+ "github.com/charmbracelet/crush/internal/log"
1920 "github.com/charmbracelet/crush/internal/lsp"
2021 "github.com/charmbracelet/crush/internal/message"
2122 "github.com/charmbracelet/crush/internal/permission"
@@ -223,7 +224,7 @@ func (a *agent) Cancel(sessionID string) {
223224 // Cancel regular requests
224225 if cancelFunc, exists := a.activeRequests.LoadAndDelete(sessionID); exists {
225226 if cancel, ok := cancelFunc.(context.CancelFunc); ok {
226− logging.InfoPersist(fmt.Sprintf("Request cancellation initiated for session: %s", sessionID))
227+ slog.Info(fmt.Sprintf("Request cancellation initiated for session: %s", sessionID))
227228 cancel()
228229 }
229230 }
@@ -231,7 +232,7 @@ func (a *agent) Cancel(sessionID string) {
231232 // Also check for summarize requests
232233 if cancelFunc, exists := a.activeRequests.LoadAndDelete(sessionID + "-summarize"); exists {
233234 if cancel, ok := cancelFunc.(context.CancelFunc); ok {
234− logging.InfoPersist(fmt.Sprintf("Summarize cancellation initiated for session: %s", sessionID))
235+ slog.Info(fmt.Sprintf("Summarize cancellation initiated for session: %s", sessionID))
235236 cancel()
236237 }
237238 }
@@ -325,8 +326,8 @@ func (a *agent) Run(ctx context.Context, sessionID string, content string, attac
325326
326327 a.activeRequests.Store(sessionID, cancel)
327328 go func() {
328− logging.Debug("Request started", "sessionID", sessionID)
329− defer logging.RecoverPanic("agent.Run", func() {
329+ slog.Debug("Request started", "sessionID", sessionID)
330+ defer log.RecoverPanic("agent.Run", func() {
330331 events <- a.err(fmt.Errorf("panic while running the agent"))
331332 })
332333 var attachmentParts []message.ContentPart
@@ -335,9 +336,9 @@ func (a *agent) Run(ctx context.Context, sessionID string, content string, attac
335336 }
336337 result := a.processGeneration(genCtx, sessionID, content, attachmentParts)
337338 if result.Error != nil && !errors.Is(result.Error, ErrRequestCancelled) && !errors.Is(result.Error, context.Canceled) {
338− logging.ErrorPersist(result.Error.Error())
339+ slog.Error(result.Error.Error())
339340 }
340− logging.Debug("Request completed", "sessionID", sessionID)
341+ slog.Debug("Request completed", "sessionID", sessionID)
341342 a.activeRequests.Delete(sessionID)
342343 cancel()
343344 a.Publish(pubsub.CreatedEvent, result)
@@ -356,12 +357,12 @@ func (a *agent) processGeneration(ctx context.Context, sessionID, content string
356357 }
357358 if len(msgs) == 0 {
358359 go func() {
359− defer logging.RecoverPanic("agent.Run", func() {
360− logging.ErrorPersist("panic while generating title")
360+ defer log.RecoverPanic("agent.Run", func() {
361+ slog.Error("panic while generating title")
361362 })
362363 titleErr := a.generateTitle(context.Background(), sessionID, content)
363364 if titleErr != nil && !errors.Is(titleErr, context.Canceled) && !errors.Is(titleErr, context.DeadlineExceeded) {
364− logging.ErrorPersist(fmt.Sprintf("failed to generate title: %v", titleErr))
365+ slog.Error(fmt.Sprintf("failed to generate title: %v", titleErr))
365366 }
366367 }()
367368 }
@@ -408,11 +409,7 @@ func (a *agent) processGeneration(ctx context.Context, sessionID, content string
408409 return a.err(fmt.Errorf("failed to process events: %w", err))
409410 }
410411 if cfg.Options.Debug {
411− seqId := (len(msgHistory) + 1) / 2
412− toolResultFilepath := logging.WriteToolResultsJson(sessionID, seqId, toolResults)
413− logging.Info("Result", "message", agentMessage.FinishReason(), "toolResults", "{}", "filepath", toolResultFilepath)
414− } else {
415− logging.Info("Result", "message", agentMessage.FinishReason(), "toolResults", toolResults)
412+ slog.Info("Result", "message", agentMessage.FinishReason(), "toolResults", toolResults)
416413 }
417414 if (agentMessage.FinishReason() == message.FinishReasonToolUse) && toolResults != nil {
418415 // 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
571568 assistantMsg.AppendContent(event.Content)
572569 return a.messages.Update(ctx, *assistantMsg)
573570 case provider.EventToolUseStart:
574− logging.Info("Tool call started", "toolCall", event.ToolCall)
571+ slog.Info("Tool call started", "toolCall", event.ToolCall)
575572 assistantMsg.AddToolCall(*event.ToolCall)
576573 return a.messages.Update(ctx, *assistantMsg)
577574 case provider.EventToolUseDelta:
578575 assistantMsg.AppendToolCallInput(event.ToolCall.ID, event.ToolCall.Input)
579576 return a.messages.Update(ctx, *assistantMsg)
580577 case provider.EventToolUseStop:
581− logging.Info("Finished tool call", "toolCall", event.ToolCall)
578+ slog.Info("Finished tool call", "toolCall", event.ToolCall)
582579 assistantMsg.FinishToolCall(event.ToolCall.ID)
583580 return a.messages.Update(ctx, *assistantMsg)
584581 case provider.EventError:
585582 if errors.Is(event.Error, context.Canceled) {
586− logging.InfoPersist(fmt.Sprintf("Event processing canceled for session: %s", sessionID))
583+ slog.Info(fmt.Sprintf("Event processing canceled for session: %s", sessionID))
587584 return context.Canceled
588585 }
589− logging.ErrorPersist(event.Error.Error())
586+ slog.Error(event.Error.Error())
590587 return event.Error
591588 case provider.EventComplete:
592589 assistantMsg.SetToolCalls(event.Response.ToolCalls)
internal/llm/agent/mcp-tools.go+6−6
@@ -7,7 +7,7 @@ import (
77
88 "github.com/charmbracelet/crush/internal/config"
99 "github.com/charmbracelet/crush/internal/llm/tools"
10− "github.com/charmbracelet/crush/internal/logging"
10+ "log/slog"
1111 "github.com/charmbracelet/crush/internal/permission"
1212 "github.com/charmbracelet/crush/internal/version"
1313
@@ -164,13 +164,13 @@ func getTools(ctx context.Context, name string, m config.MCPConfig, permissions
164164
165165 _, err := c.Initialize(ctx, initRequest)
166166 if err != nil {
167− logging.Error("error initializing mcp client", "error", err)
167+ slog.Error("error initializing mcp client", "error", err)
168168 return stdioTools
169169 }
170170 toolsRequest := mcp.ListToolsRequest{}
171171 tools, err := c.ListTools(ctx, toolsRequest)
172172 if err != nil {
173− logging.Error("error listing tools", "error", err)
173+ slog.Error("error listing tools", "error", err)
174174 return stdioTools
175175 }
176176 for _, t := range tools.Tools {
@@ -193,7 +193,7 @@ func GetMcpTools(ctx context.Context, permissions permission.Service) []tools.Ba
193193 m.Args...,
194194 )
195195 if err != nil {
196− logging.Error("error creating mcp client", "error", err)
196+ slog.Error("error creating mcp client", "error", err)
197197 continue
198198 }
199199
@@ -204,7 +204,7 @@ func GetMcpTools(ctx context.Context, permissions permission.Service) []tools.Ba
204204 transport.WithHTTPHeaders(m.Headers),
205205 )
206206 if err != nil {
207− logging.Error("error creating mcp client", "error", err)
207+ slog.Error("error creating mcp client", "error", err)
208208 continue
209209 }
210210 mcpTools = append(mcpTools, getTools(ctx, name, m, permissions, c)...)
@@ -214,7 +214,7 @@ func GetMcpTools(ctx context.Context, permissions permission.Service) []tools.Ba
214214 client.WithHeaders(m.Headers),
215215 )
216216 if err != nil {
217− logging.Error("error creating mcp client", "error", err)
217+ slog.Error("error creating mcp client", "error", err)
218218 continue
219219 }
220220 mcpTools = append(mcpTools, getTools(ctx, name, m, permissions, c)...)
internal/llm/prompt/coder.go+2−2
@@ -11,7 +11,7 @@ import (
1111 "github.com/charmbracelet/crush/internal/config"
1212 "github.com/charmbracelet/crush/internal/fur/provider"
1313 "github.com/charmbracelet/crush/internal/llm/tools"
14− "github.com/charmbracelet/crush/internal/logging"
14+ "log/slog"
1515)
1616
1717func CoderPrompt(p string, contextFiles ...string) string {
@@ -29,7 +29,7 @@ func CoderPrompt(p string, contextFiles ...string) string {
2929 basePrompt = fmt.Sprintf("%s\n\n%s\n%s", basePrompt, envInfo, lspInformation())
3030
3131 contextContent := getContextFromPaths(contextFiles)
32− logging.Debug("Context content", "Context", contextContent)
32+ slog.Debug("Context content", "Context", contextContent)
3333 if contextContent != "" {
3434 return fmt.Sprintf("%s\n\n# Project-Specific Context\n Make sure to follow the instructions in the context below\n%s", basePrompt, contextContent)
3535 }
internal/llm/provider/anthropic.go+9−9
@@ -6,6 +6,7 @@ import (
66 "errors"
77 "fmt"
88 "io"
9+ "log/slog"
910 "regexp"
1011 "strconv"
1112 "time"
@@ -16,7 +17,6 @@ import (
1617 "github.com/charmbracelet/crush/internal/config"
1718 "github.com/charmbracelet/crush/internal/fur/provider"
1819 "github.com/charmbracelet/crush/internal/llm/tools"
19− "github.com/charmbracelet/crush/internal/logging"
2020 "github.com/charmbracelet/crush/internal/message"
2121)
2222
@@ -92,7 +92,7 @@ func (a *anthropicClient) convertMessages(messages []message.Message) (anthropic
9292 }
9393
9494 if len(blocks) == 0 {
95− logging.Warn("There is a message without content, investigate, this should not happen")
95+ slog.Warn("There is a message without content, investigate, this should not happen")
9696 continue
9797 }
9898 anthropicMessages = append(anthropicMessages, anthropic.NewAssistantMessage(blocks...))
@@ -207,7 +207,7 @@ func (a *anthropicClient) send(ctx context.Context, messages []message.Message,
207207 preparedMessages := a.preparedMessages(a.convertMessages(messages), a.convertTools(tools))
208208 if cfg.Options.Debug {
209209 jsonData, _ := json.Marshal(preparedMessages)
210− logging.Debug("Prepared messages", "messages", string(jsonData))
210+ slog.Debug("Prepared messages", "messages", string(jsonData))
211211 }
212212
213213 anthropicResponse, err := a.client.Messages.New(
@@ -216,13 +216,13 @@ func (a *anthropicClient) send(ctx context.Context, messages []message.Message,
216216 )
217217 // If there is an error we are going to see if we can retry the call
218218 if err != nil {
219− logging.Error("Error in Anthropic API call", "error", err)
219+ slog.Error("Error in Anthropic API call", "error", err)
220220 retry, after, retryErr := a.shouldRetry(attempts, err)
221221 if retryErr != nil {
222222 return nil, retryErr
223223 }
224224 if retry {
225− logging.WarnPersist(fmt.Sprintf("Retrying due to rate limit... attempt %d of %d", attempts, maxRetries), logging.PersistTimeArg, time.Millisecond*time.Duration(after+100))
225+ slog.Warn(fmt.Sprintf("Retrying due to rate limit... attempt %d of %d", attempts, maxRetries))
226226 select {
227227 case <-ctx.Done():
228228 return nil, ctx.Err()
@@ -259,7 +259,7 @@ func (a *anthropicClient) stream(ctx context.Context, messages []message.Message
259259 preparedMessages := a.preparedMessages(a.convertMessages(messages), a.convertTools(tools))
260260 if cfg.Options.Debug {
261261 jsonData, _ := json.Marshal(preparedMessages)
262− logging.Debug("Prepared messages", "messages", string(jsonData))
262+ slog.Debug("Prepared messages", "messages", string(jsonData))
263263 }
264264
265265 anthropicStream := a.client.Messages.NewStreaming(
@@ -273,7 +273,7 @@ func (a *anthropicClient) stream(ctx context.Context, messages []message.Message
273273 event := anthropicStream.Current()
274274 err := accumulatedMessage.Accumulate(event)
275275 if err != nil {
276− logging.Warn("Error accumulating message", "error", err)
276+ slog.Warn("Error accumulating message", "error", err)
277277 continue
278278 }
279279
@@ -364,7 +364,7 @@ func (a *anthropicClient) stream(ctx context.Context, messages []message.Message
364364 return
365365 }
366366 if retry {
367− logging.WarnPersist(fmt.Sprintf("Retrying due to rate limit... attempt %d of %d", attempts, maxRetries), logging.PersistTimeArg, time.Millisecond*time.Duration(after+100))
367+ slog.Warn(fmt.Sprintf("Retrying due to rate limit... attempt %d of %d", attempts, maxRetries))
368368 select {
369369 case <-ctx.Done():
370370 // context cancelled
@@ -411,7 +411,7 @@ func (a *anthropicClient) shouldRetry(attempts int, err error) (bool, int64, err
411411 if apiErr.StatusCode == 400 {
412412 if adjusted, ok := a.handleContextLimitError(apiErr); ok {
413413 a.adjustedMaxTokens = adjusted
414− logging.Debug("Adjusted max_tokens due to context limit", "new_max_tokens", adjusted)
414+ slog.Debug("Adjusted max_tokens due to context limit", "new_max_tokens", adjusted)
415415 return true, 0, nil
416416 }
417417 }
internal/llm/provider/gemini.go+6−6
@@ -6,13 +6,13 @@ import (
66 "errors"
77 "fmt"
88 "io"
9+ "log/slog"
910 "strings"
1011 "time"
1112
1213 "github.com/charmbracelet/crush/internal/config"
1314 "github.com/charmbracelet/crush/internal/fur/provider"
1415 "github.com/charmbracelet/crush/internal/llm/tools"
15− "github.com/charmbracelet/crush/internal/logging"
1616 "github.com/charmbracelet/crush/internal/message"
1717 "github.com/google/uuid"
1818 "google.golang.org/genai"
@@ -28,7 +28,7 @@ type GeminiClient ProviderClient
2828func newGeminiClient(opts providerClientOptions) GeminiClient {
2929 client, err := createGeminiClient(opts)
3030 if err != nil {
31− logging.Error("Failed to create Gemini client", "error", err)
31+ slog.Error("Failed to create Gemini client", "error", err)
3232 return nil
3333 }
3434
@@ -168,7 +168,7 @@ func (g *geminiClient) send(ctx context.Context, messages []message.Message, too
168168 cfg := config.Get()
169169 if cfg.Options.Debug {
170170 jsonData, _ := json.Marshal(geminiMessages)
171− logging.Debug("Prepared messages", "messages", string(jsonData))
171+ slog.Debug("Prepared messages", "messages", string(jsonData))
172172 }
173173
174174 modelConfig := cfg.Models[config.SelectedModelTypeLarge]
@@ -210,7 +210,7 @@ func (g *geminiClient) send(ctx context.Context, messages []message.Message, too
210210 return nil, retryErr
211211 }
212212 if retry {
213− logging.WarnPersist(fmt.Sprintf("Retrying due to rate limit... attempt %d of %d", attempts, maxRetries), logging.PersistTimeArg, time.Millisecond*time.Duration(after+100))
213+ slog.Warn(fmt.Sprintf("Retrying due to rate limit... attempt %d of %d", attempts, maxRetries))
214214 select {
215215 case <-ctx.Done():
216216 return nil, ctx.Err()
@@ -266,7 +266,7 @@ func (g *geminiClient) stream(ctx context.Context, messages []message.Message, t
266266 cfg := config.Get()
267267 if cfg.Options.Debug {
268268 jsonData, _ := json.Marshal(geminiMessages)
269− logging.Debug("Prepared messages", "messages", string(jsonData))
269+ slog.Debug("Prepared messages", "messages", string(jsonData))
270270 }
271271
272272 modelConfig := cfg.Models[config.SelectedModelTypeLarge]
@@ -323,7 +323,7 @@ func (g *geminiClient) stream(ctx context.Context, messages []message.Message, t
323323 return
324324 }
325325 if retry {
326− logging.WarnPersist(fmt.Sprintf("Retrying due to rate limit... attempt %d of %d", attempts, maxRetries), logging.PersistTimeArg, time.Millisecond*time.Duration(after+100))
326+ slog.Warn(fmt.Sprintf("Retrying due to rate limit... attempt %d of %d", attempts, maxRetries))
327327 select {
328328 case <-ctx.Done():
329329 if ctx.Err() != nil {
internal/llm/provider/openai.go+6−6
@@ -6,12 +6,12 @@ import (
66 "errors"
77 "fmt"
88 "io"
9+ "log/slog"
910 "time"
1011
1112 "github.com/charmbracelet/crush/internal/config"
1213 "github.com/charmbracelet/crush/internal/fur/provider"
1314 "github.com/charmbracelet/crush/internal/llm/tools"
14− "github.com/charmbracelet/crush/internal/logging"
1515 "github.com/charmbracelet/crush/internal/message"
1616 "github.com/openai/openai-go"
1717 "github.com/openai/openai-go/option"
@@ -194,7 +194,7 @@ func (o *openaiClient) send(ctx context.Context, messages []message.Message, too
194194 cfg := config.Get()
195195 if cfg.Options.Debug {
196196 jsonData, _ := json.Marshal(params)
197− logging.Debug("Prepared messages", "messages", string(jsonData))
197+ slog.Debug("Prepared messages", "messages", string(jsonData))
198198 }
199199 attempts := 0
200200 for {
@@ -210,7 +210,7 @@ func (o *openaiClient) send(ctx context.Context, messages []message.Message, too
210210 return nil, retryErr
211211 }
212212 if retry {
213− logging.WarnPersist(fmt.Sprintf("Retrying due to rate limit... attempt %d of %d", attempts, maxRetries), logging.PersistTimeArg, time.Millisecond*time.Duration(after+100))
213+ slog.Warn(fmt.Sprintf("Retrying due to rate limit... attempt %d of %d", attempts, maxRetries))
214214 select {
215215 case <-ctx.Done():
216216 return nil, ctx.Err()
@@ -251,7 +251,7 @@ func (o *openaiClient) stream(ctx context.Context, messages []message.Message, t
251251 cfg := config.Get()
252252 if cfg.Options.Debug {
253253 jsonData, _ := json.Marshal(params)
254− logging.Debug("Prepared messages", "messages", string(jsonData))
254+ slog.Debug("Prepared messages", "messages", string(jsonData))
255255 }
256256
257257 attempts := 0
@@ -288,7 +288,7 @@ func (o *openaiClient) stream(ctx context.Context, messages []message.Message, t
288288 if err == nil || errors.Is(err, io.EOF) {
289289 if cfg.Options.Debug {
290290 jsonData, _ := json.Marshal(acc.ChatCompletion)
291− logging.Debug("Response", "messages", string(jsonData))
291+ slog.Debug("Response", "messages", string(jsonData))
292292 }
293293 resultFinishReason := acc.ChatCompletion.Choices[0].FinishReason
294294 if resultFinishReason == "" {
@@ -326,7 +326,7 @@ func (o *openaiClient) stream(ctx context.Context, messages []message.Message, t
326326 return
327327 }
328328 if retry {
329− logging.WarnPersist(fmt.Sprintf("Retrying due to rate limit... attempt %d of %d", attempts, maxRetries), logging.PersistTimeArg, time.Millisecond*time.Duration(after+100))
329+ slog.Warn(fmt.Sprintf("Retrying due to rate limit... attempt %d of %d", attempts, maxRetries))
330330 select {
331331 case <-ctx.Done():
332332 // context cancelled
internal/llm/provider/vertexai.go+2−2
@@ -3,7 +3,7 @@ package provider
33import (
44 "context"
55
6− "github.com/charmbracelet/crush/internal/logging"
6+ "log/slog"
77 "google.golang.org/genai"
88)
99
@@ -18,7 +18,7 @@ func newVertexAIClient(opts providerClientOptions) VertexAIClient {
1818 Backend: genai.BackendVertexAI,
1919 })
2020 if err != nil {
21− logging.Error("Failed to create VertexAI client", "error", err)
21+ slog.Error("Failed to create VertexAI client", "error", err)
2222 return nil
2323 }
2424
internal/llm/tools/edit.go+6−6
@@ -12,7 +12,7 @@ import (
1212 "github.com/charmbracelet/crush/internal/config"
1313 "github.com/charmbracelet/crush/internal/diff"
1414 "github.com/charmbracelet/crush/internal/history"
15− "github.com/charmbracelet/crush/internal/logging"
15+ "log/slog"
1616 "github.com/charmbracelet/crush/internal/lsp"
1717 "github.com/charmbracelet/crush/internal/permission"
1818)
@@ -246,7 +246,7 @@ func (e *editTool) createNewFile(ctx context.Context, filePath, content string)
246246 _, err = e.files.CreateVersion(ctx, sessionID, filePath, content)
247247 if err != nil {
248248 // Log error but don't fail the operation
249− logging.Debug("Error creating file history version", "error", err)
249+ slog.Debug("Error creating file history version", "error", err)
250250 }
251251
252252 recordFileWrite(filePath)
@@ -361,13 +361,13 @@ func (e *editTool) deleteContent(ctx context.Context, filePath, oldString string
361361 // User Manually changed the content store an intermediate version
362362 _, err = e.files.CreateVersion(ctx, sessionID, filePath, oldContent)
363363 if err != nil {
364− logging.Debug("Error creating file history version", "error", err)
364+ slog.Debug("Error creating file history version", "error", err)
365365 }
366366 }
367367 // Store the new version
368368 _, err = e.files.CreateVersion(ctx, sessionID, filePath, "")
369369 if err != nil {
370− logging.Debug("Error creating file history version", "error", err)
370+ slog.Debug("Error creating file history version", "error", err)
371371 }
372372
373373 recordFileWrite(filePath)
@@ -483,13 +483,13 @@ func (e *editTool) replaceContent(ctx context.Context, filePath, oldString, newS
483483 // User Manually changed the content store an intermediate version
484484 _, err = e.files.CreateVersion(ctx, sessionID, filePath, oldContent)
485485 if err != nil {
486− logging.Debug("Error creating file history version", "error", err)
486+ slog.Debug("Error creating file history version", "error", err)
487487 }
488488 }
489489 // Store the new version
490490 _, err = e.files.CreateVersion(ctx, sessionID, filePath, newContent)
491491 if err != nil {
492− logging.Debug("Error creating file history version", "error", err)
492+ slog.Debug("Error creating file history version", "error", err)
493493 }
494494
495495 recordFileWrite(filePath)
internal/llm/tools/glob.go+2−2
@@ -12,7 +12,7 @@ import (
1212
1313 "github.com/charmbracelet/crush/internal/config"
1414 "github.com/charmbracelet/crush/internal/fsext"
15− "github.com/charmbracelet/crush/internal/logging"
15+ "log/slog"
1616)
1717
1818const (
@@ -143,7 +143,7 @@ func globFiles(pattern, searchPath string, limit int) ([]string, bool, error) {
143143 if err == nil {
144144 return matches, len(matches) >= limit && limit > 0, nil
145145 }
146− logging.Warn(fmt.Sprintf("Ripgrep execution failed: %v. Falling back to doublestar.", err))
146+ slog.Warn(fmt.Sprintf("Ripgrep execution failed: %v. Falling back to doublestar.", err))
147147 }
148148
149149 return fsext.GlobWithDoubleStar(pattern, searchPath, limit)
internal/llm/tools/write.go+3−3
@@ -12,7 +12,7 @@ import (
1212 "github.com/charmbracelet/crush/internal/config"
1313 "github.com/charmbracelet/crush/internal/diff"
1414 "github.com/charmbracelet/crush/internal/history"
15− "github.com/charmbracelet/crush/internal/logging"
15+ "log/slog"
1616 "github.com/charmbracelet/crush/internal/lsp"
1717 "github.com/charmbracelet/crush/internal/permission"
1818)
@@ -211,13 +211,13 @@ func (w *writeTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error
211211 // User Manually changed the content store an intermediate version
212212 _, err = w.files.CreateVersion(ctx, sessionID, filePath, oldContent)
213213 if err != nil {
214− logging.Debug("Error creating file history version", "error", err)
214+ slog.Debug("Error creating file history version", "error", err)
215215 }
216216 }
217217 // Store the new version
218218 _, err = w.files.CreateVersion(ctx, sessionID, filePath, params.Content)
219219 if err != nil {
220− logging.Debug("Error creating file history version", "error", err)
220+ slog.Debug("Error creating file history version", "error", err)
221221 }
222222
223223 recordFileWrite(filePath)
internal/log/log.go+27
@@ -1,8 +1,12 @@
11package log
22
33import (
4+ "fmt"
45 "log/slog"
6+ "os"
7+ "runtime/debug"
58 "sync"
9+ "time"
610
711 "gopkg.in/natefinch/lumberjack.v2"
812)
@@ -32,3 +36,26 @@ func Init(logFile string, debug bool) {
3236 slog.SetDefault(slog.New(logger))
3337 })
3438}
39+
40+func RecoverPanic(name string, cleanup func()) {
41+ if r := recover(); r != nil {
42+ // Create a timestamped panic log file
43+ timestamp := time.Now().Format("20060102-150405")
44+ filename := fmt.Sprintf("crush-panic-%s-%s.log", name, timestamp)
45+
46+ file, err := os.Create(filename)
47+ if err == nil {
48+ defer file.Close()
49+
50+ // Write panic information and stack trace
51+ fmt.Fprintf(file, "Panic in %s: %v\n\n", name, r)
52+ fmt.Fprintf(file, "Time: %s\n\n", time.Now().Format(time.RFC3339))
53+ fmt.Fprintf(file, "Stack Trace:\n%s\n", debug.Stack())
54+
55+ // Execute cleanup function if provided
56+ if cleanup != nil {
57+ cleanup()
58+ }
59+ }
60+ }
61+}
internal/logging/logger.godeleted−209
@@ -1,209 +0,0 @@
1−package logging
2−
3−import (
4− "fmt"
5− "log/slog"
6− "os"
7−
8− // "path/filepath"
9− "encoding/json"
10− "runtime"
11− "runtime/debug"
12− "sync"
13− "time"
14−)
15−
16−func getCaller() string {
17− var caller string
18− if _, file, line, ok := runtime.Caller(2); ok {
19− // caller = fmt.Sprintf("%s:%d", filepath.Base(file), line)
20− caller = fmt.Sprintf("%s:%d", file, line)
21− } else {
22− caller = "unknown"
23− }
24− return caller
25−}
26−
27−func Info(msg string, args ...any) {
28− source := getCaller()
29− slog.Info(msg, append([]any{"source", source}, args...)...)
30−}
31−
32−func Debug(msg string, args ...any) {
33− // slog.Debug(msg, args...)
34− source := getCaller()
35− slog.Debug(msg, append([]any{"source", source}, args...)...)
36−}
37−
38−func Warn(msg string, args ...any) {
39− slog.Warn(msg, args...)
40−}
41−
42−func Error(msg string, args ...any) {
43− slog.Error(msg, args...)
44−}
45−
46−func InfoPersist(msg string, args ...any) {
47− args = append(args, persistKeyArg, true)
48− slog.Info(msg, args...)
49−}
50−
51−func DebugPersist(msg string, args ...any) {
52− args = append(args, persistKeyArg, true)
53− slog.Debug(msg, args...)
54−}
55−
56−func WarnPersist(msg string, args ...any) {
57− args = append(args, persistKeyArg, true)
58− slog.Warn(msg, args...)
59−}
60−
61−func ErrorPersist(msg string, args ...any) {
62− args = append(args, persistKeyArg, true)
63− slog.Error(msg, args...)
64−}
65−
66−// RecoverPanic is a common function to handle panics gracefully.
67−// It logs the error, creates a panic log file with stack trace,
68−// and executes an optional cleanup function before returning.
69−func RecoverPanic(name string, cleanup func()) {
70− if r := recover(); r != nil {
71− // Log the panic
72− ErrorPersist(fmt.Sprintf("Panic in %s: %v", name, r))
73−
74− // Create a timestamped panic log file
75− timestamp := time.Now().Format("20060102-150405")
76− filename := fmt.Sprintf("crush-panic-%s-%s.log", name, timestamp)
77−
78− file, err := os.Create(filename)
79− if err != nil {
80− ErrorPersist(fmt.Sprintf("Failed to create panic log: %v", err))
81− } else {
82− defer file.Close()
83−
84− // Write panic information and stack trace
85− fmt.Fprintf(file, "Panic in %s: %v\n\n", name, r)
86− fmt.Fprintf(file, "Time: %s\n\n", time.Now().Format(time.RFC3339))
87− fmt.Fprintf(file, "Stack Trace:\n%s\n", debug.Stack())
88−
89− InfoPersist(fmt.Sprintf("Panic details written to %s", filename))
90− }
91−
92− // Execute cleanup function if provided
93− if cleanup != nil {
94− cleanup()
95− }
96− }
97−}
98−
99−// Message Logging for Debug
100−var MessageDir string
101−
102−func GetSessionPrefix(sessionId string) string {
103− return sessionId[:8]
104−}
105−
106−var sessionLogMutex sync.Mutex
107−
108−func AppendToSessionLogFile(sessionId string, filename string, content string) string {
109− if MessageDir == "" || sessionId == "" {
110− return ""
111− }
112− sessionPrefix := GetSessionPrefix(sessionId)
113−
114− sessionLogMutex.Lock()
115− defer sessionLogMutex.Unlock()
116−
117− sessionPath := fmt.Sprintf("%s/%s", MessageDir, sessionPrefix)
118− if _, err := os.Stat(sessionPath); os.IsNotExist(err) {
119− if err := os.MkdirAll(sessionPath, 0o766); err != nil {
120− Error("Failed to create session directory", "dirpath", sessionPath, "error", err)
121− return ""
122− }
123− }
124−
125− filePath := fmt.Sprintf("%s/%s", sessionPath, filename)
126−
127− f, err := os.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
128− if err != nil {
129− Error("Failed to open session log file", "filepath", filePath, "error", err)
130− return ""
131− }
132− defer f.Close()
133−
134− // Append chunk to file
135− _, err = f.WriteString(content)
136− if err != nil {
137− Error("Failed to write chunk to session log file", "filepath", filePath, "error", err)
138− return ""
139− }
140− return filePath
141−}
142−
143−func WriteRequestMessageJson(sessionId string, requestSeqId int, message any) string {
144− if MessageDir == "" || sessionId == "" || requestSeqId <= 0 {
145− return ""
146− }
147− msgJson, err := json.Marshal(message)
148− if err != nil {
149− Error("Failed to marshal message", "session_id", sessionId, "request_seq_id", requestSeqId, "error", err)
150− return ""
151− }
152− return WriteRequestMessage(sessionId, requestSeqId, string(msgJson))
153−}
154−
155−func WriteRequestMessage(sessionId string, requestSeqId int, message string) string {
156− if MessageDir == "" || sessionId == "" || requestSeqId <= 0 {
157− return ""
158− }
159− filename := fmt.Sprintf("%d_request.json", requestSeqId)
160−
161− return AppendToSessionLogFile(sessionId, filename, message)
162−}
163−
164−func AppendToStreamSessionLogJson(sessionId string, requestSeqId int, jsonableChunk any) string {
165− if MessageDir == "" || sessionId == "" || requestSeqId <= 0 {
166− return ""
167− }
168− chunkJson, err := json.Marshal(jsonableChunk)
169− if err != nil {
170− Error("Failed to marshal message", "session_id", sessionId, "request_seq_id", requestSeqId, "error", err)
171− return ""
172− }
173− return AppendToStreamSessionLog(sessionId, requestSeqId, string(chunkJson))
174−}
175−
176−func AppendToStreamSessionLog(sessionId string, requestSeqId int, chunk string) string {
177− if MessageDir == "" || sessionId == "" || requestSeqId <= 0 {
178− return ""
179− }
180− filename := fmt.Sprintf("%d_response_stream.log", requestSeqId)
181− return AppendToSessionLogFile(sessionId, filename, chunk)
182−}
183−
184−func WriteChatResponseJson(sessionId string, requestSeqId int, response any) string {
185− if MessageDir == "" || sessionId == "" || requestSeqId <= 0 {
186− return ""
187− }
188− responseJson, err := json.Marshal(response)
189− if err != nil {
190− Error("Failed to marshal response", "session_id", sessionId, "request_seq_id", requestSeqId, "error", err)
191− return ""
192− }
193− filename := fmt.Sprintf("%d_response.json", requestSeqId)
194−
195− return AppendToSessionLogFile(sessionId, filename, string(responseJson))
196−}
197−
198−func WriteToolResultsJson(sessionId string, requestSeqId int, toolResults any) string {
199− if MessageDir == "" || sessionId == "" || requestSeqId <= 0 {
200− return ""
201− }
202− toolResultsJson, err := json.Marshal(toolResults)
203− if err != nil {
204− Error("Failed to marshal tool results", "session_id", sessionId, "request_seq_id", requestSeqId, "error", err)
205− return ""
206− }
207− filename := fmt.Sprintf("%d_tool_results.json", requestSeqId)
208− return AppendToSessionLogFile(sessionId, filename, string(toolResultsJson))
209−}
internal/logging/message.godeleted−21
@@ -1,21 +0,0 @@
1−package logging
2−
3−import (
4− "time"
5−)
6−
7−// LogMessage is the event payload for a log message
8−type LogMessage struct {
9− ID string
10− Time time.Time
11− Level string
12− Persist bool // used when we want to show the mesage in the status bar
13− PersistTime time.Duration // used when we want to show the mesage in the status bar
14− Message string `json:"msg"`
15− Attributes []Attr
16−}
17−
18−type Attr struct {
19− Key string
20− Value string
21−}
internal/logging/writer.godeleted−102
@@ -1,102 +0,0 @@
1−package logging
2−
3−import (
4− "bytes"
5− "context"
6− "fmt"
7− "strings"
8− "sync"
9− "time"
10−
11− "github.com/charmbracelet/crush/internal/pubsub"
12− "github.com/go-logfmt/logfmt"
13−)
14−
15−const (
16− persistKeyArg = "$_persist"
17− PersistTimeArg = "$_persist_time"
18−)
19−
20−type LogData struct {
21− messages []LogMessage
22− *pubsub.Broker[LogMessage]
23− lock sync.Mutex
24−}
25−
26−func (l *LogData) Add(msg LogMessage) {
27− l.lock.Lock()
28− defer l.lock.Unlock()
29− l.messages = append(l.messages, msg)
30− l.Publish(pubsub.CreatedEvent, msg)
31−}
32−
33−func (l *LogData) List() []LogMessage {
34− l.lock.Lock()
35− defer l.lock.Unlock()
36− return l.messages
37−}
38−
39−var defaultLogData = &LogData{
40− messages: make([]LogMessage, 0),
41− Broker: pubsub.NewBroker[LogMessage](),
42−}
43−
44−type writer struct{}
45−
46−func (w *writer) Write(p []byte) (int, error) {
47− d := logfmt.NewDecoder(bytes.NewReader(p))
48−
49− for d.ScanRecord() {
50− msg := LogMessage{
51− ID: fmt.Sprintf("%d", time.Now().UnixNano()),
52− Time: time.Now(),
53− }
54− for d.ScanKeyval() {
55− switch string(d.Key()) {
56− case "time":
57− parsed, err := time.Parse(time.RFC3339, string(d.Value()))
58− if err != nil {
59− return 0, fmt.Errorf("parsing time: %w", err)
60− }
61− msg.Time = parsed
62− case "level":
63− msg.Level = strings.ToLower(string(d.Value()))
64− case "msg":
65− msg.Message = string(d.Value())
66− default:
67− if string(d.Key()) == persistKeyArg {
68− msg.Persist = true
69− } else if string(d.Key()) == PersistTimeArg {
70− parsed, err := time.ParseDuration(string(d.Value()))
71− if err != nil {
72− continue
73− }
74− msg.PersistTime = parsed
75− } else {
76− msg.Attributes = append(msg.Attributes, Attr{
77− Key: string(d.Key()),
78− Value: string(d.Value()),
79− })
80− }
81− }
82− }
83− defaultLogData.Add(msg)
84− }
85− if d.Err() != nil {
86− return 0, d.Err()
87− }
88− return len(p), nil
89−}
90−
91−func NewWriter() *writer {
92− w := &writer{}
93− return w
94−}
95−
96−func Subscribe(ctx context.Context) <-chan pubsub.Event[LogMessage] {
97− return defaultLogData.Subscribe(ctx)
98−}
99−
100−func List() []LogMessage {
101− return defaultLogData.List()
102−}
internal/lsp/client.go+20−19
@@ -6,6 +6,7 @@ import (
66 "encoding/json"
77 "fmt"
88 "io"
9+ "log/slog"
910 "os"
1011 "os/exec"
1112 "path/filepath"
@@ -15,7 +16,7 @@ import (
1516 "time"
1617
1718 "github.com/charmbracelet/crush/internal/config"
18− "github.com/charmbracelet/crush/internal/logging"
19+ "github.com/charmbracelet/crush/internal/log"
1920 "github.com/charmbracelet/crush/internal/lsp/protocol"
2021)
2122
@@ -96,17 +97,17 @@ func NewClient(ctx context.Context, command string, args ...string) (*Client, er
9697 go func() {
9798 scanner := bufio.NewScanner(stderr)
9899 for scanner.Scan() {
99− logging.Error("LSP Server", "err", scanner.Text())
100+ slog.Error("LSP Server", "err", scanner.Text())
100101 }
101102 if err := scanner.Err(); err != nil {
102− logging.Error("Error reading", "err", err)
103+ slog.Error("Error reading", "err", err)
103104 }
104105 }()
105106
106107 // Start message handling loop
107108 go func() {
108− defer logging.RecoverPanic("LSP-message-handler", func() {
109− logging.ErrorPersist("LSP message handler crashed, LSP functionality may be impaired")
109+ defer log.RecoverPanic("LSP-message-handler", func() {
110+ slog.Error("LSP message handler crashed, LSP functionality may be impaired")
110111 })
111112 client.handleMessages()
112113 }()
@@ -300,7 +301,7 @@ func (c *Client) WaitForServerReady(ctx context.Context) error {
300301 defer ticker.Stop()
301302
302303 if cfg.Options.DebugLSP {
303− logging.Debug("Waiting for LSP server to be ready...")
304+ slog.Debug("Waiting for LSP server to be ready...")
304305 }
305306
306307 // Determine server type for specialized initialization
@@ -309,7 +310,7 @@ func (c *Client) WaitForServerReady(ctx context.Context) error {
309310 // For TypeScript-like servers, we need to open some key files first
310311 if serverType == ServerTypeTypeScript {
311312 if cfg.Options.DebugLSP {
312− logging.Debug("TypeScript-like server detected, opening key configuration files")
313+ slog.Debug("TypeScript-like server detected, opening key configuration files")
313314 }
314315 c.openKeyConfigFiles(ctx)
315316 }
@@ -326,15 +327,15 @@ func (c *Client) WaitForServerReady(ctx context.Context) error {
326327 // Server responded successfully
327328 c.SetServerState(StateReady)
328329 if cfg.Options.DebugLSP {
329− logging.Debug("LSP server is ready")
330+ slog.Debug("LSP server is ready")
330331 }
331332 return nil
332333 } else {
333− logging.Debug("LSP server not ready yet", "error", err, "serverType", serverType)
334+ slog.Debug("LSP server not ready yet", "error", err, "serverType", serverType)
334335 }
335336
336337 if cfg.Options.DebugLSP {
337− logging.Debug("LSP server not ready yet", "error", err, "serverType", serverType)
338+ slog.Debug("LSP server not ready yet", "error", err, "serverType", serverType)
338339 }
339340 }
340341 }
@@ -409,9 +410,9 @@ func (c *Client) openKeyConfigFiles(ctx context.Context) {
409410 if _, err := os.Stat(file); err == nil {
410411 // File exists, try to open it
411412 if err := c.OpenFile(ctx, file); err != nil {
412− logging.Debug("Failed to open key config file", "file", file, "error", err)
413+ slog.Debug("Failed to open key config file", "file", file, "error", err)
413414 } else {
414− logging.Debug("Opened key config file for initialization", "file", file)
415+ slog.Debug("Opened key config file for initialization", "file", file)
415416 }
416417 }
417418 }
@@ -487,7 +488,7 @@ func (c *Client) pingTypeScriptServer(ctx context.Context) error {
487488 return nil
488489 })
489490 if err != nil {
490− logging.Debug("Error walking directory for TypeScript files", "error", err)
491+ slog.Debug("Error walking directory for TypeScript files", "error", err)
491492 }
492493
493494 // Final fallback - just try a generic capability
@@ -527,7 +528,7 @@ func (c *Client) openTypeScriptFiles(ctx context.Context, workDir string) {
527528 if err := c.OpenFile(ctx, path); err == nil {
528529 filesOpened++
529530 if cfg.Options.DebugLSP {
530− logging.Debug("Opened TypeScript file for initialization", "file", path)
531+ slog.Debug("Opened TypeScript file for initialization", "file", path)
531532 }
532533 }
533534 }
@@ -536,11 +537,11 @@ func (c *Client) openTypeScriptFiles(ctx context.Context, workDir string) {
536537 })
537538
538539 if err != nil && cfg.Options.DebugLSP {
539− logging.Debug("Error walking directory for TypeScript files", "error", err)
540+ slog.Debug("Error walking directory for TypeScript files", "error", err)
540541 }
541542
542543 if cfg.Options.DebugLSP {
543− logging.Debug("Opened TypeScript files for initialization", "count", filesOpened)
544+ slog.Debug("Opened TypeScript files for initialization", "count", filesOpened)
544545 }
545546}
546547
@@ -681,7 +682,7 @@ func (c *Client) CloseFile(ctx context.Context, filepath string) error {
681682 }
682683
683684 if cfg.Options.DebugLSP {
684− logging.Debug("Closing file", "file", filepath)
685+ slog.Debug("Closing file", "file", filepath)
685686 }
686687 if err := c.Notify(ctx, "textDocument/didClose", params); err != nil {
687688 return err
@@ -720,12 +721,12 @@ func (c *Client) CloseAllFiles(ctx context.Context) {
720721 for _, filePath := range filesToClose {
721722 err := c.CloseFile(ctx, filePath)
722723 if err != nil && cfg.Options.DebugLSP {
723− logging.Warn("Error closing file", "file", filePath, "error", err)
724+ slog.Warn("Error closing file", "file", filePath, "error", err)
724725 }
725726 }
726727
727728 if cfg.Options.DebugLSP {
728− logging.Debug("Closed all files", "files", filesToClose)
729+ slog.Debug("Closed all files", "files", filesToClose)
729730 }
730731}
731732
internal/lsp/handlers.go+7−7
@@ -4,7 +4,7 @@ import (
44 "encoding/json"
55
66 "github.com/charmbracelet/crush/internal/config"
7− "github.com/charmbracelet/crush/internal/logging"
7+ "log/slog"
88 "github.com/charmbracelet/crush/internal/lsp/protocol"
99 "github.com/charmbracelet/crush/internal/lsp/util"
1010)
@@ -18,7 +18,7 @@ func HandleWorkspaceConfiguration(params json.RawMessage) (any, error) {
1818func HandleRegisterCapability(params json.RawMessage) (any, error) {
1919 var registerParams protocol.RegistrationParams
2020 if err := json.Unmarshal(params, &registerParams); err != nil {
21− logging.Error("Error unmarshaling registration params", "error", err)
21+ slog.Error("Error unmarshaling registration params", "error", err)
2222 return nil, err
2323 }
2424
@@ -28,13 +28,13 @@ func HandleRegisterCapability(params json.RawMessage) (any, error) {
2828 // Parse the registration options
2929 optionsJSON, err := json.Marshal(reg.RegisterOptions)
3030 if err != nil {
31− logging.Error("Error marshaling registration options", "error", err)
31+ slog.Error("Error marshaling registration options", "error", err)
3232 continue
3333 }
3434
3535 var options protocol.DidChangeWatchedFilesRegistrationOptions
3636 if err := json.Unmarshal(optionsJSON, &options); err != nil {
37− logging.Error("Error unmarshaling registration options", "error", err)
37+ slog.Error("Error unmarshaling registration options", "error", err)
3838 continue
3939 }
4040
@@ -54,7 +54,7 @@ func HandleApplyEdit(params json.RawMessage) (any, error) {
5454
5555 err := util.ApplyWorkspaceEdit(edit.Edit)
5656 if err != nil {
57− logging.Error("Error applying workspace edit", "error", err)
57+ slog.Error("Error applying workspace edit", "error", err)
5858 return protocol.ApplyWorkspaceEditResult{Applied: false, FailureReason: err.Error()}, nil
5959 }
6060
@@ -89,7 +89,7 @@ func HandleServerMessage(params json.RawMessage) {
8989 }
9090 if err := json.Unmarshal(params, &msg); err == nil {
9191 if cfg.Options.DebugLSP {
92− logging.Debug("Server message", "type", msg.Type, "message", msg.Message)
92+ slog.Debug("Server message", "type", msg.Type, "message", msg.Message)
9393 }
9494 }
9595}
@@ -97,7 +97,7 @@ func HandleServerMessage(params json.RawMessage) {
9797func HandleDiagnostics(client *Client, params json.RawMessage) {
9898 var diagParams protocol.PublishDiagnosticsParams
9999 if err := json.Unmarshal(params, &diagParams); err != nil {
100− logging.Error("Error unmarshaling diagnostics params", "error", err)
100+ slog.Error("Error unmarshaling diagnostics params", "error", err)
101101 return
102102 }
103103
internal/lsp/protocol/tsprotocol.go+1−1
@@ -55,7 +55,7 @@ type ApplyWorkspaceEditResult struct {
5555 // Indicates whether the edit was applied or not.
5656 Applied bool `json:"applied"`
5757 // An optional textual description for why the edit was not applied.
58− // This may be used by the server for diagnostic logging or to provide
58+ // This may be used by the server for diagnostic slog.or to provide
5959 // a suitable error for a request that triggered the edit.
6060 FailureReason string `json:"failureReason,omitempty"`
6161 // Depending on the client's failure handling strategy `failedChange` might
internal/lsp/transport.go+16−16
@@ -9,7 +9,7 @@ import (
99 "strings"
1010
1111 "github.com/charmbracelet/crush/internal/config"
12− "github.com/charmbracelet/crush/internal/logging"
12+ "log/slog"
1313)
1414
1515// Write writes an LSP message to the given writer
@@ -21,7 +21,7 @@ func WriteMessage(w io.Writer, msg *Message) error {
2121 cfg := config.Get()
2222
2323 if cfg.Options.DebugLSP {
24− logging.Debug("Sending message to server", "method", msg.Method, "id", msg.ID)
24+ slog.Debug("Sending message to server", "method", msg.Method, "id", msg.ID)
2525 }
2626
2727 _, err = fmt.Fprintf(w, "Content-Length: %d\r\n\r\n", len(data))
@@ -50,7 +50,7 @@ func ReadMessage(r *bufio.Reader) (*Message, error) {
5050 line = strings.TrimSpace(line)
5151
5252 if cfg.Options.DebugLSP {
53− logging.Debug("Received header", "line", line)
53+ slog.Debug("Received header", "line", line)
5454 }
5555
5656 if line == "" {
@@ -66,7 +66,7 @@ func ReadMessage(r *bufio.Reader) (*Message, error) {
6666 }
6767
6868 if cfg.Options.DebugLSP {
69− logging.Debug("Content-Length", "length", contentLength)
69+ slog.Debug("Content-Length", "length", contentLength)
7070 }
7171
7272 // Read content
@@ -77,7 +77,7 @@ func ReadMessage(r *bufio.Reader) (*Message, error) {
7777 }
7878
7979 if cfg.Options.DebugLSP {
80− logging.Debug("Received content", "content", string(content))
80+ slog.Debug("Received content", "content", string(content))
8181 }
8282
8383 // Parse message
@@ -96,7 +96,7 @@ func (c *Client) handleMessages() {
9696 msg, err := ReadMessage(c.stdout)
9797 if err != nil {
9898 if cfg.Options.DebugLSP {
99− logging.Error("Error reading message", "error", err)
99+ slog.Error("Error reading message", "error", err)
100100 }
101101 return
102102 }
@@ -104,7 +104,7 @@ func (c *Client) handleMessages() {
104104 // Handle server->client request (has both Method and ID)
105105 if msg.Method != "" && msg.ID != 0 {
106106 if cfg.Options.DebugLSP {
107− logging.Debug("Received request from server", "method", msg.Method, "id", msg.ID)
107+ slog.Debug("Received request from server", "method", msg.Method, "id", msg.ID)
108108 }
109109
110110 response := &Message{
@@ -144,7 +144,7 @@ func (c *Client) handleMessages() {
144144
145145 // Send response back to server
146146 if err := WriteMessage(c.stdin, response); err != nil {
147− logging.Error("Error sending response to server", "error", err)
147+ slog.Error("Error sending response to server", "error", err)
148148 }
149149
150150 continue
@@ -158,11 +158,11 @@ func (c *Client) handleMessages() {
158158
159159 if ok {
160160 if cfg.Options.DebugLSP {
161− logging.Debug("Handling notification", "method", msg.Method)
161+ slog.Debug("Handling notification", "method", msg.Method)
162162 }
163163 go handler(msg.Params)
164164 } else if cfg.Options.DebugLSP {
165− logging.Debug("No handler for notification", "method", msg.Method)
165+ slog.Debug("No handler for notification", "method", msg.Method)
166166 }
167167 continue
168168 }
@@ -175,12 +175,12 @@ func (c *Client) handleMessages() {
175175
176176 if ok {
177177 if cfg.Options.DebugLSP {
178− logging.Debug("Received response for request", "id", msg.ID)
178+ slog.Debug("Received response for request", "id", msg.ID)
179179 }
180180 ch <- msg
181181 close(ch)
182182 } else if cfg.Options.DebugLSP {
183− logging.Debug("No handler for response", "id", msg.ID)
183+ slog.Debug("No handler for response", "id", msg.ID)
184184 }
185185 }
186186 }
@@ -192,7 +192,7 @@ func (c *Client) Call(ctx context.Context, method string, params any, result any
192192 id := c.nextID.Add(1)
193193
194194 if cfg.Options.DebugLSP {
195− logging.Debug("Making call", "method", method, "id", id)
195+ slog.Debug("Making call", "method", method, "id", id)
196196 }
197197
198198 msg, err := NewRequest(id, method, params)
@@ -218,14 +218,14 @@ func (c *Client) Call(ctx context.Context, method string, params any, result any
218218 }
219219
220220 if cfg.Options.DebugLSP {
221− logging.Debug("Request sent", "method", method, "id", id)
221+ slog.Debug("Request sent", "method", method, "id", id)
222222 }
223223
224224 // Wait for response
225225 resp := <-ch
226226
227227 if cfg.Options.DebugLSP {
228− logging.Debug("Received response", "id", id)
228+ slog.Debug("Received response", "id", id)
229229 }
230230
231231 if resp.Error != nil {
@@ -251,7 +251,7 @@ func (c *Client) Call(ctx context.Context, method string, params any, result any
251251func (c *Client) Notify(ctx context.Context, method string, params any) error {
252252 cfg := config.Get()
253253 if cfg.Options.DebugLSP {
254− logging.Debug("Sending notification", "method", method)
254+ slog.Debug("Sending notification", "method", method)
255255 }
256256
257257 msg, err := NewNotification(method, params)
internal/lsp/watcher/watcher.go+41−41
@@ -11,7 +11,7 @@ import (
1111
1212 "github.com/bmatcuk/doublestar/v4"
1313 "github.com/charmbracelet/crush/internal/config"
14− "github.com/charmbracelet/crush/internal/logging"
14+ "log/slog"
1515 "github.com/charmbracelet/crush/internal/lsp"
1616 "github.com/charmbracelet/crush/internal/lsp/protocol"
1717 "github.com/fsnotify/fsnotify"
@@ -45,7 +45,7 @@ func NewWorkspaceWatcher(client *lsp.Client) *WorkspaceWatcher {
4545func (w *WorkspaceWatcher) AddRegistrations(ctx context.Context, id string, watchers []protocol.FileSystemWatcher) {
4646 cfg := config.Get()
4747
48− logging.Debug("Adding file watcher registrations")
48+ slog.Debug("Adding file watcher registrations")
4949 w.registrationMu.Lock()
5050 defer w.registrationMu.Unlock()
5151
@@ -54,33 +54,33 @@ func (w *WorkspaceWatcher) AddRegistrations(ctx context.Context, id string, watc
5454
5555 // Print detailed registration information for debugging
5656 if cfg.Options.DebugLSP {
57− logging.Debug("Adding file watcher registrations",
57+ slog.Debug("Adding file watcher registrations",
5858 "id", id,
5959 "watchers", len(watchers),
6060 "total", len(w.registrations),
6161 )
6262
6363 for i, watcher := range watchers {
64− logging.Debug("Registration", "index", i+1)
64+ slog.Debug("Registration", "index", i+1)
6565
6666 // Log the GlobPattern
6767 switch v := watcher.GlobPattern.Value.(type) {
6868 case string:
69− logging.Debug("GlobPattern", "pattern", v)
69+ slog.Debug("GlobPattern", "pattern", v)
7070 case protocol.RelativePattern:
71− logging.Debug("GlobPattern", "pattern", v.Pattern)
71+ slog.Debug("GlobPattern", "pattern", v.Pattern)
7272
7373 // Log BaseURI details
7474 switch u := v.BaseURI.Value.(type) {
7575 case string:
76− logging.Debug("BaseURI", "baseURI", u)
76+ slog.Debug("BaseURI", "baseURI", u)
7777 case protocol.DocumentUri:
78− logging.Debug("BaseURI", "baseURI", u)
78+ slog.Debug("BaseURI", "baseURI", u)
7979 default:
80− logging.Debug("BaseURI", "baseURI", u)
80+ slog.Debug("BaseURI", "baseURI", u)
8181 }
8282 default:
83− logging.Debug("GlobPattern", "unknown type", fmt.Sprintf("%T", v))
83+ slog.Debug("GlobPattern", "unknown type", fmt.Sprintf("%T", v))
8484 }
8585
8686 // Log WatchKind
@@ -89,13 +89,13 @@ func (w *WorkspaceWatcher) AddRegistrations(ctx context.Context, id string, watc
8989 watchKind = *watcher.Kind
9090 }
9191
92− logging.Debug("WatchKind", "kind", watchKind)
92+ slog.Debug("WatchKind", "kind", watchKind)
9393 }
9494 }
9595
9696 // Determine server type for specialized handling
9797 serverName := getServerNameFromContext(ctx)
98− logging.Debug("Server type detected", "serverName", serverName)
98+ slog.Debug("Server type detected", "serverName", serverName)
9999
100100 // Check if this server has sent file watchers
101101 hasFileWatchers := len(watchers) > 0
@@ -123,7 +123,7 @@ func (w *WorkspaceWatcher) AddRegistrations(ctx context.Context, id string, watc
123123 filesOpened += highPriorityFilesOpened
124124
125125 if cfg.Options.DebugLSP {
126− logging.Debug("Opened high-priority files",
126+ slog.Debug("Opened high-priority files",
127127 "count", highPriorityFilesOpened,
128128 "serverName", serverName)
129129 }
@@ -131,7 +131,7 @@ func (w *WorkspaceWatcher) AddRegistrations(ctx context.Context, id string, watc
131131 // If we've already opened enough high-priority files, we might not need more
132132 if filesOpened >= maxFilesToOpen {
133133 if cfg.Options.DebugLSP {
134− logging.Debug("Reached file limit with high-priority files",
134+ slog.Debug("Reached file limit with high-priority files",
135135 "filesOpened", filesOpened,
136136 "maxFiles", maxFilesToOpen)
137137 }
@@ -149,7 +149,7 @@ func (w *WorkspaceWatcher) AddRegistrations(ctx context.Context, id string, watc
149149 if d.IsDir() {
150150 if path != w.workspacePath && shouldExcludeDir(path) {
151151 if cfg.Options.DebugLSP {
152− logging.Debug("Skipping excluded directory", "path", path)
152+ slog.Debug("Skipping excluded directory", "path", path)
153153 }
154154 return filepath.SkipDir
155155 }
@@ -177,7 +177,7 @@ func (w *WorkspaceWatcher) AddRegistrations(ctx context.Context, id string, watc
177177
178178 elapsedTime := time.Since(startTime)
179179 if cfg.Options.DebugLSP {
180− logging.Debug("Limited workspace scan complete",
180+ slog.Debug("Limited workspace scan complete",
181181 "filesOpened", filesOpened,
182182 "maxFiles", maxFilesToOpen,
183183 "elapsedTime", elapsedTime.Seconds(),
@@ -186,11 +186,11 @@ func (w *WorkspaceWatcher) AddRegistrations(ctx context.Context, id string, watc
186186 }
187187
188188 if err != nil && cfg.Options.DebugLSP {
189− logging.Debug("Error scanning workspace for files to open", "error", err)
189+ slog.Debug("Error scanning workspace for files to open", "error", err)
190190 }
191191 }()
192192 } else if cfg.Options.DebugLSP {
193− logging.Debug("Using on-demand file loading for server", "server", serverName)
193+ slog.Debug("Using on-demand file loading for server", "server", serverName)
194194 }
195195}
196196
@@ -266,7 +266,7 @@ func (w *WorkspaceWatcher) openHighPriorityFiles(ctx context.Context, serverName
266266 matches, err := doublestar.Glob(os.DirFS(w.workspacePath), pattern)
267267 if err != nil {
268268 if cfg.Options.DebugLSP {
269− logging.Debug("Error finding high-priority files", "pattern", pattern, "error", err)
269+ slog.Debug("Error finding high-priority files", "pattern", pattern, "error", err)
270270 }
271271 continue
272272 }
@@ -300,12 +300,12 @@ func (w *WorkspaceWatcher) openHighPriorityFiles(ctx context.Context, serverName
300300 fullPath := filesToOpen[j]
301301 if err := w.client.OpenFile(ctx, fullPath); err != nil {
302302 if cfg.Options.DebugLSP {
303− logging.Debug("Error opening high-priority file", "path", fullPath, "error", err)
303+ slog.Debug("Error opening high-priority file", "path", fullPath, "error", err)
304304 }
305305 } else {
306306 filesOpened++
307307 if cfg.Options.DebugLSP {
308− logging.Debug("Opened high-priority file", "path", fullPath)
308+ slog.Debug("Opened high-priority file", "path", fullPath)
309309 }
310310 }
311311 }
@@ -334,7 +334,7 @@ func (w *WorkspaceWatcher) WatchWorkspace(ctx context.Context, workspacePath str
334334 }
335335
336336 serverName := getServerNameFromContext(ctx)
337− logging.Debug("Starting workspace watcher", "workspacePath", workspacePath, "serverName", serverName)
337+ slog.Debug("Starting workspace watcher", "workspacePath", workspacePath, "serverName", serverName)
338338
339339 // Register handler for file watcher registrations from the server
340340 lsp.RegisterFileWatchHandler(func(id string, watchers []protocol.FileSystemWatcher) {
@@ -343,7 +343,7 @@ func (w *WorkspaceWatcher) WatchWorkspace(ctx context.Context, workspacePath str
343343
344344 watcher, err := fsnotify.NewWatcher()
345345 if err != nil {
346− logging.Error("Error creating watcher", "error", err)
346+ slog.Error("Error creating watcher", "error", err)
347347 }
348348 defer watcher.Close()
349349
@@ -357,7 +357,7 @@ func (w *WorkspaceWatcher) WatchWorkspace(ctx context.Context, workspacePath str
357357 if d.IsDir() && path != workspacePath {
358358 if shouldExcludeDir(path) {
359359 if cfg.Options.DebugLSP {
360− logging.Debug("Skipping excluded directory", "path", path)
360+ slog.Debug("Skipping excluded directory", "path", path)
361361 }
362362 return filepath.SkipDir
363363 }
@@ -367,14 +367,14 @@ func (w *WorkspaceWatcher) WatchWorkspace(ctx context.Context, workspacePath str
367367 if d.IsDir() {
368368 err = watcher.Add(path)
369369 if err != nil {
370− logging.Error("Error watching path", "path", path, "error", err)
370+ slog.Error("Error watching path", "path", path, "error", err)
371371 }
372372 }
373373
374374 return nil
375375 })
376376 if err != nil {
377− logging.Error("Error walking workspace", "error", err)
377+ slog.Error("Error walking workspace", "error", err)
378378 }
379379
380380 // Event loop
@@ -396,7 +396,7 @@ func (w *WorkspaceWatcher) WatchWorkspace(ctx context.Context, workspacePath str
396396 // Skip excluded directories
397397 if !shouldExcludeDir(event.Name) {
398398 if err := watcher.Add(event.Name); err != nil {
399− logging.Error("Error adding directory to watcher", "path", event.Name, "error", err)
399+ slog.Error("Error adding directory to watcher", "path", event.Name, "error", err)
400400 }
401401 }
402402 } else {
@@ -411,7 +411,7 @@ func (w *WorkspaceWatcher) WatchWorkspace(ctx context.Context, workspacePath str
411411 // Debug logging
412412 if cfg.Options.DebugLSP {
413413 matched, kind := w.isPathWatched(event.Name)
414− logging.Debug("File event",
414+ slog.Debug("File event",
415415 "path", event.Name,
416416 "operation", event.Op.String(),
417417 "watched", matched,
@@ -431,7 +431,7 @@ func (w *WorkspaceWatcher) WatchWorkspace(ctx context.Context, workspacePath str
431431 // Just send the notification if needed
432432 info, err := os.Stat(event.Name)
433433 if err != nil {
434− logging.Error("Error getting file info", "path", event.Name, "error", err)
434+ slog.Error("Error getting file info", "path", event.Name, "error", err)
435435 return
436436 }
437437 if !info.IsDir() && watchKind&protocol.WatchCreate != 0 {
@@ -459,7 +459,7 @@ func (w *WorkspaceWatcher) WatchWorkspace(ctx context.Context, workspacePath str
459459 if !ok {
460460 return
461461 }
462− logging.Error("Error watching file", "error", err)
462+ slog.Error("Error watching file", "error", err)
463463 }
464464 }
465465}
@@ -584,7 +584,7 @@ func matchesSimpleGlob(pattern, path string) bool {
584584 // Fall back to simple matching for simpler patterns
585585 matched, err := filepath.Match(pattern, path)
586586 if err != nil {
587− logging.Error("Error matching pattern", "pattern", pattern, "path", path, "error", err)
587+ slog.Error("Error matching pattern", "pattern", pattern, "path", path, "error", err)
588588 return false
589589 }
590590
@@ -595,7 +595,7 @@ func matchesSimpleGlob(pattern, path string) bool {
595595func (w *WorkspaceWatcher) matchesPattern(path string, pattern protocol.GlobPattern) bool {
596596 patternInfo, err := pattern.AsPattern()
597597 if err != nil {
598− logging.Error("Error parsing pattern", "pattern", pattern, "error", err)
598+ slog.Error("Error parsing pattern", "pattern", pattern, "error", err)
599599 return false
600600 }
601601
@@ -620,7 +620,7 @@ func (w *WorkspaceWatcher) matchesPattern(path string, pattern protocol.GlobPatt
620620 // Make path relative to basePath for matching
621621 relPath, err := filepath.Rel(basePath, path)
622622 if err != nil {
623− logging.Error("Error getting relative path", "path", path, "basePath", basePath, "error", err)
623+ slog.Error("Error getting relative path", "path", path, "basePath", basePath, "error", err)
624624 return false
625625 }
626626 relPath = filepath.ToSlash(relPath)
@@ -663,14 +663,14 @@ func (w *WorkspaceWatcher) handleFileEvent(ctx context.Context, uri string, chan
663663 } else if changeType == protocol.FileChangeType(protocol.Changed) && w.client.IsFileOpen(filePath) {
664664 err := w.client.NotifyChange(ctx, filePath)
665665 if err != nil {
666− logging.Error("Error notifying change", "error", err)
666+ slog.Error("Error notifying change", "error", err)
667667 }
668668 return
669669 }
670670
671671 // Notify LSP server about the file event using didChangeWatchedFiles
672672 if err := w.notifyFileEvent(ctx, uri, changeType); err != nil {
673− logging.Error("Error notifying LSP server about file event", "error", err)
673+ slog.Error("Error notifying LSP server about file event", "error", err)
674674 }
675675}
676676
@@ -678,7 +678,7 @@ func (w *WorkspaceWatcher) handleFileEvent(ctx context.Context, uri string, chan
678678func (w *WorkspaceWatcher) notifyFileEvent(ctx context.Context, uri string, changeType protocol.FileChangeType) error {
679679 cfg := config.Get()
680680 if cfg.Options.DebugLSP {
681− logging.Debug("Notifying file event",
681+ slog.Debug("Notifying file event",
682682 "uri", uri,
683683 "changeType", changeType,
684684 )
@@ -853,7 +853,7 @@ func shouldExcludeFile(filePath string) bool {
853853 // Skip large files
854854 if info.Size() > maxFileSize {
855855 if cfg.Options.DebugLSP {
856− logging.Debug("Skipping large file",
856+ slog.Debug("Skipping large file",
857857 "path", filePath,
858858 "size", info.Size(),
859859 "maxSize", maxFileSize,
@@ -891,10 +891,10 @@ func (w *WorkspaceWatcher) openMatchingFile(ctx context.Context, path string) {
891891 // This helps with project initialization for certain language servers
892892 if isHighPriorityFile(path, serverName) {
893893 if cfg.Options.DebugLSP {
894− logging.Debug("Opening high-priority file", "path", path, "serverName", serverName)
894+ slog.Debug("Opening high-priority file", "path", path, "serverName", serverName)
895895 }
896896 if err := w.client.OpenFile(ctx, path); err != nil && cfg.Options.DebugLSP {
897− logging.Error("Error opening high-priority file", "path", path, "error", err)
897+ slog.Error("Error opening high-priority file", "path", path, "error", err)
898898 }
899899 return
900900 }
@@ -906,7 +906,7 @@ func (w *WorkspaceWatcher) openMatchingFile(ctx context.Context, path string) {
906906 // Check file size - for preloading we're more conservative
907907 if info.Size() > (1 * 1024 * 1024) { // 1MB limit for preloaded files
908908 if cfg.Options.DebugLSP {
909− logging.Debug("Skipping large file for preloading", "path", path, "size", info.Size())
909+ slog.Debug("Skipping large file for preloading", "path", path, "size", info.Size())
910910 }
911911 return
912912 }
@@ -938,7 +938,7 @@ func (w *WorkspaceWatcher) openMatchingFile(ctx context.Context, path string) {
938938 if shouldOpen {
939939 // Don't need to check if it's already open - the client.OpenFile handles that
940940 if err := w.client.OpenFile(ctx, path); err != nil && cfg.Options.DebugLSP {
941− logging.Error("Error opening file", "path", path, "error", err)
941+ slog.Error("Error opening file", "path", path, "error", err)
942942 }
943943 }
944944 }
internal/shell/persistent.go+3−4
@@ -1,9 +1,8 @@
11package shell
22
33import (
4+ "log/slog"
45 "sync"
5−
6− "github.com/charmbracelet/crush/internal/logging"
76)
87
98// PersistentShell is a singleton shell instance that maintains state across the application
@@ -30,9 +29,9 @@ func GetPersistentShell(cwd string) *PersistentShell {
3029 return shellInstance
3130}
3231
33−// loggingAdapter adapts the internal logging package to the Logger interface
32+// slog.dapter adapts the internal slog.package to the Logger interface
3433type loggingAdapter struct{}
3534
3635func (l *loggingAdapter) InfoPersist(msg string, keysAndValues ...interface{}) {
37− logging.InfoPersist(msg, keysAndValues...)
36+ slog.Info(msg, keysAndValues...)
3837}
internal/tui/components/chat/editor/editor.go+1−3
@@ -14,7 +14,6 @@ import (
1414 tea "github.com/charmbracelet/bubbletea/v2"
1515 "github.com/charmbracelet/crush/internal/app"
1616 "github.com/charmbracelet/crush/internal/fsext"
17− "github.com/charmbracelet/crush/internal/logging"
1817 "github.com/charmbracelet/crush/internal/message"
1918 "github.com/charmbracelet/crush/internal/session"
2019 "github.com/charmbracelet/crush/internal/tui/components/chat"
@@ -153,8 +152,7 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
153152 return m, nil
154153 case filepicker.FilePickedMsg:
155154 if len(m.attachments) >= maxAttachments {
156− logging.ErrorPersist(fmt.Sprintf("cannot add more than %d images", maxAttachments))
157− return m, cmd
155+ return m, util.ReportError(fmt.Errorf("cannot add more than %d images", maxAttachments))
158156 }
159157 m.attachments = append(m.attachments, msg.Attachment)
160158 return m, nil
internal/tui/components/chat/sidebar/sidebar.go+2−2
@@ -13,7 +13,7 @@ import (
1313 "github.com/charmbracelet/crush/internal/diff"
1414 "github.com/charmbracelet/crush/internal/fsext"
1515 "github.com/charmbracelet/crush/internal/history"
16− "github.com/charmbracelet/crush/internal/logging"
16+ "log/slog"
1717 "github.com/charmbracelet/crush/internal/lsp"
1818 "github.com/charmbracelet/crush/internal/lsp/protocol"
1919 "github.com/charmbracelet/crush/internal/pubsub"
@@ -94,7 +94,7 @@ func (m *sidebarCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
9494 case chat.SessionClearedMsg:
9595 m.session = session.Session{}
9696 case pubsub.Event[history.File]:
97− logging.Info("sidebar", "Received file history event", "file", msg.Payload.Path, "session", msg.Payload.SessionID)
97+ slog.Info("sidebar", "Received file history event", "file", msg.Payload.Path, "session", msg.Payload.SessionID)
9898 return m, m.handleFileHistoryEvent(msg)
9999 case pubsub.Event[session.Session]:
100100 if msg.Type == pubsub.UpdatedEvent {
internal/tui/components/core/layout/split.go+2−2
@@ -3,7 +3,7 @@ package layout
33import (
44 "github.com/charmbracelet/bubbles/v2/key"
55 tea "github.com/charmbracelet/bubbletea/v2"
6− "github.com/charmbracelet/crush/internal/logging"
6+ "log/slog"
77 "github.com/charmbracelet/crush/internal/tui/styles"
88 "github.com/charmbracelet/crush/internal/tui/util"
99 "github.com/charmbracelet/lipgloss/v2"
@@ -154,7 +154,7 @@ func (s *splitPaneLayout) View() tea.View {
154154func (s *splitPaneLayout) SetSize(width, height int) tea.Cmd {
155155 s.width = width
156156 s.height = height
157− logging.Info("Setting split pane size", "width", width, "height", height)
157+ slog.Info("Setting split pane size", "width", width, "height", height)
158158
159159 var topHeight, bottomHeight int
160160 var cmds []tea.Cmd
internal/tui/components/core/status/status.go−33
@@ -6,8 +6,6 @@ import (
66
77 "github.com/charmbracelet/bubbles/v2/help"
88 tea "github.com/charmbracelet/bubbletea/v2"
9− "github.com/charmbracelet/crush/internal/logging"
10− "github.com/charmbracelet/crush/internal/pubsub"
119 "github.com/charmbracelet/crush/internal/session"
1210 "github.com/charmbracelet/crush/internal/tui/styles"
1311 "github.com/charmbracelet/crush/internal/tui/util"
@@ -59,37 +57,6 @@ func (m *statusCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
5957 case util.ClearStatusMsg:
6058 m.info = util.InfoMsg{}
6159
62− // Handle persistent logs
63− case pubsub.Event[logging.LogMessage]:
64− if msg.Payload.Persist {
65− switch msg.Payload.Level {
66− case "error":
67− m.info = util.InfoMsg{
68− Type: util.InfoTypeError,
69− Msg: msg.Payload.Message,
70− TTL: msg.Payload.PersistTime,
71− }
72− case "info":
73− m.info = util.InfoMsg{
74− Type: util.InfoTypeInfo,
75− Msg: msg.Payload.Message,
76− TTL: msg.Payload.PersistTime,
77− }
78− case "warn":
79− m.info = util.InfoMsg{
80− Type: util.InfoTypeWarn,
81− Msg: msg.Payload.Message,
82− TTL: msg.Payload.PersistTime,
83− }
84− default:
85− m.info = util.InfoMsg{
86− Type: util.InfoTypeInfo,
87− Msg: msg.Payload.Message,
88− TTL: msg.Payload.PersistTime,
89− }
90− }
91− return m, m.clearMessageCmd(m.info.TTL)
92− }
9360 }
9461 return m, nil
9562}
internal/tui/components/dialogs/filepicker/filepicker.go+3−7
@@ -11,7 +11,6 @@ import (
1111 "github.com/charmbracelet/bubbles/v2/help"
1212 "github.com/charmbracelet/bubbles/v2/key"
1313 tea "github.com/charmbracelet/bubbletea/v2"
14− "github.com/charmbracelet/crush/internal/logging"
1514 "github.com/charmbracelet/crush/internal/message"
1615 "github.com/charmbracelet/crush/internal/tui/components/core"
1716 "github.com/charmbracelet/crush/internal/tui/components/dialogs"
@@ -119,18 +118,15 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
119118 func() tea.Msg {
120119 isFileLarge, err := ValidateFileSize(path, maxAttachmentSize)
121120 if err != nil {
122− logging.ErrorPersist("unable to read the image")
123− return nil
121+ return util.ReportError(fmt.Errorf("unable to read the image: %w", err))
124122 }
125123 if isFileLarge {
126− logging.ErrorPersist("file too large, max 5MB")
127− return nil
124+ return util.ReportError(fmt.Errorf("file too large, max 5MB"))
128125 }
129126
130127 content, err := os.ReadFile(path)
131128 if err != nil {
132− logging.ErrorPersist("Unable read selected file")
133− return nil
129+ return util.ReportError(fmt.Errorf("unable to read the image: %w", err))
134130 }
135131
136132 mimeBufferSize := min(512, len(content))
internal/tui/components/logs/details.godeleted−176
@@ -1,176 +0,0 @@
1−package logs
2−
3−import (
4− "fmt"
5− "strings"
6− "time"
7−
8− "github.com/charmbracelet/bubbles/v2/viewport"
9− tea "github.com/charmbracelet/bubbletea/v2"
10− "github.com/charmbracelet/crush/internal/logging"
11− "github.com/charmbracelet/crush/internal/tui/components/core/layout"
12− "github.com/charmbracelet/crush/internal/tui/styles"
13− "github.com/charmbracelet/crush/internal/tui/util"
14− "github.com/charmbracelet/lipgloss/v2"
15−)
16−
17−type DetailComponent interface {
18− util.Model
19− layout.Sizeable
20−}
21−
22−type detailCmp struct {
23− width, height int
24− currentLog logging.LogMessage
25− viewport viewport.Model
26−}
27−
28−func (i *detailCmp) Init() tea.Cmd {
29− messages := logging.List()
30− if len(messages) == 0 {
31− return nil
32− }
33− i.currentLog = messages[0]
34− return nil
35−}
36−
37−func (i *detailCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
38− switch msg := msg.(type) {
39− case selectedLogMsg:
40− if msg.ID != i.currentLog.ID {
41− i.currentLog = logging.LogMessage(msg)
42− i.updateContent()
43− }
44− }
45−
46− return i, nil
47−}
48−
49−func (i *detailCmp) updateContent() {
50− var content strings.Builder
51− t := styles.CurrentTheme()
52−
53− if i.currentLog.ID == "" {
54− content.WriteString(t.S().Muted.Render("No log selected"))
55− i.viewport.SetContent(content.String())
56− return
57− }
58−
59− // Level badge with background color
60− levelStyle := getLevelStyle(i.currentLog.Level)
61− levelBadge := levelStyle.Padding(0, 1).Render(strings.ToUpper(i.currentLog.Level))
62−
63− // Timestamp with relative time
64− timeStr := i.currentLog.Time.Format("2006-01-05 15:04:05 UTC")
65− relativeTime := getRelativeTime(i.currentLog.Time)
66− timeStyle := t.S().Muted
67−
68− // Header line
69− header := lipgloss.JoinHorizontal(
70− lipgloss.Left,
71− timeStr,
72− " ",
73− timeStyle.Render(relativeTime),
74− )
75−
76− content.WriteString(levelBadge)
77− content.WriteString("\n\n")
78− content.WriteString(header)
79− content.WriteString("\n\n")
80−
81− // Message section
82− messageHeaderStyle := t.S().Base.Foreground(t.Blue).Bold(true)
83− content.WriteString(messageHeaderStyle.Render("Message"))
84− content.WriteString("\n")
85− content.WriteString(i.currentLog.Message)
86− content.WriteString("\n\n")
87−
88− // Attributes section
89− if len(i.currentLog.Attributes) > 0 {
90− attrHeaderStyle := t.S().Base.Foreground(t.Blue).Bold(true)
91− content.WriteString(attrHeaderStyle.Render("Attributes"))
92− content.WriteString("\n")
93−
94− for _, attr := range i.currentLog.Attributes {
95− keyStyle := t.S().Base.Foreground(t.Accent)
96− valueStyle := t.S().Text
97− attrLine := fmt.Sprintf("%s: %s",
98− keyStyle.Render(attr.Key),
99− valueStyle.Render(attr.Value),
100− )
101− content.WriteString(attrLine)
102− content.WriteString("\n")
103− }
104− }
105−
106− i.viewport.SetContent(content.String())
107−}
108−
109−func getLevelStyle(level string) lipgloss.Style {
110− t := styles.CurrentTheme()
111− style := t.S().Base.Bold(true)
112−
113− switch strings.ToLower(level) {
114− case "info":
115− return style.Foreground(t.White).Background(t.Info)
116− case "warn", "warning":
117− return style.Foreground(t.White).Background(t.Warning)
118− case "error", "err":
119− return style.Foreground(t.White).Background(t.Error)
120− case "debug":
121− return style.Foreground(t.White).Background(t.Success)
122− case "fatal":
123− return style.Foreground(t.White).Background(t.Error)
124− default:
125− return style.Foreground(t.FgBase)
126− }
127−}
128−
129−func getRelativeTime(logTime time.Time) string {
130− now := time.Now()
131− diff := now.Sub(logTime)
132−
133− if diff < time.Minute {
134− return fmt.Sprintf("%ds ago", int(diff.Seconds()))
135− } else if diff < time.Hour {
136− return fmt.Sprintf("%dm ago", int(diff.Minutes()))
137− } else if diff < 24*time.Hour {
138− return fmt.Sprintf("%dh ago", int(diff.Hours()))
139− } else if diff < 30*24*time.Hour {
140− return fmt.Sprintf("%dd ago", int(diff.Hours()/24))
141− } else if diff < 365*24*time.Hour {
142− return fmt.Sprintf("%dmo ago", int(diff.Hours()/(24*30)))
143− } else {
144− return fmt.Sprintf("%dy ago", int(diff.Hours()/(24*365)))
145− }
146−}
147−
148−func (i *detailCmp) View() tea.View {
149− t := styles.CurrentTheme()
150− style := t.S().Base.
151− BorderStyle(lipgloss.RoundedBorder()).
152− BorderForeground(t.BorderFocus).
153− Width(i.width - 2). // Adjust width for border
154− Height(i.height - 2). // Adjust height for border
155− Padding(1)
156− return tea.NewView(style.Render(i.viewport.View()))
157−}
158−
159−func (i *detailCmp) GetSize() (int, int) {
160− return i.width, i.height
161−}
162−
163−func (i *detailCmp) SetSize(width int, height int) tea.Cmd {
164− i.width = width
165− i.height = height
166− i.viewport.SetWidth(i.width - 4)
167− i.viewport.SetHeight(i.height - 4)
168− i.updateContent()
169− return nil
170−}
171−
172−func NewLogsDetails() DetailComponent {
173− return &detailCmp{
174− viewport: viewport.New(),
175− }
176−}
internal/tui/components/logs/table.godeleted−197
@@ -1,197 +0,0 @@
1−package logs
2−
3−import (
4− "fmt"
5− "slices"
6− "strings"
7−
8− "github.com/charmbracelet/bubbles/v2/table"
9− tea "github.com/charmbracelet/bubbletea/v2"
10− "github.com/charmbracelet/crush/internal/logging"
11− "github.com/charmbracelet/crush/internal/pubsub"
12− "github.com/charmbracelet/crush/internal/tui/components/core/layout"
13− "github.com/charmbracelet/crush/internal/tui/styles"
14− "github.com/charmbracelet/crush/internal/tui/util"
15− "github.com/charmbracelet/lipgloss/v2"
16−)
17−
18−type TableComponent interface {
19− util.Model
20− layout.Sizeable
21−}
22−
23−type tableCmp struct {
24− table table.Model
25− logs []logging.LogMessage
26−}
27−
28−type selectedLogMsg logging.LogMessage
29−
30−func (i *tableCmp) Init() tea.Cmd {
31− i.logs = logging.List()
32− i.setRows()
33− return nil
34−}
35−
36−func (i *tableCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
37− var cmds []tea.Cmd
38− switch msg := msg.(type) {
39− case pubsub.Event[logging.LogMessage]:
40− return i, func() tea.Msg {
41− if msg.Type == pubsub.CreatedEvent {
42− rows := i.table.Rows()
43− for _, row := range rows {
44− if row[1] == msg.Payload.ID {
45− return nil // If the log already exists, do not add it again
46− }
47− }
48− i.logs = append(i.logs, msg.Payload)
49− i.table.SetRows(
50− append(
51− []table.Row{
52− logToRow(msg.Payload),
53− },
54− i.table.Rows()...,
55− ),
56− )
57− }
58− return selectedLogMsg(msg.Payload)
59− }
60− }
61− t, cmd := i.table.Update(msg)
62− cmds = append(cmds, cmd)
63− i.table = t
64−
65− cmds = append(cmds, func() tea.Msg {
66− for _, log := range logging.List() {
67− if log.ID == i.table.SelectedRow()[1] {
68− // If the selected row matches the log ID, return the selected log message
69− return selectedLogMsg(log)
70− }
71− }
72− return nil
73− })
74− return i, tea.Batch(cmds...)
75−}
76−
77−func (i *tableCmp) View() tea.View {
78− t := styles.CurrentTheme()
79− defaultStyles := table.DefaultStyles()
80−
81− // Header styling
82− defaultStyles.Header = defaultStyles.Header.
83− Foreground(t.Primary).
84− Bold(true).
85− BorderStyle(lipgloss.NormalBorder()).
86− BorderBottom(true).
87− BorderForeground(t.Border)
88−
89− // Selected row styling
90− defaultStyles.Selected = defaultStyles.Selected.
91− Foreground(t.FgSelected).
92− Background(t.Primary).
93− Bold(false)
94−
95− // Cell styling
96− defaultStyles.Cell = defaultStyles.Cell.
97− Foreground(t.FgBase)
98−
99− i.table.SetStyles(defaultStyles)
100− return tea.NewView(i.table.View())
101−}
102−
103−func (i *tableCmp) GetSize() (int, int) {
104− return i.table.Width(), i.table.Height()
105−}
106−
107−func (i *tableCmp) SetSize(width int, height int) tea.Cmd {
108− i.table.SetWidth(width)
109− i.table.SetHeight(height)
110−
111− columnWidth := (width - 10) / 4
112− i.table.SetColumns([]table.Column{
113− {
114− Title: "Level",
115− Width: 10,
116− },
117− {
118− Title: "ID",
119− Width: columnWidth,
120− },
121− {
122− Title: "Time",
123− Width: columnWidth,
124− },
125− {
126− Title: "Message",
127− Width: columnWidth,
128− },
129− {
130− Title: "Attributes",
131− Width: columnWidth,
132− },
133− })
134− return nil
135−}
136−
137−func (i *tableCmp) setRows() {
138− rows := []table.Row{}
139−
140− slices.SortFunc(i.logs, func(a, b logging.LogMessage) int {
141− if a.Time.Before(b.Time) {
142− return -1
143− }
144− if a.Time.After(b.Time) {
145− return 1
146− }
147− return 0
148− })
149−
150− for _, log := range i.logs {
151− rows = append(rows, logToRow(log))
152− }
153− i.table.SetRows(rows)
154−}
155−
156−func logToRow(log logging.LogMessage) table.Row {
157− // Format attributes as JSON string
158− var attrStr string
159− if len(log.Attributes) > 0 {
160− var parts []string
161− for _, attr := range log.Attributes {
162− parts = append(parts, fmt.Sprintf(`{"Key":"%s","Value":"%s"}`, attr.Key, attr.Value))
163− }
164− attrStr = "[" + strings.Join(parts, ",") + "]"
165− }
166−
167− // Format time with relative time
168− timeStr := log.Time.Format("2006-01-05 15:04:05 UTC")
169− relativeTime := getRelativeTime(log.Time)
170− fullTimeStr := timeStr + " " + relativeTime
171−
172− return table.Row{
173− strings.ToUpper(log.Level),
174− log.ID,
175− fullTimeStr,
176− log.Message,
177− attrStr,
178− }
179−}
180−
181−func NewLogsTable() TableComponent {
182− columns := []table.Column{
183− {Title: "Level"},
184− {Title: "ID"},
185− {Title: "Time"},
186− {Title: "Message"},
187− {Title: "Attributes"},
188− }
189−
190− tableModel := table.New(
191− table.WithColumns(columns),
192− )
193− tableModel.Focus()
194− return &tableCmp{
195− table: tableModel,
196− }
197−}
internal/tui/keys.go−6
@@ -5,7 +5,6 @@ import (
55)
66
77type KeyMap struct {
8− Logs key.Binding
98 Quit key.Binding
109 Help key.Binding
1110 Commands key.Binding
@@ -16,10 +15,6 @@ type KeyMap struct {
1615
1716func DefaultKeyMap() KeyMap {
1817 return KeyMap{
19− Logs: key.NewBinding(
20− key.WithKeys("ctrl+l"),
21− key.WithHelp("ctrl+l", "logs"),
22− ),
2318 Quit: key.NewBinding(
2419 key.WithKeys("ctrl+c"),
2520 key.WithHelp("ctrl+c", "quit"),
@@ -47,7 +42,6 @@ func (k KeyMap) FullHelp() [][]key.Binding {
4742 k.Sessions,
4843 k.Quit,
4944 k.Help,
50− k.Logs,
5145 }
5246 slice = k.prependEscAndTab(slice)
5347 slice = append(slice, k.pageBindings...)
internal/tui/page/logs/keys.godeleted−43
@@ -1,43 +0,0 @@
1−package logs
2−
3−import (
4− "github.com/charmbracelet/bubbles/v2/key"
5−)
6−
7−type KeyMap struct {
8− Back key.Binding
9−}
10−
11−func DefaultKeyMap() KeyMap {
12− return KeyMap{
13− Back: key.NewBinding(
14− key.WithKeys("esc", "backspace"),
15− key.WithHelp("esc/backspace", "back to chat"),
16− ),
17− }
18−}
19−
20−// KeyBindings implements layout.KeyMapProvider
21−func (k KeyMap) KeyBindings() []key.Binding {
22− return []key.Binding{
23− k.Back,
24− }
25−}
26−
27−// FullHelp implements help.KeyMap.
28−func (k KeyMap) FullHelp() [][]key.Binding {
29− m := [][]key.Binding{}
30− slice := k.KeyBindings()
31− for i := 0; i < len(slice); i += 4 {
32− end := min(i+4, len(slice))
33− m = append(m, slice[i:end])
34− }
35− return m
36−}
37−
38−// ShortHelp implements help.KeyMap.
39−func (k KeyMap) ShortHelp() []key.Binding {
40− return []key.Binding{
41− k.Back,
42− }
43−}
internal/tui/page/logs/logs.godeleted−100
@@ -1,100 +0,0 @@
1−package logs
2−
3−import (
4− "github.com/charmbracelet/bubbles/v2/key"
5− tea "github.com/charmbracelet/bubbletea/v2"
6− "github.com/charmbracelet/crush/internal/tui/components/core"
7− "github.com/charmbracelet/crush/internal/tui/components/core/layout"
8− logsComponents "github.com/charmbracelet/crush/internal/tui/components/logs"
9− "github.com/charmbracelet/crush/internal/tui/page"
10− "github.com/charmbracelet/crush/internal/tui/page/chat"
11− "github.com/charmbracelet/crush/internal/tui/styles"
12− "github.com/charmbracelet/crush/internal/tui/util"
13− "github.com/charmbracelet/lipgloss/v2"
14−)
15−
16−var LogsPage page.PageID = "logs"
17−
18−type LogPage interface {
19− util.Model
20− layout.Sizeable
21−}
22−
23−type logsPage struct {
24− width, height int
25− table logsComponents.TableComponent
26− details logsComponents.DetailComponent
27− keyMap KeyMap
28−}
29−
30−func (p *logsPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
31− var cmds []tea.Cmd
32− switch msg := msg.(type) {
33− case tea.WindowSizeMsg:
34− p.width = msg.Width
35− p.height = msg.Height
36− return p, p.SetSize(msg.Width, msg.Height)
37− case tea.KeyMsg:
38− switch {
39− case key.Matches(msg, p.keyMap.Back):
40− return p, util.CmdHandler(page.PageChangeMsg{ID: chat.ChatPageID})
41− }
42− }
43−
44− table, cmd := p.table.Update(msg)
45− cmds = append(cmds, cmd)
46− p.table = table.(logsComponents.TableComponent)
47− details, cmd := p.details.Update(msg)
48− cmds = append(cmds, cmd)
49− p.details = details.(logsComponents.DetailComponent)
50−
51− return p, tea.Batch(cmds...)
52−}
53−
54−func (p *logsPage) View() tea.View {
55− baseStyle := styles.CurrentTheme().S().Base
56− style := baseStyle.Width(p.width).Height(p.height).Padding(1)
57− title := core.Title("Logs", p.width-2)
58−
59− return tea.NewView(
60− style.Render(
61− lipgloss.JoinVertical(lipgloss.Top,
62− title,
63− p.details.View().String(),
64− p.table.View().String(),
65− ),
66− ),
67− )
68−}
69−
70−// GetSize implements LogPage.
71−func (p *logsPage) GetSize() (int, int) {
72− return p.width, p.height
73−}
74−
75−// SetSize implements LogPage.
76−func (p *logsPage) SetSize(width int, height int) tea.Cmd {
77− p.width = width
78− p.height = height
79− availableHeight := height - 2 // Padding for top and bottom
80− availableHeight -= 1 // title height
81− return tea.Batch(
82− p.table.SetSize(width-2, availableHeight/2),
83− p.details.SetSize(width-2, availableHeight/2),
84− )
85−}
86−
87−func (p *logsPage) Init() tea.Cmd {
88− return tea.Batch(
89− p.table.Init(),
90− p.details.Init(),
91− )
92−}
93−
94−func NewLogsPage() LogPage {
95− return &logsPage{
96− details: logsComponents.NewLogsDetails(),
97− table: logsComponents.NewLogsTable(),
98− keyMap: DefaultKeyMap(),
99− }
100−}
internal/tui/tui.go−22
@@ -9,7 +9,6 @@ import (
99 "github.com/charmbracelet/crush/internal/app"
1010 "github.com/charmbracelet/crush/internal/config"
1111 "github.com/charmbracelet/crush/internal/llm/agent"
12− "github.com/charmbracelet/crush/internal/logging"
1312 "github.com/charmbracelet/crush/internal/permission"
1413 "github.com/charmbracelet/crush/internal/pubsub"
1514 cmpChat "github.com/charmbracelet/crush/internal/tui/components/chat"
@@ -27,7 +26,6 @@ import (
2726 "github.com/charmbracelet/crush/internal/tui/components/dialogs/sessions"
2827 "github.com/charmbracelet/crush/internal/tui/page"
2928 "github.com/charmbracelet/crush/internal/tui/page/chat"
30− "github.com/charmbracelet/crush/internal/tui/page/logs"
3129 "github.com/charmbracelet/crush/internal/tui/styles"
3230 "github.com/charmbracelet/crush/internal/tui/util"
3331 "github.com/charmbracelet/lipgloss/v2"
@@ -135,20 +133,6 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
135133 a.selectedSessionID = msg.ID
136134 case cmpChat.SessionClearedMsg:
137135 a.selectedSessionID = ""
138− // Logs
139− case pubsub.Event[logging.LogMessage]:
140− // Send to the status component
141− s, statusCmd := a.status.Update(msg)
142− a.status = s.(status.StatusCmp)
143− cmds = append(cmds, statusCmd)
144−
145− // If the current page is logs, update the logs view
146− if a.currentPage == logs.LogsPage {
147− updated, pageCmd := a.pages[a.currentPage].Update(msg)
148− a.pages[a.currentPage] = updated.(util.Model)
149− cmds = append(cmds, pageCmd)
150− }
151− return a, tea.Batch(cmds...)
152136 // Commands
153137 case commands.SwitchSessionsMsg:
154138 return a, func() tea.Msg {
@@ -176,7 +160,6 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
176160
177161 // Update the agent with the new model/provider configuration
178162 if err := a.app.UpdateAgentModel(); err != nil {
179− logging.ErrorPersist(fmt.Sprintf("Failed to update agent model: %v", err))
180163 return a, util.ReportError(fmt.Errorf("model changed to %s but failed to update agent: %v", msg.Model.Model, err))
181164 }
182165
@@ -348,10 +331,6 @@ func (a *appModel) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
348331 },
349332 )
350333 return tea.Sequence(cmds...)
351− // Page navigation
352− case key.Matches(msg, a.keyMap.Logs):
353− return a.moveToPage(logs.LogsPage)
354−
355334 default:
356335 if a.dialog.HasDialogs() {
357336 u, dialogCmd := a.dialog.Update(msg)
@@ -453,7 +432,6 @@ func New(app *app.App) tea.Model {
453432
454433 pages: map[page.PageID]util.Model{
455434 chat.ChatPageID: chatPage,
456− logs.LogsPage: logs.NewLogsPage(),
457435 },
458436
459437 dialog: dialogs.NewDialogCmp(),
main.go+6−5
@@ -1,6 +1,7 @@
11package main
22
33import (
4+ "log/slog"
45 "net/http"
56 "os"
67
@@ -9,19 +10,19 @@ import (
910 _ "github.com/joho/godotenv/autoload" // automatically load .env files
1011
1112 "github.com/charmbracelet/crush/cmd"
12− "github.com/charmbracelet/crush/internal/logging"
13+ "github.com/charmbracelet/crush/internal/log"
1314)
1415
1516func main() {
16− defer logging.RecoverPanic("main", func() {
17− logging.ErrorPersist("Application terminated due to unhandled panic")
17+ defer log.RecoverPanic("main", func() {
18+ slog.Error("Application terminated due to unhandled panic")
1819 })
1920
2021 if os.Getenv("CRUSH_PROFILE") != "" {
2122 go func() {
22− logging.Info("Serving pprof at localhost:6060")
23+ slog.Info("Serving pprof at localhost:6060")
2324 if httpErr := http.ListenAndServe("localhost:6060", nil); httpErr != nil {
24− logging.Error("Failed to pprof listen: %v", httpErr)
25+ slog.Error("Failed to pprof listen: %v", httpErr)
2526 }
2627 }()
2728 }