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