summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authoradamdottv <[email protected]>2025-05-09 13:37:13 -0500
committeradamdottv <[email protected]>2025-05-09 13:37:13 -0500
commitf1007771997bd0401516eda87a7e0ac92f269680 (patch)
treed26198d031516eaebcc885870b470925492d8775
parentf41b7bbd0a0cc731fd7c471b7ee8b26f14a21755 (diff)
downloadopencode-f1007771997bd0401516eda87a7e0ac92f269680.tar.gz
opencode-f1007771997bd0401516eda87a7e0ac92f269680.zip
wip: logging improvements
-rw-r--r--cmd/root.go47
-rw-r--r--internal/app/app.go16
-rw-r--r--internal/app/lsp.go34
-rw-r--r--internal/config/config.go67
-rw-r--r--internal/db/connect.go10
-rw-r--r--internal/db/db.go30
-rw-r--r--internal/db/logs.sql.go128
-rw-r--r--internal/db/migrations/20250508122310_create_logs_table.sql16
-rw-r--r--internal/db/models.go10
-rw-r--r--internal/db/querier.go4
-rw-r--r--internal/db/sql/logs.sql28
-rw-r--r--internal/llm/agent/agent.go8
-rw-r--r--internal/llm/agent/mcp-tools.go10
-rw-r--r--internal/llm/agent/tools.go2
-rw-r--r--internal/llm/prompt/prompt.go4
-rw-r--r--internal/llm/prompt/prompt_test.go6
-rw-r--r--internal/llm/provider/anthropic.go12
-rw-r--r--internal/llm/provider/gemini.go8
-rw-r--r--internal/llm/provider/openai.go8
-rw-r--r--internal/llm/provider/provider.go20
-rw-r--r--internal/llm/tools/edit.go12
-rw-r--r--internal/llm/tools/patch.go8
-rw-r--r--internal/llm/tools/write.go6
-rw-r--r--internal/logging/logging.go (renamed from internal/logging/logger.go)23
-rw-r--r--internal/logging/manager.go48
-rw-r--r--internal/logging/message.go19
-rw-r--r--internal/logging/service.go167
-rw-r--r--internal/logging/writer.go68
-rw-r--r--internal/lsp/client.go34
-rw-r--r--internal/lsp/discovery/integration.go13
-rw-r--r--internal/lsp/discovery/language.go9
-rw-r--r--internal/lsp/discovery/server.go53
-rw-r--r--internal/lsp/handlers.go14
-rw-r--r--internal/lsp/transport.go32
-rw-r--r--internal/lsp/watcher/watcher.go112
-rw-r--r--internal/session/manager.go15
-rw-r--r--internal/tui/components/dialog/filepicker.go10
-rw-r--r--internal/tui/components/logs/details.go42
-rw-r--r--internal/tui/components/logs/table.go117
-rw-r--r--internal/tui/theme/manager.go14
-rw-r--r--internal/tui/tui.go2
41 files changed, 848 insertions, 438 deletions
diff --git a/cmd/root.go b/cmd/root.go
index 6f7bf1a1f..7bf8485f4 100644
--- a/cmd/root.go
+++ b/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)
}
}
diff --git a/internal/app/app.go b/internal/app/app.go
index b4812fb46..42de24548 100644
--- a/internal/app/app.go
+++ b/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()
}
diff --git a/internal/app/lsp.go b/internal/app/lsp.go
index 934bd1a89..b3763fba6 100644
--- a/internal/app/lsp.go
+++ b/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)
}
diff --git a/internal/config/config.go b/internal/config/config.go
index 745d88d8e..bb9ec447c 100644
--- a/internal/config/config.go
+++ b/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
diff --git a/internal/db/connect.go b/internal/db/connect.go
index b8fcb7362..f77ebdd2a 100644
--- a/internal/db/connect.go
+++ b/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
diff --git a/internal/db/db.go b/internal/db/db.go
index e71b86227..dfd606cb5 100644
--- a/internal/db/db.go
+++ b/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,
diff --git a/internal/db/logs.sql.go b/internal/db/logs.sql.go
new file mode 100644
index 000000000..d227b472a
--- /dev/null
+++ b/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
+}
diff --git a/internal/db/migrations/20250508122310_create_logs_table.sql b/internal/db/migrations/20250508122310_create_logs_table.sql
new file mode 100644
index 000000000..f4876fbae
--- /dev/null
+++ b/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; \ No newline at end of file
diff --git a/internal/db/models.go b/internal/db/models.go
index 47028c19a..473e18db6 100644
--- a/internal/db/models.go
+++ b/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"`
diff --git a/internal/db/querier.go b/internal/db/querier.go
index ee0a2f7bd..08ad41ec7 100644
--- a/internal/db/querier.go
+++ b/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)
diff --git a/internal/db/sql/logs.sql b/internal/db/sql/logs.sql
new file mode 100644
index 000000000..1a0655f7e
--- /dev/null
+++ b/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 ?; \ No newline at end of file
diff --git a/internal/llm/agent/agent.go b/internal/llm/agent/agent.go
index 695826cdf..295ac4654 100644
--- a/internal/llm/agent/agent.go
+++ b/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)
diff --git a/internal/llm/agent/mcp-tools.go b/internal/llm/agent/mcp-tools.go
index 237560641..9966b99d9 100644
--- a/internal/llm/agent/mcp-tools.go
+++ b/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)...)
diff --git a/internal/llm/agent/tools.go b/internal/llm/agent/tools.go
index 43e5978e4..b337efb59 100644
--- a/internal/llm/agent/tools.go
+++ b/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),
diff --git a/internal/llm/prompt/prompt.go b/internal/llm/prompt/prompt.go
index 83ec7442f..769fd51ab 100644
--- a/internal/llm/prompt/prompt.go
+++ b/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)
}
diff --git a/internal/llm/prompt/prompt_test.go b/internal/llm/prompt/prompt_test.go
index 405ad5194..fe492b136 100644
--- a/internal/llm/prompt/prompt_test.go
+++ b/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)
}
diff --git a/internal/llm/provider/anthropic.go b/internal/llm/provider/anthropic.go
index edd1c1d70..9c599df5b 100644
--- a/internal/llm/provider/anthropic.go
+++ b/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
}
diff --git a/internal/llm/provider/gemini.go b/internal/llm/provider/gemini.go
index 2986c715e..c37aee4b6 100644
--- a/internal/llm/provider/gemini.go
+++ b/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
diff --git a/internal/llm/provider/openai.go b/internal/llm/provider/openai.go
index 3bf8a6d42..777d9d8cc 100644
--- a/internal/llm/provider/openai.go
+++ b/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
}
diff --git a/internal/llm/provider/provider.go b/internal/llm/provider/provider.go
index 6aaf9ff09..45c63acac 100644
--- a/internal/llm/provider/provider.go
+++ b/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
}
diff --git a/internal/llm/tools/edit.go b/internal/llm/tools/edit.go
index a5f0687cb..44787f525 100644
--- a/internal/llm/tools/edit.go
+++ b/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)
diff --git a/internal/llm/tools/patch.go b/internal/llm/tools/patch.go
index dcd3027b5..e0c0bf5bc 100644
--- a/internal/llm/tools/patch.go
+++ b/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
diff --git a/internal/llm/tools/write.go b/internal/llm/tools/write.go
index decc51e47..617d69c29 100644
--- a/internal/llm/tools/write.go
+++ b/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)
diff --git a/internal/logging/logger.go b/internal/logging/logging.go
index 31462a542..af14a0ded 100644
--- a/internal/logging/logger.go
+++ b/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()) {
}
}
}
+
diff --git a/internal/logging/manager.go b/internal/logging/manager.go
new file mode 100644
index 000000000..e8e96520b
--- /dev/null
+++ b/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)
+}
+
diff --git a/internal/logging/message.go b/internal/logging/message.go
deleted file mode 100644
index b8a42d966..000000000
--- a/internal/logging/message.go
+++ /dev/null
@@ -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
-}
diff --git a/internal/logging/service.go b/internal/logging/service.go
new file mode 100644
index 000000000..8cf8039de
--- /dev/null
+++ b/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
+}
diff --git a/internal/logging/writer.go b/internal/logging/writer.go
index 7191f7720..4b5bcc4fe 100644
--- a/internal/logging/writer.go
+++ b/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()
-}
diff --git a/internal/lsp/client.go b/internal/lsp/client.go
index 290a01cba..ca2016691 100644
--- a/internal/lsp/client.go
+++ b/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)
}
}
diff --git a/internal/lsp/discovery/integration.go b/internal/lsp/discovery/integration.go
index 7820b6449..d44389384 100644
--- a/internal/lsp/discovery/integration.go
+++ b/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
}
-
diff --git a/internal/lsp/discovery/language.go b/internal/lsp/discovery/language.go
index 5e0a8d1af..69fef01d5 100644
--- a/internal/lsp/discovery/language.go
+++ b/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))
-} \ No newline at end of file
+}
+
diff --git a/internal/lsp/discovery/server.go b/internal/lsp/discovery/server.go
index 2b7d4eeb6..98b56bc18 100644
--- a/internal/lsp/discovery/server.go
+++ b/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
-} \ No newline at end of file
+}
+
diff --git a/internal/lsp/handlers.go b/internal/lsp/handlers.go
index e24945b42..656ec1228 100644
--- a/internal/lsp/handlers.go
+++ b/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
}
diff --git a/internal/lsp/transport.go b/internal/lsp/transport.go
index 9b07d53c9..577ba2ed0 100644
--- a/internal/lsp/transport.go
+++ b/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)
diff --git a/internal/lsp/watcher/watcher.go b/internal/lsp/watcher/watcher.go
index 58ad25695..ed8fe6b76 100644
--- a/internal/lsp/watcher/watcher.go
+++ b/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)
}
}
}
diff --git a/internal/session/manager.go b/internal/session/manager.go
index 8df421bd8..cd80fa584 100644
--- a/internal/session/manager.go
+++ b/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
-} \ No newline at end of file
+}
diff --git a/internal/tui/components/dialog/filepicker.go b/internal/tui/components/dialog/filepicker.go
index f747dc9af..fe4223f5d 100644
--- a/internal/tui/components/dialog/filepicker.go
+++ b/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)
diff --git a/internal/tui/components/logs/details.go b/internal/tui/components/logs/details.go
index 7bbfd17dc..da6edff13 100644
--- a/internal/tui/components/logs/details.go
+++ b/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())
diff --git a/internal/tui/components/logs/table.go b/internal/tui/components/logs/table.go
index fe30c6aa0..e7fc4ea70 100644
--- a/internal/tui/components/logs/table.go
+++ b/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{},
}
}
diff --git a/internal/tui/theme/manager.go b/internal/tui/theme/manager.go
index 7bb887ffa..c9e9f4205 100644
--- a/internal/tui/theme/manager.go
+++ b/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)
}
}
diff --git a/internal/tui/tui.go b/internal/tui/tui.go
index 55269af65..ddc574328 100644
--- a/internal/tui/tui.go
+++ b/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)