on main help sign in
chore: remove logs
dafbdb7Kujtim Hoxha
cmd/logs.go+1
@@ -35,6 +35,7 @@ var logsCmd = &cobra.Command{
35 return fmt.Errorf("failed to tail log file: %v", err)
35 return fmt.Errorf("failed to tail log file: %v", err)
36 }
36 }
37
37
38+ log.SetLevel(log.DebugLevel)
38 // Print the text of each received line
39 // Print the text of each received line
39 for line := range t.Lines {
40 for line := range t.Lines {
40 var data map[string]any
41 var data map[string]any
cmd/root.go+23−23
@@ -4,6 +4,7 @@ import (
4 "context"
4 "context"
5 "fmt"
5 "fmt"
6 "io"
6 "io"
7+ "log/slog"
7 "os"
8 "os"
8 "sync"
9 "sync"
9 "time"
10 "time"
@@ -14,7 +15,7 @@ import (
14 "github.com/charmbracelet/crush/internal/db"
15 "github.com/charmbracelet/crush/internal/db"
15 "github.com/charmbracelet/crush/internal/format"
16 "github.com/charmbracelet/crush/internal/format"
16 "github.com/charmbracelet/crush/internal/llm/agent"
17 "github.com/charmbracelet/crush/internal/llm/agent"
17− "github.com/charmbracelet/crush/internal/logging"
18+ "github.com/charmbracelet/crush/internal/log"
18 "github.com/charmbracelet/crush/internal/pubsub"
19 "github.com/charmbracelet/crush/internal/pubsub"
19 "github.com/charmbracelet/crush/internal/tui"
20 "github.com/charmbracelet/crush/internal/tui"
20 "github.com/charmbracelet/crush/internal/version"
21 "github.com/charmbracelet/crush/internal/version"
@@ -36,7 +37,7 @@ to assist developers in writing, debugging, and understanding code directly from
36 # Run with debug logging
37 # Run with debug logging
37 crush -d
38 crush -d
38
39
39− # Run with debug logging in a specific directory
40+ # Run with debug slog.in a specific directory
40 crush -d -c /path/to/project
41 crush -d -c /path/to/project
41
42
42 # Print version
43 # Print version
@@ -92,7 +93,7 @@ to assist developers in writing, debugging, and understanding code directly from
92
93
93 app, err := app.New(ctx, conn)
94 app, err := app.New(ctx, conn)
94 if err != nil {
95 if err != nil {
95− logging.Error("Failed to create app: %v", err)
96+ slog.Error("Failed to create app: %v", err)
96 return err
97 return err
97 }
98 }
98 // Defer shutdown here so it runs for both interactive and non-interactive modes
99 // 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
103
104
104 prompt, err = maybePrependStdin(prompt)
105 prompt, err = maybePrependStdin(prompt)
105 if err != nil {
106 if err != nil {
106− logging.Error("Failed to read stdin: %v", err)
107+ slog.Error("Failed to read stdin: %v", err)
107 return err
108 return err
108 }
109 }
109
110
@@ -132,18 +133,18 @@ to assist developers in writing, debugging, and understanding code directly from
132 // Set up message handling for the TUI
133 // Set up message handling for the TUI
133 go func() {
134 go func() {
134 defer tuiWg.Done()
135 defer tuiWg.Done()
135− defer logging.RecoverPanic("TUI-message-handler", func() {
136+ defer log.RecoverPanic("TUI-message-handler", func() {
136 attemptTUIRecovery(program)
137 attemptTUIRecovery(program)
137 })
138 })
138
139
139 for {
140 for {
140 select {
141 select {
141 case <-tuiCtx.Done():
142 case <-tuiCtx.Done():
142− logging.Info("TUI message handler shutting down")
143+ slog.Info("TUI message handler shutting down")
143 return
144 return
144 case msg, ok := <-ch:
145 case msg, ok := <-ch:
145 if !ok {
146 if !ok {
146− logging.Info("TUI message channel closed")
147+ slog.Info("TUI message channel closed")
147 return
148 return
148 }
149 }
149 program.Send(msg)
150 program.Send(msg)
@@ -165,7 +166,7 @@ to assist developers in writing, debugging, and understanding code directly from
165 // Wait for TUI message handler to finish
166 // Wait for TUI message handler to finish
166 tuiWg.Wait()
167 tuiWg.Wait()
167
168
168− logging.Info("All goroutines cleaned up")
169+ slog.Info("All goroutines cleaned up")
169 }
170 }
170
171
171 // Run the TUI
172 // Run the TUI
@@ -173,18 +174,18 @@ to assist developers in writing, debugging, and understanding code directly from
173 cleanup()
174 cleanup()
174
175
175 if err != nil {
176 if err != nil {
176− logging.Error("TUI error: %v", err)
177+ slog.Error("TUI error: %v", err)
177 return fmt.Errorf("TUI error: %v", err)
178 return fmt.Errorf("TUI error: %v", err)
178 }
179 }
179
180
180− logging.Info("TUI exited with result: %v", result)
181+ slog.Info("TUI exited with result: %v", result)
181 return nil
182 return nil
182 },
183 },
183}
184}
184
185
185// attemptTUIRecovery tries to recover the TUI after a panic
186// attemptTUIRecovery tries to recover the TUI after a panic
186func attemptTUIRecovery(program *tea.Program) {
187func attemptTUIRecovery(program *tea.Program) {
187− logging.Info("Attempting to recover TUI after panic")
188+ slog.Info("Attempting to recover TUI after panic")
188
189
189 // We could try to restart the TUI or gracefully exit
190 // We could try to restart the TUI or gracefully exit
190 // For now, we'll just quit the program to avoid further issues
191 // For now, we'll just quit the program to avoid further issues
@@ -193,7 +194,7 @@ func attemptTUIRecovery(program *tea.Program) {
193
194
194func initMCPTools(ctx context.Context, app *app.App) {
195func initMCPTools(ctx context.Context, app *app.App) {
195 go func() {
196 go func() {
196− defer logging.RecoverPanic("MCP-goroutine", nil)
197+ defer log.RecoverPanic("MCP-goroutine", nil)
197
198
198 // Create a context with timeout for the initial MCP tools fetch
199 // Create a context with timeout for the initial MCP tools fetch
199 ctxWithTimeout, cancel := context.WithTimeout(ctx, 30*time.Second)
200 ctxWithTimeout, cancel := context.WithTimeout(ctx, 30*time.Second)
@@ -201,7 +202,7 @@ func initMCPTools(ctx context.Context, app *app.App) {
201
202
202 // Set this up once with proper error handling
203 // Set this up once with proper error handling
203 agent.GetMcpTools(ctxWithTimeout, app.Permissions)
204 agent.GetMcpTools(ctxWithTimeout, app.Permissions)
204− logging.Info("MCP message handling goroutine exiting")
205+ slog.Info("MCP message handling goroutine exiting")
205 }()
206 }()
206}
207}
207
208
@@ -215,7 +216,7 @@ func setupSubscriber[T any](
215 wg.Add(1)
216 wg.Add(1)
216 go func() {
217 go func() {
217 defer wg.Done()
218 defer wg.Done()
218− defer logging.RecoverPanic(fmt.Sprintf("subscription-%s", name), nil)
219+ defer log.RecoverPanic(fmt.Sprintf("subscription-%s", name), nil)
219
220
220 subCh := subscriber(ctx)
221 subCh := subscriber(ctx)
221
222
@@ -223,7 +224,7 @@ func setupSubscriber[T any](
223 select {
224 select {
224 case event, ok := <-subCh:
225 case event, ok := <-subCh:
225 if !ok {
226 if !ok {
226− logging.Info("subscription channel closed", "name", name)
227+ slog.Info("subscription channel closed", "name", name)
227 return
228 return
228 }
229 }
229
230
@@ -232,13 +233,13 @@ func setupSubscriber[T any](
232 select {
233 select {
233 case outputCh <- msg:
234 case outputCh <- msg:
234 case <-time.After(2 * time.Second):
235 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)
236 case <-ctx.Done():
237 case <-ctx.Done():
237− logging.Info("subscription cancelled", "name", name)
238+ slog.Info("subscription cancelled", "name", name)
238 return
239 return
239 }
240 }
240 case <-ctx.Done():
241 case <-ctx.Done():
241− logging.Info("subscription cancelled", "name", name)
242+ slog.Info("subscription cancelled", "name", name)
242 return
243 return
243 }
244 }
244 }
245 }
@@ -251,7 +252,6 @@ func setupSubscriptions(app *app.App, parentCtx context.Context) (chan tea.Msg,
251 wg := sync.WaitGroup{}
252 wg := sync.WaitGroup{}
252 ctx, cancel := context.WithCancel(parentCtx) // Inherit from parent context
253 ctx, cancel := context.WithCancel(parentCtx) // Inherit from parent context
253
254
254− setupSubscriber(ctx, &wg, "logging", logging.Subscribe, ch)
255 setupSubscriber(ctx, &wg, "sessions", app.Sessions.Subscribe, ch)
255 setupSubscriber(ctx, &wg, "sessions", app.Sessions.Subscribe, ch)
256 setupSubscriber(ctx, &wg, "messages", app.Messages.Subscribe, ch)
256 setupSubscriber(ctx, &wg, "messages", app.Messages.Subscribe, ch)
257 setupSubscriber(ctx, &wg, "permissions", app.Permissions.Subscribe, ch)
257 setupSubscriber(ctx, &wg, "permissions", app.Permissions.Subscribe, ch)
@@ -259,22 +259,22 @@ func setupSubscriptions(app *app.App, parentCtx context.Context) (chan tea.Msg,
259 setupSubscriber(ctx, &wg, "history", app.History.Subscribe, ch)
259 setupSubscriber(ctx, &wg, "history", app.History.Subscribe, ch)
260
260
261 cleanupFunc := func() {
261 cleanupFunc := func() {
262− logging.Info("Cancelling all subscriptions")
262+ slog.Info("Cancelling all subscriptions")
263 cancel() // Signal all goroutines to stop
263 cancel() // Signal all goroutines to stop
264
264
265 waitCh := make(chan struct{})
265 waitCh := make(chan struct{})
266 go func() {
266 go func() {
267− defer logging.RecoverPanic("subscription-cleanup", nil)
267+ defer log.RecoverPanic("subscription-cleanup", nil)
268 wg.Wait()
268 wg.Wait()
269 close(waitCh)
269 close(waitCh)
270 }()
270 }()
271
271
272 select {
272 select {
273 case <-waitCh:
273 case <-waitCh:
274− logging.Info("All subscription goroutines completed successfully")
274+ slog.Info("All subscription goroutines completed successfully")
275 close(ch) // Only close after all writers are confirmed done
275 close(ch) // Only close after all writers are confirmed done
276 case <-time.After(5 * time.Second):
276 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")
278 close(ch)
278 close(ch)
279 }
279 }
280 }
280 }
internal/app/app.go+7−7
@@ -14,7 +14,7 @@ import (
14 "github.com/charmbracelet/crush/internal/format"
14 "github.com/charmbracelet/crush/internal/format"
15 "github.com/charmbracelet/crush/internal/history"
15 "github.com/charmbracelet/crush/internal/history"
16 "github.com/charmbracelet/crush/internal/llm/agent"
16 "github.com/charmbracelet/crush/internal/llm/agent"
17− "github.com/charmbracelet/crush/internal/logging"
17+ "log/slog"
18 "github.com/charmbracelet/crush/internal/lsp"
18 "github.com/charmbracelet/crush/internal/lsp"
19 "github.com/charmbracelet/crush/internal/message"
19 "github.com/charmbracelet/crush/internal/message"
20 "github.com/charmbracelet/crush/internal/permission"
20 "github.com/charmbracelet/crush/internal/permission"
@@ -73,7 +73,7 @@ func New(ctx context.Context, conn *sql.DB) (*App, error) {
73 app.LSPClients,
73 app.LSPClients,
74 )
74 )
75 if err != nil {
75 if err != nil {
76− logging.Error("Failed to create coder agent", err)
76+ slog.Error("Failed to create coder agent", err)
77 return nil, err
77 return nil, err
78 }
78 }
79
79
@@ -82,7 +82,7 @@ func New(ctx context.Context, conn *sql.DB) (*App, error) {
82
82
83// RunNonInteractive handles the execution flow when a prompt is provided via CLI flag.
83// RunNonInteractive handles the execution flow when a prompt is provided via CLI flag.
84func (a *App) RunNonInteractive(ctx context.Context, prompt string, outputFormat string, quiet bool) error {
84func (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")
86
86
87 // Start spinner if not in quiet mode
87 // Start spinner if not in quiet mode
88 var spinner *format.Spinner
88 var spinner *format.Spinner
@@ -107,7 +107,7 @@ func (a *App) RunNonInteractive(ctx context.Context, prompt string, outputFormat
107 if err != nil {
107 if err != nil {
108 return fmt.Errorf("failed to create session for non-interactive mode: %w", err)
108 return fmt.Errorf("failed to create session for non-interactive mode: %w", err)
109 }
109 }
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)
111
111
112 // Automatically approve all permission requests for this non-interactive session
112 // Automatically approve all permission requests for this non-interactive session
113 a.Permissions.AutoApproveSession(sess.ID)
113 a.Permissions.AutoApproveSession(sess.ID)
@@ -120,7 +120,7 @@ func (a *App) RunNonInteractive(ctx context.Context, prompt string, outputFormat
120 result := <-done
120 result := <-done
121 if result.Error != nil {
121 if result.Error != nil {
122 if errors.Is(result.Error, context.Canceled) || errors.Is(result.Error, agent.ErrRequestCancelled) {
122 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)
124 return nil
124 return nil
125 }
125 }
126 return fmt.Errorf("agent processing failed: %w", result.Error)
126 return fmt.Errorf("agent processing failed: %w", result.Error)
@@ -139,7 +139,7 @@ func (a *App) RunNonInteractive(ctx context.Context, prompt string, outputFormat
139
139
140 fmt.Println(format.FormatOutput(content, outputFormat))
140 fmt.Println(format.FormatOutput(content, outputFormat))
141
141
142− logging.Info("Non-interactive run completed", "session_id", sess.ID)
142+ slog.Info("Non-interactive run completed", "session_id", sess.ID)
143
143
144 return nil
144 return nil
145}
145}
@@ -163,7 +163,7 @@ func (app *App) Shutdown() {
163 for name, client := range clients {
163 for name, client := range clients {
164 shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
164 shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
165 if err := client.Shutdown(shutdownCtx); err != nil {
165 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)
167 }
167 }
168 cancel()
168 cancel()
169 }
169 }
internal/app/lsp.go+13−12
@@ -2,10 +2,11 @@ package app
2
2
3import (
3import (
4 "context"
4 "context"
5+ "log/slog"
5 "time"
6 "time"
6
7
7 "github.com/charmbracelet/crush/internal/config"
8 "github.com/charmbracelet/crush/internal/config"
8− "github.com/charmbracelet/crush/internal/logging"
9+ "github.com/charmbracelet/crush/internal/log"
9 "github.com/charmbracelet/crush/internal/lsp"
10 "github.com/charmbracelet/crush/internal/lsp"
10 "github.com/charmbracelet/crush/internal/lsp/watcher"
11 "github.com/charmbracelet/crush/internal/lsp/watcher"
11)
12)
@@ -18,18 +19,18 @@ func (app *App) initLSPClients(ctx context.Context) {
18 // Start each client initialization in its own goroutine
19 // Start each client initialization in its own goroutine
19 go app.createAndStartLSPClient(ctx, name, clientConfig.Command, clientConfig.Args...)
20 go app.createAndStartLSPClient(ctx, name, clientConfig.Command, clientConfig.Args...)
20 }
21 }
21− logging.Info("LSP clients initialization started in background")
22+ slog.Info("LSP clients initialization started in background")
22}
23}
23
24
24// createAndStartLSPClient creates a new LSP client, initializes it, and starts its workspace watcher
25// createAndStartLSPClient creates a new LSP client, initializes it, and starts its workspace watcher
25func (app *App) createAndStartLSPClient(ctx context.Context, name string, command string, args ...string) {
26func (app *App) createAndStartLSPClient(ctx context.Context, name string, command string, args ...string) {
26 // Create a specific context for initialization with a timeout
27 // 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)
28
29
29 // Create the LSP client
30 // Create the LSP client
30 lspClient, err := lsp.NewClient(ctx, command, args...)
31 lspClient, err := lsp.NewClient(ctx, command, args...)
31 if err != nil {
32 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)
33 return
34 return
34 }
35 }
35
36
@@ -40,7 +41,7 @@ func (app *App) createAndStartLSPClient(ctx context.Context, name string, comman
40 // Initialize with the initialization context
41 // Initialize with the initialization context
41 _, err = lspClient.InitializeLSPClient(initCtx, config.Get().WorkingDir())
42 _, err = lspClient.InitializeLSPClient(initCtx, config.Get().WorkingDir())
42 if err != nil {
43 if err != nil {
43− logging.Error("Initialize failed", "name", name, "error", err)
44+ slog.Error("Initialize failed", "name", name, "error", err)
44 // Clean up the client to prevent resource leaks
45 // Clean up the client to prevent resource leaks
45 lspClient.Close()
46 lspClient.Close()
46 return
47 return
@@ -48,15 +49,15 @@ func (app *App) createAndStartLSPClient(ctx context.Context, name string, comman
48
49
49 // Wait for the server to be ready
50 // Wait for the server to be ready
50 if err := lspClient.WaitForServerReady(initCtx); err != nil {
51 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)
52 // We'll continue anyway, as some functionality might still work
53 // We'll continue anyway, as some functionality might still work
53 lspClient.SetServerState(lsp.StateError)
54 lspClient.SetServerState(lsp.StateError)
54 } else {
55 } else {
55− logging.Info("LSP server is ready", "name", name)
56+ slog.Info("LSP server is ready", "name", name)
56 lspClient.SetServerState(lsp.StateReady)
57 lspClient.SetServerState(lsp.StateReady)
57 }
58 }
58
59
59− logging.Info("LSP client initialized", "name", name)
60+ slog.Info("LSP client initialized", "name", name)
60
61
61 // Create a child context that can be canceled when the app is shutting down
62 // Create a child context that can be canceled when the app is shutting down
62 watchCtx, cancelFunc := context.WithCancel(ctx)
63 watchCtx, cancelFunc := context.WithCancel(ctx)
@@ -86,13 +87,13 @@ func (app *App) createAndStartLSPClient(ctx context.Context, name string, comman
86// runWorkspaceWatcher executes the workspace watcher for an LSP client
87// runWorkspaceWatcher executes the workspace watcher for an LSP client
87func (app *App) runWorkspaceWatcher(ctx context.Context, name string, workspaceWatcher *watcher.WorkspaceWatcher) {
88func (app *App) runWorkspaceWatcher(ctx context.Context, name string, workspaceWatcher *watcher.WorkspaceWatcher) {
88 defer app.watcherWG.Done()
89 defer app.watcherWG.Done()
89− defer logging.RecoverPanic("LSP-"+name, func() {
90+ defer log.RecoverPanic("LSP-"+name, func() {
90 // Try to restart the client
91 // Try to restart the client
91 app.restartLSPClient(ctx, name)
92 app.restartLSPClient(ctx, name)
92 })
93 })
93
94
94 workspaceWatcher.WatchWorkspace(ctx, config.Get().WorkingDir())
95 workspaceWatcher.WatchWorkspace(ctx, config.Get().WorkingDir())
95− logging.Info("Workspace watcher stopped", "client", name)
96+ slog.Info("Workspace watcher stopped", "client", name)
96}
97}
97
98
98// restartLSPClient attempts to restart a crashed or failed LSP client
99// restartLSPClient attempts to restart a crashed or failed LSP client
@@ -101,7 +102,7 @@ func (app *App) restartLSPClient(ctx context.Context, name string) {
101 cfg := config.Get()
102 cfg := config.Get()
102 clientConfig, exists := cfg.LSP[name]
103 clientConfig, exists := cfg.LSP[name]
103 if !exists {
104 if !exists {
104− logging.Error("Cannot restart client, configuration not found", "client", name)
105+ slog.Error("Cannot restart client, configuration not found", "client", name)
105 return
106 return
106 }
107 }
107
108
@@ -122,5 +123,5 @@ func (app *App) restartLSPClient(ctx context.Context, name string) {
122
123
123 // Create a new client using the shared function
124 // Create a new client using the shared function
124 app.createAndStartLSPClient(ctx, name, clientConfig.Command, clientConfig.Args...)
125 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)
126}
127}
internal/config/init.go+2−2
@@ -8,7 +8,7 @@ import (
8 "sync"
8 "sync"
9 "sync/atomic"
9 "sync/atomic"
10
10
11− "github.com/charmbracelet/crush/internal/logging"
11+ "log/slog"
12)
12)
13
13
14const (
14const (
@@ -32,7 +32,7 @@ func Init(workingDir string, debug bool) (*Config, error) {
32 cwd = workingDir
32 cwd = workingDir
33 cfg, err := Load(cwd, debug)
33 cfg, err := Load(cwd, debug)
34 if err != nil {
34 if err != nil {
35− logging.Error("Failed to load config", "error", err)
35+ slog.Error("Failed to load config", "error", err)
36 }
36 }
37 instance.Store(cfg)
37 instance.Store(cfg)
38 })
38 })
internal/config/load.go+5−2
@@ -42,6 +42,11 @@ func Load(workingDir string, debug bool) (*Config, error) {
42 filepath.Join(workingDir, fmt.Sprintf(".%s.json", appName)),
42 filepath.Join(workingDir, fmt.Sprintf(".%s.json", appName)),
43 }
43 }
44 cfg, err := loadFromConfigPaths(configPaths)
44 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)
45
50
46 if debug {
51 if debug {
47 cfg.Options.Debug = true
52 cfg.Options.Debug = true
@@ -57,8 +62,6 @@ func Load(workingDir string, debug bool) (*Config, error) {
57 return nil, fmt.Errorf("failed to load config: %w", err)
62 return nil, fmt.Errorf("failed to load config: %w", err)
58 }
63 }
59
64
60− cfg.setDefaults(workingDir)
61−
62 // Load known providers, this loads the config from fur
65 // Load known providers, this loads the config from fur
63 providers, err := LoadProviders(client.New())
66 providers, err := LoadProviders(client.New())
64 if err != nil || len(providers) == 0 {
67 if err != nil || len(providers) == 0 {
internal/db/connect.go+5−5
@@ -11,7 +11,7 @@ import (
11 _ "github.com/ncruces/go-sqlite3/embed"
11 _ "github.com/ncruces/go-sqlite3/embed"
12
12
13 "github.com/charmbracelet/crush/internal/config"
13 "github.com/charmbracelet/crush/internal/config"
14− "github.com/charmbracelet/crush/internal/logging"
14+ "log/slog"
15
15
16 "github.com/pressly/goose/v3"
16 "github.com/pressly/goose/v3"
17)
17)
@@ -48,21 +48,21 @@ func Connect(ctx context.Context) (*sql.DB, error) {
48
48
49 for _, pragma := range pragmas {
49 for _, pragma := range pragmas {
50 if _, err = db.ExecContext(ctx, pragma); err != nil {
50 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)
52 } else {
52 } else {
53− logging.Debug("Set pragma", "pragma", pragma)
53+ slog.Debug("Set pragma", "pragma", pragma)
54 }
54 }
55 }
55 }
56
56
57 goose.SetBaseFS(FS)
57 goose.SetBaseFS(FS)
58
58
59 if err := goose.SetDialect("sqlite3"); err != nil {
59 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)
61 return nil, fmt.Errorf("failed to set dialect: %w", err)
61 return nil, fmt.Errorf("failed to set dialect: %w", err)
62 }
62 }
63
63
64 if err := goose.Up(db, "migrations"); err != nil {
64 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)
66 return nil, fmt.Errorf("failed to apply migrations: %w", err)
66 return nil, fmt.Errorf("failed to apply migrations: %w", err)
67 }
67 }
68 return db, nil
68 return db, nil
internal/fsext/fileutil.go+3−3
@@ -11,7 +11,7 @@ import (
11
11
12 "github.com/bmatcuk/doublestar/v4"
12 "github.com/bmatcuk/doublestar/v4"
13 "github.com/charlievieth/fastwalk"
13 "github.com/charlievieth/fastwalk"
14− "github.com/charmbracelet/crush/internal/logging"
14+ "log/slog"
15 ignore "github.com/sabhiram/go-gitignore"
15 ignore "github.com/sabhiram/go-gitignore"
16)
16)
17
17
@@ -24,11 +24,11 @@ func init() {
24 var err error
24 var err error
25 rgPath, err = exec.LookPath("rg")
25 rgPath, err = exec.LookPath("rg")
26 if err != nil {
26 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.")
28 }
28 }
29 fzfPath, err = exec.LookPath("fzf")
29 fzfPath, err = exec.LookPath("fzf")
30 if err != nil {
30 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.")
32 }
32 }
33}
33}
34
34
internal/llm/agent/agent.go+16−19
@@ -4,6 +4,7 @@ import (
4 "context"
4 "context"
5 "errors"
5 "errors"
6 "fmt"
6 "fmt"
7+ "log/slog"
7 "slices"
8 "slices"
8 "strings"
9 "strings"
9 "sync"
10 "sync"
@@ -15,7 +16,7 @@ import (
15 "github.com/charmbracelet/crush/internal/llm/prompt"
16 "github.com/charmbracelet/crush/internal/llm/prompt"
16 "github.com/charmbracelet/crush/internal/llm/provider"
17 "github.com/charmbracelet/crush/internal/llm/provider"
17 "github.com/charmbracelet/crush/internal/llm/tools"
18 "github.com/charmbracelet/crush/internal/llm/tools"
18− "github.com/charmbracelet/crush/internal/logging"
19+ "github.com/charmbracelet/crush/internal/log"
19 "github.com/charmbracelet/crush/internal/lsp"
20 "github.com/charmbracelet/crush/internal/lsp"
20 "github.com/charmbracelet/crush/internal/message"
21 "github.com/charmbracelet/crush/internal/message"
21 "github.com/charmbracelet/crush/internal/permission"
22 "github.com/charmbracelet/crush/internal/permission"
@@ -223,7 +224,7 @@ func (a *agent) Cancel(sessionID string) {
223 // Cancel regular requests
224 // Cancel regular requests
224 if cancelFunc, exists := a.activeRequests.LoadAndDelete(sessionID); exists {
225 if cancelFunc, exists := a.activeRequests.LoadAndDelete(sessionID); exists {
225 if cancel, ok := cancelFunc.(context.CancelFunc); ok {
226 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))
227 cancel()
228 cancel()
228 }
229 }
229 }
230 }
@@ -231,7 +232,7 @@ func (a *agent) Cancel(sessionID string) {
231 // Also check for summarize requests
232 // Also check for summarize requests
232 if cancelFunc, exists := a.activeRequests.LoadAndDelete(sessionID + "-summarize"); exists {
233 if cancelFunc, exists := a.activeRequests.LoadAndDelete(sessionID + "-summarize"); exists {
233 if cancel, ok := cancelFunc.(context.CancelFunc); ok {
234 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))
235 cancel()
236 cancel()
236 }
237 }
237 }
238 }
@@ -325,8 +326,8 @@ func (a *agent) Run(ctx context.Context, sessionID string, content string, attac
325
326
326 a.activeRequests.Store(sessionID, cancel)
327 a.activeRequests.Store(sessionID, cancel)
327 go func() {
328 go func() {
328− logging.Debug("Request started", "sessionID", sessionID)
329+ slog.Debug("Request started", "sessionID", sessionID)
329− defer logging.RecoverPanic("agent.Run", func() {
330+ defer log.RecoverPanic("agent.Run", func() {
330 events <- a.err(fmt.Errorf("panic while running the agent"))
331 events <- a.err(fmt.Errorf("panic while running the agent"))
331 })
332 })
332 var attachmentParts []message.ContentPart
333 var attachmentParts []message.ContentPart
@@ -335,9 +336,9 @@ func (a *agent) Run(ctx context.Context, sessionID string, content string, attac
335 }
336 }
336 result := a.processGeneration(genCtx, sessionID, content, attachmentParts)
337 result := a.processGeneration(genCtx, sessionID, content, attachmentParts)
337 if result.Error != nil && !errors.Is(result.Error, ErrRequestCancelled) && !errors.Is(result.Error, context.Canceled) {
338 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())
339 }
340 }
340− logging.Debug("Request completed", "sessionID", sessionID)
341+ slog.Debug("Request completed", "sessionID", sessionID)
341 a.activeRequests.Delete(sessionID)
342 a.activeRequests.Delete(sessionID)
342 cancel()
343 cancel()
343 a.Publish(pubsub.CreatedEvent, result)
344 a.Publish(pubsub.CreatedEvent, result)
@@ -356,12 +357,12 @@ func (a *agent) processGeneration(ctx context.Context, sessionID, content string
356 }
357 }
357 if len(msgs) == 0 {
358 if len(msgs) == 0 {
358 go func() {
359 go func() {
359− defer logging.RecoverPanic("agent.Run", func() {
360+ defer log.RecoverPanic("agent.Run", func() {
360− logging.ErrorPersist("panic while generating title")
361+ slog.Error("panic while generating title")
361 })
362 })
362 titleErr := a.generateTitle(context.Background(), sessionID, content)
363 titleErr := a.generateTitle(context.Background(), sessionID, content)
363 if titleErr != nil && !errors.Is(titleErr, context.Canceled) && !errors.Is(titleErr, context.DeadlineExceeded) {
364 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))
365 }
366 }
366 }()
367 }()
367 }
368 }
@@ -408,11 +409,7 @@ func (a *agent) processGeneration(ctx context.Context, sessionID, content string
408 return a.err(fmt.Errorf("failed to process events: %w", err))
409 return a.err(fmt.Errorf("failed to process events: %w", err))
409 }
410 }
410 if cfg.Options.Debug {
411 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)
416 }
413 }
417 if (agentMessage.FinishReason() == message.FinishReasonToolUse) && toolResults != nil {
414 if (agentMessage.FinishReason() == message.FinishReasonToolUse) && toolResults != nil {
418 // We are not done, we need to respond with the tool response
415 // 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
571 assistantMsg.AppendContent(event.Content)
568 assistantMsg.AppendContent(event.Content)
572 return a.messages.Update(ctx, *assistantMsg)
569 return a.messages.Update(ctx, *assistantMsg)
573 case provider.EventToolUseStart:
570 case provider.EventToolUseStart:
574− logging.Info("Tool call started", "toolCall", event.ToolCall)
571+ slog.Info("Tool call started", "toolCall", event.ToolCall)
575 assistantMsg.AddToolCall(*event.ToolCall)
572 assistantMsg.AddToolCall(*event.ToolCall)
576 return a.messages.Update(ctx, *assistantMsg)
573 return a.messages.Update(ctx, *assistantMsg)
577 case provider.EventToolUseDelta:
574 case provider.EventToolUseDelta:
578 assistantMsg.AppendToolCallInput(event.ToolCall.ID, event.ToolCall.Input)
575 assistantMsg.AppendToolCallInput(event.ToolCall.ID, event.ToolCall.Input)
579 return a.messages.Update(ctx, *assistantMsg)
576 return a.messages.Update(ctx, *assistantMsg)
580 case provider.EventToolUseStop:
577 case provider.EventToolUseStop:
581− logging.Info("Finished tool call", "toolCall", event.ToolCall)
578+ slog.Info("Finished tool call", "toolCall", event.ToolCall)
582 assistantMsg.FinishToolCall(event.ToolCall.ID)
579 assistantMsg.FinishToolCall(event.ToolCall.ID)
583 return a.messages.Update(ctx, *assistantMsg)
580 return a.messages.Update(ctx, *assistantMsg)
584 case provider.EventError:
581 case provider.EventError:
585 if errors.Is(event.Error, context.Canceled) {
582 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))
587 return context.Canceled
584 return context.Canceled
588 }
585 }
589− logging.ErrorPersist(event.Error.Error())
586+ slog.Error(event.Error.Error())
590 return event.Error
587 return event.Error
591 case provider.EventComplete:
588 case provider.EventComplete:
592 assistantMsg.SetToolCalls(event.Response.ToolCalls)
589 assistantMsg.SetToolCalls(event.Response.ToolCalls)
internal/llm/agent/mcp-tools.go+6−6
@@ -7,7 +7,7 @@ import (
7
7
8 "github.com/charmbracelet/crush/internal/config"
8 "github.com/charmbracelet/crush/internal/config"
9 "github.com/charmbracelet/crush/internal/llm/tools"
9 "github.com/charmbracelet/crush/internal/llm/tools"
10− "github.com/charmbracelet/crush/internal/logging"
10+ "log/slog"
11 "github.com/charmbracelet/crush/internal/permission"
11 "github.com/charmbracelet/crush/internal/permission"
12 "github.com/charmbracelet/crush/internal/version"
12 "github.com/charmbracelet/crush/internal/version"
13
13
@@ -164,13 +164,13 @@ func getTools(ctx context.Context, name string, m config.MCPConfig, permissions
164
164
165 _, err := c.Initialize(ctx, initRequest)
165 _, err := c.Initialize(ctx, initRequest)
166 if err != nil {
166 if err != nil {
167− logging.Error("error initializing mcp client", "error", err)
167+ slog.Error("error initializing mcp client", "error", err)
168 return stdioTools
168 return stdioTools
169 }
169 }
170 toolsRequest := mcp.ListToolsRequest{}
170 toolsRequest := mcp.ListToolsRequest{}
171 tools, err := c.ListTools(ctx, toolsRequest)
171 tools, err := c.ListTools(ctx, toolsRequest)
172 if err != nil {
172 if err != nil {
173− logging.Error("error listing tools", "error", err)
173+ slog.Error("error listing tools", "error", err)
174 return stdioTools
174 return stdioTools
175 }
175 }
176 for _, t := range tools.Tools {
176 for _, t := range tools.Tools {
@@ -193,7 +193,7 @@ func GetMcpTools(ctx context.Context, permissions permission.Service) []tools.Ba
193 m.Args...,
193 m.Args...,
194 )
194 )
195 if err != nil {
195 if err != nil {
196− logging.Error("error creating mcp client", "error", err)
196+ slog.Error("error creating mcp client", "error", err)
197 continue
197 continue
198 }
198 }
199
199
@@ -204,7 +204,7 @@ func GetMcpTools(ctx context.Context, permissions permission.Service) []tools.Ba
204 transport.WithHTTPHeaders(m.Headers),
204 transport.WithHTTPHeaders(m.Headers),
205 )
205 )
206 if err != nil {
206 if err != nil {
207− logging.Error("error creating mcp client", "error", err)
207+ slog.Error("error creating mcp client", "error", err)
208 continue
208 continue
209 }
209 }
210 mcpTools = append(mcpTools, getTools(ctx, name, m, permissions, c)...)
210 mcpTools = append(mcpTools, getTools(ctx, name, m, permissions, c)...)
@@ -214,7 +214,7 @@ func GetMcpTools(ctx context.Context, permissions permission.Service) []tools.Ba
214 client.WithHeaders(m.Headers),
214 client.WithHeaders(m.Headers),
215 )
215 )
216 if err != nil {
216 if err != nil {
217− logging.Error("error creating mcp client", "error", err)
217+ slog.Error("error creating mcp client", "error", err)
218 continue
218 continue
219 }
219 }
220 mcpTools = append(mcpTools, getTools(ctx, name, m, permissions, c)...)
220 mcpTools = append(mcpTools, getTools(ctx, name, m, permissions, c)...)
internal/llm/prompt/coder.go+2−2
@@ -11,7 +11,7 @@ import (
11 "github.com/charmbracelet/crush/internal/config"
11 "github.com/charmbracelet/crush/internal/config"
12 "github.com/charmbracelet/crush/internal/fur/provider"
12 "github.com/charmbracelet/crush/internal/fur/provider"
13 "github.com/charmbracelet/crush/internal/llm/tools"
13 "github.com/charmbracelet/crush/internal/llm/tools"
14− "github.com/charmbracelet/crush/internal/logging"
14+ "log/slog"
15)
15)
16
16
17func CoderPrompt(p string, contextFiles ...string) string {
17func CoderPrompt(p string, contextFiles ...string) string {
@@ -29,7 +29,7 @@ func CoderPrompt(p string, contextFiles ...string) string {
29 basePrompt = fmt.Sprintf("%s\n\n%s\n%s", basePrompt, envInfo, lspInformation())
29 basePrompt = fmt.Sprintf("%s\n\n%s\n%s", basePrompt, envInfo, lspInformation())
30
30
31 contextContent := getContextFromPaths(contextFiles)
31 contextContent := getContextFromPaths(contextFiles)
32− logging.Debug("Context content", "Context", contextContent)
32+ slog.Debug("Context content", "Context", contextContent)
33 if contextContent != "" {
33 if contextContent != "" {
34 return fmt.Sprintf("%s\n\n# Project-Specific Context\n Make sure to follow the instructions in the context below\n%s", basePrompt, contextContent)
34 return fmt.Sprintf("%s\n\n# Project-Specific Context\n Make sure to follow the instructions in the context below\n%s", basePrompt, contextContent)
35 }
35 }
internal/llm/provider/anthropic.go+9−9
@@ -6,6 +6,7 @@ import (
6 "errors"
6 "errors"
7 "fmt"
7 "fmt"
8 "io"
8 "io"
9+ "log/slog"
9 "regexp"
10 "regexp"
10 "strconv"
11 "strconv"
11 "time"
12 "time"
@@ -16,7 +17,6 @@ import (
16 "github.com/charmbracelet/crush/internal/config"
17 "github.com/charmbracelet/crush/internal/config"
17 "github.com/charmbracelet/crush/internal/fur/provider"
18 "github.com/charmbracelet/crush/internal/fur/provider"
18 "github.com/charmbracelet/crush/internal/llm/tools"
19 "github.com/charmbracelet/crush/internal/llm/tools"
19− "github.com/charmbracelet/crush/internal/logging"
20 "github.com/charmbracelet/crush/internal/message"
20 "github.com/charmbracelet/crush/internal/message"
21)
21)
22
22
@@ -92,7 +92,7 @@ func (a *anthropicClient) convertMessages(messages []message.Message) (anthropic
92 }
92 }
93
93
94 if len(blocks) == 0 {
94 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")
96 continue
96 continue
97 }
97 }
98 anthropicMessages = append(anthropicMessages, anthropic.NewAssistantMessage(blocks...))
98 anthropicMessages = append(anthropicMessages, anthropic.NewAssistantMessage(blocks...))
@@ -207,7 +207,7 @@ func (a *anthropicClient) send(ctx context.Context, messages []message.Message,
207 preparedMessages := a.preparedMessages(a.convertMessages(messages), a.convertTools(tools))
207 preparedMessages := a.preparedMessages(a.convertMessages(messages), a.convertTools(tools))
208 if cfg.Options.Debug {
208 if cfg.Options.Debug {
209 jsonData, _ := json.Marshal(preparedMessages)
209 jsonData, _ := json.Marshal(preparedMessages)
210− logging.Debug("Prepared messages", "messages", string(jsonData))
210+ slog.Debug("Prepared messages", "messages", string(jsonData))
211 }
211 }
212
212
213 anthropicResponse, err := a.client.Messages.New(
213 anthropicResponse, err := a.client.Messages.New(
@@ -216,13 +216,13 @@ func (a *anthropicClient) send(ctx context.Context, messages []message.Message,
216 )
216 )
217 // If there is an error we are going to see if we can retry the call
217 // If there is an error we are going to see if we can retry the call
218 if err != nil {
218 if err != nil {
219− logging.Error("Error in Anthropic API call", "error", err)
219+ slog.Error("Error in Anthropic API call", "error", err)
220 retry, after, retryErr := a.shouldRetry(attempts, err)
220 retry, after, retryErr := a.shouldRetry(attempts, err)
221 if retryErr != nil {
221 if retryErr != nil {
222 return nil, retryErr
222 return nil, retryErr
223 }
223 }
224 if retry {
224 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))
226 select {
226 select {
227 case <-ctx.Done():
227 case <-ctx.Done():
228 return nil, ctx.Err()
228 return nil, ctx.Err()
@@ -259,7 +259,7 @@ func (a *anthropicClient) stream(ctx context.Context, messages []message.Message
259 preparedMessages := a.preparedMessages(a.convertMessages(messages), a.convertTools(tools))
259 preparedMessages := a.preparedMessages(a.convertMessages(messages), a.convertTools(tools))
260 if cfg.Options.Debug {
260 if cfg.Options.Debug {
261 jsonData, _ := json.Marshal(preparedMessages)
261 jsonData, _ := json.Marshal(preparedMessages)
262− logging.Debug("Prepared messages", "messages", string(jsonData))
262+ slog.Debug("Prepared messages", "messages", string(jsonData))
263 }
263 }
264
264
265 anthropicStream := a.client.Messages.NewStreaming(
265 anthropicStream := a.client.Messages.NewStreaming(
@@ -273,7 +273,7 @@ func (a *anthropicClient) stream(ctx context.Context, messages []message.Message
273 event := anthropicStream.Current()
273 event := anthropicStream.Current()
274 err := accumulatedMessage.Accumulate(event)
274 err := accumulatedMessage.Accumulate(event)
275 if err != nil {
275 if err != nil {
276− logging.Warn("Error accumulating message", "error", err)
276+ slog.Warn("Error accumulating message", "error", err)
277 continue
277 continue
278 }
278 }
279
279
@@ -364,7 +364,7 @@ func (a *anthropicClient) stream(ctx context.Context, messages []message.Message
364 return
364 return
365 }
365 }
366 if retry {
366 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))
368 select {
368 select {
369 case <-ctx.Done():
369 case <-ctx.Done():
370 // context cancelled
370 // context cancelled
@@ -411,7 +411,7 @@ func (a *anthropicClient) shouldRetry(attempts int, err error) (bool, int64, err
411 if apiErr.StatusCode == 400 {
411 if apiErr.StatusCode == 400 {
412 if adjusted, ok := a.handleContextLimitError(apiErr); ok {
412 if adjusted, ok := a.handleContextLimitError(apiErr); ok {
413 a.adjustedMaxTokens = adjusted
413 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)
415 return true, 0, nil
415 return true, 0, nil
416 }
416 }
417 }
417 }
internal/llm/provider/gemini.go+6−6
@@ -6,13 +6,13 @@ import (
6 "errors"
6 "errors"
7 "fmt"
7 "fmt"
8 "io"
8 "io"
9+ "log/slog"
9 "strings"
10 "strings"
10 "time"
11 "time"
11
12
12 "github.com/charmbracelet/crush/internal/config"
13 "github.com/charmbracelet/crush/internal/config"
13 "github.com/charmbracelet/crush/internal/fur/provider"
14 "github.com/charmbracelet/crush/internal/fur/provider"
14 "github.com/charmbracelet/crush/internal/llm/tools"
15 "github.com/charmbracelet/crush/internal/llm/tools"
15− "github.com/charmbracelet/crush/internal/logging"
16 "github.com/charmbracelet/crush/internal/message"
16 "github.com/charmbracelet/crush/internal/message"
17 "github.com/google/uuid"
17 "github.com/google/uuid"
18 "google.golang.org/genai"
18 "google.golang.org/genai"
@@ -28,7 +28,7 @@ type GeminiClient ProviderClient
28func newGeminiClient(opts providerClientOptions) GeminiClient {
28func newGeminiClient(opts providerClientOptions) GeminiClient {
29 client, err := createGeminiClient(opts)
29 client, err := createGeminiClient(opts)
30 if err != nil {
30 if err != nil {
31− logging.Error("Failed to create Gemini client", "error", err)
31+ slog.Error("Failed to create Gemini client", "error", err)
32 return nil
32 return nil
33 }
33 }
34
34
@@ -168,7 +168,7 @@ func (g *geminiClient) send(ctx context.Context, messages []message.Message, too
168 cfg := config.Get()
168 cfg := config.Get()
169 if cfg.Options.Debug {
169 if cfg.Options.Debug {
170 jsonData, _ := json.Marshal(geminiMessages)
170 jsonData, _ := json.Marshal(geminiMessages)
171− logging.Debug("Prepared messages", "messages", string(jsonData))
171+ slog.Debug("Prepared messages", "messages", string(jsonData))
172 }
172 }
173
173
174 modelConfig := cfg.Models[config.SelectedModelTypeLarge]
174 modelConfig := cfg.Models[config.SelectedModelTypeLarge]
@@ -210,7 +210,7 @@ func (g *geminiClient) send(ctx context.Context, messages []message.Message, too
210 return nil, retryErr
210 return nil, retryErr
211 }
211 }
212 if retry {
212 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))
214 select {
214 select {
215 case <-ctx.Done():
215 case <-ctx.Done():
216 return nil, ctx.Err()
216 return nil, ctx.Err()
@@ -266,7 +266,7 @@ func (g *geminiClient) stream(ctx context.Context, messages []message.Message, t
266 cfg := config.Get()
266 cfg := config.Get()
267 if cfg.Options.Debug {
267 if cfg.Options.Debug {
268 jsonData, _ := json.Marshal(geminiMessages)
268 jsonData, _ := json.Marshal(geminiMessages)
269− logging.Debug("Prepared messages", "messages", string(jsonData))
269+ slog.Debug("Prepared messages", "messages", string(jsonData))
270 }
270 }
271
271
272 modelConfig := cfg.Models[config.SelectedModelTypeLarge]
272 modelConfig := cfg.Models[config.SelectedModelTypeLarge]
@@ -323,7 +323,7 @@ func (g *geminiClient) stream(ctx context.Context, messages []message.Message, t
323 return
323 return
324 }
324 }
325 if retry {
325 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))
327 select {
327 select {
328 case <-ctx.Done():
328 case <-ctx.Done():
329 if ctx.Err() != nil {
329 if ctx.Err() != nil {
internal/llm/provider/openai.go+6−6
@@ -6,12 +6,12 @@ import (
6 "errors"
6 "errors"
7 "fmt"
7 "fmt"
8 "io"
8 "io"
9+ "log/slog"
9 "time"
10 "time"
10
11
11 "github.com/charmbracelet/crush/internal/config"
12 "github.com/charmbracelet/crush/internal/config"
12 "github.com/charmbracelet/crush/internal/fur/provider"
13 "github.com/charmbracelet/crush/internal/fur/provider"
13 "github.com/charmbracelet/crush/internal/llm/tools"
14 "github.com/charmbracelet/crush/internal/llm/tools"
14− "github.com/charmbracelet/crush/internal/logging"
15 "github.com/charmbracelet/crush/internal/message"
15 "github.com/charmbracelet/crush/internal/message"
16 "github.com/openai/openai-go"
16 "github.com/openai/openai-go"
17 "github.com/openai/openai-go/option"
17 "github.com/openai/openai-go/option"
@@ -194,7 +194,7 @@ func (o *openaiClient) send(ctx context.Context, messages []message.Message, too
194 cfg := config.Get()
194 cfg := config.Get()
195 if cfg.Options.Debug {
195 if cfg.Options.Debug {
196 jsonData, _ := json.Marshal(params)
196 jsonData, _ := json.Marshal(params)
197− logging.Debug("Prepared messages", "messages", string(jsonData))
197+ slog.Debug("Prepared messages", "messages", string(jsonData))
198 }
198 }
199 attempts := 0
199 attempts := 0
200 for {
200 for {
@@ -210,7 +210,7 @@ func (o *openaiClient) send(ctx context.Context, messages []message.Message, too
210 return nil, retryErr
210 return nil, retryErr
211 }
211 }
212 if retry {
212 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))
214 select {
214 select {
215 case <-ctx.Done():
215 case <-ctx.Done():
216 return nil, ctx.Err()
216 return nil, ctx.Err()
@@ -251,7 +251,7 @@ func (o *openaiClient) stream(ctx context.Context, messages []message.Message, t
251 cfg := config.Get()
251 cfg := config.Get()
252 if cfg.Options.Debug {
252 if cfg.Options.Debug {
253 jsonData, _ := json.Marshal(params)
253 jsonData, _ := json.Marshal(params)
254− logging.Debug("Prepared messages", "messages", string(jsonData))
254+ slog.Debug("Prepared messages", "messages", string(jsonData))
255 }
255 }
256
256
257 attempts := 0
257 attempts := 0
@@ -288,7 +288,7 @@ func (o *openaiClient) stream(ctx context.Context, messages []message.Message, t
288 if err == nil || errors.Is(err, io.EOF) {
288 if err == nil || errors.Is(err, io.EOF) {
289 if cfg.Options.Debug {
289 if cfg.Options.Debug {
290 jsonData, _ := json.Marshal(acc.ChatCompletion)
290 jsonData, _ := json.Marshal(acc.ChatCompletion)
291− logging.Debug("Response", "messages", string(jsonData))
291+ slog.Debug("Response", "messages", string(jsonData))
292 }
292 }
293 resultFinishReason := acc.ChatCompletion.Choices[0].FinishReason
293 resultFinishReason := acc.ChatCompletion.Choices[0].FinishReason
294 if resultFinishReason == "" {
294 if resultFinishReason == "" {
@@ -326,7 +326,7 @@ func (o *openaiClient) stream(ctx context.Context, messages []message.Message, t
326 return
326 return
327 }
327 }
328 if retry {
328 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))
330 select {
330 select {
331 case <-ctx.Done():
331 case <-ctx.Done():
332 // context cancelled
332 // context cancelled
internal/llm/provider/vertexai.go+2−2
@@ -3,7 +3,7 @@ package provider
3import (
3import (
4 "context"
4 "context"
5
5
6− "github.com/charmbracelet/crush/internal/logging"
6+ "log/slog"
7 "google.golang.org/genai"
7 "google.golang.org/genai"
8)
8)
9
9
@@ -18,7 +18,7 @@ func newVertexAIClient(opts providerClientOptions) VertexAIClient {
18 Backend: genai.BackendVertexAI,
18 Backend: genai.BackendVertexAI,
19 })
19 })
20 if err != nil {
20 if err != nil {
21− logging.Error("Failed to create VertexAI client", "error", err)
21+ slog.Error("Failed to create VertexAI client", "error", err)
22 return nil
22 return nil
23 }
23 }
24
24
internal/llm/tools/edit.go+6−6
@@ -12,7 +12,7 @@ import (
12 "github.com/charmbracelet/crush/internal/config"
12 "github.com/charmbracelet/crush/internal/config"
13 "github.com/charmbracelet/crush/internal/diff"
13 "github.com/charmbracelet/crush/internal/diff"
14 "github.com/charmbracelet/crush/internal/history"
14 "github.com/charmbracelet/crush/internal/history"
15− "github.com/charmbracelet/crush/internal/logging"
15+ "log/slog"
16 "github.com/charmbracelet/crush/internal/lsp"
16 "github.com/charmbracelet/crush/internal/lsp"
17 "github.com/charmbracelet/crush/internal/permission"
17 "github.com/charmbracelet/crush/internal/permission"
18)
18)
@@ -246,7 +246,7 @@ func (e *editTool) createNewFile(ctx context.Context, filePath, content string)
246 _, err = e.files.CreateVersion(ctx, sessionID, filePath, content)
246 _, err = e.files.CreateVersion(ctx, sessionID, filePath, content)
247 if err != nil {
247 if err != nil {
248 // Log error but don't fail the operation
248 // 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)
250 }
250 }
251
251
252 recordFileWrite(filePath)
252 recordFileWrite(filePath)
@@ -361,13 +361,13 @@ func (e *editTool) deleteContent(ctx context.Context, filePath, oldString string
361 // User Manually changed the content store an intermediate version
361 // User Manually changed the content store an intermediate version
362 _, err = e.files.CreateVersion(ctx, sessionID, filePath, oldContent)
362 _, err = e.files.CreateVersion(ctx, sessionID, filePath, oldContent)
363 if err != nil {
363 if err != nil {
364− logging.Debug("Error creating file history version", "error", err)
364+ slog.Debug("Error creating file history version", "error", err)
365 }
365 }
366 }
366 }
367 // Store the new version
367 // Store the new version
368 _, err = e.files.CreateVersion(ctx, sessionID, filePath, "")
368 _, err = e.files.CreateVersion(ctx, sessionID, filePath, "")
369 if err != nil {
369 if err != nil {
370− logging.Debug("Error creating file history version", "error", err)
370+ slog.Debug("Error creating file history version", "error", err)
371 }
371 }
372
372
373 recordFileWrite(filePath)
373 recordFileWrite(filePath)
@@ -483,13 +483,13 @@ func (e *editTool) replaceContent(ctx context.Context, filePath, oldString, newS
483 // User Manually changed the content store an intermediate version
483 // User Manually changed the content store an intermediate version
484 _, err = e.files.CreateVersion(ctx, sessionID, filePath, oldContent)
484 _, err = e.files.CreateVersion(ctx, sessionID, filePath, oldContent)
485 if err != nil {
485 if err != nil {
486− logging.Debug("Error creating file history version", "error", err)
486+ slog.Debug("Error creating file history version", "error", err)
487 }
487 }
488 }
488 }
489 // Store the new version
489 // Store the new version
490 _, err = e.files.CreateVersion(ctx, sessionID, filePath, newContent)
490 _, err = e.files.CreateVersion(ctx, sessionID, filePath, newContent)
491 if err != nil {
491 if err != nil {
492− logging.Debug("Error creating file history version", "error", err)
492+ slog.Debug("Error creating file history version", "error", err)
493 }
493 }
494
494
495 recordFileWrite(filePath)
495 recordFileWrite(filePath)
internal/llm/tools/glob.go+2−2
@@ -12,7 +12,7 @@ import (
12
12
13 "github.com/charmbracelet/crush/internal/config"
13 "github.com/charmbracelet/crush/internal/config"
14 "github.com/charmbracelet/crush/internal/fsext"
14 "github.com/charmbracelet/crush/internal/fsext"
15− "github.com/charmbracelet/crush/internal/logging"
15+ "log/slog"
16)
16)
17
17
18const (
18const (
@@ -143,7 +143,7 @@ func globFiles(pattern, searchPath string, limit int) ([]string, bool, error) {
143 if err == nil {
143 if err == nil {
144 return matches, len(matches) >= limit && limit > 0, nil
144 return matches, len(matches) >= limit && limit > 0, nil
145 }
145 }
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))
147 }
147 }
148
148
149 return fsext.GlobWithDoubleStar(pattern, searchPath, limit)
149 return fsext.GlobWithDoubleStar(pattern, searchPath, limit)
internal/llm/tools/write.go+3−3
@@ -12,7 +12,7 @@ import (
12 "github.com/charmbracelet/crush/internal/config"
12 "github.com/charmbracelet/crush/internal/config"
13 "github.com/charmbracelet/crush/internal/diff"
13 "github.com/charmbracelet/crush/internal/diff"
14 "github.com/charmbracelet/crush/internal/history"
14 "github.com/charmbracelet/crush/internal/history"
15− "github.com/charmbracelet/crush/internal/logging"
15+ "log/slog"
16 "github.com/charmbracelet/crush/internal/lsp"
16 "github.com/charmbracelet/crush/internal/lsp"
17 "github.com/charmbracelet/crush/internal/permission"
17 "github.com/charmbracelet/crush/internal/permission"
18)
18)
@@ -211,13 +211,13 @@ func (w *writeTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error
211 // User Manually changed the content store an intermediate version
211 // User Manually changed the content store an intermediate version
212 _, err = w.files.CreateVersion(ctx, sessionID, filePath, oldContent)
212 _, err = w.files.CreateVersion(ctx, sessionID, filePath, oldContent)
213 if err != nil {
213 if err != nil {
214− logging.Debug("Error creating file history version", "error", err)
214+ slog.Debug("Error creating file history version", "error", err)
215 }
215 }
216 }
216 }
217 // Store the new version
217 // Store the new version
218 _, err = w.files.CreateVersion(ctx, sessionID, filePath, params.Content)
218 _, err = w.files.CreateVersion(ctx, sessionID, filePath, params.Content)
219 if err != nil {
219 if err != nil {
220− logging.Debug("Error creating file history version", "error", err)
220+ slog.Debug("Error creating file history version", "error", err)
221 }
221 }
222
222
223 recordFileWrite(filePath)
223 recordFileWrite(filePath)
internal/log/log.go+27
@@ -1,8 +1,12 @@
1package log
1package log
2
2
3import (
3import (
4+ "fmt"
4 "log/slog"
5 "log/slog"
6+ "os"
7+ "runtime/debug"
5 "sync"
8 "sync"
9+ "time"
6
10
7 "gopkg.in/natefinch/lumberjack.v2"
11 "gopkg.in/natefinch/lumberjack.v2"
8)
12)
@@ -32,3 +36,26 @@ func Init(logFile string, debug bool) {
32 slog.SetDefault(slog.New(logger))
36 slog.SetDefault(slog.New(logger))
33 })
37 })
34}
38}
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 (
6 "encoding/json"
6 "encoding/json"
7 "fmt"
7 "fmt"
8 "io"
8 "io"
9+ "log/slog"
9 "os"
10 "os"
10 "os/exec"
11 "os/exec"
11 "path/filepath"
12 "path/filepath"
@@ -15,7 +16,7 @@ import (
15 "time"
16 "time"
16
17
17 "github.com/charmbracelet/crush/internal/config"
18 "github.com/charmbracelet/crush/internal/config"
18− "github.com/charmbracelet/crush/internal/logging"
19+ "github.com/charmbracelet/crush/internal/log"
19 "github.com/charmbracelet/crush/internal/lsp/protocol"
20 "github.com/charmbracelet/crush/internal/lsp/protocol"
20)
21)
21
22
@@ -96,17 +97,17 @@ func NewClient(ctx context.Context, command string, args ...string) (*Client, er
96 go func() {
97 go func() {
97 scanner := bufio.NewScanner(stderr)
98 scanner := bufio.NewScanner(stderr)
98 for scanner.Scan() {
99 for scanner.Scan() {
99− logging.Error("LSP Server", "err", scanner.Text())
100+ slog.Error("LSP Server", "err", scanner.Text())
100 }
101 }
101 if err := scanner.Err(); err != nil {
102 if err := scanner.Err(); err != nil {
102− logging.Error("Error reading", "err", err)
103+ slog.Error("Error reading", "err", err)
103 }
104 }
104 }()
105 }()
105
106
106 // Start message handling loop
107 // Start message handling loop
107 go func() {
108 go func() {
108− defer logging.RecoverPanic("LSP-message-handler", func() {
109+ defer log.RecoverPanic("LSP-message-handler", func() {
109− logging.ErrorPersist("LSP message handler crashed, LSP functionality may be impaired")
110+ slog.Error("LSP message handler crashed, LSP functionality may be impaired")
110 })
111 })
111 client.handleMessages()
112 client.handleMessages()
112 }()
113 }()
@@ -300,7 +301,7 @@ func (c *Client) WaitForServerReady(ctx context.Context) error {
300 defer ticker.Stop()
301 defer ticker.Stop()
301
302
302 if cfg.Options.DebugLSP {
303 if cfg.Options.DebugLSP {
303− logging.Debug("Waiting for LSP server to be ready...")
304+ slog.Debug("Waiting for LSP server to be ready...")
304 }
305 }
305
306
306 // Determine server type for specialized initialization
307 // Determine server type for specialized initialization
@@ -309,7 +310,7 @@ func (c *Client) WaitForServerReady(ctx context.Context) error {
309 // For TypeScript-like servers, we need to open some key files first
310 // For TypeScript-like servers, we need to open some key files first
310 if serverType == ServerTypeTypeScript {
311 if serverType == ServerTypeTypeScript {
311 if cfg.Options.DebugLSP {
312 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")
313 }
314 }
314 c.openKeyConfigFiles(ctx)
315 c.openKeyConfigFiles(ctx)
315 }
316 }
@@ -326,15 +327,15 @@ func (c *Client) WaitForServerReady(ctx context.Context) error {
326 // Server responded successfully
327 // Server responded successfully
327 c.SetServerState(StateReady)
328 c.SetServerState(StateReady)
328 if cfg.Options.DebugLSP {
329 if cfg.Options.DebugLSP {
329− logging.Debug("LSP server is ready")
330+ slog.Debug("LSP server is ready")
330 }
331 }
331 return nil
332 return nil
332 } else {
333 } 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)
334 }
335 }
335
336
336 if cfg.Options.DebugLSP {
337 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)
338 }
339 }
339 }
340 }
340 }
341 }
@@ -409,9 +410,9 @@ func (c *Client) openKeyConfigFiles(ctx context.Context) {
409 if _, err := os.Stat(file); err == nil {
410 if _, err := os.Stat(file); err == nil {
410 // File exists, try to open it
411 // File exists, try to open it
411 if err := c.OpenFile(ctx, file); err != nil {
412 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)
413 } else {
414 } else {
414− logging.Debug("Opened key config file for initialization", "file", file)
415+ slog.Debug("Opened key config file for initialization", "file", file)
415 }
416 }
416 }
417 }
417 }
418 }
@@ -487,7 +488,7 @@ func (c *Client) pingTypeScriptServer(ctx context.Context) error {
487 return nil
488 return nil
488 })
489 })
489 if err != nil {
490 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)
491 }
492 }
492
493
493 // Final fallback - just try a generic capability
494 // Final fallback - just try a generic capability
@@ -527,7 +528,7 @@ func (c *Client) openTypeScriptFiles(ctx context.Context, workDir string) {
527 if err := c.OpenFile(ctx, path); err == nil {
528 if err := c.OpenFile(ctx, path); err == nil {
528 filesOpened++
529 filesOpened++
529 if cfg.Options.DebugLSP {
530 if cfg.Options.DebugLSP {
530− logging.Debug("Opened TypeScript file for initialization", "file", path)
531+ slog.Debug("Opened TypeScript file for initialization", "file", path)
531 }
532 }
532 }
533 }
533 }
534 }
@@ -536,11 +537,11 @@ func (c *Client) openTypeScriptFiles(ctx context.Context, workDir string) {
536 })
537 })
537
538
538 if err != nil && cfg.Options.DebugLSP {
539 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)
540 }
541 }
541
542
542 if cfg.Options.DebugLSP {
543 if cfg.Options.DebugLSP {
543− logging.Debug("Opened TypeScript files for initialization", "count", filesOpened)
544+ slog.Debug("Opened TypeScript files for initialization", "count", filesOpened)
544 }
545 }
545}
546}
546
547
@@ -681,7 +682,7 @@ func (c *Client) CloseFile(ctx context.Context, filepath string) error {
681 }
682 }
682
683
683 if cfg.Options.DebugLSP {
684 if cfg.Options.DebugLSP {
684− logging.Debug("Closing file", "file", filepath)
685+ slog.Debug("Closing file", "file", filepath)
685 }
686 }
686 if err := c.Notify(ctx, "textDocument/didClose", params); err != nil {
687 if err := c.Notify(ctx, "textDocument/didClose", params); err != nil {
687 return err
688 return err
@@ -720,12 +721,12 @@ func (c *Client) CloseAllFiles(ctx context.Context) {
720 for _, filePath := range filesToClose {
721 for _, filePath := range filesToClose {
721 err := c.CloseFile(ctx, filePath)
722 err := c.CloseFile(ctx, filePath)
722 if err != nil && cfg.Options.DebugLSP {
723 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)
724 }
725 }
725 }
726 }
726
727
727 if cfg.Options.DebugLSP {
728 if cfg.Options.DebugLSP {
728− logging.Debug("Closed all files", "files", filesToClose)
729+ slog.Debug("Closed all files", "files", filesToClose)
729 }
730 }
730}
731}
731
732
internal/lsp/handlers.go+7−7
@@ -4,7 +4,7 @@ import (
4 "encoding/json"
4 "encoding/json"
5
5
6 "github.com/charmbracelet/crush/internal/config"
6 "github.com/charmbracelet/crush/internal/config"
7− "github.com/charmbracelet/crush/internal/logging"
7+ "log/slog"
8 "github.com/charmbracelet/crush/internal/lsp/protocol"
8 "github.com/charmbracelet/crush/internal/lsp/protocol"
9 "github.com/charmbracelet/crush/internal/lsp/util"
9 "github.com/charmbracelet/crush/internal/lsp/util"
10)
10)
@@ -18,7 +18,7 @@ func HandleWorkspaceConfiguration(params json.RawMessage) (any, error) {
18func HandleRegisterCapability(params json.RawMessage) (any, error) {
18func HandleRegisterCapability(params json.RawMessage) (any, error) {
19 var registerParams protocol.RegistrationParams
19 var registerParams protocol.RegistrationParams
20 if err := json.Unmarshal(params, &registerParams); err != nil {
20 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)
22 return nil, err
22 return nil, err
23 }
23 }
24
24
@@ -28,13 +28,13 @@ func HandleRegisterCapability(params json.RawMessage) (any, error) {
28 // Parse the registration options
28 // Parse the registration options
29 optionsJSON, err := json.Marshal(reg.RegisterOptions)
29 optionsJSON, err := json.Marshal(reg.RegisterOptions)
30 if err != nil {
30 if err != nil {
31− logging.Error("Error marshaling registration options", "error", err)
31+ slog.Error("Error marshaling registration options", "error", err)
32 continue
32 continue
33 }
33 }
34
34
35 var options protocol.DidChangeWatchedFilesRegistrationOptions
35 var options protocol.DidChangeWatchedFilesRegistrationOptions
36 if err := json.Unmarshal(optionsJSON, &options); err != nil {
36 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)
38 continue
38 continue
39 }
39 }
40
40
@@ -54,7 +54,7 @@ func HandleApplyEdit(params json.RawMessage) (any, error) {
54
54
55 err := util.ApplyWorkspaceEdit(edit.Edit)
55 err := util.ApplyWorkspaceEdit(edit.Edit)
56 if err != nil {
56 if err != nil {
57− logging.Error("Error applying workspace edit", "error", err)
57+ slog.Error("Error applying workspace edit", "error", err)
58 return protocol.ApplyWorkspaceEditResult{Applied: false, FailureReason: err.Error()}, nil
58 return protocol.ApplyWorkspaceEditResult{Applied: false, FailureReason: err.Error()}, nil
59 }
59 }
60
60
@@ -89,7 +89,7 @@ func HandleServerMessage(params json.RawMessage) {
89 }
89 }
90 if err := json.Unmarshal(params, &msg); err == nil {
90 if err := json.Unmarshal(params, &msg); err == nil {
91 if cfg.Options.DebugLSP {
91 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)
93 }
93 }
94 }
94 }
95}
95}
@@ -97,7 +97,7 @@ func HandleServerMessage(params json.RawMessage) {
97func HandleDiagnostics(client *Client, params json.RawMessage) {
97func HandleDiagnostics(client *Client, params json.RawMessage) {
98 var diagParams protocol.PublishDiagnosticsParams
98 var diagParams protocol.PublishDiagnosticsParams
99 if err := json.Unmarshal(params, &diagParams); err != nil {
99 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)
101 return
101 return
102 }
102 }
103
103
internal/lsp/protocol/tsprotocol.go+1−1
@@ -55,7 +55,7 @@ type ApplyWorkspaceEditResult struct {
55 // Indicates whether the edit was applied or not.
55 // Indicates whether the edit was applied or not.
56 Applied bool `json:"applied"`
56 Applied bool `json:"applied"`
57 // An optional textual description for why the edit was not applied.
57 // 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
59 // a suitable error for a request that triggered the edit.
59 // a suitable error for a request that triggered the edit.
60 FailureReason string `json:"failureReason,omitempty"`
60 FailureReason string `json:"failureReason,omitempty"`
61 // Depending on the client's failure handling strategy `failedChange` might
61 // Depending on the client's failure handling strategy `failedChange` might
internal/lsp/transport.go+16−16
@@ -9,7 +9,7 @@ import (
9 "strings"
9 "strings"
10
10
11 "github.com/charmbracelet/crush/internal/config"
11 "github.com/charmbracelet/crush/internal/config"
12− "github.com/charmbracelet/crush/internal/logging"
12+ "log/slog"
13)
13)
14
14
15// Write writes an LSP message to the given writer
15// Write writes an LSP message to the given writer
@@ -21,7 +21,7 @@ func WriteMessage(w io.Writer, msg *Message) error {
21 cfg := config.Get()
21 cfg := config.Get()
22
22
23 if cfg.Options.DebugLSP {
23 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)
25 }
25 }
26
26
27 _, err = fmt.Fprintf(w, "Content-Length: %d\r\n\r\n", len(data))
27 _, err = fmt.Fprintf(w, "Content-Length: %d\r\n\r\n", len(data))
@@ -50,7 +50,7 @@ func ReadMessage(r *bufio.Reader) (*Message, error) {
50 line = strings.TrimSpace(line)
50 line = strings.TrimSpace(line)
51
51
52 if cfg.Options.DebugLSP {
52 if cfg.Options.DebugLSP {
53− logging.Debug("Received header", "line", line)
53+ slog.Debug("Received header", "line", line)
54 }
54 }
55
55
56 if line == "" {
56 if line == "" {
@@ -66,7 +66,7 @@ func ReadMessage(r *bufio.Reader) (*Message, error) {
66 }
66 }
67
67
68 if cfg.Options.DebugLSP {
68 if cfg.Options.DebugLSP {
69− logging.Debug("Content-Length", "length", contentLength)
69+ slog.Debug("Content-Length", "length", contentLength)
70 }
70 }
71
71
72 // Read content
72 // Read content
@@ -77,7 +77,7 @@ func ReadMessage(r *bufio.Reader) (*Message, error) {
77 }
77 }
78
78
79 if cfg.Options.DebugLSP {
79 if cfg.Options.DebugLSP {
80− logging.Debug("Received content", "content", string(content))
80+ slog.Debug("Received content", "content", string(content))
81 }
81 }
82
82
83 // Parse message
83 // Parse message
@@ -96,7 +96,7 @@ func (c *Client) handleMessages() {
96 msg, err := ReadMessage(c.stdout)
96 msg, err := ReadMessage(c.stdout)
97 if err != nil {
97 if err != nil {
98 if cfg.Options.DebugLSP {
98 if cfg.Options.DebugLSP {
99− logging.Error("Error reading message", "error", err)
99+ slog.Error("Error reading message", "error", err)
100 }
100 }
101 return
101 return
102 }
102 }
@@ -104,7 +104,7 @@ func (c *Client) handleMessages() {
104 // Handle server->client request (has both Method and ID)
104 // Handle server->client request (has both Method and ID)
105 if msg.Method != "" && msg.ID != 0 {
105 if msg.Method != "" && msg.ID != 0 {
106 if cfg.Options.DebugLSP {
106 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)
108 }
108 }
109
109
110 response := &Message{
110 response := &Message{
@@ -144,7 +144,7 @@ func (c *Client) handleMessages() {
144
144
145 // Send response back to server
145 // Send response back to server
146 if err := WriteMessage(c.stdin, response); err != nil {
146 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)
148 }
148 }
149
149
150 continue
150 continue
@@ -158,11 +158,11 @@ func (c *Client) handleMessages() {
158
158
159 if ok {
159 if ok {
160 if cfg.Options.DebugLSP {
160 if cfg.Options.DebugLSP {
161− logging.Debug("Handling notification", "method", msg.Method)
161+ slog.Debug("Handling notification", "method", msg.Method)
162 }
162 }
163 go handler(msg.Params)
163 go handler(msg.Params)
164 } else if cfg.Options.DebugLSP {
164 } 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)
166 }
166 }
167 continue
167 continue
168 }
168 }
@@ -175,12 +175,12 @@ func (c *Client) handleMessages() {
175
175
176 if ok {
176 if ok {
177 if cfg.Options.DebugLSP {
177 if cfg.Options.DebugLSP {
178− logging.Debug("Received response for request", "id", msg.ID)
178+ slog.Debug("Received response for request", "id", msg.ID)
179 }
179 }
180 ch <- msg
180 ch <- msg
181 close(ch)
181 close(ch)
182 } else if cfg.Options.DebugLSP {
182 } 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)
184 }
184 }
185 }
185 }
186 }
186 }
@@ -192,7 +192,7 @@ func (c *Client) Call(ctx context.Context, method string, params any, result any
192 id := c.nextID.Add(1)
192 id := c.nextID.Add(1)
193
193
194 if cfg.Options.DebugLSP {
194 if cfg.Options.DebugLSP {
195− logging.Debug("Making call", "method", method, "id", id)
195+ slog.Debug("Making call", "method", method, "id", id)
196 }
196 }
197
197
198 msg, err := NewRequest(id, method, params)
198 msg, err := NewRequest(id, method, params)
@@ -218,14 +218,14 @@ func (c *Client) Call(ctx context.Context, method string, params any, result any
218 }
218 }
219
219
220 if cfg.Options.DebugLSP {
220 if cfg.Options.DebugLSP {
221− logging.Debug("Request sent", "method", method, "id", id)
221+ slog.Debug("Request sent", "method", method, "id", id)
222 }
222 }
223
223
224 // Wait for response
224 // Wait for response
225 resp := <-ch
225 resp := <-ch
226
226
227 if cfg.Options.DebugLSP {
227 if cfg.Options.DebugLSP {
228− logging.Debug("Received response", "id", id)
228+ slog.Debug("Received response", "id", id)
229 }
229 }
230
230
231 if resp.Error != nil {
231 if resp.Error != nil {
@@ -251,7 +251,7 @@ func (c *Client) Call(ctx context.Context, method string, params any, result any
251func (c *Client) Notify(ctx context.Context, method string, params any) error {
251func (c *Client) Notify(ctx context.Context, method string, params any) error {
252 cfg := config.Get()
252 cfg := config.Get()
253 if cfg.Options.DebugLSP {
253 if cfg.Options.DebugLSP {
254− logging.Debug("Sending notification", "method", method)
254+ slog.Debug("Sending notification", "method", method)
255 }
255 }
256
256
257 msg, err := NewNotification(method, params)
257 msg, err := NewNotification(method, params)
internal/lsp/watcher/watcher.go+41−41
@@ -11,7 +11,7 @@ import (
11
11
12 "github.com/bmatcuk/doublestar/v4"
12 "github.com/bmatcuk/doublestar/v4"
13 "github.com/charmbracelet/crush/internal/config"
13 "github.com/charmbracelet/crush/internal/config"
14− "github.com/charmbracelet/crush/internal/logging"
14+ "log/slog"
15 "github.com/charmbracelet/crush/internal/lsp"
15 "github.com/charmbracelet/crush/internal/lsp"
16 "github.com/charmbracelet/crush/internal/lsp/protocol"
16 "github.com/charmbracelet/crush/internal/lsp/protocol"
17 "github.com/fsnotify/fsnotify"
17 "github.com/fsnotify/fsnotify"
@@ -45,7 +45,7 @@ func NewWorkspaceWatcher(client *lsp.Client) *WorkspaceWatcher {
45func (w *WorkspaceWatcher) AddRegistrations(ctx context.Context, id string, watchers []protocol.FileSystemWatcher) {
45func (w *WorkspaceWatcher) AddRegistrations(ctx context.Context, id string, watchers []protocol.FileSystemWatcher) {
46 cfg := config.Get()
46 cfg := config.Get()
47
47
48− logging.Debug("Adding file watcher registrations")
48+ slog.Debug("Adding file watcher registrations")
49 w.registrationMu.Lock()
49 w.registrationMu.Lock()
50 defer w.registrationMu.Unlock()
50 defer w.registrationMu.Unlock()
51
51
@@ -54,33 +54,33 @@ func (w *WorkspaceWatcher) AddRegistrations(ctx context.Context, id string, watc
54
54
55 // Print detailed registration information for debugging
55 // Print detailed registration information for debugging
56 if cfg.Options.DebugLSP {
56 if cfg.Options.DebugLSP {
57− logging.Debug("Adding file watcher registrations",
57+ slog.Debug("Adding file watcher registrations",
58 "id", id,
58 "id", id,
59 "watchers", len(watchers),
59 "watchers", len(watchers),
60 "total", len(w.registrations),
60 "total", len(w.registrations),
61 )
61 )
62
62
63 for i, watcher := range watchers {
63 for i, watcher := range watchers {
64− logging.Debug("Registration", "index", i+1)
64+ slog.Debug("Registration", "index", i+1)
65
65
66 // Log the GlobPattern
66 // Log the GlobPattern
67 switch v := watcher.GlobPattern.Value.(type) {
67 switch v := watcher.GlobPattern.Value.(type) {
68 case string:
68 case string:
69− logging.Debug("GlobPattern", "pattern", v)
69+ slog.Debug("GlobPattern", "pattern", v)
70 case protocol.RelativePattern:
70 case protocol.RelativePattern:
71− logging.Debug("GlobPattern", "pattern", v.Pattern)
71+ slog.Debug("GlobPattern", "pattern", v.Pattern)
72
72
73 // Log BaseURI details
73 // Log BaseURI details
74 switch u := v.BaseURI.Value.(type) {
74 switch u := v.BaseURI.Value.(type) {
75 case string:
75 case string:
76− logging.Debug("BaseURI", "baseURI", u)
76+ slog.Debug("BaseURI", "baseURI", u)
77 case protocol.DocumentUri:
77 case protocol.DocumentUri:
78− logging.Debug("BaseURI", "baseURI", u)
78+ slog.Debug("BaseURI", "baseURI", u)
79 default:
79 default:
80− logging.Debug("BaseURI", "baseURI", u)
80+ slog.Debug("BaseURI", "baseURI", u)
81 }
81 }
82 default:
82 default:
83− logging.Debug("GlobPattern", "unknown type", fmt.Sprintf("%T", v))
83+ slog.Debug("GlobPattern", "unknown type", fmt.Sprintf("%T", v))
84 }
84 }
85
85
86 // Log WatchKind
86 // Log WatchKind
@@ -89,13 +89,13 @@ func (w *WorkspaceWatcher) AddRegistrations(ctx context.Context, id string, watc
89 watchKind = *watcher.Kind
89 watchKind = *watcher.Kind
90 }
90 }
91
91
92− logging.Debug("WatchKind", "kind", watchKind)
92+ slog.Debug("WatchKind", "kind", watchKind)
93 }
93 }
94 }
94 }
95
95
96 // Determine server type for specialized handling
96 // Determine server type for specialized handling
97 serverName := getServerNameFromContext(ctx)
97 serverName := getServerNameFromContext(ctx)
98− logging.Debug("Server type detected", "serverName", serverName)
98+ slog.Debug("Server type detected", "serverName", serverName)
99
99
100 // Check if this server has sent file watchers
100 // Check if this server has sent file watchers
101 hasFileWatchers := len(watchers) > 0
101 hasFileWatchers := len(watchers) > 0
@@ -123,7 +123,7 @@ func (w *WorkspaceWatcher) AddRegistrations(ctx context.Context, id string, watc
123 filesOpened += highPriorityFilesOpened
123 filesOpened += highPriorityFilesOpened
124
124
125 if cfg.Options.DebugLSP {
125 if cfg.Options.DebugLSP {
126− logging.Debug("Opened high-priority files",
126+ slog.Debug("Opened high-priority files",
127 "count", highPriorityFilesOpened,
127 "count", highPriorityFilesOpened,
128 "serverName", serverName)
128 "serverName", serverName)
129 }
129 }
@@ -131,7 +131,7 @@ func (w *WorkspaceWatcher) AddRegistrations(ctx context.Context, id string, watc
131 // If we've already opened enough high-priority files, we might not need more
131 // If we've already opened enough high-priority files, we might not need more
132 if filesOpened >= maxFilesToOpen {
132 if filesOpened >= maxFilesToOpen {
133 if cfg.Options.DebugLSP {
133 if cfg.Options.DebugLSP {
134− logging.Debug("Reached file limit with high-priority files",
134+ slog.Debug("Reached file limit with high-priority files",
135 "filesOpened", filesOpened,
135 "filesOpened", filesOpened,
136 "maxFiles", maxFilesToOpen)
136 "maxFiles", maxFilesToOpen)
137 }
137 }
@@ -149,7 +149,7 @@ func (w *WorkspaceWatcher) AddRegistrations(ctx context.Context, id string, watc
149 if d.IsDir() {
149 if d.IsDir() {
150 if path != w.workspacePath && shouldExcludeDir(path) {
150 if path != w.workspacePath && shouldExcludeDir(path) {
151 if cfg.Options.DebugLSP {
151 if cfg.Options.DebugLSP {
152− logging.Debug("Skipping excluded directory", "path", path)
152+ slog.Debug("Skipping excluded directory", "path", path)
153 }
153 }
154 return filepath.SkipDir
154 return filepath.SkipDir
155 }
155 }
@@ -177,7 +177,7 @@ func (w *WorkspaceWatcher) AddRegistrations(ctx context.Context, id string, watc
177
177
178 elapsedTime := time.Since(startTime)
178 elapsedTime := time.Since(startTime)
179 if cfg.Options.DebugLSP {
179 if cfg.Options.DebugLSP {
180− logging.Debug("Limited workspace scan complete",
180+ slog.Debug("Limited workspace scan complete",
181 "filesOpened", filesOpened,
181 "filesOpened", filesOpened,
182 "maxFiles", maxFilesToOpen,
182 "maxFiles", maxFilesToOpen,
183 "elapsedTime", elapsedTime.Seconds(),
183 "elapsedTime", elapsedTime.Seconds(),
@@ -186,11 +186,11 @@ func (w *WorkspaceWatcher) AddRegistrations(ctx context.Context, id string, watc
186 }
186 }
187
187
188 if err != nil && cfg.Options.DebugLSP {
188 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)
190 }
190 }
191 }()
191 }()
192 } else if cfg.Options.DebugLSP {
192 } 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)
194 }
194 }
195}
195}
196
196
@@ -266,7 +266,7 @@ func (w *WorkspaceWatcher) openHighPriorityFiles(ctx context.Context, serverName
266 matches, err := doublestar.Glob(os.DirFS(w.workspacePath), pattern)
266 matches, err := doublestar.Glob(os.DirFS(w.workspacePath), pattern)
267 if err != nil {
267 if err != nil {
268 if cfg.Options.DebugLSP {
268 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)
270 }
270 }
271 continue
271 continue
272 }
272 }
@@ -300,12 +300,12 @@ func (w *WorkspaceWatcher) openHighPriorityFiles(ctx context.Context, serverName
300 fullPath := filesToOpen[j]
300 fullPath := filesToOpen[j]
301 if err := w.client.OpenFile(ctx, fullPath); err != nil {
301 if err := w.client.OpenFile(ctx, fullPath); err != nil {
302 if cfg.Options.DebugLSP {
302 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)
304 }
304 }
305 } else {
305 } else {
306 filesOpened++
306 filesOpened++
307 if cfg.Options.DebugLSP {
307 if cfg.Options.DebugLSP {
308− logging.Debug("Opened high-priority file", "path", fullPath)
308+ slog.Debug("Opened high-priority file", "path", fullPath)
309 }
309 }
310 }
310 }
311 }
311 }
@@ -334,7 +334,7 @@ func (w *WorkspaceWatcher) WatchWorkspace(ctx context.Context, workspacePath str
334 }
334 }
335
335
336 serverName := getServerNameFromContext(ctx)
336 serverName := getServerNameFromContext(ctx)
337− logging.Debug("Starting workspace watcher", "workspacePath", workspacePath, "serverName", serverName)
337+ slog.Debug("Starting workspace watcher", "workspacePath", workspacePath, "serverName", serverName)
338
338
339 // Register handler for file watcher registrations from the server
339 // Register handler for file watcher registrations from the server
340 lsp.RegisterFileWatchHandler(func(id string, watchers []protocol.FileSystemWatcher) {
340 lsp.RegisterFileWatchHandler(func(id string, watchers []protocol.FileSystemWatcher) {
@@ -343,7 +343,7 @@ func (w *WorkspaceWatcher) WatchWorkspace(ctx context.Context, workspacePath str
343
343
344 watcher, err := fsnotify.NewWatcher()
344 watcher, err := fsnotify.NewWatcher()
345 if err != nil {
345 if err != nil {
346− logging.Error("Error creating watcher", "error", err)
346+ slog.Error("Error creating watcher", "error", err)
347 }
347 }
348 defer watcher.Close()
348 defer watcher.Close()
349
349
@@ -357,7 +357,7 @@ func (w *WorkspaceWatcher) WatchWorkspace(ctx context.Context, workspacePath str
357 if d.IsDir() && path != workspacePath {
357 if d.IsDir() && path != workspacePath {
358 if shouldExcludeDir(path) {
358 if shouldExcludeDir(path) {
359 if cfg.Options.DebugLSP {
359 if cfg.Options.DebugLSP {
360− logging.Debug("Skipping excluded directory", "path", path)
360+ slog.Debug("Skipping excluded directory", "path", path)
361 }
361 }
362 return filepath.SkipDir
362 return filepath.SkipDir
363 }
363 }
@@ -367,14 +367,14 @@ func (w *WorkspaceWatcher) WatchWorkspace(ctx context.Context, workspacePath str
367 if d.IsDir() {
367 if d.IsDir() {
368 err = watcher.Add(path)
368 err = watcher.Add(path)
369 if err != nil {
369 if err != nil {
370− logging.Error("Error watching path", "path", path, "error", err)
370+ slog.Error("Error watching path", "path", path, "error", err)
371 }
371 }
372 }
372 }
373
373
374 return nil
374 return nil
375 })
375 })
376 if err != nil {
376 if err != nil {
377− logging.Error("Error walking workspace", "error", err)
377+ slog.Error("Error walking workspace", "error", err)
378 }
378 }
379
379
380 // Event loop
380 // Event loop
@@ -396,7 +396,7 @@ func (w *WorkspaceWatcher) WatchWorkspace(ctx context.Context, workspacePath str
396 // Skip excluded directories
396 // Skip excluded directories
397 if !shouldExcludeDir(event.Name) {
397 if !shouldExcludeDir(event.Name) {
398 if err := watcher.Add(event.Name); err != nil {
398 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)
400 }
400 }
401 }
401 }
402 } else {
402 } else {
@@ -411,7 +411,7 @@ func (w *WorkspaceWatcher) WatchWorkspace(ctx context.Context, workspacePath str
411 // Debug logging
411 // Debug logging
412 if cfg.Options.DebugLSP {
412 if cfg.Options.DebugLSP {
413 matched, kind := w.isPathWatched(event.Name)
413 matched, kind := w.isPathWatched(event.Name)
414− logging.Debug("File event",
414+ slog.Debug("File event",
415 "path", event.Name,
415 "path", event.Name,
416 "operation", event.Op.String(),
416 "operation", event.Op.String(),
417 "watched", matched,
417 "watched", matched,
@@ -431,7 +431,7 @@ func (w *WorkspaceWatcher) WatchWorkspace(ctx context.Context, workspacePath str
431 // Just send the notification if needed
431 // Just send the notification if needed
432 info, err := os.Stat(event.Name)
432 info, err := os.Stat(event.Name)
433 if err != nil {
433 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)
435 return
435 return
436 }
436 }
437 if !info.IsDir() && watchKind&protocol.WatchCreate != 0 {
437 if !info.IsDir() && watchKind&protocol.WatchCreate != 0 {
@@ -459,7 +459,7 @@ func (w *WorkspaceWatcher) WatchWorkspace(ctx context.Context, workspacePath str
459 if !ok {
459 if !ok {
460 return
460 return
461 }
461 }
462− logging.Error("Error watching file", "error", err)
462+ slog.Error("Error watching file", "error", err)
463 }
463 }
464 }
464 }
465}
465}
@@ -584,7 +584,7 @@ func matchesSimpleGlob(pattern, path string) bool {
584 // Fall back to simple matching for simpler patterns
584 // Fall back to simple matching for simpler patterns
585 matched, err := filepath.Match(pattern, path)
585 matched, err := filepath.Match(pattern, path)
586 if err != nil {
586 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)
588 return false
588 return false
589 }
589 }
590
590
@@ -595,7 +595,7 @@ func matchesSimpleGlob(pattern, path string) bool {
595func (w *WorkspaceWatcher) matchesPattern(path string, pattern protocol.GlobPattern) bool {
595func (w *WorkspaceWatcher) matchesPattern(path string, pattern protocol.GlobPattern) bool {
596 patternInfo, err := pattern.AsPattern()
596 patternInfo, err := pattern.AsPattern()
597 if err != nil {
597 if err != nil {
598− logging.Error("Error parsing pattern", "pattern", pattern, "error", err)
598+ slog.Error("Error parsing pattern", "pattern", pattern, "error", err)
599 return false
599 return false
600 }
600 }
601
601
@@ -620,7 +620,7 @@ func (w *WorkspaceWatcher) matchesPattern(path string, pattern protocol.GlobPatt
620 // Make path relative to basePath for matching
620 // Make path relative to basePath for matching
621 relPath, err := filepath.Rel(basePath, path)
621 relPath, err := filepath.Rel(basePath, path)
622 if err != nil {
622 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)
624 return false
624 return false
625 }
625 }
626 relPath = filepath.ToSlash(relPath)
626 relPath = filepath.ToSlash(relPath)
@@ -663,14 +663,14 @@ func (w *WorkspaceWatcher) handleFileEvent(ctx context.Context, uri string, chan
663 } else if changeType == protocol.FileChangeType(protocol.Changed) && w.client.IsFileOpen(filePath) {
663 } else if changeType == protocol.FileChangeType(protocol.Changed) && w.client.IsFileOpen(filePath) {
664 err := w.client.NotifyChange(ctx, filePath)
664 err := w.client.NotifyChange(ctx, filePath)
665 if err != nil {
665 if err != nil {
666− logging.Error("Error notifying change", "error", err)
666+ slog.Error("Error notifying change", "error", err)
667 }
667 }
668 return
668 return
669 }
669 }
670
670
671 // Notify LSP server about the file event using didChangeWatchedFiles
671 // Notify LSP server about the file event using didChangeWatchedFiles
672 if err := w.notifyFileEvent(ctx, uri, changeType); err != nil {
672 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)
674 }
674 }
675}
675}
676
676
@@ -678,7 +678,7 @@ func (w *WorkspaceWatcher) handleFileEvent(ctx context.Context, uri string, chan
678func (w *WorkspaceWatcher) notifyFileEvent(ctx context.Context, uri string, changeType protocol.FileChangeType) error {
678func (w *WorkspaceWatcher) notifyFileEvent(ctx context.Context, uri string, changeType protocol.FileChangeType) error {
679 cfg := config.Get()
679 cfg := config.Get()
680 if cfg.Options.DebugLSP {
680 if cfg.Options.DebugLSP {
681− logging.Debug("Notifying file event",
681+ slog.Debug("Notifying file event",
682 "uri", uri,
682 "uri", uri,
683 "changeType", changeType,
683 "changeType", changeType,
684 )
684 )
@@ -853,7 +853,7 @@ func shouldExcludeFile(filePath string) bool {
853 // Skip large files
853 // Skip large files
854 if info.Size() > maxFileSize {
854 if info.Size() > maxFileSize {
855 if cfg.Options.DebugLSP {
855 if cfg.Options.DebugLSP {
856− logging.Debug("Skipping large file",
856+ slog.Debug("Skipping large file",
857 "path", filePath,
857 "path", filePath,
858 "size", info.Size(),
858 "size", info.Size(),
859 "maxSize", maxFileSize,
859 "maxSize", maxFileSize,
@@ -891,10 +891,10 @@ func (w *WorkspaceWatcher) openMatchingFile(ctx context.Context, path string) {
891 // This helps with project initialization for certain language servers
891 // This helps with project initialization for certain language servers
892 if isHighPriorityFile(path, serverName) {
892 if isHighPriorityFile(path, serverName) {
893 if cfg.Options.DebugLSP {
893 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)
895 }
895 }
896 if err := w.client.OpenFile(ctx, path); err != nil && cfg.Options.DebugLSP {
896 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)
898 }
898 }
899 return
899 return
900 }
900 }
@@ -906,7 +906,7 @@ func (w *WorkspaceWatcher) openMatchingFile(ctx context.Context, path string) {
906 // Check file size - for preloading we're more conservative
906 // Check file size - for preloading we're more conservative
907 if info.Size() > (1 * 1024 * 1024) { // 1MB limit for preloaded files
907 if info.Size() > (1 * 1024 * 1024) { // 1MB limit for preloaded files
908 if cfg.Options.DebugLSP {
908 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())
910 }
910 }
911 return
911 return
912 }
912 }
@@ -938,7 +938,7 @@ func (w *WorkspaceWatcher) openMatchingFile(ctx context.Context, path string) {
938 if shouldOpen {
938 if shouldOpen {
939 // Don't need to check if it's already open - the client.OpenFile handles that
939 // Don't need to check if it's already open - the client.OpenFile handles that
940 if err := w.client.OpenFile(ctx, path); err != nil && cfg.Options.DebugLSP {
940 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)
942 }
942 }
943 }
943 }
944 }
944 }
internal/shell/persistent.go+3−4
@@ -1,9 +1,8 @@
1package shell
1package shell
2
2
3import (
3import (
4+ "log/slog"
4 "sync"
5 "sync"
5−
6− "github.com/charmbracelet/crush/internal/logging"
7)
6)
8
7
9// PersistentShell is a singleton shell instance that maintains state across the application
8// PersistentShell is a singleton shell instance that maintains state across the application
@@ -30,9 +29,9 @@ func GetPersistentShell(cwd string) *PersistentShell {
30 return shellInstance
29 return shellInstance
31}
30}
32
31
33−// loggingAdapter adapts the internal logging package to the Logger interface
32+// slog.dapter adapts the internal slog.package to the Logger interface
34type loggingAdapter struct{}
33type loggingAdapter struct{}
35
34
36func (l *loggingAdapter) InfoPersist(msg string, keysAndValues ...interface{}) {
35func (l *loggingAdapter) InfoPersist(msg string, keysAndValues ...interface{}) {
37− logging.InfoPersist(msg, keysAndValues...)
36+ slog.Info(msg, keysAndValues...)
38}
37}
internal/tui/components/chat/editor/editor.go+1−3
@@ -14,7 +14,6 @@ import (
14 tea "github.com/charmbracelet/bubbletea/v2"
14 tea "github.com/charmbracelet/bubbletea/v2"
15 "github.com/charmbracelet/crush/internal/app"
15 "github.com/charmbracelet/crush/internal/app"
16 "github.com/charmbracelet/crush/internal/fsext"
16 "github.com/charmbracelet/crush/internal/fsext"
17− "github.com/charmbracelet/crush/internal/logging"
18 "github.com/charmbracelet/crush/internal/message"
17 "github.com/charmbracelet/crush/internal/message"
19 "github.com/charmbracelet/crush/internal/session"
18 "github.com/charmbracelet/crush/internal/session"
20 "github.com/charmbracelet/crush/internal/tui/components/chat"
19 "github.com/charmbracelet/crush/internal/tui/components/chat"
@@ -153,8 +152,7 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
153 return m, nil
152 return m, nil
154 case filepicker.FilePickedMsg:
153 case filepicker.FilePickedMsg:
155 if len(m.attachments) >= maxAttachments {
154 if len(m.attachments) >= maxAttachments {
156− logging.ErrorPersist(fmt.Sprintf("cannot add more than %d images", maxAttachments))
155+ return m, util.ReportError(fmt.Errorf("cannot add more than %d images", maxAttachments))
157− return m, cmd
158 }
156 }
159 m.attachments = append(m.attachments, msg.Attachment)
157 m.attachments = append(m.attachments, msg.Attachment)
160 return m, nil
158 return m, nil
internal/tui/components/chat/sidebar/sidebar.go+2−2
@@ -13,7 +13,7 @@ import (
13 "github.com/charmbracelet/crush/internal/diff"
13 "github.com/charmbracelet/crush/internal/diff"
14 "github.com/charmbracelet/crush/internal/fsext"
14 "github.com/charmbracelet/crush/internal/fsext"
15 "github.com/charmbracelet/crush/internal/history"
15 "github.com/charmbracelet/crush/internal/history"
16− "github.com/charmbracelet/crush/internal/logging"
16+ "log/slog"
17 "github.com/charmbracelet/crush/internal/lsp"
17 "github.com/charmbracelet/crush/internal/lsp"
18 "github.com/charmbracelet/crush/internal/lsp/protocol"
18 "github.com/charmbracelet/crush/internal/lsp/protocol"
19 "github.com/charmbracelet/crush/internal/pubsub"
19 "github.com/charmbracelet/crush/internal/pubsub"
@@ -94,7 +94,7 @@ func (m *sidebarCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
94 case chat.SessionClearedMsg:
94 case chat.SessionClearedMsg:
95 m.session = session.Session{}
95 m.session = session.Session{}
96 case pubsub.Event[history.File]:
96 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)
98 return m, m.handleFileHistoryEvent(msg)
98 return m, m.handleFileHistoryEvent(msg)
99 case pubsub.Event[session.Session]:
99 case pubsub.Event[session.Session]:
100 if msg.Type == pubsub.UpdatedEvent {
100 if msg.Type == pubsub.UpdatedEvent {
internal/tui/components/core/layout/split.go+2−2
@@ -3,7 +3,7 @@ package layout
3import (
3import (
4 "github.com/charmbracelet/bubbles/v2/key"
4 "github.com/charmbracelet/bubbles/v2/key"
5 tea "github.com/charmbracelet/bubbletea/v2"
5 tea "github.com/charmbracelet/bubbletea/v2"
6− "github.com/charmbracelet/crush/internal/logging"
6+ "log/slog"
7 "github.com/charmbracelet/crush/internal/tui/styles"
7 "github.com/charmbracelet/crush/internal/tui/styles"
8 "github.com/charmbracelet/crush/internal/tui/util"
8 "github.com/charmbracelet/crush/internal/tui/util"
9 "github.com/charmbracelet/lipgloss/v2"
9 "github.com/charmbracelet/lipgloss/v2"
@@ -154,7 +154,7 @@ func (s *splitPaneLayout) View() tea.View {
154func (s *splitPaneLayout) SetSize(width, height int) tea.Cmd {
154func (s *splitPaneLayout) SetSize(width, height int) tea.Cmd {
155 s.width = width
155 s.width = width
156 s.height = height
156 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)
158
158
159 var topHeight, bottomHeight int
159 var topHeight, bottomHeight int
160 var cmds []tea.Cmd
160 var cmds []tea.Cmd
internal/tui/components/core/status/status.go−33
@@ -6,8 +6,6 @@ import (
6
6
7 "github.com/charmbracelet/bubbles/v2/help"
7 "github.com/charmbracelet/bubbles/v2/help"
8 tea "github.com/charmbracelet/bubbletea/v2"
8 tea "github.com/charmbracelet/bubbletea/v2"
9− "github.com/charmbracelet/crush/internal/logging"
10− "github.com/charmbracelet/crush/internal/pubsub"
11 "github.com/charmbracelet/crush/internal/session"
9 "github.com/charmbracelet/crush/internal/session"
12 "github.com/charmbracelet/crush/internal/tui/styles"
10 "github.com/charmbracelet/crush/internal/tui/styles"
13 "github.com/charmbracelet/crush/internal/tui/util"
11 "github.com/charmbracelet/crush/internal/tui/util"
@@ -59,37 +57,6 @@ func (m *statusCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
59 case util.ClearStatusMsg:
57 case util.ClearStatusMsg:
60 m.info = util.InfoMsg{}
58 m.info = util.InfoMsg{}
61
59
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− }
93 }
60 }
94 return m, nil
61 return m, nil
95}
62}
internal/tui/components/dialogs/filepicker/filepicker.go+3−7
@@ -11,7 +11,6 @@ import (
11 "github.com/charmbracelet/bubbles/v2/help"
11 "github.com/charmbracelet/bubbles/v2/help"
12 "github.com/charmbracelet/bubbles/v2/key"
12 "github.com/charmbracelet/bubbles/v2/key"
13 tea "github.com/charmbracelet/bubbletea/v2"
13 tea "github.com/charmbracelet/bubbletea/v2"
14− "github.com/charmbracelet/crush/internal/logging"
15 "github.com/charmbracelet/crush/internal/message"
14 "github.com/charmbracelet/crush/internal/message"
16 "github.com/charmbracelet/crush/internal/tui/components/core"
15 "github.com/charmbracelet/crush/internal/tui/components/core"
17 "github.com/charmbracelet/crush/internal/tui/components/dialogs"
16 "github.com/charmbracelet/crush/internal/tui/components/dialogs"
@@ -119,18 +118,15 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
119 func() tea.Msg {
118 func() tea.Msg {
120 isFileLarge, err := ValidateFileSize(path, maxAttachmentSize)
119 isFileLarge, err := ValidateFileSize(path, maxAttachmentSize)
121 if err != nil {
120 if err != nil {
122− logging.ErrorPersist("unable to read the image")
121+ return util.ReportError(fmt.Errorf("unable to read the image: %w", err))
123− return nil
124 }
122 }
125 if isFileLarge {
123 if isFileLarge {
126− logging.ErrorPersist("file too large, max 5MB")
124+ return util.ReportError(fmt.Errorf("file too large, max 5MB"))
127− return nil
128 }
125 }
129
126
130 content, err := os.ReadFile(path)
127 content, err := os.ReadFile(path)
131 if err != nil {
128 if err != nil {
132− logging.ErrorPersist("Unable read selected file")
129+ return util.ReportError(fmt.Errorf("unable to read the image: %w", err))
133− return nil
134 }
130 }
135
131
136 mimeBufferSize := min(512, len(content))
132 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 (
5)
5)
6
6
7type KeyMap struct {
7type KeyMap struct {
8− Logs key.Binding
9 Quit key.Binding
8 Quit key.Binding
10 Help key.Binding
9 Help key.Binding
11 Commands key.Binding
10 Commands key.Binding
@@ -16,10 +15,6 @@ type KeyMap struct {
16
15
17func DefaultKeyMap() KeyMap {
16func DefaultKeyMap() KeyMap {
18 return KeyMap{
17 return KeyMap{
19− Logs: key.NewBinding(
20− key.WithKeys("ctrl+l"),
21− key.WithHelp("ctrl+l", "logs"),
22− ),
23 Quit: key.NewBinding(
18 Quit: key.NewBinding(
24 key.WithKeys("ctrl+c"),
19 key.WithKeys("ctrl+c"),
25 key.WithHelp("ctrl+c", "quit"),
20 key.WithHelp("ctrl+c", "quit"),
@@ -47,7 +42,6 @@ func (k KeyMap) FullHelp() [][]key.Binding {
47 k.Sessions,
42 k.Sessions,
48 k.Quit,
43 k.Quit,
49 k.Help,
44 k.Help,
50− k.Logs,
51 }
45 }
52 slice = k.prependEscAndTab(slice)
46 slice = k.prependEscAndTab(slice)
53 slice = append(slice, k.pageBindings...)
47 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 (
9 "github.com/charmbracelet/crush/internal/app"
9 "github.com/charmbracelet/crush/internal/app"
10 "github.com/charmbracelet/crush/internal/config"
10 "github.com/charmbracelet/crush/internal/config"
11 "github.com/charmbracelet/crush/internal/llm/agent"
11 "github.com/charmbracelet/crush/internal/llm/agent"
12− "github.com/charmbracelet/crush/internal/logging"
13 "github.com/charmbracelet/crush/internal/permission"
12 "github.com/charmbracelet/crush/internal/permission"
14 "github.com/charmbracelet/crush/internal/pubsub"
13 "github.com/charmbracelet/crush/internal/pubsub"
15 cmpChat "github.com/charmbracelet/crush/internal/tui/components/chat"
14 cmpChat "github.com/charmbracelet/crush/internal/tui/components/chat"
@@ -27,7 +26,6 @@ import (
27 "github.com/charmbracelet/crush/internal/tui/components/dialogs/sessions"
26 "github.com/charmbracelet/crush/internal/tui/components/dialogs/sessions"
28 "github.com/charmbracelet/crush/internal/tui/page"
27 "github.com/charmbracelet/crush/internal/tui/page"
29 "github.com/charmbracelet/crush/internal/tui/page/chat"
28 "github.com/charmbracelet/crush/internal/tui/page/chat"
30− "github.com/charmbracelet/crush/internal/tui/page/logs"
31 "github.com/charmbracelet/crush/internal/tui/styles"
29 "github.com/charmbracelet/crush/internal/tui/styles"
32 "github.com/charmbracelet/crush/internal/tui/util"
30 "github.com/charmbracelet/crush/internal/tui/util"
33 "github.com/charmbracelet/lipgloss/v2"
31 "github.com/charmbracelet/lipgloss/v2"
@@ -135,20 +133,6 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
135 a.selectedSessionID = msg.ID
133 a.selectedSessionID = msg.ID
136 case cmpChat.SessionClearedMsg:
134 case cmpChat.SessionClearedMsg:
137 a.selectedSessionID = ""
135 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...)
152 // Commands
136 // Commands
153 case commands.SwitchSessionsMsg:
137 case commands.SwitchSessionsMsg:
154 return a, func() tea.Msg {
138 return a, func() tea.Msg {
@@ -176,7 +160,6 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
176
160
177 // Update the agent with the new model/provider configuration
161 // Update the agent with the new model/provider configuration
178 if err := a.app.UpdateAgentModel(); err != nil {
162 if err := a.app.UpdateAgentModel(); err != nil {
179− logging.ErrorPersist(fmt.Sprintf("Failed to update agent model: %v", err))
180 return a, util.ReportError(fmt.Errorf("model changed to %s but failed to update agent: %v", msg.Model.Model, err))
163 return a, util.ReportError(fmt.Errorf("model changed to %s but failed to update agent: %v", msg.Model.Model, err))
181 }
164 }
182
165
@@ -348,10 +331,6 @@ func (a *appModel) handleKeyPressMsg(msg tea.KeyPressMsg) tea.Cmd {
348 },
331 },
349 )
332 )
350 return tea.Sequence(cmds...)
333 return tea.Sequence(cmds...)
351− // Page navigation
352− case key.Matches(msg, a.keyMap.Logs):
353− return a.moveToPage(logs.LogsPage)
354−
355 default:
334 default:
356 if a.dialog.HasDialogs() {
335 if a.dialog.HasDialogs() {
357 u, dialogCmd := a.dialog.Update(msg)
336 u, dialogCmd := a.dialog.Update(msg)
@@ -453,7 +432,6 @@ func New(app *app.App) tea.Model {
453
432
454 pages: map[page.PageID]util.Model{
433 pages: map[page.PageID]util.Model{
455 chat.ChatPageID: chatPage,
434 chat.ChatPageID: chatPage,
456− logs.LogsPage: logs.NewLogsPage(),
457 },
435 },
458
436
459 dialog: dialogs.NewDialogCmp(),
437 dialog: dialogs.NewDialogCmp(),
main.go+6−5
@@ -1,6 +1,7 @@
1package main
1package main
2
2
3import (
3import (
4+ "log/slog"
4 "net/http"
5 "net/http"
5 "os"
6 "os"
6
7
@@ -9,19 +10,19 @@ import (
9 _ "github.com/joho/godotenv/autoload" // automatically load .env files
10 _ "github.com/joho/godotenv/autoload" // automatically load .env files
10
11
11 "github.com/charmbracelet/crush/cmd"
12 "github.com/charmbracelet/crush/cmd"
12− "github.com/charmbracelet/crush/internal/logging"
13+ "github.com/charmbracelet/crush/internal/log"
13)
14)
14
15
15func main() {
16func main() {
16− defer logging.RecoverPanic("main", func() {
17+ defer log.RecoverPanic("main", func() {
17− logging.ErrorPersist("Application terminated due to unhandled panic")
18+ slog.Error("Application terminated due to unhandled panic")
18 })
19 })
19
20
20 if os.Getenv("CRUSH_PROFILE") != "" {
21 if os.Getenv("CRUSH_PROFILE") != "" {
21 go func() {
22 go func() {
22− logging.Info("Serving pprof at localhost:6060")
23+ slog.Info("Serving pprof at localhost:6060")
23 if httpErr := http.ListenAndServe("localhost:6060", nil); httpErr != nil {
24 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)
25 }
26 }
26 }()
27 }()
27 }
28 }