فهرست منبع

wip: logging improvements

adamdottv 9 ماه پیش
والد
کامیت
f100777199
41فایلهای تغییر یافته به همراه848 افزوده شده و 438 حذف شده
  1. 28 19
      cmd/root.go
  2. 12 4
      internal/app/app.go
  3. 18 16
      internal/app/lsp.go
  4. 20 47
      internal/config/config.go
  5. 5 5
      internal/db/connect.go
  6. 30 0
      internal/db/db.go
  7. 128 0
      internal/db/logs.sql.go
  8. 16 0
      internal/db/migrations/20250508122310_create_logs_table.sql
  9. 10 0
      internal/db/models.go
  10. 4 0
      internal/db/querier.go
  11. 28 0
      internal/db/sql/logs.sql
  12. 5 3
      internal/llm/agent/agent.go
  13. 5 5
      internal/llm/agent/mcp-tools.go
  14. 1 1
      internal/llm/agent/tools.go
  15. 2 2
      internal/llm/prompt/prompt.go
  16. 5 1
      internal/llm/prompt/prompt_test.go
  17. 6 6
      internal/llm/provider/anthropic.go
  18. 4 4
      internal/llm/provider/gemini.go
  19. 4 4
      internal/llm/provider/openai.go
  20. 10 10
      internal/llm/provider/provider.go
  21. 6 6
      internal/llm/tools/edit.go
  22. 4 4
      internal/llm/tools/patch.go
  23. 3 3
      internal/llm/tools/write.go
  24. 4 19
      internal/logging/logging.go
  25. 48 0
      internal/logging/manager.go
  26. 0 19
      internal/logging/message.go
  27. 167 0
      internal/logging/service.go
  28. 11 57
      internal/logging/writer.go
  29. 18 16
      internal/lsp/client.go
  30. 6 7
      internal/lsp/discovery/integration.go
  31. 5 4
      internal/lsp/discovery/language.go
  32. 27 26
      internal/lsp/discovery/server.go
  33. 7 7
      internal/lsp/handlers.go
  34. 16 16
      internal/lsp/transport.go
  35. 56 56
      internal/lsp/watcher/watcher.go
  36. 6 9
      internal/session/manager.go
  37. 5 5
      internal/tui/components/dialog/filepicker.go
  38. 27 15
      internal/tui/components/logs/details.go
  39. 83 34
      internal/tui/components/logs/table.go
  40. 7 7
      internal/tui/theme/manager.go
  41. 1 1
      internal/tui/tui.go

+ 28 - 19
cmd/root.go

@@ -7,6 +7,8 @@ import (
 	"sync"
 	"sync"
 	"time"
 	"time"
 
 
+	"log/slog"
+
 	tea "github.com/charmbracelet/bubbletea"
 	tea "github.com/charmbracelet/bubbletea"
 	zone "github.com/lrstanley/bubblezone"
 	zone "github.com/lrstanley/bubblezone"
 	"github.com/opencode-ai/opencode/internal/app"
 	"github.com/opencode-ai/opencode/internal/app"
@@ -38,6 +40,13 @@ to assist developers in writing, debugging, and understanding code directly from
 			return nil
 			return nil
 		}
 		}
 
 
+		// Setup logging
+		lvl := new(slog.LevelVar)
+		logger := slog.New(slog.NewTextHandler(logging.NewWriter(), &slog.HandlerOptions{
+			Level: lvl,
+		}))
+		slog.SetDefault(logger)
+
 		// Load the config
 		// Load the config
 		debug, _ := cmd.Flags().GetBool("debug")
 		debug, _ := cmd.Flags().GetBool("debug")
 		cwd, _ := cmd.Flags().GetString("cwd")
 		cwd, _ := cmd.Flags().GetString("cwd")
@@ -54,14 +63,14 @@ to assist developers in writing, debugging, and understanding code directly from
 			}
 			}
 			cwd = c
 			cwd = c
 		}
 		}
-		_, err := config.Load(cwd, debug)
+		_, err := config.Load(cwd, debug, lvl)
 		if err != nil {
 		if err != nil {
 			return err
 			return err
 		}
 		}
 
 
 		// Run LSP auto-discovery
 		// Run LSP auto-discovery
 		if err := discovery.IntegrateLSPServers(cwd); err != nil {
 		if err := discovery.IntegrateLSPServers(cwd); err != nil {
-			logging.Warn("Failed to auto-discover LSP servers", "error", err)
+			slog.Warn("Failed to auto-discover LSP servers", "error", err)
 			// Continue anyway, this is not a fatal error
 			// Continue anyway, this is not a fatal error
 		}
 		}
 
 
@@ -77,7 +86,7 @@ to assist developers in writing, debugging, and understanding code directly from
 
 
 		app, err := app.New(ctx, conn)
 		app, err := app.New(ctx, conn)
 		if err != nil {
 		if err != nil {
-			logging.Error("Failed to create app: %v", err)
+			slog.Error("Failed to create app: %v", err)
 			return err
 			return err
 		}
 		}
 
 
@@ -109,11 +118,11 @@ to assist developers in writing, debugging, and understanding code directly from
 			for {
 			for {
 				select {
 				select {
 				case <-tuiCtx.Done():
 				case <-tuiCtx.Done():
-					logging.Info("TUI message handler shutting down")
+					slog.Info("TUI message handler shutting down")
 					return
 					return
 				case msg, ok := <-ch:
 				case msg, ok := <-ch:
 					if !ok {
 					if !ok {
-						logging.Info("TUI message channel closed")
+						slog.Info("TUI message channel closed")
 						return
 						return
 					}
 					}
 					program.Send(msg)
 					program.Send(msg)
@@ -135,7 +144,7 @@ to assist developers in writing, debugging, and understanding code directly from
 			// Wait for TUI message handler to finish
 			// Wait for TUI message handler to finish
 			tuiWg.Wait()
 			tuiWg.Wait()
 
 
-			logging.Info("All goroutines cleaned up")
+			slog.Info("All goroutines cleaned up")
 		}
 		}
 
 
 		// Run the TUI
 		// Run the TUI
@@ -143,18 +152,18 @@ to assist developers in writing, debugging, and understanding code directly from
 		cleanup()
 		cleanup()
 
 
 		if err != nil {
 		if err != nil {
-			logging.Error("TUI error: %v", err)
+			slog.Error("TUI error: %v", err)
 			return fmt.Errorf("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
 		return nil
 	},
 	},
 }
 }
 
 
 // attemptTUIRecovery tries to recover the TUI after a panic
 // attemptTUIRecovery tries to recover the TUI after a panic
 func attemptTUIRecovery(program *tea.Program) {
 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
 	// We could try to restart the TUI or gracefully exit
 	// For now, we'll just quit the program to avoid further issues
 	// For now, we'll just quit the program to avoid further issues
@@ -171,7 +180,7 @@ func initMCPTools(ctx context.Context, app *app.App) {
 
 
 		// Set this up once with proper error handling
 		// Set this up once with proper error handling
 		agent.GetMcpTools(ctxWithTimeout, app.Permissions)
 		agent.GetMcpTools(ctxWithTimeout, app.Permissions)
-		logging.Info("MCP message handling goroutine exiting")
+		slog.Info("MCP message handling goroutine exiting")
 	}()
 	}()
 }
 }
 
 
@@ -189,7 +198,7 @@ func setupSubscriber[T any](
 
 
 		subCh := subscriber(ctx)
 		subCh := subscriber(ctx)
 		if subCh == nil {
 		if subCh == nil {
-			logging.Warn("subscription channel is nil", "name", name)
+			slog.Warn("subscription channel is nil", "name", name)
 			return
 			return
 		}
 		}
 
 
@@ -197,7 +206,7 @@ func setupSubscriber[T any](
 			select {
 			select {
 			case event, ok := <-subCh:
 			case event, ok := <-subCh:
 				if !ok {
 				if !ok {
-					logging.Info("subscription channel closed", "name", name)
+					slog.Info("subscription channel closed", "name", name)
 					return
 					return
 				}
 				}
 
 
@@ -206,13 +215,13 @@ func setupSubscriber[T any](
 				select {
 				select {
 				case outputCh <- msg:
 				case outputCh <- msg:
 				case <-time.After(2 * time.Second):
 				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():
 				case <-ctx.Done():
-					logging.Info("subscription cancelled", "name", name)
+					slog.Info("subscription cancelled", "name", name)
 					return
 					return
 				}
 				}
 			case <-ctx.Done():
 			case <-ctx.Done():
-				logging.Info("subscription cancelled", "name", name)
+				slog.Info("subscription cancelled", "name", name)
 				return
 				return
 			}
 			}
 		}
 		}
@@ -225,14 +234,14 @@ func setupSubscriptions(app *app.App, parentCtx context.Context) (chan tea.Msg,
 	wg := sync.WaitGroup{}
 	wg := sync.WaitGroup{}
 	ctx, cancel := context.WithCancel(parentCtx) // Inherit from parent context
 	ctx, cancel := context.WithCancel(parentCtx) // Inherit from parent context
 
 
-	setupSubscriber(ctx, &wg, "logging", logging.Subscribe, ch)
+	setupSubscriber(ctx, &wg, "logging", app.Logs.Subscribe, ch)
 	setupSubscriber(ctx, &wg, "sessions", app.Sessions.Subscribe, ch)
 	setupSubscriber(ctx, &wg, "sessions", app.Sessions.Subscribe, ch)
 	setupSubscriber(ctx, &wg, "messages", app.Messages.Subscribe, ch)
 	setupSubscriber(ctx, &wg, "messages", app.Messages.Subscribe, ch)
 	setupSubscriber(ctx, &wg, "permissions", app.Permissions.Subscribe, ch)
 	setupSubscriber(ctx, &wg, "permissions", app.Permissions.Subscribe, ch)
 	setupSubscriber(ctx, &wg, "status", app.Status.Subscribe, ch)
 	setupSubscriber(ctx, &wg, "status", app.Status.Subscribe, ch)
 
 
 	cleanupFunc := func() {
 	cleanupFunc := func() {
-		logging.Info("Cancelling all subscriptions")
+		slog.Info("Cancelling all subscriptions")
 		cancel() // Signal all goroutines to stop
 		cancel() // Signal all goroutines to stop
 
 
 		waitCh := make(chan struct{})
 		waitCh := make(chan struct{})
@@ -244,10 +253,10 @@ func setupSubscriptions(app *app.App, parentCtx context.Context) (chan tea.Msg,
 
 
 		select {
 		select {
 		case <-waitCh:
 		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
 			close(ch) // Only close after all writers are confirmed done
 		case <-time.After(5 * time.Second):
 		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)
 			close(ch)
 		}
 		}
 	}
 	}

+ 12 - 4
internal/app/app.go

@@ -7,6 +7,8 @@ import (
 	"sync"
 	"sync"
 	"time"
 	"time"
 
 
+	"log/slog"
+
 	"github.com/opencode-ai/opencode/internal/config"
 	"github.com/opencode-ai/opencode/internal/config"
 	"github.com/opencode-ai/opencode/internal/db"
 	"github.com/opencode-ai/opencode/internal/db"
 	"github.com/opencode-ai/opencode/internal/history"
 	"github.com/opencode-ai/opencode/internal/history"
@@ -21,6 +23,7 @@ import (
 )
 )
 
 
 type App struct {
 type App struct {
+	Logs        logging.Service
 	Sessions    session.Service
 	Sessions    session.Service
 	Messages    message.Service
 	Messages    message.Service
 	History     history.Service
 	History     history.Service
@@ -40,12 +43,16 @@ type App struct {
 
 
 func New(ctx context.Context, conn *sql.DB) (*App, error) {
 func New(ctx context.Context, conn *sql.DB) (*App, error) {
 	q := db.New(conn)
 	q := db.New(conn)
+	loggingService := logging.NewService(q)
 	sessionService := session.NewService(q)
 	sessionService := session.NewService(q)
 	messageService := message.NewService(q)
 	messageService := message.NewService(q)
 	historyService := history.NewService(q, conn)
 	historyService := history.NewService(q, conn)
 	permissionService := permission.NewPermissionService()
 	permissionService := permission.NewPermissionService()
 	statusService := status.NewService()
 	statusService := status.NewService()
 
 
+	// Initialize logging service
+	logging.InitManager(loggingService)
+
 	// Initialize session manager
 	// Initialize session manager
 	session.InitManager(sessionService)
 	session.InitManager(sessionService)
 
 
@@ -53,6 +60,7 @@ func New(ctx context.Context, conn *sql.DB) (*App, error) {
 	status.InitManager(statusService)
 	status.InitManager(statusService)
 
 
 	app := &App{
 	app := &App{
+		Logs:        loggingService,
 		Sessions:    sessionService,
 		Sessions:    sessionService,
 		Messages:    messageService,
 		Messages:    messageService,
 		History:     historyService,
 		History:     historyService,
@@ -81,7 +89,7 @@ func New(ctx context.Context, conn *sql.DB) (*App, error) {
 		),
 		),
 	)
 	)
 	if err != nil {
 	if err != nil {
-		logging.Error("Failed to create coder agent", err)
+		slog.Error("Failed to create coder agent", err)
 		return nil, err
 		return nil, err
 	}
 	}
 
 
@@ -98,9 +106,9 @@ func (app *App) initTheme() {
 	// Try to set the theme from config
 	// Try to set the theme from config
 	err := theme.SetTheme(cfg.TUI.Theme)
 	err := theme.SetTheme(cfg.TUI.Theme)
 	if err != nil {
 	if err != nil {
-		logging.Warn("Failed to set theme from config, using default theme", "theme", cfg.TUI.Theme, "error", err)
+		slog.Warn("Failed to set theme from config, using default theme", "theme", cfg.TUI.Theme, "error", err)
 	} else {
 	} else {
-		logging.Debug("Set theme from config", "theme", cfg.TUI.Theme)
+		slog.Debug("Set theme from config", "theme", cfg.TUI.Theme)
 	}
 	}
 }
 }
 
 
@@ -123,7 +131,7 @@ func (app *App) Shutdown() {
 	for name, client := range clients {
 	for name, client := range clients {
 		shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 		shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 		if err := client.Shutdown(shutdownCtx); err != nil {
 		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()
 		cancel()
 	}
 	}

+ 18 - 16
internal/app/lsp.go

@@ -4,6 +4,8 @@ import (
 	"context"
 	"context"
 	"time"
 	"time"
 
 
+	"log/slog"
+
 	"github.com/opencode-ai/opencode/internal/config"
 	"github.com/opencode-ai/opencode/internal/config"
 	"github.com/opencode-ai/opencode/internal/logging"
 	"github.com/opencode-ai/opencode/internal/logging"
 	"github.com/opencode-ai/opencode/internal/lsp"
 	"github.com/opencode-ai/opencode/internal/lsp"
@@ -18,29 +20,29 @@ func (app *App) initLSPClients(ctx context.Context) {
 		// Start each client initialization in its own goroutine
 		// Start each client initialization in its own goroutine
 		go app.createAndStartLSPClient(ctx, name, clientConfig.Command, clientConfig.Args...)
 		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
 // 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) {
 func (app *App) createAndStartLSPClient(ctx context.Context, name string, command string, args ...string) {
 	// Create a specific context for initialization with a timeout
 	// 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
 	// Create the LSP client
 	lspClient, err := lsp.NewClient(ctx, command, args...)
 	lspClient, err := lsp.NewClient(ctx, command, args...)
 	if err != nil {
 	if err != nil {
-		logging.Error("Failed to create LSP client for", name, err)
+		slog.Error("Failed to create LSP client for", name, err)
 		return
 		return
 	}
 	}
 
 
 	// Create a longer timeout for initialization (some servers take time to start)
 	// Create a longer timeout for initialization (some servers take time to start)
 	initCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
 	initCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
 	defer cancel()
 	defer cancel()
-	
+
 	// Initialize with the initialization context
 	// Initialize with the initialization context
 	_, err = lspClient.InitializeLSPClient(initCtx, config.WorkingDirectory())
 	_, err = lspClient.InitializeLSPClient(initCtx, config.WorkingDirectory())
 	if err != nil {
 	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
 		// Clean up the client to prevent resource leaks
 		lspClient.Close()
 		lspClient.Close()
 		return
 		return
@@ -48,22 +50,22 @@ func (app *App) createAndStartLSPClient(ctx context.Context, name string, comman
 
 
 	// Wait for the server to be ready
 	// Wait for the server to be ready
 	if err := lspClient.WaitForServerReady(initCtx); err != nil {
 	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
 		// We'll continue anyway, as some functionality might still work
 		lspClient.SetServerState(lsp.StateError)
 		lspClient.SetServerState(lsp.StateError)
 	} else {
 	} else {
-		logging.Info("LSP server is ready", "name", name)
+		slog.Info("LSP server is ready", "name", name)
 		lspClient.SetServerState(lsp.StateReady)
 		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
 	// Create a child context that can be canceled when the app is shutting down
 	watchCtx, cancelFunc := context.WithCancel(ctx)
 	watchCtx, cancelFunc := context.WithCancel(ctx)
-	
+
 	// Create a context with the server name for better identification
 	// Create a context with the server name for better identification
 	watchCtx = context.WithValue(watchCtx, "serverName", name)
 	watchCtx = context.WithValue(watchCtx, "serverName", name)
-	
+
 	// Create the workspace watcher
 	// Create the workspace watcher
 	workspaceWatcher := watcher.NewWorkspaceWatcher(lspClient)
 	workspaceWatcher := watcher.NewWorkspaceWatcher(lspClient)
 
 
@@ -92,7 +94,7 @@ func (app *App) runWorkspaceWatcher(ctx context.Context, name string, workspaceW
 	})
 	})
 
 
 	workspaceWatcher.WatchWorkspace(ctx, config.WorkingDirectory())
 	workspaceWatcher.WatchWorkspace(ctx, config.WorkingDirectory())
-	logging.Info("Workspace watcher stopped", "client", name)
+	slog.Info("Workspace watcher stopped", "client", name)
 }
 }
 
 
 // restartLSPClient attempts to restart a crashed or failed LSP client
 // restartLSPClient attempts to restart a crashed or failed LSP client
@@ -101,7 +103,7 @@ func (app *App) restartLSPClient(ctx context.Context, name string) {
 	cfg := config.Get()
 	cfg := config.Get()
 	clientConfig, exists := cfg.LSP[name]
 	clientConfig, exists := cfg.LSP[name]
 	if !exists {
 	if !exists {
-		logging.Error("Cannot restart client, configuration not found", "client", name)
+		slog.Error("Cannot restart client, configuration not found", "client", name)
 		return
 		return
 	}
 	}
 
 
@@ -118,7 +120,7 @@ func (app *App) restartLSPClient(ctx context.Context, name string) {
 		shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 		shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 		_ = oldClient.Shutdown(shutdownCtx)
 		_ = oldClient.Shutdown(shutdownCtx)
 		cancel()
 		cancel()
-		
+
 		// Ensure we close the client to free resources
 		// Ensure we close the client to free resources
 		_ = oldClient.Close()
 		_ = oldClient.Close()
 	}
 	}
@@ -128,5 +130,5 @@ func (app *App) restartLSPClient(ctx context.Context, name string) {
 
 
 	// Create a new client using the shared function
 	// Create a new client using the shared function
 	app.createAndStartLSPClient(ctx, name, clientConfig.Command, clientConfig.Args...)
 	app.createAndStartLSPClient(ctx, name, clientConfig.Command, clientConfig.Args...)
-	logging.Info("Successfully restarted LSP client", "client", name)
+	slog.Info("Successfully restarted LSP client", "client", name)
 }
 }

+ 20 - 47
internal/config/config.go

@@ -11,7 +11,6 @@ import (
 	"strings"
 	"strings"
 
 
 	"github.com/opencode-ai/opencode/internal/llm/models"
 	"github.com/opencode-ai/opencode/internal/llm/models"
-	"github.com/opencode-ai/opencode/internal/logging"
 	"github.com/spf13/viper"
 	"github.com/spf13/viper"
 )
 )
 
 
@@ -70,7 +69,7 @@ type LSPConfig struct {
 
 
 // TUIConfig defines the configuration for the Terminal User Interface.
 // TUIConfig defines the configuration for the Terminal User Interface.
 type TUIConfig struct {
 type TUIConfig struct {
-	Theme       string                 `json:"theme,omitempty"`
+	Theme       string         `json:"theme,omitempty"`
 	CustomTheme map[string]any `json:"customTheme,omitempty"`
 	CustomTheme map[string]any `json:"customTheme,omitempty"`
 }
 }
 
 
@@ -119,7 +118,7 @@ var cfg *Config
 // Load initializes the configuration from environment variables and config files.
 // Load initializes the configuration from environment variables and config files.
 // If debug is true, debug mode is enabled and log level is set to debug.
 // If debug is true, debug mode is enabled and log level is set to debug.
 // It returns an error if configuration loading fails.
 // It returns an error if configuration loading fails.
-func Load(workingDir string, debug bool) (*Config, error) {
+func Load(workingDir string, debug bool, lvl *slog.LevelVar) (*Config, error) {
 	if cfg != nil {
 	if cfg != nil {
 		return cfg, nil
 		return cfg, nil
 	}
 	}
@@ -150,39 +149,13 @@ func Load(workingDir string, debug bool) (*Config, error) {
 	}
 	}
 
 
 	applyDefaultValues()
 	applyDefaultValues()
+
 	defaultLevel := slog.LevelInfo
 	defaultLevel := slog.LevelInfo
 	if cfg.Debug {
 	if cfg.Debug {
 		defaultLevel = slog.LevelDebug
 		defaultLevel = slog.LevelDebug
 	}
 	}
-	if os.Getenv("OPENCODE_DEV_DEBUG") == "true" {
-		loggingFile := fmt.Sprintf("%s/%s", cfg.Data.Directory, "debug.log")
-
-		// if file does not exist create it
-		if _, err := os.Stat(loggingFile); os.IsNotExist(err) {
-			if err := os.MkdirAll(cfg.Data.Directory, 0o755); err != nil {
-				return cfg, fmt.Errorf("failed to create directory: %w", err)
-			}
-			if _, err := os.Create(loggingFile); err != nil {
-				return cfg, fmt.Errorf("failed to create log file: %w", err)
-			}
-		}
-
-		sloggingFileWriter, err := os.OpenFile(loggingFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o666)
-		if err != nil {
-			return cfg, fmt.Errorf("failed to open log file: %w", err)
-		}
-		// Configure logger
-		logger := slog.New(slog.NewTextHandler(sloggingFileWriter, &slog.HandlerOptions{
-			Level: defaultLevel,
-		}))
-		slog.SetDefault(logger)
-	} else {
-		// Configure logger
-		logger := slog.New(slog.NewTextHandler(logging.NewWriter(), &slog.HandlerOptions{
-			Level: defaultLevel,
-		}))
-		slog.SetDefault(logger)
-	}
+	lvl.Set(defaultLevel)
+	slog.SetLogLoggerLevel(defaultLevel)
 
 
 	// Validate configuration
 	// Validate configuration
 	if err := Validate(); err != nil {
 	if err := Validate(); err != nil {
@@ -397,13 +370,13 @@ func validateAgent(cfg *Config, name AgentName, agent Agent) error {
 	// Check if model exists
 	// Check if model exists
 	model, modelExists := models.SupportedModels[agent.Model]
 	model, modelExists := models.SupportedModels[agent.Model]
 	if !modelExists {
 	if !modelExists {
-		logging.Warn("unsupported model configured, reverting to default",
+		slog.Warn("unsupported model configured, reverting to default",
 			"agent", name,
 			"agent", name,
 			"configured_model", agent.Model)
 			"configured_model", agent.Model)
 
 
 		// Set default model based on available providers
 		// Set default model based on available providers
 		if setDefaultModelForAgent(name) {
 		if setDefaultModelForAgent(name) {
-			logging.Info("set default model for agent", "agent", name, "model", cfg.Agents[name].Model)
+			slog.Info("set default model for agent", "agent", name, "model", cfg.Agents[name].Model)
 		} else {
 		} else {
 			return fmt.Errorf("no valid provider available for agent %s", name)
 			return fmt.Errorf("no valid provider available for agent %s", name)
 		}
 		}
@@ -418,14 +391,14 @@ func validateAgent(cfg *Config, name AgentName, agent Agent) error {
 		// Provider not configured, check if we have environment variables
 		// Provider not configured, check if we have environment variables
 		apiKey := getProviderAPIKey(provider)
 		apiKey := getProviderAPIKey(provider)
 		if apiKey == "" {
 		if apiKey == "" {
-			logging.Warn("provider not configured for model, reverting to default",
+			slog.Warn("provider not configured for model, reverting to default",
 				"agent", name,
 				"agent", name,
 				"model", agent.Model,
 				"model", agent.Model,
 				"provider", provider)
 				"provider", provider)
 
 
 			// Set default model based on available providers
 			// Set default model based on available providers
 			if setDefaultModelForAgent(name) {
 			if setDefaultModelForAgent(name) {
-				logging.Info("set default model for agent", "agent", name, "model", cfg.Agents[name].Model)
+				slog.Info("set default model for agent", "agent", name, "model", cfg.Agents[name].Model)
 			} else {
 			} else {
 				return fmt.Errorf("no valid provider available for agent %s", name)
 				return fmt.Errorf("no valid provider available for agent %s", name)
 			}
 			}
@@ -434,18 +407,18 @@ func validateAgent(cfg *Config, name AgentName, agent Agent) error {
 			cfg.Providers[provider] = Provider{
 			cfg.Providers[provider] = Provider{
 				APIKey: apiKey,
 				APIKey: apiKey,
 			}
 			}
-			logging.Info("added provider from environment", "provider", provider)
+			slog.Info("added provider from environment", "provider", provider)
 		}
 		}
 	} else if providerCfg.Disabled || providerCfg.APIKey == "" {
 	} else if providerCfg.Disabled || providerCfg.APIKey == "" {
 		// Provider is disabled or has no API key
 		// Provider is disabled or has no API key
-		logging.Warn("provider is disabled or has no API key, reverting to default",
+		slog.Warn("provider is disabled or has no API key, reverting to default",
 			"agent", name,
 			"agent", name,
 			"model", agent.Model,
 			"model", agent.Model,
 			"provider", provider)
 			"provider", provider)
 
 
 		// Set default model based on available providers
 		// Set default model based on available providers
 		if setDefaultModelForAgent(name) {
 		if setDefaultModelForAgent(name) {
-			logging.Info("set default model for agent", "agent", name, "model", cfg.Agents[name].Model)
+			slog.Info("set default model for agent", "agent", name, "model", cfg.Agents[name].Model)
 		} else {
 		} else {
 			return fmt.Errorf("no valid provider available for agent %s", name)
 			return fmt.Errorf("no valid provider available for agent %s", name)
 		}
 		}
@@ -453,7 +426,7 @@ func validateAgent(cfg *Config, name AgentName, agent Agent) error {
 
 
 	// Validate max tokens
 	// Validate max tokens
 	if agent.MaxTokens <= 0 {
 	if agent.MaxTokens <= 0 {
-		logging.Warn("invalid max tokens, setting to default",
+		slog.Warn("invalid max tokens, setting to default",
 			"agent", name,
 			"agent", name,
 			"model", agent.Model,
 			"model", agent.Model,
 			"max_tokens", agent.MaxTokens)
 			"max_tokens", agent.MaxTokens)
@@ -468,7 +441,7 @@ func validateAgent(cfg *Config, name AgentName, agent Agent) error {
 		cfg.Agents[name] = updatedAgent
 		cfg.Agents[name] = updatedAgent
 	} else if model.ContextWindow > 0 && agent.MaxTokens > model.ContextWindow/2 {
 	} else if model.ContextWindow > 0 && agent.MaxTokens > model.ContextWindow/2 {
 		// Ensure max tokens doesn't exceed half the context window (reasonable limit)
 		// Ensure max tokens doesn't exceed half the context window (reasonable limit)
-		logging.Warn("max tokens exceeds half the context window, adjusting",
+		slog.Warn("max tokens exceeds half the context window, adjusting",
 			"agent", name,
 			"agent", name,
 			"model", agent.Model,
 			"model", agent.Model,
 			"max_tokens", agent.MaxTokens,
 			"max_tokens", agent.MaxTokens,
@@ -484,7 +457,7 @@ func validateAgent(cfg *Config, name AgentName, agent Agent) error {
 	if model.CanReason && provider == models.ProviderOpenAI {
 	if model.CanReason && provider == models.ProviderOpenAI {
 		if agent.ReasoningEffort == "" {
 		if agent.ReasoningEffort == "" {
 			// Set default reasoning effort for models that support it
 			// Set default reasoning effort for models that support it
-			logging.Info("setting default reasoning effort for model that supports reasoning",
+			slog.Info("setting default reasoning effort for model that supports reasoning",
 				"agent", name,
 				"agent", name,
 				"model", agent.Model)
 				"model", agent.Model)
 
 
@@ -496,7 +469,7 @@ func validateAgent(cfg *Config, name AgentName, agent Agent) error {
 			// Check if reasoning effort is valid (low, medium, high)
 			// Check if reasoning effort is valid (low, medium, high)
 			effort := strings.ToLower(agent.ReasoningEffort)
 			effort := strings.ToLower(agent.ReasoningEffort)
 			if effort != "low" && effort != "medium" && effort != "high" {
 			if effort != "low" && effort != "medium" && effort != "high" {
-				logging.Warn("invalid reasoning effort, setting to medium",
+				slog.Warn("invalid reasoning effort, setting to medium",
 					"agent", name,
 					"agent", name,
 					"model", agent.Model,
 					"model", agent.Model,
 					"reasoning_effort", agent.ReasoningEffort)
 					"reasoning_effort", agent.ReasoningEffort)
@@ -509,7 +482,7 @@ func validateAgent(cfg *Config, name AgentName, agent Agent) error {
 		}
 		}
 	} else if !model.CanReason && agent.ReasoningEffort != "" {
 	} else if !model.CanReason && agent.ReasoningEffort != "" {
 		// Model doesn't support reasoning but reasoning effort is set
 		// Model doesn't support reasoning but reasoning effort is set
-		logging.Warn("model doesn't support reasoning but reasoning effort is set, ignoring",
+		slog.Warn("model doesn't support reasoning but reasoning effort is set, ignoring",
 			"agent", name,
 			"agent", name,
 			"model", agent.Model,
 			"model", agent.Model,
 			"reasoning_effort", agent.ReasoningEffort)
 			"reasoning_effort", agent.ReasoningEffort)
@@ -539,7 +512,7 @@ func Validate() error {
 	// Validate providers
 	// Validate providers
 	for provider, providerCfg := range cfg.Providers {
 	for provider, providerCfg := range cfg.Providers {
 		if providerCfg.APIKey == "" && !providerCfg.Disabled {
 		if providerCfg.APIKey == "" && !providerCfg.Disabled {
-			logging.Warn("provider has no API key, marking as disabled", "provider", provider)
+			slog.Warn("provider has no API key, marking as disabled", "provider", provider)
 			providerCfg.Disabled = true
 			providerCfg.Disabled = true
 			cfg.Providers[provider] = providerCfg
 			cfg.Providers[provider] = providerCfg
 		}
 		}
@@ -548,7 +521,7 @@ func Validate() error {
 	// Validate LSP configurations
 	// Validate LSP configurations
 	for language, lspConfig := range cfg.LSP {
 	for language, lspConfig := range cfg.LSP {
 		if lspConfig.Command == "" && !lspConfig.Disabled {
 		if lspConfig.Command == "" && !lspConfig.Disabled {
-			logging.Warn("LSP configuration has no command, marking as disabled", "language", language)
+			slog.Warn("LSP configuration has no command, marking as disabled", "language", language)
 			lspConfig.Disabled = true
 			lspConfig.Disabled = true
 			cfg.LSP[language] = lspConfig
 			cfg.LSP[language] = lspConfig
 		}
 		}
@@ -782,7 +755,7 @@ func UpdateTheme(themeName string) error {
 			return fmt.Errorf("failed to get home directory: %w", err)
 			return fmt.Errorf("failed to get home directory: %w", err)
 		}
 		}
 		configFile = filepath.Join(homeDir, fmt.Sprintf(".%s.json", appName))
 		configFile = filepath.Join(homeDir, fmt.Sprintf(".%s.json", appName))
-		logging.Info("config file not found, creating new one", "path", configFile)
+		slog.Info("config file not found, creating new one", "path", configFile)
 		configData = []byte(`{}`)
 		configData = []byte(`{}`)
 	} else {
 	} else {
 		// Read the existing config file
 		// Read the existing config file

+ 5 - 5
internal/db/connect.go

@@ -10,7 +10,7 @@ import (
 	_ "github.com/ncruces/go-sqlite3/embed"
 	_ "github.com/ncruces/go-sqlite3/embed"
 
 
 	"github.com/opencode-ai/opencode/internal/config"
 	"github.com/opencode-ai/opencode/internal/config"
-	"github.com/opencode-ai/opencode/internal/logging"
+	"log/slog"
 
 
 	"github.com/pressly/goose/v3"
 	"github.com/pressly/goose/v3"
 )
 )
@@ -47,21 +47,21 @@ func Connect() (*sql.DB, error) {
 
 
 	for _, pragma := range pragmas {
 	for _, pragma := range pragmas {
 		if _, err = db.Exec(pragma); err != nil {
 		if _, err = db.Exec(pragma); err != nil {
-			logging.Error("Failed to set pragma", pragma, err)
+			slog.Error("Failed to set pragma", pragma, err)
 		} else {
 		} else {
-			logging.Debug("Set pragma", "pragma", pragma)
+			slog.Debug("Set pragma", "pragma", pragma)
 		}
 		}
 	}
 	}
 
 
 	goose.SetBaseFS(FS)
 	goose.SetBaseFS(FS)
 
 
 	if err := goose.SetDialect("sqlite3"); err != nil {
 	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)
 		return nil, fmt.Errorf("failed to set dialect: %w", err)
 	}
 	}
 
 
 	if err := goose.Up(db, "migrations"); err != nil {
 	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 nil, fmt.Errorf("failed to apply migrations: %w", err)
 	}
 	}
 	return db, nil
 	return db, nil

+ 30 - 0
internal/db/db.go

@@ -27,6 +27,9 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) {
 	if q.createFileStmt, err = db.PrepareContext(ctx, createFile); err != nil {
 	if q.createFileStmt, err = db.PrepareContext(ctx, createFile); err != nil {
 		return nil, fmt.Errorf("error preparing query CreateFile: %w", err)
 		return nil, fmt.Errorf("error preparing query CreateFile: %w", err)
 	}
 	}
+	if q.createLogStmt, err = db.PrepareContext(ctx, createLog); err != nil {
+		return nil, fmt.Errorf("error preparing query CreateLog: %w", err)
+	}
 	if q.createMessageStmt, err = db.PrepareContext(ctx, createMessage); err != nil {
 	if q.createMessageStmt, err = db.PrepareContext(ctx, createMessage); err != nil {
 		return nil, fmt.Errorf("error preparing query CreateMessage: %w", err)
 		return nil, fmt.Errorf("error preparing query CreateMessage: %w", err)
 	}
 	}
@@ -60,6 +63,9 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) {
 	if q.getSessionByIDStmt, err = db.PrepareContext(ctx, getSessionByID); err != nil {
 	if q.getSessionByIDStmt, err = db.PrepareContext(ctx, getSessionByID); err != nil {
 		return nil, fmt.Errorf("error preparing query GetSessionByID: %w", err)
 		return nil, fmt.Errorf("error preparing query GetSessionByID: %w", err)
 	}
 	}
+	if q.listAllLogsStmt, err = db.PrepareContext(ctx, listAllLogs); err != nil {
+		return nil, fmt.Errorf("error preparing query ListAllLogs: %w", err)
+	}
 	if q.listFilesByPathStmt, err = db.PrepareContext(ctx, listFilesByPath); err != nil {
 	if q.listFilesByPathStmt, err = db.PrepareContext(ctx, listFilesByPath); err != nil {
 		return nil, fmt.Errorf("error preparing query ListFilesByPath: %w", err)
 		return nil, fmt.Errorf("error preparing query ListFilesByPath: %w", err)
 	}
 	}
@@ -69,6 +75,9 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) {
 	if q.listLatestSessionFilesStmt, err = db.PrepareContext(ctx, listLatestSessionFiles); err != nil {
 	if q.listLatestSessionFilesStmt, err = db.PrepareContext(ctx, listLatestSessionFiles); err != nil {
 		return nil, fmt.Errorf("error preparing query ListLatestSessionFiles: %w", err)
 		return nil, fmt.Errorf("error preparing query ListLatestSessionFiles: %w", err)
 	}
 	}
+	if q.listLogsBySessionStmt, err = db.PrepareContext(ctx, listLogsBySession); err != nil {
+		return nil, fmt.Errorf("error preparing query ListLogsBySession: %w", err)
+	}
 	if q.listMessagesBySessionStmt, err = db.PrepareContext(ctx, listMessagesBySession); err != nil {
 	if q.listMessagesBySessionStmt, err = db.PrepareContext(ctx, listMessagesBySession); err != nil {
 		return nil, fmt.Errorf("error preparing query ListMessagesBySession: %w", err)
 		return nil, fmt.Errorf("error preparing query ListMessagesBySession: %w", err)
 	}
 	}
@@ -100,6 +109,11 @@ func (q *Queries) Close() error {
 			err = fmt.Errorf("error closing createFileStmt: %w", cerr)
 			err = fmt.Errorf("error closing createFileStmt: %w", cerr)
 		}
 		}
 	}
 	}
+	if q.createLogStmt != nil {
+		if cerr := q.createLogStmt.Close(); cerr != nil {
+			err = fmt.Errorf("error closing createLogStmt: %w", cerr)
+		}
+	}
 	if q.createMessageStmt != nil {
 	if q.createMessageStmt != nil {
 		if cerr := q.createMessageStmt.Close(); cerr != nil {
 		if cerr := q.createMessageStmt.Close(); cerr != nil {
 			err = fmt.Errorf("error closing createMessageStmt: %w", cerr)
 			err = fmt.Errorf("error closing createMessageStmt: %w", cerr)
@@ -155,6 +169,11 @@ func (q *Queries) Close() error {
 			err = fmt.Errorf("error closing getSessionByIDStmt: %w", cerr)
 			err = fmt.Errorf("error closing getSessionByIDStmt: %w", cerr)
 		}
 		}
 	}
 	}
+	if q.listAllLogsStmt != nil {
+		if cerr := q.listAllLogsStmt.Close(); cerr != nil {
+			err = fmt.Errorf("error closing listAllLogsStmt: %w", cerr)
+		}
+	}
 	if q.listFilesByPathStmt != nil {
 	if q.listFilesByPathStmt != nil {
 		if cerr := q.listFilesByPathStmt.Close(); cerr != nil {
 		if cerr := q.listFilesByPathStmt.Close(); cerr != nil {
 			err = fmt.Errorf("error closing listFilesByPathStmt: %w", cerr)
 			err = fmt.Errorf("error closing listFilesByPathStmt: %w", cerr)
@@ -170,6 +189,11 @@ func (q *Queries) Close() error {
 			err = fmt.Errorf("error closing listLatestSessionFilesStmt: %w", cerr)
 			err = fmt.Errorf("error closing listLatestSessionFilesStmt: %w", cerr)
 		}
 		}
 	}
 	}
+	if q.listLogsBySessionStmt != nil {
+		if cerr := q.listLogsBySessionStmt.Close(); cerr != nil {
+			err = fmt.Errorf("error closing listLogsBySessionStmt: %w", cerr)
+		}
+	}
 	if q.listMessagesBySessionStmt != nil {
 	if q.listMessagesBySessionStmt != nil {
 		if cerr := q.listMessagesBySessionStmt.Close(); cerr != nil {
 		if cerr := q.listMessagesBySessionStmt.Close(); cerr != nil {
 			err = fmt.Errorf("error closing listMessagesBySessionStmt: %w", cerr)
 			err = fmt.Errorf("error closing listMessagesBySessionStmt: %w", cerr)
@@ -245,6 +269,7 @@ type Queries struct {
 	db                             DBTX
 	db                             DBTX
 	tx                             *sql.Tx
 	tx                             *sql.Tx
 	createFileStmt                 *sql.Stmt
 	createFileStmt                 *sql.Stmt
+	createLogStmt                  *sql.Stmt
 	createMessageStmt              *sql.Stmt
 	createMessageStmt              *sql.Stmt
 	createSessionStmt              *sql.Stmt
 	createSessionStmt              *sql.Stmt
 	deleteFileStmt                 *sql.Stmt
 	deleteFileStmt                 *sql.Stmt
@@ -256,9 +281,11 @@ type Queries struct {
 	getFileByPathAndSessionStmt    *sql.Stmt
 	getFileByPathAndSessionStmt    *sql.Stmt
 	getMessageStmt                 *sql.Stmt
 	getMessageStmt                 *sql.Stmt
 	getSessionByIDStmt             *sql.Stmt
 	getSessionByIDStmt             *sql.Stmt
+	listAllLogsStmt                *sql.Stmt
 	listFilesByPathStmt            *sql.Stmt
 	listFilesByPathStmt            *sql.Stmt
 	listFilesBySessionStmt         *sql.Stmt
 	listFilesBySessionStmt         *sql.Stmt
 	listLatestSessionFilesStmt     *sql.Stmt
 	listLatestSessionFilesStmt     *sql.Stmt
+	listLogsBySessionStmt          *sql.Stmt
 	listMessagesBySessionStmt      *sql.Stmt
 	listMessagesBySessionStmt      *sql.Stmt
 	listMessagesBySessionAfterStmt *sql.Stmt
 	listMessagesBySessionAfterStmt *sql.Stmt
 	listNewFilesStmt               *sql.Stmt
 	listNewFilesStmt               *sql.Stmt
@@ -273,6 +300,7 @@ func (q *Queries) WithTx(tx *sql.Tx) *Queries {
 		db:                             tx,
 		db:                             tx,
 		tx:                             tx,
 		tx:                             tx,
 		createFileStmt:                 q.createFileStmt,
 		createFileStmt:                 q.createFileStmt,
+		createLogStmt:                  q.createLogStmt,
 		createMessageStmt:              q.createMessageStmt,
 		createMessageStmt:              q.createMessageStmt,
 		createSessionStmt:              q.createSessionStmt,
 		createSessionStmt:              q.createSessionStmt,
 		deleteFileStmt:                 q.deleteFileStmt,
 		deleteFileStmt:                 q.deleteFileStmt,
@@ -284,9 +312,11 @@ func (q *Queries) WithTx(tx *sql.Tx) *Queries {
 		getFileByPathAndSessionStmt:    q.getFileByPathAndSessionStmt,
 		getFileByPathAndSessionStmt:    q.getFileByPathAndSessionStmt,
 		getMessageStmt:                 q.getMessageStmt,
 		getMessageStmt:                 q.getMessageStmt,
 		getSessionByIDStmt:             q.getSessionByIDStmt,
 		getSessionByIDStmt:             q.getSessionByIDStmt,
+		listAllLogsStmt:                q.listAllLogsStmt,
 		listFilesByPathStmt:            q.listFilesByPathStmt,
 		listFilesByPathStmt:            q.listFilesByPathStmt,
 		listFilesBySessionStmt:         q.listFilesBySessionStmt,
 		listFilesBySessionStmt:         q.listFilesBySessionStmt,
 		listLatestSessionFilesStmt:     q.listLatestSessionFilesStmt,
 		listLatestSessionFilesStmt:     q.listLatestSessionFilesStmt,
+		listLogsBySessionStmt:          q.listLogsBySessionStmt,
 		listMessagesBySessionStmt:      q.listMessagesBySessionStmt,
 		listMessagesBySessionStmt:      q.listMessagesBySessionStmt,
 		listMessagesBySessionAfterStmt: q.listMessagesBySessionAfterStmt,
 		listMessagesBySessionAfterStmt: q.listMessagesBySessionAfterStmt,
 		listNewFilesStmt:               q.listNewFilesStmt,
 		listNewFilesStmt:               q.listNewFilesStmt,

+ 128 - 0
internal/db/logs.sql.go

@@ -0,0 +1,128 @@
+// Code generated by sqlc. DO NOT EDIT.
+// versions:
+//   sqlc v1.29.0
+// source: logs.sql
+
+package db
+
+import (
+	"context"
+	"database/sql"
+)
+
+const createLog = `-- name: CreateLog :exec
+INSERT INTO logs (
+    id,
+    session_id,
+    timestamp,
+    level,
+    message,
+    attributes,
+    created_at
+) VALUES (
+    ?,
+    ?,
+    ?,
+    ?,
+    ?,
+    ?,
+    ?
+)
+`
+
+type CreateLogParams struct {
+	ID         string         `json:"id"`
+	SessionID  sql.NullString `json:"session_id"`
+	Timestamp  int64          `json:"timestamp"`
+	Level      string         `json:"level"`
+	Message    string         `json:"message"`
+	Attributes sql.NullString `json:"attributes"`
+	CreatedAt  int64          `json:"created_at"`
+}
+
+func (q *Queries) CreateLog(ctx context.Context, arg CreateLogParams) error {
+	_, err := q.exec(ctx, q.createLogStmt, createLog,
+		arg.ID,
+		arg.SessionID,
+		arg.Timestamp,
+		arg.Level,
+		arg.Message,
+		arg.Attributes,
+		arg.CreatedAt,
+	)
+	return err
+}
+
+const listAllLogs = `-- name: ListAllLogs :many
+SELECT id, session_id, timestamp, level, message, attributes, created_at FROM logs
+ORDER BY timestamp DESC
+LIMIT ?
+`
+
+func (q *Queries) ListAllLogs(ctx context.Context, limit int64) ([]Log, error) {
+	rows, err := q.query(ctx, q.listAllLogsStmt, listAllLogs, limit)
+	if err != nil {
+		return nil, err
+	}
+	defer rows.Close()
+	items := []Log{}
+	for rows.Next() {
+		var i Log
+		if err := rows.Scan(
+			&i.ID,
+			&i.SessionID,
+			&i.Timestamp,
+			&i.Level,
+			&i.Message,
+			&i.Attributes,
+			&i.CreatedAt,
+		); err != nil {
+			return nil, err
+		}
+		items = append(items, i)
+	}
+	if err := rows.Close(); err != nil {
+		return nil, err
+	}
+	if err := rows.Err(); err != nil {
+		return nil, err
+	}
+	return items, nil
+}
+
+const listLogsBySession = `-- name: ListLogsBySession :many
+SELECT id, session_id, timestamp, level, message, attributes, created_at FROM logs
+WHERE session_id = ?
+ORDER BY timestamp ASC
+`
+
+func (q *Queries) ListLogsBySession(ctx context.Context, sessionID sql.NullString) ([]Log, error) {
+	rows, err := q.query(ctx, q.listLogsBySessionStmt, listLogsBySession, sessionID)
+	if err != nil {
+		return nil, err
+	}
+	defer rows.Close()
+	items := []Log{}
+	for rows.Next() {
+		var i Log
+		if err := rows.Scan(
+			&i.ID,
+			&i.SessionID,
+			&i.Timestamp,
+			&i.Level,
+			&i.Message,
+			&i.Attributes,
+			&i.CreatedAt,
+		); err != nil {
+			return nil, err
+		}
+		items = append(items, i)
+	}
+	if err := rows.Close(); err != nil {
+		return nil, err
+	}
+	if err := rows.Err(); err != nil {
+		return nil, err
+	}
+	return items, nil
+}

+ 16 - 0
internal/db/migrations/20250508122310_create_logs_table.sql

@@ -0,0 +1,16 @@
+-- +goose Up
+CREATE TABLE logs (
+    id TEXT PRIMARY KEY,
+    session_id TEXT REFERENCES sessions(id) ON DELETE CASCADE,
+    timestamp INTEGER NOT NULL,
+    level TEXT NOT NULL,
+    message TEXT NOT NULL,
+    attributes TEXT,
+    created_at INTEGER NOT NULL
+);
+
+CREATE INDEX logs_session_id_idx ON logs(session_id);
+CREATE INDEX logs_timestamp_idx ON logs(timestamp);
+
+-- +goose Down
+DROP TABLE logs;

+ 10 - 0
internal/db/models.go

@@ -18,6 +18,16 @@ type File struct {
 	UpdatedAt int64  `json:"updated_at"`
 	UpdatedAt int64  `json:"updated_at"`
 }
 }
 
 
+type Log struct {
+	ID         string         `json:"id"`
+	SessionID  sql.NullString `json:"session_id"`
+	Timestamp  int64          `json:"timestamp"`
+	Level      string         `json:"level"`
+	Message    string         `json:"message"`
+	Attributes sql.NullString `json:"attributes"`
+	CreatedAt  int64          `json:"created_at"`
+}
+
 type Message struct {
 type Message struct {
 	ID         string         `json:"id"`
 	ID         string         `json:"id"`
 	SessionID  string         `json:"session_id"`
 	SessionID  string         `json:"session_id"`

+ 4 - 0
internal/db/querier.go

@@ -6,10 +6,12 @@ package db
 
 
 import (
 import (
 	"context"
 	"context"
+	"database/sql"
 )
 )
 
 
 type Querier interface {
 type Querier interface {
 	CreateFile(ctx context.Context, arg CreateFileParams) (File, error)
 	CreateFile(ctx context.Context, arg CreateFileParams) (File, error)
+	CreateLog(ctx context.Context, arg CreateLogParams) error
 	CreateMessage(ctx context.Context, arg CreateMessageParams) (Message, error)
 	CreateMessage(ctx context.Context, arg CreateMessageParams) (Message, error)
 	CreateSession(ctx context.Context, arg CreateSessionParams) (Session, error)
 	CreateSession(ctx context.Context, arg CreateSessionParams) (Session, error)
 	DeleteFile(ctx context.Context, id string) error
 	DeleteFile(ctx context.Context, id string) error
@@ -21,9 +23,11 @@ type Querier interface {
 	GetFileByPathAndSession(ctx context.Context, arg GetFileByPathAndSessionParams) (File, error)
 	GetFileByPathAndSession(ctx context.Context, arg GetFileByPathAndSessionParams) (File, error)
 	GetMessage(ctx context.Context, id string) (Message, error)
 	GetMessage(ctx context.Context, id string) (Message, error)
 	GetSessionByID(ctx context.Context, id string) (Session, error)
 	GetSessionByID(ctx context.Context, id string) (Session, error)
+	ListAllLogs(ctx context.Context, limit int64) ([]Log, error)
 	ListFilesByPath(ctx context.Context, path string) ([]File, error)
 	ListFilesByPath(ctx context.Context, path string) ([]File, error)
 	ListFilesBySession(ctx context.Context, sessionID string) ([]File, error)
 	ListFilesBySession(ctx context.Context, sessionID string) ([]File, error)
 	ListLatestSessionFiles(ctx context.Context, sessionID string) ([]File, error)
 	ListLatestSessionFiles(ctx context.Context, sessionID string) ([]File, error)
+	ListLogsBySession(ctx context.Context, sessionID sql.NullString) ([]Log, error)
 	ListMessagesBySession(ctx context.Context, sessionID string) ([]Message, error)
 	ListMessagesBySession(ctx context.Context, sessionID string) ([]Message, error)
 	ListMessagesBySessionAfter(ctx context.Context, arg ListMessagesBySessionAfterParams) ([]Message, error)
 	ListMessagesBySessionAfter(ctx context.Context, arg ListMessagesBySessionAfterParams) ([]Message, error)
 	ListNewFiles(ctx context.Context) ([]File, error)
 	ListNewFiles(ctx context.Context) ([]File, error)

+ 28 - 0
internal/db/sql/logs.sql

@@ -0,0 +1,28 @@
+-- name: CreateLog :exec
+INSERT INTO logs (
+    id,
+    session_id,
+    timestamp,
+    level,
+    message,
+    attributes,
+    created_at
+) VALUES (
+    ?,
+    ?,
+    ?,
+    ?,
+    ?,
+    ?,
+    ?
+);
+
+-- name: ListLogsBySession :many
+SELECT * FROM logs
+WHERE session_id = ?
+ORDER BY timestamp ASC;
+
+-- name: ListAllLogs :many
+SELECT * FROM logs
+ORDER BY timestamp DESC
+LIMIT ?;

+ 5 - 3
internal/llm/agent/agent.go

@@ -8,6 +8,8 @@ import (
 	"sync"
 	"sync"
 	"time"
 	"time"
 
 
+	"log/slog"
+
 	"github.com/opencode-ai/opencode/internal/config"
 	"github.com/opencode-ai/opencode/internal/config"
 	"github.com/opencode-ai/opencode/internal/llm/models"
 	"github.com/opencode-ai/opencode/internal/llm/models"
 	"github.com/opencode-ai/opencode/internal/llm/prompt"
 	"github.com/opencode-ai/opencode/internal/llm/prompt"
@@ -177,7 +179,7 @@ func (a *agent) Run(ctx context.Context, sessionID string, content string, attac
 
 
 	a.activeRequests.Store(sessionID, cancel)
 	a.activeRequests.Store(sessionID, cancel)
 	go func() {
 	go func() {
-		logging.Debug("Request started", "sessionID", sessionID)
+		slog.Debug("Request started", "sessionID", sessionID)
 		defer logging.RecoverPanic("agent.Run", func() {
 		defer logging.RecoverPanic("agent.Run", func() {
 			events <- a.err(fmt.Errorf("panic while running the agent"))
 			events <- a.err(fmt.Errorf("panic while running the agent"))
 		})
 		})
@@ -189,7 +191,7 @@ func (a *agent) Run(ctx context.Context, sessionID string, content string, attac
 		if result.Err() != nil && !errors.Is(result.Err(), ErrRequestCancelled) && !errors.Is(result.Err(), context.Canceled) {
 		if result.Err() != nil && !errors.Is(result.Err(), ErrRequestCancelled) && !errors.Is(result.Err(), context.Canceled) {
 			status.Error(result.Err().Error())
 			status.Error(result.Err().Error())
 		}
 		}
-		logging.Debug("Request completed", "sessionID", sessionID)
+		slog.Debug("Request completed", "sessionID", sessionID)
 		a.activeRequests.Delete(sessionID)
 		a.activeRequests.Delete(sessionID)
 		cancel()
 		cancel()
 		events <- result
 		events <- result
@@ -276,7 +278,7 @@ func (a *agent) processGeneration(ctx context.Context, sessionID, content string
 			}
 			}
 			return a.err(fmt.Errorf("failed to process events: %w", err))
 			return a.err(fmt.Errorf("failed to process events: %w", err))
 		}
 		}
-		logging.Info("Result", "message", agentMessage.FinishReason(), "toolResults", toolResults)
+		slog.Info("Result", "message", agentMessage.FinishReason(), "toolResults", toolResults)
 		if (agentMessage.FinishReason() == message.FinishReasonToolUse) && toolResults != nil {
 		if (agentMessage.FinishReason() == message.FinishReasonToolUse) && toolResults != nil {
 			// We are not done, we need to respond with the tool response
 			// We are not done, we need to respond with the tool response
 			messages = append(messages, agentMessage, *toolResults)
 			messages = append(messages, agentMessage, *toolResults)

+ 5 - 5
internal/llm/agent/mcp-tools.go

@@ -7,9 +7,9 @@ import (
 
 
 	"github.com/opencode-ai/opencode/internal/config"
 	"github.com/opencode-ai/opencode/internal/config"
 	"github.com/opencode-ai/opencode/internal/llm/tools"
 	"github.com/opencode-ai/opencode/internal/llm/tools"
-	"github.com/opencode-ai/opencode/internal/logging"
 	"github.com/opencode-ai/opencode/internal/permission"
 	"github.com/opencode-ai/opencode/internal/permission"
 	"github.com/opencode-ai/opencode/internal/version"
 	"github.com/opencode-ai/opencode/internal/version"
+	"log/slog"
 
 
 	"github.com/mark3labs/mcp-go/client"
 	"github.com/mark3labs/mcp-go/client"
 	"github.com/mark3labs/mcp-go/mcp"
 	"github.com/mark3labs/mcp-go/mcp"
@@ -146,13 +146,13 @@ func getTools(ctx context.Context, name string, m config.MCPServer, permissions
 
 
 	_, err := c.Initialize(ctx, initRequest)
 	_, err := c.Initialize(ctx, initRequest)
 	if err != nil {
 	if err != nil {
-		logging.Error("error initializing mcp client", "error", err)
+		slog.Error("error initializing mcp client", "error", err)
 		return stdioTools
 		return stdioTools
 	}
 	}
 	toolsRequest := mcp.ListToolsRequest{}
 	toolsRequest := mcp.ListToolsRequest{}
 	tools, err := c.ListTools(ctx, toolsRequest)
 	tools, err := c.ListTools(ctx, toolsRequest)
 	if err != nil {
 	if err != nil {
-		logging.Error("error listing tools", "error", err)
+		slog.Error("error listing tools", "error", err)
 		return stdioTools
 		return stdioTools
 	}
 	}
 	for _, t := range tools.Tools {
 	for _, t := range tools.Tools {
@@ -175,7 +175,7 @@ func GetMcpTools(ctx context.Context, permissions permission.Service) []tools.Ba
 				m.Args...,
 				m.Args...,
 			)
 			)
 			if err != nil {
 			if err != nil {
-				logging.Error("error creating mcp client", "error", err)
+				slog.Error("error creating mcp client", "error", err)
 				continue
 				continue
 			}
 			}
 
 
@@ -186,7 +186,7 @@ func GetMcpTools(ctx context.Context, permissions permission.Service) []tools.Ba
 				client.WithHeaders(m.Headers),
 				client.WithHeaders(m.Headers),
 			)
 			)
 			if err != nil {
 			if err != nil {
-				logging.Error("error creating mcp client", "error", err)
+				slog.Error("error creating mcp client", "error", err)
 				continue
 				continue
 			}
 			}
 			mcpTools = append(mcpTools, getTools(ctx, name, m, permissions, c)...)
 			mcpTools = append(mcpTools, getTools(ctx, name, m, permissions, c)...)

+ 1 - 1
internal/llm/agent/tools.go

@@ -33,7 +33,7 @@ func CoderAgentTools(
 			tools.NewGlobTool(),
 			tools.NewGlobTool(),
 			tools.NewGrepTool(),
 			tools.NewGrepTool(),
 			tools.NewLsTool(),
 			tools.NewLsTool(),
-			tools.NewSourcegraphTool(),
+			// tools.NewSourcegraphTool(),
 			tools.NewViewTool(lspClients),
 			tools.NewViewTool(lspClients),
 			tools.NewPatchTool(lspClients, permissions, history),
 			tools.NewPatchTool(lspClients, permissions, history),
 			tools.NewWriteTool(lspClients, permissions, history),
 			tools.NewWriteTool(lspClients, permissions, history),

+ 2 - 2
internal/llm/prompt/prompt.go

@@ -9,7 +9,7 @@ import (
 
 
 	"github.com/opencode-ai/opencode/internal/config"
 	"github.com/opencode-ai/opencode/internal/config"
 	"github.com/opencode-ai/opencode/internal/llm/models"
 	"github.com/opencode-ai/opencode/internal/llm/models"
-	"github.com/opencode-ai/opencode/internal/logging"
+	"log/slog"
 )
 )
 
 
 func GetAgentPrompt(agentName config.AgentName, provider models.ModelProvider) string {
 func GetAgentPrompt(agentName config.AgentName, provider models.ModelProvider) string {
@@ -28,7 +28,7 @@ func GetAgentPrompt(agentName config.AgentName, provider models.ModelProvider) s
 	if agentName == config.AgentCoder || agentName == config.AgentTask {
 	if agentName == config.AgentCoder || agentName == config.AgentTask {
 		// Add context from project-specific instruction files if they exist
 		// Add context from project-specific instruction files if they exist
 		contextContent := getContextFromPaths()
 		contextContent := getContextFromPaths()
-		logging.Debug("Context content", "Context", contextContent)
+		slog.Debug("Context content", "Context", contextContent)
 		if 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)
 			return fmt.Sprintf("%s\n\n# Project-Specific Context\n Make sure to follow the instructions in the context below\n%s", basePrompt, contextContent)
 		}
 		}

+ 5 - 1
internal/llm/prompt/prompt_test.go

@@ -2,6 +2,7 @@ package prompt
 
 
 import (
 import (
 	"fmt"
 	"fmt"
+	"log/slog"
 	"os"
 	"os"
 	"path/filepath"
 	"path/filepath"
 	"testing"
 	"testing"
@@ -14,8 +15,11 @@ import (
 func TestGetContextFromPaths(t *testing.T) {
 func TestGetContextFromPaths(t *testing.T) {
 	t.Parallel()
 	t.Parallel()
 
 
+	lvl := new(slog.LevelVar)
+	lvl.Set(slog.LevelDebug)
+
 	tmpDir := t.TempDir()
 	tmpDir := t.TempDir()
-	_, err := config.Load(tmpDir, false)
+	_, err := config.Load(tmpDir, false, lvl)
 	if err != nil {
 	if err != nil {
 		t.Fatalf("Failed to load config: %v", err)
 		t.Fatalf("Failed to load config: %v", err)
 	}
 	}

+ 6 - 6
internal/llm/provider/anthropic.go

@@ -15,9 +15,9 @@ import (
 	"github.com/opencode-ai/opencode/internal/config"
 	"github.com/opencode-ai/opencode/internal/config"
 	"github.com/opencode-ai/opencode/internal/llm/models"
 	"github.com/opencode-ai/opencode/internal/llm/models"
 	"github.com/opencode-ai/opencode/internal/llm/tools"
 	"github.com/opencode-ai/opencode/internal/llm/tools"
-	"github.com/opencode-ai/opencode/internal/logging"
 	"github.com/opencode-ai/opencode/internal/message"
 	"github.com/opencode-ai/opencode/internal/message"
 	"github.com/opencode-ai/opencode/internal/status"
 	"github.com/opencode-ai/opencode/internal/status"
+	"log/slog"
 )
 )
 
 
 type anthropicOptions struct {
 type anthropicOptions struct {
@@ -107,7 +107,7 @@ func (a *anthropicClient) convertMessages(messages []message.Message) (anthropic
 			}
 			}
 
 
 			if len(blocks) == 0 {
 			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
 				continue
 			}
 			}
 			anthropicMessages = append(anthropicMessages, anthropic.NewAssistantMessage(blocks...))
 			anthropicMessages = append(anthropicMessages, anthropic.NewAssistantMessage(blocks...))
@@ -210,7 +210,7 @@ func (a *anthropicClient) send(ctx context.Context, messages []message.Message,
 	cfg := config.Get()
 	cfg := config.Get()
 	if cfg.Debug {
 	if cfg.Debug {
 		jsonData, _ := json.Marshal(preparedMessages)
 		jsonData, _ := json.Marshal(preparedMessages)
-		logging.Debug("Prepared messages", "messages", string(jsonData))
+		slog.Debug("Prepared messages", "messages", string(jsonData))
 	}
 	}
 
 
 	attempts := 0
 	attempts := 0
@@ -222,7 +222,7 @@ 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 there is an error we are going to see if we can retry the call
 		if err != nil {
 		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)
 			retry, after, retryErr := a.shouldRetry(attempts, err)
 			if retryErr != nil {
 			if retryErr != nil {
 				return nil, retryErr
 				return nil, retryErr
@@ -259,7 +259,7 @@ func (a *anthropicClient) stream(ctx context.Context, messages []message.Message
 	cfg := config.Get()
 	cfg := config.Get()
 	if cfg.Debug {
 	if cfg.Debug {
 		jsonData, _ := json.Marshal(preparedMessages)
 		jsonData, _ := json.Marshal(preparedMessages)
-		logging.Debug("Prepared messages", "messages", string(jsonData))
+		slog.Debug("Prepared messages", "messages", string(jsonData))
 	}
 	}
 	attempts := 0
 	attempts := 0
 	eventChan := make(chan ProviderEvent)
 	eventChan := make(chan ProviderEvent)
@@ -277,7 +277,7 @@ func (a *anthropicClient) stream(ctx context.Context, messages []message.Message
 				event := anthropicStream.Current()
 				event := anthropicStream.Current()
 				err := accumulatedMessage.Accumulate(event)
 				err := accumulatedMessage.Accumulate(event)
 				if err != nil {
 				if err != nil {
-					logging.Warn("Error accumulating message", "error", err)
+					slog.Warn("Error accumulating message", "error", err)
 					continue
 					continue
 				}
 				}
 
 

+ 4 - 4
internal/llm/provider/gemini.go

@@ -13,11 +13,11 @@ import (
 	"github.com/google/uuid"
 	"github.com/google/uuid"
 	"github.com/opencode-ai/opencode/internal/config"
 	"github.com/opencode-ai/opencode/internal/config"
 	"github.com/opencode-ai/opencode/internal/llm/tools"
 	"github.com/opencode-ai/opencode/internal/llm/tools"
-	"github.com/opencode-ai/opencode/internal/logging"
 	"github.com/opencode-ai/opencode/internal/message"
 	"github.com/opencode-ai/opencode/internal/message"
 	"github.com/opencode-ai/opencode/internal/status"
 	"github.com/opencode-ai/opencode/internal/status"
 	"google.golang.org/api/iterator"
 	"google.golang.org/api/iterator"
 	"google.golang.org/api/option"
 	"google.golang.org/api/option"
+	"log/slog"
 )
 )
 
 
 type geminiOptions struct {
 type geminiOptions struct {
@@ -42,7 +42,7 @@ func newGeminiClient(opts providerClientOptions) GeminiClient {
 
 
 	client, err := genai.NewClient(context.Background(), option.WithAPIKey(opts.apiKey))
 	client, err := genai.NewClient(context.Background(), option.WithAPIKey(opts.apiKey))
 	if err != nil {
 	if err != nil {
-		logging.Error("Failed to create Gemini client", "error", err)
+		slog.Error("Failed to create Gemini client", "error", err)
 		return nil
 		return nil
 	}
 	}
 
 
@@ -176,7 +176,7 @@ func (g *geminiClient) send(ctx context.Context, messages []message.Message, too
 	cfg := config.Get()
 	cfg := config.Get()
 	if cfg.Debug {
 	if cfg.Debug {
 		jsonData, _ := json.Marshal(geminiMessages)
 		jsonData, _ := json.Marshal(geminiMessages)
-		logging.Debug("Prepared messages", "messages", string(jsonData))
+		slog.Debug("Prepared messages", "messages", string(jsonData))
 	}
 	}
 
 
 	attempts := 0
 	attempts := 0
@@ -263,7 +263,7 @@ func (g *geminiClient) stream(ctx context.Context, messages []message.Message, t
 	cfg := config.Get()
 	cfg := config.Get()
 	if cfg.Debug {
 	if cfg.Debug {
 		jsonData, _ := json.Marshal(geminiMessages)
 		jsonData, _ := json.Marshal(geminiMessages)
-		logging.Debug("Prepared messages", "messages", string(jsonData))
+		slog.Debug("Prepared messages", "messages", string(jsonData))
 	}
 	}
 
 
 	attempts := 0
 	attempts := 0

+ 4 - 4
internal/llm/provider/openai.go

@@ -14,9 +14,9 @@ import (
 	"github.com/opencode-ai/opencode/internal/config"
 	"github.com/opencode-ai/opencode/internal/config"
 	"github.com/opencode-ai/opencode/internal/llm/models"
 	"github.com/opencode-ai/opencode/internal/llm/models"
 	"github.com/opencode-ai/opencode/internal/llm/tools"
 	"github.com/opencode-ai/opencode/internal/llm/tools"
-	"github.com/opencode-ai/opencode/internal/logging"
 	"github.com/opencode-ai/opencode/internal/message"
 	"github.com/opencode-ai/opencode/internal/message"
 	"github.com/opencode-ai/opencode/internal/status"
 	"github.com/opencode-ai/opencode/internal/status"
+	"log/slog"
 )
 )
 
 
 type openaiOptions struct {
 type openaiOptions struct {
@@ -199,7 +199,7 @@ func (o *openaiClient) send(ctx context.Context, messages []message.Message, too
 	cfg := config.Get()
 	cfg := config.Get()
 	if cfg.Debug {
 	if cfg.Debug {
 		jsonData, _ := json.Marshal(params)
 		jsonData, _ := json.Marshal(params)
-		logging.Debug("Prepared messages", "messages", string(jsonData))
+		slog.Debug("Prepared messages", "messages", string(jsonData))
 	}
 	}
 	attempts := 0
 	attempts := 0
 	for {
 	for {
@@ -256,7 +256,7 @@ func (o *openaiClient) stream(ctx context.Context, messages []message.Message, t
 	cfg := config.Get()
 	cfg := config.Get()
 	if cfg.Debug {
 	if cfg.Debug {
 		jsonData, _ := json.Marshal(params)
 		jsonData, _ := json.Marshal(params)
-		logging.Debug("Prepared messages", "messages", string(jsonData))
+		slog.Debug("Prepared messages", "messages", string(jsonData))
 	}
 	}
 
 
 	attempts := 0
 	attempts := 0
@@ -427,7 +427,7 @@ func WithReasoningEffort(effort string) OpenAIOption {
 		case "low", "medium", "high":
 		case "low", "medium", "high":
 			defaultReasoningEffort = effort
 			defaultReasoningEffort = effort
 		default:
 		default:
-			logging.Warn("Invalid reasoning effort, using default: medium")
+			slog.Warn("Invalid reasoning effort, using default: medium")
 		}
 		}
 		options.reasoningEffort = defaultReasoningEffort
 		options.reasoningEffort = defaultReasoningEffort
 	}
 	}

+ 10 - 10
internal/llm/provider/provider.go

@@ -6,8 +6,8 @@ import (
 
 
 	"github.com/opencode-ai/opencode/internal/llm/models"
 	"github.com/opencode-ai/opencode/internal/llm/models"
 	"github.com/opencode-ai/opencode/internal/llm/tools"
 	"github.com/opencode-ai/opencode/internal/llm/tools"
-	"github.com/opencode-ai/opencode/internal/logging"
 	"github.com/opencode-ai/opencode/internal/message"
 	"github.com/opencode-ai/opencode/internal/message"
+	"log/slog"
 )
 )
 
 
 type EventType string
 type EventType string
@@ -166,13 +166,13 @@ func (p *baseProvider[C]) SendMessages(ctx context.Context, messages []message.M
 	messages = p.cleanMessages(messages)
 	messages = p.cleanMessages(messages)
 	response, err := p.client.send(ctx, messages, tools)
 	response, err := p.client.send(ctx, messages, tools)
 	if err == nil && response != nil {
 	if err == nil && response != nil {
-		logging.Debug("API request token usage", 
+		slog.Debug("API request token usage",
 			"model", p.options.model.Name,
 			"model", p.options.model.Name,
 			"input_tokens", response.Usage.InputTokens,
 			"input_tokens", response.Usage.InputTokens,
 			"output_tokens", response.Usage.OutputTokens,
 			"output_tokens", response.Usage.OutputTokens,
 			"cache_creation_tokens", response.Usage.CacheCreationTokens,
 			"cache_creation_tokens", response.Usage.CacheCreationTokens,
 			"cache_read_tokens", response.Usage.CacheReadTokens,
 			"cache_read_tokens", response.Usage.CacheReadTokens,
-			"total_tokens", response.Usage.InputTokens + response.Usage.OutputTokens)
+			"total_tokens", response.Usage.InputTokens+response.Usage.OutputTokens)
 	}
 	}
 	return response, err
 	return response, err
 }
 }
@@ -188,30 +188,30 @@ func (p *baseProvider[C]) MaxTokens() int64 {
 func (p *baseProvider[C]) StreamResponse(ctx context.Context, messages []message.Message, tools []tools.BaseTool) <-chan ProviderEvent {
 func (p *baseProvider[C]) StreamResponse(ctx context.Context, messages []message.Message, tools []tools.BaseTool) <-chan ProviderEvent {
 	messages = p.cleanMessages(messages)
 	messages = p.cleanMessages(messages)
 	eventChan := p.client.stream(ctx, messages, tools)
 	eventChan := p.client.stream(ctx, messages, tools)
-	
+
 	// Create a new channel to intercept events
 	// Create a new channel to intercept events
 	wrappedChan := make(chan ProviderEvent)
 	wrappedChan := make(chan ProviderEvent)
-	
+
 	go func() {
 	go func() {
 		defer close(wrappedChan)
 		defer close(wrappedChan)
-		
+
 		for event := range eventChan {
 		for event := range eventChan {
 			// Pass the event through
 			// Pass the event through
 			wrappedChan <- event
 			wrappedChan <- event
-			
+
 			// Log token usage when we get the complete event
 			// Log token usage when we get the complete event
 			if event.Type == EventComplete && event.Response != nil {
 			if event.Type == EventComplete && event.Response != nil {
-				logging.Debug("API streaming request token usage", 
+				slog.Debug("API streaming request token usage",
 					"model", p.options.model.Name,
 					"model", p.options.model.Name,
 					"input_tokens", event.Response.Usage.InputTokens,
 					"input_tokens", event.Response.Usage.InputTokens,
 					"output_tokens", event.Response.Usage.OutputTokens,
 					"output_tokens", event.Response.Usage.OutputTokens,
 					"cache_creation_tokens", event.Response.Usage.CacheCreationTokens,
 					"cache_creation_tokens", event.Response.Usage.CacheCreationTokens,
 					"cache_read_tokens", event.Response.Usage.CacheReadTokens,
 					"cache_read_tokens", event.Response.Usage.CacheReadTokens,
-					"total_tokens", event.Response.Usage.InputTokens + event.Response.Usage.OutputTokens)
+					"total_tokens", event.Response.Usage.InputTokens+event.Response.Usage.OutputTokens)
 			}
 			}
 		}
 		}
 	}()
 	}()
-	
+
 	return wrappedChan
 	return wrappedChan
 }
 }
 
 

+ 6 - 6
internal/llm/tools/edit.go

@@ -12,9 +12,9 @@ import (
 	"github.com/opencode-ai/opencode/internal/config"
 	"github.com/opencode-ai/opencode/internal/config"
 	"github.com/opencode-ai/opencode/internal/diff"
 	"github.com/opencode-ai/opencode/internal/diff"
 	"github.com/opencode-ai/opencode/internal/history"
 	"github.com/opencode-ai/opencode/internal/history"
-	"github.com/opencode-ai/opencode/internal/logging"
 	"github.com/opencode-ai/opencode/internal/lsp"
 	"github.com/opencode-ai/opencode/internal/lsp"
 	"github.com/opencode-ai/opencode/internal/permission"
 	"github.com/opencode-ai/opencode/internal/permission"
+	"log/slog"
 )
 )
 
 
 type EditParams struct {
 type EditParams struct {
@@ -234,7 +234,7 @@ func (e *editTool) createNewFile(ctx context.Context, filePath, content string)
 	_, err = e.files.CreateVersion(ctx, sessionID, filePath, content)
 	_, err = e.files.CreateVersion(ctx, sessionID, filePath, content)
 	if err != nil {
 	if err != nil {
 		// Log error but don't fail the operation
 		// 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)
 	recordFileWrite(filePath)
@@ -347,13 +347,13 @@ func (e *editTool) deleteContent(ctx context.Context, filePath, oldString string
 		// User Manually changed the content store an intermediate version
 		// User Manually changed the content store an intermediate version
 		_, err = e.files.CreateVersion(ctx, sessionID, filePath, oldContent)
 		_, err = e.files.CreateVersion(ctx, sessionID, filePath, oldContent)
 		if err != nil {
 		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
 	// Store the new version
 	_, err = e.files.CreateVersion(ctx, sessionID, filePath, "")
 	_, err = e.files.CreateVersion(ctx, sessionID, filePath, "")
 	if err != nil {
 	if err != nil {
-		logging.Debug("Error creating file history version", "error", err)
+		slog.Debug("Error creating file history version", "error", err)
 	}
 	}
 
 
 	recordFileWrite(filePath)
 	recordFileWrite(filePath)
@@ -467,13 +467,13 @@ func (e *editTool) replaceContent(ctx context.Context, filePath, oldString, newS
 		// User Manually changed the content store an intermediate version
 		// User Manually changed the content store an intermediate version
 		_, err = e.files.CreateVersion(ctx, sessionID, filePath, oldContent)
 		_, err = e.files.CreateVersion(ctx, sessionID, filePath, oldContent)
 		if err != nil {
 		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
 	// Store the new version
 	_, err = e.files.CreateVersion(ctx, sessionID, filePath, newContent)
 	_, err = e.files.CreateVersion(ctx, sessionID, filePath, newContent)
 	if err != nil {
 	if err != nil {
-		logging.Debug("Error creating file history version", "error", err)
+		slog.Debug("Error creating file history version", "error", err)
 	}
 	}
 
 
 	recordFileWrite(filePath)
 	recordFileWrite(filePath)

+ 4 - 4
internal/llm/tools/patch.go

@@ -11,9 +11,9 @@ import (
 	"github.com/opencode-ai/opencode/internal/config"
 	"github.com/opencode-ai/opencode/internal/config"
 	"github.com/opencode-ai/opencode/internal/diff"
 	"github.com/opencode-ai/opencode/internal/diff"
 	"github.com/opencode-ai/opencode/internal/history"
 	"github.com/opencode-ai/opencode/internal/history"
-	"github.com/opencode-ai/opencode/internal/logging"
 	"github.com/opencode-ai/opencode/internal/lsp"
 	"github.com/opencode-ai/opencode/internal/lsp"
 	"github.com/opencode-ai/opencode/internal/permission"
 	"github.com/opencode-ai/opencode/internal/permission"
+	"log/slog"
 )
 )
 
 
 type PatchParams struct {
 type PatchParams struct {
@@ -318,7 +318,7 @@ func (p *patchTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error
 			// If not adding a file, create history entry for existing file
 			// If not adding a file, create history entry for existing file
 			_, err = p.files.Create(ctx, sessionID, absPath, oldContent)
 			_, err = p.files.Create(ctx, sessionID, absPath, oldContent)
 			if err != nil {
 			if err != nil {
-				logging.Debug("Error creating file history", "error", err)
+				slog.Debug("Error creating file history", "error", err)
 			}
 			}
 		}
 		}
 
 
@@ -326,7 +326,7 @@ func (p *patchTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error
 			// User manually changed content, store intermediate version
 			// User manually changed content, store intermediate version
 			_, err = p.files.CreateVersion(ctx, sessionID, absPath, oldContent)
 			_, err = p.files.CreateVersion(ctx, sessionID, absPath, oldContent)
 			if err != nil {
 			if err != nil {
-				logging.Debug("Error creating file history version", "error", err)
+				slog.Debug("Error creating file history version", "error", err)
 			}
 			}
 		}
 		}
 
 
@@ -337,7 +337,7 @@ func (p *patchTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error
 			_, err = p.files.CreateVersion(ctx, sessionID, absPath, newContent)
 			_, err = p.files.CreateVersion(ctx, sessionID, absPath, newContent)
 		}
 		}
 		if err != nil {
 		if err != nil {
-			logging.Debug("Error creating file history version", "error", err)
+			slog.Debug("Error creating file history version", "error", err)
 		}
 		}
 
 
 		// Record file operations
 		// Record file operations

+ 3 - 3
internal/llm/tools/write.go

@@ -12,9 +12,9 @@ import (
 	"github.com/opencode-ai/opencode/internal/config"
 	"github.com/opencode-ai/opencode/internal/config"
 	"github.com/opencode-ai/opencode/internal/diff"
 	"github.com/opencode-ai/opencode/internal/diff"
 	"github.com/opencode-ai/opencode/internal/history"
 	"github.com/opencode-ai/opencode/internal/history"
-	"github.com/opencode-ai/opencode/internal/logging"
 	"github.com/opencode-ai/opencode/internal/lsp"
 	"github.com/opencode-ai/opencode/internal/lsp"
 	"github.com/opencode-ai/opencode/internal/permission"
 	"github.com/opencode-ai/opencode/internal/permission"
+	"log/slog"
 )
 )
 
 
 type WriteParams struct {
 type WriteParams struct {
@@ -201,13 +201,13 @@ func (w *writeTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error
 		// User Manually changed the content store an intermediate version
 		// User Manually changed the content store an intermediate version
 		_, err = w.files.CreateVersion(ctx, sessionID, filePath, oldContent)
 		_, err = w.files.CreateVersion(ctx, sessionID, filePath, oldContent)
 		if err != nil {
 		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
 	// Store the new version
 	_, err = w.files.CreateVersion(ctx, sessionID, filePath, params.Content)
 	_, err = w.files.CreateVersion(ctx, sessionID, filePath, params.Content)
 	if err != nil {
 	if err != nil {
-		logging.Debug("Error creating file history version", "error", err)
+		slog.Debug("Error creating file history version", "error", err)
 	}
 	}
 
 
 	recordFileWrite(filePath)
 	recordFileWrite(filePath)

+ 4 - 19
internal/logging/logger.go → internal/logging/logging.go

@@ -10,22 +10,6 @@ import (
 	"github.com/opencode-ai/opencode/internal/status"
 	"github.com/opencode-ai/opencode/internal/status"
 )
 )
 
 
-func Info(msg string, args ...any) {
-	slog.Info(msg, args...)
-}
-
-func Debug(msg string, args ...any) {
-	slog.Debug(msg, args...)
-}
-
-func Warn(msg string, args ...any) {
-	slog.Warn(msg, args...)
-}
-
-func Error(msg string, args ...any) {
-	slog.Error(msg, args...)
-}
-
 // RecoverPanic is a common function to handle panics gracefully.
 // RecoverPanic is a common function to handle panics gracefully.
 // It logs the error, creates a panic log file with stack trace,
 // It logs the error, creates a panic log file with stack trace,
 // and executes an optional cleanup function before returning.
 // and executes an optional cleanup function before returning.
@@ -33,7 +17,7 @@ func RecoverPanic(name string, cleanup func()) {
 	if r := recover(); r != nil {
 	if r := recover(); r != nil {
 		// Log the panic
 		// Log the panic
 		errorMsg := fmt.Sprintf("Panic in %s: %v", name, r)
 		errorMsg := fmt.Sprintf("Panic in %s: %v", name, r)
-		Error(errorMsg)
+		slog.Error(errorMsg)
 		status.Error(errorMsg)
 		status.Error(errorMsg)
 
 
 		// Create a timestamped panic log file
 		// Create a timestamped panic log file
@@ -43,7 +27,7 @@ func RecoverPanic(name string, cleanup func()) {
 		file, err := os.Create(filename)
 		file, err := os.Create(filename)
 		if err != nil {
 		if err != nil {
 			errMsg := fmt.Sprintf("Failed to create panic log: %v", err)
 			errMsg := fmt.Sprintf("Failed to create panic log: %v", err)
-			Error(errMsg)
+			slog.Error(errMsg)
 			status.Error(errMsg)
 			status.Error(errMsg)
 		} else {
 		} else {
 			defer file.Close()
 			defer file.Close()
@@ -54,7 +38,7 @@ func RecoverPanic(name string, cleanup func()) {
 			fmt.Fprintf(file, "Stack Trace:\n%s\n", debug.Stack())
 			fmt.Fprintf(file, "Stack Trace:\n%s\n", debug.Stack())
 
 
 			infoMsg := fmt.Sprintf("Panic details written to %s", filename)
 			infoMsg := fmt.Sprintf("Panic details written to %s", filename)
-			Info(infoMsg)
+			slog.Info(infoMsg)
 			status.Info(infoMsg)
 			status.Info(infoMsg)
 		}
 		}
 
 
@@ -64,3 +48,4 @@ func RecoverPanic(name string, cleanup func()) {
 		}
 		}
 	}
 	}
 }
 }
+

+ 48 - 0
internal/logging/manager.go

@@ -0,0 +1,48 @@
+package logging
+
+import (
+	"context"
+	"sync"
+)
+
+// Manager handles logging management
+type Manager struct {
+	service Service
+	mu      sync.RWMutex
+}
+
+// Global instance of the logging manager
+var globalManager *Manager
+
+// InitManager initializes the global logging manager with the provided service
+func InitManager(service Service) {
+	globalManager = &Manager{
+		service: service,
+	}
+
+	// Subscribe to log events if needed
+	go func() {
+		ctx := context.Background()
+		_ = service.Subscribe(ctx) // Just subscribing to keep the channel open
+	}()
+}
+
+// GetService returns the logging service
+func GetService() Service {
+	if globalManager == nil {
+		return nil
+	}
+
+	globalManager.mu.RLock()
+	defer globalManager.mu.RUnlock()
+
+	return globalManager.service
+}
+
+func Create(ctx context.Context, log Log) error {
+	if globalManager == nil {
+		return nil
+	}
+	return globalManager.service.Create(ctx, log)
+}
+

+ 0 - 19
internal/logging/message.go

@@ -1,19 +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
-	Message    string `json:"msg"`
-	Attributes []Attr
-}
-
-type Attr struct {
-	Key   string
-	Value string
-}

+ 167 - 0
internal/logging/service.go

@@ -0,0 +1,167 @@
+package logging
+
+import (
+	"context"
+	"database/sql"
+	"encoding/json"
+	"time"
+
+	"github.com/google/uuid"
+	"github.com/opencode-ai/opencode/internal/db"
+	"github.com/opencode-ai/opencode/internal/pubsub"
+)
+
+// Log represents a log entry in the system
+type Log struct {
+	ID         string
+	SessionID  string
+	Timestamp  int64
+	Level      string
+	Message    string
+	Attributes map[string]string
+	CreatedAt  int64
+}
+
+// Service defines the interface for log operations
+type Service interface {
+	pubsub.Suscriber[Log]
+	Create(ctx context.Context, log Log) error
+	ListBySession(ctx context.Context, sessionID string) ([]Log, error)
+	ListAll(ctx context.Context, limit int) ([]Log, error)
+}
+
+// service implements the Service interface
+type service struct {
+	*pubsub.Broker[Log]
+	q db.Querier
+}
+
+// NewService creates a new logging service
+func NewService(q db.Querier) Service {
+	broker := pubsub.NewBroker[Log]()
+	return &service{
+		Broker: broker,
+		q:      q,
+	}
+}
+
+// Create adds a new log entry to the database
+func (s *service) Create(ctx context.Context, log Log) error {
+	// Generate ID if not provided
+	if log.ID == "" {
+		log.ID = uuid.New().String()
+	}
+
+	// Set timestamp if not provided
+	if log.Timestamp == 0 {
+		log.Timestamp = time.Now().Unix()
+	}
+
+	// Set created_at if not provided
+	if log.CreatedAt == 0 {
+		log.CreatedAt = time.Now().Unix()
+	}
+
+	// Convert attributes to JSON string
+	var attributesJSON sql.NullString
+	if len(log.Attributes) > 0 {
+		attributesBytes, err := json.Marshal(log.Attributes)
+		if err != nil {
+			return err
+		}
+		attributesJSON = sql.NullString{
+			String: string(attributesBytes),
+			Valid:  true,
+		}
+	}
+
+	// Convert session ID to SQL nullable string
+	var sessionID sql.NullString
+	if log.SessionID != "" {
+		sessionID = sql.NullString{
+			String: log.SessionID,
+			Valid:  true,
+		}
+	}
+
+	// Insert log into database
+	err := s.q.CreateLog(ctx, db.CreateLogParams{
+		ID:         log.ID,
+		SessionID:  sessionID,
+		Timestamp:  log.Timestamp,
+		Level:      log.Level,
+		Message:    log.Message,
+		Attributes: attributesJSON,
+		CreatedAt:  log.CreatedAt,
+	})
+
+	if err != nil {
+		return err
+	}
+
+	// Publish event
+	s.Publish(pubsub.CreatedEvent, log)
+	return nil
+}
+
+// ListBySession retrieves logs for a specific session
+func (s *service) ListBySession(ctx context.Context, sessionID string) ([]Log, error) {
+	dbLogs, err := s.q.ListLogsBySession(ctx, sql.NullString{
+		String: sessionID,
+		Valid:  true,
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	logs := make([]Log, len(dbLogs))
+	for i, dbLog := range dbLogs {
+		logs[i] = s.fromDBItem(dbLog)
+	}
+	return logs, nil
+}
+
+// ListAll retrieves all logs with a limit
+func (s *service) ListAll(ctx context.Context, limit int) ([]Log, error) {
+	dbLogs, err := s.q.ListAllLogs(ctx, int64(limit))
+	if err != nil {
+		return nil, err
+	}
+
+	logs := make([]Log, len(dbLogs))
+	for i, dbLog := range dbLogs {
+		logs[i] = s.fromDBItem(dbLog)
+	}
+	return logs, nil
+}
+
+// fromDBItem converts a database log item to a Log struct
+func (s *service) fromDBItem(item db.Log) Log {
+	log := Log{
+		ID:        item.ID,
+		Timestamp: item.Timestamp,
+		Level:     item.Level,
+		Message:   item.Message,
+		CreatedAt: item.CreatedAt,
+	}
+
+	// Convert session ID if valid
+	if item.SessionID.Valid {
+		log.SessionID = item.SessionID.String
+	}
+
+	// Parse attributes JSON if present
+	if item.Attributes.Valid {
+		attributes := make(map[string]string)
+		if err := json.Unmarshal([]byte(item.Attributes.String), &attributes); err == nil {
+			log.Attributes = attributes
+		} else {
+			// Initialize empty map if parsing fails
+			log.Attributes = make(map[string]string)
+		}
+	} else {
+		log.Attributes = make(map[string]string)
+	}
+
+	return log
+}

+ 11 - 57
internal/logging/writer.go

@@ -5,59 +5,19 @@ import (
 	"context"
 	"context"
 	"fmt"
 	"fmt"
 	"strings"
 	"strings"
-	"sync"
 	"time"
 	"time"
 
 
 	"github.com/go-logfmt/logfmt"
 	"github.com/go-logfmt/logfmt"
-	"github.com/opencode-ai/opencode/internal/pubsub"
+	"github.com/opencode-ai/opencode/internal/session"
 )
 )
 
 
-const (
-	// Maximum number of log messages to keep in memory
-	maxLogMessages = 1000
-)
-
-type LogData struct {
-	messages []LogMessage
-	*pubsub.Broker[LogMessage]
-	lock sync.Mutex
-}
-
-func (l *LogData) Add(msg LogMessage) {
-	l.lock.Lock()
-	defer l.lock.Unlock()
-	
-	// Add new message
-	l.messages = append(l.messages, msg)
-	
-	// Trim if exceeding max capacity
-	if len(l.messages) > maxLogMessages {
-		l.messages = l.messages[len(l.messages)-maxLogMessages:]
-	}
-	
-	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, maxLogMessages),
-	Broker:   pubsub.NewBroker[LogMessage](),
-}
-
 type writer struct{}
 type writer struct{}
 
 
 func (w *writer) Write(p []byte) (int, error) {
 func (w *writer) Write(p []byte) (int, error) {
 	d := logfmt.NewDecoder(bytes.NewReader(p))
 	d := logfmt.NewDecoder(bytes.NewReader(p))
 	for d.ScanRecord() {
 	for d.ScanRecord() {
-		msg := LogMessage{
-			ID:   fmt.Sprintf("%d", time.Now().UnixNano()),
-			Time: time.Now(),
-		}
+		msg := Log{}
+
 		for d.ScanKeyval() {
 		for d.ScanKeyval() {
 			switch string(d.Key()) {
 			switch string(d.Key()) {
 			case "time":
 			case "time":
@@ -65,19 +25,21 @@ func (w *writer) Write(p []byte) (int, error) {
 				if err != nil {
 				if err != nil {
 					return 0, fmt.Errorf("parsing time: %w", err)
 					return 0, fmt.Errorf("parsing time: %w", err)
 				}
 				}
-				msg.Time = parsed
+				msg.Timestamp = parsed.UnixMilli()
 			case "level":
 			case "level":
 				msg.Level = strings.ToLower(string(d.Value()))
 				msg.Level = strings.ToLower(string(d.Value()))
 			case "msg":
 			case "msg":
 				msg.Message = string(d.Value())
 				msg.Message = string(d.Value())
 			default:
 			default:
-				msg.Attributes = append(msg.Attributes, Attr{
-					Key:   string(d.Key()),
-					Value: string(d.Value()),
-				})
+				if msg.Attributes == nil {
+					msg.Attributes = make(map[string]string)
+				}
+				msg.Attributes[string(d.Key())] = string(d.Value())
 			}
 			}
 		}
 		}
-		defaultLogData.Add(msg)
+
+		msg.SessionID = session.CurrentSessionID()
+		Create(context.Background(), msg)
 	}
 	}
 	if d.Err() != nil {
 	if d.Err() != nil {
 		return 0, d.Err()
 		return 0, d.Err()
@@ -89,11 +51,3 @@ func NewWriter() *writer {
 	w := &writer{}
 	w := &writer{}
 	return w
 	return w
 }
 }
-
-func Subscribe(ctx context.Context) <-chan pubsub.Event[LogMessage] {
-	return defaultLogData.Subscribe(ctx)
-}
-
-func List() []LogMessage {
-	return defaultLogData.List()
-}

+ 18 - 16
internal/lsp/client.go

@@ -14,6 +14,8 @@ import (
 	"sync/atomic"
 	"sync/atomic"
 	"time"
 	"time"
 
 
+	"log/slog"
+
 	"github.com/opencode-ai/opencode/internal/config"
 	"github.com/opencode-ai/opencode/internal/config"
 	"github.com/opencode-ai/opencode/internal/logging"
 	"github.com/opencode-ai/opencode/internal/logging"
 	"github.com/opencode-ai/opencode/internal/lsp/protocol"
 	"github.com/opencode-ai/opencode/internal/lsp/protocol"
@@ -97,10 +99,10 @@ func NewClient(ctx context.Context, command string, args ...string) (*Client, er
 	go func() {
 	go func() {
 		scanner := bufio.NewScanner(stderr)
 		scanner := bufio.NewScanner(stderr)
 		for scanner.Scan() {
 		for scanner.Scan() {
-			logging.Info("LSP Server", "message", scanner.Text())
+			slog.Info("LSP Server", "message", scanner.Text())
 		}
 		}
 		if err := scanner.Err(); err != nil {
 		if err := scanner.Err(); err != nil {
-			logging.Error("Error reading LSP stderr", "error", err)
+			slog.Error("Error reading LSP stderr", "error", err)
 		}
 		}
 	}()
 	}()
 
 
@@ -301,7 +303,7 @@ func (c *Client) WaitForServerReady(ctx context.Context) error {
 	defer ticker.Stop()
 	defer ticker.Stop()
 
 
 	if cnf.DebugLSP {
 	if cnf.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
 	// Determine server type for specialized initialization
@@ -310,7 +312,7 @@ func (c *Client) WaitForServerReady(ctx context.Context) error {
 	// For TypeScript-like servers, we need to open some key files first
 	// For TypeScript-like servers, we need to open some key files first
 	if serverType == ServerTypeTypeScript {
 	if serverType == ServerTypeTypeScript {
 		if cnf.DebugLSP {
 		if cnf.DebugLSP {
-			logging.Debug("TypeScript-like server detected, opening key configuration files")
+			slog.Debug("TypeScript-like server detected, opening key configuration files")
 		}
 		}
 		c.openKeyConfigFiles(ctx)
 		c.openKeyConfigFiles(ctx)
 	}
 	}
@@ -327,15 +329,15 @@ func (c *Client) WaitForServerReady(ctx context.Context) error {
 				// Server responded successfully
 				// Server responded successfully
 				c.SetServerState(StateReady)
 				c.SetServerState(StateReady)
 				if cnf.DebugLSP {
 				if cnf.DebugLSP {
-					logging.Debug("LSP server is ready")
+					slog.Debug("LSP server is ready")
 				}
 				}
 				return nil
 				return nil
 			} else {
 			} else {
-				logging.Debug("LSP server not ready yet", "error", err, "serverType", serverType)
+				slog.Debug("LSP server not ready yet", "error", err, "serverType", serverType)
 			}
 			}
 
 
 			if cnf.DebugLSP {
 			if cnf.DebugLSP {
-				logging.Debug("LSP server not ready yet", "error", err, "serverType", serverType)
+				slog.Debug("LSP server not ready yet", "error", err, "serverType", serverType)
 			}
 			}
 		}
 		}
 	}
 	}
@@ -410,9 +412,9 @@ func (c *Client) openKeyConfigFiles(ctx context.Context) {
 		if _, err := os.Stat(file); err == nil {
 		if _, err := os.Stat(file); err == nil {
 			// File exists, try to open it
 			// File exists, try to open it
 			if err := c.OpenFile(ctx, file); err != nil {
 			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 {
 			} else {
-				logging.Debug("Opened key config file for initialization", "file", file)
+				slog.Debug("Opened key config file for initialization", "file", file)
 			}
 			}
 		}
 		}
 	}
 	}
@@ -488,7 +490,7 @@ func (c *Client) pingTypeScriptServer(ctx context.Context) error {
 		return nil
 		return nil
 	})
 	})
 	if err != 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
 	// Final fallback - just try a generic capability
@@ -528,7 +530,7 @@ func (c *Client) openTypeScriptFiles(ctx context.Context, workDir string) {
 			if err := c.OpenFile(ctx, path); err == nil {
 			if err := c.OpenFile(ctx, path); err == nil {
 				filesOpened++
 				filesOpened++
 				if cnf.DebugLSP {
 				if cnf.DebugLSP {
-					logging.Debug("Opened TypeScript file for initialization", "file", path)
+					slog.Debug("Opened TypeScript file for initialization", "file", path)
 				}
 				}
 			}
 			}
 		}
 		}
@@ -537,11 +539,11 @@ func (c *Client) openTypeScriptFiles(ctx context.Context, workDir string) {
 	})
 	})
 
 
 	if err != nil && cnf.DebugLSP {
 	if err != nil && cnf.DebugLSP {
-		logging.Debug("Error walking directory for TypeScript files", "error", err)
+		slog.Debug("Error walking directory for TypeScript files", "error", err)
 	}
 	}
 
 
 	if cnf.DebugLSP {
 	if cnf.DebugLSP {
-		logging.Debug("Opened TypeScript files for initialization", "count", filesOpened)
+		slog.Debug("Opened TypeScript files for initialization", "count", filesOpened)
 	}
 	}
 }
 }
 
 
@@ -691,7 +693,7 @@ func (c *Client) CloseFile(ctx context.Context, filepath string) error {
 	}
 	}
 
 
 	if cnf.DebugLSP {
 	if cnf.DebugLSP {
-		logging.Debug("Closing file", "file", filepath)
+		slog.Debug("Closing file", "file", filepath)
 	}
 	}
 	if err := c.Notify(ctx, "textDocument/didClose", params); err != nil {
 	if err := c.Notify(ctx, "textDocument/didClose", params); err != nil {
 		return err
 		return err
@@ -730,12 +732,12 @@ func (c *Client) CloseAllFiles(ctx context.Context) {
 	for _, filePath := range filesToClose {
 	for _, filePath := range filesToClose {
 		err := c.CloseFile(ctx, filePath)
 		err := c.CloseFile(ctx, filePath)
 		if err != nil && cnf.DebugLSP {
 		if err != nil && cnf.DebugLSP {
-			logging.Warn("Error closing file", "file", filePath, "error", err)
+			slog.Warn("Error closing file", "file", filePath, "error", err)
 		}
 		}
 	}
 	}
 
 
 	if cnf.DebugLSP {
 	if cnf.DebugLSP {
-		logging.Debug("Closed all files", "files", filesToClose)
+		slog.Debug("Closed all files", "files", filesToClose)
 	}
 	}
 }
 }
 
 

+ 6 - 7
internal/lsp/discovery/integration.go

@@ -4,7 +4,7 @@ import (
 	"fmt"
 	"fmt"
 
 
 	"github.com/opencode-ai/opencode/internal/config"
 	"github.com/opencode-ai/opencode/internal/config"
-	"github.com/opencode-ai/opencode/internal/logging"
+	"log/slog"
 )
 )
 
 
 // IntegrateLSPServers discovers languages and LSP servers and integrates them into the application configuration
 // IntegrateLSPServers discovers languages and LSP servers and integrates them into the application configuration
@@ -23,9 +23,9 @@ func IntegrateLSPServers(workingDir string) error {
 
 
 	// Always run language detection, but log differently for first run vs. subsequent runs
 	// Always run language detection, but log differently for first run vs. subsequent runs
 	if shouldInit || len(cfg.LSP) == 0 {
 	if shouldInit || len(cfg.LSP) == 0 {
-		logging.Info("Running initial LSP auto-discovery...")
+		slog.Info("Running initial LSP auto-discovery...")
 	} else {
 	} else {
-		logging.Debug("Running LSP auto-discovery to detect new languages...")
+		slog.Debug("Running LSP auto-discovery to detect new languages...")
 	}
 	}
 
 
 	// Configure LSP servers
 	// Configure LSP servers
@@ -38,7 +38,7 @@ func IntegrateLSPServers(workingDir string) error {
 	for langID, serverInfo := range servers {
 	for langID, serverInfo := range servers {
 		// Skip languages that already have a configured server
 		// Skip languages that already have a configured server
 		if _, exists := cfg.LSP[langID]; exists {
 		if _, exists := cfg.LSP[langID]; exists {
-			logging.Debug("LSP server already configured for language", "language", langID)
+			slog.Debug("LSP server already configured for language", "language", langID)
 			continue
 			continue
 		}
 		}
 
 
@@ -49,12 +49,12 @@ func IntegrateLSPServers(workingDir string) error {
 				Command:  serverInfo.Path,
 				Command:  serverInfo.Path,
 				Args:     serverInfo.Args,
 				Args:     serverInfo.Args,
 			}
 			}
-			logging.Info("Added LSP server to configuration",
+			slog.Info("Added LSP server to configuration",
 				"language", langID,
 				"language", langID,
 				"command", serverInfo.Command,
 				"command", serverInfo.Command,
 				"path", serverInfo.Path)
 				"path", serverInfo.Path)
 		} else {
 		} else {
-			logging.Warn("LSP server not available",
+			slog.Warn("LSP server not available",
 				"language", langID,
 				"language", langID,
 				"command", serverInfo.Command,
 				"command", serverInfo.Command,
 				"installCmd", serverInfo.InstallCmd)
 				"installCmd", serverInfo.InstallCmd)
@@ -63,4 +63,3 @@ func IntegrateLSPServers(workingDir string) error {
 
 
 	return nil
 	return nil
 }
 }
-

+ 5 - 4
internal/lsp/discovery/language.go

@@ -6,8 +6,8 @@ import (
 	"strings"
 	"strings"
 	"sync"
 	"sync"
 
 
-	"github.com/opencode-ai/opencode/internal/logging"
 	"github.com/opencode-ai/opencode/internal/lsp"
 	"github.com/opencode-ai/opencode/internal/lsp"
+	"log/slog"
 )
 )
 
 
 // LanguageInfo stores information about a detected language
 // LanguageInfo stores information about a detected language
@@ -206,9 +206,9 @@ func DetectLanguages(rootDir string) (map[string]LanguageInfo, error) {
 	// Log detected languages
 	// Log detected languages
 	for id, info := range languages {
 	for id, info := range languages {
 		if info.IsPrimary {
 		if info.IsPrimary {
-			logging.Debug("Detected primary language", "language", id, "files", info.FileCount, "projectFiles", len(info.ProjectFiles))
+			slog.Debug("Detected primary language", "language", id, "files", info.FileCount, "projectFiles", len(info.ProjectFiles))
 		} else {
 		} else {
-			logging.Debug("Detected secondary language", "language", id, "files", info.FileCount)
+			slog.Debug("Detected secondary language", "language", id, "files", info.FileCount)
 		}
 		}
 	}
 	}
 
 
@@ -295,4 +295,5 @@ func GetLanguageIDFromPath(path string) string {
 	uri := "file://" + path
 	uri := "file://" + path
 	langKind := lsp.DetectLanguageID(uri)
 	langKind := lsp.DetectLanguageID(uri)
 	return GetLanguageIDFromProtocol(string(langKind))
 	return GetLanguageIDFromProtocol(string(langKind))
-}
+}
+

+ 27 - 26
internal/lsp/discovery/server.go

@@ -8,7 +8,7 @@ import (
 	"runtime"
 	"runtime"
 	"strings"
 	"strings"
 
 
-	"github.com/opencode-ai/opencode/internal/logging"
+	"log/slog"
 )
 )
 
 
 // ServerInfo contains information about an LSP server
 // ServerInfo contains information about an LSP server
@@ -114,7 +114,7 @@ func FindLSPServer(languageID string) (ServerInfo, error) {
 	if err == nil {
 	if err == nil {
 		serverInfo.Available = true
 		serverInfo.Available = true
 		serverInfo.Path = path
 		serverInfo.Path = path
-		logging.Debug("Found LSP server in PATH", "language", languageID, "command", serverInfo.Command, "path", path)
+		slog.Debug("Found LSP server in PATH", "language", languageID, "command", serverInfo.Command, "path", path)
 		return serverInfo, nil
 		return serverInfo, nil
 	}
 	}
 
 
@@ -125,13 +125,13 @@ func FindLSPServer(languageID string) (ServerInfo, error) {
 			// Found the server
 			// Found the server
 			serverInfo.Available = true
 			serverInfo.Available = true
 			serverInfo.Path = searchPath
 			serverInfo.Path = searchPath
-			logging.Debug("Found LSP server in common location", "language", languageID, "command", serverInfo.Command, "path", searchPath)
+			slog.Debug("Found LSP server in common location", "language", languageID, "command", serverInfo.Command, "path", searchPath)
 			return serverInfo, nil
 			return serverInfo, nil
 		}
 		}
 	}
 	}
 
 
 	// Server not found
 	// Server not found
-	logging.Debug("LSP server not found", "language", languageID, "command", serverInfo.Command)
+	slog.Debug("LSP server not found", "language", languageID, "command", serverInfo.Command)
 	return serverInfo, fmt.Errorf("LSP server for %s not found. Install with: %s", languageID, serverInfo.InstallCmd)
 	return serverInfo, fmt.Errorf("LSP server for %s not found. Install with: %s", languageID, serverInfo.InstallCmd)
 }
 }
 
 
@@ -140,7 +140,7 @@ func getCommonLSPPaths(languageID, command string) []string {
 	var paths []string
 	var paths []string
 	homeDir, err := os.UserHomeDir()
 	homeDir, err := os.UserHomeDir()
 	if err != nil {
 	if err != nil {
-		logging.Error("Failed to get user home directory", "error", err)
+		slog.Error("Failed to get user home directory", "error", err)
 		return paths
 		return paths
 	}
 	}
 
 
@@ -148,21 +148,21 @@ func getCommonLSPPaths(languageID, command string) []string {
 	switch runtime.GOOS {
 	switch runtime.GOOS {
 	case "darwin":
 	case "darwin":
 		// macOS paths
 		// macOS paths
-		paths = append(paths, 
+		paths = append(paths,
 			fmt.Sprintf("/usr/local/bin/%s", command),
 			fmt.Sprintf("/usr/local/bin/%s", command),
 			fmt.Sprintf("/opt/homebrew/bin/%s", command),
 			fmt.Sprintf("/opt/homebrew/bin/%s", command),
 			fmt.Sprintf("%s/.local/bin/%s", homeDir, command),
 			fmt.Sprintf("%s/.local/bin/%s", homeDir, command),
 		)
 		)
 	case "linux":
 	case "linux":
 		// Linux paths
 		// Linux paths
-		paths = append(paths, 
+		paths = append(paths,
 			fmt.Sprintf("/usr/bin/%s", command),
 			fmt.Sprintf("/usr/bin/%s", command),
 			fmt.Sprintf("/usr/local/bin/%s", command),
 			fmt.Sprintf("/usr/local/bin/%s", command),
 			fmt.Sprintf("%s/.local/bin/%s", homeDir, command),
 			fmt.Sprintf("%s/.local/bin/%s", homeDir, command),
 		)
 		)
 	case "windows":
 	case "windows":
 		// Windows paths
 		// Windows paths
-		paths = append(paths, 
+		paths = append(paths,
 			fmt.Sprintf("%s\\AppData\\Local\\Programs\\%s.exe", homeDir, command),
 			fmt.Sprintf("%s\\AppData\\Local\\Programs\\%s.exe", homeDir, command),
 			fmt.Sprintf("C:\\Program Files\\%s\\bin\\%s.exe", command, command),
 			fmt.Sprintf("C:\\Program Files\\%s\\bin\\%s.exe", command, command),
 		)
 		)
@@ -182,12 +182,12 @@ func getCommonLSPPaths(languageID, command string) []string {
 	case "typescript", "javascript", "html", "css", "json", "yaml", "php":
 	case "typescript", "javascript", "html", "css", "json", "yaml", "php":
 		// Node.js global packages
 		// Node.js global packages
 		if runtime.GOOS == "windows" {
 		if runtime.GOOS == "windows" {
-			paths = append(paths, 
+			paths = append(paths,
 				fmt.Sprintf("%s\\AppData\\Roaming\\npm\\%s.cmd", homeDir, command),
 				fmt.Sprintf("%s\\AppData\\Roaming\\npm\\%s.cmd", homeDir, command),
 				fmt.Sprintf("%s\\AppData\\Roaming\\npm\\node_modules\\.bin\\%s.cmd", homeDir, command),
 				fmt.Sprintf("%s\\AppData\\Roaming\\npm\\node_modules\\.bin\\%s.cmd", homeDir, command),
 			)
 			)
 		} else {
 		} else {
-			paths = append(paths, 
+			paths = append(paths,
 				fmt.Sprintf("%s/.npm-global/bin/%s", homeDir, command),
 				fmt.Sprintf("%s/.npm-global/bin/%s", homeDir, command),
 				fmt.Sprintf("%s/.nvm/versions/node/*/bin/%s", homeDir, command),
 				fmt.Sprintf("%s/.nvm/versions/node/*/bin/%s", homeDir, command),
 				fmt.Sprintf("/usr/local/lib/node_modules/.bin/%s", command),
 				fmt.Sprintf("/usr/local/lib/node_modules/.bin/%s", command),
@@ -196,12 +196,12 @@ func getCommonLSPPaths(languageID, command string) []string {
 	case "python":
 	case "python":
 		// Python paths
 		// Python paths
 		if runtime.GOOS == "windows" {
 		if runtime.GOOS == "windows" {
-			paths = append(paths, 
+			paths = append(paths,
 				fmt.Sprintf("%s\\AppData\\Local\\Programs\\Python\\Python*\\Scripts\\%s.exe", homeDir, command),
 				fmt.Sprintf("%s\\AppData\\Local\\Programs\\Python\\Python*\\Scripts\\%s.exe", homeDir, command),
 				fmt.Sprintf("C:\\Python*\\Scripts\\%s.exe", command),
 				fmt.Sprintf("C:\\Python*\\Scripts\\%s.exe", command),
 			)
 			)
 		} else {
 		} else {
-			paths = append(paths, 
+			paths = append(paths,
 				fmt.Sprintf("%s/.local/bin/%s", homeDir, command),
 				fmt.Sprintf("%s/.local/bin/%s", homeDir, command),
 				fmt.Sprintf("%s/.pyenv/shims/%s", homeDir, command),
 				fmt.Sprintf("%s/.pyenv/shims/%s", homeDir, command),
 				fmt.Sprintf("/usr/local/bin/%s", command),
 				fmt.Sprintf("/usr/local/bin/%s", command),
@@ -210,12 +210,12 @@ func getCommonLSPPaths(languageID, command string) []string {
 	case "rust":
 	case "rust":
 		// Rust paths
 		// Rust paths
 		if runtime.GOOS == "windows" {
 		if runtime.GOOS == "windows" {
-			paths = append(paths, 
+			paths = append(paths,
 				fmt.Sprintf("%s\\.rustup\\toolchains\\*\\bin\\%s.exe", homeDir, command),
 				fmt.Sprintf("%s\\.rustup\\toolchains\\*\\bin\\%s.exe", homeDir, command),
 				fmt.Sprintf("%s\\.cargo\\bin\\%s.exe", homeDir, command),
 				fmt.Sprintf("%s\\.cargo\\bin\\%s.exe", homeDir, command),
 			)
 			)
 		} else {
 		} else {
-			paths = append(paths, 
+			paths = append(paths,
 				fmt.Sprintf("%s/.rustup/toolchains/*/bin/%s", homeDir, command),
 				fmt.Sprintf("%s/.rustup/toolchains/*/bin/%s", homeDir, command),
 				fmt.Sprintf("%s/.cargo/bin/%s", homeDir, command),
 				fmt.Sprintf("%s/.cargo/bin/%s", homeDir, command),
 			)
 			)
@@ -248,7 +248,7 @@ func getCommonLSPPaths(languageID, command string) []string {
 // getVSCodeExtensionsPath returns the path to VSCode extensions directory
 // getVSCodeExtensionsPath returns the path to VSCode extensions directory
 func getVSCodeExtensionsPath(homeDir string) string {
 func getVSCodeExtensionsPath(homeDir string) string {
 	var basePath string
 	var basePath string
-	
+
 	switch runtime.GOOS {
 	switch runtime.GOOS {
 	case "darwin":
 	case "darwin":
 		basePath = filepath.Join(homeDir, "Library", "Application Support", "Code", "User", "globalStorage")
 		basePath = filepath.Join(homeDir, "Library", "Application Support", "Code", "User", "globalStorage")
@@ -259,12 +259,12 @@ func getVSCodeExtensionsPath(homeDir string) string {
 	default:
 	default:
 		return ""
 		return ""
 	}
 	}
-	
+
 	// Check if the directory exists
 	// Check if the directory exists
 	if _, err := os.Stat(basePath); err != nil {
 	if _, err := os.Stat(basePath); err != nil {
 		return ""
 		return ""
 	}
 	}
-	
+
 	return basePath
 	return basePath
 }
 }
 
 
@@ -275,32 +275,33 @@ func ConfigureLSPServers(rootDir string) (map[string]ServerInfo, error) {
 	if err != nil {
 	if err != nil {
 		return nil, fmt.Errorf("failed to detect languages: %w", err)
 		return nil, fmt.Errorf("failed to detect languages: %w", err)
 	}
 	}
-	
+
 	// Find LSP servers for detected languages
 	// Find LSP servers for detected languages
 	servers := make(map[string]ServerInfo)
 	servers := make(map[string]ServerInfo)
 	for langID, langInfo := range languages {
 	for langID, langInfo := range languages {
 		// Prioritize primary languages but include all languages that have server definitions
 		// Prioritize primary languages but include all languages that have server definitions
 		if !langInfo.IsPrimary && langInfo.FileCount < 3 {
 		if !langInfo.IsPrimary && langInfo.FileCount < 3 {
 			// Skip non-primary languages with very few files
 			// Skip non-primary languages with very few files
-			logging.Debug("Skipping non-primary language with few files", "language", langID, "files", langInfo.FileCount)
+			slog.Debug("Skipping non-primary language with few files", "language", langID, "files", langInfo.FileCount)
 			continue
 			continue
 		}
 		}
-		
+
 		// Check if we have a server for this language
 		// Check if we have a server for this language
 		serverInfo, err := FindLSPServer(langID)
 		serverInfo, err := FindLSPServer(langID)
 		if err != nil {
 		if err != nil {
-			logging.Warn("LSP server not found", "language", langID, "error", err)
+			slog.Warn("LSP server not found", "language", langID, "error", err)
 			continue
 			continue
 		}
 		}
-		
+
 		// Add to the map of configured servers
 		// Add to the map of configured servers
 		servers[langID] = serverInfo
 		servers[langID] = serverInfo
 		if langInfo.IsPrimary {
 		if langInfo.IsPrimary {
-			logging.Info("Configured LSP server for primary language", "language", langID, "command", serverInfo.Command, "path", serverInfo.Path)
+			slog.Info("Configured LSP server for primary language", "language", langID, "command", serverInfo.Command, "path", serverInfo.Path)
 		} else {
 		} else {
-			logging.Info("Configured LSP server for secondary language", "language", langID, "command", serverInfo.Command, "path", serverInfo.Path)
+			slog.Info("Configured LSP server for secondary language", "language", langID, "command", serverInfo.Command, "path", serverInfo.Path)
 		}
 		}
 	}
 	}
-	
+
 	return servers, nil
 	return servers, nil
-}
+}
+

+ 7 - 7
internal/lsp/handlers.go

@@ -4,9 +4,9 @@ import (
 	"encoding/json"
 	"encoding/json"
 
 
 	"github.com/opencode-ai/opencode/internal/config"
 	"github.com/opencode-ai/opencode/internal/config"
-	"github.com/opencode-ai/opencode/internal/logging"
 	"github.com/opencode-ai/opencode/internal/lsp/protocol"
 	"github.com/opencode-ai/opencode/internal/lsp/protocol"
 	"github.com/opencode-ai/opencode/internal/lsp/util"
 	"github.com/opencode-ai/opencode/internal/lsp/util"
+	"log/slog"
 )
 )
 
 
 // Requests
 // Requests
@@ -18,7 +18,7 @@ func HandleWorkspaceConfiguration(params json.RawMessage) (any, error) {
 func HandleRegisterCapability(params json.RawMessage) (any, error) {
 func HandleRegisterCapability(params json.RawMessage) (any, error) {
 	var registerParams protocol.RegistrationParams
 	var registerParams protocol.RegistrationParams
 	if err := json.Unmarshal(params, &registerParams); err != nil {
 	if err := json.Unmarshal(params, &registerParams); err != nil {
-		logging.Error("Error unmarshaling registration params", "error", err)
+		slog.Error("Error unmarshaling registration params", "error", err)
 		return nil, err
 		return nil, err
 	}
 	}
 
 
@@ -28,13 +28,13 @@ func HandleRegisterCapability(params json.RawMessage) (any, error) {
 			// Parse the registration options
 			// Parse the registration options
 			optionsJSON, err := json.Marshal(reg.RegisterOptions)
 			optionsJSON, err := json.Marshal(reg.RegisterOptions)
 			if err != nil {
 			if err != nil {
-				logging.Error("Error marshaling registration options", "error", err)
+				slog.Error("Error marshaling registration options", "error", err)
 				continue
 				continue
 			}
 			}
 
 
 			var options protocol.DidChangeWatchedFilesRegistrationOptions
 			var options protocol.DidChangeWatchedFilesRegistrationOptions
 			if err := json.Unmarshal(optionsJSON, &options); err != nil {
 			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
 				continue
 			}
 			}
 
 
@@ -54,7 +54,7 @@ func HandleApplyEdit(params json.RawMessage) (any, error) {
 
 
 	err := util.ApplyWorkspaceEdit(edit.Edit)
 	err := util.ApplyWorkspaceEdit(edit.Edit)
 	if err != nil {
 	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
 		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 err := json.Unmarshal(params, &msg); err == nil {
 		if cnf.DebugLSP {
 		if cnf.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) {
 func HandleDiagnostics(client *Client, params json.RawMessage) {
 	var diagParams protocol.PublishDiagnosticsParams
 	var diagParams protocol.PublishDiagnosticsParams
 	if err := json.Unmarshal(params, &diagParams); err != nil {
 	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
 		return
 	}
 	}
 
 

+ 16 - 16
internal/lsp/transport.go

@@ -9,7 +9,7 @@ import (
 	"strings"
 	"strings"
 
 
 	"github.com/opencode-ai/opencode/internal/config"
 	"github.com/opencode-ai/opencode/internal/config"
-	"github.com/opencode-ai/opencode/internal/logging"
+	"log/slog"
 )
 )
 
 
 // Write writes an LSP message to the given writer
 // Write writes an LSP message to the given writer
@@ -21,7 +21,7 @@ func WriteMessage(w io.Writer, msg *Message) error {
 	cnf := config.Get()
 	cnf := config.Get()
 
 
 	if cnf.DebugLSP {
 	if cnf.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))
 	_, 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)
 		line = strings.TrimSpace(line)
 
 
 		if cnf.DebugLSP {
 		if cnf.DebugLSP {
-			logging.Debug("Received header", "line", line)
+			slog.Debug("Received header", "line", line)
 		}
 		}
 
 
 		if line == "" {
 		if line == "" {
@@ -66,7 +66,7 @@ func ReadMessage(r *bufio.Reader) (*Message, error) {
 	}
 	}
 
 
 	if cnf.DebugLSP {
 	if cnf.DebugLSP {
-		logging.Debug("Content-Length", "length", contentLength)
+		slog.Debug("Content-Length", "length", contentLength)
 	}
 	}
 
 
 	// Read content
 	// Read content
@@ -77,7 +77,7 @@ func ReadMessage(r *bufio.Reader) (*Message, error) {
 	}
 	}
 
 
 	if cnf.DebugLSP {
 	if cnf.DebugLSP {
-		logging.Debug("Received content", "content", string(content))
+		slog.Debug("Received content", "content", string(content))
 	}
 	}
 
 
 	// Parse message
 	// Parse message
@@ -96,7 +96,7 @@ func (c *Client) handleMessages() {
 		msg, err := ReadMessage(c.stdout)
 		msg, err := ReadMessage(c.stdout)
 		if err != nil {
 		if err != nil {
 			if cnf.DebugLSP {
 			if cnf.DebugLSP {
-				logging.Error("Error reading message", "error", err)
+				slog.Error("Error reading message", "error", err)
 			}
 			}
 			return
 			return
 		}
 		}
@@ -104,7 +104,7 @@ func (c *Client) handleMessages() {
 		// Handle server->client request (has both Method and ID)
 		// Handle server->client request (has both Method and ID)
 		if msg.Method != "" && msg.ID != 0 {
 		if msg.Method != "" && msg.ID != 0 {
 			if cnf.DebugLSP {
 			if cnf.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{
 			response := &Message{
@@ -144,7 +144,7 @@ func (c *Client) handleMessages() {
 
 
 			// Send response back to server
 			// Send response back to server
 			if err := WriteMessage(c.stdin, response); err != nil {
 			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
 			continue
@@ -158,11 +158,11 @@ func (c *Client) handleMessages() {
 
 
 			if ok {
 			if ok {
 				if cnf.DebugLSP {
 				if cnf.DebugLSP {
-					logging.Debug("Handling notification", "method", msg.Method)
+					slog.Debug("Handling notification", "method", msg.Method)
 				}
 				}
 				go handler(msg.Params)
 				go handler(msg.Params)
 			} else if cnf.DebugLSP {
 			} else if cnf.DebugLSP {
-				logging.Debug("No handler for notification", "method", msg.Method)
+				slog.Debug("No handler for notification", "method", msg.Method)
 			}
 			}
 			continue
 			continue
 		}
 		}
@@ -175,12 +175,12 @@ func (c *Client) handleMessages() {
 
 
 			if ok {
 			if ok {
 				if cnf.DebugLSP {
 				if cnf.DebugLSP {
-					logging.Debug("Received response for request", "id", msg.ID)
+					slog.Debug("Received response for request", "id", msg.ID)
 				}
 				}
 				ch <- msg
 				ch <- msg
 				close(ch)
 				close(ch)
 			} else if cnf.DebugLSP {
 			} else if cnf.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)
 	id := c.nextID.Add(1)
 
 
 	if cnf.DebugLSP {
 	if cnf.DebugLSP {
-		logging.Debug("Making call", "method", method, "id", id)
+		slog.Debug("Making call", "method", method, "id", id)
 	}
 	}
 
 
 	msg, err := NewRequest(id, method, params)
 	msg, err := NewRequest(id, method, params)
@@ -218,14 +218,14 @@ func (c *Client) Call(ctx context.Context, method string, params any, result any
 	}
 	}
 
 
 	if cnf.DebugLSP {
 	if cnf.DebugLSP {
-		logging.Debug("Request sent", "method", method, "id", id)
+		slog.Debug("Request sent", "method", method, "id", id)
 	}
 	}
 
 
 	// Wait for response
 	// Wait for response
 	resp := <-ch
 	resp := <-ch
 
 
 	if cnf.DebugLSP {
 	if cnf.DebugLSP {
-		logging.Debug("Received response", "id", id)
+		slog.Debug("Received response", "id", id)
 	}
 	}
 
 
 	if resp.Error != nil {
 	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 {
 func (c *Client) Notify(ctx context.Context, method string, params any) error {
 	cnf := config.Get()
 	cnf := config.Get()
 	if cnf.DebugLSP {
 	if cnf.DebugLSP {
-		logging.Debug("Sending notification", "method", method)
+		slog.Debug("Sending notification", "method", method)
 	}
 	}
 
 
 	msg, err := NewNotification(method, params)
 	msg, err := NewNotification(method, params)

+ 56 - 56
internal/lsp/watcher/watcher.go

@@ -13,9 +13,9 @@ import (
 	"github.com/bmatcuk/doublestar/v4"
 	"github.com/bmatcuk/doublestar/v4"
 	"github.com/fsnotify/fsnotify"
 	"github.com/fsnotify/fsnotify"
 	"github.com/opencode-ai/opencode/internal/config"
 	"github.com/opencode-ai/opencode/internal/config"
-	"github.com/opencode-ai/opencode/internal/logging"
 	"github.com/opencode-ai/opencode/internal/lsp"
 	"github.com/opencode-ai/opencode/internal/lsp"
 	"github.com/opencode-ai/opencode/internal/lsp/protocol"
 	"github.com/opencode-ai/opencode/internal/lsp/protocol"
+	"log/slog"
 )
 )
 
 
 // WorkspaceWatcher manages LSP file watching
 // WorkspaceWatcher manages LSP file watching
@@ -46,7 +46,7 @@ func NewWorkspaceWatcher(client *lsp.Client) *WorkspaceWatcher {
 func (w *WorkspaceWatcher) AddRegistrations(ctx context.Context, id string, watchers []protocol.FileSystemWatcher) {
 func (w *WorkspaceWatcher) AddRegistrations(ctx context.Context, id string, watchers []protocol.FileSystemWatcher) {
 	cnf := config.Get()
 	cnf := config.Get()
 
 
-	logging.Debug("Adding file watcher registrations")
+	slog.Debug("Adding file watcher registrations")
 	w.registrationMu.Lock()
 	w.registrationMu.Lock()
 	defer w.registrationMu.Unlock()
 	defer w.registrationMu.Unlock()
 
 
@@ -55,33 +55,33 @@ func (w *WorkspaceWatcher) AddRegistrations(ctx context.Context, id string, watc
 
 
 	// Print detailed registration information for debugging
 	// Print detailed registration information for debugging
 	if cnf.DebugLSP {
 	if cnf.DebugLSP {
-		logging.Debug("Adding file watcher registrations",
+		slog.Debug("Adding file watcher registrations",
 			"id", id,
 			"id", id,
 			"watchers", len(watchers),
 			"watchers", len(watchers),
 			"total", len(w.registrations),
 			"total", len(w.registrations),
 		)
 		)
 
 
 		for i, watcher := range watchers {
 		for i, watcher := range watchers {
-			logging.Debug("Registration", "index", i+1)
+			slog.Debug("Registration", "index", i+1)
 
 
 			// Log the GlobPattern
 			// Log the GlobPattern
 			switch v := watcher.GlobPattern.Value.(type) {
 			switch v := watcher.GlobPattern.Value.(type) {
 			case string:
 			case string:
-				logging.Debug("GlobPattern", "pattern", v)
+				slog.Debug("GlobPattern", "pattern", v)
 			case protocol.RelativePattern:
 			case protocol.RelativePattern:
-				logging.Debug("GlobPattern", "pattern", v.Pattern)
+				slog.Debug("GlobPattern", "pattern", v.Pattern)
 
 
 				// Log BaseURI details
 				// Log BaseURI details
 				switch u := v.BaseURI.Value.(type) {
 				switch u := v.BaseURI.Value.(type) {
 				case string:
 				case string:
-					logging.Debug("BaseURI", "baseURI", u)
+					slog.Debug("BaseURI", "baseURI", u)
 				case protocol.DocumentUri:
 				case protocol.DocumentUri:
-					logging.Debug("BaseURI", "baseURI", u)
+					slog.Debug("BaseURI", "baseURI", u)
 				default:
 				default:
-					logging.Debug("BaseURI", "baseURI", u)
+					slog.Debug("BaseURI", "baseURI", u)
 				}
 				}
 			default:
 			default:
-				logging.Debug("GlobPattern", "unknown type", fmt.Sprintf("%T", v))
+				slog.Debug("GlobPattern", "unknown type", fmt.Sprintf("%T", v))
 			}
 			}
 
 
 			// Log WatchKind
 			// Log WatchKind
@@ -90,13 +90,13 @@ func (w *WorkspaceWatcher) AddRegistrations(ctx context.Context, id string, watc
 				watchKind = *watcher.Kind
 				watchKind = *watcher.Kind
 			}
 			}
 
 
-			logging.Debug("WatchKind", "kind", watchKind)
+			slog.Debug("WatchKind", "kind", watchKind)
 		}
 		}
 	}
 	}
 
 
 	// Determine server type for specialized handling
 	// Determine server type for specialized handling
 	serverName := getServerNameFromContext(ctx)
 	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
 	// Check if this server has sent file watchers
 	hasFileWatchers := len(watchers) > 0
 	hasFileWatchers := len(watchers) > 0
@@ -124,7 +124,7 @@ func (w *WorkspaceWatcher) AddRegistrations(ctx context.Context, id string, watc
 			filesOpened += highPriorityFilesOpened
 			filesOpened += highPriorityFilesOpened
 
 
 			if cnf.DebugLSP {
 			if cnf.DebugLSP {
-				logging.Debug("Opened high-priority files",
+				slog.Debug("Opened high-priority files",
 					"count", highPriorityFilesOpened,
 					"count", highPriorityFilesOpened,
 					"serverName", serverName)
 					"serverName", serverName)
 			}
 			}
@@ -132,7 +132,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 we've already opened enough high-priority files, we might not need more
 			if filesOpened >= maxFilesToOpen {
 			if filesOpened >= maxFilesToOpen {
 				if cnf.DebugLSP {
 				if cnf.DebugLSP {
-					logging.Debug("Reached file limit with high-priority files",
+					slog.Debug("Reached file limit with high-priority files",
 						"filesOpened", filesOpened,
 						"filesOpened", filesOpened,
 						"maxFiles", maxFilesToOpen)
 						"maxFiles", maxFilesToOpen)
 				}
 				}
@@ -150,7 +150,7 @@ func (w *WorkspaceWatcher) AddRegistrations(ctx context.Context, id string, watc
 				if d.IsDir() {
 				if d.IsDir() {
 					if path != w.workspacePath && shouldExcludeDir(path) {
 					if path != w.workspacePath && shouldExcludeDir(path) {
 						if cnf.DebugLSP {
 						if cnf.DebugLSP {
-							logging.Debug("Skipping excluded directory", "path", path)
+							slog.Debug("Skipping excluded directory", "path", path)
 						}
 						}
 						return filepath.SkipDir
 						return filepath.SkipDir
 					}
 					}
@@ -178,7 +178,7 @@ func (w *WorkspaceWatcher) AddRegistrations(ctx context.Context, id string, watc
 
 
 			elapsedTime := time.Since(startTime)
 			elapsedTime := time.Since(startTime)
 			if cnf.DebugLSP {
 			if cnf.DebugLSP {
-				logging.Debug("Limited workspace scan complete",
+				slog.Debug("Limited workspace scan complete",
 					"filesOpened", filesOpened,
 					"filesOpened", filesOpened,
 					"maxFiles", maxFilesToOpen,
 					"maxFiles", maxFilesToOpen,
 					"elapsedTime", elapsedTime.Seconds(),
 					"elapsedTime", elapsedTime.Seconds(),
@@ -187,11 +187,11 @@ func (w *WorkspaceWatcher) AddRegistrations(ctx context.Context, id string, watc
 			}
 			}
 
 
 			if err != nil && cnf.DebugLSP {
 			if err != nil && cnf.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 cnf.DebugLSP {
 	} else if cnf.DebugLSP {
-		logging.Debug("Using on-demand file loading for server", "server", serverName)
+		slog.Debug("Using on-demand file loading for server", "server", serverName)
 	}
 	}
 }
 }
 
 
@@ -264,7 +264,7 @@ func (w *WorkspaceWatcher) openHighPriorityFiles(ctx context.Context, serverName
 		matches, err := doublestar.Glob(os.DirFS(w.workspacePath), pattern)
 		matches, err := doublestar.Glob(os.DirFS(w.workspacePath), pattern)
 		if err != nil {
 		if err != nil {
 			if cnf.DebugLSP {
 			if cnf.DebugLSP {
-				logging.Debug("Error finding high-priority files", "pattern", pattern, "error", err)
+				slog.Debug("Error finding high-priority files", "pattern", pattern, "error", err)
 			}
 			}
 			continue
 			continue
 		}
 		}
@@ -282,12 +282,12 @@ func (w *WorkspaceWatcher) openHighPriorityFiles(ctx context.Context, serverName
 			// Open the file
 			// Open the file
 			if err := w.client.OpenFile(ctx, fullPath); err != nil {
 			if err := w.client.OpenFile(ctx, fullPath); err != nil {
 				if cnf.DebugLSP {
 				if cnf.DebugLSP {
-					logging.Debug("Error opening high-priority file", "path", fullPath, "error", err)
+					slog.Debug("Error opening high-priority file", "path", fullPath, "error", err)
 				}
 				}
 			} else {
 			} else {
 				filesOpened++
 				filesOpened++
 				if cnf.DebugLSP {
 				if cnf.DebugLSP {
-					logging.Debug("Opened high-priority file", "path", fullPath)
+					slog.Debug("Opened high-priority file", "path", fullPath)
 				}
 				}
 			}
 			}
 
 
@@ -319,7 +319,7 @@ func (w *WorkspaceWatcher) WatchWorkspace(ctx context.Context, workspacePath str
 	}
 	}
 
 
 	serverName := getServerNameFromContext(ctx)
 	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
 	// Register handler for file watcher registrations from the server
 	lsp.RegisterFileWatchHandler(func(id string, watchers []protocol.FileSystemWatcher) {
 	lsp.RegisterFileWatchHandler(func(id string, watchers []protocol.FileSystemWatcher) {
@@ -328,7 +328,7 @@ func (w *WorkspaceWatcher) WatchWorkspace(ctx context.Context, workspacePath str
 
 
 	watcher, err := fsnotify.NewWatcher()
 	watcher, err := fsnotify.NewWatcher()
 	if err != nil {
 	if err != nil {
-		logging.Error("Error creating watcher", "error", err)
+		slog.Error("Error creating watcher", "error", err)
 	}
 	}
 	defer watcher.Close()
 	defer watcher.Close()
 
 
@@ -342,7 +342,7 @@ func (w *WorkspaceWatcher) WatchWorkspace(ctx context.Context, workspacePath str
 		if d.IsDir() && path != workspacePath {
 		if d.IsDir() && path != workspacePath {
 			if shouldExcludeDir(path) {
 			if shouldExcludeDir(path) {
 				if cnf.DebugLSP {
 				if cnf.DebugLSP {
-					logging.Debug("Skipping excluded directory", "path", path)
+					slog.Debug("Skipping excluded directory", "path", path)
 				}
 				}
 				return filepath.SkipDir
 				return filepath.SkipDir
 			}
 			}
@@ -352,14 +352,14 @@ func (w *WorkspaceWatcher) WatchWorkspace(ctx context.Context, workspacePath str
 		if d.IsDir() {
 		if d.IsDir() {
 			err = watcher.Add(path)
 			err = watcher.Add(path)
 			if err != nil {
 			if err != nil {
-				logging.Error("Error watching path", "path", path, "error", err)
+				slog.Error("Error watching path", "path", path, "error", err)
 			}
 			}
 		}
 		}
 
 
 		return nil
 		return nil
 	})
 	})
 	if err != nil {
 	if err != nil {
-		logging.Error("Error walking workspace", "error", err)
+		slog.Error("Error walking workspace", "error", err)
 	}
 	}
 
 
 	// Event loop
 	// Event loop
@@ -381,18 +381,18 @@ func (w *WorkspaceWatcher) WatchWorkspace(ctx context.Context, workspacePath str
 				if err != nil {
 				if err != nil {
 					if os.IsNotExist(err) {
 					if os.IsNotExist(err) {
 						// File was deleted between event and processing - ignore
 						// File was deleted between event and processing - ignore
-						logging.Debug("File deleted between create event and stat", "path", event.Name)
+						slog.Debug("File deleted between create event and stat", "path", event.Name)
 						continue
 						continue
 					}
 					}
-					logging.Error("Error getting file info", "path", event.Name, "error", err)
+					slog.Error("Error getting file info", "path", event.Name, "error", err)
 					continue
 					continue
 				}
 				}
-				
+
 				if info.IsDir() {
 				if info.IsDir() {
 					// Skip excluded directories
 					// Skip excluded directories
 					if !shouldExcludeDir(event.Name) {
 					if !shouldExcludeDir(event.Name) {
 						if err := watcher.Add(event.Name); err != nil {
 						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 {
 				} else {
@@ -406,7 +406,7 @@ func (w *WorkspaceWatcher) WatchWorkspace(ctx context.Context, workspacePath str
 			// Debug logging
 			// Debug logging
 			if cnf.DebugLSP {
 			if cnf.DebugLSP {
 				matched, kind := w.isPathWatched(event.Name)
 				matched, kind := w.isPathWatched(event.Name)
-				logging.Debug("File event",
+				slog.Debug("File event",
 					"path", event.Name,
 					"path", event.Name,
 					"operation", event.Op.String(),
 					"operation", event.Op.String(),
 					"watched", matched,
 					"watched", matched,
@@ -427,7 +427,7 @@ func (w *WorkspaceWatcher) WatchWorkspace(ctx context.Context, workspacePath str
 					// Just send the notification if needed
 					// Just send the notification if needed
 					info, err := os.Stat(event.Name)
 					info, err := os.Stat(event.Name)
 					if err != nil {
 					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
 						return
 					}
 					}
 					if !info.IsDir() && watchKind&protocol.WatchCreate != 0 {
 					if !info.IsDir() && watchKind&protocol.WatchCreate != 0 {
@@ -455,7 +455,7 @@ func (w *WorkspaceWatcher) WatchWorkspace(ctx context.Context, workspacePath str
 			if !ok {
 			if !ok {
 				return
 				return
 			}
 			}
-			logging.Error("Error watching file", "error", err)
+			slog.Error("Error watching file", "error", err)
 		}
 		}
 	}
 	}
 }
 }
@@ -580,7 +580,7 @@ func matchesSimpleGlob(pattern, path string) bool {
 	// Fall back to simple matching for simpler patterns
 	// Fall back to simple matching for simpler patterns
 	matched, err := filepath.Match(pattern, path)
 	matched, err := filepath.Match(pattern, path)
 	if err != nil {
 	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
 		return false
 	}
 	}
 
 
@@ -591,7 +591,7 @@ func matchesSimpleGlob(pattern, path string) bool {
 func (w *WorkspaceWatcher) matchesPattern(path string, pattern protocol.GlobPattern) bool {
 func (w *WorkspaceWatcher) matchesPattern(path string, pattern protocol.GlobPattern) bool {
 	patternInfo, err := pattern.AsPattern()
 	patternInfo, err := pattern.AsPattern()
 	if err != nil {
 	if err != nil {
-		logging.Error("Error parsing pattern", "pattern", pattern, "error", err)
+		slog.Error("Error parsing pattern", "pattern", pattern, "error", err)
 		return false
 		return false
 	}
 	}
 
 
@@ -616,7 +616,7 @@ func (w *WorkspaceWatcher) matchesPattern(path string, pattern protocol.GlobPatt
 	// Make path relative to basePath for matching
 	// Make path relative to basePath for matching
 	relPath, err := filepath.Rel(basePath, path)
 	relPath, err := filepath.Rel(basePath, path)
 	if err != nil {
 	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
 		return false
 	}
 	}
 	relPath = filepath.ToSlash(relPath)
 	relPath = filepath.ToSlash(relPath)
@@ -654,15 +654,15 @@ func (w *WorkspaceWatcher) debounceHandleFileEvent(ctx context.Context, uri stri
 func (w *WorkspaceWatcher) handleFileEvent(ctx context.Context, uri string, changeType protocol.FileChangeType) {
 func (w *WorkspaceWatcher) handleFileEvent(ctx context.Context, uri string, changeType protocol.FileChangeType) {
 	// If the file is open and it's a change event, use didChange notification
 	// If the file is open and it's a change event, use didChange notification
 	filePath := uri[7:] // Remove "file://" prefix
 	filePath := uri[7:] // Remove "file://" prefix
-	
+
 	if changeType == protocol.FileChangeType(protocol.Deleted) {
 	if changeType == protocol.FileChangeType(protocol.Deleted) {
 		// Always clear diagnostics for deleted files
 		// Always clear diagnostics for deleted files
 		w.client.ClearDiagnosticsForURI(protocol.DocumentUri(uri))
 		w.client.ClearDiagnosticsForURI(protocol.DocumentUri(uri))
-		
+
 		// If the file was open, close it in the LSP client
 		// If the file was open, close it in the LSP client
 		if w.client.IsFileOpen(filePath) {
 		if w.client.IsFileOpen(filePath) {
 			if err := w.client.CloseFile(ctx, filePath); err != nil {
 			if err := w.client.CloseFile(ctx, filePath); err != nil {
-				logging.Debug("Error closing deleted file in LSP client", "file", filePath, "error", err)
+				slog.Debug("Error closing deleted file in LSP client", "file", filePath, "error", err)
 				// Continue anyway - the file is gone
 				// Continue anyway - the file is gone
 			}
 			}
 		}
 		}
@@ -671,19 +671,19 @@ func (w *WorkspaceWatcher) handleFileEvent(ctx context.Context, uri string, chan
 		if _, err := os.Stat(filePath); err != nil {
 		if _, err := os.Stat(filePath); err != nil {
 			if os.IsNotExist(err) {
 			if os.IsNotExist(err) {
 				// File was deleted between the event and now - treat as delete
 				// File was deleted between the event and now - treat as delete
-				logging.Debug("File deleted between change event and processing", "file", filePath)
+				slog.Debug("File deleted between change event and processing", "file", filePath)
 				w.handleFileEvent(ctx, uri, protocol.FileChangeType(protocol.Deleted))
 				w.handleFileEvent(ctx, uri, protocol.FileChangeType(protocol.Deleted))
 				return
 				return
 			}
 			}
-			logging.Error("Error getting file info", "path", filePath, "error", err)
+			slog.Error("Error getting file info", "path", filePath, "error", err)
 			return
 			return
 		}
 		}
-		
+
 		// File exists and is open, notify change
 		// File exists and is open, notify change
 		if w.client.IsFileOpen(filePath) {
 		if w.client.IsFileOpen(filePath) {
 			err := w.client.NotifyChange(ctx, filePath)
 			err := w.client.NotifyChange(ctx, filePath)
 			if err != nil {
 			if err != nil {
-				logging.Error("Error notifying change", "error", err)
+				slog.Error("Error notifying change", "error", err)
 			}
 			}
 			return
 			return
 		}
 		}
@@ -692,17 +692,17 @@ func (w *WorkspaceWatcher) handleFileEvent(ctx context.Context, uri string, chan
 		if _, err := os.Stat(filePath); err != nil {
 		if _, err := os.Stat(filePath); err != nil {
 			if os.IsNotExist(err) {
 			if os.IsNotExist(err) {
 				// File was deleted between the event and now - ignore
 				// File was deleted between the event and now - ignore
-				logging.Debug("File deleted between create event and processing", "file", filePath)
+				slog.Debug("File deleted between create event and processing", "file", filePath)
 				return
 				return
 			}
 			}
-			logging.Error("Error getting file info", "path", filePath, "error", err)
+			slog.Error("Error getting file info", "path", filePath, "error", err)
 			return
 			return
 		}
 		}
 	}
 	}
 
 
 	// Notify LSP server about the file event using didChangeWatchedFiles
 	// Notify LSP server about the file event using didChangeWatchedFiles
 	if err := w.notifyFileEvent(ctx, uri, changeType); err != nil {
 	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)
 	}
 	}
 }
 }
 
 
@@ -710,7 +710,7 @@ func (w *WorkspaceWatcher) handleFileEvent(ctx context.Context, uri string, chan
 func (w *WorkspaceWatcher) notifyFileEvent(ctx context.Context, uri string, changeType protocol.FileChangeType) error {
 func (w *WorkspaceWatcher) notifyFileEvent(ctx context.Context, uri string, changeType protocol.FileChangeType) error {
 	cnf := config.Get()
 	cnf := config.Get()
 	if cnf.DebugLSP {
 	if cnf.DebugLSP {
-		logging.Debug("Notifying file event",
+		slog.Debug("Notifying file event",
 			"uri", uri,
 			"uri", uri,
 			"changeType", changeType,
 			"changeType", changeType,
 		)
 		)
@@ -874,7 +874,7 @@ func shouldExcludeFile(filePath string) bool {
 	if strings.HasSuffix(filePath, "~") {
 	if strings.HasSuffix(filePath, "~") {
 		return true
 		return true
 	}
 	}
-	
+
 	// Skip numeric temporary files (often created by editors)
 	// Skip numeric temporary files (often created by editors)
 	if _, err := strconv.Atoi(fileName); err == nil {
 	if _, err := strconv.Atoi(fileName); err == nil {
 		return true
 		return true
@@ -890,7 +890,7 @@ func shouldExcludeFile(filePath string) bool {
 	// Skip large files
 	// Skip large files
 	if info.Size() > maxFileSize {
 	if info.Size() > maxFileSize {
 		if cnf.DebugLSP {
 		if cnf.DebugLSP {
-			logging.Debug("Skipping large file",
+			slog.Debug("Skipping large file",
 				"path", filePath,
 				"path", filePath,
 				"size", info.Size(),
 				"size", info.Size(),
 				"maxSize", maxFileSize,
 				"maxSize", maxFileSize,
@@ -913,13 +913,13 @@ func (w *WorkspaceWatcher) openMatchingFile(ctx context.Context, path string) {
 	if err != nil {
 	if err != nil {
 		if os.IsNotExist(err) {
 		if os.IsNotExist(err) {
 			// File was deleted between event and processing - ignore
 			// File was deleted between event and processing - ignore
-			logging.Debug("File deleted between event and openMatchingFile", "path", path)
+			slog.Debug("File deleted between event and openMatchingFile", "path", path)
 			return
 			return
 		}
 		}
-		logging.Error("Error getting file info", "path", path, "error", err)
+		slog.Error("Error getting file info", "path", path, "error", err)
 		return
 		return
 	}
 	}
-	
+
 	if info.IsDir() {
 	if info.IsDir() {
 		return
 		return
 	}
 	}
@@ -938,10 +938,10 @@ func (w *WorkspaceWatcher) openMatchingFile(ctx context.Context, path string) {
 		// This helps with project initialization for certain language servers
 		// This helps with project initialization for certain language servers
 		if isHighPriorityFile(path, serverName) {
 		if isHighPriorityFile(path, serverName) {
 			if cnf.DebugLSP {
 			if cnf.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 && cnf.DebugLSP {
 			if err := w.client.OpenFile(ctx, path); err != nil && cnf.DebugLSP {
-				logging.Error("Error opening high-priority file", "path", path, "error", err)
+				slog.Error("Error opening high-priority file", "path", path, "error", err)
 			}
 			}
 			return
 			return
 		}
 		}
@@ -953,7 +953,7 @@ func (w *WorkspaceWatcher) openMatchingFile(ctx context.Context, path string) {
 			// Check file size - for preloading we're more conservative
 			// Check file size - for preloading we're more conservative
 			if info.Size() > (1 * 1024 * 1024) { // 1MB limit for preloaded files
 			if info.Size() > (1 * 1024 * 1024) { // 1MB limit for preloaded files
 				if cnf.DebugLSP {
 				if cnf.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
 				return
 			}
 			}
@@ -985,7 +985,7 @@ func (w *WorkspaceWatcher) openMatchingFile(ctx context.Context, path string) {
 			if shouldOpen {
 			if shouldOpen {
 				// Don't need to check if it's already open - the client.OpenFile handles that
 				// Don't need to check if it's already open - the client.OpenFile handles that
 				if err := w.client.OpenFile(ctx, path); err != nil && cnf.DebugLSP {
 				if err := w.client.OpenFile(ctx, path); err != nil && cnf.DebugLSP {
-					logging.Error("Error opening file", "path", path, "error", err)
+					slog.Error("Error opening file", "path", path, "error", err)
 				}
 				}
 			}
 			}
 		}
 		}

+ 6 - 9
internal/session/manager.go

@@ -4,8 +4,8 @@ import (
 	"context"
 	"context"
 	"sync"
 	"sync"
 
 
-	"github.com/opencode-ai/opencode/internal/logging"
 	"github.com/opencode-ai/opencode/internal/pubsub"
 	"github.com/opencode-ai/opencode/internal/pubsub"
+	"log/slog"
 )
 )
 
 
 // Manager handles session management, tracking the currently active session.
 // Manager handles session management, tracking the currently active session.
@@ -41,7 +41,7 @@ func InitManager(service Service) {
 // SetCurrentSession changes the active session to the one with the specified ID.
 // SetCurrentSession changes the active session to the one with the specified ID.
 func SetCurrentSession(sessionID string) {
 func SetCurrentSession(sessionID string) {
 	if globalManager == nil {
 	if globalManager == nil {
-		logging.Warn("Session manager not initialized")
+		slog.Warn("Session manager not initialized")
 		return
 		return
 	}
 	}
 
 
@@ -49,18 +49,17 @@ func SetCurrentSession(sessionID string) {
 	defer globalManager.mu.Unlock()
 	defer globalManager.mu.Unlock()
 
 
 	globalManager.currentSessionID = sessionID
 	globalManager.currentSessionID = sessionID
-	logging.Debug("Current session changed", "sessionID", sessionID)
+	slog.Debug("Current session changed", "sessionID", sessionID)
 }
 }
 
 
 // CurrentSessionID returns the ID of the currently active session.
 // CurrentSessionID returns the ID of the currently active session.
 func CurrentSessionID() string {
 func CurrentSessionID() string {
 	if globalManager == nil {
 	if globalManager == nil {
-		logging.Warn("Session manager not initialized")
 		return ""
 		return ""
 	}
 	}
 
 
-	globalManager.mu.RLock()
-	defer globalManager.mu.RUnlock()
+	// globalManager.mu.RLock()
+	// defer globalManager.mu.RUnlock()
 
 
 	return globalManager.currentSessionID
 	return globalManager.currentSessionID
 }
 }
@@ -69,7 +68,6 @@ func CurrentSessionID() string {
 // If no session is set or the session cannot be found, it returns nil.
 // If no session is set or the session cannot be found, it returns nil.
 func CurrentSession() *Session {
 func CurrentSession() *Session {
 	if globalManager == nil {
 	if globalManager == nil {
-		logging.Warn("Session manager not initialized")
 		return nil
 		return nil
 	}
 	}
 
 
@@ -80,9 +78,8 @@ func CurrentSession() *Session {
 
 
 	session, err := globalManager.service.Get(context.Background(), sessionID)
 	session, err := globalManager.service.Get(context.Background(), sessionID)
 	if err != nil {
 	if err != nil {
-		logging.Warn("Failed to get current session", "err", err)
 		return nil
 		return nil
 	}
 	}
 
 
 	return &session
 	return &session
-}
+}

+ 5 - 5
internal/tui/components/dialog/filepicker.go

@@ -16,13 +16,13 @@ import (
 	"github.com/charmbracelet/lipgloss"
 	"github.com/charmbracelet/lipgloss"
 	"github.com/opencode-ai/opencode/internal/app"
 	"github.com/opencode-ai/opencode/internal/app"
 	"github.com/opencode-ai/opencode/internal/config"
 	"github.com/opencode-ai/opencode/internal/config"
-	"github.com/opencode-ai/opencode/internal/logging"
 	"github.com/opencode-ai/opencode/internal/message"
 	"github.com/opencode-ai/opencode/internal/message"
 	"github.com/opencode-ai/opencode/internal/status"
 	"github.com/opencode-ai/opencode/internal/status"
 	"github.com/opencode-ai/opencode/internal/tui/image"
 	"github.com/opencode-ai/opencode/internal/tui/image"
 	"github.com/opencode-ai/opencode/internal/tui/styles"
 	"github.com/opencode-ai/opencode/internal/tui/styles"
 	"github.com/opencode-ai/opencode/internal/tui/theme"
 	"github.com/opencode-ai/opencode/internal/tui/theme"
 	"github.com/opencode-ai/opencode/internal/tui/util"
 	"github.com/opencode-ai/opencode/internal/tui/util"
+	"log/slog"
 )
 )
 
 
 const (
 const (
@@ -376,7 +376,7 @@ func (f *filepickerCmp) IsCWDFocused() bool {
 func NewFilepickerCmp(app *app.App) FilepickerCmp {
 func NewFilepickerCmp(app *app.App) FilepickerCmp {
 	homepath, err := os.UserHomeDir()
 	homepath, err := os.UserHomeDir()
 	if err != nil {
 	if err != nil {
-		logging.Error("error loading user files")
+		slog.Error("error loading user files")
 		return nil
 		return nil
 	}
 	}
 	baseDir := DirNode{parent: nil, directory: homepath}
 	baseDir := DirNode{parent: nil, directory: homepath}
@@ -392,7 +392,7 @@ func NewFilepickerCmp(app *app.App) FilepickerCmp {
 
 
 func (f *filepickerCmp) getCurrentFileBelowCursor() {
 func (f *filepickerCmp) getCurrentFileBelowCursor() {
 	if len(f.dirs) == 0 || f.cursor < 0 || f.cursor >= len(f.dirs) {
 	if len(f.dirs) == 0 || f.cursor < 0 || f.cursor >= len(f.dirs) {
-		logging.Error(fmt.Sprintf("Invalid cursor position. Dirs length: %d, Cursor: %d", len(f.dirs), f.cursor))
+		slog.Error(fmt.Sprintf("Invalid cursor position. Dirs length: %d, Cursor: %d", len(f.dirs), f.cursor))
 		f.viewport.SetContent("Preview unavailable")
 		f.viewport.SetContent("Preview unavailable")
 		return
 		return
 	}
 	}
@@ -405,7 +405,7 @@ func (f *filepickerCmp) getCurrentFileBelowCursor() {
 		go func() {
 		go func() {
 			imageString, err := image.ImagePreview(f.viewport.Width-4, fullPath)
 			imageString, err := image.ImagePreview(f.viewport.Width-4, fullPath)
 			if err != nil {
 			if err != nil {
-				logging.Error(err.Error())
+				slog.Error(err.Error())
 				f.viewport.SetContent("Preview unavailable")
 				f.viewport.SetContent("Preview unavailable")
 				return
 				return
 			}
 			}
@@ -418,7 +418,7 @@ func (f *filepickerCmp) getCurrentFileBelowCursor() {
 }
 }
 
 
 func readDir(path string, showHidden bool) []os.DirEntry {
 func readDir(path string, showHidden bool) []os.DirEntry {
-	logging.Info(fmt.Sprintf("Reading directory: %s", path))
+	slog.Info(fmt.Sprintf("Reading directory: %s", path))
 
 
 	entriesChan := make(chan []os.DirEntry, 1)
 	entriesChan := make(chan []os.DirEntry, 1)
 	errChan := make(chan error, 1)
 	errChan := make(chan error, 1)

+ 27 - 15
internal/tui/components/logs/details.go

@@ -23,17 +23,12 @@ type DetailComponent interface {
 
 
 type detailCmp struct {
 type detailCmp struct {
 	width, height int
 	width, height int
-	currentLog    logging.LogMessage
+	currentLog    logging.Log
 	viewport      viewport.Model
 	viewport      viewport.Model
 	focused       bool
 	focused       bool
 }
 }
 
 
 func (i *detailCmp) Init() tea.Cmd {
 func (i *detailCmp) Init() tea.Cmd {
-	messages := logging.List()
-	if len(messages) == 0 {
-		return nil
-	}
-	i.currentLog = messages[0]
 	return nil
 	return nil
 }
 }
 
 
@@ -42,8 +37,12 @@ func (i *detailCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	switch msg := msg.(type) {
 	switch msg := msg.(type) {
 	case selectedLogMsg:
 	case selectedLogMsg:
 		if msg.ID != i.currentLog.ID {
 		if msg.ID != i.currentLog.ID {
-			i.currentLog = logging.LogMessage(msg)
-			i.updateContent()
+			i.currentLog = logging.Log(msg)
+			// Defer content update to avoid blocking the UI
+			cmd = tea.Tick(time.Millisecond*1, func(time.Time) tea.Msg {
+				i.updateContent()
+				return nil
+			})
 		}
 		}
 	case tea.KeyMsg:
 	case tea.KeyMsg:
 		// Only process keyboard input when focused
 		// Only process keyboard input when focused
@@ -55,7 +54,7 @@ func (i *detailCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		return i, cmd
 		return i, cmd
 	}
 	}
 
 
-	return i, nil
+	return i, cmd
 }
 }
 
 
 func (i *detailCmp) updateContent() {
 func (i *detailCmp) updateContent() {
@@ -66,9 +65,12 @@ func (i *detailCmp) updateContent() {
 	timeStyle := lipgloss.NewStyle().Foreground(t.TextMuted())
 	timeStyle := lipgloss.NewStyle().Foreground(t.TextMuted())
 	levelStyle := getLevelStyle(i.currentLog.Level)
 	levelStyle := getLevelStyle(i.currentLog.Level)
 
 
+	// Format timestamp
+	timeStr := time.Unix(i.currentLog.Timestamp, 0).Format(time.RFC3339)
+
 	header := lipgloss.JoinHorizontal(
 	header := lipgloss.JoinHorizontal(
 		lipgloss.Center,
 		lipgloss.Center,
-		timeStyle.Render(i.currentLog.Time.Format(time.RFC3339)),
+		timeStyle.Render(timeStr),
 		"  ",
 		"  ",
 		levelStyle.Render(i.currentLog.Level),
 		levelStyle.Render(i.currentLog.Level),
 	)
 	)
@@ -93,23 +95,33 @@ func (i *detailCmp) updateContent() {
 		keyStyle := lipgloss.NewStyle().Foreground(t.Primary()).Bold(true)
 		keyStyle := lipgloss.NewStyle().Foreground(t.Primary()).Bold(true)
 		valueStyle := lipgloss.NewStyle().Foreground(t.Text())
 		valueStyle := lipgloss.NewStyle().Foreground(t.Text())
 
 
-		for _, attr := range i.currentLog.Attributes {
-			attrLine := fmt.Sprintf("%s: %s",
-				keyStyle.Render(attr.Key),
-				valueStyle.Render(attr.Value),
+		for key, value := range i.currentLog.Attributes {
+			attrLine := fmt.Sprintf("%s: %s", 
+				keyStyle.Render(key),
+				valueStyle.Render(value),
 			)
 			)
+
 			content.WriteString(lipgloss.NewStyle().Padding(0, 2).Render(attrLine))
 			content.WriteString(lipgloss.NewStyle().Padding(0, 2).Render(attrLine))
 			content.WriteString("\n")
 			content.WriteString("\n")
 		}
 		}
 	}
 	}
 
 
+	// Session ID if available
+	if i.currentLog.SessionID != "" {
+		sessionStyle := lipgloss.NewStyle().Bold(true).Foreground(t.Text())
+		content.WriteString("\n")
+		content.WriteString(sessionStyle.Render("Session:"))
+		content.WriteString("\n")
+		content.WriteString(lipgloss.NewStyle().Padding(0, 2).Render(i.currentLog.SessionID))
+	}
+
 	i.viewport.SetContent(content.String())
 	i.viewport.SetContent(content.String())
 }
 }
 
 
 func getLevelStyle(level string) lipgloss.Style {
 func getLevelStyle(level string) lipgloss.Style {
 	style := lipgloss.NewStyle().Bold(true)
 	style := lipgloss.NewStyle().Bold(true)
 	t := theme.CurrentTheme()
 	t := theme.CurrentTheme()
-	
+
 	switch strings.ToLower(level) {
 	switch strings.ToLower(level) {
 	case "info":
 	case "info":
 		return style.Foreground(t.Info())
 		return style.Foreground(t.Info())

+ 83 - 34
internal/tui/components/logs/table.go

@@ -1,15 +1,17 @@
 package logs
 package logs
 
 
 import (
 import (
-	"slices"
+	"context"
+	"time"
 
 
 	"github.com/charmbracelet/bubbles/key"
 	"github.com/charmbracelet/bubbles/key"
 	"github.com/charmbracelet/bubbles/table"
 	"github.com/charmbracelet/bubbles/table"
 	tea "github.com/charmbracelet/bubbletea"
 	tea "github.com/charmbracelet/bubbletea"
 	"github.com/opencode-ai/opencode/internal/logging"
 	"github.com/opencode-ai/opencode/internal/logging"
 	"github.com/opencode-ai/opencode/internal/pubsub"
 	"github.com/opencode-ai/opencode/internal/pubsub"
+	"github.com/opencode-ai/opencode/internal/session"
+	"github.com/opencode-ai/opencode/internal/tui/components/chat"
 	"github.com/opencode-ai/opencode/internal/tui/layout"
 	"github.com/opencode-ai/opencode/internal/tui/layout"
-	// "github.com/opencode-ai/opencode/internal/tui/styles"
 	"github.com/opencode-ai/opencode/internal/tui/theme"
 	"github.com/opencode-ai/opencode/internal/tui/theme"
 	"github.com/opencode-ai/opencode/internal/tui/util"
 	"github.com/opencode-ai/opencode/internal/tui/util"
 )
 )
@@ -23,46 +25,97 @@ type TableComponent interface {
 type tableCmp struct {
 type tableCmp struct {
 	table   table.Model
 	table   table.Model
 	focused bool
 	focused bool
+	logs    []logging.Log
 }
 }
 
 
-type selectedLogMsg logging.LogMessage
+type selectedLogMsg logging.Log
+
+// Message for when logs are loaded from the database
+type logsLoadedMsg struct {
+	logs []logging.Log
+}
 
 
 func (i *tableCmp) Init() tea.Cmd {
 func (i *tableCmp) Init() tea.Cmd {
-	i.setRows()
-	return nil
+	return i.fetchLogs()
+}
+
+func (i *tableCmp) fetchLogs() tea.Cmd {
+	return func() tea.Msg {
+		ctx := context.Background()
+		loggingService := logging.GetService()
+		if loggingService == nil {
+			return nil
+		}
+
+		var logs []logging.Log
+		var err error
+		sessionId := session.CurrentSessionID()
+
+		// Limit the number of logs to improve performance
+		const logLimit = 100
+		if sessionId == "" {
+			logs, err = loggingService.ListAll(ctx, logLimit)
+		} else {
+			logs, err = loggingService.ListBySession(ctx, sessionId)
+			// Trim logs if there are too many
+			if err == nil && len(logs) > logLimit {
+				logs = logs[len(logs)-logLimit:]
+			}
+		}
+
+		if err != nil {
+			return nil
+		}
+
+		return logsLoadedMsg{logs: logs}
+	}
 }
 }
 
 
 func (i *tableCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 func (i *tableCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	var cmds []tea.Cmd
 	var cmds []tea.Cmd
-	switch msg.(type) {
-	case pubsub.Event[logging.LogMessage]:
-		i.setRows()
+
+	switch msg := msg.(type) {
+	case logsLoadedMsg:
+		i.logs = msg.logs
+		i.updateRows()
+		return i, nil
+
+	case chat.SessionSelectedMsg:
+		return i, i.fetchLogs()
+
+	case pubsub.Event[logging.Log]:
+		// Only handle created events
+		if msg.Type == pubsub.CreatedEvent {
+			// Add the new log to our list
+			i.logs = append([]logging.Log{msg.Payload}, i.logs...)
+			// Keep the list at a reasonable size
+			if len(i.logs) > 100 {
+				i.logs = i.logs[:100]
+			}
+			i.updateRows()
+		}
 		return i, nil
 		return i, nil
 	}
 	}
-	
+
 	// Only process keyboard input when focused
 	// Only process keyboard input when focused
 	if _, ok := msg.(tea.KeyMsg); ok && !i.focused {
 	if _, ok := msg.(tea.KeyMsg); ok && !i.focused {
 		return i, nil
 		return i, nil
 	}
 	}
-	
+
 	t, cmd := i.table.Update(msg)
 	t, cmd := i.table.Update(msg)
 	cmds = append(cmds, cmd)
 	cmds = append(cmds, cmd)
 	i.table = t
 	i.table = t
+	
+	// Only send selected log message when selection changes
 	selectedRow := i.table.SelectedRow()
 	selectedRow := i.table.SelectedRow()
 	if selectedRow != nil {
 	if selectedRow != nil {
-		// Always send the selected log message when a row is selected
-		// This fixes the issue where navigation doesn't update the detail pane
-		// when returning to the logs page
-		var log logging.LogMessage
-		for _, row := range logging.List() {
-			if row.ID == selectedRow[0] {
-				log = row
+		// Use a map for faster lookups by ID
+		for _, log := range i.logs {
+			if log.ID == selectedRow[0] {
+				cmds = append(cmds, util.CmdHandler(selectedLogMsg(log)))
 				break
 				break
 			}
 			}
 		}
 		}
-		if log.ID != "" {
-			cmds = append(cmds, util.CmdHandler(selectedLogMsg(log)))
-		}
 	}
 	}
 	return i, tea.Batch(cmds...)
 	return i, tea.Batch(cmds...)
 }
 }
@@ -105,25 +158,20 @@ func (i *tableCmp) BindingKeys() []key.Binding {
 	return layout.KeyMapToSlice(i.table.KeyMap)
 	return layout.KeyMapToSlice(i.table.KeyMap)
 }
 }
 
 
-func (i *tableCmp) setRows() {
-	rows := []table.Row{}
+func (i *tableCmp) updateRows() {
+	rows := make([]table.Row, 0, len(i.logs))
 
 
-	logs := logging.List()
-	slices.SortFunc(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
-	})
+	// Logs are already sorted by timestamp (newest first) from the database query
+	// Skip the expensive sort operation
+
+	for _, log := range i.logs {
+		// Format timestamp as time
+		timeStr := time.Unix(log.Timestamp, 0).Format("15:04:05")
 
 
-	for _, log := range logs {
 		// Include ID as hidden first column for selection
 		// Include ID as hidden first column for selection
 		row := table.Row{
 		row := table.Row{
 			log.ID,
 			log.ID,
-			log.Time.Format("15:04:05"),
+			timeStr,
 			log.Level,
 			log.Level,
 			log.Message,
 			log.Message,
 		}
 		}
@@ -146,6 +194,7 @@ func NewLogsTable() TableComponent {
 	tableModel.Focus()
 	tableModel.Focus()
 	return &tableCmp{
 	return &tableCmp{
 		table: tableModel,
 		table: tableModel,
+		logs:  []logging.Log{},
 	}
 	}
 }
 }
 
 

+ 7 - 7
internal/tui/theme/manager.go

@@ -2,13 +2,13 @@ package theme
 
 
 import (
 import (
 	"fmt"
 	"fmt"
+	"log/slog"
 	"slices"
 	"slices"
 	"strings"
 	"strings"
 	"sync"
 	"sync"
 
 
 	"github.com/alecthomas/chroma/v2/styles"
 	"github.com/alecthomas/chroma/v2/styles"
 	"github.com/opencode-ai/opencode/internal/config"
 	"github.com/opencode-ai/opencode/internal/config"
-	"github.com/opencode-ai/opencode/internal/logging"
 )
 )
 
 
 // Manager handles theme registration, selection, and retrieval.
 // Manager handles theme registration, selection, and retrieval.
@@ -49,19 +49,19 @@ func SetTheme(name string) error {
 	defer globalManager.mu.Unlock()
 	defer globalManager.mu.Unlock()
 
 
 	delete(styles.Registry, "charm")
 	delete(styles.Registry, "charm")
-	
+
 	// Handle custom theme
 	// Handle custom theme
 	if name == "custom" {
 	if name == "custom" {
 		cfg := config.Get()
 		cfg := config.Get()
 		if cfg == nil || cfg.TUI.CustomTheme == nil || len(cfg.TUI.CustomTheme) == 0 {
 		if cfg == nil || cfg.TUI.CustomTheme == nil || len(cfg.TUI.CustomTheme) == 0 {
 			return fmt.Errorf("custom theme selected but no custom theme colors defined in config")
 			return fmt.Errorf("custom theme selected but no custom theme colors defined in config")
 		}
 		}
-		
+
 		customTheme, err := LoadCustomTheme(cfg.TUI.CustomTheme)
 		customTheme, err := LoadCustomTheme(cfg.TUI.CustomTheme)
 		if err != nil {
 		if err != nil {
 			return fmt.Errorf("failed to load custom theme: %w", err)
 			return fmt.Errorf("failed to load custom theme: %w", err)
 		}
 		}
-		
+
 		// Register the custom theme
 		// Register the custom theme
 		globalManager.themes["custom"] = customTheme
 		globalManager.themes["custom"] = customTheme
 	} else if _, exists := globalManager.themes[name]; !exists {
 	} else if _, exists := globalManager.themes[name]; !exists {
@@ -73,7 +73,7 @@ func SetTheme(name string) error {
 	// Update the config file using viper
 	// Update the config file using viper
 	if err := updateConfigTheme(name); err != nil {
 	if err := updateConfigTheme(name); err != nil {
 		// Log the error but don't fail the theme change
 		// Log the error but don't fail the theme change
-		logging.Warn("Warning: Failed to update config file with new theme", "err", err)
+		slog.Warn("Warning: Failed to update config file with new theme", "err", err)
 	}
 	}
 
 
 	return nil
 	return nil
@@ -140,7 +140,7 @@ func LoadCustomTheme(customTheme map[string]any) (Theme, error) {
 	for key, value := range customTheme {
 	for key, value := range customTheme {
 		adaptiveColor, err := ParseAdaptiveColor(value)
 		adaptiveColor, err := ParseAdaptiveColor(value)
 		if err != nil {
 		if err != nil {
-			logging.Warn("Invalid color definition in custom theme", "key", key, "error", err)
+			slog.Warn("Invalid color definition in custom theme", "key", key, "error", err)
 			continue // Skip this color but continue processing others
 			continue // Skip this color but continue processing others
 		}
 		}
 
 
@@ -203,7 +203,7 @@ func LoadCustomTheme(customTheme map[string]any) (Theme, error) {
 		case "diffremovedlinenumberbg":
 		case "diffremovedlinenumberbg":
 			theme.DiffRemovedLineNumberBgColor = adaptiveColor
 			theme.DiffRemovedLineNumberBgColor = adaptiveColor
 		default:
 		default:
-			logging.Warn("Unknown color key in custom theme", "key", key)
+			slog.Warn("Unknown color key in custom theme", "key", key)
 		}
 		}
 	}
 	}
 
 

+ 1 - 1
internal/tui/tui.go

@@ -201,7 +201,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 
 
 		return a, tea.Batch(cmds...)
 		return a, tea.Batch(cmds...)
 
 
-	case pubsub.Event[logging.LogMessage]:
+	case pubsub.Event[logging.Log]:
 		a.pages[page.LogsPage], cmd = a.pages[page.LogsPage].Update(msg)
 		a.pages[page.LogsPage], cmd = a.pages[page.LogsPage].Update(msg)
 		cmds = append(cmds, cmd)
 		cmds = append(cmds, cmd)