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

+ 12 - 4
internal/app/app.go

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

+ 18 - 16
internal/app/lsp.go

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

+ 20 - 47
internal/config/config.go

@@ -11,7 +11,6 @@ import (
 	"strings"
 
 	"github.com/opencode-ai/opencode/internal/llm/models"
-	"github.com/opencode-ai/opencode/internal/logging"
 	"github.com/spf13/viper"
 )
 
@@ -70,7 +69,7 @@ type LSPConfig struct {
 
 // TUIConfig defines the configuration for the Terminal User Interface.
 type TUIConfig struct {
-	Theme       string                 `json:"theme,omitempty"`
+	Theme       string         `json:"theme,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.
 // If debug is true, debug mode is enabled and log level is set to debug.
 // 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 {
 		return cfg, nil
 	}
@@ -150,39 +149,13 @@ func Load(workingDir string, debug bool) (*Config, error) {
 	}
 
 	applyDefaultValues()
+
 	defaultLevel := slog.LevelInfo
 	if cfg.Debug {
 		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
 	if err := Validate(); err != nil {
@@ -397,13 +370,13 @@ func validateAgent(cfg *Config, name AgentName, agent Agent) error {
 	// Check if model exists
 	model, modelExists := models.SupportedModels[agent.Model]
 	if !modelExists {
-		logging.Warn("unsupported model configured, reverting to default",
+		slog.Warn("unsupported model configured, reverting to default",
 			"agent", name,
 			"configured_model", agent.Model)
 
 		// Set default model based on available providers
 		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 {
 			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
 		apiKey := getProviderAPIKey(provider)
 		if apiKey == "" {
-			logging.Warn("provider not configured for model, reverting to default",
+			slog.Warn("provider not configured for model, reverting to default",
 				"agent", name,
 				"model", agent.Model,
 				"provider", provider)
 
 			// Set default model based on available providers
 			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 {
 				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{
 				APIKey: apiKey,
 			}
-			logging.Info("added provider from environment", "provider", provider)
+			slog.Info("added provider from environment", "provider", provider)
 		}
 	} else if providerCfg.Disabled || providerCfg.APIKey == "" {
 		// 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,
 			"model", agent.Model,
 			"provider", provider)
 
 		// Set default model based on available providers
 		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 {
 			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
 	if agent.MaxTokens <= 0 {
-		logging.Warn("invalid max tokens, setting to default",
+		slog.Warn("invalid max tokens, setting to default",
 			"agent", name,
 			"model", agent.Model,
 			"max_tokens", agent.MaxTokens)
@@ -468,7 +441,7 @@ func validateAgent(cfg *Config, name AgentName, agent Agent) error {
 		cfg.Agents[name] = updatedAgent
 	} else if model.ContextWindow > 0 && agent.MaxTokens > model.ContextWindow/2 {
 		// 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,
 			"model", agent.Model,
 			"max_tokens", agent.MaxTokens,
@@ -484,7 +457,7 @@ func validateAgent(cfg *Config, name AgentName, agent Agent) error {
 	if model.CanReason && provider == models.ProviderOpenAI {
 		if agent.ReasoningEffort == "" {
 			// 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,
 				"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)
 			effort := strings.ToLower(agent.ReasoningEffort)
 			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,
 					"model", agent.Model,
 					"reasoning_effort", agent.ReasoningEffort)
@@ -509,7 +482,7 @@ func validateAgent(cfg *Config, name AgentName, agent Agent) error {
 		}
 	} else if !model.CanReason && agent.ReasoningEffort != "" {
 		// 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,
 			"model", agent.Model,
 			"reasoning_effort", agent.ReasoningEffort)
@@ -539,7 +512,7 @@ func Validate() error {
 	// Validate providers
 	for provider, providerCfg := range cfg.Providers {
 		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
 			cfg.Providers[provider] = providerCfg
 		}
@@ -548,7 +521,7 @@ func Validate() error {
 	// Validate LSP configurations
 	for language, lspConfig := range cfg.LSP {
 		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
 			cfg.LSP[language] = lspConfig
 		}
@@ -782,7 +755,7 @@ func UpdateTheme(themeName string) error {
 			return fmt.Errorf("failed to get home directory: %w", err)
 		}
 		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(`{}`)
 	} else {
 		// Read the existing config file

+ 5 - 5
internal/db/connect.go

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

+ 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 {
 		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 {
 		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 {
 		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 {
 		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 {
 		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 {
 		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)
 		}
 	}
+	if q.createLogStmt != nil {
+		if cerr := q.createLogStmt.Close(); cerr != nil {
+			err = fmt.Errorf("error closing createLogStmt: %w", cerr)
+		}
+	}
 	if q.createMessageStmt != nil {
 		if cerr := q.createMessageStmt.Close(); cerr != nil {
 			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)
 		}
 	}
+	if q.listAllLogsStmt != nil {
+		if cerr := q.listAllLogsStmt.Close(); cerr != nil {
+			err = fmt.Errorf("error closing listAllLogsStmt: %w", cerr)
+		}
+	}
 	if q.listFilesByPathStmt != nil {
 		if cerr := q.listFilesByPathStmt.Close(); cerr != nil {
 			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)
 		}
 	}
+	if q.listLogsBySessionStmt != nil {
+		if cerr := q.listLogsBySessionStmt.Close(); cerr != nil {
+			err = fmt.Errorf("error closing listLogsBySessionStmt: %w", cerr)
+		}
+	}
 	if q.listMessagesBySessionStmt != nil {
 		if cerr := q.listMessagesBySessionStmt.Close(); cerr != nil {
 			err = fmt.Errorf("error closing listMessagesBySessionStmt: %w", cerr)
@@ -245,6 +269,7 @@ type Queries struct {
 	db                             DBTX
 	tx                             *sql.Tx
 	createFileStmt                 *sql.Stmt
+	createLogStmt                  *sql.Stmt
 	createMessageStmt              *sql.Stmt
 	createSessionStmt              *sql.Stmt
 	deleteFileStmt                 *sql.Stmt
@@ -256,9 +281,11 @@ type Queries struct {
 	getFileByPathAndSessionStmt    *sql.Stmt
 	getMessageStmt                 *sql.Stmt
 	getSessionByIDStmt             *sql.Stmt
+	listAllLogsStmt                *sql.Stmt
 	listFilesByPathStmt            *sql.Stmt
 	listFilesBySessionStmt         *sql.Stmt
 	listLatestSessionFilesStmt     *sql.Stmt
+	listLogsBySessionStmt          *sql.Stmt
 	listMessagesBySessionStmt      *sql.Stmt
 	listMessagesBySessionAfterStmt *sql.Stmt
 	listNewFilesStmt               *sql.Stmt
@@ -273,6 +300,7 @@ func (q *Queries) WithTx(tx *sql.Tx) *Queries {
 		db:                             tx,
 		tx:                             tx,
 		createFileStmt:                 q.createFileStmt,
+		createLogStmt:                  q.createLogStmt,
 		createMessageStmt:              q.createMessageStmt,
 		createSessionStmt:              q.createSessionStmt,
 		deleteFileStmt:                 q.deleteFileStmt,
@@ -284,9 +312,11 @@ func (q *Queries) WithTx(tx *sql.Tx) *Queries {
 		getFileByPathAndSessionStmt:    q.getFileByPathAndSessionStmt,
 		getMessageStmt:                 q.getMessageStmt,
 		getSessionByIDStmt:             q.getSessionByIDStmt,
+		listAllLogsStmt:                q.listAllLogsStmt,
 		listFilesByPathStmt:            q.listFilesByPathStmt,
 		listFilesBySessionStmt:         q.listFilesBySessionStmt,
 		listLatestSessionFilesStmt:     q.listLatestSessionFilesStmt,
+		listLogsBySessionStmt:          q.listLogsBySessionStmt,
 		listMessagesBySessionStmt:      q.listMessagesBySessionStmt,
 		listMessagesBySessionAfterStmt: q.listMessagesBySessionAfterStmt,
 		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"`
 }
 
+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 {
 	ID         string         `json:"id"`
 	SessionID  string         `json:"session_id"`

+ 4 - 0
internal/db/querier.go

@@ -6,10 +6,12 @@ package db
 
 import (
 	"context"
+	"database/sql"
 )
 
 type Querier interface {
 	CreateFile(ctx context.Context, arg CreateFileParams) (File, error)
+	CreateLog(ctx context.Context, arg CreateLogParams) error
 	CreateMessage(ctx context.Context, arg CreateMessageParams) (Message, error)
 	CreateSession(ctx context.Context, arg CreateSessionParams) (Session, error)
 	DeleteFile(ctx context.Context, id string) error
@@ -21,9 +23,11 @@ type Querier interface {
 	GetFileByPathAndSession(ctx context.Context, arg GetFileByPathAndSessionParams) (File, error)
 	GetMessage(ctx context.Context, id string) (Message, 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)
 	ListFilesBySession(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)
 	ListMessagesBySessionAfter(ctx context.Context, arg ListMessagesBySessionAfterParams) ([]Message, 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"
 	"time"
 
+	"log/slog"
+
 	"github.com/opencode-ai/opencode/internal/config"
 	"github.com/opencode-ai/opencode/internal/llm/models"
 	"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)
 	go func() {
-		logging.Debug("Request started", "sessionID", sessionID)
+		slog.Debug("Request started", "sessionID", sessionID)
 		defer logging.RecoverPanic("agent.Run", func() {
 			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) {
 			status.Error(result.Err().Error())
 		}
-		logging.Debug("Request completed", "sessionID", sessionID)
+		slog.Debug("Request completed", "sessionID", sessionID)
 		a.activeRequests.Delete(sessionID)
 		cancel()
 		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))
 		}
-		logging.Info("Result", "message", agentMessage.FinishReason(), "toolResults", toolResults)
+		slog.Info("Result", "message", agentMessage.FinishReason(), "toolResults", toolResults)
 		if (agentMessage.FinishReason() == message.FinishReasonToolUse) && toolResults != nil {
 			// We are not done, we need to respond with the tool response
 			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/llm/tools"
-	"github.com/opencode-ai/opencode/internal/logging"
 	"github.com/opencode-ai/opencode/internal/permission"
 	"github.com/opencode-ai/opencode/internal/version"
+	"log/slog"
 
 	"github.com/mark3labs/mcp-go/client"
 	"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)
 	if err != nil {
-		logging.Error("error initializing mcp client", "error", err)
+		slog.Error("error initializing mcp client", "error", err)
 		return stdioTools
 	}
 	toolsRequest := mcp.ListToolsRequest{}
 	tools, err := c.ListTools(ctx, toolsRequest)
 	if err != nil {
-		logging.Error("error listing tools", "error", err)
+		slog.Error("error listing tools", "error", err)
 		return stdioTools
 	}
 	for _, t := range tools.Tools {
@@ -175,7 +175,7 @@ func GetMcpTools(ctx context.Context, permissions permission.Service) []tools.Ba
 				m.Args...,
 			)
 			if err != nil {
-				logging.Error("error creating mcp client", "error", err)
+				slog.Error("error creating mcp client", "error", err)
 				continue
 			}
 
@@ -186,7 +186,7 @@ func GetMcpTools(ctx context.Context, permissions permission.Service) []tools.Ba
 				client.WithHeaders(m.Headers),
 			)
 			if err != nil {
-				logging.Error("error creating mcp client", "error", err)
+				slog.Error("error creating mcp client", "error", err)
 				continue
 			}
 			mcpTools = append(mcpTools, getTools(ctx, name, m, permissions, c)...)

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

@@ -33,7 +33,7 @@ func CoderAgentTools(
 			tools.NewGlobTool(),
 			tools.NewGrepTool(),
 			tools.NewLsTool(),
-			tools.NewSourcegraphTool(),
+			// tools.NewSourcegraphTool(),
 			tools.NewViewTool(lspClients),
 			tools.NewPatchTool(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/llm/models"
-	"github.com/opencode-ai/opencode/internal/logging"
+	"log/slog"
 )
 
 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 {
 		// Add context from project-specific instruction files if they exist
 		contextContent := getContextFromPaths()
-		logging.Debug("Context content", "Context", contextContent)
+		slog.Debug("Context content", "Context", contextContent)
 		if contextContent != "" {
 			return fmt.Sprintf("%s\n\n# Project-Specific Context\n Make sure to follow the instructions in the context below\n%s", basePrompt, contextContent)
 		}

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

@@ -2,6 +2,7 @@ package prompt
 
 import (
 	"fmt"
+	"log/slog"
 	"os"
 	"path/filepath"
 	"testing"
@@ -14,8 +15,11 @@ import (
 func TestGetContextFromPaths(t *testing.T) {
 	t.Parallel()
 
+	lvl := new(slog.LevelVar)
+	lvl.Set(slog.LevelDebug)
+
 	tmpDir := t.TempDir()
-	_, err := config.Load(tmpDir, false)
+	_, err := config.Load(tmpDir, false, lvl)
 	if err != nil {
 		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/llm/models"
 	"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/status"
+	"log/slog"
 )
 
 type anthropicOptions struct {
@@ -107,7 +107,7 @@ func (a *anthropicClient) convertMessages(messages []message.Message) (anthropic
 			}
 
 			if len(blocks) == 0 {
-				logging.Warn("There is a message without content, investigate, this should not happen")
+				slog.Warn("There is a message without content, investigate, this should not happen")
 				continue
 			}
 			anthropicMessages = append(anthropicMessages, anthropic.NewAssistantMessage(blocks...))
@@ -210,7 +210,7 @@ func (a *anthropicClient) send(ctx context.Context, messages []message.Message,
 	cfg := config.Get()
 	if cfg.Debug {
 		jsonData, _ := json.Marshal(preparedMessages)
-		logging.Debug("Prepared messages", "messages", string(jsonData))
+		slog.Debug("Prepared messages", "messages", string(jsonData))
 	}
 
 	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 err != nil {
-			logging.Error("Error in Anthropic API call", "error", err)
+			slog.Error("Error in Anthropic API call", "error", err)
 			retry, after, retryErr := a.shouldRetry(attempts, err)
 			if retryErr != nil {
 				return nil, retryErr
@@ -259,7 +259,7 @@ func (a *anthropicClient) stream(ctx context.Context, messages []message.Message
 	cfg := config.Get()
 	if cfg.Debug {
 		jsonData, _ := json.Marshal(preparedMessages)
-		logging.Debug("Prepared messages", "messages", string(jsonData))
+		slog.Debug("Prepared messages", "messages", string(jsonData))
 	}
 	attempts := 0
 	eventChan := make(chan ProviderEvent)
@@ -277,7 +277,7 @@ func (a *anthropicClient) stream(ctx context.Context, messages []message.Message
 				event := anthropicStream.Current()
 				err := accumulatedMessage.Accumulate(event)
 				if err != nil {
-					logging.Warn("Error accumulating message", "error", err)
+					slog.Warn("Error accumulating message", "error", err)
 					continue
 				}
 

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

@@ -13,11 +13,11 @@ import (
 	"github.com/google/uuid"
 	"github.com/opencode-ai/opencode/internal/config"
 	"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/status"
 	"google.golang.org/api/iterator"
 	"google.golang.org/api/option"
+	"log/slog"
 )
 
 type geminiOptions struct {
@@ -42,7 +42,7 @@ func newGeminiClient(opts providerClientOptions) GeminiClient {
 
 	client, err := genai.NewClient(context.Background(), option.WithAPIKey(opts.apiKey))
 	if err != nil {
-		logging.Error("Failed to create Gemini client", "error", err)
+		slog.Error("Failed to create Gemini client", "error", err)
 		return nil
 	}
 
@@ -176,7 +176,7 @@ func (g *geminiClient) send(ctx context.Context, messages []message.Message, too
 	cfg := config.Get()
 	if cfg.Debug {
 		jsonData, _ := json.Marshal(geminiMessages)
-		logging.Debug("Prepared messages", "messages", string(jsonData))
+		slog.Debug("Prepared messages", "messages", string(jsonData))
 	}
 
 	attempts := 0
@@ -263,7 +263,7 @@ func (g *geminiClient) stream(ctx context.Context, messages []message.Message, t
 	cfg := config.Get()
 	if cfg.Debug {
 		jsonData, _ := json.Marshal(geminiMessages)
-		logging.Debug("Prepared messages", "messages", string(jsonData))
+		slog.Debug("Prepared messages", "messages", string(jsonData))
 	}
 
 	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/llm/models"
 	"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/status"
+	"log/slog"
 )
 
 type openaiOptions struct {
@@ -199,7 +199,7 @@ func (o *openaiClient) send(ctx context.Context, messages []message.Message, too
 	cfg := config.Get()
 	if cfg.Debug {
 		jsonData, _ := json.Marshal(params)
-		logging.Debug("Prepared messages", "messages", string(jsonData))
+		slog.Debug("Prepared messages", "messages", string(jsonData))
 	}
 	attempts := 0
 	for {
@@ -256,7 +256,7 @@ func (o *openaiClient) stream(ctx context.Context, messages []message.Message, t
 	cfg := config.Get()
 	if cfg.Debug {
 		jsonData, _ := json.Marshal(params)
-		logging.Debug("Prepared messages", "messages", string(jsonData))
+		slog.Debug("Prepared messages", "messages", string(jsonData))
 	}
 
 	attempts := 0
@@ -427,7 +427,7 @@ func WithReasoningEffort(effort string) OpenAIOption {
 		case "low", "medium", "high":
 			defaultReasoningEffort = effort
 		default:
-			logging.Warn("Invalid reasoning effort, using default: medium")
+			slog.Warn("Invalid reasoning effort, using default: medium")
 		}
 		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/tools"
-	"github.com/opencode-ai/opencode/internal/logging"
 	"github.com/opencode-ai/opencode/internal/message"
+	"log/slog"
 )
 
 type EventType string
@@ -166,13 +166,13 @@ func (p *baseProvider[C]) SendMessages(ctx context.Context, messages []message.M
 	messages = p.cleanMessages(messages)
 	response, err := p.client.send(ctx, messages, tools)
 	if err == nil && response != nil {
-		logging.Debug("API request token usage", 
+		slog.Debug("API request token usage",
 			"model", p.options.model.Name,
 			"input_tokens", response.Usage.InputTokens,
 			"output_tokens", response.Usage.OutputTokens,
 			"cache_creation_tokens", response.Usage.CacheCreationTokens,
 			"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
 }
@@ -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 {
 	messages = p.cleanMessages(messages)
 	eventChan := p.client.stream(ctx, messages, tools)
-	
+
 	// Create a new channel to intercept events
 	wrappedChan := make(chan ProviderEvent)
-	
+
 	go func() {
 		defer close(wrappedChan)
-		
+
 		for event := range eventChan {
 			// Pass the event through
 			wrappedChan <- event
-			
+
 			// Log token usage when we get the complete event
 			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,
 					"input_tokens", event.Response.Usage.InputTokens,
 					"output_tokens", event.Response.Usage.OutputTokens,
 					"cache_creation_tokens", event.Response.Usage.CacheCreationTokens,
 					"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
 }
 

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

+ 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/diff"
 	"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/permission"
+	"log/slog"
 )
 
 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
 			_, err = p.files.Create(ctx, sessionID, absPath, oldContent)
 			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
 			_, err = p.files.CreateVersion(ctx, sessionID, absPath, oldContent)
 			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)
 		}
 		if err != nil {
-			logging.Debug("Error creating file history version", "error", err)
+			slog.Debug("Error creating file history version", "error", err)
 		}
 
 		// 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/diff"
 	"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/permission"
+	"log/slog"
 )
 
 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
 		_, err = w.files.CreateVersion(ctx, sessionID, filePath, oldContent)
 		if err != nil {
-			logging.Debug("Error creating file history version", "error", err)
+			slog.Debug("Error creating file history version", "error", err)
 		}
 	}
 	// Store the new version
 	_, err = w.files.CreateVersion(ctx, sessionID, filePath, params.Content)
 	if err != nil {
-		logging.Debug("Error creating file history version", "error", err)
+		slog.Debug("Error creating file history version", "error", err)
 	}
 
 	recordFileWrite(filePath)

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

@@ -10,22 +10,6 @@ import (
 	"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.
 // It logs the error, creates a panic log file with stack trace,
 // and executes an optional cleanup function before returning.
@@ -33,7 +17,7 @@ func RecoverPanic(name string, cleanup func()) {
 	if r := recover(); r != nil {
 		// Log the panic
 		errorMsg := fmt.Sprintf("Panic in %s: %v", name, r)
-		Error(errorMsg)
+		slog.Error(errorMsg)
 		status.Error(errorMsg)
 
 		// Create a timestamped panic log file
@@ -43,7 +27,7 @@ func RecoverPanic(name string, cleanup func()) {
 		file, err := os.Create(filename)
 		if err != nil {
 			errMsg := fmt.Sprintf("Failed to create panic log: %v", err)
-			Error(errMsg)
+			slog.Error(errMsg)
 			status.Error(errMsg)
 		} else {
 			defer file.Close()
@@ -54,7 +38,7 @@ func RecoverPanic(name string, cleanup func()) {
 			fmt.Fprintf(file, "Stack Trace:\n%s\n", debug.Stack())
 
 			infoMsg := fmt.Sprintf("Panic details written to %s", filename)
-			Info(infoMsg)
+			slog.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"
 	"fmt"
 	"strings"
-	"sync"
 	"time"
 
 	"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{}
 
 func (w *writer) Write(p []byte) (int, error) {
 	d := logfmt.NewDecoder(bytes.NewReader(p))
 	for d.ScanRecord() {
-		msg := LogMessage{
-			ID:   fmt.Sprintf("%d", time.Now().UnixNano()),
-			Time: time.Now(),
-		}
+		msg := Log{}
+
 		for d.ScanKeyval() {
 			switch string(d.Key()) {
 			case "time":
@@ -65,19 +25,21 @@ func (w *writer) Write(p []byte) (int, error) {
 				if err != nil {
 					return 0, fmt.Errorf("parsing time: %w", err)
 				}
-				msg.Time = parsed
+				msg.Timestamp = parsed.UnixMilli()
 			case "level":
 				msg.Level = strings.ToLower(string(d.Value()))
 			case "msg":
 				msg.Message = string(d.Value())
 			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 {
 		return 0, d.Err()
@@ -89,11 +51,3 @@ func NewWriter() *writer {
 	w := &writer{}
 	return w
 }
-
-func Subscribe(ctx context.Context) <-chan pubsub.Event[LogMessage] {
-	return defaultLogData.Subscribe(ctx)
-}
-
-func List() []LogMessage {
-	return defaultLogData.List()
-}

+ 18 - 16
internal/lsp/client.go

@@ -14,6 +14,8 @@ import (
 	"sync/atomic"
 	"time"
 
+	"log/slog"
+
 	"github.com/opencode-ai/opencode/internal/config"
 	"github.com/opencode-ai/opencode/internal/logging"
 	"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() {
 		scanner := bufio.NewScanner(stderr)
 		for scanner.Scan() {
-			logging.Info("LSP Server", "message", scanner.Text())
+			slog.Info("LSP Server", "message", scanner.Text())
 		}
 		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()
 
 	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
@@ -310,7 +312,7 @@ func (c *Client) WaitForServerReady(ctx context.Context) error {
 	// For TypeScript-like servers, we need to open some key files first
 	if serverType == ServerTypeTypeScript {
 		if 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)
 	}
@@ -327,15 +329,15 @@ func (c *Client) WaitForServerReady(ctx context.Context) error {
 				// Server responded successfully
 				c.SetServerState(StateReady)
 				if cnf.DebugLSP {
-					logging.Debug("LSP server is ready")
+					slog.Debug("LSP server is ready")
 				}
 				return nil
 			} else {
-				logging.Debug("LSP server not ready yet", "error", err, "serverType", serverType)
+				slog.Debug("LSP server not ready yet", "error", err, "serverType", serverType)
 			}
 
 			if 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 {
 			// File exists, try to open it
 			if err := c.OpenFile(ctx, file); err != nil {
-				logging.Debug("Failed to open key config file", "file", file, "error", err)
+				slog.Debug("Failed to open key config file", "file", file, "error", err)
 			} else {
-				logging.Debug("Opened key config file for initialization", "file", file)
+				slog.Debug("Opened key config file for initialization", "file", file)
 			}
 		}
 	}
@@ -488,7 +490,7 @@ func (c *Client) pingTypeScriptServer(ctx context.Context) error {
 		return nil
 	})
 	if err != nil {
-		logging.Debug("Error walking directory for TypeScript files", "error", err)
+		slog.Debug("Error walking directory for TypeScript files", "error", err)
 	}
 
 	// Final fallback - just try a generic capability
@@ -528,7 +530,7 @@ func (c *Client) openTypeScriptFiles(ctx context.Context, workDir string) {
 			if err := c.OpenFile(ctx, path); err == nil {
 				filesOpened++
 				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 {
-		logging.Debug("Error walking directory for TypeScript files", "error", err)
+		slog.Debug("Error walking directory for TypeScript files", "error", err)
 	}
 
 	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 {
-		logging.Debug("Closing file", "file", filepath)
+		slog.Debug("Closing file", "file", filepath)
 	}
 	if err := c.Notify(ctx, "textDocument/didClose", params); err != nil {
 		return err
@@ -730,12 +732,12 @@ func (c *Client) CloseAllFiles(ctx context.Context) {
 	for _, filePath := range filesToClose {
 		err := c.CloseFile(ctx, filePath)
 		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 {
-		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"
 
 	"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
@@ -23,9 +23,9 @@ func IntegrateLSPServers(workingDir string) error {
 
 	// Always run language detection, but log differently for first run vs. subsequent runs
 	if shouldInit || len(cfg.LSP) == 0 {
-		logging.Info("Running initial LSP auto-discovery...")
+		slog.Info("Running initial LSP auto-discovery...")
 	} else {
-		logging.Debug("Running LSP auto-discovery to detect new languages...")
+		slog.Debug("Running LSP auto-discovery to detect new languages...")
 	}
 
 	// Configure LSP servers
@@ -38,7 +38,7 @@ func IntegrateLSPServers(workingDir string) error {
 	for langID, serverInfo := range servers {
 		// Skip languages that already have a configured server
 		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
 		}
 
@@ -49,12 +49,12 @@ func IntegrateLSPServers(workingDir string) error {
 				Command:  serverInfo.Path,
 				Args:     serverInfo.Args,
 			}
-			logging.Info("Added LSP server to configuration",
+			slog.Info("Added LSP server to configuration",
 				"language", langID,
 				"command", serverInfo.Command,
 				"path", serverInfo.Path)
 		} else {
-			logging.Warn("LSP server not available",
+			slog.Warn("LSP server not available",
 				"language", langID,
 				"command", serverInfo.Command,
 				"installCmd", serverInfo.InstallCmd)
@@ -63,4 +63,3 @@ func IntegrateLSPServers(workingDir string) error {
 
 	return nil
 }
-

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

@@ -6,8 +6,8 @@ import (
 	"strings"
 	"sync"
 
-	"github.com/opencode-ai/opencode/internal/logging"
 	"github.com/opencode-ai/opencode/internal/lsp"
+	"log/slog"
 )
 
 // LanguageInfo stores information about a detected language
@@ -206,9 +206,9 @@ func DetectLanguages(rootDir string) (map[string]LanguageInfo, error) {
 	// Log detected languages
 	for id, info := range languages {
 		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 {
-			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
 	langKind := lsp.DetectLanguageID(uri)
 	return GetLanguageIDFromProtocol(string(langKind))
-}
+}
+

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

@@ -8,7 +8,7 @@ import (
 	"runtime"
 	"strings"
 
-	"github.com/opencode-ai/opencode/internal/logging"
+	"log/slog"
 )
 
 // ServerInfo contains information about an LSP server
@@ -114,7 +114,7 @@ func FindLSPServer(languageID string) (ServerInfo, error) {
 	if err == nil {
 		serverInfo.Available = true
 		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
 	}
 
@@ -125,13 +125,13 @@ func FindLSPServer(languageID string) (ServerInfo, error) {
 			// Found the server
 			serverInfo.Available = true
 			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
 		}
 	}
 
 	// 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)
 }
 
@@ -140,7 +140,7 @@ func getCommonLSPPaths(languageID, command string) []string {
 	var paths []string
 	homeDir, err := os.UserHomeDir()
 	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
 	}
 
@@ -148,21 +148,21 @@ func getCommonLSPPaths(languageID, command string) []string {
 	switch runtime.GOOS {
 	case "darwin":
 		// macOS paths
-		paths = append(paths, 
+		paths = append(paths,
 			fmt.Sprintf("/usr/local/bin/%s", command),
 			fmt.Sprintf("/opt/homebrew/bin/%s", command),
 			fmt.Sprintf("%s/.local/bin/%s", homeDir, command),
 		)
 	case "linux":
 		// Linux paths
-		paths = append(paths, 
+		paths = append(paths,
 			fmt.Sprintf("/usr/bin/%s", command),
 			fmt.Sprintf("/usr/local/bin/%s", command),
 			fmt.Sprintf("%s/.local/bin/%s", homeDir, command),
 		)
 	case "windows":
 		// Windows paths
-		paths = append(paths, 
+		paths = append(paths,
 			fmt.Sprintf("%s\\AppData\\Local\\Programs\\%s.exe", homeDir, 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":
 		// Node.js global packages
 		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\\node_modules\\.bin\\%s.cmd", homeDir, command),
 			)
 		} else {
-			paths = append(paths, 
+			paths = append(paths,
 				fmt.Sprintf("%s/.npm-global/bin/%s", homeDir, command),
 				fmt.Sprintf("%s/.nvm/versions/node/*/bin/%s", homeDir, command),
 				fmt.Sprintf("/usr/local/lib/node_modules/.bin/%s", command),
@@ -196,12 +196,12 @@ func getCommonLSPPaths(languageID, command string) []string {
 	case "python":
 		// Python paths
 		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("C:\\Python*\\Scripts\\%s.exe", command),
 			)
 		} else {
-			paths = append(paths, 
+			paths = append(paths,
 				fmt.Sprintf("%s/.local/bin/%s", homeDir, command),
 				fmt.Sprintf("%s/.pyenv/shims/%s", homeDir, command),
 				fmt.Sprintf("/usr/local/bin/%s", command),
@@ -210,12 +210,12 @@ func getCommonLSPPaths(languageID, command string) []string {
 	case "rust":
 		// Rust paths
 		if runtime.GOOS == "windows" {
-			paths = append(paths, 
+			paths = append(paths,
 				fmt.Sprintf("%s\\.rustup\\toolchains\\*\\bin\\%s.exe", homeDir, command),
 				fmt.Sprintf("%s\\.cargo\\bin\\%s.exe", homeDir, command),
 			)
 		} else {
-			paths = append(paths, 
+			paths = append(paths,
 				fmt.Sprintf("%s/.rustup/toolchains/*/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
 func getVSCodeExtensionsPath(homeDir string) string {
 	var basePath string
-	
+
 	switch runtime.GOOS {
 	case "darwin":
 		basePath = filepath.Join(homeDir, "Library", "Application Support", "Code", "User", "globalStorage")
@@ -259,12 +259,12 @@ func getVSCodeExtensionsPath(homeDir string) string {
 	default:
 		return ""
 	}
-	
+
 	// Check if the directory exists
 	if _, err := os.Stat(basePath); err != nil {
 		return ""
 	}
-	
+
 	return basePath
 }
 
@@ -275,32 +275,33 @@ func ConfigureLSPServers(rootDir string) (map[string]ServerInfo, error) {
 	if err != nil {
 		return nil, fmt.Errorf("failed to detect languages: %w", err)
 	}
-	
+
 	// Find LSP servers for detected languages
 	servers := make(map[string]ServerInfo)
 	for langID, langInfo := range languages {
 		// Prioritize primary languages but include all languages that have server definitions
 		if !langInfo.IsPrimary && langInfo.FileCount < 3 {
 			// 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
 		}
-		
+
 		// Check if we have a server for this language
 		serverInfo, err := FindLSPServer(langID)
 		if err != nil {
-			logging.Warn("LSP server not found", "language", langID, "error", err)
+			slog.Warn("LSP server not found", "language", langID, "error", err)
 			continue
 		}
-		
+
 		// Add to the map of configured servers
 		servers[langID] = serverInfo
 		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 {
-			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
-}
+}
+

+ 7 - 7
internal/lsp/handlers.go

@@ -4,9 +4,9 @@ import (
 	"encoding/json"
 
 	"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/util"
+	"log/slog"
 )
 
 // Requests
@@ -18,7 +18,7 @@ func HandleWorkspaceConfiguration(params json.RawMessage) (any, error) {
 func HandleRegisterCapability(params json.RawMessage) (any, error) {
 	var registerParams protocol.RegistrationParams
 	if err := json.Unmarshal(params, &registerParams); err != nil {
-		logging.Error("Error unmarshaling registration params", "error", err)
+		slog.Error("Error unmarshaling registration params", "error", err)
 		return nil, err
 	}
 
@@ -28,13 +28,13 @@ func HandleRegisterCapability(params json.RawMessage) (any, error) {
 			// Parse the registration options
 			optionsJSON, err := json.Marshal(reg.RegisterOptions)
 			if err != nil {
-				logging.Error("Error marshaling registration options", "error", err)
+				slog.Error("Error marshaling registration options", "error", err)
 				continue
 			}
 
 			var options protocol.DidChangeWatchedFilesRegistrationOptions
 			if err := json.Unmarshal(optionsJSON, &options); err != nil {
-				logging.Error("Error unmarshaling registration options", "error", err)
+				slog.Error("Error unmarshaling registration options", "error", err)
 				continue
 			}
 
@@ -54,7 +54,7 @@ func HandleApplyEdit(params json.RawMessage) (any, error) {
 
 	err := util.ApplyWorkspaceEdit(edit.Edit)
 	if err != nil {
-		logging.Error("Error applying workspace edit", "error", err)
+		slog.Error("Error applying workspace edit", "error", err)
 		return protocol.ApplyWorkspaceEditResult{Applied: false, FailureReason: err.Error()}, nil
 	}
 
@@ -89,7 +89,7 @@ func HandleServerMessage(params json.RawMessage) {
 	}
 	if err := json.Unmarshal(params, &msg); err == nil {
 		if 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) {
 	var diagParams protocol.PublishDiagnosticsParams
 	if err := json.Unmarshal(params, &diagParams); err != nil {
-		logging.Error("Error unmarshaling diagnostics params", "error", err)
+		slog.Error("Error unmarshaling diagnostics params", "error", err)
 		return
 	}
 

+ 16 - 16
internal/lsp/transport.go

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

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

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

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

@@ -16,13 +16,13 @@ import (
 	"github.com/charmbracelet/lipgloss"
 	"github.com/opencode-ai/opencode/internal/app"
 	"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/status"
 	"github.com/opencode-ai/opencode/internal/tui/image"
 	"github.com/opencode-ai/opencode/internal/tui/styles"
 	"github.com/opencode-ai/opencode/internal/tui/theme"
 	"github.com/opencode-ai/opencode/internal/tui/util"
+	"log/slog"
 )
 
 const (
@@ -376,7 +376,7 @@ func (f *filepickerCmp) IsCWDFocused() bool {
 func NewFilepickerCmp(app *app.App) FilepickerCmp {
 	homepath, err := os.UserHomeDir()
 	if err != nil {
-		logging.Error("error loading user files")
+		slog.Error("error loading user files")
 		return nil
 	}
 	baseDir := DirNode{parent: nil, directory: homepath}
@@ -392,7 +392,7 @@ func NewFilepickerCmp(app *app.App) FilepickerCmp {
 
 func (f *filepickerCmp) getCurrentFileBelowCursor() {
 	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")
 		return
 	}
@@ -405,7 +405,7 @@ func (f *filepickerCmp) getCurrentFileBelowCursor() {
 		go func() {
 			imageString, err := image.ImagePreview(f.viewport.Width-4, fullPath)
 			if err != nil {
-				logging.Error(err.Error())
+				slog.Error(err.Error())
 				f.viewport.SetContent("Preview unavailable")
 				return
 			}
@@ -418,7 +418,7 @@ func (f *filepickerCmp) getCurrentFileBelowCursor() {
 }
 
 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)
 	errChan := make(chan error, 1)

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

@@ -23,17 +23,12 @@ type DetailComponent interface {
 
 type detailCmp struct {
 	width, height int
-	currentLog    logging.LogMessage
+	currentLog    logging.Log
 	viewport      viewport.Model
 	focused       bool
 }
 
 func (i *detailCmp) Init() tea.Cmd {
-	messages := logging.List()
-	if len(messages) == 0 {
-		return nil
-	}
-	i.currentLog = messages[0]
 	return nil
 }
 
@@ -42,8 +37,12 @@ func (i *detailCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	switch msg := msg.(type) {
 	case selectedLogMsg:
 		if msg.ID != i.currentLog.ID {
-			i.currentLog = logging.LogMessage(msg)
-			i.updateContent()
+			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:
 		// 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, nil
+	return i, cmd
 }
 
 func (i *detailCmp) updateContent() {
@@ -66,9 +65,12 @@ func (i *detailCmp) updateContent() {
 	timeStyle := lipgloss.NewStyle().Foreground(t.TextMuted())
 	levelStyle := getLevelStyle(i.currentLog.Level)
 
+	// Format timestamp
+	timeStr := time.Unix(i.currentLog.Timestamp, 0).Format(time.RFC3339)
+
 	header := lipgloss.JoinHorizontal(
 		lipgloss.Center,
-		timeStyle.Render(i.currentLog.Time.Format(time.RFC3339)),
+		timeStyle.Render(timeStr),
 		"  ",
 		levelStyle.Render(i.currentLog.Level),
 	)
@@ -93,23 +95,33 @@ func (i *detailCmp) updateContent() {
 		keyStyle := lipgloss.NewStyle().Foreground(t.Primary()).Bold(true)
 		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("\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())
 }
 
 func getLevelStyle(level string) lipgloss.Style {
 	style := lipgloss.NewStyle().Bold(true)
 	t := theme.CurrentTheme()
-	
+
 	switch strings.ToLower(level) {
 	case "info":
 		return style.Foreground(t.Info())

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

@@ -1,15 +1,17 @@
 package logs
 
 import (
-	"slices"
+	"context"
+	"time"
 
 	"github.com/charmbracelet/bubbles/key"
 	"github.com/charmbracelet/bubbles/table"
 	tea "github.com/charmbracelet/bubbletea"
 	"github.com/opencode-ai/opencode/internal/logging"
 	"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/styles"
 	"github.com/opencode-ai/opencode/internal/tui/theme"
 	"github.com/opencode-ai/opencode/internal/tui/util"
 )
@@ -23,46 +25,97 @@ type TableComponent interface {
 type tableCmp struct {
 	table   table.Model
 	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 {
-	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) {
 	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
 	}
-	
+
 	// Only process keyboard input when focused
 	if _, ok := msg.(tea.KeyMsg); ok && !i.focused {
 		return i, nil
 	}
-	
+
 	t, cmd := i.table.Update(msg)
 	cmds = append(cmds, cmd)
 	i.table = t
+	
+	// Only send selected log message when selection changes
 	selectedRow := i.table.SelectedRow()
 	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
 			}
 		}
-		if log.ID != "" {
-			cmds = append(cmds, util.CmdHandler(selectedLogMsg(log)))
-		}
 	}
 	return i, tea.Batch(cmds...)
 }
@@ -105,25 +158,20 @@ func (i *tableCmp) BindingKeys() []key.Binding {
 	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
 		row := table.Row{
 			log.ID,
-			log.Time.Format("15:04:05"),
+			timeStr,
 			log.Level,
 			log.Message,
 		}
@@ -146,6 +194,7 @@ func NewLogsTable() TableComponent {
 	tableModel.Focus()
 	return &tableCmp{
 		table: tableModel,
+		logs:  []logging.Log{},
 	}
 }
 

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

@@ -2,13 +2,13 @@ package theme
 
 import (
 	"fmt"
+	"log/slog"
 	"slices"
 	"strings"
 	"sync"
 
 	"github.com/alecthomas/chroma/v2/styles"
 	"github.com/opencode-ai/opencode/internal/config"
-	"github.com/opencode-ai/opencode/internal/logging"
 )
 
 // Manager handles theme registration, selection, and retrieval.
@@ -49,19 +49,19 @@ func SetTheme(name string) error {
 	defer globalManager.mu.Unlock()
 
 	delete(styles.Registry, "charm")
-	
+
 	// Handle custom theme
 	if name == "custom" {
 		cfg := config.Get()
 		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")
 		}
-		
+
 		customTheme, err := LoadCustomTheme(cfg.TUI.CustomTheme)
 		if err != nil {
 			return fmt.Errorf("failed to load custom theme: %w", err)
 		}
-		
+
 		// Register the custom theme
 		globalManager.themes["custom"] = customTheme
 	} else if _, exists := globalManager.themes[name]; !exists {
@@ -73,7 +73,7 @@ func SetTheme(name string) error {
 	// Update the config file using viper
 	if err := updateConfigTheme(name); err != nil {
 		// 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
@@ -140,7 +140,7 @@ func LoadCustomTheme(customTheme map[string]any) (Theme, error) {
 	for key, value := range customTheme {
 		adaptiveColor, err := ParseAdaptiveColor(value)
 		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
 		}
 
@@ -203,7 +203,7 @@ func LoadCustomTheme(customTheme map[string]any) (Theme, error) {
 		case "diffremovedlinenumberbg":
 			theme.DiffRemovedLineNumberBgColor = adaptiveColor
 		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...)
 
-	case pubsub.Event[logging.LogMessage]:
+	case pubsub.Event[logging.Log]:
 		a.pages[page.LogsPage], cmd = a.pages[page.LogsPage].Update(msg)
 		cmds = append(cmds, cmd)