``` ├── .github/ ├── workflows/ ├── build.yml (100 tokens) ├── release.yml (200 tokens) ├── .gitignore (100 tokens) ├── .goreleaser.yml (400 tokens) ├── .opencode.json ├── LICENSE (omitted) ├── README.md (4.5k tokens) ├── cmd/ ├── root.go (1700 tokens) ├── schema/ ├── README.md (300 tokens) ├── main.go (1800 tokens) ├── go.mod (1200 tokens) ├── go.sum (6.6k tokens) ├── install (1000 tokens) ├── internal/ ├── app/ ├── app.go (1000 tokens) ├── lsp.go (900 tokens) ├── completions/ ├── files-folders.go (1000 tokens) ├── config/ ├── config.go (5.2k tokens) ├── init.go (300 tokens) ├── db/ ├── connect.go (300 tokens) ├── db.go (2.2k tokens) ├── embed.go ├── files.sql.go (1300 tokens) ├── messages.sql.go (700 tokens) ├── migrations/ ├── 20250424200609_initial.sql (600 tokens) ├── 20250515105448_add_summary_message_id.sql ├── models.go (300 tokens) ├── querier.go (300 tokens) ├── sessions.sql.go (900 tokens) ├── sql/ ├── files.sql (300 tokens) ├── messages.sql (100 tokens) ├── sessions.sql (200 tokens) ├── diff/ ├── diff.go (5.2k tokens) ├── patch.go (3.6k tokens) ├── fileutil/ ├── fileutil.go (700 tokens) ├── format/ ├── format.go (500 tokens) ├── spinner.go (400 tokens) ├── history/ ├── file.go (1300 tokens) ├── llm/ ├── agent/ ├── agent-tool.go (900 tokens) ├── agent.go (4.4k tokens) ├── mcp-tools.go (1100 tokens) ├── tools.go (300 tokens) ├── models/ ├── anthropic.go (700 tokens) ├── azure.go (1400 tokens) ├── gemini.go (400 tokens) ├── groq.go (500 tokens) ├── local.go (900 tokens) ├── models.go (500 tokens) ├── openai.go (1000 tokens) ├── openrouter.go (2.6k tokens) ├── vertexai.go (300 tokens) ├── xai.go (300 tokens) ├── prompt/ ├── coder.go (2.9k tokens) ├── prompt.go (600 tokens) ├── prompt_test.go (300 tokens) ├── summarizer.go (100 tokens) ├── task.go (200 tokens) ├── title.go (100 tokens) ├── provider/ ├── anthropic.go (2.7k tokens) ├── azure.go (200 tokens) ├── bedrock.go (500 tokens) ├── gemini.go (2.9k tokens) ├── openai.go (2.4k tokens) ├── provider.go (1300 tokens) ├── vertexai.go (100 tokens) ├── tools/ ├── bash.go (2.9k tokens) ├── diagnostics.go (1600 tokens) ├── edit.go (3.1k tokens) ├── fetch.go (1200 tokens) ├── file.go (200 tokens) ├── glob.go (1000 tokens) ├── grep.go (1900 tokens) ├── ls.go (1500 tokens) ├── ls_test.go (2.3k tokens) ├── patch.go (2.2k tokens) ├── shell/ ├── shell.go (1300 tokens) ├── sourcegraph.go (2.4k tokens) ├── tools.go (400 tokens) ├── view.go (1600 tokens) ├── write.go (1400 tokens) ├── logging/ ├── logger.go (400 tokens) ├── message.go (100 tokens) ├── writer.go (400 tokens) ├── lsp/ ├── client.go (4.4k tokens) ├── handlers.go (700 tokens) ├── language.go (600 tokens) ├── methods.go (6.9k tokens) ├── protocol.go (200 tokens) ├── protocol/ ├── LICENSE (300 tokens) ├── interface.go (700 tokens) ├── pattern_interfaces.go (300 tokens) ├── tables.go (200 tokens) ├── tsdocument-changes.go (400 tokens) ├── tsjson.go (17.7k tokens) ├── tsprotocol.go (55.7k tokens) ├── uri.go (1400 tokens) ├── transport.go (1300 tokens) ├── util/ ├── edit.go (1400 tokens) ├── watcher/ ├── watcher.go (5.7k tokens) ├── message/ ├── attachment.go ├── content.go (1400 tokens) ├── message.go (1300 tokens) ├── permission/ ├── permission.go (600 tokens) ├── pubsub/ ├── broker.go (400 tokens) ├── events.go (100 tokens) ├── session/ ├── session.go (800 tokens) ├── tui/ ├── components/ ├── chat/ ├── chat.go (500 tokens) ├── editor.go (1700 tokens) ├── list.go (2.3k tokens) ├── message.go (3.8k tokens) ├── sidebar.go (1900 tokens) ├── core/ ├── status.go (1600 tokens) ├── dialog/ ├── arguments.go (1400 tokens) ├── commands.go (800 tokens) ├── complete.go (1200 tokens) ├── custom_commands.go (1100 tokens) ├── custom_commands_test.go (500 tokens) ├── filepicker.go (2.5k tokens) ├── help.go (900 tokens) ├── init.go (1000 tokens) ├── models.go (1800 tokens) ├── permission.go (3k tokens) ├── quit.go (600 tokens) ├── session.go (1200 tokens) ├── theme.go (1000 tokens) ├── logs/ ├── details.go (700 tokens) ├── table.go (600 tokens) ├── util/ ├── simple-list.go (800 tokens) ├── image/ ├── images.go (300 tokens) ├── layout/ ├── container.go (900 tokens) ├── layout.go (100 tokens) ├── overlay.go (800 tokens) ├── split.go (1200 tokens) ├── page/ ├── chat.go (1200 tokens) ├── logs.go (400 tokens) ├── page.go ├── styles/ ├── background.go (500 tokens) ├── icons.go (100 tokens) ├── markdown.go (1800 tokens) ├── styles.go (900 tokens) ├── theme/ ├── catppuccin.go (1400 tokens) ├── dracula.go (1300 tokens) ├── flexoki.go (1500 tokens) ├── gruvbox.go (1700 tokens) ├── manager.go (600 tokens) ├── monokai.go (1300 tokens) ├── onedark.go (1300 tokens) ├── opencode.go (1400 tokens) ├── theme.go (2000 tokens) ├── theme_test.go (400 tokens) ├── tokyonight.go (1300 tokens) ├── tron.go (1300 tokens) ├── tui.go (5.1k tokens) ├── util/ ├── util.go (200 tokens) ├── version/ ├── version.go (100 tokens) ├── main.go (100 tokens) ├── opencode-schema.json (2.3k tokens) ├── scripts/ ├── check_hidden_chars.sh (300 tokens) ├── release (200 tokens) ├── snapshot ├── sqlc.yaml (100 tokens) ``` ## /.github/workflows/build.yml ```yml path="/.github/workflows/build.yml" name: build on: workflow_dispatch: push: branches: - main concurrency: ${{ github.workflow }}-${{ github.ref }} permissions: contents: write packages: write jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 with: fetch-depth: 0 - run: git fetch --force --tags - uses: actions/setup-go@v5 with: go-version: ">=1.23.2" cache: true cache-dependency-path: go.sum - run: go mod download - uses: goreleaser/goreleaser-action@v6 with: distribution: goreleaser version: latest args: build --snapshot --clean ``` ## /.github/workflows/release.yml ```yml path="/.github/workflows/release.yml" name: release on: workflow_dispatch: push: tags: - "*" concurrency: ${{ github.workflow }}-${{ github.ref }} permissions: contents: write packages: write jobs: goreleaser: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 with: fetch-depth: 0 - run: git fetch --force --tags - uses: actions/setup-go@v5 with: go-version: ">=1.23.2" cache: true cache-dependency-path: go.sum - run: go mod download - uses: goreleaser/goreleaser-action@v6 with: distribution: goreleaser version: latest args: release --clean env: GITHUB_TOKEN: ${{ secrets.HOMEBREW_GITHUB_TOKEN }} AUR_KEY: ${{ secrets.AUR_KEY }} ``` ## /.gitignore ```gitignore path="/.gitignore" # Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib # Test binary, built with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # Dependency directories (remove the comment below to include it) # vendor/ # Go workspace file go.work # IDE specific files .idea/ .vscode/ *.swp *.swo # OS specific files .DS_Store .DS_Store? ._* .Spotlight-V100 .Trashes ehthumbs.db Thumbs.db *.log # Binary output directory /bin/ /dist/ # Local environment variables .env .env.local .opencode/ opencode ``` ## /.goreleaser.yml ```yml path="/.goreleaser.yml" version: 2 project_name: opencode before: hooks: builds: - env: - CGO_ENABLED=0 goos: - linux - darwin goarch: - amd64 - arm64 ldflags: - -s -w -X github.com/opencode-ai/opencode/internal/version.Version={{.Version}} main: ./main.go archives: - format: tar.gz name_template: >- opencode- {{- if eq .Os "darwin" }}mac- {{- else if eq .Os "windows" }}windows- {{- else if eq .Os "linux" }}linux-{{end}} {{- if eq .Arch "amd64" }}x86_64 {{- else if eq .Arch "#86" }}i386 {{- else }}{{ .Arch }}{{ end }} {{- if .Arm }}v{{ .Arm }}{{ end }} format_overrides: - goos: windows format: zip checksum: name_template: "checksums.txt" snapshot: name_template: "0.0.0-{{ .Timestamp }}" aurs: - name: opencode-ai homepage: "https://github.com/opencode-ai/opencode" description: "terminal based agent that can build anything" maintainers: - "kujtimiihoxha <kujtimii.h@gmail.com>" license: "MIT" private_key: "{{ .Env.AUR_KEY }}" git_url: "ssh://aur@aur.archlinux.org/opencode-ai-bin.git" provides: - opencode conflicts: - opencode package: |- install -Dm755 ./opencode "${pkgdir}/usr/bin/opencode" brews: - repository: owner: opencode-ai name: homebrew-tap nfpms: - maintainer: kujtimiihoxha description: terminal based agent that can build anything formats: - deb - rpm file_name_template: >- {{ .ProjectName }}- {{- if eq .Os "darwin" }}mac {{- else }}{{ .Os }}{{ end }}-{{ .Arch }} changelog: sort: asc filters: exclude: - "^docs:" - "^doc:" - "^test:" - "^ci:" - "^ignore:" - "^example:" - "^wip:" ``` ## /.opencode.json ```json path="/.opencode.json" { "$schema": "./opencode-schema.json", "lsp": { "gopls": { "command": "gopls" } } } ``` ## /README.md # ⌬ OpenCode <p align="center"><img src="https://github.com/user-attachments/assets/9ae61ef6-70e5-4876-bc45-5bcb4e52c714" width="800"></p> > **⚠️ Early Development Notice:** This project is in early development and is not yet ready for production use. Features may change, break, or be incomplete. Use at your own risk. A powerful terminal-based AI assistant for developers, providing intelligent coding assistance directly in your terminal. ## Overview OpenCode is a Go-based CLI application that brings AI assistance to your terminal. It provides a TUI (Terminal User Interface) for interacting with various AI models to help with coding tasks, debugging, and more. <p>For a quick video overview, check out <a href="https://www.youtube.com/watch?v=P8luPmEa1QI"><img width="25" src="https://upload.wikimedia.org/wikipedia/commons/0/09/YouTube_full-color_icon_%282017%29.svg"> OpenCode + Gemini 2.5 Pro: BYE Claude Code! I'm SWITCHING To the FASTEST AI Coder!</a></p> <a href="https://www.youtube.com/watch?v=P8luPmEa1QI"><img width="550" src="https://i3.ytimg.com/vi/P8luPmEa1QI/maxresdefault.jpg"></a><p> ## Features - **Interactive TUI**: Built with [Bubble Tea](https://github.com/charmbracelet/bubbletea) for a smooth terminal experience - **Multiple AI Providers**: Support for OpenAI, Anthropic Claude, Google Gemini, AWS Bedrock, Groq, Azure OpenAI, and OpenRouter - **Session Management**: Save and manage multiple conversation sessions - **Tool Integration**: AI can execute commands, search files, and modify code - **Vim-like Editor**: Integrated editor with text input capabilities - **Persistent Storage**: SQLite database for storing conversations and sessions - **LSP Integration**: Language Server Protocol support for code intelligence - **File Change Tracking**: Track and visualize file changes during sessions - **External Editor Support**: Open your preferred editor for composing messages - **Named Arguments for Custom Commands**: Create powerful custom commands with multiple named placeholders ## Installation ### Using the Install Script ```bash # Install the latest version curl -fsSL https://raw.githubusercontent.com/opencode-ai/opencode/refs/heads/main/install | bash # Install a specific version curl -fsSL https://raw.githubusercontent.com/opencode-ai/opencode/refs/heads/main/install | VERSION=0.1.0 bash ``` ### Using Homebrew (macOS and Linux) ```bash brew install opencode-ai/tap/opencode ``` ### Using AUR (Arch Linux) ```bash # Using yay yay -S opencode-ai-bin # Using paru paru -S opencode-ai-bin ``` ### Using Go ```bash go install github.com/opencode-ai/opencode@latest ``` ## Configuration OpenCode looks for configuration in the following locations: - `$HOME/.opencode.json` - `$XDG_CONFIG_HOME/opencode/.opencode.json` - `./.opencode.json` (local directory) ### Auto Compact Feature OpenCode includes an auto compact feature that automatically summarizes your conversation when it approaches the model's context window limit. When enabled (default setting), this feature: - Monitors token usage during your conversation - Automatically triggers summarization when usage reaches 95% of the model's context window - Creates a new session with the summary, allowing you to continue your work without losing context - Helps prevent "out of context" errors that can occur with long conversations You can enable or disable this feature in your configuration file: ```json { "autoCompact": true // default is true } ``` ### Environment Variables You can configure OpenCode using environment variables: | Environment Variable | Purpose | | -------------------------- | ------------------------------------------------------ | | `ANTHROPIC_API_KEY` | For Claude models | | `OPENAI_API_KEY` | For OpenAI models | | `GEMINI_API_KEY` | For Google Gemini models | | `VERTEXAI_PROJECT` | For Google Cloud VertexAI (Gemini) | | `VERTEXAI_LOCATION` | For Google Cloud VertexAI (Gemini) | | `GROQ_API_KEY` | For Groq models | | `AWS_ACCESS_KEY_ID` | For AWS Bedrock (Claude) | | `AWS_SECRET_ACCESS_KEY` | For AWS Bedrock (Claude) | | `AWS_REGION` | For AWS Bedrock (Claude) | | `AZURE_OPENAI_ENDPOINT` | For Azure OpenAI models | | `AZURE_OPENAI_API_KEY` | For Azure OpenAI models (optional when using Entra ID) | | `AZURE_OPENAI_API_VERSION` | For Azure OpenAI models | | `LOCAL_ENDPOINT` | For self-hosted models | | `SHELL` | Default shell to use (if not specified in config) | ### Shell Configuration OpenCode allows you to configure the shell used by the bash tool. By default, it uses the shell specified in the `SHELL` environment variable, or falls back to `/bin/bash` if not set. You can override this in your configuration file: ```json { "shell": { "path": "/bin/zsh", "args": ["-l"] } } ``` This is useful if you want to use a different shell than your default system shell, or if you need to pass specific arguments to the shell. ### Configuration File Structure ```json { "data": { "directory": ".opencode" }, "providers": { "openai": { "apiKey": "your-api-key", "disabled": false }, "anthropic": { "apiKey": "your-api-key", "disabled": false }, "groq": { "apiKey": "your-api-key", "disabled": false }, "openrouter": { "apiKey": "your-api-key", "disabled": false } }, "agents": { "coder": { "model": "claude-3.7-sonnet", "maxTokens": 5000 }, "task": { "model": "claude-3.7-sonnet", "maxTokens": 5000 }, "title": { "model": "claude-3.7-sonnet", "maxTokens": 80 } }, "shell": { "path": "/bin/bash", "args": ["-l"] }, "mcpServers": { "example": { "type": "stdio", "command": "path/to/mcp-server", "env": [], "args": [] } }, "lsp": { "go": { "disabled": false, "command": "gopls" } }, "debug": false, "debugLSP": false, "autoCompact": true } ``` ## Supported AI Models OpenCode supports a variety of AI models from different providers: ### OpenAI - GPT-4.1 family (gpt-4.1, gpt-4.1-mini, gpt-4.1-nano) - GPT-4.5 Preview - GPT-4o family (gpt-4o, gpt-4o-mini) - O1 family (o1, o1-pro, o1-mini) - O3 family (o3, o3-mini) - O4 Mini ### Anthropic - Claude 4 Sonnet - Claude 4 Opus - Claude 3.5 Sonnet - Claude 3.5 Haiku - Claude 3.7 Sonnet - Claude 3 Haiku - Claude 3 Opus ### Google - Gemini 2.5 - Gemini 2.5 Flash - Gemini 2.0 Flash - Gemini 2.0 Flash Lite ### AWS Bedrock - Claude 3.7 Sonnet ### Groq - Llama 4 Maverick (17b-128e-instruct) - Llama 4 Scout (17b-16e-instruct) - QWEN QWQ-32b - Deepseek R1 distill Llama 70b - Llama 3.3 70b Versatile ### Azure OpenAI - GPT-4.1 family (gpt-4.1, gpt-4.1-mini, gpt-4.1-nano) - GPT-4.5 Preview - GPT-4o family (gpt-4o, gpt-4o-mini) - O1 family (o1, o1-mini) - O3 family (o3, o3-mini) - O4 Mini ### Google Cloud VertexAI - Gemini 2.5 - Gemini 2.5 Flash ## Usage ```bash # Start OpenCode opencode # Start with debug logging opencode -d # Start with a specific working directory opencode -c /path/to/project ``` ## Non-interactive Prompt Mode You can run OpenCode in non-interactive mode by passing a prompt directly as a command-line argument. This is useful for scripting, automation, or when you want a quick answer without launching the full TUI. ```bash # Run a single prompt and print the AI's response to the terminal opencode -p "Explain the use of context in Go" # Get response in JSON format opencode -p "Explain the use of context in Go" -f json # Run without showing the spinner (useful for scripts) opencode -p "Explain the use of context in Go" -q ``` In this mode, OpenCode will process your prompt, print the result to standard output, and then exit. All permissions are auto-approved for the session. By default, a spinner animation is displayed while the model is processing your query. You can disable this spinner with the `-q` or `--quiet` flag, which is particularly useful when running OpenCode from scripts or automated workflows. ### Output Formats OpenCode supports the following output formats in non-interactive mode: | Format | Description | | ------ | ------------------------------- | | `text` | Plain text output (default) | | `json` | Output wrapped in a JSON object | The output format is implemented as a strongly-typed `OutputFormat` in the codebase, ensuring type safety and validation when processing outputs. ## Command-line Flags | Flag | Short | Description | | ----------------- | ----- | --------------------------------------------------- | | `--help` | `-h` | Display help information | | `--debug` | `-d` | Enable debug mode | | `--cwd` | `-c` | Set current working directory | | `--prompt` | `-p` | Run a single prompt in non-interactive mode | | `--output-format` | `-f` | Output format for non-interactive mode (text, json) | | `--quiet` | `-q` | Hide spinner in non-interactive mode | ## Keyboard Shortcuts ### Global Shortcuts | Shortcut | Action | | -------- | ------------------------------------------------------- | | `Ctrl+C` | Quit application | | `Ctrl+?` | Toggle help dialog | | `?` | Toggle help dialog (when not in editing mode) | | `Ctrl+L` | View logs | | `Ctrl+A` | Switch session | | `Ctrl+K` | Command dialog | | `Ctrl+O` | Toggle model selection dialog | | `Esc` | Close current overlay/dialog or return to previous mode | ### Chat Page Shortcuts | Shortcut | Action | | -------- | --------------------------------------- | | `Ctrl+N` | Create new session | | `Ctrl+X` | Cancel current operation/generation | | `i` | Focus editor (when not in writing mode) | | `Esc` | Exit writing mode and focus messages | ### Editor Shortcuts | Shortcut | Action | | ------------------- | ----------------------------------------- | | `Ctrl+S` | Send message (when editor is focused) | | `Enter` or `Ctrl+S` | Send message (when editor is not focused) | | `Ctrl+E` | Open external editor | | `Esc` | Blur editor and focus messages | ### Session Dialog Shortcuts | Shortcut | Action | | ---------- | ---------------- | | `↑` or `k` | Previous session | | `↓` or `j` | Next session | | `Enter` | Select session | | `Esc` | Close dialog | ### Model Dialog Shortcuts | Shortcut | Action | | ---------- | ----------------- | | `↑` or `k` | Move up | | `↓` or `j` | Move down | | `←` or `h` | Previous provider | | `→` or `l` | Next provider | | `Esc` | Close dialog | ### Permission Dialog Shortcuts | Shortcut | Action | | ----------------------- | ---------------------------- | | `←` or `left` | Switch options left | | `→` or `right` or `tab` | Switch options right | | `Enter` or `space` | Confirm selection | | `a` | Allow permission | | `A` | Allow permission for session | | `d` | Deny permission | ### Logs Page Shortcuts | Shortcut | Action | | ------------------ | ------------------- | | `Backspace` or `q` | Return to chat page | ## AI Assistant Tools OpenCode's AI assistant has access to various tools to help with coding tasks: ### File and Code Tools | Tool | Description | Parameters | | ------------- | --------------------------- | ---------------------------------------------------------------------------------------- | | `glob` | Find files by pattern | `pattern` (required), `path` (optional) | | `grep` | Search file contents | `pattern` (required), `path` (optional), `include` (optional), `literal_text` (optional) | | `ls` | List directory contents | `path` (optional), `ignore` (optional array of patterns) | | `view` | View file contents | `file_path` (required), `offset` (optional), `limit` (optional) | | `write` | Write to files | `file_path` (required), `content` (required) | | `edit` | Edit files | Various parameters for file editing | | `patch` | Apply patches to files | `file_path` (required), `diff` (required) | | `diagnostics` | Get diagnostics information | `file_path` (optional) | ### Other Tools | Tool | Description | Parameters | | ------------- | -------------------------------------- | ----------------------------------------------------------------------------------------- | | `bash` | Execute shell commands | `command` (required), `timeout` (optional) | | `fetch` | Fetch data from URLs | `url` (required), `format` (required), `timeout` (optional) | | `sourcegraph` | Search code across public repositories | `query` (required), `count` (optional), `context_window` (optional), `timeout` (optional) | | `agent` | Run sub-tasks with the AI agent | `prompt` (required) | ## Architecture OpenCode is built with a modular architecture: - **cmd**: Command-line interface using Cobra - **internal/app**: Core application services - **internal/config**: Configuration management - **internal/db**: Database operations and migrations - **internal/llm**: LLM providers and tools integration - **internal/tui**: Terminal UI components and layouts - **internal/logging**: Logging infrastructure - **internal/message**: Message handling - **internal/session**: Session management - **internal/lsp**: Language Server Protocol integration ## Custom Commands OpenCode supports custom commands that can be created by users to quickly send predefined prompts to the AI assistant. ### Creating Custom Commands Custom commands are predefined prompts stored as Markdown files in one of three locations: 1. **User Commands** (prefixed with `user:`): ``` $XDG_CONFIG_HOME/opencode/commands/ ``` (typically `~/.config/opencode/commands/` on Linux/macOS) or ``` $HOME/.opencode/commands/ ``` 2. **Project Commands** (prefixed with `project:`): ``` <PROJECT DIR>/.opencode/commands/ ``` Each `.md` file in these directories becomes a custom command. The file name (without extension) becomes the command ID. For example, creating a file at `~/.config/opencode/commands/prime-context.md` with content: ```markdown RUN git ls-files READ README.md ``` This creates a command called `user:prime-context`. ### Command Arguments OpenCode supports named arguments in custom commands using placeholders in the format `$NAME` (where NAME consists of uppercase letters, numbers, and underscores, and must start with a letter). For example: ```markdown # Fetch Context for Issue $ISSUE_NUMBER RUN gh issue view $ISSUE_NUMBER --json title,body,comments RUN git grep --author="$AUTHOR_NAME" -n . RUN grep -R "$SEARCH_PATTERN" $DIRECTORY ``` When you run a command with arguments, OpenCode will prompt you to enter values for each unique placeholder. Named arguments provide several benefits: - Clear identification of what each argument represents - Ability to use the same argument multiple times - Better organization for commands with multiple inputs ### Organizing Commands You can organize commands in subdirectories: ``` ~/.config/opencode/commands/git/commit.md ``` This creates a command with ID `user:git:commit`. ### Using Custom Commands 1. Press `Ctrl+K` to open the command dialog 2. Select your custom command (prefixed with either `user:` or `project:`) 3. Press Enter to execute the command The content of the command file will be sent as a message to the AI assistant. ### Built-in Commands OpenCode includes several built-in commands: | Command | Description | | ------------------ | --------------------------------------------------------------------------------------------------- | | Initialize Project | Creates or updates the OpenCode.md memory file with project-specific information | | Compact Session | Manually triggers the summarization of the current session, creating a new session with the summary | ## MCP (Model Context Protocol) OpenCode implements the Model Context Protocol (MCP) to extend its capabilities through external tools. MCP provides a standardized way for the AI assistant to interact with external services and tools. ### MCP Features - **External Tool Integration**: Connect to external tools and services via a standardized protocol - **Tool Discovery**: Automatically discover available tools from MCP servers - **Multiple Connection Types**: - **Stdio**: Communicate with tools via standard input/output - **SSE**: Communicate with tools via Server-Sent Events - **Security**: Permission system for controlling access to MCP tools ### Configuring MCP Servers MCP servers are defined in the configuration file under the `mcpServers` section: ```json { "mcpServers": { "example": { "type": "stdio", "command": "path/to/mcp-server", "env": [], "args": [] }, "web-example": { "type": "sse", "url": "https://example.com/mcp", "headers": { "Authorization": "Bearer token" } } } } ``` ### MCP Tool Usage Once configured, MCP tools are automatically available to the AI assistant alongside built-in tools. They follow the same permission model as other tools, requiring user approval before execution. ## LSP (Language Server Protocol) OpenCode integrates with Language Server Protocol to provide code intelligence features across multiple programming languages. ### LSP Features - **Multi-language Support**: Connect to language servers for different programming languages - **Diagnostics**: Receive error checking and linting information - **File Watching**: Automatically notify language servers of file changes ### Configuring LSP Language servers are configured in the configuration file under the `lsp` section: ```json { "lsp": { "go": { "disabled": false, "command": "gopls" }, "typescript": { "disabled": false, "command": "typescript-language-server", "args": ["--stdio"] } } } ``` ### LSP Integration with AI The AI assistant can access LSP features through the `diagnostics` tool, allowing it to: - Check for errors in your code - Suggest fixes based on diagnostics While the LSP client implementation supports the full LSP protocol (including completions, hover, definition, etc.), currently only diagnostics are exposed to the AI assistant. ## Using a self-hosted model provider OpenCode can also load and use models from a self-hosted (OpenAI-like) provider. This is useful for developers who want to experiment with custom models. ### Configuring a self-hosted provider You can use a self-hosted model by setting the `LOCAL_ENDPOINT` environment variable. This will cause OpenCode to load and use the models from the specified endpoint. ```bash LOCAL_ENDPOINT=http://localhost:1235/v1 ``` ### Configuring a self-hosted model You can also configure a self-hosted model in the configuration file under the `agents` section: ```json { "agents": { "coder": { "model": "local.granite-3.3-2b-instruct@q8_0", "reasoningEffort": "high" } } } ``` ## Development ### Prerequisites - Go 1.24.0 or higher ### Building from Source ```bash # Clone the repository git clone https://github.com/opencode-ai/opencode.git cd opencode # Build go build -o opencode # Run ./opencode ``` ## Acknowledgments OpenCode gratefully acknowledges the contributions and support from these key individuals: - [@isaacphi](https://github.com/isaacphi) - For the [mcp-language-server](https://github.com/isaacphi/mcp-language-server) project which provided the foundation for our LSP client implementation - [@adamdottv](https://github.com/adamdottv) - For the design direction and UI/UX architecture Special thanks to the broader open source community whose tools and libraries have made this project possible. ## License OpenCode is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. ## Contributing Contributions are welcome! Here's how you can contribute: 1. Fork the repository 2. Create a feature branch (`git checkout -b feature/amazing-feature`) 3. Commit your changes (`git commit -m 'Add some amazing feature'`) 4. Push to the branch (`git push origin feature/amazing-feature`) 5. Open a Pull Request Please make sure to update tests as appropriate and follow the existing code style. ## /cmd/root.go ```go path="/cmd/root.go" package cmd import ( "context" "fmt" "os" "sync" "time" tea "github.com/charmbracelet/bubbletea" zone "github.com/lrstanley/bubblezone" "github.com/opencode-ai/opencode/internal/app" "github.com/opencode-ai/opencode/internal/config" "github.com/opencode-ai/opencode/internal/db" "github.com/opencode-ai/opencode/internal/format" "github.com/opencode-ai/opencode/internal/llm/agent" "github.com/opencode-ai/opencode/internal/logging" "github.com/opencode-ai/opencode/internal/pubsub" "github.com/opencode-ai/opencode/internal/tui" "github.com/opencode-ai/opencode/internal/version" "github.com/spf13/cobra" ) var rootCmd = &cobra.Command{ Use: "opencode", Short: "Terminal-based AI assistant for software development", Long: `OpenCode is a powerful terminal-based AI assistant that helps with software development tasks. It provides an interactive chat interface with AI capabilities, code analysis, and LSP integration to assist developers in writing, debugging, and understanding code directly from the terminal.`, Example: ` # Run in interactive mode opencode # Run with debug logging opencode -d # Run with debug logging in a specific directory opencode -d -c /path/to/project # Print version opencode -v # Run a single non-interactive prompt opencode -p "Explain the use of context in Go" # Run a single non-interactive prompt with JSON output format opencode -p "Explain the use of context in Go" -f json `, RunE: func(cmd *cobra.Command, args []string) error { // If the help flag is set, show the help message if cmd.Flag("help").Changed { cmd.Help() return nil } if cmd.Flag("version").Changed { fmt.Println(version.Version) return nil } // Load the config debug, _ := cmd.Flags().GetBool("debug") cwd, _ := cmd.Flags().GetString("cwd") prompt, _ := cmd.Flags().GetString("prompt") outputFormat, _ := cmd.Flags().GetString("output-format") quiet, _ := cmd.Flags().GetBool("quiet") // Validate format option if !format.IsValid(outputFormat) { return fmt.Errorf("invalid format option: %s\n%s", outputFormat, format.GetHelpText()) } if cwd != "" { err := os.Chdir(cwd) if err != nil { return fmt.Errorf("failed to change directory: %v", err) } } if cwd == "" { c, err := os.Getwd() if err != nil { return fmt.Errorf("failed to get current working directory: %v", err) } cwd = c } _, err := config.Load(cwd, debug) if err != nil { return err } // Connect DB, this will also run migrations conn, err := db.Connect() if err != nil { return err } // Create main context for the application ctx, cancel := context.WithCancel(context.Background()) defer cancel() app, err := app.New(ctx, conn) if err != nil { logging.Error("Failed to create app: %v", err) return err } // Defer shutdown here so it runs for both interactive and non-interactive modes defer app.Shutdown() // Initialize MCP tools early for both modes initMCPTools(ctx, app) // Non-interactive mode if prompt != "" { // Run non-interactive flow using the App method return app.RunNonInteractive(ctx, prompt, outputFormat, quiet) } // Interactive mode // Set up the TUI zone.NewGlobal() program := tea.NewProgram( tui.New(app), tea.WithAltScreen(), ) // Setup the subscriptions, this will send services events to the TUI ch, cancelSubs := setupSubscriptions(app, ctx) // Create a context for the TUI message handler tuiCtx, tuiCancel := context.WithCancel(ctx) var tuiWg sync.WaitGroup tuiWg.Add(1) // Set up message handling for the TUI go func() { defer tuiWg.Done() defer logging.RecoverPanic("TUI-message-handler", func() { attemptTUIRecovery(program) }) for { select { case <-tuiCtx.Done(): logging.Info("TUI message handler shutting down") return case msg, ok := <-ch: if !ok { logging.Info("TUI message channel closed") return } program.Send(msg) } } }() // Cleanup function for when the program exits cleanup := func() { // Shutdown the app app.Shutdown() // Cancel subscriptions first cancelSubs() // Then cancel TUI message handler tuiCancel() // Wait for TUI message handler to finish tuiWg.Wait() logging.Info("All goroutines cleaned up") } // Run the TUI result, err := program.Run() cleanup() if err != nil { logging.Error("TUI error: %v", err) return fmt.Errorf("TUI error: %v", err) } logging.Info("TUI exited with result: %v", result) return nil }, } // attemptTUIRecovery tries to recover the TUI after a panic func attemptTUIRecovery(program *tea.Program) { logging.Info("Attempting to recover TUI after panic") // 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 initMCPTools(ctx context.Context, app *app.App) { go func() { defer logging.RecoverPanic("MCP-goroutine", nil) // Create a context with timeout for the initial MCP tools fetch ctxWithTimeout, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() // Set this up once with proper error handling agent.GetMcpTools(ctxWithTimeout, app.Permissions) logging.Info("MCP message handling goroutine exiting") }() } func setupSubscriber[T any]( ctx context.Context, wg *sync.WaitGroup, name string, subscriber func(context.Context) <-chan pubsub.Event[T], outputCh chan<- tea.Msg, ) { wg.Add(1) go func() { defer wg.Done() defer logging.RecoverPanic(fmt.Sprintf("subscription-%s", name), nil) subCh := subscriber(ctx) for { select { case event, ok := <-subCh: if !ok { logging.Info("subscription channel closed", "name", name) return } var msg tea.Msg = event select { case outputCh <- msg: case <-time.After(2 * time.Second): logging.Warn("message dropped due to slow consumer", "name", name) case <-ctx.Done(): logging.Info("subscription cancelled", "name", name) return } case <-ctx.Done(): logging.Info("subscription cancelled", "name", name) return } } }() } func setupSubscriptions(app *app.App, parentCtx context.Context) (chan tea.Msg, func()) { ch := make(chan tea.Msg, 100) wg := sync.WaitGroup{} ctx, cancel := context.WithCancel(parentCtx) // Inherit from parent context setupSubscriber(ctx, &wg, "logging", logging.Subscribe, ch) setupSubscriber(ctx, &wg, "sessions", app.Sessions.Subscribe, ch) setupSubscriber(ctx, &wg, "messages", app.Messages.Subscribe, ch) setupSubscriber(ctx, &wg, "permissions", app.Permissions.Subscribe, ch) setupSubscriber(ctx, &wg, "coderAgent", app.CoderAgent.Subscribe, ch) cleanupFunc := func() { logging.Info("Cancelling all subscriptions") cancel() // Signal all goroutines to stop waitCh := make(chan struct{}) go func() { defer logging.RecoverPanic("subscription-cleanup", nil) wg.Wait() close(waitCh) }() select { case <-waitCh: logging.Info("All subscription goroutines completed successfully") close(ch) // Only close after all writers are confirmed done case <-time.After(5 * time.Second): logging.Warn("Timed out waiting for some subscription goroutines to complete") close(ch) } } return ch, cleanupFunc } func Execute() { err := rootCmd.Execute() if err != nil { os.Exit(1) } } func init() { rootCmd.Flags().BoolP("help", "h", false, "Help") rootCmd.Flags().BoolP("version", "v", false, "Version") rootCmd.Flags().BoolP("debug", "d", false, "Debug") rootCmd.Flags().StringP("cwd", "c", "", "Current working directory") rootCmd.Flags().StringP("prompt", "p", "", "Prompt to run in non-interactive mode") // Add format flag with validation logic rootCmd.Flags().StringP("output-format", "f", format.Text.String(), "Output format for non-interactive mode (text, json)") // Add quiet flag to hide spinner in non-interactive mode rootCmd.Flags().BoolP("quiet", "q", false, "Hide spinner in non-interactive mode") // Register custom validation for the format flag rootCmd.RegisterFlagCompletionFunc("output-format", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return format.SupportedFormats, cobra.ShellCompDirectiveNoFileComp }) } ``` ## /cmd/schema/README.md # 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": { "coder": { "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 } } } ``` ## /cmd/schema/main.go ```go path="/cmd/schema/main.go" package main import ( "encoding/json" "fmt" "os" "github.com/opencode-ai/opencode/internal/config" "github.com/opencode-ai/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", "catppuccin", "dracula", "flexoki", "gruvbox", "monokai", "onedark", "tokyonight", "tron", }, }, }, } // 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.AgentCoder), 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 } ``` ## /go.mod ```mod path="/go.mod" module github.com/opencode-ai/opencode go 1.24.0 require ( github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 github.com/JohannesKaufmann/html-to-markdown v1.6.0 github.com/PuerkitoBio/goquery v1.9.2 github.com/alecthomas/chroma/v2 v2.15.0 github.com/anthropics/anthropic-sdk-go v1.4.0 github.com/aymanbagabas/go-udiff v0.2.0 github.com/bmatcuk/doublestar/v4 v4.8.1 github.com/catppuccin/go v0.3.0 github.com/charmbracelet/bubbles v0.21.0 github.com/charmbracelet/bubbletea v1.3.5 github.com/charmbracelet/glamour v0.9.1 github.com/charmbracelet/lipgloss v1.1.0 github.com/charmbracelet/x/ansi v0.8.0 github.com/fsnotify/fsnotify v1.8.0 github.com/go-logfmt/logfmt v0.6.0 github.com/google/uuid v1.6.0 github.com/lrstanley/bubblezone v0.0.0-20250315020633-c249a3fe1231 github.com/mark3labs/mcp-go v0.17.0 github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 github.com/muesli/reflow v0.3.0 github.com/muesli/termenv v0.16.0 github.com/ncruces/go-sqlite3 v0.25.0 github.com/openai/openai-go v0.1.0-beta.2 github.com/pressly/goose/v3 v3.24.2 github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 github.com/spf13/cobra v1.9.1 github.com/spf13/viper v1.20.0 github.com/stretchr/testify v1.10.0 ) require ( cloud.google.com/go v0.116.0 // indirect cloud.google.com/go/auth v0.13.0 // indirect cloud.google.com/go/compute/metadata v0.6.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect github.com/andybalholm/cascadia v1.3.2 // indirect github.com/atotto/clipboard v0.1.4 // indirect github.com/aws/aws-sdk-go-v2 v1.30.3 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 // indirect github.com/aws/aws-sdk-go-v2/config v1.27.27 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.17.27 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 // indirect github.com/aws/smithy-go v1.20.3 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect github.com/charmbracelet/x/term v0.2.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/disintegration/imaging v1.6.2 github.com/dlclark/regexp2 v1.11.4 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/golang-jwt/jwt/v5 v5.2.2 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/s2a-go v0.1.8 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect github.com/googleapis/gax-go/v2 v2.14.1 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/gorilla/websocket v1.5.3 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/lithammer/fuzzysearch v1.1.8 github.com/lucasb-eyer/go-colorful v1.2.0 github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mfridman/interpolate v0.0.2 // indirect github.com/microcosm-cc/bluemonday v1.0.27 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/ncruces/julianday v1.0.0 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/sagikazarmark/locafero v0.7.0 // indirect github.com/sethvargo/go-retry v0.3.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.12.0 // indirect github.com/spf13/cast v1.7.1 // indirect github.com/spf13/pflag v1.0.6 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/tetratelabs/wazero v1.9.0 // indirect github.com/tidwall/gjson v1.18.0 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect github.com/yuin/goldmark v1.7.8 // indirect github.com/yuin/goldmark-emoji v1.0.5 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect go.opentelemetry.io/otel v1.35.0 // indirect go.opentelemetry.io/otel/metric v1.35.0 // indirect go.opentelemetry.io/otel/trace v1.35.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/crypto v0.37.0 // indirect golang.org/x/image v0.26.0 // indirect golang.org/x/net v0.39.0 // indirect golang.org/x/sync v0.13.0 // indirect golang.org/x/sys v0.32.0 // indirect golang.org/x/term v0.31.0 // indirect golang.org/x/text v0.24.0 // indirect google.golang.org/genai v1.3.0 google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect google.golang.org/grpc v1.71.0 // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ``` ## /go.sum ```sum path="/go.sum" cloud.google.com/go v0.116.0 h1:B3fRrSDkLRt5qSHWe40ERJvhvnQwdZiHu0bJOpldweE= cloud.google.com/go v0.116.0/go.mod h1:cEPSRWPzZEswwdr9BxE6ChEn01dWlTaF05LiC2Xs70U= cloud.google.com/go/auth v0.13.0 h1:8Fu8TZy167JkW8Tj3q7dIkr2v4cndv41ouecJx0PAHs= cloud.google.com/go/auth v0.13.0/go.mod h1:COOjD9gwfKNKz+IIduatIhYJQIc0mG3H102r/EMxX6Q= cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 h1:g0EZJwz7xkXQiZAI5xi9f3WWFYBlX1CPTrR+NDToRkQ= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0/go.mod h1:XCW7KnZet0Opnr7HccfUw1PLc4CjHqpcaxW8DHklNkQ= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 h1:tfLQ34V6F7tVSwoTf/4lH5sE0o6eCJuNDTmH09nDpbc= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg= github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY= github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY= github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU= github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/JohannesKaufmann/html-to-markdown v1.6.0 h1:04VXMiE50YYfCfLboJCLcgqF5x+rHJnb1ssNmqpLH/k= github.com/JohannesKaufmann/html-to-markdown v1.6.0/go.mod h1:NUI78lGg/a7vpEJTz/0uOcYMaibytE4BUOQS8k78yPQ= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/PuerkitoBio/goquery v1.9.2 h1:4/wZksC3KgkQw7SQgkKotmKljk0M6V8TUvA8Wb4yPeE= github.com/PuerkitoBio/goquery v1.9.2/go.mod h1:GHPCaP0ODyyxqcNoFGYlAprUFH81NuRPd0GX3Zu2Mvk= github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/chroma/v2 v2.15.0 h1:LxXTQHFoYrstG2nnV9y2X5O94sOBzf0CIUpSTbpxvMc= github.com/alecthomas/chroma/v2 v2.15.0/go.mod h1:gUhVLrPDXPtp/f+L1jo9xepo9gL4eLwRuGAunSZMkio= github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss= github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU= github.com/anthropics/anthropic-sdk-go v1.4.0 h1:fU1jKxYbQdQDiEXCxeW5XZRIOwKevn/PMg8Ay1nnUx0= github.com/anthropics/anthropic-sdk-go v1.4.0/go.mod h1:AapDW22irxK2PSumZiQXYUFvsdQgkwIWlpESweWZI/c= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aws/aws-sdk-go-v2 v1.30.3 h1:jUeBtG0Ih+ZIFH0F4UkmL9w3cSpaMv9tYYDbzILP8dY= github.com/aws/aws-sdk-go-v2 v1.30.3/go.mod h1:nIQjQVp5sfpQcTc9mPSr1B0PaWK5ByX9MOoDadSN4lc= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 h1:tW1/Rkad38LA15X4UQtjXZXNKsCgkshC3EbmcUmghTg= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3/go.mod h1:UbnqO+zjqk3uIt9yCACHJ9IVNhyhOCnYk8yA19SAWrM= github.com/aws/aws-sdk-go-v2/config v1.27.27 h1:HdqgGt1OAP0HkEDDShEl0oSYa9ZZBSOmKpdpsDMdO90= github.com/aws/aws-sdk-go-v2/config v1.27.27/go.mod h1:MVYamCg76dFNINkZFu4n4RjDixhVr51HLj4ErWzrVwg= github.com/aws/aws-sdk-go-v2/credentials v1.17.27 h1:2raNba6gr2IfA0eqqiP2XiQ0UVOpGPgDSi0I9iAP+UI= github.com/aws/aws-sdk-go-v2/credentials v1.17.27/go.mod h1:gniiwbGahQByxan6YjQUMcW4Aov6bLC3m+evgcoN4r4= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 h1:KreluoV8FZDEtI6Co2xuNk/UqI9iwMrOx/87PBNIKqw= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11/go.mod h1:SeSUYBLsMYFoRvHE0Tjvn7kbxaUhl75CJi1sbfhMxkU= github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 h1:SoNJ4RlFEQEbtDcCEt+QG56MY4fm4W8rYirAmq+/DdU= github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15/go.mod h1:U9ke74k1n2bf+RIgoX1SXFed1HLs51OgUSs+Ph0KJP8= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 h1:C6WHdGnTDIYETAm5iErQUiVNsclNx9qbJVPIt03B6bI= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15/go.mod h1:ZQLZqhcu+JhSrA9/NXRm8SkDvsycE+JkV3WGY41e+IM= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 h1:dT3MqvGhSoaIhRseqw2I0yH81l7wiR2vjs57O51EAm8= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3/go.mod h1:GlAeCkHwugxdHaueRr4nhPuY+WW+gR8UjlcqzPr1SPI= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 h1:HGErhhrxZlQ044RiM+WdoZxp0p+EGM62y3L6pwA4olE= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17/go.mod h1:RkZEx4l0EHYDJpWppMJ3nD9wZJAa8/0lq9aVC+r2UII= github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 h1:BXx0ZIxvrJdSgSvKTZ+yRBeSqqgPM89VPlulEcl37tM= github.com/aws/aws-sdk-go-v2/service/sso v1.22.4/go.mod h1:ooyCOXjvJEsUw7x+ZDHeISPMhtwI3ZCB7ggFMcFfWLU= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 h1:yiwVzJW2ZxZTurVbYWA7QOrAaCYQR72t0wrSBfoesUE= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4/go.mod h1:0oxfLkpz3rQ/CHlx5hB7H69YUpFiI1tql6Q6Ne+1bCw= github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 h1:ZsDKRLXGWHk8WdtyYMoGNO7bTudrvuKpDKgMVRlepGE= github.com/aws/aws-sdk-go-v2/service/sts v1.30.3/go.mod h1:zwySh8fpFyXp9yOr/KVzxOl8SRqgf/IDw5aUt9UKFcQ= github.com/aws/smithy-go v1.20.3 h1:ryHwveWzPV5BIof6fyDvor6V3iUL7nTfiTKXHiW05nE= github.com/aws/smithy-go v1.20.3/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38= github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY= github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs= github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg= github.com/charmbracelet/bubbletea v1.3.5 h1:JAMNLTbqMOhSwoELIr0qyP4VidFq72/6E9j7HHmRKQc= github.com/charmbracelet/bubbletea v1.3.5/go.mod h1:TkCnmH+aBd4LrXhXcqrKiYwRs7qyQx5rBgH5fVY3v54= github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= github.com/charmbracelet/glamour v0.9.1 h1:11dEfiGP8q1BEqvGoIjivuc2rBk+5qEXdPtaQ2WoiCM= github.com/charmbracelet/glamour v0.9.1/go.mod h1:+SHvIS8qnwhgTpVMiXwn7OfGomSqff1cHBCI8jLOetk= github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ= github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo= github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM= github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw= github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA= github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q= github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4= github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4= github.com/lrstanley/bubblezone v0.0.0-20250315020633-c249a3fe1231 h1:9rjt7AfnrXKNSZhp36A3/4QAZAwGGCGD/p8Bse26zms= github.com/lrstanley/bubblezone v0.0.0-20250315020633-c249a3fe1231/go.mod h1:S5etECMx+sZnW0Gm100Ma9J1PgVCTgNyFaqGu2b08b4= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mark3labs/mcp-go v0.17.0 h1:5Ps6T7qXr7De/2QTqs9h6BKeZ/qdeUeGrgM5lPzi930= github.com/mark3labs/mcp-go v0.17.0/go.mod h1:KmJndYv7GIgcPVwEKJjNcbhVQ+hJGJhrCCB/9xITzpE= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY= github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/ncruces/go-sqlite3 v0.25.0 h1:trugKUs98Zwy9KwRr/EUxZHL92LYt7UqcKqAfpGpK+I= github.com/ncruces/go-sqlite3 v0.25.0/go.mod h1:n6Z7036yFilJx04yV0mi5JWaF66rUmXn1It9Ux8dx68= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M= github.com/ncruces/julianday v1.0.0/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g= github.com/openai/openai-go v0.1.0-beta.2 h1:Ra5nCFkbEl9w+UJwAciC4kqnIBUCcJazhmMA0/YN894= github.com/openai/openai-go v0.1.0-beta.2/go.mod h1:g461MYGXEXBVdV5SaR/5tNzNbSfwTBBefwc+LlDCK0Y= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pressly/goose/v3 v3.24.2 h1:c/ie0Gm8rnIVKvnDQ/scHErv46jrDv9b4I0WRcFJzYU= github.com/pressly/goose/v3 v3.24.2/go.mod h1:kjefwFB0eR4w30Td2Gj2Mznyw94vSP+2jJYkOVNbD1k= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= github.com/sebdah/goldie/v2 v2.5.3 h1:9ES/mNN+HNUbNWpVAlrzuZ7jE+Nrczbj8uFRjM7624Y= github.com/sebdah/goldie/v2 v2.5.3/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE= github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4= github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.20.0 h1:zrxIyR3RQIOsarIrgL8+sAvALXul9jeEPa06Y0Ph6vY= github.com/spf13/viper v1.20.0/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I= github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic= github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk= github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.26.0 h1:4XjIFEZWQmCZi6Wv8BoxsDhRU3RVnLX04dToTDAEPlY= golang.org/x/image v0.26.0/go.mod h1:lcxbMFAovzpnJxzXS3nyL83K27tmqtKzIJpctK8YO5c= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/genai v1.3.0 h1:tXhPJF30skOjnnDY7ZnjK3q7IKy4PuAlEA0fk7uEaEI= google.golang.org/genai v1.3.0/go.mod h1:TyfOKRz/QyCaj6f/ZDt505x+YreXnY40l2I6k8TvgqY= google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g= google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= modernc.org/libc v1.61.13 h1:3LRd6ZO1ezsFiX1y+bHd1ipyEHIJKvuprv0sLTBwLW8= modernc.org/libc v1.61.13/go.mod h1:8F/uJWL/3nNil0Lgt1Dpz+GgkApWh04N3el3hxJcA6E= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/memory v1.9.1 h1:V/Z1solwAVmMW1yttq3nDdZPJqV1rM05Ccq6KMSZ34g= modernc.org/memory v1.9.1/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= modernc.org/sqlite v1.36.2 h1:vjcSazuoFve9Wm0IVNHgmJECoOXLZM1KfMXbcX2axHA= modernc.org/sqlite v1.36.2/go.mod h1:ADySlx7K4FdY5MaJcEv86hTJ0PjedAloTUuif0YS3ws= ``` ## /install ``` path="/install" #!/usr/bin/env bash set -euo pipefail APP=opencode RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' ORANGE='\033[38;2;255;140;0m' NC='\033[0m' # No Color requested_version=${VERSION:-} os=$(uname -s | tr '[:upper:]' '[:lower:]') if [[ "$os" == "darwin" ]]; then os="mac" fi arch=$(uname -m) if [[ "$arch" == "aarch64" ]]; then arch="arm64" fi filename="$APP-$os-$arch.tar.gz" case "$filename" in *"-linux-"*) [[ "$arch" == "x86_64" || "$arch" == "arm64" || "$arch" == "i386" ]] || exit 1 ;; *"-mac-"*) [[ "$arch" == "x86_64" || "$arch" == "arm64" ]] || exit 1 ;; *) echo "${RED}Unsupported OS/Arch: $os/$arch${NC}" exit 1 ;; esac INSTALL_DIR=$HOME/.opencode/bin mkdir -p "$INSTALL_DIR" if [ -z "$requested_version" ]; then url="https://github.com/opencode-ai/opencode/releases/latest/download/$filename" specific_version=$(curl -s https://api.github.com/repos/opencode-ai/opencode/releases/latest | awk -F'"' '/"tag_name": "/ {gsub(/^v/, "", $4); print $4}') if [[ $? -ne 0 ]]; then echo "${RED}Failed to fetch version information${NC}" exit 1 fi else url="https://github.com/opencode-ai/opencode/releases/download/v${requested_version}/$filename" specific_version=$requested_version fi print_message() { local level=$1 local message=$2 local color="" case $level in info) color="${GREEN}" ;; warning) color="${YELLOW}" ;; error) color="${RED}" ;; esac echo -e "${color}${message}${NC}" } check_version() { if command -v opencode >/dev/null 2>&1; then opencode_path=$(which opencode) ## TODO: check if version is installed # installed_version=$(opencode version) installed_version="0.0.1" installed_version=$(echo $installed_version | awk '{print $2}') if [[ "$installed_version" != "$specific_version" ]]; then print_message info "Installed version: ${YELLOW}$installed_version." else print_message info "Version ${YELLOW}$specific_version${GREEN} already installed" exit 0 fi fi } download_and_install() { print_message info "Downloading ${ORANGE}opencode ${GREEN}version: ${YELLOW}$specific_version ${GREEN}..." mkdir -p opencodetmp && cd opencodetmp curl -# -L $url | tar xz mv opencode $INSTALL_DIR cd .. && rm -rf opencodetmp } check_version download_and_install add_to_path() { local config_file=$1 local command=$2 if [[ -w $config_file ]]; then echo -e "\n# opencode" >> "$config_file" echo "$command" >> "$config_file" print_message info "Successfully added ${ORANGE}opencode ${GREEN}to \$PATH in $config_file" else print_message warning "Manually add the directory to $config_file (or similar):" print_message info " $command" fi } XDG_CONFIG_HOME=${XDG_CONFIG_HOME:-$HOME/.config} current_shell=$(basename "$SHELL") case $current_shell in fish) config_files="$HOME/.config/fish/config.fish" ;; zsh) config_files="$HOME/.zshrc $HOME/.zshenv $XDG_CONFIG_HOME/zsh/.zshrc $XDG_CONFIG_HOME/zsh/.zshenv" ;; bash) config_files="$HOME/.bashrc $HOME/.bash_profile $HOME/.profile $XDG_CONFIG_HOME/bash/.bashrc $XDG_CONFIG_HOME/bash/.bash_profile" ;; ash) config_files="$HOME/.ashrc $HOME/.profile /etc/profile" ;; sh) config_files="$HOME/.ashrc $HOME/.profile /etc/profile" ;; *) # Default case if none of the above matches config_files="$HOME/.bashrc $HOME/.bash_profile $XDG_CONFIG_HOME/bash/.bashrc $XDG_CONFIG_HOME/bash/.bash_profile" ;; esac config_file="" for file in $config_files; do if [[ -f $file ]]; then config_file=$file break fi done if [[ -z $config_file ]]; then print_message error "No config file found for $current_shell. Checked files: ${config_files[@]}" exit 1 fi if [[ ":$PATH:" != *":$INSTALL_DIR:"* ]]; then case $current_shell in fish) add_to_path "$config_file" "fish_add_path $INSTALL_DIR" ;; zsh) add_to_path "$config_file" "export PATH=$INSTALL_DIR:\$PATH" ;; bash) add_to_path "$config_file" "export PATH=$INSTALL_DIR:\$PATH" ;; ash) add_to_path "$config_file" "export PATH=$INSTALL_DIR:\$PATH" ;; sh) add_to_path "$config_file" "export PATH=$INSTALL_DIR:\$PATH" ;; *) print_message warning "Manually add the directory to $config_file (or similar):" print_message info " export PATH=$INSTALL_DIR:\$PATH" ;; esac fi if [ -n "${GITHUB_ACTIONS-}" ] && [ "${GITHUB_ACTIONS}" == "true" ]; then echo "$INSTALL_DIR" >> $GITHUB_PATH print_message info "Added $INSTALL_DIR to \$GITHUB_PATH" fi ``` ## /internal/app/app.go ```go path="/internal/app/app.go" package app import ( "context" "database/sql" "errors" "fmt" "maps" "sync" "time" "github.com/opencode-ai/opencode/internal/config" "github.com/opencode-ai/opencode/internal/db" "github.com/opencode-ai/opencode/internal/format" "github.com/opencode-ai/opencode/internal/history" "github.com/opencode-ai/opencode/internal/llm/agent" "github.com/opencode-ai/opencode/internal/logging" "github.com/opencode-ai/opencode/internal/lsp" "github.com/opencode-ai/opencode/internal/message" "github.com/opencode-ai/opencode/internal/permission" "github.com/opencode-ai/opencode/internal/session" "github.com/opencode-ai/opencode/internal/tui/theme" ) 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) { q := db.New(conn) sessions := session.NewService(q) messages := message.NewService(q) files := history.NewService(q, conn) app := &App{ Sessions: sessions, Messages: messages, History: files, Permissions: permission.NewPermissionService(), LSPClients: make(map[string]*lsp.Client), } // Initialize theme based on configuration app.initTheme() // Initialize LSP clients in the background go app.initLSPClients(ctx) var err error app.CoderAgent, err = agent.NewAgent( config.AgentCoder, app.Sessions, app.Messages, agent.CoderAgentTools( app.Permissions, app.Sessions, app.Messages, app.History, app.LSPClients, ), ) if err != nil { logging.Error("Failed to create coder agent", err) return nil, err } return app, nil } // initTheme sets the application theme based on the configuration func (app *App) initTheme() { cfg := config.Get() if cfg == nil || cfg.TUI.Theme == "" { return // Use default theme } // Try to set the theme from config err := theme.SetTheme(cfg.TUI.Theme) if err != nil { logging.Warn("Failed to set theme from config, using default theme", "theme", cfg.TUI.Theme, "error", err) } else { logging.Debug("Set theme from config", "theme", cfg.TUI.Theme) } } // RunNonInteractive handles the execution flow when a prompt is provided via CLI flag. func (a *App) RunNonInteractive(ctx context.Context, prompt string, outputFormat string, quiet bool) error { logging.Info("Running in non-interactive mode") // Start spinner if not in quiet mode var spinner *format.Spinner if !quiet { spinner = format.NewSpinner("Thinking...") spinner.Start() defer spinner.Stop() } const maxPromptLengthForTitle = 100 titlePrefix := "Non-interactive: " var titleSuffix string if len(prompt) > maxPromptLengthForTitle { titleSuffix = prompt[:maxPromptLengthForTitle] + "..." } else { titleSuffix = prompt } title := titlePrefix + titleSuffix sess, err := a.Sessions.Create(ctx, title) if err != nil { return fmt.Errorf("failed to create session for non-interactive mode: %w", err) } logging.Info("Created session for non-interactive run", "session_id", sess.ID) // Automatically approve all permission requests for this non-interactive session a.Permissions.AutoApproveSession(sess.ID) done, err := a.CoderAgent.Run(ctx, sess.ID, prompt) if err != nil { return fmt.Errorf("failed to start agent processing stream: %w", err) } result := <-done if result.Error != nil { if errors.Is(result.Error, context.Canceled) || errors.Is(result.Error, agent.ErrRequestCancelled) { logging.Info("Agent processing cancelled", "session_id", sess.ID) return nil } return fmt.Errorf("agent processing failed: %w", result.Error) } // Stop spinner before printing output if !quiet && spinner != nil { spinner.Stop() } // Get the text content from the response content := "No content available" if result.Message.Content().String() != "" { content = result.Message.Content().String() } fmt.Println(format.FormatOutput(content, outputFormat)) logging.Info("Non-interactive run completed", "session_id", sess.ID) return nil } // Shutdown performs a clean shutdown of the application func (app *App) Shutdown() { // Cancel all watcher goroutines app.cancelFuncsMutex.Lock() for _, cancel := range app.watcherCancelFuncs { cancel() } app.cancelFuncsMutex.Unlock() app.watcherWG.Wait() // Perform additional cleanup for LSP clients app.clientsMutex.RLock() clients := make(map[string]*lsp.Client, len(app.LSPClients)) maps.Copy(clients, app.LSPClients) app.clientsMutex.RUnlock() for name, client := range clients { shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) if err := client.Shutdown(shutdownCtx); err != nil { logging.Error("Failed to shutdown LSP client", "name", name, "error", err) } cancel() } } ``` ## /internal/app/lsp.go ```go path="/internal/app/lsp.go" package app import ( "context" "time" "github.com/opencode-ai/opencode/internal/config" "github.com/opencode-ai/opencode/internal/logging" "github.com/opencode-ai/opencode/internal/lsp" "github.com/opencode-ai/opencode/internal/lsp/watcher" ) func (app *App) initLSPClients(ctx context.Context) { cfg := config.Get() // Initialize LSP clients for name, clientConfig := range cfg.LSP { // Start each client initialization in its own goroutine go app.createAndStartLSPClient(ctx, name, clientConfig.Command, clientConfig.Args...) } logging.Info("LSP clients initialization started in background") } // createAndStartLSPClient creates a new LSP client, initializes it, and starts its workspace watcher func (app *App) createAndStartLSPClient(ctx context.Context, name string, command string, args ...string) { // Create a specific context for initialization with a timeout logging.Info("Creating LSP client", "name", name, "command", command, "args", args) // Create the LSP client lspClient, err := lsp.NewClient(ctx, command, args...) if err != nil { logging.Error("Failed to create LSP client for", name, err) return } // Create a longer timeout for initialization (some servers take time to start) initCtx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() // Initialize with the initialization context _, err = lspClient.InitializeLSPClient(initCtx, config.WorkingDirectory()) if err != nil { logging.Error("Initialize failed", "name", name, "error", err) // Clean up the client to prevent resource leaks lspClient.Close() return } // Wait for the server to be ready if err := lspClient.WaitForServerReady(initCtx); err != nil { logging.Error("Server failed to become ready", "name", name, "error", err) // We'll continue anyway, as some functionality might still work lspClient.SetServerState(lsp.StateError) } else { logging.Info("LSP server is ready", "name", name) lspClient.SetServerState(lsp.StateReady) } logging.Info("LSP client initialized", "name", name) // Create a child context that can be canceled when the app is shutting down watchCtx, cancelFunc := context.WithCancel(ctx) // Create a context with the server name for better identification watchCtx = context.WithValue(watchCtx, "serverName", name) // Create the workspace watcher workspaceWatcher := watcher.NewWorkspaceWatcher(lspClient) // Store the cancel function to be called during cleanup app.cancelFuncsMutex.Lock() app.watcherCancelFuncs = append(app.watcherCancelFuncs, cancelFunc) app.cancelFuncsMutex.Unlock() // Add the watcher to a WaitGroup to track active goroutines app.watcherWG.Add(1) // Add to map with mutex protection before starting goroutine app.clientsMutex.Lock() app.LSPClients[name] = lspClient app.clientsMutex.Unlock() go app.runWorkspaceWatcher(watchCtx, name, workspaceWatcher) } // runWorkspaceWatcher executes the workspace watcher for an LSP client func (app *App) runWorkspaceWatcher(ctx context.Context, name string, workspaceWatcher *watcher.WorkspaceWatcher) { defer app.watcherWG.Done() defer logging.RecoverPanic("LSP-"+name, func() { // Try to restart the client app.restartLSPClient(ctx, name) }) workspaceWatcher.WatchWorkspace(ctx, config.WorkingDirectory()) logging.Info("Workspace watcher stopped", "client", name) } // restartLSPClient attempts to restart a crashed or failed LSP client func (app *App) restartLSPClient(ctx context.Context, name string) { // Get the original configuration cfg := config.Get() clientConfig, exists := cfg.LSP[name] if !exists { logging.Error("Cannot restart client, configuration not found", "client", name) return } // Clean up the old client if it exists app.clientsMutex.Lock() oldClient, exists := app.LSPClients[name] if exists { delete(app.LSPClients, name) // Remove from map before potentially slow shutdown } app.clientsMutex.Unlock() if exists && oldClient != nil { // Try to shut it down gracefully, but don't block on errors shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) _ = oldClient.Shutdown(shutdownCtx) cancel() } // Create a new client using the shared function app.createAndStartLSPClient(ctx, name, clientConfig.Command, clientConfig.Args...) logging.Info("Successfully restarted LSP client", "client", name) } ``` ## /internal/completions/files-folders.go ```go path="/internal/completions/files-folders.go" package completions import ( "bytes" "fmt" "os/exec" "path/filepath" "github.com/lithammer/fuzzysearch/fuzzy" "github.com/opencode-ai/opencode/internal/fileutil" "github.com/opencode-ai/opencode/internal/logging" "github.com/opencode-ai/opencode/internal/tui/components/dialog" ) type filesAndFoldersContextGroup struct { prefix string } func (cg *filesAndFoldersContextGroup) GetId() string { return cg.prefix } func (cg *filesAndFoldersContextGroup) GetEntry() dialog.CompletionItemI { return dialog.NewCompletionItem(dialog.CompletionItem{ Title: "Files & Folders", Value: "files", }) } func processNullTerminatedOutput(outputBytes []byte) []string { if len(outputBytes) > 0 && outputBytes[len(outputBytes)-1] == 0 { outputBytes = outputBytes[:len(outputBytes)-1] } if len(outputBytes) == 0 { return []string{} } split := bytes.Split(outputBytes, []byte{0}) matches := make([]string, 0, len(split)) for _, p := range split { if len(p) == 0 { continue } path := string(p) path = filepath.Join(".", path) if !fileutil.SkipHidden(path) { matches = append(matches, path) } } return matches } func (cg *filesAndFoldersContextGroup) getFiles(query string) ([]string, error) { cmdRg := fileutil.GetRgCmd("") // No glob pattern for this use case cmdFzf := fileutil.GetFzfCmd(query) var matches []string // Case 1: Both rg and fzf available if cmdRg != nil && cmdFzf != nil { rgPipe, err := cmdRg.StdoutPipe() if err != nil { return nil, fmt.Errorf("failed to get rg stdout pipe: %w", err) } defer rgPipe.Close() cmdFzf.Stdin = rgPipe var fzfOut bytes.Buffer var fzfErr bytes.Buffer cmdFzf.Stdout = &fzfOut cmdFzf.Stderr = &fzfErr if err := cmdFzf.Start(); err != nil { return nil, fmt.Errorf("failed to start fzf: %w", err) } errRg := cmdRg.Run() errFzf := cmdFzf.Wait() if errRg != nil { logging.Warn(fmt.Sprintf("rg command failed during pipe: %v", errRg)) } if errFzf != nil { if exitErr, ok := errFzf.(*exec.ExitError); ok && exitErr.ExitCode() == 1 { return []string{}, nil // No matches from fzf } return nil, fmt.Errorf("fzf command failed: %w\nStderr: %s", errFzf, fzfErr.String()) } matches = processNullTerminatedOutput(fzfOut.Bytes()) // Case 2: Only rg available } else if cmdRg != nil { logging.Debug("Using Ripgrep with fuzzy match fallback for file completions") var rgOut bytes.Buffer var rgErr bytes.Buffer cmdRg.Stdout = &rgOut cmdRg.Stderr = &rgErr if err := cmdRg.Run(); err != nil { return nil, fmt.Errorf("rg command failed: %w\nStderr: %s", err, rgErr.String()) } allFiles := processNullTerminatedOutput(rgOut.Bytes()) matches = fuzzy.Find(query, allFiles) // Case 3: Only fzf available } else if cmdFzf != nil { logging.Debug("Using FZF with doublestar fallback for file completions") files, _, err := fileutil.GlobWithDoublestar("**/*", ".", 0) if err != nil { return nil, fmt.Errorf("failed to list files for fzf: %w", err) } allFiles := make([]string, 0, len(files)) for _, file := range files { if !fileutil.SkipHidden(file) { allFiles = append(allFiles, file) } } var fzfIn bytes.Buffer for _, file := range allFiles { fzfIn.WriteString(file) fzfIn.WriteByte(0) } cmdFzf.Stdin = &fzfIn var fzfOut bytes.Buffer var fzfErr bytes.Buffer cmdFzf.Stdout = &fzfOut cmdFzf.Stderr = &fzfErr if err := cmdFzf.Run(); err != nil { if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 1 { return []string{}, nil } return nil, fmt.Errorf("fzf command failed: %w\nStderr: %s", err, fzfErr.String()) } matches = processNullTerminatedOutput(fzfOut.Bytes()) // Case 4: Fallback to doublestar with fuzzy match } else { logging.Debug("Using doublestar with fuzzy match for file completions") allFiles, _, err := fileutil.GlobWithDoublestar("**/*", ".", 0) if err != nil { return nil, fmt.Errorf("failed to glob files: %w", err) } filteredFiles := make([]string, 0, len(allFiles)) for _, file := range allFiles { if !fileutil.SkipHidden(file) { filteredFiles = append(filteredFiles, file) } } matches = fuzzy.Find(query, filteredFiles) } return matches, nil } func (cg *filesAndFoldersContextGroup) GetChildEntries(query string) ([]dialog.CompletionItemI, error) { matches, err := cg.getFiles(query) if err != nil { return nil, err } items := make([]dialog.CompletionItemI, 0, len(matches)) for _, file := range matches { item := dialog.NewCompletionItem(dialog.CompletionItem{ Title: file, Value: file, }) items = append(items, item) } return items, nil } func NewFileAndFolderContextGroup() dialog.CompletionProvider { return &filesAndFoldersContextGroup{ prefix: "file", } } ``` ## /internal/config/config.go ```go path="/internal/config/config.go" // Package config manages application configuration from various sources. package config import ( "encoding/json" "fmt" "log/slog" "os" "path/filepath" "strings" "github.com/opencode-ai/opencode/internal/llm/models" "github.com/opencode-ai/opencode/internal/logging" "github.com/spf13/viper" ) // MCPType defines the type of MCP (Model Control Protocol) server. type MCPType string // Supported MCP types const ( MCPStdio MCPType = "stdio" MCPSse MCPType = "sse" ) // MCPServer defines the configuration for a Model Control Protocol server. type MCPServer struct { Command string `json:"command"` Env []string `json:"env"` Args []string `json:"args"` Type MCPType `json:"type"` URL string `json:"url"` Headers map[string]string `json:"headers"` } type AgentName string const ( AgentCoder AgentName = "coder" AgentSummarizer AgentName = "summarizer" AgentTask AgentName = "task" AgentTitle AgentName = "title" ) // Agent defines configuration for different LLM models and their token limits. type Agent struct { Model models.ModelID `json:"model"` MaxTokens int64 `json:"maxTokens"` ReasoningEffort string `json:"reasoningEffort"` // For openai models low,medium,heigh } // Provider defines configuration for an LLM provider. type Provider struct { APIKey string `json:"apiKey"` Disabled bool `json:"disabled"` } // Data defines storage configuration. type Data struct { Directory string `json:"directory,omitempty"` } // LSPConfig defines configuration for Language Server Protocol integration. type LSPConfig struct { Disabled bool `json:"enabled"` Command string `json:"command"` Args []string `json:"args"` Options any `json:"options"` } // TUIConfig defines the configuration for the Terminal User Interface. type TUIConfig struct { Theme string `json:"theme,omitempty"` } // ShellConfig defines the configuration for the shell used by the bash tool. type ShellConfig struct { Path string `json:"path,omitempty"` Args []string `json:"args,omitempty"` } // Config is the main configuration structure for the application. type Config struct { Data Data `json:"data"` WorkingDir string `json:"wd,omitempty"` MCPServers map[string]MCPServer `json:"mcpServers,omitempty"` Providers map[models.ModelProvider]Provider `json:"providers,omitempty"` LSP map[string]LSPConfig `json:"lsp,omitempty"` Agents map[AgentName]Agent `json:"agents,omitempty"` Debug bool `json:"debug,omitempty"` DebugLSP bool `json:"debugLSP,omitempty"` ContextPaths []string `json:"contextPaths,omitempty"` TUI TUIConfig `json:"tui"` Shell ShellConfig `json:"shell,omitempty"` AutoCompact bool `json:"autoCompact,omitempty"` } // Application constants const ( defaultDataDirectory = ".opencode" defaultLogLevel = "info" appName = "opencode" MaxTokensFallbackDefault = 4096 ) var defaultContextPaths = []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", } // Global configuration instance var cfg *Config // Load initializes the configuration from environment variables and config files. // If debug is true, debug mode is enabled and log level is set to debug. // It returns an error if configuration loading fails. func Load(workingDir string, debug bool) (*Config, error) { if cfg != nil { return cfg, nil } cfg = &Config{ WorkingDir: workingDir, MCPServers: make(map[string]MCPServer), Providers: make(map[models.ModelProvider]Provider), LSP: make(map[string]LSPConfig), } configureViper() setDefaults(debug) // Read global config if err := readConfig(viper.ReadInConfig()); err != nil { return cfg, err } // Load and merge local config mergeLocalConfig(workingDir) setProviderDefaults() // Apply configuration to the struct if err := viper.Unmarshal(cfg); err != nil { return cfg, fmt.Errorf("failed to unmarshal config: %w", err) } applyDefaultValues() defaultLevel := slog.LevelInfo if cfg.Debug { defaultLevel = slog.LevelDebug } if os.Getenv("OPENCODE_DEV_DEBUG") == "true" { loggingFile := fmt.Sprintf("%s/%s", cfg.Data.Directory, "debug.log") // if file does not exist create it if _, err := os.Stat(loggingFile); os.IsNotExist(err) { if err := os.MkdirAll(cfg.Data.Directory, 0o755); err != nil { return cfg, fmt.Errorf("failed to create directory: %w", err) } if _, err := os.Create(loggingFile); err != nil { return cfg, fmt.Errorf("failed to create log file: %w", err) } } sloggingFileWriter, err := os.OpenFile(loggingFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o666) if err != nil { return cfg, fmt.Errorf("failed to open log file: %w", err) } // Configure logger logger := slog.New(slog.NewTextHandler(sloggingFileWriter, &slog.HandlerOptions{ Level: defaultLevel, })) slog.SetDefault(logger) } else { // Configure logger logger := slog.New(slog.NewTextHandler(logging.NewWriter(), &slog.HandlerOptions{ Level: defaultLevel, })) slog.SetDefault(logger) } // Validate configuration if err := Validate(); err != nil { return cfg, fmt.Errorf("config validation failed: %w", err) } if cfg.Agents == nil { cfg.Agents = make(map[AgentName]Agent) } // Override the max tokens for title agent cfg.Agents[AgentTitle] = Agent{ Model: cfg.Agents[AgentTitle].Model, MaxTokens: 80, } return cfg, nil } // configureViper sets up viper's configuration paths and environment variables. func configureViper() { viper.SetConfigName(fmt.Sprintf(".%s", appName)) viper.SetConfigType("json") viper.AddConfigPath("$HOME") viper.AddConfigPath(fmt.Sprintf("$XDG_CONFIG_HOME/%s", appName)) viper.AddConfigPath(fmt.Sprintf("$HOME/.config/%s", appName)) viper.SetEnvPrefix(strings.ToUpper(appName)) viper.AutomaticEnv() } // setDefaults configures default values for configuration options. func setDefaults(debug bool) { viper.SetDefault("data.directory", defaultDataDirectory) viper.SetDefault("contextPaths", defaultContextPaths) viper.SetDefault("tui.theme", "opencode") viper.SetDefault("autoCompact", true) // Set default shell from environment or fallback to /bin/bash shellPath := os.Getenv("SHELL") if shellPath == "" { shellPath = "/bin/bash" } viper.SetDefault("shell.path", shellPath) viper.SetDefault("shell.args", []string{"-l"}) if debug { viper.SetDefault("debug", true) viper.Set("log.level", "debug") } else { viper.SetDefault("debug", false) viper.SetDefault("log.level", defaultLogLevel) } } // setProviderDefaults configures LLM provider defaults based on provider provided by // environment variables and configuration file. func setProviderDefaults() { // Set all API keys we can find in the environment if apiKey := os.Getenv("ANTHROPIC_API_KEY"); apiKey != "" { viper.SetDefault("providers.anthropic.apiKey", apiKey) } if apiKey := os.Getenv("OPENAI_API_KEY"); apiKey != "" { viper.SetDefault("providers.openai.apiKey", apiKey) } if apiKey := os.Getenv("GEMINI_API_KEY"); apiKey != "" { viper.SetDefault("providers.gemini.apiKey", apiKey) } if apiKey := os.Getenv("GROQ_API_KEY"); apiKey != "" { viper.SetDefault("providers.groq.apiKey", apiKey) } if apiKey := os.Getenv("OPENROUTER_API_KEY"); apiKey != "" { viper.SetDefault("providers.openrouter.apiKey", apiKey) } if apiKey := os.Getenv("XAI_API_KEY"); apiKey != "" { viper.SetDefault("providers.xai.apiKey", apiKey) } if apiKey := os.Getenv("AZURE_OPENAI_ENDPOINT"); apiKey != "" { // api-key may be empty when using Entra ID credentials – that's okay viper.SetDefault("providers.azure.apiKey", os.Getenv("AZURE_OPENAI_API_KEY")) } // Use this order to set the default models // 1. Anthropic // 2. OpenAI // 3. Google Gemini // 4. Groq // 5. OpenRouter // 6. AWS Bedrock // 7. Azure // 8. Google Cloud VertexAI // Anthropic configuration if key := viper.GetString("providers.anthropic.apiKey"); strings.TrimSpace(key) != "" { viper.SetDefault("agents.coder.model", models.Claude4Sonnet) viper.SetDefault("agents.summarizer.model", models.Claude4Sonnet) viper.SetDefault("agents.task.model", models.Claude4Sonnet) viper.SetDefault("agents.title.model", models.Claude4Sonnet) return } // OpenAI configuration if key := viper.GetString("providers.openai.apiKey"); strings.TrimSpace(key) != "" { viper.SetDefault("agents.coder.model", models.GPT41) viper.SetDefault("agents.summarizer.model", models.GPT41) viper.SetDefault("agents.task.model", models.GPT41Mini) viper.SetDefault("agents.title.model", models.GPT41Mini) return } // Google Gemini configuration if key := viper.GetString("providers.gemini.apiKey"); strings.TrimSpace(key) != "" { viper.SetDefault("agents.coder.model", models.Gemini25) viper.SetDefault("agents.summarizer.model", models.Gemini25) viper.SetDefault("agents.task.model", models.Gemini25Flash) viper.SetDefault("agents.title.model", models.Gemini25Flash) return } // Groq configuration if key := viper.GetString("providers.groq.apiKey"); strings.TrimSpace(key) != "" { viper.SetDefault("agents.coder.model", models.QWENQwq) viper.SetDefault("agents.summarizer.model", models.QWENQwq) viper.SetDefault("agents.task.model", models.QWENQwq) viper.SetDefault("agents.title.model", models.QWENQwq) return } // OpenRouter configuration if key := viper.GetString("providers.openrouter.apiKey"); strings.TrimSpace(key) != "" { viper.SetDefault("agents.coder.model", models.OpenRouterClaude37Sonnet) viper.SetDefault("agents.summarizer.model", models.OpenRouterClaude37Sonnet) viper.SetDefault("agents.task.model", models.OpenRouterClaude37Sonnet) viper.SetDefault("agents.title.model", models.OpenRouterClaude35Haiku) return } // XAI configuration if key := viper.GetString("providers.xai.apiKey"); strings.TrimSpace(key) != "" { viper.SetDefault("agents.coder.model", models.XAIGrok3Beta) viper.SetDefault("agents.summarizer.model", models.XAIGrok3Beta) viper.SetDefault("agents.task.model", models.XAIGrok3Beta) viper.SetDefault("agents.title.model", models.XAiGrok3MiniFastBeta) return } // AWS Bedrock configuration if hasAWSCredentials() { viper.SetDefault("agents.coder.model", models.BedrockClaude37Sonnet) viper.SetDefault("agents.summarizer.model", models.BedrockClaude37Sonnet) viper.SetDefault("agents.task.model", models.BedrockClaude37Sonnet) viper.SetDefault("agents.title.model", models.BedrockClaude37Sonnet) return } // Azure OpenAI configuration if os.Getenv("AZURE_OPENAI_ENDPOINT") != "" { viper.SetDefault("agents.coder.model", models.AzureGPT41) viper.SetDefault("agents.summarizer.model", models.AzureGPT41) viper.SetDefault("agents.task.model", models.AzureGPT41Mini) viper.SetDefault("agents.title.model", models.AzureGPT41Mini) return } // Google Cloud VertexAI configuration if hasVertexAICredentials() { viper.SetDefault("agents.coder.model", models.VertexAIGemini25) viper.SetDefault("agents.summarizer.model", models.VertexAIGemini25) viper.SetDefault("agents.task.model", models.VertexAIGemini25Flash) viper.SetDefault("agents.title.model", models.VertexAIGemini25Flash) return } } // hasAWSCredentials checks if AWS credentials are available in the environment. func hasAWSCredentials() bool { // Check for explicit AWS credentials if os.Getenv("AWS_ACCESS_KEY_ID") != "" && os.Getenv("AWS_SECRET_ACCESS_KEY") != "" { return true } // Check for AWS profile if os.Getenv("AWS_PROFILE") != "" || os.Getenv("AWS_DEFAULT_PROFILE") != "" { return true } // Check for AWS region if os.Getenv("AWS_REGION") != "" || os.Getenv("AWS_DEFAULT_REGION") != "" { return true } // Check if running on EC2 with instance profile if os.Getenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI") != "" || os.Getenv("AWS_CONTAINER_CREDENTIALS_FULL_URI") != "" { return true } return false } // hasVertexAICredentials checks if VertexAI credentials are available in the environment. func hasVertexAICredentials() bool { // Check for explicit VertexAI parameters if os.Getenv("VERTEXAI_PROJECT") != "" && os.Getenv("VERTEXAI_LOCATION") != "" { return true } // Check for Google Cloud project and location if os.Getenv("GOOGLE_CLOUD_PROJECT") != "" && (os.Getenv("GOOGLE_CLOUD_REGION") != "" || os.Getenv("GOOGLE_CLOUD_LOCATION") != "") { return true } return false } // readConfig handles the result of reading a configuration file. func readConfig(err error) error { if err == nil { return nil } // It's okay if the config file doesn't exist if _, ok := err.(viper.ConfigFileNotFoundError); ok { return nil } return fmt.Errorf("failed to read config: %w", err) } // mergeLocalConfig loads and merges configuration from the local directory. func mergeLocalConfig(workingDir string) { local := viper.New() local.SetConfigName(fmt.Sprintf(".%s", appName)) local.SetConfigType("json") local.AddConfigPath(workingDir) // Merge local config if it exists if err := local.ReadInConfig(); err == nil { viper.MergeConfigMap(local.AllSettings()) } } // applyDefaultValues sets default values for configuration fields that need processing. func applyDefaultValues() { // Set default MCP type if not specified for k, v := range cfg.MCPServers { if v.Type == "" { v.Type = MCPStdio cfg.MCPServers[k] = v } } } // It validates model IDs and providers, ensuring they are supported. func validateAgent(cfg *Config, name AgentName, agent Agent) error { // Check if model exists model, modelExists := models.SupportedModels[agent.Model] if !modelExists { logging.Warn("unsupported model configured, reverting to default", "agent", name, "configured_model", agent.Model) // Set default model based on available providers if setDefaultModelForAgent(name) { logging.Info("set default model for agent", "agent", name, "model", cfg.Agents[name].Model) } else { return fmt.Errorf("no valid provider available for agent %s", name) } return nil } // Check if provider for the model is configured provider := model.Provider providerCfg, providerExists := cfg.Providers[provider] if !providerExists { // Provider not configured, check if we have environment variables apiKey := getProviderAPIKey(provider) if apiKey == "" { logging.Warn("provider not configured for model, reverting to default", "agent", name, "model", agent.Model, "provider", provider) // Set default model based on available providers if setDefaultModelForAgent(name) { logging.Info("set default model for agent", "agent", name, "model", cfg.Agents[name].Model) } else { return fmt.Errorf("no valid provider available for agent %s", name) } } else { // Add provider with API key from environment cfg.Providers[provider] = Provider{ APIKey: apiKey, } logging.Info("added provider from environment", "provider", provider) } } else if providerCfg.Disabled || providerCfg.APIKey == "" { // Provider is disabled or has no API key logging.Warn("provider is disabled or has no API key, reverting to default", "agent", name, "model", agent.Model, "provider", provider) // Set default model based on available providers if setDefaultModelForAgent(name) { logging.Info("set default model for agent", "agent", name, "model", cfg.Agents[name].Model) } else { return fmt.Errorf("no valid provider available for agent %s", name) } } // Validate max tokens if agent.MaxTokens <= 0 { logging.Warn("invalid max tokens, setting to default", "agent", name, "model", agent.Model, "max_tokens", agent.MaxTokens) // Update the agent with default max tokens updatedAgent := cfg.Agents[name] if model.DefaultMaxTokens > 0 { updatedAgent.MaxTokens = model.DefaultMaxTokens } else { updatedAgent.MaxTokens = MaxTokensFallbackDefault } cfg.Agents[name] = updatedAgent } else if model.ContextWindow > 0 && agent.MaxTokens > model.ContextWindow/2 { // Ensure max tokens doesn't exceed half the context window (reasonable limit) logging.Warn("max tokens exceeds half the context window, adjusting", "agent", name, "model", agent.Model, "max_tokens", agent.MaxTokens, "context_window", model.ContextWindow) // Update the agent with adjusted max tokens updatedAgent := cfg.Agents[name] updatedAgent.MaxTokens = model.ContextWindow / 2 cfg.Agents[name] = updatedAgent } // Validate reasoning effort for models that support reasoning if model.CanReason && provider == models.ProviderOpenAI || provider == models.ProviderLocal { if agent.ReasoningEffort == "" { // Set default reasoning effort for models that support it logging.Info("setting default reasoning effort for model that supports reasoning", "agent", name, "model", agent.Model) // Update the agent with default reasoning effort updatedAgent := cfg.Agents[name] updatedAgent.ReasoningEffort = "medium" cfg.Agents[name] = updatedAgent } else { // Check if reasoning effort is valid (low, medium, high) effort := strings.ToLower(agent.ReasoningEffort) if effort != "low" && effort != "medium" && effort != "high" { logging.Warn("invalid reasoning effort, setting to medium", "agent", name, "model", agent.Model, "reasoning_effort", agent.ReasoningEffort) // Update the agent with valid reasoning effort updatedAgent := cfg.Agents[name] updatedAgent.ReasoningEffort = "medium" cfg.Agents[name] = updatedAgent } } } else if !model.CanReason && agent.ReasoningEffort != "" { // Model doesn't support reasoning but reasoning effort is set logging.Warn("model doesn't support reasoning but reasoning effort is set, ignoring", "agent", name, "model", agent.Model, "reasoning_effort", agent.ReasoningEffort) // Update the agent to remove reasoning effort updatedAgent := cfg.Agents[name] updatedAgent.ReasoningEffort = "" cfg.Agents[name] = updatedAgent } return nil } // Validate checks if the configuration is valid and applies defaults where needed. func Validate() error { if cfg == nil { return fmt.Errorf("config not loaded") } // Validate agent models for name, agent := range cfg.Agents { if err := validateAgent(cfg, name, agent); err != nil { return err } } // Validate providers for provider, providerCfg := range cfg.Providers { if providerCfg.APIKey == "" && !providerCfg.Disabled { logging.Warn("provider has no API key, marking as disabled", "provider", provider) providerCfg.Disabled = true cfg.Providers[provider] = providerCfg } } // Validate LSP configurations for language, lspConfig := range cfg.LSP { if lspConfig.Command == "" && !lspConfig.Disabled { logging.Warn("LSP configuration has no command, marking as disabled", "language", language) lspConfig.Disabled = true cfg.LSP[language] = lspConfig } } return nil } // getProviderAPIKey gets the API key for a provider from environment variables func getProviderAPIKey(provider models.ModelProvider) string { switch provider { case models.ProviderAnthropic: return os.Getenv("ANTHROPIC_API_KEY") case models.ProviderOpenAI: return os.Getenv("OPENAI_API_KEY") case models.ProviderGemini: return os.Getenv("GEMINI_API_KEY") case models.ProviderGROQ: return os.Getenv("GROQ_API_KEY") case models.ProviderAzure: return os.Getenv("AZURE_OPENAI_API_KEY") case models.ProviderOpenRouter: return os.Getenv("OPENROUTER_API_KEY") case models.ProviderBedrock: if hasAWSCredentials() { return "aws-credentials-available" } case models.ProviderVertexAI: if hasVertexAICredentials() { return "vertex-ai-credentials-available" } } return "" } // setDefaultModelForAgent sets a default model for an agent based on available providers func setDefaultModelForAgent(agent AgentName) bool { // Check providers in order of preference if apiKey := os.Getenv("ANTHROPIC_API_KEY"); apiKey != "" { maxTokens := int64(5000) if agent == AgentTitle { maxTokens = 80 } cfg.Agents[agent] = Agent{ Model: models.Claude37Sonnet, MaxTokens: maxTokens, } return true } if apiKey := os.Getenv("OPENAI_API_KEY"); apiKey != "" { var model models.ModelID maxTokens := int64(5000) reasoningEffort := "" switch agent { case AgentTitle: model = models.GPT41Mini maxTokens = 80 case AgentTask: model = models.GPT41Mini default: model = models.GPT41 } // Check if model supports reasoning if modelInfo, ok := models.SupportedModels[model]; ok && modelInfo.CanReason { reasoningEffort = "medium" } cfg.Agents[agent] = Agent{ Model: model, MaxTokens: maxTokens, ReasoningEffort: reasoningEffort, } return true } if apiKey := os.Getenv("OPENROUTER_API_KEY"); apiKey != "" { var model models.ModelID maxTokens := int64(5000) reasoningEffort := "" switch agent { case AgentTitle: model = models.OpenRouterClaude35Haiku maxTokens = 80 case AgentTask: model = models.OpenRouterClaude37Sonnet default: model = models.OpenRouterClaude37Sonnet } // Check if model supports reasoning if modelInfo, ok := models.SupportedModels[model]; ok && modelInfo.CanReason { reasoningEffort = "medium" } cfg.Agents[agent] = Agent{ Model: model, MaxTokens: maxTokens, ReasoningEffort: reasoningEffort, } return true } if apiKey := os.Getenv("GEMINI_API_KEY"); apiKey != "" { var model models.ModelID maxTokens := int64(5000) if agent == AgentTitle { model = models.Gemini25Flash maxTokens = 80 } else { model = models.Gemini25 } cfg.Agents[agent] = Agent{ Model: model, MaxTokens: maxTokens, } return true } if apiKey := os.Getenv("GROQ_API_KEY"); apiKey != "" { maxTokens := int64(5000) if agent == AgentTitle { maxTokens = 80 } cfg.Agents[agent] = Agent{ Model: models.QWENQwq, MaxTokens: maxTokens, } return true } if hasAWSCredentials() { maxTokens := int64(5000) if agent == AgentTitle { maxTokens = 80 } cfg.Agents[agent] = Agent{ Model: models.BedrockClaude37Sonnet, MaxTokens: maxTokens, ReasoningEffort: "medium", // Claude models support reasoning } return true } if hasVertexAICredentials() { var model models.ModelID maxTokens := int64(5000) if agent == AgentTitle { model = models.VertexAIGemini25Flash maxTokens = 80 } else { model = models.VertexAIGemini25 } cfg.Agents[agent] = Agent{ Model: model, MaxTokens: maxTokens, } return true } return false } func updateCfgFile(updateCfg func(config *Config)) error { if cfg == nil { return fmt.Errorf("config not loaded") } // Get the config file path configFile := viper.ConfigFileUsed() var configData []byte if configFile == "" { homeDir, err := os.UserHomeDir() if err != nil { return fmt.Errorf("failed to get home directory: %w", err) } configFile = filepath.Join(homeDir, fmt.Sprintf(".%s.json", appName)) logging.Info("config file not found, creating new one", "path", configFile) configData = []byte(`{}`) } else { // Read the existing config file data, err := os.ReadFile(configFile) if err != nil { return fmt.Errorf("failed to read config file: %w", err) } configData = data } // Parse the JSON var userCfg *Config if err := json.Unmarshal(configData, &userCfg); err != nil { return fmt.Errorf("failed to parse config file: %w", err) } updateCfg(userCfg) // Write the updated config back to file updatedData, err := json.MarshalIndent(userCfg, "", " ") if err != nil { return fmt.Errorf("failed to marshal config: %w", err) } if err := os.WriteFile(configFile, updatedData, 0o644); err != nil { return fmt.Errorf("failed to write config file: %w", err) } return nil } // Get returns the current configuration. // It's safe to call this function multiple times. func Get() *Config { return cfg } // WorkingDirectory returns the current working directory from the configuration. func WorkingDirectory() string { if cfg == nil { panic("config not loaded") } return cfg.WorkingDir } func UpdateAgentModel(agentName AgentName, modelID models.ModelID) error { if cfg == nil { panic("config not loaded") } existingAgentCfg := cfg.Agents[agentName] model, ok := models.SupportedModels[modelID] if !ok { return fmt.Errorf("model %s not supported", modelID) } maxTokens := existingAgentCfg.MaxTokens if model.DefaultMaxTokens > 0 { maxTokens = model.DefaultMaxTokens } newAgentCfg := Agent{ Model: modelID, MaxTokens: maxTokens, ReasoningEffort: existingAgentCfg.ReasoningEffort, } cfg.Agents[agentName] = newAgentCfg if err := validateAgent(cfg, agentName, newAgentCfg); err != nil { // revert config update on failure cfg.Agents[agentName] = existingAgentCfg return fmt.Errorf("failed to update agent model: %w", err) } return updateCfgFile(func(config *Config) { if config.Agents == nil { config.Agents = make(map[AgentName]Agent) } config.Agents[agentName] = newAgentCfg }) } // UpdateTheme updates the theme in the configuration and writes it to the config file. func UpdateTheme(themeName string) error { if cfg == nil { return fmt.Errorf("config not loaded") } // Update the in-memory config cfg.TUI.Theme = themeName // Update the file config return updateCfgFile(func(config *Config) { config.TUI.Theme = themeName }) } ``` ## /internal/config/init.go ```go path="/internal/config/init.go" package config import ( "fmt" "os" "path/filepath" ) const ( // InitFlagFilename is the name of the file that indicates whether the project has been initialized InitFlagFilename = "init" ) // ProjectInitFlag represents the initialization status for a project directory type ProjectInitFlag struct { Initialized bool `json:"initialized"` } // ShouldShowInitDialog checks if the initialization dialog should be shown for the current directory func ShouldShowInitDialog() (bool, error) { if cfg == nil { return false, fmt.Errorf("config not loaded") } // Create the flag file path flagFilePath := filepath.Join(cfg.Data.Directory, InitFlagFilename) // Check if the flag file exists _, err := os.Stat(flagFilePath) if err == nil { // File exists, don't show the dialog return false, nil } // If the error is not "file not found", return the error if !os.IsNotExist(err) { return false, fmt.Errorf("failed to check init flag file: %w", err) } // File doesn't exist, show the dialog return true, nil } // MarkProjectInitialized marks the current project as initialized func MarkProjectInitialized() error { if cfg == nil { return fmt.Errorf("config not loaded") } // Create the flag file path flagFilePath := filepath.Join(cfg.Data.Directory, InitFlagFilename) // Create an empty file to mark the project as initialized file, err := os.Create(flagFilePath) if err != nil { return fmt.Errorf("failed to create init flag file: %w", err) } defer file.Close() return nil } ``` ## /internal/db/connect.go ```go path="/internal/db/connect.go" package db import ( "database/sql" "fmt" "os" "path/filepath" _ "github.com/ncruces/go-sqlite3/driver" _ "github.com/ncruces/go-sqlite3/embed" "github.com/opencode-ai/opencode/internal/config" "github.com/opencode-ai/opencode/internal/logging" "github.com/pressly/goose/v3" ) func Connect() (*sql.DB, error) { dataDir := config.Get().Data.Directory if dataDir == "" { return nil, fmt.Errorf("data.dir is not set") } if err := os.MkdirAll(dataDir, 0o700); err != nil { return nil, fmt.Errorf("failed to create data directory: %w", err) } dbPath := filepath.Join(dataDir, "opencode.db") // Open the SQLite database db, err := sql.Open("sqlite3", dbPath) if err != nil { return nil, fmt.Errorf("failed to open database: %w", err) } // Verify connection if err = db.Ping(); err != nil { db.Close() return nil, fmt.Errorf("failed to connect to database: %w", err) } // Set pragmas for better performance pragmas := []string{ "PRAGMA foreign_keys = ON;", "PRAGMA journal_mode = WAL;", "PRAGMA page_size = 4096;", "PRAGMA cache_size = -8000;", "PRAGMA synchronous = NORMAL;", } for _, pragma := range pragmas { if _, err = db.Exec(pragma); err != nil { logging.Error("Failed to set pragma", pragma, err) } else { logging.Debug("Set pragma", "pragma", pragma) } } goose.SetBaseFS(FS) if err := goose.SetDialect("sqlite3"); err != nil { logging.Error("Failed to set dialect", "error", err) return nil, fmt.Errorf("failed to set dialect: %w", err) } if err := goose.Up(db, "migrations"); err != nil { logging.Error("Failed to apply migrations", "error", err) return nil, fmt.Errorf("failed to apply migrations: %w", err) } return db, nil } ``` ## /internal/db/db.go ```go path="/internal/db/db.go" // Code generated by sqlc. DO NOT EDIT. // versions: // sqlc v1.29.0 package db import ( "context" "database/sql" "fmt" ) type DBTX interface { ExecContext(context.Context, string, ...interface{}) (sql.Result, error) PrepareContext(context.Context, string) (*sql.Stmt, error) QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) QueryRowContext(context.Context, string, ...interface{}) *sql.Row } func New(db DBTX) *Queries { return &Queries{db: db} } func Prepare(ctx context.Context, db DBTX) (*Queries, error) { q := Queries{db: db} var err error if q.createFileStmt, err = db.PrepareContext(ctx, createFile); err != nil { return nil, fmt.Errorf("error preparing query CreateFile: %w", err) } if q.createMessageStmt, err = db.PrepareContext(ctx, createMessage); err != nil { return nil, fmt.Errorf("error preparing query CreateMessage: %w", err) } if q.createSessionStmt, err = db.PrepareContext(ctx, createSession); err != nil { return nil, fmt.Errorf("error preparing query CreateSession: %w", err) } if q.deleteFileStmt, err = db.PrepareContext(ctx, deleteFile); err != nil { return nil, fmt.Errorf("error preparing query DeleteFile: %w", err) } if q.deleteMessageStmt, err = db.PrepareContext(ctx, deleteMessage); err != nil { return nil, fmt.Errorf("error preparing query DeleteMessage: %w", err) } if q.deleteSessionStmt, err = db.PrepareContext(ctx, deleteSession); err != nil { return nil, fmt.Errorf("error preparing query DeleteSession: %w", err) } if q.deleteSessionFilesStmt, err = db.PrepareContext(ctx, deleteSessionFiles); err != nil { return nil, fmt.Errorf("error preparing query DeleteSessionFiles: %w", err) } if q.deleteSessionMessagesStmt, err = db.PrepareContext(ctx, deleteSessionMessages); err != nil { return nil, fmt.Errorf("error preparing query DeleteSessionMessages: %w", err) } if q.getFileStmt, err = db.PrepareContext(ctx, getFile); err != nil { return nil, fmt.Errorf("error preparing query GetFile: %w", err) } if q.getFileByPathAndSessionStmt, err = db.PrepareContext(ctx, getFileByPathAndSession); err != nil { return nil, fmt.Errorf("error preparing query GetFileByPathAndSession: %w", err) } if q.getMessageStmt, err = db.PrepareContext(ctx, getMessage); err != nil { return nil, fmt.Errorf("error preparing query GetMessage: %w", err) } if q.getSessionByIDStmt, err = db.PrepareContext(ctx, getSessionByID); err != nil { return nil, fmt.Errorf("error preparing query GetSessionByID: %w", err) } if q.listFilesByPathStmt, err = db.PrepareContext(ctx, listFilesByPath); err != nil { return nil, fmt.Errorf("error preparing query ListFilesByPath: %w", err) } if q.listFilesBySessionStmt, err = db.PrepareContext(ctx, listFilesBySession); err != nil { return nil, fmt.Errorf("error preparing query ListFilesBySession: %w", err) } if q.listLatestSessionFilesStmt, err = db.PrepareContext(ctx, listLatestSessionFiles); err != nil { return nil, fmt.Errorf("error preparing query ListLatestSessionFiles: %w", err) } if q.listMessagesBySessionStmt, err = db.PrepareContext(ctx, listMessagesBySession); err != nil { return nil, fmt.Errorf("error preparing query ListMessagesBySession: %w", err) } if q.listNewFilesStmt, err = db.PrepareContext(ctx, listNewFiles); err != nil { return nil, fmt.Errorf("error preparing query ListNewFiles: %w", err) } if q.listSessionsStmt, err = db.PrepareContext(ctx, listSessions); err != nil { return nil, fmt.Errorf("error preparing query ListSessions: %w", err) } if q.updateFileStmt, err = db.PrepareContext(ctx, updateFile); err != nil { return nil, fmt.Errorf("error preparing query UpdateFile: %w", err) } if q.updateMessageStmt, err = db.PrepareContext(ctx, updateMessage); err != nil { return nil, fmt.Errorf("error preparing query UpdateMessage: %w", err) } if q.updateSessionStmt, err = db.PrepareContext(ctx, updateSession); err != nil { return nil, fmt.Errorf("error preparing query UpdateSession: %w", err) } return &q, nil } func (q *Queries) Close() error { var err error if q.createFileStmt != nil { if cerr := q.createFileStmt.Close(); cerr != nil { err = fmt.Errorf("error closing createFileStmt: %w", cerr) } } if q.createMessageStmt != nil { if cerr := q.createMessageStmt.Close(); cerr != nil { err = fmt.Errorf("error closing createMessageStmt: %w", cerr) } } if q.createSessionStmt != nil { if cerr := q.createSessionStmt.Close(); cerr != nil { err = fmt.Errorf("error closing createSessionStmt: %w", cerr) } } if q.deleteFileStmt != nil { if cerr := q.deleteFileStmt.Close(); cerr != nil { err = fmt.Errorf("error closing deleteFileStmt: %w", cerr) } } if q.deleteMessageStmt != nil { if cerr := q.deleteMessageStmt.Close(); cerr != nil { err = fmt.Errorf("error closing deleteMessageStmt: %w", cerr) } } if q.deleteSessionStmt != nil { if cerr := q.deleteSessionStmt.Close(); cerr != nil { err = fmt.Errorf("error closing deleteSessionStmt: %w", cerr) } } if q.deleteSessionFilesStmt != nil { if cerr := q.deleteSessionFilesStmt.Close(); cerr != nil { err = fmt.Errorf("error closing deleteSessionFilesStmt: %w", cerr) } } if q.deleteSessionMessagesStmt != nil { if cerr := q.deleteSessionMessagesStmt.Close(); cerr != nil { err = fmt.Errorf("error closing deleteSessionMessagesStmt: %w", cerr) } } if q.getFileStmt != nil { if cerr := q.getFileStmt.Close(); cerr != nil { err = fmt.Errorf("error closing getFileStmt: %w", cerr) } } if q.getFileByPathAndSessionStmt != nil { if cerr := q.getFileByPathAndSessionStmt.Close(); cerr != nil { err = fmt.Errorf("error closing getFileByPathAndSessionStmt: %w", cerr) } } if q.getMessageStmt != nil { if cerr := q.getMessageStmt.Close(); cerr != nil { err = fmt.Errorf("error closing getMessageStmt: %w", cerr) } } if q.getSessionByIDStmt != nil { if cerr := q.getSessionByIDStmt.Close(); cerr != nil { err = fmt.Errorf("error closing getSessionByIDStmt: %w", cerr) } } if q.listFilesByPathStmt != nil { if cerr := q.listFilesByPathStmt.Close(); cerr != nil { err = fmt.Errorf("error closing listFilesByPathStmt: %w", cerr) } } if q.listFilesBySessionStmt != nil { if cerr := q.listFilesBySessionStmt.Close(); cerr != nil { err = fmt.Errorf("error closing listFilesBySessionStmt: %w", cerr) } } if q.listLatestSessionFilesStmt != nil { if cerr := q.listLatestSessionFilesStmt.Close(); cerr != nil { err = fmt.Errorf("error closing listLatestSessionFilesStmt: %w", cerr) } } if q.listMessagesBySessionStmt != nil { if cerr := q.listMessagesBySessionStmt.Close(); cerr != nil { err = fmt.Errorf("error closing listMessagesBySessionStmt: %w", cerr) } } if q.listNewFilesStmt != nil { if cerr := q.listNewFilesStmt.Close(); cerr != nil { err = fmt.Errorf("error closing listNewFilesStmt: %w", cerr) } } if q.listSessionsStmt != nil { if cerr := q.listSessionsStmt.Close(); cerr != nil { err = fmt.Errorf("error closing listSessionsStmt: %w", cerr) } } if q.updateFileStmt != nil { if cerr := q.updateFileStmt.Close(); cerr != nil { err = fmt.Errorf("error closing updateFileStmt: %w", cerr) } } if q.updateMessageStmt != nil { if cerr := q.updateMessageStmt.Close(); cerr != nil { err = fmt.Errorf("error closing updateMessageStmt: %w", cerr) } } if q.updateSessionStmt != nil { if cerr := q.updateSessionStmt.Close(); cerr != nil { err = fmt.Errorf("error closing updateSessionStmt: %w", cerr) } } return err } func (q *Queries) exec(ctx context.Context, stmt *sql.Stmt, query string, args ...interface{}) (sql.Result, error) { switch { case stmt != nil && q.tx != nil: return q.tx.StmtContext(ctx, stmt).ExecContext(ctx, args...) case stmt != nil: return stmt.ExecContext(ctx, args...) default: return q.db.ExecContext(ctx, query, args...) } } func (q *Queries) query(ctx context.Context, stmt *sql.Stmt, query string, args ...interface{}) (*sql.Rows, error) { switch { case stmt != nil && q.tx != nil: return q.tx.StmtContext(ctx, stmt).QueryContext(ctx, args...) case stmt != nil: return stmt.QueryContext(ctx, args...) default: return q.db.QueryContext(ctx, query, args...) } } func (q *Queries) queryRow(ctx context.Context, stmt *sql.Stmt, query string, args ...interface{}) *sql.Row { switch { case stmt != nil && q.tx != nil: return q.tx.StmtContext(ctx, stmt).QueryRowContext(ctx, args...) case stmt != nil: return stmt.QueryRowContext(ctx, args...) default: return q.db.QueryRowContext(ctx, query, args...) } } type Queries struct { db DBTX tx *sql.Tx createFileStmt *sql.Stmt createMessageStmt *sql.Stmt createSessionStmt *sql.Stmt deleteFileStmt *sql.Stmt deleteMessageStmt *sql.Stmt deleteSessionStmt *sql.Stmt deleteSessionFilesStmt *sql.Stmt deleteSessionMessagesStmt *sql.Stmt getFileStmt *sql.Stmt getFileByPathAndSessionStmt *sql.Stmt getMessageStmt *sql.Stmt getSessionByIDStmt *sql.Stmt listFilesByPathStmt *sql.Stmt listFilesBySessionStmt *sql.Stmt listLatestSessionFilesStmt *sql.Stmt listMessagesBySessionStmt *sql.Stmt listNewFilesStmt *sql.Stmt listSessionsStmt *sql.Stmt updateFileStmt *sql.Stmt updateMessageStmt *sql.Stmt updateSessionStmt *sql.Stmt } func (q *Queries) WithTx(tx *sql.Tx) *Queries { return &Queries{ db: tx, tx: tx, createFileStmt: q.createFileStmt, createMessageStmt: q.createMessageStmt, createSessionStmt: q.createSessionStmt, deleteFileStmt: q.deleteFileStmt, deleteMessageStmt: q.deleteMessageStmt, deleteSessionStmt: q.deleteSessionStmt, deleteSessionFilesStmt: q.deleteSessionFilesStmt, deleteSessionMessagesStmt: q.deleteSessionMessagesStmt, getFileStmt: q.getFileStmt, getFileByPathAndSessionStmt: q.getFileByPathAndSessionStmt, getMessageStmt: q.getMessageStmt, getSessionByIDStmt: q.getSessionByIDStmt, listFilesByPathStmt: q.listFilesByPathStmt, listFilesBySessionStmt: q.listFilesBySessionStmt, listLatestSessionFilesStmt: q.listLatestSessionFilesStmt, listMessagesBySessionStmt: q.listMessagesBySessionStmt, listNewFilesStmt: q.listNewFilesStmt, listSessionsStmt: q.listSessionsStmt, updateFileStmt: q.updateFileStmt, updateMessageStmt: q.updateMessageStmt, updateSessionStmt: q.updateSessionStmt, } } ``` ## /internal/db/embed.go ```go path="/internal/db/embed.go" package db import "embed" //go:embed migrations/*.sql var FS embed.FS ``` ## /internal/db/files.sql.go ```go path="/internal/db/files.sql.go" // Code generated by sqlc. DO NOT EDIT. // versions: // sqlc v1.29.0 // source: files.sql package db import ( "context" ) const createFile = `-- name: CreateFile :one INSERT INTO files ( id, session_id, path, content, version, created_at, updated_at ) VALUES ( ?, ?, ?, ?, ?, strftime('%s', 'now'), strftime('%s', 'now') ) RETURNING id, session_id, path, content, version, created_at, updated_at ` type CreateFileParams struct { ID string `json:"id"` SessionID string `json:"session_id"` Path string `json:"path"` Content string `json:"content"` Version string `json:"version"` } func (q *Queries) CreateFile(ctx context.Context, arg CreateFileParams) (File, error) { row := q.queryRow(ctx, q.createFileStmt, createFile, arg.ID, arg.SessionID, arg.Path, arg.Content, arg.Version, ) var i File err := row.Scan( &i.ID, &i.SessionID, &i.Path, &i.Content, &i.Version, &i.CreatedAt, &i.UpdatedAt, ) return i, err } const deleteFile = `-- name: DeleteFile :exec DELETE FROM files WHERE id = ? ` func (q *Queries) DeleteFile(ctx context.Context, id string) error { _, err := q.exec(ctx, q.deleteFileStmt, deleteFile, id) return err } const deleteSessionFiles = `-- name: DeleteSessionFiles :exec DELETE FROM files WHERE session_id = ? ` func (q *Queries) DeleteSessionFiles(ctx context.Context, sessionID string) error { _, err := q.exec(ctx, q.deleteSessionFilesStmt, deleteSessionFiles, sessionID) return err } const getFile = `-- name: GetFile :one SELECT id, session_id, path, content, version, created_at, updated_at FROM files WHERE id = ? LIMIT 1 ` func (q *Queries) GetFile(ctx context.Context, id string) (File, error) { row := q.queryRow(ctx, q.getFileStmt, getFile, id) var i File err := row.Scan( &i.ID, &i.SessionID, &i.Path, &i.Content, &i.Version, &i.CreatedAt, &i.UpdatedAt, ) return i, err } const getFileByPathAndSession = `-- name: GetFileByPathAndSession :one SELECT id, session_id, path, content, version, created_at, updated_at FROM files WHERE path = ? AND session_id = ? ORDER BY created_at DESC LIMIT 1 ` type GetFileByPathAndSessionParams struct { Path string `json:"path"` SessionID string `json:"session_id"` } func (q *Queries) GetFileByPathAndSession(ctx context.Context, arg GetFileByPathAndSessionParams) (File, error) { row := q.queryRow(ctx, q.getFileByPathAndSessionStmt, getFileByPathAndSession, arg.Path, arg.SessionID) var i File err := row.Scan( &i.ID, &i.SessionID, &i.Path, &i.Content, &i.Version, &i.CreatedAt, &i.UpdatedAt, ) return i, err } const listFilesByPath = `-- name: ListFilesByPath :many SELECT id, session_id, path, content, version, created_at, updated_at FROM files WHERE path = ? ORDER BY created_at DESC ` func (q *Queries) ListFilesByPath(ctx context.Context, path string) ([]File, error) { rows, err := q.query(ctx, q.listFilesByPathStmt, listFilesByPath, path) if err != nil { return nil, err } defer rows.Close() items := []File{} for rows.Next() { var i File if err := rows.Scan( &i.ID, &i.SessionID, &i.Path, &i.Content, &i.Version, &i.CreatedAt, &i.UpdatedAt, ); err != nil { return nil, err } items = append(items, i) } if err := rows.Close(); err != nil { return nil, err } if err := rows.Err(); err != nil { return nil, err } return items, nil } const listFilesBySession = `-- name: ListFilesBySession :many SELECT id, session_id, path, content, version, created_at, updated_at FROM files WHERE session_id = ? ORDER BY created_at ASC ` func (q *Queries) ListFilesBySession(ctx context.Context, sessionID string) ([]File, error) { rows, err := q.query(ctx, q.listFilesBySessionStmt, listFilesBySession, sessionID) if err != nil { return nil, err } defer rows.Close() items := []File{} for rows.Next() { var i File if err := rows.Scan( &i.ID, &i.SessionID, &i.Path, &i.Content, &i.Version, &i.CreatedAt, &i.UpdatedAt, ); err != nil { return nil, err } items = append(items, i) } if err := rows.Close(); err != nil { return nil, err } if err := rows.Err(); err != nil { return nil, err } return items, nil } const listLatestSessionFiles = `-- name: ListLatestSessionFiles :many SELECT f.id, f.session_id, f.path, f.content, f.version, f.created_at, f.updated_at FROM files f INNER JOIN ( SELECT path, MAX(created_at) as max_created_at FROM files GROUP BY path ) latest ON f.path = latest.path AND f.created_at = latest.max_created_at WHERE f.session_id = ? ORDER BY f.path ` func (q *Queries) ListLatestSessionFiles(ctx context.Context, sessionID string) ([]File, error) { rows, err := q.query(ctx, q.listLatestSessionFilesStmt, listLatestSessionFiles, sessionID) if err != nil { return nil, err } defer rows.Close() items := []File{} for rows.Next() { var i File if err := rows.Scan( &i.ID, &i.SessionID, &i.Path, &i.Content, &i.Version, &i.CreatedAt, &i.UpdatedAt, ); err != nil { return nil, err } items = append(items, i) } if err := rows.Close(); err != nil { return nil, err } if err := rows.Err(); err != nil { return nil, err } return items, nil } const listNewFiles = `-- name: ListNewFiles :many SELECT id, session_id, path, content, version, created_at, updated_at FROM files WHERE is_new = 1 ORDER BY created_at DESC ` func (q *Queries) ListNewFiles(ctx context.Context) ([]File, error) { rows, err := q.query(ctx, q.listNewFilesStmt, listNewFiles) if err != nil { return nil, err } defer rows.Close() items := []File{} for rows.Next() { var i File if err := rows.Scan( &i.ID, &i.SessionID, &i.Path, &i.Content, &i.Version, &i.CreatedAt, &i.UpdatedAt, ); err != nil { return nil, err } items = append(items, i) } if err := rows.Close(); err != nil { return nil, err } if err := rows.Err(); err != nil { return nil, err } return items, nil } const updateFile = `-- name: UpdateFile :one UPDATE files SET content = ?, version = ?, updated_at = strftime('%s', 'now') WHERE id = ? RETURNING id, session_id, path, content, version, created_at, updated_at ` type UpdateFileParams struct { Content string `json:"content"` Version string `json:"version"` ID string `json:"id"` } func (q *Queries) UpdateFile(ctx context.Context, arg UpdateFileParams) (File, error) { row := q.queryRow(ctx, q.updateFileStmt, updateFile, arg.Content, arg.Version, arg.ID) var i File err := row.Scan( &i.ID, &i.SessionID, &i.Path, &i.Content, &i.Version, &i.CreatedAt, &i.UpdatedAt, ) return i, err } ``` ## /internal/db/messages.sql.go ```go path="/internal/db/messages.sql.go" // Code generated by sqlc. DO NOT EDIT. // versions: // sqlc v1.29.0 // source: messages.sql package db import ( "context" "database/sql" ) const createMessage = `-- name: CreateMessage :one INSERT INTO messages ( id, session_id, role, parts, model, created_at, updated_at ) VALUES ( ?, ?, ?, ?, ?, strftime('%s', 'now'), strftime('%s', 'now') ) RETURNING id, session_id, role, parts, model, created_at, updated_at, finished_at ` type CreateMessageParams struct { ID string `json:"id"` SessionID string `json:"session_id"` Role string `json:"role"` Parts string `json:"parts"` Model sql.NullString `json:"model"` } func (q *Queries) CreateMessage(ctx context.Context, arg CreateMessageParams) (Message, error) { row := q.queryRow(ctx, q.createMessageStmt, createMessage, arg.ID, arg.SessionID, arg.Role, arg.Parts, arg.Model, ) var i Message err := row.Scan( &i.ID, &i.SessionID, &i.Role, &i.Parts, &i.Model, &i.CreatedAt, &i.UpdatedAt, &i.FinishedAt, ) return i, err } const deleteMessage = `-- name: DeleteMessage :exec DELETE FROM messages WHERE id = ? ` func (q *Queries) DeleteMessage(ctx context.Context, id string) error { _, err := q.exec(ctx, q.deleteMessageStmt, deleteMessage, id) return err } const deleteSessionMessages = `-- name: DeleteSessionMessages :exec DELETE FROM messages WHERE session_id = ? ` func (q *Queries) DeleteSessionMessages(ctx context.Context, sessionID string) error { _, err := q.exec(ctx, q.deleteSessionMessagesStmt, deleteSessionMessages, sessionID) return err } const getMessage = `-- name: GetMessage :one SELECT id, session_id, role, parts, model, created_at, updated_at, finished_at FROM messages WHERE id = ? LIMIT 1 ` func (q *Queries) GetMessage(ctx context.Context, id string) (Message, error) { row := q.queryRow(ctx, q.getMessageStmt, getMessage, id) var i Message err := row.Scan( &i.ID, &i.SessionID, &i.Role, &i.Parts, &i.Model, &i.CreatedAt, &i.UpdatedAt, &i.FinishedAt, ) return i, err } const listMessagesBySession = `-- name: ListMessagesBySession :many SELECT id, session_id, role, parts, model, created_at, updated_at, finished_at FROM messages WHERE session_id = ? ORDER BY created_at ASC ` func (q *Queries) ListMessagesBySession(ctx context.Context, sessionID string) ([]Message, error) { rows, err := q.query(ctx, q.listMessagesBySessionStmt, listMessagesBySession, sessionID) if err != nil { return nil, err } defer rows.Close() items := []Message{} for rows.Next() { var i Message if err := rows.Scan( &i.ID, &i.SessionID, &i.Role, &i.Parts, &i.Model, &i.CreatedAt, &i.UpdatedAt, &i.FinishedAt, ); err != nil { return nil, err } items = append(items, i) } if err := rows.Close(); err != nil { return nil, err } if err := rows.Err(); err != nil { return nil, err } return items, nil } const updateMessage = `-- name: UpdateMessage :exec UPDATE messages SET parts = ?, finished_at = ?, updated_at = strftime('%s', 'now') WHERE id = ? ` type UpdateMessageParams struct { Parts string `json:"parts"` FinishedAt sql.NullInt64 `json:"finished_at"` ID string `json:"id"` } func (q *Queries) UpdateMessage(ctx context.Context, arg UpdateMessageParams) error { _, err := q.exec(ctx, q.updateMessageStmt, updateMessage, arg.Parts, arg.FinishedAt, arg.ID) return err } ``` ## /internal/db/migrations/20250424200609_initial.sql ```sql path="/internal/db/migrations/20250424200609_initial.sql" -- +goose Up -- +goose StatementBegin -- Sessions CREATE TABLE IF NOT EXISTS sessions ( id TEXT PRIMARY KEY, parent_session_id TEXT, title TEXT NOT NULL, message_count INTEGER NOT NULL DEFAULT 0 CHECK (message_count >= 0), prompt_tokens INTEGER NOT NULL DEFAULT 0 CHECK (prompt_tokens >= 0), completion_tokens INTEGER NOT NULL DEFAULT 0 CHECK (completion_tokens>= 0), cost REAL NOT NULL DEFAULT 0.0 CHECK (cost >= 0.0), updated_at INTEGER NOT NULL, -- Unix timestamp in milliseconds created_at INTEGER NOT NULL -- Unix timestamp in milliseconds ); CREATE TRIGGER IF NOT EXISTS update_sessions_updated_at AFTER UPDATE ON sessions BEGIN UPDATE sessions SET updated_at = strftime('%s', 'now') WHERE id = new.id; END; -- Files CREATE TABLE IF NOT EXISTS files ( id TEXT PRIMARY KEY, session_id TEXT NOT NULL, path TEXT NOT NULL, content TEXT NOT NULL, version TEXT NOT NULL, created_at INTEGER NOT NULL, -- Unix timestamp in milliseconds updated_at INTEGER NOT NULL, -- Unix timestamp in milliseconds FOREIGN KEY (session_id) REFERENCES sessions (id) ON DELETE CASCADE, UNIQUE(path, session_id, version) ); CREATE INDEX IF NOT EXISTS idx_files_session_id ON files (session_id); CREATE INDEX IF NOT EXISTS idx_files_path ON files (path); CREATE TRIGGER IF NOT EXISTS update_files_updated_at AFTER UPDATE ON files BEGIN UPDATE files SET updated_at = strftime('%s', 'now') WHERE id = new.id; END; -- Messages CREATE TABLE IF NOT EXISTS messages ( id TEXT PRIMARY KEY, session_id TEXT NOT NULL, role TEXT NOT NULL, parts TEXT NOT NULL default '[]', model TEXT, created_at INTEGER NOT NULL, -- Unix timestamp in milliseconds updated_at INTEGER NOT NULL, -- Unix timestamp in milliseconds finished_at INTEGER, -- Unix timestamp in milliseconds FOREIGN KEY (session_id) REFERENCES sessions (id) ON DELETE CASCADE ); CREATE INDEX IF NOT EXISTS idx_messages_session_id ON messages (session_id); CREATE TRIGGER IF NOT EXISTS update_messages_updated_at AFTER UPDATE ON messages BEGIN UPDATE messages SET updated_at = strftime('%s', 'now') WHERE id = new.id; END; CREATE TRIGGER IF NOT EXISTS update_session_message_count_on_insert AFTER INSERT ON messages BEGIN UPDATE sessions SET message_count = message_count + 1 WHERE id = new.session_id; END; CREATE TRIGGER IF NOT EXISTS update_session_message_count_on_delete AFTER DELETE ON messages BEGIN UPDATE sessions SET message_count = message_count - 1 WHERE id = old.session_id; END; -- +goose StatementEnd -- +goose Down -- +goose StatementBegin DROP TRIGGER IF EXISTS update_sessions_updated_at; DROP TRIGGER IF EXISTS update_messages_updated_at; DROP TRIGGER IF EXISTS update_files_updated_at; DROP TRIGGER IF EXISTS update_session_message_count_on_delete; DROP TRIGGER IF EXISTS update_session_message_count_on_insert; DROP TABLE IF EXISTS sessions; DROP TABLE IF EXISTS messages; DROP TABLE IF EXISTS files; -- +goose StatementEnd ``` ## /internal/db/migrations/20250515105448_add_summary_message_id.sql ```sql path="/internal/db/migrations/20250515105448_add_summary_message_id.sql" -- +goose Up -- +goose StatementBegin ALTER TABLE sessions ADD COLUMN summary_message_id TEXT; -- +goose StatementEnd -- +goose Down -- +goose StatementBegin ALTER TABLE sessions DROP COLUMN summary_message_id; -- +goose StatementEnd ``` ## /internal/db/models.go ```go path="/internal/db/models.go" // Code generated by sqlc. DO NOT EDIT. // versions: // sqlc v1.29.0 package db import ( "database/sql" ) type File struct { ID string `json:"id"` SessionID string `json:"session_id"` Path string `json:"path"` Content string `json:"content"` Version string `json:"version"` CreatedAt int64 `json:"created_at"` UpdatedAt int64 `json:"updated_at"` } type Message struct { ID string `json:"id"` SessionID string `json:"session_id"` Role string `json:"role"` Parts string `json:"parts"` Model sql.NullString `json:"model"` CreatedAt int64 `json:"created_at"` UpdatedAt int64 `json:"updated_at"` FinishedAt sql.NullInt64 `json:"finished_at"` } type Session struct { ID string `json:"id"` ParentSessionID sql.NullString `json:"parent_session_id"` Title string `json:"title"` MessageCount int64 `json:"message_count"` PromptTokens int64 `json:"prompt_tokens"` CompletionTokens int64 `json:"completion_tokens"` Cost float64 `json:"cost"` UpdatedAt int64 `json:"updated_at"` CreatedAt int64 `json:"created_at"` SummaryMessageID sql.NullString `json:"summary_message_id"` } ``` ## /internal/db/querier.go ```go path="/internal/db/querier.go" // Code generated by sqlc. DO NOT EDIT. // versions: // sqlc v1.29.0 package db import ( "context" ) type Querier interface { CreateFile(ctx context.Context, arg CreateFileParams) (File, error) CreateMessage(ctx context.Context, arg CreateMessageParams) (Message, error) CreateSession(ctx context.Context, arg CreateSessionParams) (Session, error) DeleteFile(ctx context.Context, id string) error DeleteMessage(ctx context.Context, id string) error DeleteSession(ctx context.Context, id string) error DeleteSessionFiles(ctx context.Context, sessionID string) error DeleteSessionMessages(ctx context.Context, sessionID string) error GetFile(ctx context.Context, id string) (File, error) GetFileByPathAndSession(ctx context.Context, arg GetFileByPathAndSessionParams) (File, error) GetMessage(ctx context.Context, id string) (Message, error) GetSessionByID(ctx context.Context, id string) (Session, error) ListFilesByPath(ctx context.Context, path string) ([]File, error) ListFilesBySession(ctx context.Context, sessionID string) ([]File, error) ListLatestSessionFiles(ctx context.Context, sessionID string) ([]File, error) ListMessagesBySession(ctx context.Context, sessionID string) ([]Message, error) ListNewFiles(ctx context.Context) ([]File, error) ListSessions(ctx context.Context) ([]Session, error) UpdateFile(ctx context.Context, arg UpdateFileParams) (File, error) UpdateMessage(ctx context.Context, arg UpdateMessageParams) error UpdateSession(ctx context.Context, arg UpdateSessionParams) (Session, error) } var _ Querier = (*Queries)(nil) ``` ## /internal/db/sessions.sql.go ```go path="/internal/db/sessions.sql.go" // Code generated by sqlc. DO NOT EDIT. // versions: // sqlc v1.29.0 // source: sessions.sql package db import ( "context" "database/sql" ) const createSession = `-- name: CreateSession :one INSERT INTO sessions ( id, parent_session_id, title, message_count, prompt_tokens, completion_tokens, cost, summary_message_id, updated_at, created_at ) VALUES ( ?, ?, ?, ?, ?, ?, ?, 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 ` type CreateSessionParams struct { ID string `json:"id"` ParentSessionID sql.NullString `json:"parent_session_id"` Title string `json:"title"` MessageCount int64 `json:"message_count"` PromptTokens int64 `json:"prompt_tokens"` CompletionTokens int64 `json:"completion_tokens"` Cost float64 `json:"cost"` } func (q *Queries) CreateSession(ctx context.Context, arg CreateSessionParams) (Session, error) { row := q.queryRow(ctx, q.createSessionStmt, createSession, arg.ID, arg.ParentSessionID, arg.Title, arg.MessageCount, arg.PromptTokens, arg.CompletionTokens, arg.Cost, ) var i Session err := row.Scan( &i.ID, &i.ParentSessionID, &i.Title, &i.MessageCount, &i.PromptTokens, &i.CompletionTokens, &i.Cost, &i.UpdatedAt, &i.CreatedAt, &i.SummaryMessageID, ) return i, err } const deleteSession = `-- name: DeleteSession :exec DELETE FROM sessions WHERE id = ? ` func (q *Queries) DeleteSession(ctx context.Context, id string) error { _, err := q.exec(ctx, q.deleteSessionStmt, deleteSession, id) return err } 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 FROM sessions WHERE id = ? LIMIT 1 ` func (q *Queries) GetSessionByID(ctx context.Context, id string) (Session, error) { row := q.queryRow(ctx, q.getSessionByIDStmt, getSessionByID, id) var i Session err := row.Scan( &i.ID, &i.ParentSessionID, &i.Title, &i.MessageCount, &i.PromptTokens, &i.CompletionTokens, &i.Cost, &i.UpdatedAt, &i.CreatedAt, &i.SummaryMessageID, ) 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 FROM sessions WHERE parent_session_id is NULL ORDER BY created_at DESC ` func (q *Queries) ListSessions(ctx context.Context) ([]Session, error) { rows, err := q.query(ctx, q.listSessionsStmt, listSessions) if err != nil { return nil, err } defer rows.Close() items := []Session{} for rows.Next() { var i Session if err := rows.Scan( &i.ID, &i.ParentSessionID, &i.Title, &i.MessageCount, &i.PromptTokens, &i.CompletionTokens, &i.Cost, &i.UpdatedAt, &i.CreatedAt, &i.SummaryMessageID, ); err != nil { return nil, err } items = append(items, i) } if err := rows.Close(); err != nil { return nil, err } if err := rows.Err(); err != nil { return nil, err } return items, nil } const updateSession = `-- name: UpdateSession :one UPDATE sessions SET title = ?, prompt_tokens = ?, completion_tokens = ?, summary_message_id = ?, cost = ? WHERE id = ? RETURNING id, parent_session_id, title, message_count, prompt_tokens, completion_tokens, cost, updated_at, created_at, summary_message_id ` type UpdateSessionParams struct { Title string `json:"title"` PromptTokens int64 `json:"prompt_tokens"` CompletionTokens int64 `json:"completion_tokens"` SummaryMessageID sql.NullString `json:"summary_message_id"` Cost float64 `json:"cost"` ID string `json:"id"` } func (q *Queries) UpdateSession(ctx context.Context, arg UpdateSessionParams) (Session, error) { row := q.queryRow(ctx, q.updateSessionStmt, updateSession, arg.Title, arg.PromptTokens, arg.CompletionTokens, arg.SummaryMessageID, arg.Cost, arg.ID, ) var i Session err := row.Scan( &i.ID, &i.ParentSessionID, &i.Title, &i.MessageCount, &i.PromptTokens, &i.CompletionTokens, &i.Cost, &i.UpdatedAt, &i.CreatedAt, &i.SummaryMessageID, ) return i, err } ``` ## /internal/db/sql/files.sql ```sql path="/internal/db/sql/files.sql" -- name: GetFile :one SELECT * FROM files WHERE id = ? LIMIT 1; -- name: GetFileByPathAndSession :one SELECT * FROM files WHERE path = ? AND session_id = ? ORDER BY created_at DESC LIMIT 1; -- name: ListFilesBySession :many SELECT * FROM files WHERE session_id = ? ORDER BY created_at ASC; -- name: ListFilesByPath :many SELECT * FROM files WHERE path = ? ORDER BY created_at DESC; -- name: CreateFile :one INSERT INTO files ( id, session_id, path, content, version, created_at, updated_at ) VALUES ( ?, ?, ?, ?, ?, strftime('%s', 'now'), strftime('%s', 'now') ) RETURNING *; -- name: UpdateFile :one UPDATE files SET content = ?, version = ?, updated_at = strftime('%s', 'now') WHERE id = ? RETURNING *; -- name: DeleteFile :exec DELETE FROM files WHERE id = ?; -- name: DeleteSessionFiles :exec DELETE FROM files WHERE session_id = ?; -- name: ListLatestSessionFiles :many SELECT f.* FROM files f INNER JOIN ( SELECT path, MAX(created_at) as max_created_at FROM files GROUP BY path ) latest ON f.path = latest.path AND f.created_at = latest.max_created_at WHERE f.session_id = ? ORDER BY f.path; -- name: ListNewFiles :many SELECT * FROM files WHERE is_new = 1 ORDER BY created_at DESC; ``` ## /internal/db/sql/messages.sql ```sql path="/internal/db/sql/messages.sql" -- name: GetMessage :one SELECT * FROM messages WHERE id = ? LIMIT 1; -- name: ListMessagesBySession :many SELECT * FROM messages WHERE session_id = ? ORDER BY created_at ASC; -- name: CreateMessage :one INSERT INTO messages ( id, session_id, role, parts, model, created_at, updated_at ) VALUES ( ?, ?, ?, ?, ?, strftime('%s', 'now'), strftime('%s', 'now') ) RETURNING *; -- name: UpdateMessage :exec UPDATE messages SET parts = ?, finished_at = ?, updated_at = strftime('%s', 'now') WHERE id = ?; -- name: DeleteMessage :exec DELETE FROM messages WHERE id = ?; -- name: DeleteSessionMessages :exec DELETE FROM messages WHERE session_id = ?; ``` ## /internal/db/sql/sessions.sql ```sql path="/internal/db/sql/sessions.sql" -- name: CreateSession :one INSERT INTO sessions ( id, parent_session_id, title, message_count, prompt_tokens, completion_tokens, cost, summary_message_id, updated_at, created_at ) VALUES ( ?, ?, ?, ?, ?, ?, ?, null, strftime('%s', 'now'), strftime('%s', 'now') ) RETURNING *; -- name: GetSessionByID :one SELECT * FROM sessions WHERE id = ? LIMIT 1; -- name: ListSessions :many SELECT * FROM sessions WHERE parent_session_id is NULL ORDER BY created_at DESC; -- name: UpdateSession :one UPDATE sessions SET title = ?, prompt_tokens = ?, completion_tokens = ?, summary_message_id = ?, cost = ? WHERE id = ? RETURNING *; -- name: DeleteSession :exec DELETE FROM sessions WHERE id = ?; ``` ## /internal/diff/diff.go ```go path="/internal/diff/diff.go" package diff import ( "bytes" "fmt" "io" "regexp" "strconv" "strings" "github.com/alecthomas/chroma/v2" "github.com/alecthomas/chroma/v2/formatters" "github.com/alecthomas/chroma/v2/lexers" "github.com/alecthomas/chroma/v2/styles" "github.com/aymanbagabas/go-udiff" "github.com/charmbracelet/lipgloss" "github.com/charmbracelet/x/ansi" "github.com/opencode-ai/opencode/internal/config" "github.com/opencode-ai/opencode/internal/tui/theme" "github.com/sergi/go-diff/diffmatchpatch" ) // ------------------------------------------------------------------------- // Core Types // ------------------------------------------------------------------------- // LineType represents the kind of line in a diff. type LineType int const ( LineContext LineType = iota // Line exists in both files LineAdded // Line added in the new file LineRemoved // Line removed from the old file ) // Segment represents a portion of a line for intra-line highlighting type Segment struct { Start int End int Type LineType Text string } // DiffLine represents a single line in a diff type DiffLine struct { OldLineNo int // Line number in old file (0 for added lines) NewLineNo int // Line number in new file (0 for removed lines) Kind LineType // Type of line (added, removed, context) Content string // Content of the line Segments []Segment // Segments for intraline highlighting } // Hunk represents a section of changes in a diff type Hunk struct { Header string Lines []DiffLine } // DiffResult contains the parsed result of a diff type DiffResult struct { OldFile string NewFile string Hunks []Hunk } // linePair represents a pair of lines for side-by-side display type linePair struct { left *DiffLine right *DiffLine } // ------------------------------------------------------------------------- // Parse Configuration // ------------------------------------------------------------------------- // ParseConfig configures the behavior of diff parsing type ParseConfig struct { ContextSize int // Number of context lines to include } // ParseOption modifies a ParseConfig type ParseOption func(*ParseConfig) // WithContextSize sets the number of context lines to include func WithContextSize(size int) ParseOption { return func(p *ParseConfig) { if size >= 0 { p.ContextSize = size } } } // ------------------------------------------------------------------------- // Side-by-Side Configuration // ------------------------------------------------------------------------- // SideBySideConfig configures the rendering of side-by-side diffs type SideBySideConfig struct { TotalWidth int } // SideBySideOption modifies a SideBySideConfig type SideBySideOption func(*SideBySideConfig) // NewSideBySideConfig creates a SideBySideConfig with default values func NewSideBySideConfig(opts ...SideBySideOption) SideBySideConfig { config := SideBySideConfig{ TotalWidth: 160, // Default width for side-by-side view } for _, opt := range opts { opt(&config) } return config } // WithTotalWidth sets the total width for side-by-side view func WithTotalWidth(width int) SideBySideOption { return func(s *SideBySideConfig) { if width > 0 { s.TotalWidth = width } } } // ------------------------------------------------------------------------- // Diff Parsing // ------------------------------------------------------------------------- // ParseUnifiedDiff parses a unified diff format string into structured data func ParseUnifiedDiff(diff string) (DiffResult, error) { var result DiffResult var currentHunk *Hunk hunkHeaderRe := regexp.MustCompile(`^@@ -(\d+),?(\d*) \+(\d+),?(\d*) @@`) lines := strings.Split(diff, "\n") var oldLine, newLine int inFileHeader := true for _, line := range lines { // Parse file headers if inFileHeader { if strings.HasPrefix(line, "--- a/") { result.OldFile = strings.TrimPrefix(line, "--- a/") continue } if strings.HasPrefix(line, "+++ b/") { result.NewFile = strings.TrimPrefix(line, "+++ b/") inFileHeader = false continue } } // Parse hunk headers if matches := hunkHeaderRe.FindStringSubmatch(line); matches != nil { if currentHunk != nil { result.Hunks = append(result.Hunks, *currentHunk) } currentHunk = &Hunk{ Header: line, Lines: []DiffLine{}, } oldStart, _ := strconv.Atoi(matches[1]) newStart, _ := strconv.Atoi(matches[3]) oldLine = oldStart newLine = newStart continue } // Ignore "No newline at end of file" markers if strings.HasPrefix(line, "\\ No newline at end of file") { continue } if currentHunk == nil { continue } // Process the line based on its prefix if len(line) > 0 { switch line[0] { case '+': currentHunk.Lines = append(currentHunk.Lines, DiffLine{ OldLineNo: 0, NewLineNo: newLine, Kind: LineAdded, Content: line[1:], }) newLine++ case '-': currentHunk.Lines = append(currentHunk.Lines, DiffLine{ OldLineNo: oldLine, NewLineNo: 0, Kind: LineRemoved, Content: line[1:], }) oldLine++ default: currentHunk.Lines = append(currentHunk.Lines, DiffLine{ OldLineNo: oldLine, NewLineNo: newLine, Kind: LineContext, Content: line, }) oldLine++ newLine++ } } else { // Handle empty lines currentHunk.Lines = append(currentHunk.Lines, DiffLine{ OldLineNo: oldLine, NewLineNo: newLine, Kind: LineContext, Content: "", }) oldLine++ newLine++ } } // Add the last hunk if there is one if currentHunk != nil { result.Hunks = append(result.Hunks, *currentHunk) } return result, nil } // HighlightIntralineChanges updates lines in a hunk to show character-level differences func HighlightIntralineChanges(h *Hunk) { var updated []DiffLine dmp := diffmatchpatch.New() for i := 0; i < len(h.Lines); i++ { // Look for removed line followed by added line if i+1 < len(h.Lines) && h.Lines[i].Kind == LineRemoved && h.Lines[i+1].Kind == LineAdded { oldLine := h.Lines[i] newLine := h.Lines[i+1] // Find character-level differences patches := dmp.DiffMain(oldLine.Content, newLine.Content, false) patches = dmp.DiffCleanupSemantic(patches) patches = dmp.DiffCleanupMerge(patches) patches = dmp.DiffCleanupEfficiency(patches) segments := make([]Segment, 0) removeStart := 0 addStart := 0 for _, patch := range patches { switch patch.Type { case diffmatchpatch.DiffDelete: segments = append(segments, Segment{ Start: removeStart, End: removeStart + len(patch.Text), Type: LineRemoved, Text: patch.Text, }) removeStart += len(patch.Text) case diffmatchpatch.DiffInsert: segments = append(segments, Segment{ Start: addStart, End: addStart + len(patch.Text), Type: LineAdded, Text: patch.Text, }) addStart += len(patch.Text) default: // Context text, no highlighting needed removeStart += len(patch.Text) addStart += len(patch.Text) } } oldLine.Segments = segments newLine.Segments = segments updated = append(updated, oldLine, newLine) i++ // Skip the next line as we've already processed it } else { updated = append(updated, h.Lines[i]) } } h.Lines = updated } // pairLines converts a flat list of diff lines to pairs for side-by-side display func pairLines(lines []DiffLine) []linePair { var pairs []linePair i := 0 for i < len(lines) { switch lines[i].Kind { case LineRemoved: // Check if the next line is an addition, if so pair them if i+1 < len(lines) && lines[i+1].Kind == LineAdded { pairs = append(pairs, linePair{left: &lines[i], right: &lines[i+1]}) i += 2 } else { pairs = append(pairs, linePair{left: &lines[i], right: nil}) i++ } case LineAdded: pairs = append(pairs, linePair{left: nil, right: &lines[i]}) i++ case LineContext: pairs = append(pairs, linePair{left: &lines[i], right: &lines[i]}) i++ } } return pairs } // ------------------------------------------------------------------------- // Syntax Highlighting // ------------------------------------------------------------------------- // SyntaxHighlight applies syntax highlighting to text based on file extension func SyntaxHighlight(w io.Writer, source, fileName, formatter string, bg lipgloss.TerminalColor) error { t := theme.CurrentTheme() // Determine the language lexer to use l := lexers.Match(fileName) if l == nil { l = lexers.Analyse(source) } if l == nil { l = lexers.Fallback } l = chroma.Coalesce(l) // Get the formatter f := formatters.Get(formatter) if f == nil { f = formatters.Fallback } // Dynamic theme based on current theme values syntaxThemeXml := fmt.Sprintf(` <style name="opencode-theme"> <!-- Base colors --> <entry type="Background" style="bg:%s"/> <entry type="Text" style="%s"/> <entry type="Other" style="%s"/> <entry type="Error" style="%s"/> <!-- Keywords --> <entry type="Keyword" style="%s"/> <entry type="KeywordConstant" style="%s"/> <entry type="KeywordDeclaration" style="%s"/> <entry type="KeywordNamespace" style="%s"/> <entry type="KeywordPseudo" style="%s"/> <entry type="KeywordReserved" style="%s"/> <entry type="KeywordType" style="%s"/> <!-- Names --> <entry type="Name" style="%s"/> <entry type="NameAttribute" style="%s"/> <entry type="NameBuiltin" style="%s"/> <entry type="NameBuiltinPseudo" style="%s"/> <entry type="NameClass" style="%s"/> <entry type="NameConstant" style="%s"/> <entry type="NameDecorator" style="%s"/> <entry type="NameEntity" style="%s"/> <entry type="NameException" style="%s"/> <entry type="NameFunction" style="%s"/> <entry type="NameLabel" style="%s"/> <entry type="NameNamespace" style="%s"/> <entry type="NameOther" style="%s"/> <entry type="NameTag" style="%s"/> <entry type="NameVariable" style="%s"/> <entry type="NameVariableClass" style="%s"/> <entry type="NameVariableGlobal" style="%s"/> <entry type="NameVariableInstance" style="%s"/> <!-- Literals --> <entry type="Literal" style="%s"/> <entry type="LiteralDate" style="%s"/> <entry type="LiteralString" style="%s"/> <entry type="LiteralStringBacktick" style="%s"/> <entry type="LiteralStringChar" style="%s"/> <entry type="LiteralStringDoc" style="%s"/> <entry type="LiteralStringDouble" style="%s"/> <entry type="LiteralStringEscape" style="%s"/> <entry type="LiteralStringHeredoc" style="%s"/> <entry type="LiteralStringInterpol" style="%s"/> <entry type="LiteralStringOther" style="%s"/> <entry type="LiteralStringRegex" style="%s"/> <entry type="LiteralStringSingle" style="%s"/> <entry type="LiteralStringSymbol" style="%s"/> <!-- Numbers --> <entry type="LiteralNumber" style="%s"/> <entry type="LiteralNumberBin" style="%s"/> <entry type="LiteralNumberFloat" style="%s"/> <entry type="LiteralNumberHex" style="%s"/> <entry type="LiteralNumberInteger" style="%s"/> <entry type="LiteralNumberIntegerLong" style="%s"/> <entry type="LiteralNumberOct" style="%s"/> <!-- Operators --> <entry type="Operator" style="%s"/> <entry type="OperatorWord" style="%s"/> <entry type="Punctuation" style="%s"/> <!-- Comments --> <entry type="Comment" style="%s"/> <entry type="CommentHashbang" style="%s"/> <entry type="CommentMultiline" style="%s"/> <entry type="CommentSingle" style="%s"/> <entry type="CommentSpecial" style="%s"/> <entry type="CommentPreproc" style="%s"/> <!-- Generic styles --> <entry type="Generic" style="%s"/> <entry type="GenericDeleted" style="%s"/> <entry type="GenericEmph" style="italic %s"/> <entry type="GenericError" style="%s"/> <entry type="GenericHeading" style="bold %s"/> <entry type="GenericInserted" style="%s"/> <entry type="GenericOutput" style="%s"/> <entry type="GenericPrompt" style="%s"/> <entry type="GenericStrong" style="bold %s"/> <entry type="GenericSubheading" style="bold %s"/> <entry type="GenericTraceback" style="%s"/> <entry type="GenericUnderline" style="underline"/> <entry type="TextWhitespace" style="%s"/> </style> `, getColor(t.Background()), // Background getColor(t.Text()), // Text getColor(t.Text()), // Other getColor(t.Error()), // Error getColor(t.SyntaxKeyword()), // Keyword getColor(t.SyntaxKeyword()), // KeywordConstant getColor(t.SyntaxKeyword()), // KeywordDeclaration getColor(t.SyntaxKeyword()), // KeywordNamespace getColor(t.SyntaxKeyword()), // KeywordPseudo getColor(t.SyntaxKeyword()), // KeywordReserved getColor(t.SyntaxType()), // KeywordType getColor(t.Text()), // Name getColor(t.SyntaxVariable()), // NameAttribute getColor(t.SyntaxType()), // NameBuiltin getColor(t.SyntaxVariable()), // NameBuiltinPseudo getColor(t.SyntaxType()), // NameClass getColor(t.SyntaxVariable()), // NameConstant getColor(t.SyntaxFunction()), // NameDecorator getColor(t.SyntaxVariable()), // NameEntity getColor(t.SyntaxType()), // NameException getColor(t.SyntaxFunction()), // NameFunction getColor(t.Text()), // NameLabel getColor(t.SyntaxType()), // NameNamespace getColor(t.SyntaxVariable()), // NameOther getColor(t.SyntaxKeyword()), // NameTag getColor(t.SyntaxVariable()), // NameVariable getColor(t.SyntaxVariable()), // NameVariableClass getColor(t.SyntaxVariable()), // NameVariableGlobal getColor(t.SyntaxVariable()), // NameVariableInstance getColor(t.SyntaxString()), // Literal getColor(t.SyntaxString()), // LiteralDate getColor(t.SyntaxString()), // LiteralString getColor(t.SyntaxString()), // LiteralStringBacktick getColor(t.SyntaxString()), // LiteralStringChar getColor(t.SyntaxString()), // LiteralStringDoc getColor(t.SyntaxString()), // LiteralStringDouble getColor(t.SyntaxString()), // LiteralStringEscape getColor(t.SyntaxString()), // LiteralStringHeredoc getColor(t.SyntaxString()), // LiteralStringInterpol getColor(t.SyntaxString()), // LiteralStringOther getColor(t.SyntaxString()), // LiteralStringRegex getColor(t.SyntaxString()), // LiteralStringSingle getColor(t.SyntaxString()), // LiteralStringSymbol getColor(t.SyntaxNumber()), // LiteralNumber getColor(t.SyntaxNumber()), // LiteralNumberBin getColor(t.SyntaxNumber()), // LiteralNumberFloat getColor(t.SyntaxNumber()), // LiteralNumberHex getColor(t.SyntaxNumber()), // LiteralNumberInteger getColor(t.SyntaxNumber()), // LiteralNumberIntegerLong getColor(t.SyntaxNumber()), // LiteralNumberOct getColor(t.SyntaxOperator()), // Operator getColor(t.SyntaxKeyword()), // OperatorWord getColor(t.SyntaxPunctuation()), // Punctuation getColor(t.SyntaxComment()), // Comment getColor(t.SyntaxComment()), // CommentHashbang getColor(t.SyntaxComment()), // CommentMultiline getColor(t.SyntaxComment()), // CommentSingle getColor(t.SyntaxComment()), // CommentSpecial getColor(t.SyntaxKeyword()), // CommentPreproc getColor(t.Text()), // Generic getColor(t.Error()), // GenericDeleted getColor(t.Text()), // GenericEmph getColor(t.Error()), // GenericError getColor(t.Text()), // GenericHeading getColor(t.Success()), // GenericInserted getColor(t.TextMuted()), // GenericOutput getColor(t.Text()), // GenericPrompt getColor(t.Text()), // GenericStrong getColor(t.Text()), // GenericSubheading getColor(t.Error()), // GenericTraceback getColor(t.Text()), // TextWhitespace ) r := strings.NewReader(syntaxThemeXml) style := chroma.MustNewXMLStyle(r) // Modify the style to use the provided background s, err := style.Builder().Transform( func(t chroma.StyleEntry) chroma.StyleEntry { r, g, b, _ := bg.RGBA() t.Background = chroma.NewColour(uint8(r>>8), uint8(g>>8), uint8(b>>8)) return t }, ).Build() if err != nil { s = styles.Fallback } // Tokenize and format it, err := l.Tokenise(nil, source) if err != nil { return err } return f.Format(w, s, it) } // getColor returns the appropriate hex color string based on terminal background func getColor(adaptiveColor lipgloss.AdaptiveColor) string { if lipgloss.HasDarkBackground() { return adaptiveColor.Dark } return adaptiveColor.Light } // highlightLine applies syntax highlighting to a single line func highlightLine(fileName string, line string, bg lipgloss.TerminalColor) string { var buf bytes.Buffer err := SyntaxHighlight(&buf, line, fileName, "terminal16m", bg) if err != nil { return line } return buf.String() } // createStyles generates the lipgloss styles needed for rendering diffs func createStyles(t theme.Theme) (removedLineStyle, addedLineStyle, contextLineStyle, lineNumberStyle lipgloss.Style) { removedLineStyle = lipgloss.NewStyle().Background(t.DiffRemovedBg()) addedLineStyle = lipgloss.NewStyle().Background(t.DiffAddedBg()) contextLineStyle = lipgloss.NewStyle().Background(t.DiffContextBg()) lineNumberStyle = lipgloss.NewStyle().Foreground(t.DiffLineNumber()) return } // ------------------------------------------------------------------------- // Rendering Functions // ------------------------------------------------------------------------- func lipglossToHex(color lipgloss.Color) string { r, g, b, a := color.RGBA() // Scale uint32 values (0-65535) to uint8 (0-255). r8 := uint8(r >> 8) g8 := uint8(g >> 8) b8 := uint8(b >> 8) a8 := uint8(a >> 8) return fmt.Sprintf("#%02x%02x%02x%02x", r8, g8, b8, a8) } // applyHighlighting applies intra-line highlighting to a piece of text func applyHighlighting(content string, segments []Segment, segmentType LineType, highlightBg lipgloss.AdaptiveColor) string { // Find all ANSI sequences in the content ansiRegex := regexp.MustCompile(`\x1b(?:[@-Z\\-_]|\[[0-9?]*(?:;[0-9?]*)*[@-~])`) ansiMatches := ansiRegex.FindAllStringIndex(content, -1) // Build a mapping of visible character positions to their actual indices visibleIdx := 0 ansiSequences := make(map[int]string) lastAnsiSeq := "\x1b[0m" // Default reset sequence for i := 0; i < len(content); { isAnsi := false for _, match := range ansiMatches { if match[0] == i { ansiSequences[visibleIdx] = content[match[0]:match[1]] lastAnsiSeq = content[match[0]:match[1]] i = match[1] isAnsi = true break } } if isAnsi { continue } // For non-ANSI positions, store the last ANSI sequence if _, exists := ansiSequences[visibleIdx]; !exists { ansiSequences[visibleIdx] = lastAnsiSeq } visibleIdx++ i++ } // Apply highlighting var sb strings.Builder inSelection := false currentPos := 0 // Get the appropriate color based on terminal background bgColor := lipgloss.Color(getColor(highlightBg)) fgColor := lipgloss.Color(getColor(theme.CurrentTheme().Background())) for i := 0; i < len(content); { // Check if we're at an ANSI sequence isAnsi := false for _, match := range ansiMatches { if match[0] == i { sb.WriteString(content[match[0]:match[1]]) // Preserve ANSI sequence i = match[1] isAnsi = true break } } if isAnsi { continue } // Check for segment boundaries for _, seg := range segments { if seg.Type == segmentType { if currentPos == seg.Start { inSelection = true } if currentPos == seg.End { inSelection = false } } } // Get current character char := string(content[i]) if inSelection { // Get the current styling currentStyle := ansiSequences[currentPos] // Apply foreground and background highlight sb.WriteString("\x1b[38;2;") r, g, b, _ := fgColor.RGBA() sb.WriteString(fmt.Sprintf("%d;%d;%dm", r>>8, g>>8, b>>8)) sb.WriteString("\x1b[48;2;") r, g, b, _ = bgColor.RGBA() sb.WriteString(fmt.Sprintf("%d;%d;%dm", r>>8, g>>8, b>>8)) sb.WriteString(char) // Reset foreground and background sb.WriteString("\x1b[39m") // Reapply the original ANSI sequence sb.WriteString(currentStyle) } else { // Not in selection, just copy the character sb.WriteString(char) } currentPos++ i++ } return sb.String() } // renderLeftColumn formats the left side of a side-by-side diff func renderLeftColumn(fileName string, dl *DiffLine, colWidth int) string { t := theme.CurrentTheme() if dl == nil { contextLineStyle := lipgloss.NewStyle().Background(t.DiffContextBg()) return contextLineStyle.Width(colWidth).Render("") } removedLineStyle, _, contextLineStyle, lineNumberStyle := createStyles(t) // Determine line style based on line type var marker string var bgStyle lipgloss.Style switch dl.Kind { case LineRemoved: marker = removedLineStyle.Foreground(t.DiffRemoved()).Render("-") bgStyle = removedLineStyle lineNumberStyle = lineNumberStyle.Foreground(t.DiffRemoved()).Background(t.DiffRemovedLineNumberBg()) case LineAdded: marker = "?" bgStyle = contextLineStyle case LineContext: marker = contextLineStyle.Render(" ") bgStyle = contextLineStyle } // Format line number lineNum := "" if dl.OldLineNo > 0 { lineNum = fmt.Sprintf("%6d", dl.OldLineNo) } // Create the line prefix prefix := lineNumberStyle.Render(lineNum + " " + marker) // Apply syntax highlighting content := highlightLine(fileName, dl.Content, bgStyle.GetBackground()) // Apply intra-line highlighting for removed lines if dl.Kind == LineRemoved && len(dl.Segments) > 0 { content = applyHighlighting(content, dl.Segments, LineRemoved, t.DiffHighlightRemoved()) } // Add a padding space for removed lines if dl.Kind == LineRemoved { content = bgStyle.Render(" ") + content } // Create the final line and truncate if needed lineText := prefix + content return bgStyle.MaxHeight(1).Width(colWidth).Render( ansi.Truncate( lineText, colWidth, lipgloss.NewStyle().Background(bgStyle.GetBackground()).Foreground(t.TextMuted()).Render("..."), ), ) } // renderRightColumn formats the right side of a side-by-side diff func renderRightColumn(fileName string, dl *DiffLine, colWidth int) string { t := theme.CurrentTheme() if dl == nil { contextLineStyle := lipgloss.NewStyle().Background(t.DiffContextBg()) return contextLineStyle.Width(colWidth).Render("") } _, addedLineStyle, contextLineStyle, lineNumberStyle := createStyles(t) // Determine line style based on line type var marker string var bgStyle lipgloss.Style switch dl.Kind { case LineAdded: marker = addedLineStyle.Foreground(t.DiffAdded()).Render("+") bgStyle = addedLineStyle lineNumberStyle = lineNumberStyle.Foreground(t.DiffAdded()).Background(t.DiffAddedLineNumberBg()) case LineRemoved: marker = "?" bgStyle = contextLineStyle case LineContext: marker = contextLineStyle.Render(" ") bgStyle = contextLineStyle } // Format line number lineNum := "" if dl.NewLineNo > 0 { lineNum = fmt.Sprintf("%6d", dl.NewLineNo) } // Create the line prefix prefix := lineNumberStyle.Render(lineNum + " " + marker) // Apply syntax highlighting content := highlightLine(fileName, dl.Content, bgStyle.GetBackground()) // Apply intra-line highlighting for added lines if dl.Kind == LineAdded && len(dl.Segments) > 0 { content = applyHighlighting(content, dl.Segments, LineAdded, t.DiffHighlightAdded()) } // Add a padding space for added lines if dl.Kind == LineAdded { content = bgStyle.Render(" ") + content } // Create the final line and truncate if needed lineText := prefix + content return bgStyle.MaxHeight(1).Width(colWidth).Render( ansi.Truncate( lineText, colWidth, lipgloss.NewStyle().Background(bgStyle.GetBackground()).Foreground(t.TextMuted()).Render("..."), ), ) } // ------------------------------------------------------------------------- // Public API // ------------------------------------------------------------------------- // RenderSideBySideHunk formats a hunk for side-by-side display func RenderSideBySideHunk(fileName string, h Hunk, opts ...SideBySideOption) string { // Apply options to create the configuration config := NewSideBySideConfig(opts...) // Make a copy of the hunk so we don't modify the original hunkCopy := Hunk{Lines: make([]DiffLine, len(h.Lines))} copy(hunkCopy.Lines, h.Lines) // Highlight changes within lines HighlightIntralineChanges(&hunkCopy) // Pair lines for side-by-side display pairs := pairLines(hunkCopy.Lines) // Calculate column width colWidth := config.TotalWidth / 2 leftWidth := colWidth rightWidth := config.TotalWidth - colWidth var sb strings.Builder for _, p := range pairs { leftStr := renderLeftColumn(fileName, p.left, leftWidth) rightStr := renderRightColumn(fileName, p.right, rightWidth) sb.WriteString(leftStr + rightStr + "\n") } return sb.String() } // FormatDiff creates a side-by-side formatted view of a diff func FormatDiff(diffText string, opts ...SideBySideOption) (string, error) { diffResult, err := ParseUnifiedDiff(diffText) if err != nil { return "", err } var sb strings.Builder for _, h := range diffResult.Hunks { sb.WriteString(RenderSideBySideHunk(diffResult.OldFile, h, opts...)) } return sb.String(), nil } // GenerateDiff creates a unified diff from two file contents func GenerateDiff(beforeContent, afterContent, fileName string) (string, int, int) { // remove the cwd prefix and ensure consistent path format // this prevents issues with absolute paths in different environments cwd := config.WorkingDirectory() fileName = strings.TrimPrefix(fileName, cwd) fileName = strings.TrimPrefix(fileName, "/") var ( unified = udiff.Unified("a/"+fileName, "b/"+fileName, beforeContent, afterContent) additions = 0 removals = 0 ) lines := strings.SplitSeq(unified, "\n") for line := range lines { if strings.HasPrefix(line, "+") && !strings.HasPrefix(line, "+++") { additions++ } else if strings.HasPrefix(line, "-") && !strings.HasPrefix(line, "---") { removals++ } } return unified, additions, removals } ``` ## /internal/diff/patch.go ```go path="/internal/diff/patch.go" package diff import ( "errors" "fmt" "os" "path/filepath" "strings" ) type ActionType string const ( ActionAdd ActionType = "add" ActionDelete ActionType = "delete" ActionUpdate ActionType = "update" ) type FileChange struct { Type ActionType OldContent *string NewContent *string MovePath *string } type Commit struct { Changes map[string]FileChange } type Chunk struct { OrigIndex int // line index of the first line in the original file DelLines []string // lines to delete InsLines []string // lines to insert } type PatchAction struct { Type ActionType NewFile *string Chunks []Chunk MovePath *string } type Patch struct { Actions map[string]PatchAction } type DiffError struct { message string } func (e DiffError) Error() string { return e.message } // Helper functions for error handling func NewDiffError(message string) DiffError { return DiffError{message: message} } func fileError(action, reason, path string) DiffError { return NewDiffError(fmt.Sprintf("%s File Error: %s: %s", action, reason, path)) } func contextError(index int, context string, isEOF bool) DiffError { prefix := "Invalid Context" if isEOF { prefix = "Invalid EOF Context" } return NewDiffError(fmt.Sprintf("%s %d:\n%s", prefix, index, context)) } type Parser struct { currentFiles map[string]string lines []string index int patch Patch fuzz int } func NewParser(currentFiles map[string]string, lines []string) *Parser { return &Parser{ currentFiles: currentFiles, lines: lines, index: 0, patch: Patch{Actions: make(map[string]PatchAction, len(currentFiles))}, fuzz: 0, } } func (p *Parser) isDone(prefixes []string) bool { if p.index >= len(p.lines) { return true } for _, prefix := range prefixes { if strings.HasPrefix(p.lines[p.index], prefix) { return true } } return false } func (p *Parser) startsWith(prefix any) bool { var prefixes []string switch v := prefix.(type) { case string: prefixes = []string{v} case []string: prefixes = v } for _, pfx := range prefixes { if strings.HasPrefix(p.lines[p.index], pfx) { return true } } return false } func (p *Parser) readStr(prefix string, returnEverything bool) string { if p.index >= len(p.lines) { return "" // Changed from panic to return empty string for safer operation } if strings.HasPrefix(p.lines[p.index], prefix) { var text string if returnEverything { text = p.lines[p.index] } else { text = p.lines[p.index][len(prefix):] } p.index++ return text } return "" } func (p *Parser) Parse() error { endPatchPrefixes := []string{"*** End Patch"} for !p.isDone(endPatchPrefixes) { path := p.readStr("*** Update File: ", false) if path != "" { if _, exists := p.patch.Actions[path]; exists { return fileError("Update", "Duplicate Path", path) } moveTo := p.readStr("*** Move to: ", false) if _, exists := p.currentFiles[path]; !exists { return fileError("Update", "Missing File", path) } text := p.currentFiles[path] action, err := p.parseUpdateFile(text) if err != nil { return err } if moveTo != "" { action.MovePath = &moveTo } p.patch.Actions[path] = action continue } path = p.readStr("*** Delete File: ", false) if path != "" { if _, exists := p.patch.Actions[path]; exists { return fileError("Delete", "Duplicate Path", path) } if _, exists := p.currentFiles[path]; !exists { return fileError("Delete", "Missing File", path) } p.patch.Actions[path] = PatchAction{Type: ActionDelete, Chunks: []Chunk{}} continue } path = p.readStr("*** Add File: ", false) if path != "" { if _, exists := p.patch.Actions[path]; exists { return fileError("Add", "Duplicate Path", path) } if _, exists := p.currentFiles[path]; exists { return fileError("Add", "File already exists", path) } action, err := p.parseAddFile() if err != nil { return err } p.patch.Actions[path] = action continue } return NewDiffError(fmt.Sprintf("Unknown Line: %s", p.lines[p.index])) } if !p.startsWith("*** End Patch") { return NewDiffError("Missing End Patch") } p.index++ return nil } func (p *Parser) parseUpdateFile(text string) (PatchAction, error) { action := PatchAction{Type: ActionUpdate, Chunks: []Chunk{}} fileLines := strings.Split(text, "\n") index := 0 endPrefixes := []string{ "*** End Patch", "*** Update File:", "*** Delete File:", "*** Add File:", "*** End of File", } for !p.isDone(endPrefixes) { defStr := p.readStr("@@ ", false) sectionStr := "" if defStr == "" && p.index < len(p.lines) && p.lines[p.index] == "@@" { sectionStr = p.lines[p.index] p.index++ } if defStr == "" && sectionStr == "" && index != 0 { return action, NewDiffError(fmt.Sprintf("Invalid Line:\n%s", p.lines[p.index])) } if strings.TrimSpace(defStr) != "" { found := false for i := range fileLines[:index] { if fileLines[i] == defStr { found = true break } } if !found { for i := index; i < len(fileLines); i++ { if fileLines[i] == defStr { index = i + 1 found = true break } } } if !found { for i := range fileLines[:index] { if strings.TrimSpace(fileLines[i]) == strings.TrimSpace(defStr) { found = true break } } } if !found { for i := index; i < len(fileLines); i++ { if strings.TrimSpace(fileLines[i]) == strings.TrimSpace(defStr) { index = i + 1 p.fuzz++ found = true break } } } } nextChunkContext, chunks, endPatchIndex, eof := peekNextSection(p.lines, p.index) newIndex, fuzz := findContext(fileLines, nextChunkContext, index, eof) if newIndex == -1 { ctxText := strings.Join(nextChunkContext, "\n") return action, contextError(index, ctxText, eof) } p.fuzz += fuzz for _, ch := range chunks { ch.OrigIndex += newIndex action.Chunks = append(action.Chunks, ch) } index = newIndex + len(nextChunkContext) p.index = endPatchIndex } return action, nil } func (p *Parser) parseAddFile() (PatchAction, error) { lines := make([]string, 0, 16) // Preallocate space for better performance endPrefixes := []string{ "*** End Patch", "*** Update File:", "*** Delete File:", "*** Add File:", } for !p.isDone(endPrefixes) { s := p.readStr("", true) if !strings.HasPrefix(s, "+") { return PatchAction{}, NewDiffError(fmt.Sprintf("Invalid Add File Line: %s", s)) } lines = append(lines, s[1:]) } newFile := strings.Join(lines, "\n") return PatchAction{ Type: ActionAdd, NewFile: &newFile, Chunks: []Chunk{}, }, nil } // Refactored to use a matcher function for each comparison type func findContextCore(lines []string, context []string, start int) (int, int) { if len(context) == 0 { return start, 0 } // Try exact match if idx, fuzz := tryFindMatch(lines, context, start, func(a, b string) bool { return a == b }); idx >= 0 { return idx, fuzz } // Try trimming right whitespace if idx, fuzz := tryFindMatch(lines, context, start, func(a, b string) bool { return strings.TrimRight(a, " \t") == strings.TrimRight(b, " \t") }); idx >= 0 { return idx, fuzz } // Try trimming all whitespace if idx, fuzz := tryFindMatch(lines, context, start, func(a, b string) bool { return strings.TrimSpace(a) == strings.TrimSpace(b) }); idx >= 0 { return idx, fuzz } return -1, 0 } // Helper function to DRY up the match logic func tryFindMatch(lines []string, context []string, start int, compareFunc func(string, string) bool, ) (int, int) { for i := start; i < len(lines); i++ { if i+len(context) <= len(lines) { match := true for j := range context { if !compareFunc(lines[i+j], context[j]) { match = false break } } if match { // Return fuzz level: 0 for exact, 1 for trimRight, 100 for trimSpace var fuzz int if compareFunc("a ", "a") && !compareFunc("a", "b") { fuzz = 1 } else if compareFunc("a ", "a") { fuzz = 100 } return i, fuzz } } } return -1, 0 } func findContext(lines []string, context []string, start int, eof bool) (int, int) { if eof { newIndex, fuzz := findContextCore(lines, context, len(lines)-len(context)) if newIndex != -1 { return newIndex, fuzz } newIndex, fuzz = findContextCore(lines, context, start) return newIndex, fuzz + 10000 } return findContextCore(lines, context, start) } func peekNextSection(lines []string, initialIndex int) ([]string, []Chunk, int, bool) { index := initialIndex old := make([]string, 0, 32) // Preallocate for better performance delLines := make([]string, 0, 8) insLines := make([]string, 0, 8) chunks := make([]Chunk, 0, 4) mode := "keep" // End conditions for the section endSectionConditions := func(s string) bool { return strings.HasPrefix(s, "@@") || strings.HasPrefix(s, "*** End Patch") || strings.HasPrefix(s, "*** Update File:") || strings.HasPrefix(s, "*** Delete File:") || strings.HasPrefix(s, "*** Add File:") || strings.HasPrefix(s, "*** End of File") || s == "***" || strings.HasPrefix(s, "***") } for index < len(lines) { s := lines[index] if endSectionConditions(s) { break } index++ lastMode := mode line := s if len(line) > 0 { switch line[0] { case '+': mode = "add" case '-': mode = "delete" case ' ': mode = "keep" default: mode = "keep" line = " " + line } } else { mode = "keep" line = " " } line = line[1:] if mode == "keep" && lastMode != mode { if len(insLines) > 0 || len(delLines) > 0 { chunks = append(chunks, Chunk{ OrigIndex: len(old) - len(delLines), DelLines: delLines, InsLines: insLines, }) } delLines = make([]string, 0, 8) insLines = make([]string, 0, 8) } switch mode { case "delete": delLines = append(delLines, line) old = append(old, line) case "add": insLines = append(insLines, line) default: old = append(old, line) } } if len(insLines) > 0 || len(delLines) > 0 { chunks = append(chunks, Chunk{ OrigIndex: len(old) - len(delLines), DelLines: delLines, InsLines: insLines, }) } if index < len(lines) && lines[index] == "*** End of File" { index++ return old, chunks, index, true } return old, chunks, index, false } func TextToPatch(text string, orig map[string]string) (Patch, int, error) { text = strings.TrimSpace(text) lines := strings.Split(text, "\n") if len(lines) < 2 || !strings.HasPrefix(lines[0], "*** Begin Patch") || lines[len(lines)-1] != "*** End Patch" { return Patch{}, 0, NewDiffError("Invalid patch text") } parser := NewParser(orig, lines) parser.index = 1 if err := parser.Parse(); err != nil { return Patch{}, 0, err } return parser.patch, parser.fuzz, nil } func IdentifyFilesNeeded(text string) []string { text = strings.TrimSpace(text) lines := strings.Split(text, "\n") result := make(map[string]bool) for _, line := range lines { if strings.HasPrefix(line, "*** Update File: ") { result[line[len("*** Update File: "):]] = true } if strings.HasPrefix(line, "*** Delete File: ") { result[line[len("*** Delete File: "):]] = true } } files := make([]string, 0, len(result)) for file := range result { files = append(files, file) } return files } func IdentifyFilesAdded(text string) []string { text = strings.TrimSpace(text) lines := strings.Split(text, "\n") result := make(map[string]bool) for _, line := range lines { if strings.HasPrefix(line, "*** Add File: ") { result[line[len("*** Add File: "):]] = true } } files := make([]string, 0, len(result)) for file := range result { files = append(files, file) } return files } func getUpdatedFile(text string, action PatchAction, path string) (string, error) { if action.Type != ActionUpdate { return "", errors.New("expected UPDATE action") } origLines := strings.Split(text, "\n") destLines := make([]string, 0, len(origLines)) // Preallocate with capacity origIndex := 0 for _, chunk := range action.Chunks { if chunk.OrigIndex > len(origLines) { return "", NewDiffError(fmt.Sprintf("%s: chunk.orig_index %d > len(lines) %d", path, chunk.OrigIndex, len(origLines))) } if origIndex > chunk.OrigIndex { return "", NewDiffError(fmt.Sprintf("%s: orig_index %d > chunk.orig_index %d", path, origIndex, chunk.OrigIndex)) } destLines = append(destLines, origLines[origIndex:chunk.OrigIndex]...) delta := chunk.OrigIndex - origIndex origIndex += delta if len(chunk.InsLines) > 0 { destLines = append(destLines, chunk.InsLines...) } origIndex += len(chunk.DelLines) } destLines = append(destLines, origLines[origIndex:]...) return strings.Join(destLines, "\n"), nil } func PatchToCommit(patch Patch, orig map[string]string) (Commit, error) { commit := Commit{Changes: make(map[string]FileChange, len(patch.Actions))} for pathKey, action := range patch.Actions { switch action.Type { case ActionDelete: oldContent := orig[pathKey] commit.Changes[pathKey] = FileChange{ Type: ActionDelete, OldContent: &oldContent, } case ActionAdd: commit.Changes[pathKey] = FileChange{ Type: ActionAdd, NewContent: action.NewFile, } case ActionUpdate: newContent, err := getUpdatedFile(orig[pathKey], action, pathKey) if err != nil { return Commit{}, err } oldContent := orig[pathKey] fileChange := FileChange{ Type: ActionUpdate, OldContent: &oldContent, NewContent: &newContent, } if action.MovePath != nil { fileChange.MovePath = action.MovePath } commit.Changes[pathKey] = fileChange } } return commit, nil } func AssembleChanges(orig map[string]string, updatedFiles map[string]string) Commit { commit := Commit{Changes: make(map[string]FileChange, len(updatedFiles))} for p, newContent := range updatedFiles { oldContent, exists := orig[p] if exists && oldContent == newContent { continue } if exists && newContent != "" { commit.Changes[p] = FileChange{ Type: ActionUpdate, OldContent: &oldContent, NewContent: &newContent, } } else if newContent != "" { commit.Changes[p] = FileChange{ Type: ActionAdd, NewContent: &newContent, } } else if exists { commit.Changes[p] = FileChange{ Type: ActionDelete, OldContent: &oldContent, } } else { return commit // Changed from panic to simply return current commit } } return commit } func LoadFiles(paths []string, openFn func(string) (string, error)) (map[string]string, error) { orig := make(map[string]string, len(paths)) for _, p := range paths { content, err := openFn(p) if err != nil { return nil, fileError("Open", "File not found", p) } orig[p] = content } return orig, nil } func ApplyCommit(commit Commit, writeFn func(string, string) error, removeFn func(string) error) error { for p, change := range commit.Changes { switch change.Type { case ActionDelete: if err := removeFn(p); err != nil { return err } case ActionAdd: if change.NewContent == nil { return NewDiffError(fmt.Sprintf("Add action for %s has nil new_content", p)) } if err := writeFn(p, *change.NewContent); err != nil { return err } case ActionUpdate: if change.NewContent == nil { return NewDiffError(fmt.Sprintf("Update action for %s has nil new_content", p)) } if change.MovePath != nil { if err := writeFn(*change.MovePath, *change.NewContent); err != nil { return err } if err := removeFn(p); err != nil { return err } } else { if err := writeFn(p, *change.NewContent); err != nil { return err } } } } return nil } func ProcessPatch(text string, openFn func(string) (string, error), writeFn func(string, string) error, removeFn func(string) error) (string, error) { if !strings.HasPrefix(text, "*** Begin Patch") { return "", NewDiffError("Patch must start with *** Begin Patch") } paths := IdentifyFilesNeeded(text) orig, err := LoadFiles(paths, openFn) if err != nil { return "", err } patch, fuzz, err := TextToPatch(text, orig) if err != nil { return "", err } if fuzz > 0 { return "", NewDiffError(fmt.Sprintf("Patch contains fuzzy matches (fuzz level: %d)", fuzz)) } commit, err := PatchToCommit(patch, orig) if err != nil { return "", err } if err := ApplyCommit(commit, writeFn, removeFn); err != nil { return "", err } return "Patch applied successfully", nil } func OpenFile(p string) (string, error) { data, err := os.ReadFile(p) if err != nil { return "", err } return string(data), nil } func WriteFile(p string, content string) error { if filepath.IsAbs(p) { return NewDiffError("We do not support absolute paths.") } dir := filepath.Dir(p) if dir != "." { if err := os.MkdirAll(dir, 0o755); err != nil { return err } } return os.WriteFile(p, []byte(content), 0o644) } func RemoveFile(p string) error { return os.Remove(p) } func ValidatePatch(patchText string, files map[string]string) (bool, string, error) { if !strings.HasPrefix(patchText, "*** Begin Patch") { return false, "Patch must start with *** Begin Patch", nil } neededFiles := IdentifyFilesNeeded(patchText) for _, filePath := range neededFiles { if _, exists := files[filePath]; !exists { return false, fmt.Sprintf("File not found: %s", filePath), nil } } patch, fuzz, err := TextToPatch(patchText, files) if err != nil { return false, err.Error(), nil } if fuzz > 0 { return false, fmt.Sprintf("Patch contains fuzzy matches (fuzz level: %d)", fuzz), nil } _, err = PatchToCommit(patch, files) if err != nil { return false, err.Error(), nil } return true, "Patch is valid", nil } ``` ## /internal/fileutil/fileutil.go ```go path="/internal/fileutil/fileutil.go" package fileutil import ( "fmt" "io/fs" "os" "os/exec" "path/filepath" "sort" "strings" "time" "github.com/bmatcuk/doublestar/v4" "github.com/opencode-ai/opencode/internal/logging" ) var ( rgPath string fzfPath string ) func init() { var err error rgPath, err = exec.LookPath("rg") if err != nil { logging.Warn("Ripgrep (rg) not found in $PATH. Some features might be limited or slower.") rgPath = "" } fzfPath, err = exec.LookPath("fzf") if err != nil { logging.Warn("FZF not found in $PATH. Some features might be limited or slower.") fzfPath = "" } } func GetRgCmd(globPattern string) *exec.Cmd { if rgPath == "" { return nil } rgArgs := []string{ "--files", "-L", "--null", } if globPattern != "" { if !filepath.IsAbs(globPattern) && !strings.HasPrefix(globPattern, "/") { globPattern = "/" + globPattern } rgArgs = append(rgArgs, "--glob", globPattern) } cmd := exec.Command(rgPath, rgArgs...) cmd.Dir = "." return cmd } func GetFzfCmd(query string) *exec.Cmd { if fzfPath == "" { return nil } fzfArgs := []string{ "--filter", query, "--read0", "--print0", } cmd := exec.Command(fzfPath, fzfArgs...) cmd.Dir = "." return cmd } type FileInfo struct { Path string ModTime time.Time } func SkipHidden(path string) bool { // Check for hidden files (starting with a dot) base := filepath.Base(path) if base != "." && strings.HasPrefix(base, ".") { return true } commonIgnoredDirs := map[string]bool{ ".opencode": true, "node_modules": true, "vendor": true, "dist": true, "build": true, "target": true, ".git": true, ".idea": true, ".vscode": true, "__pycache__": true, "bin": true, "obj": true, "out": true, "coverage": true, "tmp": true, "temp": true, "logs": true, "generated": true, "bower_components": true, "jspm_packages": true, } parts := strings.Split(path, string(os.PathSeparator)) for _, part := range parts { if commonIgnoredDirs[part] { return true } } return false } func GlobWithDoublestar(pattern, searchPath string, limit int) ([]string, bool, error) { fsys := os.DirFS(searchPath) relPattern := strings.TrimPrefix(pattern, "/") var matches []FileInfo err := doublestar.GlobWalk(fsys, relPattern, func(path string, d fs.DirEntry) error { if d.IsDir() { return nil } if SkipHidden(path) { return nil } info, err := d.Info() if err != nil { return nil } absPath := path if !strings.HasPrefix(absPath, searchPath) && searchPath != "." { absPath = filepath.Join(searchPath, absPath) } else if !strings.HasPrefix(absPath, "/") && searchPath == "." { absPath = filepath.Join(searchPath, absPath) // Ensure relative paths are joined correctly } matches = append(matches, FileInfo{Path: absPath, ModTime: info.ModTime()}) if limit > 0 && len(matches) >= limit*2 { return fs.SkipAll } return nil }) if err != nil { return nil, false, fmt.Errorf("glob walk error: %w", err) } sort.Slice(matches, func(i, j int) bool { return matches[i].ModTime.After(matches[j].ModTime) }) truncated := false if limit > 0 && len(matches) > limit { matches = matches[:limit] truncated = true } results := make([]string, len(matches)) for i, m := range matches { results[i] = m.Path } return results, truncated, nil } ``` ## /internal/format/format.go ```go path="/internal/format/format.go" package format import ( "encoding/json" "fmt" "strings" ) // OutputFormat represents the output format type for non-interactive mode type OutputFormat string const ( // Text format outputs the AI response as plain text. Text OutputFormat = "text" // JSON format outputs the AI response wrapped in a JSON object. JSON OutputFormat = "json" ) // String returns the string representation of the OutputFormat func (f OutputFormat) String() string { return string(f) } // SupportedFormats is a list of all supported output formats as strings var SupportedFormats = []string{ string(Text), string(JSON), } // Parse converts a string to an OutputFormat func Parse(s string) (OutputFormat, error) { s = strings.ToLower(strings.TrimSpace(s)) switch s { case string(Text): return Text, nil case string(JSON): return JSON, nil default: return "", fmt.Errorf("invalid format: %s", s) } } // IsValid checks if the provided format string is supported func IsValid(s string) bool { _, err := Parse(s) return err == nil } // GetHelpText returns a formatted string describing all supported formats func GetHelpText() string { return fmt.Sprintf(`Supported output formats: - %s: Plain text output (default) - %s: Output wrapped in a JSON object`, Text, JSON) } // FormatOutput formats the AI response according to the specified format func FormatOutput(content string, formatStr string) string { format, err := Parse(formatStr) if err != nil { // Default to text format on error return content } switch format { case JSON: return formatAsJSON(content) case Text: fallthrough default: return content } } // formatAsJSON wraps the content in a simple JSON object func formatAsJSON(content string) string { // Use the JSON package to properly escape the content response := struct { Response string `json:"response"` }{ Response: content, } jsonBytes, err := json.MarshalIndent(response, "", " ") if err != nil { // In case of an error, return a manually formatted JSON jsonEscaped := strings.Replace(content, "\\", "\\\\", -1) jsonEscaped = strings.Replace(jsonEscaped, "\"", "\\\"", -1) jsonEscaped = strings.Replace(jsonEscaped, "\n", "\\n", -1) jsonEscaped = strings.Replace(jsonEscaped, "\r", "\\r", -1) jsonEscaped = strings.Replace(jsonEscaped, "\t", "\\t", -1) return fmt.Sprintf("{\n \"response\": \"%s\"\n}", jsonEscaped) } return string(jsonBytes) } ``` ## /internal/format/spinner.go ```go path="/internal/format/spinner.go" package format import ( "context" "fmt" "os" "github.com/charmbracelet/bubbles/spinner" tea "github.com/charmbracelet/bubbletea" ) // Spinner wraps the bubbles spinner for non-interactive mode type Spinner struct { model spinner.Model done chan struct{} prog *tea.Program ctx context.Context cancel context.CancelFunc } // spinnerModel is the tea.Model for the spinner type spinnerModel struct { spinner spinner.Model message string quitting bool } func (m spinnerModel) Init() tea.Cmd { return m.spinner.Tick } func (m spinnerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: m.quitting = true return m, tea.Quit case spinner.TickMsg: var cmd tea.Cmd m.spinner, cmd = m.spinner.Update(msg) return m, cmd case quitMsg: m.quitting = true return m, tea.Quit default: return m, nil } } func (m spinnerModel) View() string { if m.quitting { return "" } return fmt.Sprintf("%s %s", m.spinner.View(), m.message) } // quitMsg is sent when we want to quit the spinner type quitMsg struct{} // NewSpinner creates a new spinner with the given message func NewSpinner(message string) *Spinner { s := spinner.New() s.Spinner = spinner.Dot s.Style = s.Style.Foreground(s.Style.GetForeground()) ctx, cancel := context.WithCancel(context.Background()) model := spinnerModel{ spinner: s, message: message, } prog := tea.NewProgram(model, tea.WithOutput(os.Stderr), tea.WithoutCatchPanics()) return &Spinner{ model: s, done: make(chan struct{}), prog: prog, ctx: ctx, cancel: cancel, } } // Start begins the spinner animation func (s *Spinner) Start() { go func() { defer close(s.done) go func() { <-s.ctx.Done() s.prog.Send(quitMsg{}) }() _, err := s.prog.Run() if err != nil { fmt.Fprintf(os.Stderr, "Error running spinner: %v\n", err) } }() } // Stop ends the spinner animation func (s *Spinner) Stop() { s.cancel() <-s.done } ``` ## /internal/history/file.go ```go path="/internal/history/file.go" package history import ( "context" "database/sql" "fmt" "strconv" "strings" "time" "github.com/google/uuid" "github.com/opencode-ai/opencode/internal/db" "github.com/opencode-ai/opencode/internal/pubsub" ) const ( InitialVersion = "initial" ) type File struct { ID string SessionID string Path string Content string Version string CreatedAt int64 UpdatedAt int64 } type Service interface { pubsub.Suscriber[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) GetByPathAndSession(ctx context.Context, path, sessionID string) (File, error) ListBySession(ctx context.Context, sessionID string) ([]File, error) ListLatestSessionFiles(ctx context.Context, sessionID 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 { *pubsub.Broker[File] db *sql.DB q *db.Queries } func NewService(q *db.Queries, db *sql.DB) Service { return &service{ Broker: pubsub.NewBroker[File](), q: q, db: db, } } func (s *service) Create(ctx context.Context, sessionID, path, content string) (File, error) { return s.createWithVersion(ctx, sessionID, path, content, InitialVersion) } func (s *service) CreateVersion(ctx context.Context, sessionID, path, content string) (File, error) { // Get the latest version for this path files, err := s.q.ListFilesByPath(ctx, path) if err != nil { return File{}, err } if len(files) == 0 { // No previous versions, create initial return s.Create(ctx, sessionID, path, content) } // Get the latest version latestFile := files[0] // Files are ordered by created_at DESC latestVersion := latestFile.Version // Generate the next version var nextVersion string if latestVersion == InitialVersion { nextVersion = "v1" } else if strings.HasPrefix(latestVersion, "v") { versionNum, err := strconv.Atoi(latestVersion[1:]) if err != nil { // If we can't parse the version, just use a timestamp-based version nextVersion = fmt.Sprintf("v%d", latestFile.CreatedAt) } else { nextVersion = fmt.Sprintf("v%d", versionNum+1) } } else { // If the version format is unexpected, use a timestamp-based version nextVersion = fmt.Sprintf("v%d", latestFile.CreatedAt) } return s.createWithVersion(ctx, sessionID, path, content, nextVersion) } func (s *service) createWithVersion(ctx context.Context, sessionID, path, content, version string) (File, error) { // Maximum number of retries for transaction conflicts const maxRetries = 3 var file File var err error // Retry loop for transaction conflicts for attempt := range maxRetries { // Start a transaction tx, txErr := s.db.Begin() if txErr != nil { return File{}, fmt.Errorf("failed to begin transaction: %w", txErr) } // Create a new queries instance with the transaction qtx := s.q.WithTx(tx) // Try to create the file within the transaction dbFile, txErr := qtx.CreateFile(ctx, db.CreateFileParams{ ID: uuid.New().String(), SessionID: sessionID, Path: path, Content: content, Version: version, }) if txErr != nil { // Rollback the transaction tx.Rollback() // Check if this is a uniqueness constraint violation if strings.Contains(txErr.Error(), "UNIQUE constraint failed") { if attempt < maxRetries-1 { // If we have retries left, generate a new version and try again if strings.HasPrefix(version, "v") { versionNum, parseErr := strconv.Atoi(version[1:]) if parseErr == nil { version = fmt.Sprintf("v%d", versionNum+1) continue } } // If we can't parse the version, use a timestamp-based version version = fmt.Sprintf("v%d", time.Now().Unix()) continue } } return File{}, txErr } // Commit the transaction if txErr = tx.Commit(); txErr != nil { return File{}, fmt.Errorf("failed to commit transaction: %w", txErr) } file = s.fromDBItem(dbFile) s.Publish(pubsub.CreatedEvent, file) return file, nil } return file, err } func (s *service) Get(ctx context.Context, id string) (File, error) { dbFile, err := s.q.GetFile(ctx, id) if err != nil { return File{}, err } return s.fromDBItem(dbFile), nil } func (s *service) GetByPathAndSession(ctx context.Context, path, sessionID string) (File, error) { dbFile, err := s.q.GetFileByPathAndSession(ctx, db.GetFileByPathAndSessionParams{ Path: path, SessionID: sessionID, }) if err != nil { return File{}, err } return s.fromDBItem(dbFile), nil } func (s *service) ListBySession(ctx context.Context, sessionID string) ([]File, error) { dbFiles, err := s.q.ListFilesBySession(ctx, sessionID) if err != nil { return nil, err } files := make([]File, len(dbFiles)) for i, dbFile := range dbFiles { files[i] = s.fromDBItem(dbFile) } return files, nil } func (s *service) ListLatestSessionFiles(ctx context.Context, sessionID string) ([]File, error) { dbFiles, err := s.q.ListLatestSessionFiles(ctx, sessionID) if err != nil { return nil, err } files := make([]File, len(dbFiles)) for i, dbFile := range dbFiles { files[i] = s.fromDBItem(dbFile) } return files, nil } func (s *service) Update(ctx context.Context, file File) (File, error) { dbFile, err := s.q.UpdateFile(ctx, db.UpdateFileParams{ ID: file.ID, Content: file.Content, Version: file.Version, }) if err != nil { return File{}, err } updatedFile := s.fromDBItem(dbFile) s.Publish(pubsub.UpdatedEvent, updatedFile) return updatedFile, nil } func (s *service) Delete(ctx context.Context, id string) error { file, err := s.Get(ctx, id) if err != nil { return err } err = s.q.DeleteFile(ctx, id) if err != nil { return err } s.Publish(pubsub.DeletedEvent, file) return nil } func (s *service) DeleteSessionFiles(ctx context.Context, sessionID string) error { files, err := s.ListBySession(ctx, sessionID) if err != nil { return err } for _, file := range files { err = s.Delete(ctx, file.ID) if err != nil { return err } } return nil } func (s *service) fromDBItem(item db.File) File { return File{ ID: item.ID, SessionID: item.SessionID, Path: item.Path, Content: item.Content, Version: item.Version, CreatedAt: item.CreatedAt, UpdatedAt: item.UpdatedAt, } } ``` ## /internal/llm/agent/agent-tool.go ```go path="/internal/llm/agent/agent-tool.go" package agent import ( "context" "encoding/json" "fmt" "github.com/opencode-ai/opencode/internal/config" "github.com/opencode-ai/opencode/internal/llm/tools" "github.com/opencode-ai/opencode/internal/lsp" "github.com/opencode-ai/opencode/internal/message" "github.com/opencode-ai/opencode/internal/session" ) type agentTool struct { sessions session.Service messages message.Service lspClients map[string]*lsp.Client } const ( AgentToolName = "agent" ) type AgentParams struct { Prompt string `json:"prompt"` } func (b *agentTool) Info() tools.ToolInfo { return tools.ToolInfo{ Name: AgentToolName, Description: "Launch a new agent that has access to the following tools: GlobTool, GrepTool, LS, View. When you are searching for a keyword or file and are not confident that you will find the right match on the first try, use the Agent tool to perform the search for you. For example:\n\n- If you are searching for a keyword like \"config\" or \"logger\", or for questions like \"which file does X?\", the Agent tool is strongly recommended\n- If you want to read a specific file path, use the View or GlobTool tool instead of the Agent tool, to find the match more quickly\n- If you are searching for a specific class definition like \"class Foo\", use the GlobTool tool instead, to find the match more quickly\n\nUsage notes:\n1. Launch multiple agents concurrently whenever possible, to maximize performance; to do that, use a single message with multiple tool uses\n2. When the agent is done, it will return a single message back to you. The result returned by the agent is not visible to the user. To show the user the result, you should send a text message back to the user with a concise summary of the result.\n3. Each agent invocation is stateless. You will not be able to send additional messages to the agent, nor will the agent be able to communicate with you outside of its final report. Therefore, your prompt should contain a highly detailed task description for the agent to perform autonomously and you should specify exactly what information the agent should return back to you in its final and only message to you.\n4. The agent's outputs should generally be trusted\n5. IMPORTANT: The agent can not use Bash, Replace, Edit, so can not modify files. If you want to use these tools, use them directly instead of going through the agent.", Parameters: map[string]any{ "prompt": map[string]any{ "type": "string", "description": "The task for the agent to perform", }, }, Required: []string{"prompt"}, } } func (b *agentTool) Run(ctx context.Context, call tools.ToolCall) (tools.ToolResponse, error) { var params AgentParams if err := json.Unmarshal([]byte(call.Input), ¶ms); err != nil { return tools.NewTextErrorResponse(fmt.Sprintf("error parsing parameters: %s", err)), nil } if params.Prompt == "" { return tools.NewTextErrorResponse("prompt is required"), nil } sessionID, messageID := tools.GetContextValues(ctx) if sessionID == "" || messageID == "" { return tools.ToolResponse{}, fmt.Errorf("session_id and message_id are required") } agent, err := NewAgent(config.AgentTask, b.sessions, b.messages, TaskAgentTools(b.lspClients)) if err != nil { return tools.ToolResponse{}, fmt.Errorf("error creating agent: %s", err) } session, err := b.sessions.CreateTaskSession(ctx, call.ID, sessionID, "New Agent Session") if err != nil { return tools.ToolResponse{}, fmt.Errorf("error creating session: %s", err) } done, err := agent.Run(ctx, session.ID, params.Prompt) if err != nil { return tools.ToolResponse{}, fmt.Errorf("error generating agent: %s", err) } result := <-done if result.Error != nil { return tools.ToolResponse{}, fmt.Errorf("error generating agent: %s", result.Error) } response := result.Message if response.Role != message.Assistant { return tools.NewTextErrorResponse("no response"), nil } updatedSession, err := b.sessions.Get(ctx, session.ID) if err != nil { return tools.ToolResponse{}, fmt.Errorf("error getting session: %s", err) } parentSession, err := b.sessions.Get(ctx, sessionID) if err != nil { return tools.ToolResponse{}, fmt.Errorf("error getting parent session: %s", err) } parentSession.Cost += updatedSession.Cost _, err = b.sessions.Save(ctx, parentSession) if err != nil { return tools.ToolResponse{}, fmt.Errorf("error saving parent session: %s", err) } return tools.NewTextResponse(response.Content().String()), nil } func NewAgentTool( Sessions session.Service, Messages message.Service, LspClients map[string]*lsp.Client, ) tools.BaseTool { return &agentTool{ sessions: Sessions, messages: Messages, lspClients: LspClients, } } ``` ## /internal/llm/agent/mcp-tools.go ```go path="/internal/llm/agent/mcp-tools.go" package agent import ( "context" "encoding/json" "fmt" "github.com/opencode-ai/opencode/internal/config" "github.com/opencode-ai/opencode/internal/llm/tools" "github.com/opencode-ai/opencode/internal/logging" "github.com/opencode-ai/opencode/internal/permission" "github.com/opencode-ai/opencode/internal/version" "github.com/mark3labs/mcp-go/client" "github.com/mark3labs/mcp-go/mcp" ) type mcpTool struct { mcpName string tool mcp.Tool mcpConfig config.MCPServer permissions permission.Service } type MCPClient interface { Initialize( ctx context.Context, request mcp.InitializeRequest, ) (*mcp.InitializeResult, error) ListTools(ctx context.Context, request mcp.ListToolsRequest) (*mcp.ListToolsResult, error) CallTool(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) Close() error } func (b *mcpTool) Info() tools.ToolInfo { return tools.ToolInfo{ Name: fmt.Sprintf("%s_%s", b.mcpName, b.tool.Name), Description: b.tool.Description, Parameters: b.tool.InputSchema.Properties, Required: b.tool.InputSchema.Required, } } func runTool(ctx context.Context, c MCPClient, toolName string, input string) (tools.ToolResponse, error) { defer c.Close() initRequest := mcp.InitializeRequest{} initRequest.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION initRequest.Params.ClientInfo = mcp.Implementation{ Name: "OpenCode", Version: version.Version, } _, err := c.Initialize(ctx, initRequest) if err != nil { return tools.NewTextErrorResponse(err.Error()), nil } toolRequest := mcp.CallToolRequest{} toolRequest.Params.Name = toolName var args map[string]any if err = json.Unmarshal([]byte(input), &args); err != nil { return tools.NewTextErrorResponse(fmt.Sprintf("error parsing parameters: %s", err)), nil } toolRequest.Params.Arguments = args result, err := c.CallTool(ctx, toolRequest) if err != nil { return tools.NewTextErrorResponse(err.Error()), nil } output := "" for _, v := range result.Content { if v, ok := v.(mcp.TextContent); ok { output = v.Text } else { output = fmt.Sprintf("%v", v) } } return tools.NewTextResponse(output), nil } func (b *mcpTool) Run(ctx context.Context, params tools.ToolCall) (tools.ToolResponse, error) { sessionID, messageID := tools.GetContextValues(ctx) if sessionID == "" || messageID == "" { return tools.ToolResponse{}, fmt.Errorf("session ID and message ID are required for creating a new file") } permissionDescription := fmt.Sprintf("execute %s with the following parameters: %s", b.Info().Name, params.Input) p := b.permissions.Request( permission.CreatePermissionRequest{ SessionID: sessionID, Path: config.WorkingDirectory(), ToolName: b.Info().Name, Action: "execute", Description: permissionDescription, Params: params.Input, }, ) if !p { return tools.NewTextErrorResponse("permission denied"), nil } switch b.mcpConfig.Type { case config.MCPStdio: c, err := client.NewStdioMCPClient( b.mcpConfig.Command, b.mcpConfig.Env, b.mcpConfig.Args..., ) if err != nil { return tools.NewTextErrorResponse(err.Error()), nil } return runTool(ctx, c, b.tool.Name, params.Input) case config.MCPSse: c, err := client.NewSSEMCPClient( b.mcpConfig.URL, client.WithHeaders(b.mcpConfig.Headers), ) if err != nil { return tools.NewTextErrorResponse(err.Error()), nil } return runTool(ctx, c, b.tool.Name, params.Input) } return tools.NewTextErrorResponse("invalid mcp type"), nil } func NewMcpTool(name string, tool mcp.Tool, permissions permission.Service, mcpConfig config.MCPServer) tools.BaseTool { return &mcpTool{ mcpName: name, tool: tool, mcpConfig: mcpConfig, permissions: permissions, } } var mcpTools []tools.BaseTool func getTools(ctx context.Context, name string, m config.MCPServer, permissions permission.Service, c MCPClient) []tools.BaseTool { var stdioTools []tools.BaseTool initRequest := mcp.InitializeRequest{} initRequest.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION initRequest.Params.ClientInfo = mcp.Implementation{ Name: "OpenCode", Version: version.Version, } _, err := c.Initialize(ctx, initRequest) if err != nil { logging.Error("error initializing mcp client", "error", err) return stdioTools } toolsRequest := mcp.ListToolsRequest{} tools, err := c.ListTools(ctx, toolsRequest) if err != nil { logging.Error("error listing tools", "error", err) return stdioTools } for _, t := range tools.Tools { stdioTools = append(stdioTools, NewMcpTool(name, t, permissions, m)) } defer c.Close() return stdioTools } func GetMcpTools(ctx context.Context, permissions permission.Service) []tools.BaseTool { if len(mcpTools) > 0 { return mcpTools } for name, m := range config.Get().MCPServers { switch m.Type { case config.MCPStdio: c, err := client.NewStdioMCPClient( m.Command, m.Env, m.Args..., ) if err != nil { logging.Error("error creating mcp client", "error", err) continue } mcpTools = append(mcpTools, getTools(ctx, name, m, permissions, c)...) case config.MCPSse: c, err := client.NewSSEMCPClient( m.URL, client.WithHeaders(m.Headers), ) if err != nil { logging.Error("error creating mcp client", "error", err) continue } mcpTools = append(mcpTools, getTools(ctx, name, m, permissions, c)...) } } return mcpTools } ``` ## /internal/llm/agent/tools.go ```go path="/internal/llm/agent/tools.go" package agent import ( "context" "github.com/opencode-ai/opencode/internal/history" "github.com/opencode-ai/opencode/internal/llm/tools" "github.com/opencode-ai/opencode/internal/lsp" "github.com/opencode-ai/opencode/internal/message" "github.com/opencode-ai/opencode/internal/permission" "github.com/opencode-ai/opencode/internal/session" ) func CoderAgentTools( permissions permission.Service, sessions session.Service, messages message.Service, history history.Service, lspClients map[string]*lsp.Client, ) []tools.BaseTool { ctx := context.Background() otherTools := GetMcpTools(ctx, permissions) if len(lspClients) > 0 { otherTools = append(otherTools, tools.NewDiagnosticsTool(lspClients)) } return append( []tools.BaseTool{ tools.NewBashTool(permissions), tools.NewEditTool(lspClients, permissions, history), tools.NewFetchTool(permissions), tools.NewGlobTool(), tools.NewGrepTool(), tools.NewLsTool(), tools.NewSourcegraphTool(), tools.NewViewTool(lspClients), tools.NewPatchTool(lspClients, permissions, history), tools.NewWriteTool(lspClients, permissions, history), NewAgentTool(sessions, messages, lspClients), }, otherTools..., ) } func TaskAgentTools(lspClients map[string]*lsp.Client) []tools.BaseTool { return []tools.BaseTool{ tools.NewGlobTool(), tools.NewGrepTool(), tools.NewLsTool(), tools.NewSourcegraphTool(), tools.NewViewTool(lspClients), } } ``` ## /internal/llm/models/anthropic.go ```go path="/internal/llm/models/anthropic.go" package models const ( ProviderAnthropic ModelProvider = "anthropic" // Models Claude35Sonnet ModelID = "claude-3.5-sonnet" Claude3Haiku ModelID = "claude-3-haiku" Claude37Sonnet ModelID = "claude-3.7-sonnet" Claude35Haiku ModelID = "claude-3.5-haiku" Claude3Opus ModelID = "claude-3-opus" Claude4Opus ModelID = "claude-4-opus" Claude4Sonnet ModelID = "claude-4-sonnet" ) // https://docs.anthropic.com/en/docs/about-claude/models/all-models var AnthropicModels = map[ModelID]Model{ Claude35Sonnet: { ID: Claude35Sonnet, Name: "Claude 3.5 Sonnet", Provider: ProviderAnthropic, APIModel: "claude-3-5-sonnet-latest", CostPer1MIn: 3.0, CostPer1MInCached: 3.75, CostPer1MOutCached: 0.30, CostPer1MOut: 15.0, ContextWindow: 200000, DefaultMaxTokens: 5000, SupportsAttachments: true, }, Claude3Haiku: { ID: Claude3Haiku, Name: "Claude 3 Haiku", Provider: ProviderAnthropic, APIModel: "claude-3-haiku-20240307", // doesn't support "-latest" CostPer1MIn: 0.25, CostPer1MInCached: 0.30, CostPer1MOutCached: 0.03, CostPer1MOut: 1.25, ContextWindow: 200000, DefaultMaxTokens: 4096, SupportsAttachments: true, }, Claude37Sonnet: { ID: Claude37Sonnet, Name: "Claude 3.7 Sonnet", Provider: ProviderAnthropic, APIModel: "claude-3-7-sonnet-latest", CostPer1MIn: 3.0, CostPer1MInCached: 3.75, CostPer1MOutCached: 0.30, CostPer1MOut: 15.0, ContextWindow: 200000, DefaultMaxTokens: 50000, CanReason: true, SupportsAttachments: true, }, Claude35Haiku: { ID: Claude35Haiku, Name: "Claude 3.5 Haiku", Provider: ProviderAnthropic, APIModel: "claude-3-5-haiku-latest", CostPer1MIn: 0.80, CostPer1MInCached: 1.0, CostPer1MOutCached: 0.08, CostPer1MOut: 4.0, ContextWindow: 200000, DefaultMaxTokens: 4096, SupportsAttachments: true, }, Claude3Opus: { ID: Claude3Opus, Name: "Claude 3 Opus", Provider: ProviderAnthropic, APIModel: "claude-3-opus-latest", CostPer1MIn: 15.0, CostPer1MInCached: 18.75, CostPer1MOutCached: 1.50, CostPer1MOut: 75.0, ContextWindow: 200000, DefaultMaxTokens: 4096, SupportsAttachments: true, }, Claude4Sonnet: { ID: Claude4Sonnet, Name: "Claude 4 Sonnet", Provider: ProviderAnthropic, APIModel: "claude-sonnet-4-20250514", CostPer1MIn: 3.0, CostPer1MInCached: 3.75, CostPer1MOutCached: 0.30, CostPer1MOut: 15.0, ContextWindow: 200000, DefaultMaxTokens: 50000, CanReason: true, SupportsAttachments: true, }, Claude4Opus: { ID: Claude4Opus, Name: "Claude 4 Opus", Provider: ProviderAnthropic, APIModel: "claude-opus-4-20250514", CostPer1MIn: 15.0, CostPer1MInCached: 18.75, CostPer1MOutCached: 1.50, CostPer1MOut: 75.0, ContextWindow: 200000, DefaultMaxTokens: 4096, SupportsAttachments: true, }, } ``` ## /internal/llm/models/gemini.go ```go path="/internal/llm/models/gemini.go" package models const ( ProviderGemini ModelProvider = "gemini" // Models Gemini25Flash ModelID = "gemini-2.5-flash" Gemini25 ModelID = "gemini-2.5" Gemini20Flash ModelID = "gemini-2.0-flash" Gemini20FlashLite ModelID = "gemini-2.0-flash-lite" ) var GeminiModels = map[ModelID]Model{ Gemini25Flash: { ID: Gemini25Flash, Name: "Gemini 2.5 Flash", Provider: ProviderGemini, APIModel: "gemini-2.5-flash-preview-04-17", CostPer1MIn: 0.15, CostPer1MInCached: 0, CostPer1MOutCached: 0, CostPer1MOut: 0.60, ContextWindow: 1000000, DefaultMaxTokens: 50000, SupportsAttachments: true, }, Gemini25: { ID: Gemini25, Name: "Gemini 2.5 Pro", Provider: ProviderGemini, APIModel: "gemini-2.5-pro-preview-05-06", CostPer1MIn: 1.25, CostPer1MInCached: 0, CostPer1MOutCached: 0, CostPer1MOut: 10, ContextWindow: 1000000, DefaultMaxTokens: 50000, SupportsAttachments: true, }, Gemini20Flash: { ID: Gemini20Flash, Name: "Gemini 2.0 Flash", Provider: ProviderGemini, APIModel: "gemini-2.0-flash", CostPer1MIn: 0.10, CostPer1MInCached: 0, CostPer1MOutCached: 0, CostPer1MOut: 0.40, ContextWindow: 1000000, DefaultMaxTokens: 6000, SupportsAttachments: true, }, Gemini20FlashLite: { ID: Gemini20FlashLite, Name: "Gemini 2.0 Flash Lite", Provider: ProviderGemini, APIModel: "gemini-2.0-flash-lite", CostPer1MIn: 0.05, CostPer1MInCached: 0, CostPer1MOutCached: 0, CostPer1MOut: 0.30, ContextWindow: 1000000, DefaultMaxTokens: 6000, SupportsAttachments: true, }, } ``` ## /internal/llm/models/groq.go ```go path="/internal/llm/models/groq.go" package models const ( ProviderGROQ ModelProvider = "groq" // GROQ QWENQwq ModelID = "qwen-qwq" // GROQ preview models Llama4Scout ModelID = "meta-llama/llama-4-scout-17b-16e-instruct" Llama4Maverick ModelID = "meta-llama/llama-4-maverick-17b-128e-instruct" Llama3_3_70BVersatile ModelID = "llama-3.3-70b-versatile" DeepseekR1DistillLlama70b ModelID = "deepseek-r1-distill-llama-70b" ) var GroqModels = map[ModelID]Model{ // // GROQ QWENQwq: { ID: QWENQwq, Name: "Qwen Qwq", Provider: ProviderGROQ, APIModel: "qwen-qwq-32b", CostPer1MIn: 0.29, CostPer1MInCached: 0.275, CostPer1MOutCached: 0.0, CostPer1MOut: 0.39, ContextWindow: 128_000, DefaultMaxTokens: 50000, // for some reason, the groq api doesn't like the reasoningEffort parameter CanReason: false, SupportsAttachments: false, }, Llama4Scout: { ID: Llama4Scout, Name: "Llama4Scout", Provider: ProviderGROQ, APIModel: "meta-llama/llama-4-scout-17b-16e-instruct", CostPer1MIn: 0.11, CostPer1MInCached: 0, CostPer1MOutCached: 0, CostPer1MOut: 0.34, ContextWindow: 128_000, // 10M when? SupportsAttachments: true, }, Llama4Maverick: { ID: Llama4Maverick, Name: "Llama4Maverick", Provider: ProviderGROQ, APIModel: "meta-llama/llama-4-maverick-17b-128e-instruct", CostPer1MIn: 0.20, CostPer1MInCached: 0, CostPer1MOutCached: 0, CostPer1MOut: 0.20, ContextWindow: 128_000, SupportsAttachments: true, }, Llama3_3_70BVersatile: { ID: Llama3_3_70BVersatile, Name: "Llama3_3_70BVersatile", Provider: ProviderGROQ, APIModel: "llama-3.3-70b-versatile", CostPer1MIn: 0.59, CostPer1MInCached: 0, CostPer1MOutCached: 0, CostPer1MOut: 0.79, ContextWindow: 128_000, SupportsAttachments: false, }, DeepseekR1DistillLlama70b: { ID: DeepseekR1DistillLlama70b, Name: "DeepseekR1DistillLlama70b", Provider: ProviderGROQ, APIModel: "deepseek-r1-distill-llama-70b", CostPer1MIn: 0.75, CostPer1MInCached: 0, CostPer1MOutCached: 0, CostPer1MOut: 0.99, ContextWindow: 128_000, CanReason: true, SupportsAttachments: false, }, } ``` ## /internal/llm/models/vertexai.go ```go path="/internal/llm/models/vertexai.go" package models const ( ProviderVertexAI ModelProvider = "vertexai" // Models VertexAIGemini25Flash ModelID = "vertexai.gemini-2.5-flash" VertexAIGemini25 ModelID = "vertexai.gemini-2.5" ) var VertexAIGeminiModels = map[ModelID]Model{ VertexAIGemini25Flash: { ID: VertexAIGemini25Flash, Name: "VertexAI: Gemini 2.5 Flash", Provider: ProviderVertexAI, APIModel: "gemini-2.5-flash-preview-04-17", CostPer1MIn: GeminiModels[Gemini25Flash].CostPer1MIn, CostPer1MInCached: GeminiModels[Gemini25Flash].CostPer1MInCached, CostPer1MOut: GeminiModels[Gemini25Flash].CostPer1MOut, CostPer1MOutCached: GeminiModels[Gemini25Flash].CostPer1MOutCached, ContextWindow: GeminiModels[Gemini25Flash].ContextWindow, DefaultMaxTokens: GeminiModels[Gemini25Flash].DefaultMaxTokens, SupportsAttachments: true, }, VertexAIGemini25: { ID: VertexAIGemini25, Name: "VertexAI: Gemini 2.5 Pro", Provider: ProviderVertexAI, APIModel: "gemini-2.5-pro-preview-03-25", CostPer1MIn: GeminiModels[Gemini25].CostPer1MIn, CostPer1MInCached: GeminiModels[Gemini25].CostPer1MInCached, CostPer1MOut: GeminiModels[Gemini25].CostPer1MOut, CostPer1MOutCached: GeminiModels[Gemini25].CostPer1MOutCached, ContextWindow: GeminiModels[Gemini25].ContextWindow, DefaultMaxTokens: GeminiModels[Gemini25].DefaultMaxTokens, SupportsAttachments: true, }, } ``` The content has been capped at 50000 tokens. The user could consider applying other filters to refine the result. The better and more specific the context, the better the LLM can follow instructions. If the context seems verbose, the user can refine the filter using uithub. Thank you for using https://uithub.com - Perfect LLM context for any GitHub repo.