From 19930838e3e2f47252296b4a9c3b069e7526598b Mon Sep 17 00:00:00 2001 From: Wolfgang Meyers Date: Thu, 24 Jul 2025 16:11:30 -0700 Subject: [PATCH] TODO system --- docs/api-reference.md | 879 ++++++++++++++++++ docs/architecture.md | 739 +++++++++++++++ docs/development-guide.md | 729 +++++++++++++++ .../20250724230647_add_todos_to_sessions.sql | 26 + internal/db/models.go | 1 + internal/db/sessions.sql.go | 17 +- internal/db/sql/sessions.sql | 3 +- internal/llm/agent/tools.go | 2 + internal/llm/prompt/coder.go | 106 ++- internal/llm/tools/todo.go | 158 ++++ internal/session/session.go | 5 +- internal/tui/components/chat/sidebar.go | 120 +++ 12 files changed, 2772 insertions(+), 13 deletions(-) create mode 100644 docs/api-reference.md create mode 100644 docs/architecture.md create mode 100644 docs/development-guide.md create mode 100644 internal/db/migrations/20250724230647_add_todos_to_sessions.sql create mode 100644 internal/llm/tools/todo.go diff --git a/docs/api-reference.md b/docs/api-reference.md new file mode 100644 index 000000000..537ad8d64 --- /dev/null +++ b/docs/api-reference.md @@ -0,0 +1,879 @@ +# OpenCode API Reference + +## Overview + +This document provides a comprehensive reference for OpenCode's internal APIs, interfaces, and key components. While OpenCode is primarily a CLI application, understanding its internal architecture is crucial for development and extension. + +## Core Interfaces + +### App Service Interface + +The main application orchestrator that coordinates all services. + +```go +type App struct { + Sessions session.Service + Messages message.Service + History history.Service + Permissions permission.Service + + CoderAgent agent.Service + + LSPClients map[string]*lsp.Client + + clientsMutex sync.RWMutex + + watcherCancelFuncs []context.CancelFunc + cancelFuncsMutex sync.Mutex + watcherWG sync.WaitGroup +} + +func New(ctx context.Context, conn *sql.DB) (*App, error) +func (a *App) RunNonInteractive(ctx context.Context, prompt string, outputFormat string, quiet bool) error +func (a *App) Shutdown() +``` + +**Usage Example:** +```go +app, err := app.New(ctx, db) +if err != nil { + return err +} +defer app.Shutdown() + +// Non-interactive mode +err = app.RunNonInteractive(ctx, "Explain Go contexts", "json", false) +``` + +## Configuration API + +### Config Structure + +```go +type Config struct { + Data Data `json:"data"` + WD string `json:"wd"` + Debug bool `json:"debug"` + DebugLSP bool `json:"debugLSP"` + ContextPaths []string `json:"contextPaths"` + TUI TUIConfig `json:"tui"` + MCPServers map[string]MCPServer `json:"mcpServers"` + Providers map[models.ModelProvider]Provider `json:"providers"` + Agents map[AgentName]Agent `json:"agents"` + LSP map[string]LSPConfig `json:"lsp"` + Shell ShellConfig `json:"shell"` + AutoCompact bool `json:"autoCompact"` +} + +type Agent struct { + Model string `json:"model"` + MaxTokens int `json:"maxTokens"` + ReasoningEffort string `json:"reasoningEffort,omitempty"` +} + +type Provider struct { + APIKey string `json:"apiKey"` + Disabled bool `json:"disabled"` +} + +type LSPConfig struct { + Disabled bool `json:"disabled"` + Command string `json:"command"` + Args []string `json:"args,omitempty"` + Options any `json:"options,omitempty"` +} +``` + +### Configuration Functions + +```go +func Load(workingDir string, debug bool) (*Config, error) +func (c *Config) GetAgent(name AgentName) (Agent, bool) +func (c *Config) GetProvider(provider models.ModelProvider) (Provider, bool) +func (c *Config) IsProviderEnabled(provider models.ModelProvider) bool +func (c *Config) GetPreferredProvider() models.ModelProvider +``` + +## Session Management API + +### Session Service Interface + +```go +type Service interface { + pubsub.Suscriber[Session] + Create(ctx context.Context, title string) (Session, error) + CreateTitleSession(ctx context.Context, parentSessionID string) (Session, error) + CreateTaskSession(ctx context.Context, toolCallID, parentSessionID, title string) (Session, error) + Get(ctx context.Context, id string) (Session, error) + List(ctx context.Context) ([]Session, error) + Save(ctx context.Context, session Session) (Session, error) + Delete(ctx context.Context, id string) error +} + +type Session struct { + ID string + ParentSessionID string + Title string + MessageCount int64 + PromptTokens int64 + CompletionTokens int64 + SummaryMessageID string + Cost float64 + CreatedAt int64 + UpdatedAt int64 +} +``` + +**Usage Example:** +```go +session, err := sessionService.CreateSession(ctx, "Debug Session", nil) +if err != nil { + return err +} + +sessions, err := sessionService.ListSessions(ctx, 10, 0) +if err != nil { + return err +} +``` + +## Message Management API + +### Message Service Interface + +```go +type Service interface { + pubsub.Suscriber[Message] + Create(ctx context.Context, sessionID string, params CreateMessageParams) (Message, error) + Update(ctx context.Context, message Message) error + Get(ctx context.Context, id string) (Message, error) + List(ctx context.Context, sessionID string) ([]Message, error) + Delete(ctx context.Context, id string) error + DeleteSessionMessages(ctx context.Context, sessionID string) error +} + +type Message struct { + ID string `json:"id"` + SessionID string `json:"sessionId"` + Role Role `json:"role"` + Content []ContentPart `json:"content"` + TokenCount int `json:"tokenCount"` + FinishReason *string `json:"finishReason,omitempty"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} + +type Role string +const ( + RoleUser Role = "user" + RoleAssistant Role = "assistant" + RoleSystem Role = "system" +) +``` + +### Content Types + +```go +type ContentPart interface { + isPart() +} + +type TextContent struct { + Text string `json:"text"` +} + +type ImageURLContent struct { + ImageURL ImageURL `json:"image_url"` +} + +type ToolCall struct { + ID string `json:"id"` + Type string `json:"type"` + Function struct { + Name string `json:"name"` + Arguments string `json:"arguments"` + } `json:"function"` +} + +type ToolResult struct { + ToolCallID string `json:"tool_call_id"` + Content string `json:"content"` + Success bool `json:"success"` +} +``` + +## AI Agent API + +### Agent Service Interface + +```go +type Service interface { + pubsub.Suscriber[AgentEvent] + Model() models.Model + Run(ctx context.Context, sessionID string, content string, attachments ...message.Attachment) (<-chan AgentEvent, error) + Cancel(sessionID string) + IsSessionBusy(sessionID string) bool + IsBusy() bool + Update(agentName config.AgentName, modelID models.ModelID) (models.Model, error) + Summarize(ctx context.Context, sessionID string) error +} + +type AgentEvent struct { + Type AgentEventType + Message message.Message + Error error + + // When summarizing + SessionID string + Progress string + Done bool +} + +type AgentEventType string +const ( + AgentEventTypeError AgentEventType = "error" + AgentEventTypeResponse AgentEventType = "response" + AgentEventTypeSummarize AgentEventType = "summarize" +) +``` + +**Usage Example:** +```go +events, err := agentService.Run(ctx, sessionID, "Help me debug this function", attachments...) +if err != nil { + return err +} + +for event := range events { + switch event.Type { + case AgentEventContent: + fmt.Print(event.Content) + case AgentEventToolCall: + fmt.Printf("Executing tool: %s\n", event.ToolCall.Function.Name) + case AgentEventError: + fmt.Printf("Error: %v\n", event.Error) + case AgentEventFinish: + fmt.Println("Agent finished") + return + } +} +``` + +## LLM Provider API + +### Provider Interface + +```go +type Provider interface { + SendMessages(ctx context.Context, messages []message.Message, tools []tools.BaseTool) (*ProviderResponse, error) + StreamResponse(ctx context.Context, messages []message.Message, tools []tools.BaseTool) <-chan ProviderEvent + Model() models.Model +} + +type GenerationParams struct { + Model models.ModelID `json:"model"` + Messages []ProviderMessage `json:"messages"` + MaxTokens int `json:"max_tokens,omitempty"` + Temperature float64 `json:"temperature,omitempty"` + Tools []ToolDefinition `json:"tools,omitempty"` + ReasoningEffort string `json:"reasoning_effort,omitempty"` +} + +type ProviderEvent struct { + Type EventType + + Content string + Thinking string + Response *ProviderResponse + ToolCall *message.ToolCall + Error error +} + +type EventType string +const ( + EventContentStart EventType = "content_start" + EventToolUseStart EventType = "tool_use_start" + EventToolUseDelta EventType = "tool_use_delta" + EventToolUseStop EventType = "tool_use_stop" + EventContentDelta EventType = "content_delta" + EventThinkingDelta EventType = "thinking_delta" + EventContentStop EventType = "content_stop" + EventComplete EventType = "complete" + EventError EventType = "error" + EventWarning EventType = "warning" +) +``` + +### Supported Providers + +```go +type ModelProvider string +const ( + ProviderOpenAI ModelProvider = "openai" + ProviderAnthropic ModelProvider = "anthropic" + ProviderGemini ModelProvider = "gemini" + ProviderGroq ModelProvider = "groq" + ProviderBedrock ModelProvider = "bedrock" + ProviderAzure ModelProvider = "azure" + ProviderVertexAI ModelProvider = "vertexai" + ProviderOpenRouter ModelProvider = "openrouter" + ProviderCopilot ModelProvider = "copilot" + ProviderLocal ModelProvider = "local" + ProviderXAI ModelProvider = "xai" +) +``` + +## Tool System API + +### Tool Interface + +```go +type BaseTool interface { + Info() ToolInfo + Run(ctx context.Context, params ToolCall) (ToolResponse, error) +} + +type ToolInfo struct { + Name string `json:"name"` + Description string `json:"description"` + Parameters map[string]interface{} `json:"parameters"` + Required []string `json:"required"` +} + +type ToolCall struct { + ID string `json:"id"` + Name string `json:"name"` + Parameters map[string]interface{} `json:"parameters"` +} + +type ToolResponse struct { + Type toolResponseType `json:"type"` + Content string `json:"content"` + Metadata string `json:"metadata,omitempty"` + IsError bool `json:"is_error"` +} +``` + +### Built-in Tools + +#### File Operations + +```go +// View tool - read file contents +type ViewTool struct{} +func (t *ViewTool) Info() ToolInfo { + return ToolInfo{ + Name: "view", + Description: "View the contents of a file", + Parameters: map[string]interface{}{ + "file_path": map[string]interface{}{ + "type": "string", + "description": "Path to the file to view", + }, + "offset": map[string]interface{}{ + "type": "integer", + "description": "Line offset to start reading from (0-based)", + }, + "limit": map[string]interface{}{ + "type": "integer", + "description": "Maximum number of lines to read", + }, + }, + Required: []string{"file_path"}, + } +} + +// Write tool - write to files +type WriteTool struct{} +func (t *WriteTool) Info() ToolInfo { + return ToolInfo{ + Name: "write", + Description: "Write content to a file", + Parameters: map[string]interface{}{ + "file_path": map[string]interface{}{ + "type": "string", + "description": "Path to the file to write", + }, + "content": map[string]interface{}{ + "type": "string", + "description": "Content to write to the file", + }, + }, + Required: []string{"file_path", "content"}, + } +} +``` + +#### Directory Operations + +```go +// List tool - directory listing +type LsTool struct{} +func (t *LsTool) Info() ToolInfo { + return ToolInfo{ + Name: "ls", + Description: "List directory contents", + Parameters: map[string]interface{}{ + "path": map[string]interface{}{ + "type": "string", + "description": "Directory path to list (defaults to current directory)", + }, + "ignore": map[string]interface{}{ + "type": "array", + "description": "Patterns to ignore", + "items": map[string]interface{}{ + "type": "string", + }, + }, + }, + Required: []string{}, + } +} + +// Glob tool - pattern matching +type GlobTool struct{} +func (t *GlobTool) Info() ToolInfo { + return ToolInfo{ + Name: "glob", + Description: "Find files matching a pattern", + Parameters: map[string]interface{}{ + "pattern": map[string]interface{}{ + "type": "string", + "description": "Glob pattern to match files (e.g., '*.go', '**/*.js')", + }, + "path": map[string]interface{}{ + "type": "string", + "description": "Base path to search from (defaults to current directory)", + }, + }, + Required: []string{"pattern"}, + } +} +``` + +#### Shell Operations + +```go +// Bash tool - command execution +type BashTool struct{} +func (t *BashTool) Info() ToolInfo { + return ToolInfo{ + Name: "bash", + Description: "Execute a bash command", + Parameters: map[string]interface{}{ + "command": map[string]interface{}{ + "type": "string", + "description": "The bash command to execute", + }, + "timeout": map[string]interface{}{ + "type": "integer", + "description": "Timeout in seconds (default: 30)", + }, + }, + Required: []string{"command"}, + } +} +``` + +## LSP Integration API + +### LSP Client Interface + +```go +type Client struct { + serverType ServerType + command string + args []string + conn *Connection + capabilities ServerCapabilities + openFiles map[string]*OpenFile + diagnostics map[string][]Diagnostic +} + +func NewClient(language string, config LSPConfig) (*Client, error) +func (c *Client) Initialize(ctx context.Context, rootPath string) error +func (c *Client) OpenFile(ctx context.Context, filePath string) error +func (c *Client) CloseFile(ctx context.Context, filePath string) error +func (c *Client) NotifyFileChange(ctx context.Context, filePath, content string) error +func (c *Client) GetDiagnostics(ctx context.Context, filePath string) ([]Diagnostic, error) +func (c *Client) Shutdown(ctx context.Context) error +``` + +### LSP Types + +```go +type Diagnostic struct { + Range Range `json:"range"` + Severity int `json:"severity"` + Message string `json:"message"` + Source string `json:"source,omitempty"` + Code any `json:"code,omitempty"` +} + +type Range struct { + Start Position `json:"start"` + End Position `json:"end"` +} + +type Position struct { + Line int `json:"line"` // 0-based + Character int `json:"character"` // 0-based +} + +type ServerCapabilities struct { + TextDocumentSync int `json:"textDocumentSync"` + CompletionProvider bool `json:"completionProvider"` + HoverProvider bool `json:"hoverProvider"` + SignatureHelpProvider bool `json:"signatureHelpProvider"` + DefinitionProvider bool `json:"definitionProvider"` + DiagnosticProvider bool `json:"diagnosticProvider"` +} +``` + +## Permission System API + +### Permission Service Interface + +```go +type Service interface { + RequestPermission(ctx context.Context, req Request) (bool, error) + HasPermission(ctx context.Context, tool string) bool + GrantSessionPermission(ctx context.Context, tool string) + RevokePermission(ctx context.Context, tool string) + Subscribe(ctx context.Context) <-chan pubsub.Event[PermissionEvent] +} + +type Request struct { + Tool string `json:"tool"` + Description string `json:"description"` + Sensitive bool `json:"sensitive"` + Parameters any `json:"parameters,omitempty"` +} + +type PermissionEvent struct { + Type PermissionEventType `json:"type"` + Tool string `json:"tool"` + Granted bool `json:"granted"` + SessionOnly bool `json:"sessionOnly"` +} + +type PermissionEventType string +const ( + PermissionRequested PermissionEventType = "requested" + PermissionGranted PermissionEventType = "granted" + PermissionDenied PermissionEventType = "denied" +) +``` + +## Database API + +### Generated SQLC Queries + +```go +type Queries struct { + db DBTX +} + +// Session operations +func (q *Queries) CreateSession(ctx context.Context, arg CreateSessionParams) (Session, error) +func (q *Queries) GetSession(ctx context.Context, id string) (Session, error) +func (q *Queries) ListSessions(ctx context.Context, arg ListSessionsParams) ([]Session, error) +func (q *Queries) UpdateSession(ctx context.Context, arg UpdateSessionParams) error +func (q *Queries) DeleteSession(ctx context.Context, id string) error + +// Message operations +func (q *Queries) CreateMessage(ctx context.Context, arg CreateMessageParams) (Message, error) +func (q *Queries) GetMessage(ctx context.Context, id string) (Message, error) +func (q *Queries) ListMessages(ctx context.Context, arg ListMessagesParams) ([]Message, error) +func (q *Queries) UpdateMessage(ctx context.Context, arg UpdateMessageParams) error +func (q *Queries) DeleteMessage(ctx context.Context, id string) error + +// File operations +func (q *Queries) CreateFile(ctx context.Context, arg CreateFileParams) (File, error) +func (q *Queries) GetFile(ctx context.Context, path string) (File, error) +func (q *Queries) ListFiles(ctx context.Context) ([]File, error) +func (q *Queries) UpdateFile(ctx context.Context, arg UpdateFileParams) error +``` + +### Database Models + +```go +type Session struct { + ID string `json:"id"` + Title string `json:"title"` + ParentID *string `json:"parent_id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + ModelProvider string `json:"model_provider"` + ModelID string `json:"model_id"` + TotalTokens int32 `json:"total_tokens"` + InputTokens int32 `json:"input_tokens"` + OutputTokens int32 `json:"output_tokens"` + TotalCost float64 `json:"total_cost"` +} + +type Message struct { + ID string `json:"id"` + SessionID string `json:"session_id"` + Role string `json:"role"` + Content string `json:"content"` // JSON serialized ContentPart[] + TokenCount int32 `json:"token_count"` + FinishReason *string `json:"finish_reason"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type File struct { + Path string `json:"path"` + Content string `json:"content"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} +``` + +## Event System API + +### PubSub Interface + +```go +type Event[T any] struct { + Type EventType `json:"type"` + Data T `json:"data"` +} + +type Broker[T any] struct { + subscribers map[string]chan Event[T] + mu sync.RWMutex +} + +func NewBroker[T any]() *Broker[T] +func (b *Broker[T]) Subscribe(ctx context.Context) <-chan Event[T] +func (b *Broker[T]) Publish(event Event[T]) +func (b *Broker[T]) Close() +``` + +### Event Types + +```go +// Session events +type SessionEvent struct { + SessionID string `json:"sessionId"` + Action SessionAction `json:"action"` + Session *Session `json:"session,omitempty"` +} + +// Message events +type MessageEvent struct { + MessageID string `json:"messageId"` + SessionID string `json:"sessionId"` + Action MessageAction `json:"action"` + Message *Message `json:"message,omitempty"` +} + +// Agent events +type AgentEvent struct { + SessionID string `json:"sessionId"` + Type AgentEventType `json:"type"` + Content string `json:"content,omitempty"` + ToolCall *ToolCall `json:"toolCall,omitempty"` + ToolResult *ToolResult `json:"toolResult,omitempty"` + Error error `json:"error,omitempty"` +} +``` + +## TUI Component API + +### Base Component Interface + +```go +type Component interface { + tea.Model + SetSize(width, height int) + Focus() + Blur() + Focused() bool +} + +type BaseComponent struct { + width int + height int + focused bool + theme theme.Theme +} + +func (c *BaseComponent) SetSize(width, height int) { + c.width, c.height = width, height +} + +func (c *BaseComponent) Focus() { + c.focused = true +} + +func (c *BaseComponent) Blur() { + c.focused = false +} + +func (c *BaseComponent) Focused() bool { + return c.focused +} +``` + +### Theme Interface + +```go +type Theme interface { + Name() string + Colors() ThemeColors +} + +type ThemeColors struct { + Background lipgloss.Color + Text lipgloss.Color + Primary lipgloss.Color + Secondary lipgloss.Color + Success lipgloss.Color + Warning lipgloss.Color + Error lipgloss.Color + Border lipgloss.Color + BorderFocused lipgloss.Color + Highlight lipgloss.Color + Muted lipgloss.Color +} +``` + +## Error Types + +### Common Errors + +```go +var ( + ErrNotFound = errors.New("not found") + ErrInvalidInput = errors.New("invalid input") + ErrPermissionDenied = errors.New("permission denied") + ErrProviderNotFound = errors.New("provider not found") + ErrModelNotSupported = errors.New("model not supported") + ErrToolNotFound = errors.New("tool not found") + ErrSessionNotFound = errors.New("session not found") + ErrMessageNotFound = errors.New("message not found") +) + +// Wrapped errors provide context +type ProviderError struct { + Provider models.ModelProvider + Err error +} + +func (e *ProviderError) Error() string { + return fmt.Sprintf("provider %s: %v", e.Provider, e.Err) +} + +func (e *ProviderError) Unwrap() error { + return e.Err +} +``` + +## Usage Examples + +### Complete Agent Interaction + +```go +func ExampleAgentInteraction() error { + // Setup + ctx := context.Background() + db, err := db.Connect() + if err != nil { + return err + } + defer db.Close() + + app, err := app.New(ctx, db) + if err != nil { + return err + } + defer app.Shutdown() + + // Create session + session, err := app.Sessions.CreateSession(ctx, "API Example", nil) + if err != nil { + return err + } + + // Run agent + events, err := app.CoderAgent.Run(ctx, session.ID, "Explain Go interfaces") + if err != nil { + return err + } + + // Process events + for event := range events { + switch event.Type { + case AgentEventContent: + fmt.Print(event.Content) + case AgentEventToolCall: + fmt.Printf("\n[Tool: %s]\n", event.ToolCall.Function.Name) + case AgentEventFinish: + fmt.Println("\n[Finished]") + return nil + case AgentEventError: + return event.Error + } + } + + return nil +} +``` + +### Custom Tool Implementation + +```go +func ExampleCustomTool() { + type CustomTool struct { + permissions permission.Service + } + + func (t *CustomTool) Info() ToolInfo { + return ToolInfo{ + Name: "custom_tool", + Description: "A custom tool example", + Parameters: map[string]interface{}{ + "input": map[string]interface{}{ + "type": "string", + "description": "Input parameter", + }, + }, + Required: []string{"input"}, + } + } + + func (t *CustomTool) Run(ctx context.Context, params ToolCall) (ToolResponse, error) { + // Permission check + permitted, err := t.permissions.RequestPermission(ctx, permission.Request{ + Tool: "custom_tool", + Description: "Execute custom operation", + Sensitive: false, + }) + if err != nil { + return ToolResponse{}, err + } + if !permitted { + return ToolResponse{}, ErrPermissionDenied + } + + // Extract parameters + input, ok := params.Parameters["input"].(string) + if !ok { + return ToolResponse{}, fmt.Errorf("input must be a string") + } + + // Tool logic + result := fmt.Sprintf("Processed: %s", input) + + return ToolResponse{ + Success: true, + Content: result, + }, nil + } +} +``` + +This API reference provides the foundation for understanding and extending OpenCode's capabilities. Each interface is designed for extensibility while maintaining type safety and clear contracts. diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 000000000..e8f651819 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,739 @@ +# OpenCode Architecture Documentation + +## Overview + +OpenCode is a sophisticated terminal-based AI coding assistant built in Go, designed to provide intelligent development assistance through a rich Terminal User Interface (TUI). It integrates multiple Large Language Model (LLM) providers, Language Server Protocol (LSP) support, and a comprehensive tool ecosystem to help developers with coding tasks, debugging, and code understanding. + +## Project Structure + +``` +opencode/ +├── .claude/ # Claude AI configuration +├── .git/ # Git repository data +├── .github/ # GitHub workflows and CI/CD +│ └── workflows/ +│ ├── build.yml +│ └── release.yml +├── .opencode/ # Local application data +│ ├── commands/ # User custom commands +│ ├── init # Initialization script +│ ├── opencode.db # SQLite database +│ ├── opencode.db-shm # SQLite shared memory +│ └── opencode.db-wal # SQLite WAL file +├── cmd/ # CLI interface and commands +│ ├── root.go # Main CLI command definition +│ └── schema/ # Configuration schema generation +│ ├── README.md +│ └── main.go +├── internal/ # Core application logic +│ ├── app/ # Application orchestration +│ │ ├── app.go # Main app service +│ │ └── lsp.go # LSP integration +│ ├── completions/ # Shell completion logic +│ │ └── files-folders.go +│ ├── config/ # Configuration management +│ │ ├── config.go # Configuration types and loading +│ │ └── init.go # Initialization logic +│ ├── db/ # Database layer (SQLite + SQLC) +│ │ ├── connect.go # Database connection +│ │ ├── db.go # Database interface +│ │ ├── embed.go # Embedded migrations +│ │ ├── files.sql.go # Generated file queries +│ │ ├── messages.sql.go # Generated message queries +│ │ ├── models.go # Generated database models +│ │ ├── querier.go # Generated query interface +│ │ ├── sessions.sql.go # Generated session queries +│ │ ├── migrations/ # Database migrations +│ │ │ ├── 20250424200609_initial.sql +│ │ │ └── 20250515105448_add_summary_message_id.sql +│ │ └── sql/ # SQL query definitions +│ │ ├── files.sql +│ │ ├── messages.sql +│ │ └── sessions.sql +│ ├── diff/ # File diff and patch utilities +│ │ ├── diff.go +│ │ └── patch.go +│ ├── fileutil/ # File system utilities +│ │ └── fileutil.go +│ ├── format/ # Output formatting +│ │ ├── format.go # Text/JSON output formatting +│ │ └── spinner.go # Loading spinner for CLI +│ ├── history/ # File change history +│ │ └── file.go +│ ├── llm/ # LLM integration layer +│ │ ├── agent/ # AI agent orchestration +│ │ │ ├── agent-tool.go # Agent tool integration +│ │ │ ├── agent.go # Core agent logic +│ │ │ ├── mcp-tools.go # MCP tool integration +│ │ │ └── tools.go # Tool definitions +│ │ ├── models/ # Model definitions and metadata +│ │ │ ├── anthropic.go # Anthropic model definitions +│ │ │ ├── azure.go # Azure OpenAI models +│ │ │ ├── copilot.go # GitHub Copilot models +│ │ │ ├── gemini.go # Google Gemini models +│ │ │ ├── groq.go # GROQ models +│ │ │ ├── local.go # Local model support +│ │ │ ├── models.go # Model registry and metadata +│ │ │ ├── openai.go # OpenAI models +│ │ │ ├── openrouter.go # OpenRouter models +│ │ │ ├── vertexai.go # Google VertexAI models +│ │ │ └── xai.go # xAI models +│ │ ├── prompt/ # Prompt engineering and templates +│ │ │ ├── coder.go # Coding assistant prompts +│ │ │ ├── prompt.go # Base prompt types +│ │ │ ├── prompt_test.go # Prompt testing +│ │ │ ├── summarizer.go # Context summarization +│ │ │ ├── task.go # Task-specific prompts +│ │ │ └── title.go # Title generation prompts +│ │ ├── provider/ # LLM provider implementations +│ │ │ ├── anthropic.go # Anthropic Claude integration +│ │ │ ├── azure.go # Azure OpenAI integration +│ │ │ ├── bedrock.go # AWS Bedrock integration +│ │ │ ├── copilot.go # GitHub Copilot integration +│ │ │ ├── gemini.go # Google Gemini integration +│ │ │ ├── openai.go # OpenAI integration +│ │ │ ├── provider.go # Provider interface and common logic +│ │ │ ├── provider_test.go # Provider testing +│ │ │ └── vertexai.go # Google VertexAI integration +│ │ └── tools/ # AI tool implementations +│ │ ├── bash.go # Shell command execution +│ │ ├── diagnostics.go # LSP diagnostics integration +│ │ ├── edit.go # File editing operations +│ │ ├── fetch.go # HTTP/web content fetching +│ │ ├── file.go # File operations +│ │ ├── glob.go # File pattern matching +│ │ ├── grep.go # Text search operations +│ │ ├── ls.go # Directory listing +│ │ ├── ls_test.go # Directory listing tests +│ │ ├── patch.go # File patching operations +│ │ ├── sourcegraph.go # Sourcegraph code search +│ │ ├── tools.go # Tool registry and interface +│ │ ├── view.go # File viewing operations +│ │ ├── write.go # File writing operations +│ │ └── shell/ # Shell integration utilities +│ │ └── shell.go +│ ├── logging/ # Structured logging system +│ │ ├── logger.go # Core logging functionality +│ │ ├── message.go # Message-specific logging +│ │ └── writer.go # Log writers and output +│ ├── lsp/ # Language Server Protocol integration +│ │ ├── client.go # LSP client implementation +│ │ ├── handlers.go # LSP message handlers +│ │ ├── language.go # Language-specific configurations +│ │ ├── methods.go # LSP method implementations +│ │ ├── transport.go # LSP transport layer +│ │ ├── protocol.go # LSP protocol definitions +│ │ ├── protocol/ # LSP protocol types +│ │ │ ├── LICENSE # Protocol license +│ │ │ ├── interface.go # Protocol interfaces +│ │ │ ├── pattern_interfaces.go # Pattern matching interfaces +│ │ │ ├── tables.go # Protocol tables +│ │ │ ├── tsdocument-changes.go # Document change tracking +│ │ │ ├── tsjson.go # JSON protocol handling +│ │ │ ├── tsprotocol.go # TypeScript protocol definitions +│ │ │ └── uri.go # URI handling +│ │ ├── util/ # LSP utilities +│ │ │ └── edit.go # Edit operation utilities +│ │ └── watcher/ # File watching for LSP +│ │ └── watcher.go +│ ├── message/ # Message handling system +│ │ ├── attachment.go # File attachments +│ │ ├── content.go # Message content types +│ │ └── message.go # Core message logic +│ ├── permission/ # Permission and security system +│ │ └── permission.go +│ ├── pubsub/ # Event system +│ │ ├── broker.go # Event broker +│ │ └── events.go # Event definitions +│ ├── session/ # Session management +│ │ └── session.go +│ ├── tui/ # Terminal User Interface +│ │ ├── tui.go # Main TUI orchestration +│ │ ├── components/ # UI components +│ │ │ ├── chat/ # Chat interface components +│ │ │ │ ├── chat.go # Chat container +│ │ │ │ ├── editor.go # Message editor +│ │ │ │ ├── list.go # Message list +│ │ │ │ ├── message.go # Individual messages +│ │ │ │ └── sidebar.go # Chat sidebar +│ │ │ ├── core/ # Core UI components +│ │ │ │ └── status.go # Status bar +│ │ │ ├── dialog/ # Modal dialogs +│ │ │ │ ├── arguments.go # Command argument dialogs +│ │ │ │ ├── commands.go # Command selection dialog +│ │ │ │ ├── complete.go # Completion dialogs +│ │ │ │ ├── custom_commands.go # Custom command dialog +│ │ │ │ ├── custom_commands_test.go # Custom command tests +│ │ │ │ ├── filepicker.go # File picker dialog +│ │ │ │ ├── help.go # Help dialog +│ │ │ │ ├── init.go # Initialization dialog +│ │ │ │ ├── models.go # Model selection dialog +│ │ │ │ ├── permission.go # Permission dialog +│ │ │ │ ├── quit.go # Quit confirmation dialog +│ │ │ │ ├── session.go # Session selection dialog +│ │ │ │ └── theme.go # Theme selection dialog +│ │ │ ├── logs/ # Log viewing components +│ │ │ │ ├── details.go # Log detail view +│ │ │ │ └── table.go # Log table view +│ │ │ └── util/ # UI utilities +│ │ │ └── simple-list.go # Simple list component +│ │ ├── image/ # Image handling for TUI +│ │ │ └── images.go +│ │ ├── layout/ # Layout management +│ │ │ ├── container.go # Container layouts +│ │ │ ├── layout.go # Layout utilities +│ │ │ ├── overlay.go # Overlay management +│ │ │ └── split.go # Split pane layouts +│ │ ├── page/ # Page management +│ │ │ ├── chat.go # Chat page +│ │ │ ├── logs.go # Logs page +│ │ │ └── page.go # Page interface +│ │ ├── styles/ # UI styling +│ │ │ ├── background.go # Background styles +│ │ │ ├── icons.go # Icon definitions +│ │ │ ├── markdown.go # Markdown rendering styles +│ │ │ └── styles.go # Core styles +│ │ ├── theme/ # Theme system +│ │ │ ├── catppuccin.go # Catppuccin theme +│ │ │ ├── dracula.go # Dracula theme +│ │ │ ├── flexoki.go # Flexoki theme +│ │ │ ├── gruvbox.go # Gruvbox theme +│ │ │ ├── manager.go # Theme manager +│ │ │ ├── monokai.go # Monokai theme +│ │ │ ├── onedark.go # OneDark theme +│ │ │ ├── opencode.go # OpenCode default theme +│ │ │ ├── theme.go # Theme interface +│ │ │ ├── theme_test.go # Theme tests +│ │ │ ├── tokyonight.go # Tokyo Night theme +│ │ │ └── tron.go # Tron theme +│ │ └── util/ # TUI utilities +│ │ └── util.go +│ └── version/ # Version information +│ └── version.go +├── sandbox/ # Development sandbox +│ └── zoo-simulator/ +│ └── zoo.py +├── scripts/ # Build and utility scripts +│ ├── check_hidden_chars.sh # Character validation script +│ ├── release # Release script +│ └── snapshot # Snapshot build script +├── .gitignore # Git ignore rules +├── .goreleaser.yml # GoReleaser configuration +├── .opencode.json # Local configuration file +├── AGENT.md # Agent development guide +├── LICENSE # MIT license +├── OpenCode.md # Development commands and conventions +├── README.md # Main project documentation +├── checklist-plan.md # Development checklist +├── go.mod # Go module definition +├── go.sum # Go module checksums +├── install # Installation script +├── main.go # Application entry point +├── opencode # Compiled binary +├── opencode-schema.json # Configuration schema +├── proposal.md # Project proposal document +└── sqlc.yaml # SQLC configuration +``` + +## Core Architecture + +### High-Level Architecture + +OpenCode follows a layered architecture with clear separation of concerns: + +```mermaid +graph TB + subgraph "User Interface Layer" + TUI[Terminal UI
Bubble Tea] + CLI[CLI Interface
Cobra] + end + + subgraph "Application Layer" + App[App Service
Orchestration] + Session[Session Management] + Permission[Permission System] + end + + subgraph "Service Layer" + Agent[AI Agent
Orchestration] + Messages[Message Service] + History[File History] + end + + subgraph "Integration Layer" + LLM[LLM Providers
Multi-provider] + LSP[Language Servers
LSP Integration] + Tools[Tool System
Extensible] + end + + subgraph "Data Layer" + DB[(SQLite Database
SQLC Generated)] + Config[Configuration
Viper + Files] + Files[File System
Operations] + end + + TUI --> App + CLI --> App + App --> Session + App --> Permission + App --> Agent + Agent --> Messages + Agent --> History + Agent --> LLM + Agent --> LSP + Agent --> Tools + Messages --> DB + Session --> DB + History --> Files + Tools --> Files + Tools --> LSP + LSP --> Files + App --> Config +``` + +### Component Relationships + +The architecture is built around several key patterns: + +1. **Service-Oriented Architecture**: Each major functionality is encapsulated in a service with clear interfaces +2. **Event-Driven Communication**: Components communicate through a pub/sub system for loose coupling +3. **Provider Pattern**: Multiple LLM providers are abstracted behind a common interface +4. **Tool System**: Extensible capabilities through a plugin-like tool architecture + +## Core Components + +### 1. Application Layer (`internal/app/`) + +The application layer serves as the composition root and orchestrator for all services. + +**Key Responsibilities:** +- Service lifecycle management and dependency injection +- Coordination between TUI and CLI modes +- LSP client initialization and management +- Theme system initialization +- Non-interactive mode execution + +**Core Type:** +```go +type App struct { + Sessions session.Service // Session management + Messages message.Service // Message handling + History history.Service // File change tracking + Permissions permission.Service // Security and permissions + CoderAgent agent.Service // AI agent orchestration + LSPClients map[string]*lsp.Client // Language server clients +} +``` + +### 2. Configuration System (`internal/config/`) + +Comprehensive configuration management supporting multiple sources and runtime updates. + +**Features:** +- Multi-source configuration (environment variables, config files, defaults) +- Provider and model validation +- Agent-specific configuration +- MCP server configuration +- LSP configuration per language +- Runtime configuration updates + +**Configuration Structure:** +```go +type Config struct { + Data Data // Storage configuration + MCPServers map[string]MCPServer // MCP server configs + Providers map[models.ModelProvider]Provider // LLM provider configs + LSP map[string]LSPConfig // LSP configurations + Agents map[AgentName]Agent // Agent configurations + TUI TUIConfig // UI configuration + Shell ShellConfig // Shell configuration + AutoCompact bool // Auto-summarization +} +``` + +### 3. Database Layer (`internal/db/`) + +SQLite-based persistence layer with SQLC-generated type-safe operations. + +**Key Features:** +- SQLC code generation for type safety +- Goose-based schema migrations +- Prepared statement optimization +- Transaction support +- Connection pooling + +**Core Entities:** +- **Sessions**: Conversation sessions with metadata, token tracking, and cost calculation +- **Messages**: Individual messages with multi-part content support +- **Files**: File history and version tracking for change management + +### 4. LLM Integration (`internal/llm/`) + +Sophisticated multi-provider LLM integration with agent orchestration. + +#### Agent System (`internal/llm/agent/`) + +High-level conversation management and tool orchestration. + +**Key Features:** +- Agent lifecycle management (create, run, cancel, summarize) +- Tool execution coordination and permission management +- Token usage tracking and cost calculation +- Context summarization for long conversations +- Automatic title generation +- Session management integration + +**Agent Service Interface:** +```go +type Service interface { + Run(ctx context.Context, sessionID string, content string, attachments ...message.Attachment) (<-chan AgentEvent, error) + Cancel(sessionID string) + Summarize(ctx context.Context, sessionID string) error + Subscribe(ctx context.Context) <-chan pubsub.Event[AgentEvent] +} +``` + +#### Provider System (`internal/llm/provider/`) + +Multi-provider abstraction for LLM integration. + +**Supported Providers:** +- **Anthropic**: Claude models with reasoning capabilities +- **OpenAI**: GPT models including O1 and O3 series +- **Google**: Gemini models via both direct API and VertexAI +- **GitHub Copilot**: Enterprise model access +- **GROQ**: High-speed inference models +- **Azure OpenAI**: Enterprise OpenAI models +- **AWS Bedrock**: Claude models via AWS +- **OpenRouter**: Multi-provider aggregation +- **Local**: Self-hosted model support +- **XAI**: Grok models + +**Provider Interface:** +```go +type Provider interface { + GenerateCompletion(ctx context.Context, params GenerationParams) (<-chan ProviderEvent, error) + GetModels() []models.Model + SupportsModel(modelID models.ModelID) bool +} +``` + +#### Models System (`internal/llm/models/`) + +Model metadata and capability management. + +**Key Features:** +- Model capability tracking (reasoning, vision, tool calling) +- Cost calculation parameters (input/output token costs) +- Context window limits and management +- Provider-model relationship mapping +- Model availability validation + +#### Tools System (`internal/llm/tools/`) + +Extensible tool ecosystem providing AI agents with capabilities. + +**Available Tools:** +- **File Operations**: `view`, `write`, `edit`, `patch` - Complete file manipulation +- **Directory Operations**: `ls`, `glob` - File system navigation and search +- **Text Operations**: `grep` - Content search and pattern matching +- **Shell Integration**: `bash` - Command execution with timeout and security +- **LSP Integration**: `diagnostics` - Code analysis and error detection +- **Web Integration**: `fetch` - HTTP content retrieval +- **Code Search**: `sourcegraph` - Public code repository search +- **Agent Delegation**: `agent` - Sub-task delegation to specialized agents + +**Tool Interface:** +```go +type BaseTool interface { + Info() ToolInfo + Run(ctx context.Context, params ToolCall) (ToolResponse, error) +} +``` + +### 5. Terminal User Interface (`internal/tui/`) + +Rich terminal interface built with the Bubble Tea framework. + +**Architecture:** +- **Page-based Navigation**: Chat and Logs pages with smooth transitions +- **Component Composition**: Modular UI components with clear responsibilities +- **Theme System**: Multiple color schemes with runtime switching +- **Modal System**: Overlay dialogs for configuration and selection +- **Keyboard Management**: Vim-inspired key bindings with context sensitivity + +**Key Components:** +- **Pages**: Chat interface, log viewing +- **Dialogs**: Permissions, sessions, commands, models, file picker, themes, help +- **Components**: Status bar, message list, editor, sidebar +- **Themes**: 10+ built-in themes with customization support + +**TUI Architecture:** +```go +type TUI struct { + app *app.App + pages map[page.PageType]page.Page + currentPage page.PageType + overlay layout.Overlay + theme theme.Theme + keyBindings KeyBindings +} +``` + +### 6. Language Server Protocol (`internal/lsp/`) + +IDE-like language intelligence integration. + +**Features:** +- **Multi-language Support**: Go, TypeScript, Rust, Python, and more +- **Smart Initialization**: Language detection and appropriate server selection +- **File Management**: Efficient file opening, closing, and change notifications +- **Diagnostic Integration**: Real-time error and warning collection +- **Performance Optimization**: Lazy loading and caching strategies + +**LSP Integration Strategy:** +- Lazy file opening for performance +- Background server initialization +- Diagnostic caching with automatic updates +- Context-aware tool integration +- Server lifecycle management + +### 7. Session Management (`internal/session/`) + +Conversation session lifecycle and relationship management. + +**Key Features:** +- Session creation with metadata tracking +- Parent-child relationships for task delegation +- Token usage and cost tracking per session +- Title generation and automatic naming +- Session persistence and restoration +- Event broadcasting for UI updates + +### 8. Message System (`internal/message/`) + +Sophisticated message handling with multi-part content support. + +**Features:** +- **Multi-part Content**: Text, images, tool calls, tool results, reasoning content +- **Streaming Support**: Real-time message updates during generation +- **Content Serialization**: Efficient storage and retrieval +- **Role Management**: User, assistant, system message classification +- **Attachment Handling**: File attachments with metadata + +**Content Architecture:** +```go +type ContentPart interface { + isPart() +} + +// Implementations: +// - TextContent: Regular text content +// - ReasoningContent: Model reasoning traces +// - ImageURLContent: Image references +// - BinaryContent: File attachments +// - ToolCall: Tool execution requests +// - ToolResult: Tool execution results +// - Finish: Completion metadata +``` + +### 9. Permission System (`internal/permission/`) + +Security and user consent management for tool execution. + +**Features:** +- **Tool Execution Permissions**: Granular control over tool access +- **Session-based Auto-approval**: Persistent permissions within sessions +- **Interactive Consent**: User prompts for sensitive operations +- **Permission Persistence**: Remember user choices across sessions +- **Security Context**: Track and validate permission grants + +### 10. Event System (`internal/pubsub/`) + +Type-safe event-driven communication between components. + +**Features:** +- **Type Safety**: Generic event types with compile-time validation +- **Subscriber Management**: Automatic subscription lifecycle +- **Context Integration**: Cancellation and timeout support +- **Buffered Channels**: Performance optimization for high-frequency events +- **Error Handling**: Graceful degradation on subscriber failures + +### 11. Logging Infrastructure (`internal/logging/`) + +Comprehensive logging system with persistence and structure. + +**Features:** +- **Structured Logging**: Consistent log format with contextual fields +- **Level-based Filtering**: Debug, Info, Warn, Error with appropriate routing +- **Persistent Logging**: Important events saved for debugging +- **Session Context**: Message-level logging with session association +- **Panic Recovery**: Graceful error handling with stack traces +- **Caller Information**: Automatic source location tracking + +## Data Flow Architecture + +### User Interaction Flow + +```mermaid +sequenceDiagram + participant User + participant TUI + participant App + participant Agent + participant Provider + participant Tools + participant DB + + User->>TUI: Input message + TUI->>App: Process input + App->>Agent: Run conversation + Agent->>DB: Save message + Agent->>Provider: Generate response + Provider-->>Agent: Stream response + Agent->>Tools: Execute tool calls + Tools-->>Agent: Return results + Agent->>DB: Save response + Agent-->>TUI: Update UI + TUI-->>User: Display response +``` + +### Configuration Flow + +```mermaid +flowchart TD + A[Environment Variables] --> D[Config Loading] + B[Config Files] --> D + C[Default Values] --> D + D --> E[Provider Validation] + E --> F[Model Selection] + F --> G[Agent Creation] + G --> H[Tool Registration] + H --> I[Application Ready] +``` + +### Tool Execution Flow + +```mermaid +sequenceDiagram + participant Agent + participant Permission + participant Tool + participant LSP + participant FileSystem + + Agent->>Permission: Request tool permission + Permission-->>Agent: Permission granted + Agent->>Tool: Execute tool call + Tool->>LSP: Get diagnostics (if applicable) + Tool->>FileSystem: Perform operation + FileSystem-->>Tool: Operation result + LSP-->>Tool: Diagnostic info + Tool-->>Agent: Tool response + Agent->>Agent: Continue conversation +``` + +## Key Design Patterns + +### 1. Service Layer Pattern +Each major component implements a service interface with clear contracts: +- Clean dependency injection through the App struct +- Service lifecycle management +- Consistent error handling and logging +- Interface-based design for testability + +### 2. Repository Pattern +Database operations are abstracted through service interfaces: +- SQLC provides type-safe query generation +- Transaction support for complex operations +- Consistent data access patterns +- Migration management with Goose + +### 3. Strategy Pattern +Multiple implementations behind common interfaces: +- **Provider System**: Swap LLM providers transparently +- **Tool System**: Extensible capabilities through plugins +- **Theme System**: Runtime UI customization +- **Model Selection**: Dynamic model switching + +### 4. Observer Pattern +Event-driven architecture for loose coupling: +- Pub/sub system for component communication +- Real-time UI updates without tight coupling +- Service event broadcasting +- Context-aware event handling + +### 5. Factory Pattern +Dynamic object creation based on configuration: +- Provider instantiation based on available credentials +- Agent creation with proper dependency injection +- Tool registration and instantiation +- Theme loading and customization + +## Integration Points + +### LSP Integration +- **Tool Integration**: Tools can query LSP servers for diagnostics and code intelligence +- **File Operations**: File changes trigger appropriate LSP notifications +- **Multi-language Support**: Different language servers for different file types +- **Performance**: Lazy loading and caching for optimal performance + +### Database Integration +- **Persistence**: All application state flows through SQLC-generated queries +- **Transactions**: Complex operations wrapped in database transactions +- **Migrations**: Schema evolution managed through Goose migrations +- **Type Safety**: Compile-time verification of database operations + +### Configuration Integration +- **Runtime Updates**: Configuration changes without application restart +- **Validation**: Provider availability and model compatibility checking +- **Defaults**: Sensible defaults with override capabilities +- **Environment**: Environment variable integration for deployment flexibility + +## Performance Considerations + +### Streaming Architecture +- **Real-time Responses**: LLM responses streamed to UI in real-time +- **Progressive Updates**: UI updates incrementally during generation +- **Concurrent Operations**: Multiple tools can execute simultaneously +- **Memory Efficiency**: Streaming reduces memory usage for large responses + +### Lazy Loading +- **LSP Files**: Language server files opened only when needed +- **UI Components**: Page components loaded on demand +- **Provider Initialization**: Providers initialized only when used +- **Tool Loading**: Tools registered but not initialized until first use + +### Caching Strategies +- **LSP Diagnostics**: Diagnostic results cached and updated incrementally +- **Configuration**: Config values cached with invalidation on change +- **Message Content**: Efficient serialization and retrieval of message parts +- **Theme Data**: Theme calculations cached for performance + +### Resource Management +- **Connection Pooling**: Database connections efficiently managed +- **Goroutine Management**: Careful lifecycle management of background processes +- **Memory Usage**: Efficient data structures and garbage collection awareness +- **File Handles**: Proper cleanup of file system resources + +## Security Considerations + +### Tool Execution Security +- **Permission System**: User consent required for potentially dangerous operations +- **Command Validation**: Shell commands validated before execution +- **File System Access**: Controlled access to file system operations +- **Network Access**: Controlled HTTP requests with timeout limits + +### Data Security +- **Credential Management**: API keys stored securely and not logged +- **Database Security**: SQLite database with appropriate file permissions +- **Session Security**: Session data protected from unauthorized access +- **Logging Security**: Sensitive data filtered from logs + +### Input Validation +- **Command Parameters**: All tool inputs validated before execution +- **File Paths**: Path traversal protection for file operations +- **Network Requests**: URL validation and timeout enforcement +- **Configuration**: Config values validated and sanitized + +This architecture demonstrates a mature, production-ready codebase with clear separation of concerns, comprehensive error handling, and extensible design patterns suitable for a complex AI coding assistant. The system is designed for maintainability, testability, and extensibility while providing a rich user experience through the terminal interface. diff --git a/docs/development-guide.md b/docs/development-guide.md new file mode 100644 index 000000000..ac66c1287 --- /dev/null +++ b/docs/development-guide.md @@ -0,0 +1,729 @@ +# OpenCode Development Guide + +## Overview + +This guide provides comprehensive information for developers working on the OpenCode project. It covers development setup, code conventions, testing strategies, and contribution guidelines. + +## Prerequisites + +- **Go**: Version 1.24.0 or higher +- **SQLite**: For database operations (usually system-provided) +- **Git**: For version control + +## Development Setup + +### 1. Clone and Build + +```bash +# Clone the repository +git clone https://github.com/opencode-ai/opencode.git +cd opencode + +# Build the application +go build -o opencode + +# Run the application +./opencode +``` + +### 2. Development Commands + +Based on the project's `OpenCode.md` file: + +```bash +# Build +go build -o opencode ./main.go + +# Lint/Check +go vet ./... + +# Test all packages +go test ./... + +# Test single package +go test -v ./internal/tui/theme/... + +# Test specific test +go test -v ./internal/tui/theme/... -run TestThemeRegistration + +# Generate database code +sqlc generate + +# Snapshot build (for releases) +goreleaser build --clean --snapshot --skip validate +``` + +### 3. Environment Variables + +Set up API keys for LLM providers you want to test: + +```bash +export ANTHROPIC_API_KEY="your-key" +export OPENAI_API_KEY="your-key" +export GEMINI_API_KEY="your-key" +export GITHUB_TOKEN="your-token" # For Copilot +export GROQ_API_KEY="your-key" +``` + +## Code Style and Conventions + +### Import Organization +Follow Go standards with clear separation: +```go +import ( + // Standard library first + "context" + "fmt" + "os" + + // External packages second + "github.com/spf13/cobra" + "github.com/charmbracelet/bubbletea" + + // Internal packages third + "github.com/opencode-ai/opencode/internal/app" + "github.com/opencode-ai/opencode/internal/config" +) +``` + +### Naming Conventions +- **Exported functions/types**: PascalCase (`ConfigManager`, `LoadConfig`) +- **Unexported functions/types**: camelCase (`configManager`, `loadConfig`) +- **Constants**: UPPER_SNAKE_CASE (`MAX_TOKENS`, `DEFAULT_MODEL`) +- **Interfaces**: Use descriptive names, often ending in -er (`Provider`, `Service`) + +### Error Handling +Use structured error handling with context: + +```go +// Good +func processFile(path string) error { + if _, err := os.Stat(path); err != nil { + return fmt.Errorf("failed to access file %s: %w", path, err) + } + // ... processing logic + return nil +} + +// Context with errors +func runAgent(ctx context.Context, prompt string) error { + select { + case <-ctx.Done(): + return fmt.Errorf("agent execution cancelled: %w", ctx.Err()) + default: + // ... agent logic + } + return nil +} +``` + +### Database Patterns +Use SQLC for type-safe database operations: + +```go +// Generated by SQLC +func (q *Queries) CreateSession(ctx context.Context, arg CreateSessionParams) (Session, error) { + // ... generated code +} + +// Usage in service +func (s *SessionService) CreateSession(ctx context.Context, title string) (*Session, error) { + session, err := s.queries.CreateSession(ctx, CreateSessionParams{ + Title: title, + CreatedAt: time.Now(), + }) + if err != nil { + return nil, fmt.Errorf("failed to create session: %w", err) + } + return &session, nil +} +``` + +### Context Usage +Always pass `context.Context` as the first parameter: + +```go +func (p *Provider) GenerateCompletion(ctx context.Context, params GenerationParams) (<-chan ProviderEvent, error) { + // Use context for cancellation and timeouts + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + // ... implementation + } +} +``` + +## Architecture Patterns + +### Service Pattern +Each major component follows the service pattern: + +```go +type Service interface { + Method(ctx context.Context, params SomeParams) (Result, error) + Subscribe(ctx context.Context) <-chan pubsub.Event[EventType] +} + +type serviceImpl struct { + db *sql.DB + config *config.Config + logger *logging.Logger +} + +func NewService(db *sql.DB, config *config.Config) Service { + return &serviceImpl{ + db: db, + config: config, + logger: logging.NewLogger("service-name"), + } +} +``` + +### Provider Pattern +For LLM providers, implement the common interface: + +```go +type Provider interface { + GenerateCompletion(ctx context.Context, params GenerationParams) (<-chan ProviderEvent, error) + GetModels() []models.Model + SupportsModel(modelID models.ModelID) bool +} + +type anthropicProvider struct { + client *anthropic.Client + config ProviderConfig +} + +func (p *anthropicProvider) GenerateCompletion(ctx context.Context, params GenerationParams) (<-chan ProviderEvent, error) { + eventChan := make(chan ProviderEvent, 100) + + go func() { + defer close(eventChan) + // ... streaming implementation + }() + + return eventChan, nil +} +``` + +### Tool Pattern +Tools follow a consistent interface: + +```go +type BaseTool interface { + Info() ToolInfo + Run(ctx context.Context, params ToolCall) (ToolResponse, error) +} + +type FileTool struct { + permissions permission.Service +} + +func (t *FileTool) Info() ToolInfo { + return ToolInfo{ + Name: "write", + Description: "Write content to a file", + Parameters: map[string]interface{}{ + "file_path": map[string]interface{}{ + "type": "string", + "description": "Path to the file to write", + }, + "content": map[string]interface{}{ + "type": "string", + "description": "Content to write to the file", + }, + }, + Required: []string{"file_path", "content"}, + } +} + +func (t *FileTool) Run(ctx context.Context, params ToolCall) (ToolResponse, error) { + // Permission check + if !t.permissions.HasPermission(ctx, "file.write") { + return ToolResponse{}, ErrPermissionDenied + } + + // Implementation + // ... + + return ToolResponse{ + Success: true, + Content: "File written successfully", + }, nil +} +``` + +## Testing Strategy + +### Unit Tests +Place tests in `_test.go` files alongside source files: + +```go +func TestSessionCreation(t *testing.T) { + // Setup + db := setupTestDB(t) + service := NewSessionService(db, testConfig()) + + // Test + session, err := service.CreateSession(context.Background(), "Test Session") + + // Assertions + assert.NoError(t, err) + assert.NotNil(t, session) + assert.Equal(t, "Test Session", session.Title) +} + +func TestProviderSelection(t *testing.T) { + tests := []struct { + name string + config ProviderConfig + expected models.ModelProvider + wantErr bool + }{ + { + name: "anthropic preferred", + config: ProviderConfig{ + Providers: map[models.ModelProvider]Provider{ + models.ProviderAnthropic: {Disabled: false}, + models.ProviderOpenAI: {Disabled: false}, + }, + }, + expected: models.ProviderAnthropic, + wantErr: false, + }, + // ... more test cases + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := SelectProvider(tt.config) + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assert.Equal(t, tt.expected, result) + }) + } +} +``` + +### Integration Tests +For components that interact with external services: + +```go +func TestLSPIntegration(t *testing.T) { + if testing.Short() { + t.Skip("Skipping integration test in short mode") + } + + // Setup LSP client + client, err := lsp.NewClient("gopls") + require.NoError(t, err) + defer client.Shutdown() + + // Test diagnostics + diagnostics, err := client.GetDiagnostics(context.Background(), "test.go") + assert.NoError(t, err) + assert.IsType(t, []lsp.Diagnostic{}, diagnostics) +} +``` + +### TUI Testing +For TUI components, test the underlying logic: + +```go +func TestChatMessage(t *testing.T) { + msg := NewChatMessage("user", "Hello, world!") + + // Test rendering + rendered := msg.View() + assert.Contains(t, rendered, "Hello, world!") + + // Test updates + msg.SetContent("Updated content") + updated := msg.View() + assert.Contains(t, updated, "Updated content") +} +``` + +## Database Development + +### Migrations +Use Goose for database migrations: + +```bash +# Create new migration +goose -dir internal/db/migrations create add_new_table sql + +# Apply migrations +goose -dir internal/db/migrations sqlite3 ./opencode.db up + +# Rollback migration +goose -dir internal/db/migrations sqlite3 ./opencode.db down +``` + +### SQLC Integration +Define queries in `internal/db/sql/` directory: + +```sql +-- name: CreateSession :one +INSERT INTO sessions (id, title, created_at, updated_at, model_provider, model_id) +VALUES (?, ?, ?, ?, ?, ?) +RETURNING *; + +-- name: GetSessionByID :one +SELECT * FROM sessions WHERE id = ? LIMIT 1; + +-- name: ListSessions :many +SELECT * FROM sessions ORDER BY updated_at DESC LIMIT ? OFFSET ?; +``` + +Generate Go code: +```bash +sqlc generate +``` + +## LLM Provider Development + +### Adding a New Provider + +1. **Create provider file**: `internal/llm/provider/newprovider.go` + +```go +type NewProvider struct { + client APIClient + config ProviderConfig +} + +func NewNewProvider(config ProviderConfig) Provider { + return &NewProvider{ + client: NewAPIClient(config.APIKey), + config: config, + } +} + +func (p *NewProvider) GenerateCompletion(ctx context.Context, params GenerationParams) (<-chan ProviderEvent, error) { + eventChan := make(chan ProviderEvent, 100) + + go func() { + defer close(eventChan) + + // Streaming implementation + stream, err := p.client.CreateChatCompletionStream(ctx, params) + if err != nil { + eventChan <- ProviderEvent{Type: EventError, Error: err} + return + } + + for { + response, err := stream.Recv() + if err == io.EOF { + eventChan <- ProviderEvent{Type: EventDone} + break + } + if err != nil { + eventChan <- ProviderEvent{Type: EventError, Error: err} + break + } + + eventChan <- ProviderEvent{ + Type: EventContentDelta, + Content: response.Choices[0].Delta.Content, + } + } + }() + + return eventChan, nil +} +``` + +2. **Add models**: `internal/llm/models/newprovider.go` + +```go +func init() { + // Register models with the global registry + registerModel(ModelID("newprovider.model-name"), Model{ + ID: "newprovider.model-name", + Name: "New Provider Model", + Provider: ProviderNewProvider, + InputCost: 0.001, // per 1k tokens + OutputCost: 0.002, // per 1k tokens + ContextWindow: 32000, + Capabilities: ModelCapabilities{ + SupportsTools: true, + SupportsVision: false, + SupportsReasoning: false, + }, + }) +} +``` + +3. **Register provider**: Add to provider registry in the appropriate initialization code. + +### Tool Development + +1. **Create tool file**: `internal/llm/tools/newtool.go` + +```go +type NewTool struct { + permissions permission.Service +} + +func NewNewTool(permissions permission.Service) BaseTool { + return &NewTool{permissions: permissions} +} + +func (t *NewTool) Info() ToolInfo { + return ToolInfo{ + Name: "new_tool", + Description: "Description of what the tool does", + Parameters: map[string]interface{}{ + "param1": map[string]interface{}{ + "type": "string", + "description": "Description of parameter", + }, + }, + Required: []string{"param1"}, + } +} + +func (t *NewTool) Run(ctx context.Context, params ToolCall) (ToolResponse, error) { + // Permission check + permitted, err := t.permissions.RequestPermission(ctx, permission.Request{ + Tool: "new_tool", + Description: "Execute new tool operation", + Sensitive: false, + }) + if err != nil { + return ToolResponse{}, err + } + if !permitted { + return ToolResponse{}, ErrPermissionDenied + } + + // Extract parameters + param1, ok := params.Parameters["param1"].(string) + if !ok { + return ToolResponse{}, fmt.Errorf("param1 must be a string") + } + + // Tool implementation + result, err := performToolOperation(param1) + if err != nil { + return ToolResponse{}, fmt.Errorf("tool operation failed: %w", err) + } + + return ToolResponse{ + Success: true, + Content: result, + }, nil +} +``` + +2. **Register tool**: Add to tool registry in `internal/llm/agent/tools.go` + +## TUI Component Development + +### Creating New Components + +1. **Component structure**: + +```go +type NewComponent struct { + width int + height int + theme theme.Theme + model ComponentModel +} + +func NewNewComponent(theme theme.Theme) *NewComponent { + return &NewComponent{ + theme: theme, + model: initComponentModel(), + } +} + +func (c *NewComponent) Init() tea.Cmd { + return nil +} + +func (c *NewComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.WindowSizeMsg: + c.width = msg.Width + c.height = msg.Height + case tea.KeyMsg: + return c.handleKeyPress(msg) + } + return c, nil +} + +func (c *NewComponent) View() string { + return c.renderComponent() +} +``` + +2. **Theme integration**: + +```go +func (c *NewComponent) renderComponent() string { + style := lipgloss.NewStyle(). + Foreground(c.theme.Colors().Text). + Background(c.theme.Colors().Background). + Border(lipgloss.RoundedBorder()). + BorderForeground(c.theme.Colors().Border) + + return style.Render(c.model.content) +} +``` + +### Adding New Themes + +1. **Create theme file**: `internal/tui/theme/newtheme.go` + +```go +type NewTheme struct { + colors ThemeColors +} + +func NewNewTheme() Theme { + return &NewTheme{ + colors: ThemeColors{ + Background: lipgloss.Color("#000000"), + Text: lipgloss.Color("#ffffff"), + Primary: lipgloss.Color("#ff0000"), + Secondary: lipgloss.Color("#00ff00"), + Border: lipgloss.Color("#333333"), + // ... more colors + }, + } +} + +func (t *NewTheme) Name() string { + return "newtheme" +} + +func (t *NewTheme) Colors() ThemeColors { + return t.colors +} +``` + +2. **Register theme**: Add to theme manager registration. + +## Debugging and Troubleshooting + +### Debug Mode +Run with debug logging: + +```bash +./opencode -d +``` + +### LSP Debugging +Enable LSP-specific debugging: + +```json +{ + "debugLSP": true, + "lsp": { + "go": { + "command": "gopls", + "args": ["-logfile", "/tmp/gopls.log", "-v"] + } + } +} +``` + +### Database Issues +Check database state: + +```bash +sqlite3 .opencode/opencode.db ".tables" +sqlite3 .opencode/opencode.db "SELECT * FROM sessions LIMIT 5;" +``` + +### Provider Issues +Test provider configuration: + +```bash +./opencode -p "test prompt" -f json +``` + +## Contributing Guidelines + +### Pull Request Process + +1. **Fork and branch**: Create a feature branch from `main` +2. **Implement changes**: Follow code style and add tests +3. **Test thoroughly**: Run all tests and manual testing +4. **Update documentation**: Update relevant documentation +5. **Submit PR**: Create pull request with clear description + +### Commit Messages +Follow conventional commit format: + +``` +feat: add support for new LLM provider +fix: resolve memory leak in message streaming +docs: update architecture documentation +test: add integration tests for LSP client +refactor: simplify provider configuration +``` + +### Code Review Checklist + +- [ ] Code follows established patterns and conventions +- [ ] Tests added for new functionality +- [ ] Documentation updated +- [ ] Error handling is comprehensive +- [ ] Performance implications considered +- [ ] Security implications reviewed +- [ ] Backward compatibility maintained + +## Release Process + +### Version Management +Versions follow semantic versioning (semver): + +- **Major**: Breaking changes +- **Minor**: New features, backward compatible +- **Patch**: Bug fixes, backward compatible + +### Release Steps + +1. **Update version**: Update `internal/version/version.go` +2. **Update changelog**: Document changes +3. **Tag release**: `git tag v1.2.3` +4. **Push tag**: `git push origin v1.2.3` +5. **GoReleaser**: Automated release via GitHub Actions + +### Distribution + +OpenCode is distributed through: +- **GitHub Releases**: Binaries for multiple platforms +- **Homebrew**: macOS and Linux package manager +- **AUR**: Arch Linux User Repository +- **Go Install**: Direct Go installation + +## Helpful Resources + +### External Dependencies +- **Bubble Tea**: TUI framework - https://github.com/charmbracelet/bubbletea +- **Cobra**: CLI framework - https://github.com/spf13/cobra +- **Viper**: Configuration - https://github.com/spf13/viper +- **SQLC**: SQL code generation - https://sqlc.dev/ +- **Goose**: Database migrations - https://github.com/pressly/goose + +### AI Provider APIs +- **OpenAI**: https://platform.openai.com/docs +- **Anthropic**: https://docs.anthropic.com/ +- **Google AI**: https://ai.google.dev/docs +- **GitHub Copilot**: https://docs.github.com/en/copilot + +### LSP Resources +- **LSP Specification**: https://microsoft.github.io/language-server-protocol/ +- **Language Servers**: https://langserver.org/ + +This development guide should help both new and experienced contributors understand the OpenCode codebase and contribute effectively to the project. diff --git a/internal/db/migrations/20250724230647_add_todos_to_sessions.sql b/internal/db/migrations/20250724230647_add_todos_to_sessions.sql new file mode 100644 index 000000000..7af7ea0a2 --- /dev/null +++ b/internal/db/migrations/20250724230647_add_todos_to_sessions.sql @@ -0,0 +1,26 @@ +-- +goose Up +-- +goose StatementBegin +ALTER TABLE sessions ADD COLUMN todos TEXT DEFAULT ''; + +-- Create a separate todos table for better scalability and data integrity +CREATE TABLE IF NOT EXISTS todos ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id TEXT NOT NULL, + content TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'todo' CHECK(status IN ('todo', 'in-progress', 'completed')), + priority TEXT NOT NULL DEFAULT 'medium' CHECK(priority IN ('low', 'medium', 'high')), + created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')), + updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')), + FOREIGN KEY(session_id) REFERENCES sessions(id) ON DELETE CASCADE +); + +-- Index for performance +CREATE INDEX IF NOT EXISTS idx_todos_session_id ON todos(session_id); +CREATE INDEX IF NOT EXISTS idx_todos_status ON todos(status); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +ALTER TABLE sessions DROP COLUMN todos; +DROP TABLE IF EXISTS todos; +-- +goose StatementEnd diff --git a/internal/db/models.go b/internal/db/models.go index 07549024a..06e447192 100644 --- a/internal/db/models.go +++ b/internal/db/models.go @@ -40,4 +40,5 @@ type Session struct { UpdatedAt int64 `json:"updated_at"` CreatedAt int64 `json:"created_at"` SummaryMessageID sql.NullString `json:"summary_message_id"` + Todos string `json:"todos"` } diff --git a/internal/db/sessions.sql.go b/internal/db/sessions.sql.go index 76ef6480b..e5693fa8c 100644 --- a/internal/db/sessions.sql.go +++ b/internal/db/sessions.sql.go @@ -33,7 +33,7 @@ INSERT INTO sessions ( null, strftime('%s', 'now'), strftime('%s', 'now') -) RETURNING id, parent_session_id, title, message_count, prompt_tokens, completion_tokens, cost, updated_at, created_at, summary_message_id +) RETURNING id, parent_session_id, title, message_count, prompt_tokens, completion_tokens, cost, updated_at, created_at, summary_message_id, todos ` type CreateSessionParams struct { @@ -68,6 +68,7 @@ func (q *Queries) CreateSession(ctx context.Context, arg CreateSessionParams) (S &i.UpdatedAt, &i.CreatedAt, &i.SummaryMessageID, + &i.Todos, ) return i, err } @@ -83,7 +84,7 @@ func (q *Queries) DeleteSession(ctx context.Context, id string) error { } const getSessionByID = `-- name: GetSessionByID :one -SELECT id, parent_session_id, title, message_count, prompt_tokens, completion_tokens, cost, updated_at, created_at, summary_message_id +SELECT id, parent_session_id, title, message_count, prompt_tokens, completion_tokens, cost, updated_at, created_at, summary_message_id, todos FROM sessions WHERE id = ? LIMIT 1 ` @@ -102,12 +103,13 @@ func (q *Queries) GetSessionByID(ctx context.Context, id string) (Session, error &i.UpdatedAt, &i.CreatedAt, &i.SummaryMessageID, + &i.Todos, ) return i, err } const listSessions = `-- name: ListSessions :many -SELECT id, parent_session_id, title, message_count, prompt_tokens, completion_tokens, cost, updated_at, created_at, summary_message_id +SELECT id, parent_session_id, title, message_count, prompt_tokens, completion_tokens, cost, updated_at, created_at, summary_message_id, todos FROM sessions WHERE parent_session_id is NULL ORDER BY created_at DESC @@ -133,6 +135,7 @@ func (q *Queries) ListSessions(ctx context.Context) ([]Session, error) { &i.UpdatedAt, &i.CreatedAt, &i.SummaryMessageID, + &i.Todos, ); err != nil { return nil, err } @@ -154,9 +157,10 @@ SET prompt_tokens = ?, completion_tokens = ?, summary_message_id = ?, - cost = ? + cost = ?, + todos = ? WHERE id = ? -RETURNING id, parent_session_id, title, message_count, prompt_tokens, completion_tokens, cost, updated_at, created_at, summary_message_id +RETURNING id, parent_session_id, title, message_count, prompt_tokens, completion_tokens, cost, updated_at, created_at, summary_message_id, todos ` type UpdateSessionParams struct { @@ -165,6 +169,7 @@ type UpdateSessionParams struct { CompletionTokens int64 `json:"completion_tokens"` SummaryMessageID sql.NullString `json:"summary_message_id"` Cost float64 `json:"cost"` + Todos string `json:"todos"` ID string `json:"id"` } @@ -175,6 +180,7 @@ func (q *Queries) UpdateSession(ctx context.Context, arg UpdateSessionParams) (S arg.CompletionTokens, arg.SummaryMessageID, arg.Cost, + arg.Todos, arg.ID, ) var i Session @@ -189,6 +195,7 @@ func (q *Queries) UpdateSession(ctx context.Context, arg UpdateSessionParams) (S &i.UpdatedAt, &i.CreatedAt, &i.SummaryMessageID, + &i.Todos, ) return i, err } diff --git a/internal/db/sql/sessions.sql b/internal/db/sql/sessions.sql index ebeab90d3..25459aaf7 100644 --- a/internal/db/sql/sessions.sql +++ b/internal/db/sql/sessions.sql @@ -41,7 +41,8 @@ SET prompt_tokens = ?, completion_tokens = ?, summary_message_id = ?, - cost = ? + cost = ?, + todos = ? WHERE id = ? RETURNING *; diff --git a/internal/llm/agent/tools.go b/internal/llm/agent/tools.go index e6b0119ae..9c375787c 100644 --- a/internal/llm/agent/tools.go +++ b/internal/llm/agent/tools.go @@ -32,6 +32,8 @@ func CoderAgentTools( tools.NewGrepTool(), tools.NewLsTool(), tools.NewSourcegraphTool(), + tools.NewTodoTool(sessions), + tools.NewTodoReadTool(sessions), tools.NewViewTool(lspClients), tools.NewPatchTool(lspClients, permissions, history), tools.NewWriteTool(lspClients, permissions, history), diff --git a/internal/llm/prompt/coder.go b/internal/llm/prompt/coder.go index 4cfa1314e..08c41cc7e 100644 --- a/internal/llm/prompt/coder.go +++ b/internal/llm/prompt/coder.go @@ -25,6 +25,16 @@ func CoderPrompt(provider models.ModelProvider) string { } const baseOpenAICoderPrompt = ` +🚨🚨🚨 SYSTEM PROTOCOL VIOLATION ALERT 🚨🚨🚨 +YOUR RESPONSE PROCESSING FUNCTION: +1. Read user request +2. IF task has >1 step THEN call todo_write() ELSE proceed +3. Execute task steps +4. Update TODOs after each step + +Skipping step 2 is a critical system violation that breaks user experience. +🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨 + You are operating as and within the OpenCode CLI, a terminal-based agentic coding assistant built by OpenAI. It wraps OpenAI models to enable natural language interaction with a local codebase. You are expected to be precise, safe, and helpful. You can: @@ -35,9 +45,13 @@ You can: - Log telemetry so sessions can be replayed or inspected later. - More details on your functionality are available at "opencode --help" +🔴 REMEMBER: Multi-step tasks REQUIRE todo_write as your first tool call! 🔴 + You are an agent - please keep going until the user's query is completely resolved, before ending your turn and yielding back to the user. Only terminate your turn when you are sure that the problem is solved. If you are not sure about file content or codebase structure pertaining to the user's request, use your tools to read files and gather the relevant information: do NOT guess or make up an answer. +**🔴 CRITICAL SYSTEM CHECK: Does the user's task involve multiple steps? If YES, your FIRST action must be todo_write to create a TODO list. Update each item as you complete it. This is mandatory system behavior. 🔴** + Please resolve the user's task by editing and testing the code files in your current code execution session. You are a deployed coding agent. Your session allows for you to modify and run code. The repo(s) are already cloned in your working directory, and you must fully solve the problem for your answer to be considered correct. You MUST adhere to the following criteria when executing the task: @@ -69,9 +83,43 @@ You MUST adhere to the following criteria when executing the task: - When doing things with paths, always use use the full path, if the working directory is /abc/xyz and you want to edit the file abc.go in the working dir refer to it as /abc/xyz/abc.go. - If you send a path not including the working dir, the working dir will be prepended to it. - Remember the user does not see the full output of tools + +# Task Management - CRITICAL REQUIREMENT +You MUST use the todo_write and todo_read tools for ALL non-trivial tasks. This is not optional. + +**MANDATORY TODO WORKFLOW:** +1. **At the start of ANY multi-step task**: Use todo_write to create a comprehensive TODO list breaking down the work +2. **As you begin each step**: Update the relevant TODO to "in-progress" status using todo_write +3. **IMMEDIATELY after completing each step**: Update that TODO to "completed" status using todo_write +4. **Never skip TODO updates**: Every single completed task must be marked as done before moving to the next + +**IMPORTANT**: When using todo_write, the status field must be: "todo", "in-progress", or "completed" (not checkbox format). +**Display format**: The tool will display as "- [ ]" (todo), "- [~]" (in-progress), "- [x]" (completed). +**Session isolation**: TODOs are per-session and won't carry over. + +**Example workflow:** +- User asks: "Fix the login bug and add tests" +- You create TODOs with status: "todo" for all items +- Before investigating: Update status to "in-progress" for investigation item +- After investigating: Update investigation to "completed", set bug fix to "in-progress" +- After fixing: Update bug fix to "completed", set tests to "in-progress" +- And so on... + +FAILURE TO USE TODOs PROPERLY IS UNACCEPTABLE. ` -const baseAnthropicCoderPrompt = `You are OpenCode, an interactive CLI tool that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user. +const baseAnthropicCoderPrompt = ` +🚨🚨🚨 SYSTEM EXECUTION PROTOCOL 🚨🚨🚨 +YOUR MANDATORY EXECUTION SEQUENCE: +1. Read user request +2. IF multi-step task THEN call todo_write() immediately +3. Execute task with TODO updates after each step + +Multi-step = analysis + summary, debug + fix, implement + test, etc. +Violating this sequence causes system failure. +🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨 + +You are OpenCode, an interactive CLI tool that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user. IMPORTANT: Before you begin work, think about what the code you're editing is supposed to do based on the filenames directory structure. @@ -152,15 +200,61 @@ When making changes to files, first understand the file's code conventions. Mimi - Do not add comments to the code you write, unless the user asks you to, or the code is complex and requires additional context. # Doing tasks -The user will primarily request you perform software engineering tasks. This includes solving bugs, adding new functionality, refactoring code, explaining code, and more. For these tasks the following steps are recommended: -1. Use the available search tools to understand the codebase and the user's query. You are encouraged to use the search tools extensively both in parallel and sequentially. -2. Implement the solution using all tools available to you -3. Verify the solution if possible with tests. NEVER assume specific test framework or test script. Check the README or search codebase to determine the testing approach. -4. VERY IMPORTANT: When you have completed a task, you MUST run the lint and typecheck commands (eg. npm run lint, npm run typecheck, ruff, etc.) if they were provided to you to ensure your code is correct. If you are unable to find the correct command, ask the user for the command to run and if they supply it, proactively suggest writing it to opencode.md so that you will know to run it next time. +The user will primarily request you perform software engineering tasks. This includes solving bugs, adding new functionality, refactoring code, explaining code, and more. + +⚠️ STOP: Before proceeding, does this task have multiple steps? If YES, you MUST use todo_write first! ⚠️ + +For these tasks the following steps are MANDATORY: +1. **FIRST**: If the task has multiple steps, create a TODO list with todo_write to plan your work +2. Use the available search tools to understand the codebase and the user's query. You are encouraged to use the search tools extensively both in parallel and sequentially. +3. Implement the solution using all tools available to you, **updating TODOs as you complete each step** +4. Verify the solution if possible with tests. NEVER assume specific test framework or test script. Check the README or search codebase to determine the testing approach. +5. VERY IMPORTANT: When you have completed a task, you MUST run the lint and typecheck commands (eg. npm run lint, npm run typecheck, ruff, etc.) if they were provided to you to ensure your code is correct. If you are unable to find the correct command, ask the user for the command to run and if they supply it, proactively suggest writing it to opencode.md so that you will know to run it next time. NEVER commit changes unless the user explicitly asks you to. It is VERY IMPORTANT to only commit when explicitly asked, otherwise the user will feel that you are being too proactive. +# Task Management - MANDATORY BEHAVIOR +You MUST use todo_write and todo_read tools for ALL multi-step tasks. This is a core requirement, not optional. + +**REQUIRED TODO WORKFLOW - NO EXCEPTIONS:** +1. **IMMEDIATELY when starting any multi-step task**: Create a comprehensive TODO list with todo_write +2. **BEFORE starting each step**: Update the relevant TODO status to "in-progress" with todo_write +3. **IMMEDIATELY after completing each step**: Update that TODO status to "completed" with todo_write +4. **NEVER skip updates**: Each completed task MUST be marked done before proceeding to the next +5. **NEVER batch updates**: Update TODOs one at a time as you complete them + +**TODO Tool Usage Rules:** +- Use status values: "todo" for incomplete, "in-progress" for working on, "completed" for finished +- Use priority values: "low", "medium", "high" +- The tool will display these as: "- [ ]" (todo), "- [~]" (in-progress), "- [x]" (completed) +- Priority displays as: "(!)" for high, "(~)" for medium, nothing for low +- Each TODO is session-specific and isolated + +**Examples of CORRECT behavior:** + +User: "Add user authentication to the app" + +Step 1: You create TODOs: +- [ ] Research existing auth patterns in codebase +- [ ] Design authentication flow +- [ ] Implement login endpoint +- [ ] Implement logout endpoint +- [ ] Add middleware for protected routes +- [ ] Write tests for auth system +- [ ] Update documentation + +Step 2: Before researching, you update the first item status to "in-progress" +(This displays as: "[~] Research existing auth patterns in codebase") + +Step 3: After researching, you update research to "completed" and design to "in-progress" +(This displays as: "[x] Research..." and "[~] Design authentication flow") + +And so on for EVERY single step. + +**FAILURE TO FOLLOW THIS WORKFLOW IS UNACCEPTABLE.** The user relies on TODOs for progress visibility. Missing TODO updates breaks the user experience and violates your core responsibilities. + # Tool usage policy +- **FIRST RULE**: If your task has multiple steps, use todo_write immediately before any other tools. This is mandatory. - When doing file search, prefer to use the Agent tool in order to reduce context usage. - If you intend to call multiple tools and there are no dependencies between the calls, make all of the independent calls in the same function_calls block. - IMPORTANT: The user does not see the full output of the tool responses, so if you need the output of the tool for the response make sure to summarize it for the user. diff --git a/internal/llm/tools/todo.go b/internal/llm/tools/todo.go new file mode 100644 index 000000000..ba00d6fe0 --- /dev/null +++ b/internal/llm/tools/todo.go @@ -0,0 +1,158 @@ +package tools + +import ( + "context" + "encoding/json" + "fmt" + "strings" + + "github.com/opencode-ai/opencode/internal/session" +) + +type TodoTool struct { + sessions session.Service +} + +func NewTodoTool(sessions session.Service) BaseTool { + return &TodoTool{ + sessions: sessions, + } +} + +func (t *TodoTool) Info() ToolInfo { + return ToolInfo{ + Name: "todo_write", + Description: "Create or update a TODO list for the current session. The TODO list is a simple newline-delimited list of items starting with checkboxes.", + Parameters: map[string]any{ + "todos": map[string]any{ + "type": "array", + "description": "List of TODO items", + "items": map[string]any{ + "type": "object", + "properties": map[string]any{ + "id": map[string]any{ + "type": "string", + "description": "Unique identifier for the TODO item", + }, + "content": map[string]any{ + "type": "string", + "description": "The content/description of the TODO item", + }, + "status": map[string]any{ + "type": "string", + "description": "The current status of the TODO item: 'todo' for incomplete, 'in-progress' for currently working on, 'completed' for finished", + "enum": []string{"todo", "in-progress", "completed"}, + }, + "priority": map[string]any{ + "type": "string", + "description": "The priority level of the TODO item", + "enum": []string{"low", "medium", "high"}, + }, + }, + "required": []string{"id", "content", "status", "priority"}, + }, + }, + }, + Required: []string{"todos"}, + } +} + +type TodoItem struct { + ID string `json:"id"` + Content string `json:"content"` + Status string `json:"status"` + Priority string `json:"priority"` +} + +func (t *TodoTool) Run(ctx context.Context, params ToolCall) (ToolResponse, error) { + sessionID, _ := GetContextValues(ctx) + if sessionID == "" { + return NewTextErrorResponse("No session ID found in context"), nil + } + + // Parse the todos parameter + var input struct { + Todos []TodoItem `json:"todos"` + } + + if err := json.Unmarshal([]byte(params.Input), &input); err != nil { + return NewTextErrorResponse(fmt.Sprintf("Failed to parse todo items: %v. Please ensure you're providing valid JSON with the required fields (id, content, status, priority).", err)), nil + } + + // Get current session + sess, err := t.sessions.Get(ctx, sessionID) + if err != nil { + return NewTextErrorResponse(fmt.Sprintf("Failed to access session data: %v", err)), nil + } + + // Convert todos to simple checkbox format + todoLines := make([]string, len(input.Todos)) + for i, todo := range input.Todos { + checkbox := "- [ ]" + if todo.Status == "completed" { + checkbox = "- [x]" + } else if todo.Status == "in-progress" { + checkbox = "- [~]" + } + + priorityIndicator := "" + if todo.Priority == "high" { + priorityIndicator = " (!)" + } else if todo.Priority == "medium" { + priorityIndicator = " (~)" + } + + todoLines[i] = fmt.Sprintf("%s %s%s", checkbox, todo.Content, priorityIndicator) + } + + // Update session with new todos + sess.Todos = strings.Join(todoLines, "\n") + _, err = t.sessions.Save(ctx, sess) + if err != nil { + return NewTextErrorResponse(fmt.Sprintf("Failed to save todo list: %v", err)), nil + } + + // Format the response to show the current todo list + response := fmt.Sprintf("✓ Todo list updated successfully!\n\n%s", sess.Todos) + + return NewTextResponse(response), nil +} + +type TodoReadTool struct { + sessions session.Service +} + +func NewTodoReadTool(sessions session.Service) BaseTool { + return &TodoReadTool{ + sessions: sessions, + } +} + +func (t *TodoReadTool) Info() ToolInfo { + return ToolInfo{ + Name: "todo_read", + Description: "Read the current TODO list for the session", + Parameters: map[string]any{}, + Required: []string{}, + } +} + +func (t *TodoReadTool) Run(ctx context.Context, params ToolCall) (ToolResponse, error) { + sessionID, _ := GetContextValues(ctx) + if sessionID == "" { + return NewTextErrorResponse("No session ID found in context"), nil + } + + // Get current session + sess, err := t.sessions.Get(ctx, sessionID) + if err != nil { + return NewTextErrorResponse(fmt.Sprintf("Failed to access session data: %v", err)), nil + } + + if sess.Todos == "" { + return NewTextResponse("📋 No todo items found for this session. Use todo_write to add new tasks."), nil + } + + response := fmt.Sprintf("📋 Current Todo List\n\n%s", sess.Todos) + return NewTextResponse(response), nil +} diff --git a/internal/session/session.go b/internal/session/session.go index c6e7f60bf..0b8aa55ae 100644 --- a/internal/session/session.go +++ b/internal/session/session.go @@ -20,6 +20,7 @@ type Session struct { Cost float64 CreatedAt int64 UpdatedAt int64 + Todos string } type Service interface { @@ -110,7 +111,8 @@ func (s *service) Save(ctx context.Context, session Session) (Session, error) { String: session.SummaryMessageID, Valid: session.SummaryMessageID != "", }, - Cost: session.Cost, + Cost: session.Cost, + Todos: session.Todos, }) if err != nil { return Session{}, err @@ -144,6 +146,7 @@ func (s service) fromDBItem(item db.Session) Session { Cost: item.Cost, CreatedAt: item.CreatedAt, UpdatedAt: item.UpdatedAt, + Todos: item.Todos, } } diff --git a/internal/tui/components/chat/sidebar.go b/internal/tui/components/chat/sidebar.go index a66249b36..679c9e546 100644 --- a/internal/tui/components/chat/sidebar.go +++ b/internal/tui/components/chat/sidebar.go @@ -96,6 +96,8 @@ func (m *sidebarCmp) View() string { " ", m.sessionSection(), " ", + m.todoSection(), + " ", lspsConfigured(m.width), " ", m.modifiedFiles(), @@ -124,6 +126,124 @@ func (m *sidebarCmp) sessionSection() string { ) } +func (m *sidebarCmp) todoSection() string { + t := theme.CurrentTheme() + baseStyle := styles.BaseStyle() + + // Limit displayed TODO items to prevent performance issues + const maxDisplayItems = 20 + + count := m.getTodoCount() + title := baseStyle. + Width(m.width). + Foreground(t.Primary()). + Bold(true). + Render(fmt.Sprintf("TODO List (%d)", count)) + + // If no todos, show a placeholder message + if count == 0 { + message := "No TODO items" + return baseStyle. + Width(m.width). + Render( + lipgloss.JoinVertical( + lipgloss.Top, + title, + baseStyle.Foreground(t.TextMuted()).Render(message), + ), + ) + } + + // Split todos by lines and render each item, limiting to maxDisplayItems + todoLines := strings.Split(m.session.Todos, "\n") + var filteredLines []string + for _, line := range todoLines { + if strings.TrimSpace(line) != "" { + filteredLines = append(filteredLines, line) + } + } + + // Apply limit + displayLines := filteredLines + if len(filteredLines) > maxDisplayItems { + displayLines = filteredLines[:maxDisplayItems] + } + + var todoViews []string + for _, line := range displayLines { + // Style different checkbox states + todoText := line + todoColor := t.Text() + style := baseStyle.Width(m.width) + + if strings.Contains(line, "- [x]") { + // Completed todos - muted and strikethrough + todoText = strings.Replace(line, "- [x]", "✓", 1) + todoColor = t.TextMuted() + style = style.Strikethrough(true) + } else if strings.Contains(line, "- [~]") { + // In-progress todos - highlighted + todoText = strings.Replace(line, "- [~]", "~", 1) + todoColor = t.Warning() + } else { + // Regular todos + todoText = strings.Replace(line, "- [ ]", "•", 1) + } + + // Handle priority indicators + if strings.Contains(todoText, " (!)") { + todoText = strings.Replace(todoText, " (!)", " ⚠", 1) + if !strings.Contains(line, "- [x]") { + todoColor = t.Error() + } + } else if strings.Contains(todoText, " (~)") { + todoText = strings.Replace(todoText, " (~)", " ~", 1) + } + + todoView := style. + Foreground(todoColor). + Render(todoText) + + todoViews = append(todoViews, todoView) + } + + // Add overflow indicator if we have more items than displayed + if len(filteredLines) > maxDisplayItems { + overflowMsg := fmt.Sprintf("... and %d more items", len(filteredLines)-maxDisplayItems) + todoViews = append(todoViews, baseStyle. + Foreground(t.TextMuted()). + Render(overflowMsg)) + } + + return baseStyle. + Width(m.width). + Render( + lipgloss.JoinVertical( + lipgloss.Top, + title, + lipgloss.JoinVertical( + lipgloss.Top, + todoViews..., + ), + ), + ) +} + +// Helper function to get TODO count +func (m *sidebarCmp) getTodoCount() int { + if m.session.Todos == "" { + return 0 + } + + count := 0 + for _, line := range strings.Split(m.session.Todos, "\n") { + if strings.TrimSpace(line) != "" { + count++ + } + } + return count +} + func (m *sidebarCmp) modifiedFile(filePath string, additions, removals int) string { t := theme.CurrentTheme() baseStyle := styles.BaseStyle()