mirror of
https://github.com/sst/opencode.git
synced 2025-08-04 13:30:52 +00:00
wip: logging improvements
This commit is contained in:
parent
f41b7bbd0a
commit
f100777199
41 changed files with 848 additions and 438 deletions
47
cmd/root.go
47
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -27,6 +27,9 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) {
|
|||
if q.createFileStmt, err = db.PrepareContext(ctx, createFile); err != nil {
|
||||
return nil, fmt.Errorf("error preparing query CreateFile: %w", err)
|
||||
}
|
||||
if q.createLogStmt, err = db.PrepareContext(ctx, createLog); err != nil {
|
||||
return nil, fmt.Errorf("error preparing query CreateLog: %w", err)
|
||||
}
|
||||
if q.createMessageStmt, err = db.PrepareContext(ctx, createMessage); err != nil {
|
||||
return nil, fmt.Errorf("error preparing query CreateMessage: %w", err)
|
||||
}
|
||||
|
@ -60,6 +63,9 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) {
|
|||
if q.getSessionByIDStmt, err = db.PrepareContext(ctx, getSessionByID); err != nil {
|
||||
return nil, fmt.Errorf("error preparing query GetSessionByID: %w", err)
|
||||
}
|
||||
if q.listAllLogsStmt, err = db.PrepareContext(ctx, listAllLogs); err != nil {
|
||||
return nil, fmt.Errorf("error preparing query ListAllLogs: %w", err)
|
||||
}
|
||||
if q.listFilesByPathStmt, err = db.PrepareContext(ctx, listFilesByPath); err != nil {
|
||||
return nil, fmt.Errorf("error preparing query ListFilesByPath: %w", err)
|
||||
}
|
||||
|
@ -69,6 +75,9 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) {
|
|||
if q.listLatestSessionFilesStmt, err = db.PrepareContext(ctx, listLatestSessionFiles); err != nil {
|
||||
return nil, fmt.Errorf("error preparing query ListLatestSessionFiles: %w", err)
|
||||
}
|
||||
if q.listLogsBySessionStmt, err = db.PrepareContext(ctx, listLogsBySession); err != nil {
|
||||
return nil, fmt.Errorf("error preparing query ListLogsBySession: %w", err)
|
||||
}
|
||||
if q.listMessagesBySessionStmt, err = db.PrepareContext(ctx, listMessagesBySession); err != nil {
|
||||
return nil, fmt.Errorf("error preparing query ListMessagesBySession: %w", err)
|
||||
}
|
||||
|
@ -100,6 +109,11 @@ func (q *Queries) Close() error {
|
|||
err = fmt.Errorf("error closing createFileStmt: %w", cerr)
|
||||
}
|
||||
}
|
||||
if q.createLogStmt != nil {
|
||||
if cerr := q.createLogStmt.Close(); cerr != nil {
|
||||
err = fmt.Errorf("error closing createLogStmt: %w", cerr)
|
||||
}
|
||||
}
|
||||
if q.createMessageStmt != nil {
|
||||
if cerr := q.createMessageStmt.Close(); cerr != nil {
|
||||
err = fmt.Errorf("error closing createMessageStmt: %w", cerr)
|
||||
|
@ -155,6 +169,11 @@ func (q *Queries) Close() error {
|
|||
err = fmt.Errorf("error closing getSessionByIDStmt: %w", cerr)
|
||||
}
|
||||
}
|
||||
if q.listAllLogsStmt != nil {
|
||||
if cerr := q.listAllLogsStmt.Close(); cerr != nil {
|
||||
err = fmt.Errorf("error closing listAllLogsStmt: %w", cerr)
|
||||
}
|
||||
}
|
||||
if q.listFilesByPathStmt != nil {
|
||||
if cerr := q.listFilesByPathStmt.Close(); cerr != nil {
|
||||
err = fmt.Errorf("error closing listFilesByPathStmt: %w", cerr)
|
||||
|
@ -170,6 +189,11 @@ func (q *Queries) Close() error {
|
|||
err = fmt.Errorf("error closing listLatestSessionFilesStmt: %w", cerr)
|
||||
}
|
||||
}
|
||||
if q.listLogsBySessionStmt != nil {
|
||||
if cerr := q.listLogsBySessionStmt.Close(); cerr != nil {
|
||||
err = fmt.Errorf("error closing listLogsBySessionStmt: %w", cerr)
|
||||
}
|
||||
}
|
||||
if q.listMessagesBySessionStmt != nil {
|
||||
if cerr := q.listMessagesBySessionStmt.Close(); cerr != nil {
|
||||
err = fmt.Errorf("error closing listMessagesBySessionStmt: %w", cerr)
|
||||
|
@ -245,6 +269,7 @@ type Queries struct {
|
|||
db DBTX
|
||||
tx *sql.Tx
|
||||
createFileStmt *sql.Stmt
|
||||
createLogStmt *sql.Stmt
|
||||
createMessageStmt *sql.Stmt
|
||||
createSessionStmt *sql.Stmt
|
||||
deleteFileStmt *sql.Stmt
|
||||
|
@ -256,9 +281,11 @@ type Queries struct {
|
|||
getFileByPathAndSessionStmt *sql.Stmt
|
||||
getMessageStmt *sql.Stmt
|
||||
getSessionByIDStmt *sql.Stmt
|
||||
listAllLogsStmt *sql.Stmt
|
||||
listFilesByPathStmt *sql.Stmt
|
||||
listFilesBySessionStmt *sql.Stmt
|
||||
listLatestSessionFilesStmt *sql.Stmt
|
||||
listLogsBySessionStmt *sql.Stmt
|
||||
listMessagesBySessionStmt *sql.Stmt
|
||||
listMessagesBySessionAfterStmt *sql.Stmt
|
||||
listNewFilesStmt *sql.Stmt
|
||||
|
@ -273,6 +300,7 @@ func (q *Queries) WithTx(tx *sql.Tx) *Queries {
|
|||
db: tx,
|
||||
tx: tx,
|
||||
createFileStmt: q.createFileStmt,
|
||||
createLogStmt: q.createLogStmt,
|
||||
createMessageStmt: q.createMessageStmt,
|
||||
createSessionStmt: q.createSessionStmt,
|
||||
deleteFileStmt: q.deleteFileStmt,
|
||||
|
@ -284,9 +312,11 @@ func (q *Queries) WithTx(tx *sql.Tx) *Queries {
|
|||
getFileByPathAndSessionStmt: q.getFileByPathAndSessionStmt,
|
||||
getMessageStmt: q.getMessageStmt,
|
||||
getSessionByIDStmt: q.getSessionByIDStmt,
|
||||
listAllLogsStmt: q.listAllLogsStmt,
|
||||
listFilesByPathStmt: q.listFilesByPathStmt,
|
||||
listFilesBySessionStmt: q.listFilesBySessionStmt,
|
||||
listLatestSessionFilesStmt: q.listLatestSessionFilesStmt,
|
||||
listLogsBySessionStmt: q.listLogsBySessionStmt,
|
||||
listMessagesBySessionStmt: q.listMessagesBySessionStmt,
|
||||
listMessagesBySessionAfterStmt: q.listMessagesBySessionAfterStmt,
|
||||
listNewFilesStmt: q.listNewFilesStmt,
|
||||
|
|
128
internal/db/logs.sql.go
Normal file
128
internal/db/logs.sql.go
Normal file
|
@ -0,0 +1,128 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.29.0
|
||||
// source: logs.sql
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
const createLog = `-- name: CreateLog :exec
|
||||
INSERT INTO logs (
|
||||
id,
|
||||
session_id,
|
||||
timestamp,
|
||||
level,
|
||||
message,
|
||||
attributes,
|
||||
created_at
|
||||
) VALUES (
|
||||
?,
|
||||
?,
|
||||
?,
|
||||
?,
|
||||
?,
|
||||
?,
|
||||
?
|
||||
)
|
||||
`
|
||||
|
||||
type CreateLogParams struct {
|
||||
ID string `json:"id"`
|
||||
SessionID sql.NullString `json:"session_id"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
Level string `json:"level"`
|
||||
Message string `json:"message"`
|
||||
Attributes sql.NullString `json:"attributes"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateLog(ctx context.Context, arg CreateLogParams) error {
|
||||
_, err := q.exec(ctx, q.createLogStmt, createLog,
|
||||
arg.ID,
|
||||
arg.SessionID,
|
||||
arg.Timestamp,
|
||||
arg.Level,
|
||||
arg.Message,
|
||||
arg.Attributes,
|
||||
arg.CreatedAt,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
const listAllLogs = `-- name: ListAllLogs :many
|
||||
SELECT id, session_id, timestamp, level, message, attributes, created_at FROM logs
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT ?
|
||||
`
|
||||
|
||||
func (q *Queries) ListAllLogs(ctx context.Context, limit int64) ([]Log, error) {
|
||||
rows, err := q.query(ctx, q.listAllLogsStmt, listAllLogs, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []Log{}
|
||||
for rows.Next() {
|
||||
var i Log
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.SessionID,
|
||||
&i.Timestamp,
|
||||
&i.Level,
|
||||
&i.Message,
|
||||
&i.Attributes,
|
||||
&i.CreatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listLogsBySession = `-- name: ListLogsBySession :many
|
||||
SELECT id, session_id, timestamp, level, message, attributes, created_at FROM logs
|
||||
WHERE session_id = ?
|
||||
ORDER BY timestamp ASC
|
||||
`
|
||||
|
||||
func (q *Queries) ListLogsBySession(ctx context.Context, sessionID sql.NullString) ([]Log, error) {
|
||||
rows, err := q.query(ctx, q.listLogsBySessionStmt, listLogsBySession, sessionID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []Log{}
|
||||
for rows.Next() {
|
||||
var i Log
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.SessionID,
|
||||
&i.Timestamp,
|
||||
&i.Level,
|
||||
&i.Message,
|
||||
&i.Attributes,
|
||||
&i.CreatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
16
internal/db/migrations/20250508122310_create_logs_table.sql
Normal file
16
internal/db/migrations/20250508122310_create_logs_table.sql
Normal file
|
@ -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;
|
|
@ -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"`
|
||||
|
|
|
@ -6,10 +6,12 @@ package db
|
|||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
type Querier interface {
|
||||
CreateFile(ctx context.Context, arg CreateFileParams) (File, error)
|
||||
CreateLog(ctx context.Context, arg CreateLogParams) error
|
||||
CreateMessage(ctx context.Context, arg CreateMessageParams) (Message, error)
|
||||
CreateSession(ctx context.Context, arg CreateSessionParams) (Session, error)
|
||||
DeleteFile(ctx context.Context, id string) error
|
||||
|
@ -21,9 +23,11 @@ type Querier interface {
|
|||
GetFileByPathAndSession(ctx context.Context, arg GetFileByPathAndSessionParams) (File, error)
|
||||
GetMessage(ctx context.Context, id string) (Message, error)
|
||||
GetSessionByID(ctx context.Context, id string) (Session, error)
|
||||
ListAllLogs(ctx context.Context, limit int64) ([]Log, error)
|
||||
ListFilesByPath(ctx context.Context, path string) ([]File, error)
|
||||
ListFilesBySession(ctx context.Context, sessionID string) ([]File, error)
|
||||
ListLatestSessionFiles(ctx context.Context, sessionID string) ([]File, error)
|
||||
ListLogsBySession(ctx context.Context, sessionID sql.NullString) ([]Log, error)
|
||||
ListMessagesBySession(ctx context.Context, sessionID string) ([]Message, error)
|
||||
ListMessagesBySessionAfter(ctx context.Context, arg ListMessagesBySessionAfterParams) ([]Message, error)
|
||||
ListNewFiles(ctx context.Context) ([]File, error)
|
||||
|
|
28
internal/db/sql/logs.sql
Normal file
28
internal/db/sql/logs.sql
Normal file
|
@ -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 ?;
|
|
@ -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)
|
||||
|
|
|
@ -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)...)
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -10,22 +10,6 @@ import (
|
|||
"github.com/opencode-ai/opencode/internal/status"
|
||||
)
|
||||
|
||||
func Info(msg string, args ...any) {
|
||||
slog.Info(msg, args...)
|
||||
}
|
||||
|
||||
func Debug(msg string, args ...any) {
|
||||
slog.Debug(msg, args...)
|
||||
}
|
||||
|
||||
func Warn(msg string, args ...any) {
|
||||
slog.Warn(msg, args...)
|
||||
}
|
||||
|
||||
func Error(msg string, args ...any) {
|
||||
slog.Error(msg, args...)
|
||||
}
|
||||
|
||||
// RecoverPanic is a common function to handle panics gracefully.
|
||||
// It logs the error, creates a panic log file with stack trace,
|
||||
// and executes an optional cleanup function before returning.
|
||||
|
@ -33,7 +17,7 @@ func RecoverPanic(name string, cleanup func()) {
|
|||
if r := recover(); r != nil {
|
||||
// Log the panic
|
||||
errorMsg := fmt.Sprintf("Panic in %s: %v", name, r)
|
||||
Error(errorMsg)
|
||||
slog.Error(errorMsg)
|
||||
status.Error(errorMsg)
|
||||
|
||||
// Create a timestamped panic log file
|
||||
|
@ -43,7 +27,7 @@ func RecoverPanic(name string, cleanup func()) {
|
|||
file, err := os.Create(filename)
|
||||
if err != nil {
|
||||
errMsg := fmt.Sprintf("Failed to create panic log: %v", err)
|
||||
Error(errMsg)
|
||||
slog.Error(errMsg)
|
||||
status.Error(errMsg)
|
||||
} else {
|
||||
defer file.Close()
|
||||
|
@ -54,7 +38,7 @@ func RecoverPanic(name string, cleanup func()) {
|
|||
fmt.Fprintf(file, "Stack Trace:\n%s\n", debug.Stack())
|
||||
|
||||
infoMsg := fmt.Sprintf("Panic details written to %s", filename)
|
||||
Info(infoMsg)
|
||||
slog.Info(infoMsg)
|
||||
status.Info(infoMsg)
|
||||
}
|
||||
|
||||
|
@ -64,3 +48,4 @@ func RecoverPanic(name string, cleanup func()) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
48
internal/logging/manager.go
Normal file
48
internal/logging/manager.go
Normal file
|
@ -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)
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
package logging
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// LogMessage is the event payload for a log message
|
||||
type LogMessage struct {
|
||||
ID string
|
||||
Time time.Time
|
||||
Level string
|
||||
Message string `json:"msg"`
|
||||
Attributes []Attr
|
||||
}
|
||||
|
||||
type Attr struct {
|
||||
Key string
|
||||
Value string
|
||||
}
|
167
internal/logging/service.go
Normal file
167
internal/logging/service.go
Normal file
|
@ -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
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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, ®isterParams); err != nil {
|
||||
logging.Error("Error unmarshaling registration params", "error", err)
|
||||
slog.Error("Error unmarshaling registration params", "error", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
@ -28,13 +28,13 @@ func HandleRegisterCapability(params json.RawMessage) (any, error) {
|
|||
// Parse the registration options
|
||||
optionsJSON, err := json.Marshal(reg.RegisterOptions)
|
||||
if err != nil {
|
||||
logging.Error("Error marshaling registration options", "error", err)
|
||||
slog.Error("Error marshaling registration options", "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
var options protocol.DidChangeWatchedFilesRegistrationOptions
|
||||
if err := json.Unmarshal(optionsJSON, &options); err != nil {
|
||||
logging.Error("Error unmarshaling registration options", "error", err)
|
||||
slog.Error("Error unmarshaling registration options", "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
|
@ -54,7 +54,7 @@ func HandleApplyEdit(params json.RawMessage) (any, error) {
|
|||
|
||||
err := util.ApplyWorkspaceEdit(edit.Edit)
|
||||
if err != nil {
|
||||
logging.Error("Error applying workspace edit", "error", err)
|
||||
slog.Error("Error applying workspace edit", "error", err)
|
||||
return protocol.ApplyWorkspaceEditResult{Applied: false, FailureReason: err.Error()}, nil
|
||||
}
|
||||
|
||||
|
@ -89,7 +89,7 @@ func HandleServerMessage(params json.RawMessage) {
|
|||
}
|
||||
if err := json.Unmarshal(params, &msg); err == nil {
|
||||
if 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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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{},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue