Browse Source

wip: refactoring tui

adamdottv 9 months ago
parent
commit
1c01ee4834
7 changed files with 6 additions and 1229 deletions
  1. 5 15
      cmd/root.go
  2. 0 65
      cmd/schema/README.md
  3. 0 337
      cmd/schema/main.go
  4. 0 441
      internal/history/history.go
  5. 0 292
      internal/logging/logging.go
  6. 1 73
      internal/tui/components/chat/sidebar.go
  7. 0 6
      main.go

+ 5 - 15
cmd/root.go

@@ -13,7 +13,6 @@ import (
 	zone "github.com/lrstanley/bubblezone"
 	zone "github.com/lrstanley/bubblezone"
 	"github.com/spf13/cobra"
 	"github.com/spf13/cobra"
 	"github.com/sst/opencode/internal/config"
 	"github.com/sst/opencode/internal/config"
-	"github.com/sst/opencode/internal/logging"
 	"github.com/sst/opencode/internal/pubsub"
 	"github.com/sst/opencode/internal/pubsub"
 	"github.com/sst/opencode/internal/tui"
 	"github.com/sst/opencode/internal/tui"
 	"github.com/sst/opencode/internal/tui/app"
 	"github.com/sst/opencode/internal/tui/app"
@@ -107,9 +106,9 @@ to assist developers in writing, debugging, and understanding code directly from
 		// Set up message handling for the TUI
 		// Set up message handling for the TUI
 		go func() {
 		go func() {
 			defer tuiWg.Done()
 			defer tuiWg.Done()
-			defer logging.RecoverPanic("TUI-message-handler", func() {
-				attemptTUIRecovery(program)
-			})
+			// defer logging.RecoverPanic("TUI-message-handler", func() {
+			// 	attemptTUIRecovery(program)
+			// })
 
 
 			for {
 			for {
 				select {
 				select {
@@ -157,15 +156,6 @@ to assist developers in writing, debugging, and understanding code directly from
 	},
 	},
 }
 }
 
 
-// attemptTUIRecovery tries to recover the TUI after a panic
-func attemptTUIRecovery(program *tea.Program) {
-	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
-	program.Quit()
-}
-
 func setupSubscriber[T any](
 func setupSubscriber[T any](
 	ctx context.Context,
 	ctx context.Context,
 	wg *sync.WaitGroup,
 	wg *sync.WaitGroup,
@@ -176,7 +166,7 @@ func setupSubscriber[T any](
 	wg.Add(1)
 	wg.Add(1)
 	go func() {
 	go func() {
 		defer wg.Done()
 		defer wg.Done()
-		defer logging.RecoverPanic(fmt.Sprintf("subscription-%s", name), nil)
+		// defer logging.RecoverPanic(fmt.Sprintf("subscription-%s", name), nil)
 
 
 		subCh := subscriber(ctx)
 		subCh := subscriber(ctx)
 		if subCh == nil {
 		if subCh == nil {
@@ -224,7 +214,7 @@ func setupSubscriptions(app *app.App, parentCtx context.Context) (chan tea.Msg,
 
 
 		waitCh := make(chan struct{})
 		waitCh := make(chan struct{})
 		go func() {
 		go func() {
-			defer logging.RecoverPanic("subscription-cleanup", nil)
+			// defer logging.RecoverPanic("subscription-cleanup", nil)
 			wg.Wait()
 			wg.Wait()
 			close(waitCh)
 			close(waitCh)
 		}()
 		}()

+ 0 - 65
cmd/schema/README.md

@@ -1,65 +0,0 @@
-# OpenCode Configuration Schema Generator
-
-This tool generates a JSON Schema for the OpenCode configuration file. The schema can be used to validate configuration files and provide autocompletion in editors that support JSON Schema.
-
-## Usage
-
-```bash
-go run cmd/schema/main.go > opencode-schema.json
-```
-
-This will generate a JSON Schema file that can be used to validate configuration files.
-
-## Schema Features
-
-The generated schema includes:
-
-- All configuration options with descriptions
-- Default values where applicable
-- Validation for enum values (e.g., model IDs, provider types)
-- Required fields
-- Type checking
-
-## Using the Schema
-
-You can use the generated schema in several ways:
-
-1. **Editor Integration**: Many editors (VS Code, JetBrains IDEs, etc.) support JSON Schema for validation and autocompletion. You can configure your editor to use the generated schema for `.opencode.json` files.
-
-2. **Validation Tools**: You can use tools like [jsonschema](https://github.com/Julian/jsonschema) to validate your configuration files against the schema.
-
-3. **Documentation**: The schema serves as documentation for the configuration options.
-
-## Example Configuration
-
-Here's an example configuration that conforms to the schema:
-
-```json
-{
-  "data": {
-    "directory": ".opencode"
-  },
-  "debug": false,
-  "providers": {
-    "anthropic": {
-      "apiKey": "your-api-key"
-    }
-  },
-  "agents": {
-    "primary": {
-      "model": "claude-3.7-sonnet",
-      "maxTokens": 5000,
-      "reasoningEffort": "medium"
-    },
-    "task": {
-      "model": "claude-3.7-sonnet",
-      "maxTokens": 5000
-    },
-    "title": {
-      "model": "claude-3.7-sonnet",
-      "maxTokens": 80
-    }
-  }
-}
-```
-

+ 0 - 337
cmd/schema/main.go

@@ -1,337 +0,0 @@
-package main
-
-import (
-	"encoding/json"
-	"fmt"
-	"os"
-
-	"github.com/sst/opencode/internal/config"
-	"github.com/sst/opencode/internal/llm/models"
-)
-
-// JSONSchemaType represents a JSON Schema type
-type JSONSchemaType struct {
-	Type                 string           `json:"type,omitempty"`
-	Description          string           `json:"description,omitempty"`
-	Properties           map[string]any   `json:"properties,omitempty"`
-	Required             []string         `json:"required,omitempty"`
-	AdditionalProperties any              `json:"additionalProperties,omitempty"`
-	Enum                 []any            `json:"enum,omitempty"`
-	Items                map[string]any   `json:"items,omitempty"`
-	OneOf                []map[string]any `json:"oneOf,omitempty"`
-	AnyOf                []map[string]any `json:"anyOf,omitempty"`
-	Default              any              `json:"default,omitempty"`
-}
-
-func main() {
-	schema := generateSchema()
-
-	// Pretty print the schema
-	encoder := json.NewEncoder(os.Stdout)
-	encoder.SetIndent("", "  ")
-	if err := encoder.Encode(schema); err != nil {
-		fmt.Fprintf(os.Stderr, "Error encoding schema: %v\n", err)
-		os.Exit(1)
-	}
-}
-
-func generateSchema() map[string]any {
-	schema := map[string]any{
-		"$schema":     "http://json-schema.org/draft-07/schema#",
-		"title":       "OpenCode Configuration",
-		"description": "Configuration schema for the OpenCode application",
-		"type":        "object",
-		"properties":  map[string]any{},
-	}
-
-	// Add Data configuration
-	schema["properties"].(map[string]any)["data"] = map[string]any{
-		"type":        "object",
-		"description": "Storage configuration",
-		"properties": map[string]any{
-			"directory": map[string]any{
-				"type":        "string",
-				"description": "Directory where application data is stored",
-				"default":     ".opencode",
-			},
-		},
-		"required": []string{"directory"},
-	}
-
-	// Add working directory
-	schema["properties"].(map[string]any)["wd"] = map[string]any{
-		"type":        "string",
-		"description": "Working directory for the application",
-	}
-
-	// Add debug flags
-	schema["properties"].(map[string]any)["debug"] = map[string]any{
-		"type":        "boolean",
-		"description": "Enable debug mode",
-		"default":     false,
-	}
-
-	schema["properties"].(map[string]any)["debugLSP"] = map[string]any{
-		"type":        "boolean",
-		"description": "Enable LSP debug mode",
-		"default":     false,
-	}
-
-	schema["properties"].(map[string]any)["contextPaths"] = map[string]any{
-		"type":        "array",
-		"description": "Context paths for the application",
-		"items": map[string]any{
-			"type": "string",
-		},
-		"default": []string{
-			".github/copilot-instructions.md",
-			".cursorrules",
-			".cursor/rules/",
-			"CLAUDE.md",
-			"CLAUDE.local.md",
-			"opencode.md",
-			"opencode.local.md",
-			"OpenCode.md",
-			"OpenCode.local.md",
-			"OPENCODE.md",
-			"OPENCODE.local.md",
-		},
-	}
-
-	schema["properties"].(map[string]any)["tui"] = map[string]any{
-		"type":        "object",
-		"description": "Terminal User Interface configuration",
-		"properties": map[string]any{
-			"theme": map[string]any{
-				"type":        "string",
-				"description": "TUI theme name",
-				"default":     "opencode",
-				"enum": []string{
-					"opencode",
-					"ayu",
-					"catppuccin",
-					"dracula",
-					"flexoki",
-					"gruvbox",
-					"monokai",
-					"onedark",
-					"tokyonight",
-					"tron",
-					"custom",
-				},
-			},
-			"customTheme": map[string]any{
-				"type":        "object",
-				"description": "Custom theme color definitions",
-				"additionalProperties": map[string]any{
-					"oneOf": []map[string]any{
-						{
-							"type":    "string",
-							"pattern": "^#[0-9a-fA-F]{6}$",
-						},
-						{
-							"type": "object",
-							"properties": map[string]any{
-								"dark": map[string]any{
-									"type":    "string",
-									"pattern": "^#[0-9a-fA-F]{6}$",
-								},
-								"light": map[string]any{
-									"type":    "string",
-									"pattern": "^#[0-9a-fA-F]{6}$",
-								},
-							},
-							"required":             []string{"dark", "light"},
-							"additionalProperties": false,
-						},
-					},
-				},
-			},
-		},
-	}
-
-	// Add MCP servers
-	schema["properties"].(map[string]any)["mcpServers"] = map[string]any{
-		"type":        "object",
-		"description": "Model Control Protocol server configurations",
-		"additionalProperties": map[string]any{
-			"type":        "object",
-			"description": "MCP server configuration",
-			"properties": map[string]any{
-				"command": map[string]any{
-					"type":        "string",
-					"description": "Command to execute for the MCP server",
-				},
-				"env": map[string]any{
-					"type":        "array",
-					"description": "Environment variables for the MCP server",
-					"items": map[string]any{
-						"type": "string",
-					},
-				},
-				"args": map[string]any{
-					"type":        "array",
-					"description": "Command arguments for the MCP server",
-					"items": map[string]any{
-						"type": "string",
-					},
-				},
-				"type": map[string]any{
-					"type":        "string",
-					"description": "Type of MCP server",
-					"enum":        []string{"stdio", "sse"},
-					"default":     "stdio",
-				},
-				"url": map[string]any{
-					"type":        "string",
-					"description": "URL for SSE type MCP servers",
-				},
-				"headers": map[string]any{
-					"type":        "object",
-					"description": "HTTP headers for SSE type MCP servers",
-					"additionalProperties": map[string]any{
-						"type": "string",
-					},
-				},
-			},
-			"required": []string{"command"},
-		},
-	}
-
-	// Add providers
-	providerSchema := map[string]any{
-		"type":        "object",
-		"description": "LLM provider configurations",
-		"additionalProperties": map[string]any{
-			"type":        "object",
-			"description": "Provider configuration",
-			"properties": map[string]any{
-				"apiKey": map[string]any{
-					"type":        "string",
-					"description": "API key for the provider",
-				},
-				"disabled": map[string]any{
-					"type":        "boolean",
-					"description": "Whether the provider is disabled",
-					"default":     false,
-				},
-			},
-		},
-	}
-
-	// Add known providers
-	knownProviders := []string{
-		string(models.ProviderAnthropic),
-		string(models.ProviderOpenAI),
-		string(models.ProviderGemini),
-		string(models.ProviderGROQ),
-		string(models.ProviderOpenRouter),
-		string(models.ProviderBedrock),
-		string(models.ProviderAzure),
-		string(models.ProviderVertexAI),
-	}
-
-	providerSchema["additionalProperties"].(map[string]any)["properties"].(map[string]any)["provider"] = map[string]any{
-		"type":        "string",
-		"description": "Provider type",
-		"enum":        knownProviders,
-	}
-
-	schema["properties"].(map[string]any)["providers"] = providerSchema
-
-	// Add agents
-	agentSchema := map[string]any{
-		"type":        "object",
-		"description": "Agent configurations",
-		"additionalProperties": map[string]any{
-			"type":        "object",
-			"description": "Agent configuration",
-			"properties": map[string]any{
-				"model": map[string]any{
-					"type":        "string",
-					"description": "Model ID for the agent",
-				},
-				"maxTokens": map[string]any{
-					"type":        "integer",
-					"description": "Maximum tokens for the agent",
-					"minimum":     1,
-				},
-				"reasoningEffort": map[string]any{
-					"type":        "string",
-					"description": "Reasoning effort for models that support it (OpenAI, Anthropic)",
-					"enum":        []string{"low", "medium", "high"},
-				},
-			},
-			"required": []string{"model"},
-		},
-	}
-
-	// Add model enum
-	modelEnum := []string{}
-	for modelID := range models.SupportedModels {
-		modelEnum = append(modelEnum, string(modelID))
-	}
-	agentSchema["additionalProperties"].(map[string]any)["properties"].(map[string]any)["model"].(map[string]any)["enum"] = modelEnum
-
-	// Add specific agent properties
-	agentProperties := map[string]any{}
-	knownAgents := []string{
-		string(config.AgentPrimary),
-		string(config.AgentTask),
-		string(config.AgentTitle),
-	}
-
-	for _, agentName := range knownAgents {
-		agentProperties[agentName] = map[string]any{
-			"$ref": "#/definitions/agent",
-		}
-	}
-
-	// Create a combined schema that allows both specific agents and additional ones
-	combinedAgentSchema := map[string]any{
-		"type":                 "object",
-		"description":          "Agent configurations",
-		"properties":           agentProperties,
-		"additionalProperties": agentSchema["additionalProperties"],
-	}
-
-	schema["properties"].(map[string]any)["agents"] = combinedAgentSchema
-	schema["definitions"] = map[string]any{
-		"agent": agentSchema["additionalProperties"],
-	}
-
-	// Add LSP configuration
-	schema["properties"].(map[string]any)["lsp"] = map[string]any{
-		"type":        "object",
-		"description": "Language Server Protocol configurations",
-		"additionalProperties": map[string]any{
-			"type":        "object",
-			"description": "LSP configuration for a language",
-			"properties": map[string]any{
-				"disabled": map[string]any{
-					"type":        "boolean",
-					"description": "Whether the LSP is disabled",
-					"default":     false,
-				},
-				"command": map[string]any{
-					"type":        "string",
-					"description": "Command to execute for the LSP server",
-				},
-				"args": map[string]any{
-					"type":        "array",
-					"description": "Command arguments for the LSP server",
-					"items": map[string]any{
-						"type": "string",
-					},
-				},
-				"options": map[string]any{
-					"type":        "object",
-					"description": "Additional options for the LSP server",
-				},
-			},
-			"required": []string{"command"},
-		},
-	}
-
-	return schema
-}

+ 0 - 441
internal/history/history.go

@@ -1,441 +0,0 @@
-package history
-
-import (
-	"context"
-	"database/sql"
-	"fmt"
-	"log/slog"
-	"slices"
-	"strconv"
-	"strings"
-	"sync"
-	"time"
-
-	"github.com/google/uuid"
-	"github.com/sst/opencode/internal/db"
-	"github.com/sst/opencode/internal/pubsub"
-)
-
-const (
-	InitialVersion = "initial"
-)
-
-type File struct {
-	ID        string
-	SessionID string
-	Path      string
-	Content   string
-	Version   string
-	CreatedAt time.Time
-	UpdatedAt time.Time
-}
-
-const (
-	EventFileCreated         pubsub.EventType = "history_file_created"
-	EventFileVersionCreated  pubsub.EventType = "history_file_version_created"
-	EventFileUpdated         pubsub.EventType = "history_file_updated"
-	EventFileDeleted         pubsub.EventType = "history_file_deleted"
-	EventSessionFilesDeleted pubsub.EventType = "history_session_files_deleted"
-)
-
-type Service interface {
-	pubsub.Subscriber[File]
-
-	Create(ctx context.Context, sessionID, path, content string) (File, error)
-	CreateVersion(ctx context.Context, sessionID, path, content string) (File, error)
-	Get(ctx context.Context, id string) (File, error)
-	GetByPathAndVersion(ctx context.Context, sessionID, path, version string) (File, error)
-	GetLatestByPathAndSession(ctx context.Context, path, sessionID string) (File, error)
-	ListBySession(ctx context.Context, sessionID string) ([]File, error)
-	ListLatestSessionFiles(ctx context.Context, sessionID string) ([]File, error)
-	ListVersionsByPath(ctx context.Context, path string) ([]File, error)
-	Update(ctx context.Context, file File) (File, error)
-	Delete(ctx context.Context, id string) error
-	DeleteSessionFiles(ctx context.Context, sessionID string) error
-}
-
-type service struct {
-	db     *db.Queries
-	sqlDB  *sql.DB
-	broker *pubsub.Broker[File]
-	mu     sync.RWMutex
-}
-
-var globalHistoryService *service
-
-func InitService(sqlDatabase *sql.DB) error {
-	if globalHistoryService != nil {
-		return fmt.Errorf("history service already initialized")
-	}
-	queries := db.New(sqlDatabase)
-	broker := pubsub.NewBroker[File]()
-
-	globalHistoryService = &service{
-		db:     queries,
-		sqlDB:  sqlDatabase,
-		broker: broker,
-	}
-	return nil
-}
-
-func GetService() Service {
-	if globalHistoryService == nil {
-		panic("history service not initialized. Call history.InitService() first.")
-	}
-	return globalHistoryService
-}
-
-func (s *service) Create(ctx context.Context, sessionID, path, content string) (File, error) {
-	return s.createWithVersion(ctx, sessionID, path, content, InitialVersion, EventFileCreated)
-}
-
-func (s *service) CreateVersion(ctx context.Context, sessionID, path, content string) (File, error) {
-	s.mu.RLock()
-	files, err := s.db.ListFilesByPath(ctx, path)
-	s.mu.RUnlock()
-
-	if err != nil && err != sql.ErrNoRows {
-		return File{}, fmt.Errorf("db.ListFilesByPath for next version: %w", err)
-	}
-
-	latestVersionNumber := 0
-	if len(files) > 0 {
-		// Sort to be absolutely sure about the latest version globally for this path
-		slices.SortFunc(files, func(a, b db.File) int {
-			if strings.HasPrefix(a.Version, "v") && strings.HasPrefix(b.Version, "v") {
-				vA, _ := strconv.Atoi(a.Version[1:])
-				vB, _ := strconv.Atoi(b.Version[1:])
-				return vB - vA // Descending to get latest first
-			}
-			if a.Version == InitialVersion && b.Version != InitialVersion {
-				return 1 // initial comes after vX
-			}
-			if b.Version == InitialVersion && a.Version != InitialVersion {
-				return -1
-			}
-			// Compare timestamps as strings (ISO format sorts correctly)
-			if b.CreatedAt > a.CreatedAt {
-				return 1
-			} else if a.CreatedAt > b.CreatedAt {
-				return -1
-			}
-			return 0 // Equal timestamps
-		})
-
-		latestFile := files[0]
-		if strings.HasPrefix(latestFile.Version, "v") {
-			vNum, parseErr := strconv.Atoi(latestFile.Version[1:])
-			if parseErr == nil {
-				latestVersionNumber = vNum
-			}
-		}
-	}
-	nextVersionStr := fmt.Sprintf("v%d", latestVersionNumber+1)
-	return s.createWithVersion(ctx, sessionID, path, content, nextVersionStr, EventFileVersionCreated)
-}
-
-func (s *service) createWithVersion(ctx context.Context, sessionID, path, content, version string, eventType pubsub.EventType) (File, error) {
-	s.mu.Lock()
-	defer s.mu.Unlock()
-
-	const maxRetries = 3
-	var file File
-	var err error
-
-	for attempt := range maxRetries {
-		tx, txErr := s.sqlDB.BeginTx(ctx, nil)
-		if txErr != nil {
-			return File{}, fmt.Errorf("failed to begin transaction: %w", txErr)
-		}
-		qtx := s.db.WithTx(tx)
-
-		dbFile, createErr := qtx.CreateFile(ctx, db.CreateFileParams{
-			ID:        uuid.New().String(),
-			SessionID: sessionID,
-			Path:      path,
-			Content:   content,
-			Version:   version,
-		})
-
-		if createErr != nil {
-			if rbErr := tx.Rollback(); rbErr != nil {
-				slog.Error("Failed to rollback transaction on create error", "error", rbErr)
-			}
-			if strings.Contains(createErr.Error(), "UNIQUE constraint failed: files.path, files.session_id, files.version") {
-				if attempt < maxRetries-1 {
-					slog.Warn("Unique constraint violation for file version, retrying with incremented version", "path", path, "session", sessionID, "attempted_version", version, "attempt", attempt+1)
-					// Increment version string like v1, v2, v3...
-					if strings.HasPrefix(version, "v") {
-						numPart := version[1:]
-						num, parseErr := strconv.Atoi(numPart)
-						if parseErr == nil {
-							version = fmt.Sprintf("v%d", num+1)
-							continue // Retry with new version
-						}
-					}
-					// Fallback if version is not "vX" or parsing failed
-					version = fmt.Sprintf("%s-retry%d", version, attempt+1)
-					continue
-				}
-			}
-			return File{}, fmt.Errorf("db.CreateFile within transaction: %w", createErr)
-		}
-
-		if commitErr := tx.Commit(); commitErr != nil {
-			return File{}, fmt.Errorf("failed to commit transaction: %w", commitErr)
-		}
-
-		file = s.fromDBItem(dbFile)
-		s.broker.Publish(eventType, file)
-		return file, nil // Success
-	}
-
-	return File{}, fmt.Errorf("failed to create file after %d retries due to version conflicts: %w", maxRetries, err)
-}
-
-func (s *service) Get(ctx context.Context, id string) (File, error) {
-	s.mu.RLock()
-	defer s.mu.RUnlock()
-	dbFile, err := s.db.GetFile(ctx, id)
-	if err != nil {
-		if err == sql.ErrNoRows {
-			return File{}, fmt.Errorf("file with ID '%s' not found", id)
-		}
-		return File{}, fmt.Errorf("db.GetFile: %w", err)
-	}
-	return s.fromDBItem(dbFile), nil
-}
-
-func (s *service) GetByPathAndVersion(ctx context.Context, sessionID, path, version string) (File, error) {
-	s.mu.RLock()
-	defer s.mu.RUnlock()
-
-	// sqlc doesn't directly support GetyByPathAndVersionAndSession
-	// We list and filter. This could be optimized with a custom query if performance is an issue.
-	allFilesForPath, err := s.db.ListFilesByPath(ctx, path)
-	if err != nil {
-		return File{}, fmt.Errorf("db.ListFilesByPath for GetByPathAndVersion: %w", err)
-	}
-
-	for _, dbFile := range allFilesForPath {
-		if dbFile.SessionID == sessionID && dbFile.Version == version {
-			return s.fromDBItem(dbFile), nil
-		}
-	}
-	return File{}, fmt.Errorf("file not found for session '%s', path '%s', version '%s'", sessionID, path, version)
-}
-
-func (s *service) GetLatestByPathAndSession(ctx context.Context, path, sessionID string) (File, error) {
-	s.mu.RLock()
-	defer s.mu.RUnlock()
-	// GetFileByPathAndSession in sqlc already orders by created_at DESC and takes LIMIT 1
-	dbFile, err := s.db.GetFileByPathAndSession(ctx, db.GetFileByPathAndSessionParams{
-		Path:      path,
-		SessionID: sessionID,
-	})
-	if err != nil {
-		if err == sql.ErrNoRows {
-			return File{}, fmt.Errorf("no file found for path '%s' in session '%s'", path, sessionID)
-		}
-		return File{}, fmt.Errorf("db.GetFileByPathAndSession: %w", err)
-	}
-	return s.fromDBItem(dbFile), nil
-}
-
-func (s *service) ListBySession(ctx context.Context, sessionID string) ([]File, error) {
-	s.mu.RLock()
-	defer s.mu.RUnlock()
-	dbFiles, err := s.db.ListFilesBySession(ctx, sessionID) // Assumes this orders by created_at ASC
-	if err != nil {
-		return nil, fmt.Errorf("db.ListFilesBySession: %w", err)
-	}
-	files := make([]File, len(dbFiles))
-	for i, dbF := range dbFiles {
-		files[i] = s.fromDBItem(dbF)
-	}
-	return files, nil
-}
-
-func (s *service) ListLatestSessionFiles(ctx context.Context, sessionID string) ([]File, error) {
-	s.mu.RLock()
-	defer s.mu.RUnlock()
-	dbFiles, err := s.db.ListLatestSessionFiles(ctx, sessionID) // Uses the specific sqlc query
-	if err != nil {
-		return nil, fmt.Errorf("db.ListLatestSessionFiles: %w", err)
-	}
-	files := make([]File, len(dbFiles))
-	for i, dbF := range dbFiles {
-		files[i] = s.fromDBItem(dbF)
-	}
-	return files, nil
-}
-
-func (s *service) ListVersionsByPath(ctx context.Context, path string) ([]File, error) {
-	s.mu.RLock()
-	defer s.mu.RUnlock()
-	dbFiles, err := s.db.ListFilesByPath(ctx, path) // sqlc query orders by created_at DESC
-	if err != nil {
-		return nil, fmt.Errorf("db.ListFilesByPath: %w", err)
-	}
-	files := make([]File, len(dbFiles))
-	for i, dbF := range dbFiles {
-		files[i] = s.fromDBItem(dbF)
-	}
-	return files, nil
-}
-
-func (s *service) Update(ctx context.Context, file File) (File, error) {
-	s.mu.Lock()
-	defer s.mu.Unlock()
-
-	if file.ID == "" {
-		return File{}, fmt.Errorf("cannot update file with empty ID")
-	}
-	// UpdatedAt is handled by DB trigger
-	dbFile, err := s.db.UpdateFile(ctx, db.UpdateFileParams{
-		ID:      file.ID,
-		Content: file.Content,
-		Version: file.Version,
-	})
-	if err != nil {
-		return File{}, fmt.Errorf("db.UpdateFile: %w", err)
-	}
-	updatedFile := s.fromDBItem(dbFile)
-	s.broker.Publish(EventFileUpdated, updatedFile)
-	return updatedFile, nil
-}
-
-func (s *service) Delete(ctx context.Context, id string) error {
-	s.mu.Lock()
-	fileToPublish, err := s.getServiceForPublish(ctx, id) // Use internal method with appropriate locking
-	s.mu.Unlock()
-
-	if err != nil {
-		if strings.Contains(err.Error(), "not found") {
-			slog.Warn("Attempted to delete non-existent file history", "id", id)
-			return nil // Or return specific error if needed
-		}
-		return err
-	}
-
-	s.mu.Lock()
-	defer s.mu.Unlock()
-	err = s.db.DeleteFile(ctx, id)
-	if err != nil {
-		return fmt.Errorf("db.DeleteFile: %w", err)
-	}
-	if fileToPublish != nil {
-		s.broker.Publish(EventFileDeleted, *fileToPublish)
-	}
-	return nil
-}
-
-func (s *service) getServiceForPublish(ctx context.Context, id string) (*File, error) {
-	// Assumes outer lock is NOT held or caller manages it.
-	// For GetFile, it has its own RLock.
-	dbFile, err := s.db.GetFile(ctx, id)
-	if err != nil {
-		return nil, err
-	}
-	file := s.fromDBItem(dbFile)
-	return &file, nil
-}
-
-func (s *service) DeleteSessionFiles(ctx context.Context, sessionID string) error {
-	s.mu.Lock() // Lock for the entire operation
-	defer s.mu.Unlock()
-
-	// Get files first for publishing events
-	filesToDelete, err := s.db.ListFilesBySession(ctx, sessionID)
-	if err != nil {
-		return fmt.Errorf("db.ListFilesBySession for deletion: %w", err)
-	}
-
-	err = s.db.DeleteSessionFiles(ctx, sessionID)
-	if err != nil {
-		return fmt.Errorf("db.DeleteSessionFiles: %w", err)
-	}
-
-	for _, dbFile := range filesToDelete {
-		file := s.fromDBItem(dbFile)
-		s.broker.Publish(EventFileDeleted, file) // Individual delete events
-	}
-	return nil
-}
-
-func (s *service) Subscribe(ctx context.Context) <-chan pubsub.Event[File] {
-	return s.broker.Subscribe(ctx)
-}
-
-func (s *service) fromDBItem(item db.File) File {
-	// Parse timestamps from ISO strings
-	createdAt, err := time.Parse(time.RFC3339Nano, item.CreatedAt)
-	if err != nil {
-		slog.Error("Failed to parse created_at", "value", item.CreatedAt, "error", err)
-		createdAt = time.Now() // Fallback
-	}
-
-	updatedAt, err := time.Parse(time.RFC3339Nano, item.UpdatedAt)
-	if err != nil {
-		slog.Error("Failed to parse created_at", "value", item.CreatedAt, "error", err)
-		updatedAt = time.Now() // Fallback
-	}
-
-	return File{
-		ID:        item.ID,
-		SessionID: item.SessionID,
-		Path:      item.Path,
-		Content:   item.Content,
-		Version:   item.Version,
-		CreatedAt: createdAt,
-		UpdatedAt: updatedAt,
-	}
-}
-
-func Create(ctx context.Context, sessionID, path, content string) (File, error) {
-	return GetService().Create(ctx, sessionID, path, content)
-}
-
-func CreateVersion(ctx context.Context, sessionID, path, content string) (File, error) {
-	return GetService().CreateVersion(ctx, sessionID, path, content)
-}
-
-func Get(ctx context.Context, id string) (File, error) {
-	return GetService().Get(ctx, id)
-}
-
-func GetByPathAndVersion(ctx context.Context, sessionID, path, version string) (File, error) {
-	return GetService().GetByPathAndVersion(ctx, sessionID, path, version)
-}
-
-func GetLatestByPathAndSession(ctx context.Context, path, sessionID string) (File, error) {
-	return GetService().GetLatestByPathAndSession(ctx, path, sessionID)
-}
-
-func ListBySession(ctx context.Context, sessionID string) ([]File, error) {
-	return GetService().ListBySession(ctx, sessionID)
-}
-
-func ListLatestSessionFiles(ctx context.Context, sessionID string) ([]File, error) {
-	return GetService().ListLatestSessionFiles(ctx, sessionID)
-}
-
-func ListVersionsByPath(ctx context.Context, path string) ([]File, error) {
-	return GetService().ListVersionsByPath(ctx, path)
-}
-
-func Update(ctx context.Context, file File) (File, error) {
-	return GetService().Update(ctx, file)
-}
-
-func Delete(ctx context.Context, id string) error {
-	return GetService().Delete(ctx, id)
-}
-
-func DeleteSessionFiles(ctx context.Context, sessionID string) error {
-	return GetService().DeleteSessionFiles(ctx, sessionID)
-}
-
-func Subscribe(ctx context.Context) <-chan pubsub.Event[File] {
-	return GetService().Subscribe(ctx)
-}

+ 0 - 292
internal/logging/logging.go

@@ -1,292 +0,0 @@
-package logging
-
-import (
-	"bytes"
-	"context"
-	"database/sql"
-	"encoding/json"
-	"fmt"
-	"io"
-	"log/slog"
-	"os"
-	"runtime/debug"
-	"strings"
-	"time"
-
-	"github.com/go-logfmt/logfmt"
-	"github.com/google/uuid"
-	"github.com/sst/opencode/internal/db"
-	"github.com/sst/opencode/internal/pubsub"
-)
-
-type Log struct {
-	ID         string
-	SessionID  string
-	Timestamp  time.Time
-	Level      string
-	Message    string
-	Attributes map[string]string
-	CreatedAt  time.Time
-}
-
-const (
-	EventLogCreated pubsub.EventType = "log_created"
-)
-
-type Service interface {
-	pubsub.Subscriber[Log]
-
-	Create(ctx context.Context, timestamp time.Time, level, message string, attributes map[string]string, sessionID string) error
-	ListBySession(ctx context.Context, sessionID string) ([]Log, error)
-	ListAll(ctx context.Context, limit int) ([]Log, error)
-}
-
-type service struct {
-	db     *db.Queries
-	broker *pubsub.Broker[Log]
-}
-
-var globalLoggingService *service
-
-func InitService(dbConn *sql.DB) error {
-	if globalLoggingService != nil {
-		return fmt.Errorf("logging service already initialized")
-	}
-	queries := db.New(dbConn)
-	broker := pubsub.NewBroker[Log]()
-
-	globalLoggingService = &service{
-		db:     queries,
-		broker: broker,
-	}
-	return nil
-}
-
-func GetService() Service {
-	if globalLoggingService == nil {
-		panic("logging service not initialized. Call logging.InitService() first.")
-	}
-	return globalLoggingService
-}
-
-func (s *service) Create(ctx context.Context, timestamp time.Time, level, message string, attributes map[string]string, sessionID string) error {
-	if level == "" {
-		level = "info"
-	}
-
-	var attributesJSON sql.NullString
-	if len(attributes) > 0 {
-		attributesBytes, err := json.Marshal(attributes)
-		if err != nil {
-			return fmt.Errorf("failed to marshal log attributes: %w", err)
-		}
-		attributesJSON = sql.NullString{String: string(attributesBytes), Valid: true}
-	}
-
-	dbLog, err := s.db.CreateLog(ctx, db.CreateLogParams{
-		ID:         uuid.New().String(),
-		SessionID:  sql.NullString{String: sessionID, Valid: sessionID != ""},
-		Timestamp:  timestamp.UTC().Format(time.RFC3339Nano),
-		Level:      level,
-		Message:    message,
-		Attributes: attributesJSON,
-	})
-
-	if err != nil {
-		return fmt.Errorf("db.CreateLog: %w", err)
-	}
-
-	log := s.fromDBItem(dbLog)
-	s.broker.Publish(EventLogCreated, log)
-	return nil
-}
-
-func (s *service) ListBySession(ctx context.Context, sessionID string) ([]Log, error) {
-	dbLogs, err := s.db.ListLogsBySession(ctx, sql.NullString{String: sessionID, Valid: true})
-	if err != nil {
-		return nil, fmt.Errorf("db.ListLogsBySession: %w", err)
-	}
-
-	logs := make([]Log, len(dbLogs))
-	for i, dbSess := range dbLogs {
-		logs[i] = s.fromDBItem(dbSess)
-	}
-	return logs, nil
-}
-
-func (s *service) ListAll(ctx context.Context, limit int) ([]Log, error) {
-	dbLogs, err := s.db.ListAllLogs(ctx, int64(limit))
-	if err != nil {
-		return nil, fmt.Errorf("db.ListAllLogs: %w", err)
-	}
-	logs := make([]Log, len(dbLogs))
-	for i, dbSess := range dbLogs {
-		logs[i] = s.fromDBItem(dbSess)
-	}
-	return logs, nil
-}
-
-func (s *service) Subscribe(ctx context.Context) <-chan pubsub.Event[Log] {
-	return s.broker.Subscribe(ctx)
-}
-
-func (s *service) fromDBItem(item db.Log) Log {
-	log := Log{
-		ID:        item.ID,
-		SessionID: item.SessionID.String,
-		Level:     item.Level,
-		Message:   item.Message,
-	}
-
-	// Parse timestamp from ISO string
-	timestamp, err := time.Parse(time.RFC3339Nano, item.Timestamp)
-	if err == nil {
-		log.Timestamp = timestamp
-	} else {
-		log.Timestamp = time.Now() // Fallback
-	}
-
-	// Parse created_at from ISO string
-	createdAt, err := time.Parse(time.RFC3339Nano, item.CreatedAt)
-	if err == nil {
-		log.CreatedAt = createdAt
-	} else {
-		log.CreatedAt = time.Now() // Fallback
-	}
-
-	if item.Attributes.Valid && item.Attributes.String != "" {
-		if err := json.Unmarshal([]byte(item.Attributes.String), &log.Attributes); err != nil {
-			slog.Error("Failed to unmarshal log attributes", "log_id", item.ID, "error", err)
-			log.Attributes = make(map[string]string)
-		}
-	} else {
-		log.Attributes = make(map[string]string)
-	}
-
-	return log
-}
-
-func Create(ctx context.Context, timestamp time.Time, level, message string, attributes map[string]string, sessionID string) error {
-	return GetService().Create(ctx, timestamp, level, message, attributes, sessionID)
-}
-
-func ListBySession(ctx context.Context, sessionID string) ([]Log, error) {
-	return GetService().ListBySession(ctx, sessionID)
-}
-
-func ListAll(ctx context.Context, limit int) ([]Log, error) {
-	return GetService().ListAll(ctx, limit)
-}
-
-func Subscribe(ctx context.Context) <-chan pubsub.Event[Log] {
-	return GetService().Subscribe(ctx)
-}
-
-type slogWriter struct{}
-
-func (sw *slogWriter) Write(p []byte) (n int, err error) {
-	// Example: time=2024-05-09T12:34:56.789-05:00 level=INFO msg="User request" session=xyz foo=bar
-	d := logfmt.NewDecoder(bytes.NewReader(p))
-	for d.ScanRecord() {
-		var timestamp time.Time
-		var level string
-		var message string
-		var sessionID string
-		var attributes map[string]string
-
-		attributes = make(map[string]string)
-		hasTimestamp := false
-
-		for d.ScanKeyval() {
-			key := string(d.Key())
-			value := string(d.Value())
-
-			switch key {
-			case "time":
-				parsedTime, timeErr := time.Parse(time.RFC3339Nano, value)
-				if timeErr != nil {
-					parsedTime, timeErr = time.Parse(time.RFC3339, value)
-					if timeErr != nil {
-						slog.Error("Failed to parse time in slog writer", "value", value, "error", timeErr)
-						timestamp = time.Now().UTC()
-						hasTimestamp = true
-						continue
-					}
-				}
-				timestamp = parsedTime
-				hasTimestamp = true
-			case "level":
-				level = strings.ToLower(value)
-			case "msg", "message":
-				message = value
-			case "session_id":
-				sessionID = value
-			default:
-				attributes[key] = value
-			}
-		}
-		if d.Err() != nil {
-			return len(p), fmt.Errorf("logfmt.ScanRecord: %w", d.Err())
-		}
-
-		if !hasTimestamp {
-			timestamp = time.Now()
-		}
-
-		// Create log entry via the service (non-blocking or handle error appropriately)
-		// Using context.Background() as this is a low-level logging write.
-		go func(timestamp time.Time, level, message string, attributes map[string]string, sessionID string) { // Run in a goroutine to avoid blocking slog
-			if globalLoggingService == nil {
-				// If the logging service is not initialized, log the message to stderr
-				// fmt.Fprintf(os.Stderr, "ERROR [logging.slogWriter]: logging service not initialized\n")
-				return
-			}
-			if err := Create(context.Background(), timestamp, level, message, attributes, sessionID); err != nil {
-				// Log internal error using a more primitive logger to avoid loops
-				fmt.Fprintf(os.Stderr, "ERROR [logging.slogWriter]: failed to persist log: %v\n", err)
-			}
-		}(timestamp, level, message, attributes, sessionID)
-	}
-	if d.Err() != nil {
-		return len(p), fmt.Errorf("logfmt.ScanRecord final: %w", d.Err())
-	}
-	return len(p), nil
-}
-
-func NewSlogWriter() io.Writer {
-	return &slogWriter{}
-}
-
-// 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.
-func RecoverPanic(name string, cleanup func()) {
-	if r := recover(); r != nil {
-		errorMsg := fmt.Sprintf("Panic in %s: %v", name, r)
-		// Use slog directly here, as our service might be the one panicking.
-		slog.Error(errorMsg)
-		// status.Error(errorMsg)
-
-		timestamp := time.Now().Format("20060102-150405")
-		filename := fmt.Sprintf("opencode-panic-%s-%s.log", name, timestamp)
-
-		file, err := os.Create(filename)
-		if err != nil {
-			errMsg := fmt.Sprintf("Failed to create panic log file '%s': %v", filename, err)
-			slog.Error(errMsg)
-			// status.Error(errMsg)
-		} else {
-			defer file.Close()
-			fmt.Fprintf(file, "Panic in %s: %v\n\n", name, r)
-			fmt.Fprintf(file, "Time: %s\n\n", time.Now().Format(time.RFC3339))
-			fmt.Fprintf(file, "Stack Trace:\n%s\n", string(debug.Stack())) // Capture stack trace
-			infoMsg := fmt.Sprintf("Panic details written to %s", filename)
-			slog.Info(infoMsg)
-			// status.Info(infoMsg)
-		}
-
-		if cleanup != nil {
-			cleanup()
-		}
-	}
-}

+ 1 - 73
internal/tui/components/chat/sidebar.go

@@ -10,9 +10,6 @@ import (
 	"github.com/charmbracelet/lipgloss"
 	"github.com/charmbracelet/lipgloss"
 	"github.com/sst/opencode/internal/config"
 	"github.com/sst/opencode/internal/config"
 	"github.com/sst/opencode/internal/tui/app"
 	"github.com/sst/opencode/internal/tui/app"
-	// "github.com/sst/opencode/internal/diff"
-	"github.com/sst/opencode/internal/history"
-	"github.com/sst/opencode/internal/pubsub"
 	"github.com/sst/opencode/internal/tui/state"
 	"github.com/sst/opencode/internal/tui/state"
 	"github.com/sst/opencode/internal/tui/styles"
 	"github.com/sst/opencode/internal/tui/styles"
 	"github.com/sst/opencode/internal/tui/theme"
 	"github.com/sst/opencode/internal/tui/theme"
@@ -43,7 +40,7 @@ func (m *sidebarCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 		// TODO: History service not implemented in API yet
 		// TODO: History service not implemented in API yet
 		// ctx := context.Background()
 		// ctx := context.Background()
 		// m.loadModifiedFiles(ctx)
 		// m.loadModifiedFiles(ctx)
-	case pubsub.Event[history.File]:
+		// case pubsub.Event[history.File]:
 		// TODO: History service not implemented in API yet
 		// TODO: History service not implemented in API yet
 		// if msg.Payload.SessionID == m.app.CurrentSession.ID {
 		// if msg.Payload.SessionID == m.app.CurrentSession.ID {
 		// 	// Process the individual file change instead of reloading all files
 		// 	// Process the individual file change instead of reloading all files
@@ -283,75 +280,6 @@ func (m *sidebarCmp) loadModifiedFiles(ctx context.Context) {
 	*/
 	*/
 }
 }
 
 
-func (m *sidebarCmp) processFileChanges(ctx context.Context, file history.File) {
-	// TODO: History service not implemented in API yet
-	return
-	/*
-		// Skip if this is the initial version (no changes to show)
-		if file.Version == history.InitialVersion {
-			return
-		}
-
-		// Find the initial version for this file
-		initialVersion, err := m.findInitialVersion(ctx, file.Path)
-		if err != nil || initialVersion.ID == "" {
-			return
-		}
-
-		// Skip if content hasn't changed
-		if initialVersion.Content == file.Content {
-			// If this file was previously modified but now matches the initial version,
-			// remove it from the modified files list
-			displayPath := getDisplayPath(file.Path)
-			delete(m.modFiles, displayPath)
-			return
-		}
-
-		// Calculate diff between initial and latest version
-		_, additions, removals := diff.GenerateDiff(initialVersion.Content, file.Content, file.Path)
-
-		// Only add to modified files if there are changes
-		if additions > 0 || removals > 0 {
-			displayPath := getDisplayPath(file.Path)
-			m.modFiles[displayPath] = struct {
-				additions int
-				removals  int
-			}{
-				additions: additions,
-				removals:  removals,
-			}
-		} else {
-			// If no changes, remove from modified files
-			displayPath := getDisplayPath(file.Path)
-			delete(m.modFiles, displayPath)
-		}
-	*/
-}
-
-// Helper function to find the initial version of a file
-func (m *sidebarCmp) findInitialVersion(ctx context.Context, path string) (history.File, error) {
-	// TODO: History service not implemented in API yet
-	return history.File{}, fmt.Errorf("history service not implemented")
-	/*
-		// Get all versions of this file for the session
-		fileVersions, err := m.app.History.ListBySession(ctx, m.app.CurrentSession.ID)
-		if err != nil {
-			return history.File{}, err
-		}
-	*/
-
-	/*
-		// Find the initial version
-		for _, v := range fileVersions {
-			if v.Path == path && v.Version == history.InitialVersion {
-				return v, nil
-			}
-		}
-
-		return history.File{}, fmt.Errorf("initial version not found")
-	*/
-}
-
 // Helper function to get the display path for a file
 // Helper function to get the display path for a file
 func getDisplayPath(path string) string {
 func getDisplayPath(path string) string {
 	workingDir := config.WorkingDirectory()
 	workingDir := config.WorkingDirectory()

+ 0 - 6
main.go

@@ -2,14 +2,8 @@ package main
 
 
 import (
 import (
 	"github.com/sst/opencode/cmd"
 	"github.com/sst/opencode/cmd"
-	"github.com/sst/opencode/internal/logging"
-	"github.com/sst/opencode/internal/status"
 )
 )
 
 
 func main() {
 func main() {
-	defer logging.RecoverPanic("main", func() {
-		status.Error("Application terminated due to unhandled panic")
-	})
-
 	cmd.Execute()
 	cmd.Execute()
 }
 }