wip: logging improvements

This commit is contained in:
adamdottv 2025-05-09 13:37:13 -05:00
parent f41b7bbd0a
commit f100777199
No known key found for this signature in database
GPG key ID: 9CB48779AF150E75
41 changed files with 848 additions and 438 deletions

View file

@ -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)
}
}

View file

@ -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()
}

View file

@ -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)
}

View file

@ -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

View 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

View file

@ -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
View 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
}

View 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;

View file

@ -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"`

View file

@ -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
View 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 ?;

View file

@ -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)

View file

@ -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)...)

View file

@ -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),

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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
}

View file

@ -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

View file

@ -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
}

View file

@ -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
}

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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()) {
}
}
}

View 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)
}

View file

@ -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
View 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
}

View file

@ -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()
}

View file

@ -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)
}
}

View file

@ -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
}

View file

@ -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))
}
}

View file

@ -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
}
}

View file

@ -4,9 +4,9 @@ import (
"encoding/json"
"github.com/opencode-ai/opencode/internal/config"
"github.com/opencode-ai/opencode/internal/logging"
"github.com/opencode-ai/opencode/internal/lsp/protocol"
"github.com/opencode-ai/opencode/internal/lsp/util"
"log/slog"
)
// Requests
@ -18,7 +18,7 @@ func HandleWorkspaceConfiguration(params json.RawMessage) (any, error) {
func HandleRegisterCapability(params json.RawMessage) (any, error) {
var registerParams protocol.RegistrationParams
if err := json.Unmarshal(params, &registerParams); err != nil {
logging.Error("Error unmarshaling registration params", "error", err)
slog.Error("Error unmarshaling registration params", "error", err)
return nil, err
}
@ -28,13 +28,13 @@ func HandleRegisterCapability(params json.RawMessage) (any, error) {
// Parse the registration options
optionsJSON, err := json.Marshal(reg.RegisterOptions)
if err != nil {
logging.Error("Error marshaling registration options", "error", err)
slog.Error("Error marshaling registration options", "error", err)
continue
}
var options protocol.DidChangeWatchedFilesRegistrationOptions
if err := json.Unmarshal(optionsJSON, &options); err != nil {
logging.Error("Error unmarshaling registration options", "error", err)
slog.Error("Error unmarshaling registration options", "error", err)
continue
}
@ -54,7 +54,7 @@ func HandleApplyEdit(params json.RawMessage) (any, error) {
err := util.ApplyWorkspaceEdit(edit.Edit)
if err != nil {
logging.Error("Error applying workspace edit", "error", err)
slog.Error("Error applying workspace edit", "error", err)
return protocol.ApplyWorkspaceEditResult{Applied: false, FailureReason: err.Error()}, nil
}
@ -89,7 +89,7 @@ func HandleServerMessage(params json.RawMessage) {
}
if err := json.Unmarshal(params, &msg); err == nil {
if cnf.DebugLSP {
logging.Debug("Server message", "type", msg.Type, "message", msg.Message)
slog.Debug("Server message", "type", msg.Type, "message", msg.Message)
}
}
}
@ -97,7 +97,7 @@ func HandleServerMessage(params json.RawMessage) {
func HandleDiagnostics(client *Client, params json.RawMessage) {
var diagParams protocol.PublishDiagnosticsParams
if err := json.Unmarshal(params, &diagParams); err != nil {
logging.Error("Error unmarshaling diagnostics params", "error", err)
slog.Error("Error unmarshaling diagnostics params", "error", err)
return
}

View file

@ -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)

View file

@ -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)
}
}
}

View file

@ -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
}
}

View file

@ -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)

View file

@ -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())

View file

@ -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{},
}
}

View file

@ -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)
}
}

View file

@ -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)