mirror of
https://fastgit.cc/https://github.com/anomalyco/opencode
synced 2026-05-02 23:04:07 +08:00
Compare commits
51 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a58e607c5f | ||
|
|
cd04c44517 | ||
|
|
88711db796 | ||
|
|
9fec8df7d0 | ||
|
|
603a3e3c71 | ||
|
|
e14de7a211 | ||
|
|
004cfe7e8e | ||
|
|
58705a1352 | ||
|
|
82de14371d | ||
|
|
61d9dc9511 | ||
|
|
76275e533e | ||
|
|
98e2910e82 | ||
|
|
2941137416 | ||
|
|
b3c0285db3 | ||
|
|
805aeff83c | ||
|
|
bce2ec5c10 | ||
|
|
292e9d90ca | ||
|
|
2b4441a0d1 | ||
|
|
8f3a94df92 | ||
|
|
4415220555 | ||
|
|
a3a04d8a54 | ||
|
|
792e2b164b | ||
|
|
5859dcdc00 | ||
|
|
3c2b0f4dd0 | ||
|
|
9738886620 | ||
|
|
f3dccad54b | ||
|
|
b3a8dbd0d9 | ||
|
|
d93694a979 | ||
|
|
8a4d4152ce | ||
|
|
f12386e558 | ||
|
|
94aeb7b7fe | ||
|
|
a35466cdb3 | ||
|
|
170c7ad67a | ||
|
|
7a62ab7675 | ||
|
|
1586d757dc | ||
|
|
d043526200 | ||
|
|
aaf0bc14ba | ||
|
|
f2d9bb7ee3 | ||
|
|
de41703e20 | ||
|
|
2c24bfb7b3 | ||
|
|
47a37b7dd6 | ||
|
|
bdbf31f0b9 | ||
|
|
4e6560efb9 | ||
|
|
f2f6efdd35 | ||
|
|
b106787a50 | ||
|
|
e1b2ce483f | ||
|
|
c42d94c465 | ||
|
|
f879a94c95 | ||
|
|
6d05d5a7c3 | ||
|
|
2c5003e3fc | ||
|
|
1b22acbc58 |
@@ -4,14 +4,19 @@ 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
|
||||
# this name template makes the OS and Arch compatible with the results of uname.
|
||||
name_template: >-
|
||||
opencode-
|
||||
{{- if eq .Os "darwin" }}mac-
|
||||
@@ -21,7 +26,6 @@ archives:
|
||||
{{- else if eq .Arch "#86" }}i386
|
||||
{{- else }}{{ .Arch }}{{ end }}
|
||||
{{- if .Arm }}v{{ .Arm }}{{ end }}
|
||||
# use zip for windows archives
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
@@ -32,9 +36,9 @@ snapshot:
|
||||
aurs:
|
||||
- name: opencode
|
||||
homepage: "https://github.com/opencode-ai/opencode"
|
||||
description: "Deploy anything"
|
||||
description: "terminal based agent that can build anything"
|
||||
maintainers:
|
||||
- "opencode <noreply@opencode.ai>"
|
||||
- "kujtimiihoxha <kujtimii.h@gmail.com>"
|
||||
license: "MIT"
|
||||
private_key: "{{ .Env.AUR_KEY }}"
|
||||
git_url: "ssh://aur@aur.archlinux.org/opencode-bin.git"
|
||||
@@ -49,7 +53,7 @@ brews:
|
||||
owner: opencode-ai
|
||||
name: homebrew-tap
|
||||
nfpms:
|
||||
- maintainer: opencode
|
||||
- maintainer: kujtimiihoxha
|
||||
description: terminal based agent that can build anything
|
||||
formats:
|
||||
- deb
|
||||
|
||||
159
README.md
159
README.md
@@ -1,4 +1,4 @@
|
||||
# OpenCode
|
||||
# ⌬ OpenCode
|
||||
|
||||
> **⚠️ 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.
|
||||
|
||||
@@ -11,7 +11,7 @@ OpenCode is a Go-based CLI application that brings AI assistance to your termina
|
||||
## 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, and Groq
|
||||
- **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
|
||||
@@ -22,9 +22,36 @@ OpenCode is a Go-based CLI application that brings AI assistance to your termina
|
||||
|
||||
## Installation
|
||||
|
||||
### Using the Install Script
|
||||
|
||||
```bash
|
||||
# Coming soon
|
||||
go install github.com/kujtimiihoxha/opencode@latest
|
||||
# Install the latest version
|
||||
curl -fsSL https://opencode.ai/install | bash
|
||||
|
||||
# Install a specific version
|
||||
curl -fsSL https://opencode.ai/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-bin
|
||||
|
||||
# Using paru
|
||||
paru -S opencode-bin
|
||||
```
|
||||
|
||||
### Using Go
|
||||
|
||||
```bash
|
||||
go install github.com/opencode-ai/opencode@latest
|
||||
```
|
||||
|
||||
## Configuration
|
||||
@@ -39,15 +66,19 @@ OpenCode looks for configuration in the following locations:
|
||||
|
||||
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 |
|
||||
| `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) |
|
||||
| Environment Variable | Purpose |
|
||||
|----------------------------|--------------------------------------------------------|
|
||||
| `ANTHROPIC_API_KEY` | For Claude models |
|
||||
| `OPENAI_API_KEY` | For OpenAI models |
|
||||
| `GEMINI_API_KEY` | For Google Gemini models |
|
||||
| `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 |
|
||||
|
||||
|
||||
### Configuration File Structure
|
||||
|
||||
@@ -64,6 +95,14 @@ You can configure OpenCode using environment variables:
|
||||
"anthropic": {
|
||||
"apiKey": "your-api-key",
|
||||
"disabled": false
|
||||
},
|
||||
"groq": {
|
||||
"apiKey": "your-api-key",
|
||||
"disabled": false
|
||||
},
|
||||
"openrouter": {
|
||||
"apiKey": "your-api-key",
|
||||
"disabled": false
|
||||
}
|
||||
},
|
||||
"agents": {
|
||||
@@ -131,6 +170,23 @@ OpenCode supports a variety of AI models from different providers:
|
||||
|
||||
- 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
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
@@ -164,6 +220,7 @@ opencode -c /path/to/project
|
||||
| `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
|
||||
@@ -193,6 +250,16 @@ opencode -c /path/to/project
|
||||
| `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 |
|
||||
@@ -251,6 +318,70 @@ OpenCode is built with a modular architecture:
|
||||
- **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
|
||||
|
||||
You can create commands that accept arguments by including the `$ARGUMENTS` placeholder in your command file:
|
||||
|
||||
```markdown
|
||||
RUN git show $ARGUMENTS
|
||||
```
|
||||
|
||||
When you run this command, OpenCode will prompt you to enter the text that should replace `$ARGUMENTS`.
|
||||
|
||||
### 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.
|
||||
|
||||
## 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.
|
||||
@@ -341,7 +472,7 @@ While the LSP client implementation supports the full LSP protocol (including co
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/kujtimiihoxha/opencode.git
|
||||
git clone https://github.com/opencode-ai/opencode.git
|
||||
cd opencode
|
||||
|
||||
# Build
|
||||
|
||||
21
cmd/root.go
21
cmd/root.go
@@ -8,14 +8,15 @@ import (
|
||||
"time"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/kujtimiihoxha/opencode/internal/app"
|
||||
"github.com/kujtimiihoxha/opencode/internal/config"
|
||||
"github.com/kujtimiihoxha/opencode/internal/db"
|
||||
"github.com/kujtimiihoxha/opencode/internal/llm/agent"
|
||||
"github.com/kujtimiihoxha/opencode/internal/logging"
|
||||
"github.com/kujtimiihoxha/opencode/internal/pubsub"
|
||||
"github.com/kujtimiihoxha/opencode/internal/tui"
|
||||
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/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"
|
||||
)
|
||||
|
||||
@@ -31,6 +32,10 @@ to assist developers in writing, debugging, and understanding code directly from
|
||||
cmd.Help()
|
||||
return nil
|
||||
}
|
||||
if cmd.Flag("version").Changed {
|
||||
fmt.Println(version.Version)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Load the config
|
||||
debug, _ := cmd.Flags().GetBool("debug")
|
||||
@@ -74,7 +79,6 @@ to assist developers in writing, debugging, and understanding code directly from
|
||||
program := tea.NewProgram(
|
||||
tui.New(app),
|
||||
tea.WithAltScreen(),
|
||||
tea.WithMouseCellMotion(),
|
||||
)
|
||||
|
||||
// Initialize MCP tools in the background
|
||||
@@ -247,6 +251,7 @@ func Execute() {
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/kujtimiihoxha/opencode/internal/config"
|
||||
"github.com/kujtimiihoxha/opencode/internal/llm/models"
|
||||
"github.com/opencode-ai/opencode/internal/config"
|
||||
"github.com/opencode-ai/opencode/internal/llm/models"
|
||||
)
|
||||
|
||||
// JSONSchemaType represents a JSON Schema type
|
||||
@@ -77,6 +77,50 @@ func generateSchema() map[string]any {
|
||||
"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",
|
||||
@@ -152,7 +196,9 @@ func generateSchema() map[string]any {
|
||||
string(models.ProviderOpenAI),
|
||||
string(models.ProviderGemini),
|
||||
string(models.ProviderGROQ),
|
||||
string(models.ProviderOpenRouter),
|
||||
string(models.ProviderBedrock),
|
||||
string(models.ProviderAzure),
|
||||
}
|
||||
|
||||
providerSchema["additionalProperties"].(map[string]any)["properties"].(map[string]any)["provider"] = map[string]any{
|
||||
@@ -259,4 +305,3 @@ func generateSchema() map[string]any {
|
||||
|
||||
return schema
|
||||
}
|
||||
|
||||
|
||||
77
go.mod
77
go.mod
@@ -1,53 +1,47 @@
|
||||
module github.com/kujtimiihoxha/opencode
|
||||
module github.com/opencode-ai/opencode
|
||||
|
||||
go 1.24.0
|
||||
|
||||
toolchain go1.24.2
|
||||
|
||||
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 v0.2.0-beta.2
|
||||
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.20.0
|
||||
github.com/charmbracelet/bubbletea v1.3.4
|
||||
github.com/charmbracelet/glamour v0.9.1
|
||||
github.com/charmbracelet/huh v0.6.0
|
||||
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-git/go-git/v5 v5.15.0
|
||||
github.com/go-logfmt/logfmt v0.6.0
|
||||
github.com/golang-migrate/migrate/v4 v4.18.2
|
||||
github.com/google/generative-ai-go v0.19.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/mattn/go-runewidth v0.0.16
|
||||
github.com/mattn/go-sqlite3 v1.14.24
|
||||
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
|
||||
google.golang.org/api v0.215.0
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.116.0 // indirect
|
||||
cloud.google.com/go/ai v0.8.0 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
cloud.google.com/go/auth v0.13.0 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.6 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.6.0 // indirect
|
||||
cloud.google.com/go/longrunning v0.5.7 // indirect
|
||||
dario.cat/mergo v1.0.0 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.1.6 // 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
|
||||
@@ -68,76 +62,67 @@ require (
|
||||
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/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
|
||||
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||
github.com/cloudflare/circl v1.6.1 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.4.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/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.6.2 // 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/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 // 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/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
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/mitchellh/hashstructure/v2 v2.0.2 // 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/pjbgf/sha1cd v0.3.2 // 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/skeema/knownhosts v1.3.1 // 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/xanzy/ssh-agent v0.3.3 // 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/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 // 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.29.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.29.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.29.0 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
go.uber.org/multierr v1.9.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/oauth2 v0.25.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
|
||||
golang.org/x/time v0.8.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 // indirect
|
||||
google.golang.org/grpc v1.67.3 // indirect
|
||||
google.golang.org/protobuf v1.36.1 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // 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
|
||||
)
|
||||
|
||||
181
go.sum
181
go.sum
@@ -1,26 +1,21 @@
|
||||
cloud.google.com/go v0.116.0 h1:B3fRrSDkLRt5qSHWe40ERJvhvnQwdZiHu0bJOpldweE=
|
||||
cloud.google.com/go v0.116.0/go.mod h1:cEPSRWPzZEswwdr9BxE6ChEn01dWlTaF05LiC2Xs70U=
|
||||
cloud.google.com/go/ai v0.8.0 h1:rXUEz8Wp2OlrM8r1bfmpF2+VKqc1VJpafE3HgzRnD/w=
|
||||
cloud.google.com/go/ai v0.8.0/go.mod h1:t3Dfk4cM61sytiggo2UyGsDVW3RF1qGZaUKDrZFyqkE=
|
||||
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/auth/oauth2adapt v0.2.6 h1:V6a6XDu2lTwPZWOawrAa9HUK+DB2zfJyTuciBG5hFkU=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.6/go.mod h1:AlmsELtlEBnaNTL7jCj8VQFLy6mbZv0s4Q7NGBeQ5E8=
|
||||
cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
|
||||
cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
|
||||
cloud.google.com/go/longrunning v0.5.7 h1:WLbHekDbjK1fVFD3ibpFFVoyizlLRl73I7YKuAKilhU=
|
||||
cloud.google.com/go/longrunning v0.5.7/go.mod h1:8GClkudohy1Fxm3owmBGid8W0pSgodEMwEAztp38Xng=
|
||||
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
|
||||
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||
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/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
|
||||
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
|
||||
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=
|
||||
@@ -31,12 +26,8 @@ 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/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||
github.com/anthropics/anthropic-sdk-go v0.2.0-beta.2 h1:h7qxtumNjKPWFv1QM/HJy60MteeW23iKeEtBoY7bYZk=
|
||||
github.com/anthropics/anthropic-sdk-go v0.2.0-beta.2/go.mod h1:AapDW22irxK2PSumZiQXYUFvsdQgkwIWlpESweWZI/c=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
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=
|
||||
@@ -85,8 +76,6 @@ github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4p
|
||||
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/huh v0.6.0 h1:mZM8VvZGuE0hoDXq6XLxRtgfWyTI3b2jZNKh0xWmax8=
|
||||
github.com/charmbracelet/huh v0.6.0/go.mod h1:GGNKeWCeNzKpEOh/OJD8WBwTQjV3prFAtQPpLv+AVwU=
|
||||
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=
|
||||
@@ -95,26 +84,18 @@ github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0G
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b h1:MnAMdlwSltxJyULnrYbkZpp4k58Co7Tah3ciKhSNo0Q=
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
|
||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4=
|
||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ=
|
||||
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
||||
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
||||
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
||||
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
|
||||
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
|
||||
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/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
|
||||
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
|
||||
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
||||
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
||||
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=
|
||||
@@ -123,16 +104,6 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
|
||||
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/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
||||
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
||||
github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=
|
||||
github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
||||
github.com/go-git/go-git/v5 v5.15.0 h1:f5Qn0W0F7ry1iN0ZwIU5m/n7/BKB4hiZfc+zlZx7ly0=
|
||||
github.com/go-git/go-git/v5 v5.15.0/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
|
||||
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=
|
||||
@@ -142,12 +113,10 @@ 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-migrate/migrate/v4 v4.18.2 h1:2VSCMz7x7mjyTXx3m2zPokOY82LTRgxK1yQYKo6wWQ8=
|
||||
github.com/golang-migrate/migrate/v4 v4.18.2/go.mod h1:2CM6tJvn2kqPXwnXO/d3rAQYiyoIm180VsO8PRX6Rpk=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
||||
github.com/google/generative-ai-go v0.19.0 h1:R71szggh8wHMCUlEMsW2A/3T+5LdEIkiaHSYgSpUgdg=
|
||||
github.com/google/generative-ai-go v0.19.0/go.mod h1:JYolL13VG7j79kM5BtHz4qwONHkeJQzOCkKXnpqtS/E=
|
||||
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=
|
||||
@@ -160,19 +129,12 @@ github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrk
|
||||
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/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
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/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
|
||||
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
||||
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=
|
||||
@@ -180,8 +142,8 @@ 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/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
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/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=
|
||||
@@ -195,12 +157,10 @@ github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+Ei
|
||||
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/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
|
||||
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
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/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
|
||||
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=
|
||||
@@ -209,19 +169,25 @@ 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/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
|
||||
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
|
||||
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/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
|
||||
github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
|
||||
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/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.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=
|
||||
@@ -237,9 +203,8 @@ github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAm
|
||||
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/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
|
||||
github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
|
||||
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=
|
||||
@@ -253,13 +218,14 @@ github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
|
||||
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.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
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=
|
||||
@@ -270,8 +236,6 @@ 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/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
|
||||
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
|
||||
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=
|
||||
@@ -282,35 +246,38 @@ 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/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 h1:r6I7RJCN86bpD/FQwedZ0vSixDpwuWREjW9oRMsmqDc=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0/go.mod h1:B9yO6b04uB80CzjedvewuqDhxJxi11s7/GtiGa8bAjI=
|
||||
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.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
|
||||
go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8=
|
||||
go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc=
|
||||
go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8=
|
||||
go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4=
|
||||
go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
|
||||
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
|
||||
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
|
||||
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.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
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-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
|
||||
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-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
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=
|
||||
@@ -320,23 +287,18 @@ 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/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
|
||||
golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
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-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/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-20220715151400-c0bba94af5f8/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=
|
||||
@@ -358,7 +320,6 @@ 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.6/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=
|
||||
@@ -366,31 +327,33 @@ 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/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
|
||||
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
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/api v0.215.0 h1:jdYF4qnyczlEz2ReWIsosNLDuzXyvFHJtI5gcr0J7t0=
|
||||
google.golang.org/api v0.215.0/go.mod h1:fta3CVtuJYOEdugLNWm6WodzOS8KdFckABwN4I40hzY=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 h1:TqExAhdPaB60Ux47Cn0oLV07rGnxZzIsaRhQaqS666A=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA=
|
||||
google.golang.org/grpc v1.67.3 h1:OgPcDAFKHnH8X3O4WcO4XUc8GRDeKsKReqbQtiCj7N8=
|
||||
google.golang.org/grpc v1.67.3/go.mod h1:YGaHCc6Oap+FzBJTZLBzkGSYt/cvGPFTPxkn7QfSU8s=
|
||||
google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
|
||||
google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
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/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
|
||||
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
||||
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=
|
||||
|
||||
180
install
Executable file
180
install
Executable file
@@ -0,0 +1,180 @@
|
||||
#!/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
|
||||
|
||||
@@ -7,15 +7,16 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/kujtimiihoxha/opencode/internal/config"
|
||||
"github.com/kujtimiihoxha/opencode/internal/db"
|
||||
"github.com/kujtimiihoxha/opencode/internal/history"
|
||||
"github.com/kujtimiihoxha/opencode/internal/llm/agent"
|
||||
"github.com/kujtimiihoxha/opencode/internal/logging"
|
||||
"github.com/kujtimiihoxha/opencode/internal/lsp"
|
||||
"github.com/kujtimiihoxha/opencode/internal/message"
|
||||
"github.com/kujtimiihoxha/opencode/internal/permission"
|
||||
"github.com/kujtimiihoxha/opencode/internal/session"
|
||||
"github.com/opencode-ai/opencode/internal/config"
|
||||
"github.com/opencode-ai/opencode/internal/db"
|
||||
"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 {
|
||||
@@ -49,6 +50,9 @@ func New(ctx context.Context, conn *sql.DB) (*App, error) {
|
||||
LSPClients: make(map[string]*lsp.Client),
|
||||
}
|
||||
|
||||
// Initialize theme based on configuration
|
||||
app.initTheme()
|
||||
|
||||
// Initialize LSP clients in the background
|
||||
go app.initLSPClients(ctx)
|
||||
|
||||
@@ -73,6 +77,22 @@ func New(ctx context.Context, conn *sql.DB) (*App, error) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// Shutdown performs a clean shutdown of the application
|
||||
func (app *App) Shutdown() {
|
||||
// Cancel all watcher goroutines
|
||||
|
||||
@@ -4,10 +4,10 @@ import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/kujtimiihoxha/opencode/internal/config"
|
||||
"github.com/kujtimiihoxha/opencode/internal/logging"
|
||||
"github.com/kujtimiihoxha/opencode/internal/lsp"
|
||||
"github.com/kujtimiihoxha/opencode/internal/lsp/watcher"
|
||||
"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) {
|
||||
|
||||
@@ -2,13 +2,15 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/kujtimiihoxha/opencode/internal/llm/models"
|
||||
"github.com/kujtimiihoxha/opencode/internal/logging"
|
||||
"github.com/opencode-ai/opencode/internal/llm/models"
|
||||
"github.com/opencode-ai/opencode/internal/logging"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
@@ -65,16 +67,23 @@ type LSPConfig struct {
|
||||
Options any `json:"options"`
|
||||
}
|
||||
|
||||
// TUIConfig defines the configuration for the Terminal User Interface.
|
||||
type TUIConfig struct {
|
||||
Theme string `json:"theme,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"`
|
||||
Debug bool `json:"debug,omitempty"`
|
||||
DebugLSP bool `json:"debugLSP,omitempty"`
|
||||
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"`
|
||||
Debug bool `json:"debug,omitempty"`
|
||||
DebugLSP bool `json:"debugLSP,omitempty"`
|
||||
ContextPaths []string `json:"contextPaths,omitempty"`
|
||||
TUI TUIConfig `json:"tui"`
|
||||
}
|
||||
|
||||
// Application constants
|
||||
@@ -82,8 +91,24 @@ 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
|
||||
|
||||
@@ -104,7 +129,6 @@ func Load(workingDir string, debug bool) (*Config, error) {
|
||||
|
||||
configureViper()
|
||||
setDefaults(debug)
|
||||
setProviderDefaults()
|
||||
|
||||
// Read global config
|
||||
if err := readConfig(viper.ReadInConfig()); err != nil {
|
||||
@@ -114,6 +138,8 @@ func Load(workingDir string, debug bool) (*Config, error) {
|
||||
// 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)
|
||||
@@ -185,6 +211,8 @@ func configureViper() {
|
||||
// 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")
|
||||
|
||||
if debug {
|
||||
viper.SetDefault("debug", true)
|
||||
@@ -195,17 +223,44 @@ func setDefaults(debug bool) {
|
||||
}
|
||||
}
|
||||
|
||||
// setProviderDefaults configures LLM provider defaults based on environment variables.
|
||||
// the default model priority is:
|
||||
// 1. Anthropic
|
||||
// 2. OpenAI
|
||||
// 3. Google Gemini
|
||||
// 4. Groq
|
||||
// 5. AWS Bedrock
|
||||
// setProviderDefaults configures LLM provider defaults based on provider provided by
|
||||
// environment variables and configuration file.
|
||||
func setProviderDefaults() {
|
||||
// Anthropic configuration
|
||||
// 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
|
||||
|
||||
// Anthropic configuration
|
||||
if key := viper.GetString("providers.anthropic.apiKey"); strings.TrimSpace(key) != "" {
|
||||
viper.SetDefault("agents.coder.model", models.Claude37Sonnet)
|
||||
viper.SetDefault("agents.task.model", models.Claude37Sonnet)
|
||||
viper.SetDefault("agents.title.model", models.Claude37Sonnet)
|
||||
@@ -213,8 +268,7 @@ func setProviderDefaults() {
|
||||
}
|
||||
|
||||
// OpenAI configuration
|
||||
if apiKey := os.Getenv("OPENAI_API_KEY"); apiKey != "" {
|
||||
viper.SetDefault("providers.openai.apiKey", apiKey)
|
||||
if key := viper.GetString("providers.openai.apiKey"); strings.TrimSpace(key) != "" {
|
||||
viper.SetDefault("agents.coder.model", models.GPT41)
|
||||
viper.SetDefault("agents.task.model", models.GPT41Mini)
|
||||
viper.SetDefault("agents.title.model", models.GPT41Mini)
|
||||
@@ -222,8 +276,7 @@ func setProviderDefaults() {
|
||||
}
|
||||
|
||||
// Google Gemini configuration
|
||||
if apiKey := os.Getenv("GEMINI_API_KEY"); apiKey != "" {
|
||||
viper.SetDefault("providers.gemini.apiKey", apiKey)
|
||||
if key := viper.GetString("providers.gemini.apiKey"); strings.TrimSpace(key) != "" {
|
||||
viper.SetDefault("agents.coder.model", models.Gemini25)
|
||||
viper.SetDefault("agents.task.model", models.Gemini25Flash)
|
||||
viper.SetDefault("agents.title.model", models.Gemini25Flash)
|
||||
@@ -231,14 +284,29 @@ func setProviderDefaults() {
|
||||
}
|
||||
|
||||
// Groq configuration
|
||||
if apiKey := os.Getenv("GROQ_API_KEY"); apiKey != "" {
|
||||
viper.SetDefault("providers.groq.apiKey", apiKey)
|
||||
if key := viper.GetString("providers.groq.apiKey"); strings.TrimSpace(key) != "" {
|
||||
viper.SetDefault("agents.coder.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.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.task.model", models.XAIGrok3Beta)
|
||||
viper.SetDefault("agents.title.model", models.XAiGrok3MiniFastBeta)
|
||||
return
|
||||
}
|
||||
|
||||
// AWS Bedrock configuration
|
||||
if hasAWSCredentials() {
|
||||
viper.SetDefault("agents.coder.model", models.BedrockClaude37Sonnet)
|
||||
@@ -246,6 +314,14 @@ func setProviderDefaults() {
|
||||
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.task.model", models.AzureGPT41Mini)
|
||||
viper.SetDefault("agents.title.model", models.AzureGPT41Mini)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// hasAWSCredentials checks if AWS credentials are available in the environment.
|
||||
@@ -312,60 +388,33 @@ func applyDefaultValues() {
|
||||
}
|
||||
}
|
||||
|
||||
// Validate checks if the configuration is valid and applies defaults where needed.
|
||||
// It validates model IDs and providers, ensuring they are supported.
|
||||
func Validate() error {
|
||||
if cfg == nil {
|
||||
return fmt.Errorf("config not loaded")
|
||||
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
|
||||
}
|
||||
|
||||
// Validate agent models
|
||||
for name, agent := range cfg.Agents {
|
||||
// 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)
|
||||
// Check if provider for the model is configured
|
||||
provider := model.Provider
|
||||
providerCfg, providerExists := cfg.Providers[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)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// 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",
|
||||
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)
|
||||
@@ -376,75 +425,110 @@ func Validate() error {
|
||||
} 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 = 4096 // Fallback default
|
||||
} else {
|
||||
// Add provider with API key from environment
|
||||
cfg.Providers[provider] = Provider{
|
||||
APIKey: apiKey,
|
||||
}
|
||||
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
|
||||
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)
|
||||
|
||||
// Validate reasoning effort for models that support reasoning
|
||||
if model.CanReason && provider == models.ProviderOpenAI {
|
||||
if agent.ReasoningEffort == "" {
|
||||
// Set default reasoning effort for models that support it
|
||||
logging.Info("setting default reasoning effort for model that supports reasoning",
|
||||
// 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 {
|
||||
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)
|
||||
"model", agent.Model,
|
||||
"reasoning_effort", agent.ReasoningEffort)
|
||||
|
||||
// Update the agent with default reasoning effort
|
||||
// Update the agent with valid 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)
|
||||
}
|
||||
} 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
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -480,6 +564,10 @@ func getProviderAPIKey(provider models.ModelProvider) string {
|
||||
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"
|
||||
@@ -531,6 +619,34 @@ func setDefaultModelForAgent(agent AgentName) bool {
|
||||
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)
|
||||
@@ -592,3 +708,95 @@ func WorkingDirectory() string {
|
||||
}
|
||||
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 nil
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
// 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 configMap map[string]interface{}
|
||||
if err := json.Unmarshal(configData, &configMap); err != nil {
|
||||
return fmt.Errorf("failed to parse config file: %w", err)
|
||||
}
|
||||
|
||||
// Update just the theme value
|
||||
tuiConfig, ok := configMap["tui"].(map[string]interface{})
|
||||
if !ok {
|
||||
// TUI config doesn't exist yet, create it
|
||||
configMap["tui"] = map[string]interface{}{"theme": themeName}
|
||||
} else {
|
||||
// Update existing TUI config
|
||||
tuiConfig["theme"] = themeName
|
||||
configMap["tui"] = tuiConfig
|
||||
}
|
||||
|
||||
// Write the updated config back to file
|
||||
updatedData, err := json.MarshalIndent(configMap, "", " ")
|
||||
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
|
||||
}
|
||||
|
||||
@@ -6,14 +6,13 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
"github.com/golang-migrate/migrate/v4/source/iofs"
|
||||
_ "github.com/ncruces/go-sqlite3/driver"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4/database/sqlite3"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"github.com/opencode-ai/opencode/internal/config"
|
||||
"github.com/opencode-ai/opencode/internal/logging"
|
||||
|
||||
"github.com/kujtimiihoxha/opencode/internal/config"
|
||||
"github.com/kujtimiihoxha/opencode/internal/logging"
|
||||
"github.com/pressly/goose/v3"
|
||||
)
|
||||
|
||||
func Connect() (*sql.DB, error) {
|
||||
@@ -54,38 +53,16 @@ func Connect() (*sql.DB, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize schema from embedded file
|
||||
d, err := iofs.New(FS, "migrations")
|
||||
if err != nil {
|
||||
logging.Error("Failed to open embedded migrations", "error", err)
|
||||
db.Close()
|
||||
return nil, fmt.Errorf("failed to open embedded migrations: %w", err)
|
||||
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)
|
||||
}
|
||||
|
||||
driver, err := sqlite3.WithInstance(db, &sqlite3.Config{})
|
||||
if err != nil {
|
||||
logging.Error("Failed to create SQLite driver", "error", err)
|
||||
db.Close()
|
||||
return nil, fmt.Errorf("failed to create SQLite driver: %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)
|
||||
}
|
||||
|
||||
m, err := migrate.NewWithInstance("iofs", d, "ql", driver)
|
||||
if err != nil {
|
||||
logging.Error("Failed to create migration instance", "error", err)
|
||||
db.Close()
|
||||
return nil, fmt.Errorf("failed to create migration instance: %w", err)
|
||||
}
|
||||
|
||||
err = m.Up()
|
||||
if err != nil && err != migrate.ErrNoChange {
|
||||
logging.Error("Migration failed", "error", err)
|
||||
db.Close()
|
||||
return nil, fmt.Errorf("failed to apply schema: %w", err)
|
||||
} else if err == migrate.ErrNoChange {
|
||||
logging.Info("No schema changes to apply")
|
||||
} else {
|
||||
logging.Info("Schema migration applied successfully")
|
||||
}
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
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;
|
||||
@@ -1,3 +1,5 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
-- Sessions
|
||||
CREATE TABLE IF NOT EXISTS sessions (
|
||||
id TEXT PRIMARY KEY,
|
||||
@@ -78,3 +80,19 @@ 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
|
||||
@@ -4,23 +4,19 @@ import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"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/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/plumbing/object"
|
||||
"github.com/kujtimiihoxha/opencode/internal/config"
|
||||
"github.com/kujtimiihoxha/opencode/internal/logging"
|
||||
"github.com/opencode-ai/opencode/internal/config"
|
||||
"github.com/opencode-ai/opencode/internal/tui/theme"
|
||||
"github.com/sergi/go-diff/diffmatchpatch"
|
||||
)
|
||||
|
||||
@@ -73,143 +69,6 @@ type linePair struct {
|
||||
right *DiffLine
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Style Configuration
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
// StyleConfig defines styling for diff rendering
|
||||
type StyleConfig struct {
|
||||
ShowHeader bool
|
||||
ShowHunkHeader bool
|
||||
FileNameFg lipgloss.Color
|
||||
// Background colors
|
||||
RemovedLineBg lipgloss.Color
|
||||
AddedLineBg lipgloss.Color
|
||||
ContextLineBg lipgloss.Color
|
||||
HunkLineBg lipgloss.Color
|
||||
RemovedLineNumberBg lipgloss.Color
|
||||
AddedLineNamerBg lipgloss.Color
|
||||
|
||||
// Foreground colors
|
||||
HunkLineFg lipgloss.Color
|
||||
RemovedFg lipgloss.Color
|
||||
AddedFg lipgloss.Color
|
||||
LineNumberFg lipgloss.Color
|
||||
RemovedHighlightFg lipgloss.Color
|
||||
AddedHighlightFg lipgloss.Color
|
||||
|
||||
// Highlight settings
|
||||
HighlightStyle string
|
||||
RemovedHighlightBg lipgloss.Color
|
||||
AddedHighlightBg lipgloss.Color
|
||||
}
|
||||
|
||||
// StyleOption is a function that modifies a StyleConfig
|
||||
type StyleOption func(*StyleConfig)
|
||||
|
||||
// NewStyleConfig creates a StyleConfig with default values
|
||||
func NewStyleConfig(opts ...StyleOption) StyleConfig {
|
||||
// Default color scheme
|
||||
config := StyleConfig{
|
||||
ShowHeader: true,
|
||||
ShowHunkHeader: true,
|
||||
FileNameFg: lipgloss.Color("#a0a0a0"),
|
||||
RemovedLineBg: lipgloss.Color("#3A3030"),
|
||||
AddedLineBg: lipgloss.Color("#303A30"),
|
||||
ContextLineBg: lipgloss.Color("#212121"),
|
||||
HunkLineBg: lipgloss.Color("#212121"),
|
||||
HunkLineFg: lipgloss.Color("#a0a0a0"),
|
||||
RemovedFg: lipgloss.Color("#7C4444"),
|
||||
AddedFg: lipgloss.Color("#478247"),
|
||||
LineNumberFg: lipgloss.Color("#888888"),
|
||||
HighlightStyle: "dracula",
|
||||
RemovedHighlightBg: lipgloss.Color("#612726"),
|
||||
AddedHighlightBg: lipgloss.Color("#256125"),
|
||||
RemovedLineNumberBg: lipgloss.Color("#332929"),
|
||||
AddedLineNamerBg: lipgloss.Color("#293229"),
|
||||
RemovedHighlightFg: lipgloss.Color("#FADADD"),
|
||||
AddedHighlightFg: lipgloss.Color("#DAFADA"),
|
||||
}
|
||||
|
||||
// Apply all provided options
|
||||
for _, opt := range opts {
|
||||
opt(&config)
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
// Style option functions
|
||||
func WithFileNameFg(color lipgloss.Color) StyleOption {
|
||||
return func(s *StyleConfig) { s.FileNameFg = color }
|
||||
}
|
||||
|
||||
func WithRemovedLineBg(color lipgloss.Color) StyleOption {
|
||||
return func(s *StyleConfig) { s.RemovedLineBg = color }
|
||||
}
|
||||
|
||||
func WithAddedLineBg(color lipgloss.Color) StyleOption {
|
||||
return func(s *StyleConfig) { s.AddedLineBg = color }
|
||||
}
|
||||
|
||||
func WithContextLineBg(color lipgloss.Color) StyleOption {
|
||||
return func(s *StyleConfig) { s.ContextLineBg = color }
|
||||
}
|
||||
|
||||
func WithRemovedFg(color lipgloss.Color) StyleOption {
|
||||
return func(s *StyleConfig) { s.RemovedFg = color }
|
||||
}
|
||||
|
||||
func WithAddedFg(color lipgloss.Color) StyleOption {
|
||||
return func(s *StyleConfig) { s.AddedFg = color }
|
||||
}
|
||||
|
||||
func WithLineNumberFg(color lipgloss.Color) StyleOption {
|
||||
return func(s *StyleConfig) { s.LineNumberFg = color }
|
||||
}
|
||||
|
||||
func WithHighlightStyle(style string) StyleOption {
|
||||
return func(s *StyleConfig) { s.HighlightStyle = style }
|
||||
}
|
||||
|
||||
func WithRemovedHighlightColors(bg, fg lipgloss.Color) StyleOption {
|
||||
return func(s *StyleConfig) {
|
||||
s.RemovedHighlightBg = bg
|
||||
s.RemovedHighlightFg = fg
|
||||
}
|
||||
}
|
||||
|
||||
func WithAddedHighlightColors(bg, fg lipgloss.Color) StyleOption {
|
||||
return func(s *StyleConfig) {
|
||||
s.AddedHighlightBg = bg
|
||||
s.AddedHighlightFg = fg
|
||||
}
|
||||
}
|
||||
|
||||
func WithRemovedLineNumberBg(color lipgloss.Color) StyleOption {
|
||||
return func(s *StyleConfig) { s.RemovedLineNumberBg = color }
|
||||
}
|
||||
|
||||
func WithAddedLineNumberBg(color lipgloss.Color) StyleOption {
|
||||
return func(s *StyleConfig) { s.AddedLineNamerBg = color }
|
||||
}
|
||||
|
||||
func WithHunkLineBg(color lipgloss.Color) StyleOption {
|
||||
return func(s *StyleConfig) { s.HunkLineBg = color }
|
||||
}
|
||||
|
||||
func WithHunkLineFg(color lipgloss.Color) StyleOption {
|
||||
return func(s *StyleConfig) { s.HunkLineFg = color }
|
||||
}
|
||||
|
||||
func WithShowHeader(show bool) StyleOption {
|
||||
return func(s *StyleConfig) { s.ShowHeader = show }
|
||||
}
|
||||
|
||||
func WithShowHunkHeader(show bool) StyleOption {
|
||||
return func(s *StyleConfig) { s.ShowHunkHeader = show }
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Parse Configuration
|
||||
// -------------------------------------------------------------------------
|
||||
@@ -238,7 +97,6 @@ func WithContextSize(size int) ParseOption {
|
||||
// SideBySideConfig configures the rendering of side-by-side diffs
|
||||
type SideBySideConfig struct {
|
||||
TotalWidth int
|
||||
Style StyleConfig
|
||||
}
|
||||
|
||||
// SideBySideOption modifies a SideBySideConfig
|
||||
@@ -248,7 +106,6 @@ type SideBySideOption func(*SideBySideConfig)
|
||||
func NewSideBySideConfig(opts ...SideBySideOption) SideBySideConfig {
|
||||
config := SideBySideConfig{
|
||||
TotalWidth: 160, // Default width for side-by-side view
|
||||
Style: NewStyleConfig(),
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
@@ -267,20 +124,6 @@ func WithTotalWidth(width int) SideBySideOption {
|
||||
}
|
||||
}
|
||||
|
||||
// WithStyle sets the styling configuration
|
||||
func WithStyle(style StyleConfig) SideBySideOption {
|
||||
return func(s *SideBySideConfig) {
|
||||
s.Style = style
|
||||
}
|
||||
}
|
||||
|
||||
// WithStyleOptions applies the specified style options
|
||||
func WithStyleOptions(opts ...StyleOption) SideBySideOption {
|
||||
return func(s *SideBySideConfig) {
|
||||
s.Style = NewStyleConfig(opts...)
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Diff Parsing
|
||||
// -------------------------------------------------------------------------
|
||||
@@ -387,7 +230,7 @@ func ParseUnifiedDiff(diff string) (DiffResult, error) {
|
||||
}
|
||||
|
||||
// HighlightIntralineChanges updates lines in a hunk to show character-level differences
|
||||
func HighlightIntralineChanges(h *Hunk, style StyleConfig) {
|
||||
func HighlightIntralineChanges(h *Hunk) {
|
||||
var updated []DiffLine
|
||||
dmp := diffmatchpatch.New()
|
||||
|
||||
@@ -481,6 +324,8 @@ func pairLines(lines []DiffLine) []linePair {
|
||||
|
||||
// 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 {
|
||||
@@ -496,93 +341,175 @@ func SyntaxHighlight(w io.Writer, source, fileName, formatter string, bg lipglos
|
||||
if f == nil {
|
||||
f = formatters.Fallback
|
||||
}
|
||||
theme := `
|
||||
<style name="vscode-dark-plus">
|
||||
<!-- Base colors -->
|
||||
<entry type="Background" style="bg:#1E1E1E"/>
|
||||
<entry type="Text" style="#D4D4D4"/>
|
||||
<entry type="Other" style="#D4D4D4"/>
|
||||
<entry type="Error" style="#F44747"/>
|
||||
<!-- Keywords - using the Control flow / Special keywords color -->
|
||||
<entry type="Keyword" style="#C586C0"/>
|
||||
<entry type="KeywordConstant" style="#4FC1FF"/>
|
||||
<entry type="KeywordDeclaration" style="#C586C0"/>
|
||||
<entry type="KeywordNamespace" style="#C586C0"/>
|
||||
<entry type="KeywordPseudo" style="#C586C0"/>
|
||||
<entry type="KeywordReserved" style="#C586C0"/>
|
||||
<entry type="KeywordType" style="#4EC9B0"/>
|
||||
<!-- Names -->
|
||||
<entry type="Name" style="#D4D4D4"/>
|
||||
<entry type="NameAttribute" style="#9CDCFE"/>
|
||||
<entry type="NameBuiltin" style="#4EC9B0"/>
|
||||
<entry type="NameBuiltinPseudo" style="#9CDCFE"/>
|
||||
<entry type="NameClass" style="#4EC9B0"/>
|
||||
<entry type="NameConstant" style="#4FC1FF"/>
|
||||
<entry type="NameDecorator" style="#DCDCAA"/>
|
||||
<entry type="NameEntity" style="#9CDCFE"/>
|
||||
<entry type="NameException" style="#4EC9B0"/>
|
||||
<entry type="NameFunction" style="#DCDCAA"/>
|
||||
<entry type="NameLabel" style="#C8C8C8"/>
|
||||
<entry type="NameNamespace" style="#4EC9B0"/>
|
||||
<entry type="NameOther" style="#9CDCFE"/>
|
||||
<entry type="NameTag" style="#569CD6"/>
|
||||
<entry type="NameVariable" style="#9CDCFE"/>
|
||||
<entry type="NameVariableClass" style="#9CDCFE"/>
|
||||
<entry type="NameVariableGlobal" style="#9CDCFE"/>
|
||||
<entry type="NameVariableInstance" style="#9CDCFE"/>
|
||||
<!-- Literals -->
|
||||
<entry type="Literal" style="#CE9178"/>
|
||||
<entry type="LiteralDate" style="#CE9178"/>
|
||||
<entry type="LiteralString" style="#CE9178"/>
|
||||
<entry type="LiteralStringBacktick" style="#CE9178"/>
|
||||
<entry type="LiteralStringChar" style="#CE9178"/>
|
||||
<entry type="LiteralStringDoc" style="#CE9178"/>
|
||||
<entry type="LiteralStringDouble" style="#CE9178"/>
|
||||
<entry type="LiteralStringEscape" style="#d7ba7d"/>
|
||||
<entry type="LiteralStringHeredoc" style="#CE9178"/>
|
||||
<entry type="LiteralStringInterpol" style="#CE9178"/>
|
||||
<entry type="LiteralStringOther" style="#CE9178"/>
|
||||
<entry type="LiteralStringRegex" style="#d16969"/>
|
||||
<entry type="LiteralStringSingle" style="#CE9178"/>
|
||||
<entry type="LiteralStringSymbol" style="#CE9178"/>
|
||||
<!-- Numbers - using the numberLiteral color -->
|
||||
<entry type="LiteralNumber" style="#b5cea8"/>
|
||||
<entry type="LiteralNumberBin" style="#b5cea8"/>
|
||||
<entry type="LiteralNumberFloat" style="#b5cea8"/>
|
||||
<entry type="LiteralNumberHex" style="#b5cea8"/>
|
||||
<entry type="LiteralNumberInteger" style="#b5cea8"/>
|
||||
<entry type="LiteralNumberIntegerLong" style="#b5cea8"/>
|
||||
<entry type="LiteralNumberOct" style="#b5cea8"/>
|
||||
<!-- Operators -->
|
||||
<entry type="Operator" style="#D4D4D4"/>
|
||||
<entry type="OperatorWord" style="#C586C0"/>
|
||||
<entry type="Punctuation" style="#D4D4D4"/>
|
||||
<!-- Comments - standard VSCode Dark+ comment color -->
|
||||
<entry type="Comment" style="#6A9955"/>
|
||||
<entry type="CommentHashbang" style="#6A9955"/>
|
||||
<entry type="CommentMultiline" style="#6A9955"/>
|
||||
<entry type="CommentSingle" style="#6A9955"/>
|
||||
<entry type="CommentSpecial" style="#6A9955"/>
|
||||
<entry type="CommentPreproc" style="#C586C0"/>
|
||||
<!-- Generic styles -->
|
||||
<entry type="Generic" style="#D4D4D4"/>
|
||||
<entry type="GenericDeleted" style="#F44747"/>
|
||||
<entry type="GenericEmph" style="italic #D4D4D4"/>
|
||||
<entry type="GenericError" style="#F44747"/>
|
||||
<entry type="GenericHeading" style="bold #D4D4D4"/>
|
||||
<entry type="GenericInserted" style="#b5cea8"/>
|
||||
<entry type="GenericOutput" style="#808080"/>
|
||||
<entry type="GenericPrompt" style="#D4D4D4"/>
|
||||
<entry type="GenericStrong" style="bold #D4D4D4"/>
|
||||
<entry type="GenericSubheading" style="bold #D4D4D4"/>
|
||||
<entry type="GenericTraceback" style="#F44747"/>
|
||||
<entry type="GenericUnderline" style="underline"/>
|
||||
<entry type="TextWhitespace" style="#D4D4D4"/>
|
||||
</style>
|
||||
`
|
||||
|
||||
r := strings.NewReader(theme)
|
||||
// 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 {
|
||||
@@ -604,6 +531,14 @@ func SyntaxHighlight(w io.Writer, source, fileName, formatter string, bg lipglos
|
||||
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
|
||||
@@ -615,11 +550,11 @@ func highlightLine(fileName string, line string, bg lipgloss.TerminalColor) stri
|
||||
}
|
||||
|
||||
// createStyles generates the lipgloss styles needed for rendering diffs
|
||||
func createStyles(config StyleConfig) (removedLineStyle, addedLineStyle, contextLineStyle, lineNumberStyle lipgloss.Style) {
|
||||
removedLineStyle = lipgloss.NewStyle().Background(config.RemovedLineBg)
|
||||
addedLineStyle = lipgloss.NewStyle().Background(config.AddedLineBg)
|
||||
contextLineStyle = lipgloss.NewStyle().Background(config.ContextLineBg)
|
||||
lineNumberStyle = lipgloss.NewStyle().Foreground(config.LineNumberFg)
|
||||
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
|
||||
}
|
||||
@@ -628,9 +563,20 @@ func createStyles(config StyleConfig) (removedLineStyle, addedLineStyle, context
|
||||
// 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.Color,
|
||||
) string {
|
||||
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)
|
||||
@@ -668,6 +614,10 @@ func applyHighlighting(content string, segments []Segment, segmentType LineType,
|
||||
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
|
||||
@@ -702,12 +652,16 @@ func applyHighlighting(content string, segments []Segment, segmentType LineType,
|
||||
// Get the current styling
|
||||
currentStyle := ansiSequences[currentPos]
|
||||
|
||||
// Apply background highlight
|
||||
// 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, _ := highlightBg.RGBA()
|
||||
r, g, b, _ = bgColor.RGBA()
|
||||
sb.WriteString(fmt.Sprintf("%d;%d;%dm", r>>8, g>>8, b>>8))
|
||||
sb.WriteString(char)
|
||||
sb.WriteString("\x1b[49m") // Reset only background
|
||||
// Reset foreground and background
|
||||
sb.WriteString("\x1b[39m")
|
||||
|
||||
// Reapply the original ANSI sequence
|
||||
sb.WriteString(currentStyle)
|
||||
@@ -724,22 +678,24 @@ func applyHighlighting(content string, segments []Segment, segmentType LineType,
|
||||
}
|
||||
|
||||
// renderLeftColumn formats the left side of a side-by-side diff
|
||||
func renderLeftColumn(fileName string, dl *DiffLine, colWidth int, styles StyleConfig) string {
|
||||
func renderLeftColumn(fileName string, dl *DiffLine, colWidth int) string {
|
||||
t := theme.CurrentTheme()
|
||||
|
||||
if dl == nil {
|
||||
contextLineStyle := lipgloss.NewStyle().Background(styles.ContextLineBg)
|
||||
contextLineStyle := lipgloss.NewStyle().Background(t.DiffContextBg())
|
||||
return contextLineStyle.Width(colWidth).Render("")
|
||||
}
|
||||
|
||||
removedLineStyle, _, contextLineStyle, lineNumberStyle := createStyles(styles)
|
||||
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(styles.RemovedFg).Render("-")
|
||||
marker = removedLineStyle.Foreground(t.DiffRemoved()).Render("-")
|
||||
bgStyle = removedLineStyle
|
||||
lineNumberStyle = lineNumberStyle.Foreground(styles.RemovedFg).Background(styles.RemovedLineNumberBg)
|
||||
lineNumberStyle = lineNumberStyle.Foreground(t.DiffRemoved()).Background(t.DiffRemovedLineNumberBg())
|
||||
case LineAdded:
|
||||
marker = "?"
|
||||
bgStyle = contextLineStyle
|
||||
@@ -762,7 +718,7 @@ func renderLeftColumn(fileName string, dl *DiffLine, colWidth int, styles StyleC
|
||||
|
||||
// Apply intra-line highlighting for removed lines
|
||||
if dl.Kind == LineRemoved && len(dl.Segments) > 0 {
|
||||
content = applyHighlighting(content, dl.Segments, LineRemoved, styles.RemovedHighlightBg)
|
||||
content = applyHighlighting(content, dl.Segments, LineRemoved, t.DiffHighlightRemoved())
|
||||
}
|
||||
|
||||
// Add a padding space for removed lines
|
||||
@@ -776,28 +732,30 @@ func renderLeftColumn(fileName string, dl *DiffLine, colWidth int, styles StyleC
|
||||
ansi.Truncate(
|
||||
lineText,
|
||||
colWidth,
|
||||
lipgloss.NewStyle().Background(styles.HunkLineBg).Foreground(styles.HunkLineFg).Render("..."),
|
||||
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, styles StyleConfig) string {
|
||||
func renderRightColumn(fileName string, dl *DiffLine, colWidth int) string {
|
||||
t := theme.CurrentTheme()
|
||||
|
||||
if dl == nil {
|
||||
contextLineStyle := lipgloss.NewStyle().Background(styles.ContextLineBg)
|
||||
contextLineStyle := lipgloss.NewStyle().Background(t.DiffContextBg())
|
||||
return contextLineStyle.Width(colWidth).Render("")
|
||||
}
|
||||
|
||||
_, addedLineStyle, contextLineStyle, lineNumberStyle := createStyles(styles)
|
||||
_, 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(styles.AddedFg).Render("+")
|
||||
marker = addedLineStyle.Foreground(t.DiffAdded()).Render("+")
|
||||
bgStyle = addedLineStyle
|
||||
lineNumberStyle = lineNumberStyle.Foreground(styles.AddedFg).Background(styles.AddedLineNamerBg)
|
||||
lineNumberStyle = lineNumberStyle.Foreground(t.DiffAdded()).Background(t.DiffAddedLineNumberBg())
|
||||
case LineRemoved:
|
||||
marker = "?"
|
||||
bgStyle = contextLineStyle
|
||||
@@ -820,7 +778,7 @@ func renderRightColumn(fileName string, dl *DiffLine, colWidth int, styles Style
|
||||
|
||||
// Apply intra-line highlighting for added lines
|
||||
if dl.Kind == LineAdded && len(dl.Segments) > 0 {
|
||||
content = applyHighlighting(content, dl.Segments, LineAdded, styles.AddedHighlightBg)
|
||||
content = applyHighlighting(content, dl.Segments, LineAdded, t.DiffHighlightAdded())
|
||||
}
|
||||
|
||||
// Add a padding space for added lines
|
||||
@@ -834,7 +792,7 @@ func renderRightColumn(fileName string, dl *DiffLine, colWidth int, styles Style
|
||||
ansi.Truncate(
|
||||
lineText,
|
||||
colWidth,
|
||||
lipgloss.NewStyle().Background(styles.HunkLineBg).Foreground(styles.HunkLineFg).Render("..."),
|
||||
lipgloss.NewStyle().Background(bgStyle.GetBackground()).Foreground(t.TextMuted()).Render("..."),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -853,7 +811,7 @@ func RenderSideBySideHunk(fileName string, h Hunk, opts ...SideBySideOption) str
|
||||
copy(hunkCopy.Lines, h.Lines)
|
||||
|
||||
// Highlight changes within lines
|
||||
HighlightIntralineChanges(&hunkCopy, config.Style)
|
||||
HighlightIntralineChanges(&hunkCopy)
|
||||
|
||||
// Pair lines for side-by-side display
|
||||
pairs := pairLines(hunkCopy.Lines)
|
||||
@@ -865,8 +823,8 @@ func RenderSideBySideHunk(fileName string, h Hunk, opts ...SideBySideOption) str
|
||||
rightWidth := config.TotalWidth - colWidth
|
||||
var sb strings.Builder
|
||||
for _, p := range pairs {
|
||||
leftStr := renderLeftColumn(fileName, p.left, leftWidth, config.Style)
|
||||
rightStr := renderRightColumn(fileName, p.right, rightWidth, config.Style)
|
||||
leftStr := renderLeftColumn(fileName, p.left, leftWidth)
|
||||
rightStr := renderRightColumn(fileName, p.right, rightWidth)
|
||||
sb.WriteString(leftStr + rightStr + "\n")
|
||||
}
|
||||
|
||||
@@ -881,54 +839,7 @@ func FormatDiff(diffText string, opts ...SideBySideOption) (string, error) {
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
config := NewSideBySideConfig(opts...)
|
||||
|
||||
if config.Style.ShowHeader {
|
||||
removeIcon := lipgloss.NewStyle().
|
||||
Background(config.Style.RemovedLineBg).
|
||||
Foreground(config.Style.RemovedFg).
|
||||
Render("⏹")
|
||||
addIcon := lipgloss.NewStyle().
|
||||
Background(config.Style.AddedLineBg).
|
||||
Foreground(config.Style.AddedFg).
|
||||
Render("⏹")
|
||||
|
||||
fileName := lipgloss.NewStyle().
|
||||
Background(config.Style.ContextLineBg).
|
||||
Foreground(config.Style.FileNameFg).
|
||||
Render(" " + diffResult.OldFile)
|
||||
sb.WriteString(
|
||||
lipgloss.NewStyle().
|
||||
Background(config.Style.ContextLineBg).
|
||||
Padding(0, 1, 0, 1).
|
||||
Foreground(config.Style.FileNameFg).
|
||||
BorderStyle(lipgloss.NormalBorder()).
|
||||
BorderTop(true).
|
||||
BorderBottom(true).
|
||||
BorderForeground(config.Style.FileNameFg).
|
||||
BorderBackground(config.Style.ContextLineBg).
|
||||
Width(config.TotalWidth).
|
||||
Render(
|
||||
lipgloss.JoinHorizontal(lipgloss.Top,
|
||||
removeIcon,
|
||||
addIcon,
|
||||
fileName,
|
||||
),
|
||||
) + "\n",
|
||||
)
|
||||
}
|
||||
|
||||
for _, h := range diffResult.Hunks {
|
||||
// Render hunk header
|
||||
if config.Style.ShowHunkHeader {
|
||||
sb.WriteString(
|
||||
lipgloss.NewStyle().
|
||||
Background(config.Style.HunkLineBg).
|
||||
Foreground(config.Style.HunkLineFg).
|
||||
Width(config.TotalWidth).
|
||||
Render(h.Header) + "\n",
|
||||
)
|
||||
}
|
||||
sb.WriteString(RenderSideBySideHunk(diffResult.OldFile, h, opts...))
|
||||
}
|
||||
|
||||
@@ -942,106 +853,21 @@ func GenerateDiff(beforeContent, afterContent, fileName string) (string, int, in
|
||||
cwd := config.WorkingDirectory()
|
||||
fileName = strings.TrimPrefix(fileName, cwd)
|
||||
fileName = strings.TrimPrefix(fileName, "/")
|
||||
// Create temporary directory for git operations
|
||||
tempDir, err := os.MkdirTemp("", fmt.Sprintf("git-diff-%d", time.Now().UnixNano()))
|
||||
if err != nil {
|
||||
logging.Error("Failed to create temp directory for git diff", "error", err)
|
||||
return "", 0, 0
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Initialize git repo
|
||||
repo, err := git.PlainInit(tempDir, false)
|
||||
if err != nil {
|
||||
logging.Error("Failed to initialize git repository", "error", err)
|
||||
return "", 0, 0
|
||||
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++
|
||||
}
|
||||
}
|
||||
|
||||
wt, err := repo.Worktree()
|
||||
if err != nil {
|
||||
logging.Error("Failed to get git worktree", "error", err)
|
||||
return "", 0, 0
|
||||
}
|
||||
|
||||
// Write the "before" content and commit it
|
||||
fullPath := filepath.Join(tempDir, fileName)
|
||||
if err = os.MkdirAll(filepath.Dir(fullPath), 0o755); err != nil {
|
||||
logging.Error("Failed to create directory for file", "error", err)
|
||||
return "", 0, 0
|
||||
}
|
||||
if err = os.WriteFile(fullPath, []byte(beforeContent), 0o644); err != nil {
|
||||
logging.Error("Failed to write before content to file", "error", err)
|
||||
return "", 0, 0
|
||||
}
|
||||
|
||||
_, err = wt.Add(fileName)
|
||||
if err != nil {
|
||||
logging.Error("Failed to add file to git", "error", err)
|
||||
return "", 0, 0
|
||||
}
|
||||
|
||||
beforeCommit, err := wt.Commit("Before", &git.CommitOptions{
|
||||
Author: &object.Signature{
|
||||
Name: "OpenCode",
|
||||
Email: "coder@opencode.ai",
|
||||
When: time.Now(),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
logging.Error("Failed to commit before content", "error", err)
|
||||
return "", 0, 0
|
||||
}
|
||||
|
||||
// Write the "after" content and commit it
|
||||
if err = os.WriteFile(fullPath, []byte(afterContent), 0o644); err != nil {
|
||||
logging.Error("Failed to write after content to file", "error", err)
|
||||
return "", 0, 0
|
||||
}
|
||||
|
||||
_, err = wt.Add(fileName)
|
||||
if err != nil {
|
||||
logging.Error("Failed to add file to git", "error", err)
|
||||
return "", 0, 0
|
||||
}
|
||||
|
||||
afterCommit, err := wt.Commit("After", &git.CommitOptions{
|
||||
Author: &object.Signature{
|
||||
Name: "OpenCode",
|
||||
Email: "coder@opencode.ai",
|
||||
When: time.Now(),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
logging.Error("Failed to commit after content", "error", err)
|
||||
return "", 0, 0
|
||||
}
|
||||
|
||||
// Get the diff between the two commits
|
||||
beforeCommitObj, err := repo.CommitObject(beforeCommit)
|
||||
if err != nil {
|
||||
logging.Error("Failed to get before commit object", "error", err)
|
||||
return "", 0, 0
|
||||
}
|
||||
|
||||
afterCommitObj, err := repo.CommitObject(afterCommit)
|
||||
if err != nil {
|
||||
logging.Error("Failed to get after commit object", "error", err)
|
||||
return "", 0, 0
|
||||
}
|
||||
|
||||
patch, err := beforeCommitObj.Patch(afterCommitObj)
|
||||
if err != nil {
|
||||
logging.Error("Failed to create git diff patch", "error", err)
|
||||
return "", 0, 0
|
||||
}
|
||||
|
||||
// Count additions and removals
|
||||
additions := 0
|
||||
removals := 0
|
||||
for _, fileStat := range patch.Stats() {
|
||||
additions += fileStat.Addition
|
||||
removals += fileStat.Deletion
|
||||
}
|
||||
|
||||
return patch.String(), additions, removals
|
||||
return unified, additions, removals
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/kujtimiihoxha/opencode/internal/db"
|
||||
"github.com/kujtimiihoxha/opencode/internal/pubsub"
|
||||
"github.com/opencode-ai/opencode/internal/db"
|
||||
"github.com/opencode-ai/opencode/internal/pubsub"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -5,11 +5,11 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/kujtimiihoxha/opencode/internal/config"
|
||||
"github.com/kujtimiihoxha/opencode/internal/llm/tools"
|
||||
"github.com/kujtimiihoxha/opencode/internal/lsp"
|
||||
"github.com/kujtimiihoxha/opencode/internal/message"
|
||||
"github.com/kujtimiihoxha/opencode/internal/session"
|
||||
"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 {
|
||||
|
||||
@@ -7,15 +7,15 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/kujtimiihoxha/opencode/internal/config"
|
||||
"github.com/kujtimiihoxha/opencode/internal/llm/models"
|
||||
"github.com/kujtimiihoxha/opencode/internal/llm/prompt"
|
||||
"github.com/kujtimiihoxha/opencode/internal/llm/provider"
|
||||
"github.com/kujtimiihoxha/opencode/internal/llm/tools"
|
||||
"github.com/kujtimiihoxha/opencode/internal/logging"
|
||||
"github.com/kujtimiihoxha/opencode/internal/message"
|
||||
"github.com/kujtimiihoxha/opencode/internal/permission"
|
||||
"github.com/kujtimiihoxha/opencode/internal/session"
|
||||
"github.com/opencode-ai/opencode/internal/config"
|
||||
"github.com/opencode-ai/opencode/internal/llm/models"
|
||||
"github.com/opencode-ai/opencode/internal/llm/prompt"
|
||||
"github.com/opencode-ai/opencode/internal/llm/provider"
|
||||
"github.com/opencode-ai/opencode/internal/llm/tools"
|
||||
"github.com/opencode-ai/opencode/internal/logging"
|
||||
"github.com/opencode-ai/opencode/internal/message"
|
||||
"github.com/opencode-ai/opencode/internal/permission"
|
||||
"github.com/opencode-ai/opencode/internal/session"
|
||||
)
|
||||
|
||||
// Common errors
|
||||
@@ -38,10 +38,11 @@ func (e *AgentEvent) Response() message.Message {
|
||||
}
|
||||
|
||||
type Service interface {
|
||||
Run(ctx context.Context, sessionID string, content string) (<-chan AgentEvent, error)
|
||||
Run(ctx context.Context, sessionID string, content string, attachments ...message.Attachment) (<-chan AgentEvent, error)
|
||||
Cancel(sessionID string)
|
||||
IsSessionBusy(sessionID string) bool
|
||||
IsBusy() bool
|
||||
Update(agentName config.AgentName, modelID models.ModelID) (models.Model, error)
|
||||
}
|
||||
|
||||
type agent struct {
|
||||
@@ -116,6 +117,9 @@ func (a *agent) IsSessionBusy(sessionID string) bool {
|
||||
}
|
||||
|
||||
func (a *agent) generateTitle(ctx context.Context, sessionID string, content string) error {
|
||||
if content == "" {
|
||||
return nil
|
||||
}
|
||||
if a.titleProvider == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -123,16 +127,13 @@ func (a *agent) generateTitle(ctx context.Context, sessionID string, content str
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
parts := []message.ContentPart{message.TextContent{Text: content}}
|
||||
response, err := a.titleProvider.SendMessages(
|
||||
ctx,
|
||||
[]message.Message{
|
||||
{
|
||||
Role: message.User,
|
||||
Parts: []message.ContentPart{
|
||||
message.TextContent{
|
||||
Text: content,
|
||||
},
|
||||
},
|
||||
Role: message.User,
|
||||
Parts: parts,
|
||||
},
|
||||
},
|
||||
make([]tools.BaseTool, 0),
|
||||
@@ -157,7 +158,10 @@ func (a *agent) err(err error) AgentEvent {
|
||||
}
|
||||
}
|
||||
|
||||
func (a *agent) Run(ctx context.Context, sessionID string, content string) (<-chan AgentEvent, error) {
|
||||
func (a *agent) Run(ctx context.Context, sessionID string, content string, attachments ...message.Attachment) (<-chan AgentEvent, error) {
|
||||
if !a.provider.Model().SupportsAttachments && attachments != nil {
|
||||
attachments = nil
|
||||
}
|
||||
events := make(chan AgentEvent)
|
||||
if a.IsSessionBusy(sessionID) {
|
||||
return nil, ErrSessionBusy
|
||||
@@ -171,10 +175,13 @@ func (a *agent) Run(ctx context.Context, sessionID string, content string) (<-ch
|
||||
defer logging.RecoverPanic("agent.Run", func() {
|
||||
events <- a.err(fmt.Errorf("panic while running the agent"))
|
||||
})
|
||||
|
||||
result := a.processGeneration(genCtx, sessionID, content)
|
||||
var attachmentParts []message.ContentPart
|
||||
for _, attachment := range attachments {
|
||||
attachmentParts = append(attachmentParts, message.BinaryContent{Path: attachment.FilePath, MIMEType: attachment.MimeType, Data: attachment.Content})
|
||||
}
|
||||
result := a.processGeneration(genCtx, sessionID, content, attachmentParts)
|
||||
if result.Err() != nil && !errors.Is(result.Err(), ErrRequestCancelled) && !errors.Is(result.Err(), context.Canceled) {
|
||||
logging.ErrorPersist(fmt.Sprintf("Generation error for session %s: %v", sessionID, result))
|
||||
logging.ErrorPersist(result.Err().Error())
|
||||
}
|
||||
logging.Debug("Request completed", "sessionID", sessionID)
|
||||
a.activeRequests.Delete(sessionID)
|
||||
@@ -185,7 +192,7 @@ func (a *agent) Run(ctx context.Context, sessionID string, content string) (<-ch
|
||||
return events, nil
|
||||
}
|
||||
|
||||
func (a *agent) processGeneration(ctx context.Context, sessionID, content string) AgentEvent {
|
||||
func (a *agent) processGeneration(ctx context.Context, sessionID, content string, attachmentParts []message.ContentPart) AgentEvent {
|
||||
// List existing messages; if none, start title generation asynchronously.
|
||||
msgs, err := a.messages.List(ctx, sessionID)
|
||||
if err != nil {
|
||||
@@ -203,13 +210,13 @@ func (a *agent) processGeneration(ctx context.Context, sessionID, content string
|
||||
}()
|
||||
}
|
||||
|
||||
userMsg, err := a.createUserMessage(ctx, sessionID, content)
|
||||
userMsg, err := a.createUserMessage(ctx, sessionID, content, attachmentParts)
|
||||
if err != nil {
|
||||
return a.err(fmt.Errorf("failed to create user message: %w", err))
|
||||
}
|
||||
|
||||
// Append the new user message to the conversation history.
|
||||
msgHistory := append(msgs, userMsg)
|
||||
|
||||
for {
|
||||
// Check for cancellation before each iteration
|
||||
select {
|
||||
@@ -239,12 +246,12 @@ func (a *agent) processGeneration(ctx context.Context, sessionID, content string
|
||||
}
|
||||
}
|
||||
|
||||
func (a *agent) createUserMessage(ctx context.Context, sessionID, content string) (message.Message, error) {
|
||||
func (a *agent) createUserMessage(ctx context.Context, sessionID, content string, attachmentParts []message.ContentPart) (message.Message, error) {
|
||||
parts := []message.ContentPart{message.TextContent{Text: content}}
|
||||
parts = append(parts, attachmentParts...)
|
||||
return a.messages.Create(ctx, sessionID, message.CreateMessageParams{
|
||||
Role: message.User,
|
||||
Parts: []message.ContentPart{
|
||||
message.TextContent{Text: content},
|
||||
},
|
||||
Role: message.User,
|
||||
Parts: parts,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -309,7 +316,6 @@ func (a *agent) streamAndHandleEvents(ctx context.Context, sessionID string, msg
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
toolResult, toolErr := tool.Run(ctx, tools.ToolCall{
|
||||
ID: toolCall.ID,
|
||||
Name: toolCall.Name,
|
||||
@@ -436,6 +442,25 @@ func (a *agent) TrackUsage(ctx context.Context, sessionID string, model models.M
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *agent) Update(agentName config.AgentName, modelID models.ModelID) (models.Model, error) {
|
||||
if a.IsBusy() {
|
||||
return models.Model{}, fmt.Errorf("cannot change model while processing requests")
|
||||
}
|
||||
|
||||
if err := config.UpdateAgentModel(agentName, modelID); err != nil {
|
||||
return models.Model{}, fmt.Errorf("failed to update config: %w", err)
|
||||
}
|
||||
|
||||
provider, err := createAgentProvider(agentName)
|
||||
if err != nil {
|
||||
return models.Model{}, fmt.Errorf("failed to create provider for model %s: %w", modelID, err)
|
||||
}
|
||||
|
||||
a.provider = provider
|
||||
|
||||
return a.provider.Model(), nil
|
||||
}
|
||||
|
||||
func createAgentProvider(agentName config.AgentName) (provider.Provider, error) {
|
||||
cfg := config.Get()
|
||||
agentConfig, ok := cfg.Agents[agentName]
|
||||
|
||||
@@ -5,11 +5,11 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/kujtimiihoxha/opencode/internal/config"
|
||||
"github.com/kujtimiihoxha/opencode/internal/llm/tools"
|
||||
"github.com/kujtimiihoxha/opencode/internal/logging"
|
||||
"github.com/kujtimiihoxha/opencode/internal/permission"
|
||||
"github.com/kujtimiihoxha/opencode/internal/version"
|
||||
"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"
|
||||
@@ -58,7 +58,7 @@ func runTool(ctx context.Context, c MCPClient, toolName string, input string) (t
|
||||
toolRequest := mcp.CallToolRequest{}
|
||||
toolRequest.Params.Name = toolName
|
||||
var args map[string]any
|
||||
if err = json.Unmarshal([]byte(input), &input); err != nil {
|
||||
if err = json.Unmarshal([]byte(input), &args); err != nil {
|
||||
return tools.NewTextErrorResponse(fmt.Sprintf("error parsing parameters: %s", err)), nil
|
||||
}
|
||||
toolRequest.Params.Arguments = args
|
||||
|
||||
@@ -3,12 +3,12 @@ package agent
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/kujtimiihoxha/opencode/internal/history"
|
||||
"github.com/kujtimiihoxha/opencode/internal/llm/tools"
|
||||
"github.com/kujtimiihoxha/opencode/internal/lsp"
|
||||
"github.com/kujtimiihoxha/opencode/internal/message"
|
||||
"github.com/kujtimiihoxha/opencode/internal/permission"
|
||||
"github.com/kujtimiihoxha/opencode/internal/session"
|
||||
"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(
|
||||
|
||||
@@ -11,67 +11,72 @@ const (
|
||||
Claude3Opus ModelID = "claude-3-opus"
|
||||
)
|
||||
|
||||
// https://docs.anthropic.com/en/docs/about-claude/models/all-models
|
||||
var AnthropicModels = map[ModelID]Model{
|
||||
// Anthropic
|
||||
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,
|
||||
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-latest",
|
||||
CostPer1MIn: 0.25,
|
||||
CostPer1MInCached: 0.30,
|
||||
CostPer1MOutCached: 0.03,
|
||||
CostPer1MOut: 1.25,
|
||||
ContextWindow: 200000,
|
||||
DefaultMaxTokens: 5000,
|
||||
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,
|
||||
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,
|
||||
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,
|
||||
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,
|
||||
},
|
||||
}
|
||||
|
||||
168
internal/llm/models/azure.go
Normal file
168
internal/llm/models/azure.go
Normal file
@@ -0,0 +1,168 @@
|
||||
package models
|
||||
|
||||
const ProviderAzure ModelProvider = "azure"
|
||||
|
||||
const (
|
||||
AzureGPT41 ModelID = "azure.gpt-4.1"
|
||||
AzureGPT41Mini ModelID = "azure.gpt-4.1-mini"
|
||||
AzureGPT41Nano ModelID = "azure.gpt-4.1-nano"
|
||||
AzureGPT45Preview ModelID = "azure.gpt-4.5-preview"
|
||||
AzureGPT4o ModelID = "azure.gpt-4o"
|
||||
AzureGPT4oMini ModelID = "azure.gpt-4o-mini"
|
||||
AzureO1 ModelID = "azure.o1"
|
||||
AzureO1Mini ModelID = "azure.o1-mini"
|
||||
AzureO3 ModelID = "azure.o3"
|
||||
AzureO3Mini ModelID = "azure.o3-mini"
|
||||
AzureO4Mini ModelID = "azure.o4-mini"
|
||||
)
|
||||
|
||||
var AzureModels = map[ModelID]Model{
|
||||
AzureGPT41: {
|
||||
ID: AzureGPT41,
|
||||
Name: "Azure OpenAI – GPT 4.1",
|
||||
Provider: ProviderAzure,
|
||||
APIModel: "gpt-4.1",
|
||||
CostPer1MIn: OpenAIModels[GPT41].CostPer1MIn,
|
||||
CostPer1MInCached: OpenAIModels[GPT41].CostPer1MInCached,
|
||||
CostPer1MOut: OpenAIModels[GPT41].CostPer1MOut,
|
||||
CostPer1MOutCached: OpenAIModels[GPT41].CostPer1MOutCached,
|
||||
ContextWindow: OpenAIModels[GPT41].ContextWindow,
|
||||
DefaultMaxTokens: OpenAIModels[GPT41].DefaultMaxTokens,
|
||||
SupportsAttachments: true,
|
||||
},
|
||||
AzureGPT41Mini: {
|
||||
ID: AzureGPT41Mini,
|
||||
Name: "Azure OpenAI – GPT 4.1 mini",
|
||||
Provider: ProviderAzure,
|
||||
APIModel: "gpt-4.1-mini",
|
||||
CostPer1MIn: OpenAIModels[GPT41Mini].CostPer1MIn,
|
||||
CostPer1MInCached: OpenAIModels[GPT41Mini].CostPer1MInCached,
|
||||
CostPer1MOut: OpenAIModels[GPT41Mini].CostPer1MOut,
|
||||
CostPer1MOutCached: OpenAIModels[GPT41Mini].CostPer1MOutCached,
|
||||
ContextWindow: OpenAIModels[GPT41Mini].ContextWindow,
|
||||
DefaultMaxTokens: OpenAIModels[GPT41Mini].DefaultMaxTokens,
|
||||
SupportsAttachments: true,
|
||||
},
|
||||
AzureGPT41Nano: {
|
||||
ID: AzureGPT41Nano,
|
||||
Name: "Azure OpenAI – GPT 4.1 nano",
|
||||
Provider: ProviderAzure,
|
||||
APIModel: "gpt-4.1-nano",
|
||||
CostPer1MIn: OpenAIModels[GPT41Nano].CostPer1MIn,
|
||||
CostPer1MInCached: OpenAIModels[GPT41Nano].CostPer1MInCached,
|
||||
CostPer1MOut: OpenAIModels[GPT41Nano].CostPer1MOut,
|
||||
CostPer1MOutCached: OpenAIModels[GPT41Nano].CostPer1MOutCached,
|
||||
ContextWindow: OpenAIModels[GPT41Nano].ContextWindow,
|
||||
DefaultMaxTokens: OpenAIModels[GPT41Nano].DefaultMaxTokens,
|
||||
SupportsAttachments: true,
|
||||
},
|
||||
AzureGPT45Preview: {
|
||||
ID: AzureGPT45Preview,
|
||||
Name: "Azure OpenAI – GPT 4.5 preview",
|
||||
Provider: ProviderAzure,
|
||||
APIModel: "gpt-4.5-preview",
|
||||
CostPer1MIn: OpenAIModels[GPT45Preview].CostPer1MIn,
|
||||
CostPer1MInCached: OpenAIModels[GPT45Preview].CostPer1MInCached,
|
||||
CostPer1MOut: OpenAIModels[GPT45Preview].CostPer1MOut,
|
||||
CostPer1MOutCached: OpenAIModels[GPT45Preview].CostPer1MOutCached,
|
||||
ContextWindow: OpenAIModels[GPT45Preview].ContextWindow,
|
||||
DefaultMaxTokens: OpenAIModels[GPT45Preview].DefaultMaxTokens,
|
||||
SupportsAttachments: true,
|
||||
},
|
||||
AzureGPT4o: {
|
||||
ID: AzureGPT4o,
|
||||
Name: "Azure OpenAI – GPT-4o",
|
||||
Provider: ProviderAzure,
|
||||
APIModel: "gpt-4o",
|
||||
CostPer1MIn: OpenAIModels[GPT4o].CostPer1MIn,
|
||||
CostPer1MInCached: OpenAIModels[GPT4o].CostPer1MInCached,
|
||||
CostPer1MOut: OpenAIModels[GPT4o].CostPer1MOut,
|
||||
CostPer1MOutCached: OpenAIModels[GPT4o].CostPer1MOutCached,
|
||||
ContextWindow: OpenAIModels[GPT4o].ContextWindow,
|
||||
DefaultMaxTokens: OpenAIModels[GPT4o].DefaultMaxTokens,
|
||||
SupportsAttachments: true,
|
||||
},
|
||||
AzureGPT4oMini: {
|
||||
ID: AzureGPT4oMini,
|
||||
Name: "Azure OpenAI – GPT-4o mini",
|
||||
Provider: ProviderAzure,
|
||||
APIModel: "gpt-4o-mini",
|
||||
CostPer1MIn: OpenAIModels[GPT4oMini].CostPer1MIn,
|
||||
CostPer1MInCached: OpenAIModels[GPT4oMini].CostPer1MInCached,
|
||||
CostPer1MOut: OpenAIModels[GPT4oMini].CostPer1MOut,
|
||||
CostPer1MOutCached: OpenAIModels[GPT4oMini].CostPer1MOutCached,
|
||||
ContextWindow: OpenAIModels[GPT4oMini].ContextWindow,
|
||||
DefaultMaxTokens: OpenAIModels[GPT4oMini].DefaultMaxTokens,
|
||||
SupportsAttachments: true,
|
||||
},
|
||||
AzureO1: {
|
||||
ID: AzureO1,
|
||||
Name: "Azure OpenAI – O1",
|
||||
Provider: ProviderAzure,
|
||||
APIModel: "o1",
|
||||
CostPer1MIn: OpenAIModels[O1].CostPer1MIn,
|
||||
CostPer1MInCached: OpenAIModels[O1].CostPer1MInCached,
|
||||
CostPer1MOut: OpenAIModels[O1].CostPer1MOut,
|
||||
CostPer1MOutCached: OpenAIModels[O1].CostPer1MOutCached,
|
||||
ContextWindow: OpenAIModels[O1].ContextWindow,
|
||||
DefaultMaxTokens: OpenAIModels[O1].DefaultMaxTokens,
|
||||
CanReason: OpenAIModels[O1].CanReason,
|
||||
SupportsAttachments: true,
|
||||
},
|
||||
AzureO1Mini: {
|
||||
ID: AzureO1Mini,
|
||||
Name: "Azure OpenAI – O1 mini",
|
||||
Provider: ProviderAzure,
|
||||
APIModel: "o1-mini",
|
||||
CostPer1MIn: OpenAIModels[O1Mini].CostPer1MIn,
|
||||
CostPer1MInCached: OpenAIModels[O1Mini].CostPer1MInCached,
|
||||
CostPer1MOut: OpenAIModels[O1Mini].CostPer1MOut,
|
||||
CostPer1MOutCached: OpenAIModels[O1Mini].CostPer1MOutCached,
|
||||
ContextWindow: OpenAIModels[O1Mini].ContextWindow,
|
||||
DefaultMaxTokens: OpenAIModels[O1Mini].DefaultMaxTokens,
|
||||
CanReason: OpenAIModels[O1Mini].CanReason,
|
||||
SupportsAttachments: true,
|
||||
},
|
||||
AzureO3: {
|
||||
ID: AzureO3,
|
||||
Name: "Azure OpenAI – O3",
|
||||
Provider: ProviderAzure,
|
||||
APIModel: "o3",
|
||||
CostPer1MIn: OpenAIModels[O3].CostPer1MIn,
|
||||
CostPer1MInCached: OpenAIModels[O3].CostPer1MInCached,
|
||||
CostPer1MOut: OpenAIModels[O3].CostPer1MOut,
|
||||
CostPer1MOutCached: OpenAIModels[O3].CostPer1MOutCached,
|
||||
ContextWindow: OpenAIModels[O3].ContextWindow,
|
||||
DefaultMaxTokens: OpenAIModels[O3].DefaultMaxTokens,
|
||||
CanReason: OpenAIModels[O3].CanReason,
|
||||
SupportsAttachments: true,
|
||||
},
|
||||
AzureO3Mini: {
|
||||
ID: AzureO3Mini,
|
||||
Name: "Azure OpenAI – O3 mini",
|
||||
Provider: ProviderAzure,
|
||||
APIModel: "o3-mini",
|
||||
CostPer1MIn: OpenAIModels[O3Mini].CostPer1MIn,
|
||||
CostPer1MInCached: OpenAIModels[O3Mini].CostPer1MInCached,
|
||||
CostPer1MOut: OpenAIModels[O3Mini].CostPer1MOut,
|
||||
CostPer1MOutCached: OpenAIModels[O3Mini].CostPer1MOutCached,
|
||||
ContextWindow: OpenAIModels[O3Mini].ContextWindow,
|
||||
DefaultMaxTokens: OpenAIModels[O3Mini].DefaultMaxTokens,
|
||||
CanReason: OpenAIModels[O3Mini].CanReason,
|
||||
SupportsAttachments: false,
|
||||
},
|
||||
AzureO4Mini: {
|
||||
ID: AzureO4Mini,
|
||||
Name: "Azure OpenAI – O4 mini",
|
||||
Provider: ProviderAzure,
|
||||
APIModel: "o4-mini",
|
||||
CostPer1MIn: OpenAIModels[O4Mini].CostPer1MIn,
|
||||
CostPer1MInCached: OpenAIModels[O4Mini].CostPer1MInCached,
|
||||
CostPer1MOut: OpenAIModels[O4Mini].CostPer1MOut,
|
||||
CostPer1MOutCached: OpenAIModels[O4Mini].CostPer1MOutCached,
|
||||
ContextWindow: OpenAIModels[O4Mini].ContextWindow,
|
||||
DefaultMaxTokens: OpenAIModels[O4Mini].DefaultMaxTokens,
|
||||
CanReason: OpenAIModels[O4Mini].CanReason,
|
||||
SupportsAttachments: true,
|
||||
},
|
||||
}
|
||||
@@ -12,52 +12,56 @@ const (
|
||||
|
||||
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,
|
||||
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-03-25",
|
||||
CostPer1MIn: 1.25,
|
||||
CostPer1MInCached: 0,
|
||||
CostPer1MOutCached: 0,
|
||||
CostPer1MOut: 10,
|
||||
ContextWindow: 1000000,
|
||||
DefaultMaxTokens: 50000,
|
||||
ID: Gemini25,
|
||||
Name: "Gemini 2.5 Pro",
|
||||
Provider: ProviderGemini,
|
||||
APIModel: "gemini-2.5-pro-preview-03-25",
|
||||
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,
|
||||
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,
|
||||
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,
|
||||
},
|
||||
}
|
||||
|
||||
87
internal/llm/models/groq.go
Normal file
87
internal/llm/models/groq.go
Normal file
@@ -0,0 +1,87 @@
|
||||
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,
|
||||
},
|
||||
}
|
||||
@@ -8,36 +8,43 @@ type (
|
||||
)
|
||||
|
||||
type Model struct {
|
||||
ID ModelID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Provider ModelProvider `json:"provider"`
|
||||
APIModel string `json:"api_model"`
|
||||
CostPer1MIn float64 `json:"cost_per_1m_in"`
|
||||
CostPer1MOut float64 `json:"cost_per_1m_out"`
|
||||
CostPer1MInCached float64 `json:"cost_per_1m_in_cached"`
|
||||
CostPer1MOutCached float64 `json:"cost_per_1m_out_cached"`
|
||||
ContextWindow int64 `json:"context_window"`
|
||||
DefaultMaxTokens int64 `json:"default_max_tokens"`
|
||||
CanReason bool `json:"can_reason"`
|
||||
ID ModelID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Provider ModelProvider `json:"provider"`
|
||||
APIModel string `json:"api_model"`
|
||||
CostPer1MIn float64 `json:"cost_per_1m_in"`
|
||||
CostPer1MOut float64 `json:"cost_per_1m_out"`
|
||||
CostPer1MInCached float64 `json:"cost_per_1m_in_cached"`
|
||||
CostPer1MOutCached float64 `json:"cost_per_1m_out_cached"`
|
||||
ContextWindow int64 `json:"context_window"`
|
||||
DefaultMaxTokens int64 `json:"default_max_tokens"`
|
||||
CanReason bool `json:"can_reason"`
|
||||
SupportsAttachments bool `json:"supports_attachments"`
|
||||
}
|
||||
|
||||
// Model IDs
|
||||
const ( // GEMINI
|
||||
// GROQ
|
||||
QWENQwq ModelID = "qwen-qwq"
|
||||
|
||||
// Bedrock
|
||||
BedrockClaude37Sonnet ModelID = "bedrock.claude-3.7-sonnet"
|
||||
)
|
||||
|
||||
const (
|
||||
ProviderBedrock ModelProvider = "bedrock"
|
||||
ProviderGROQ ModelProvider = "groq"
|
||||
|
||||
// ForTests
|
||||
ProviderMock ModelProvider = "__mock"
|
||||
)
|
||||
|
||||
// Providers in order of popularity
|
||||
var ProviderPopularity = map[ModelProvider]int{
|
||||
ProviderAnthropic: 1,
|
||||
ProviderOpenAI: 2,
|
||||
ProviderGemini: 3,
|
||||
ProviderGROQ: 4,
|
||||
ProviderOpenRouter: 5,
|
||||
ProviderBedrock: 6,
|
||||
ProviderAzure: 7,
|
||||
}
|
||||
|
||||
var SupportedModels = map[ModelID]Model{
|
||||
//
|
||||
// // GEMINI
|
||||
@@ -63,18 +70,6 @@ var SupportedModels = map[ModelID]Model{
|
||||
// CostPer1MOut: 0.4,
|
||||
// },
|
||||
//
|
||||
// // GROQ
|
||||
// QWENQwq: {
|
||||
// ID: QWENQwq,
|
||||
// Name: "Qwen Qwq",
|
||||
// Provider: ProviderGROQ,
|
||||
// APIModel: "qwen-qwq-32b",
|
||||
// CostPer1MIn: 0,
|
||||
// CostPer1MInCached: 0,
|
||||
// CostPer1MOutCached: 0,
|
||||
// CostPer1MOut: 0,
|
||||
// },
|
||||
//
|
||||
// // Bedrock
|
||||
BedrockClaude37Sonnet: {
|
||||
ID: BedrockClaude37Sonnet,
|
||||
@@ -92,4 +87,8 @@ func init() {
|
||||
maps.Copy(SupportedModels, AnthropicModels)
|
||||
maps.Copy(SupportedModels, OpenAIModels)
|
||||
maps.Copy(SupportedModels, GeminiModels)
|
||||
maps.Copy(SupportedModels, GroqModels)
|
||||
maps.Copy(SupportedModels, AzureModels)
|
||||
maps.Copy(SupportedModels, OpenRouterModels)
|
||||
maps.Copy(SupportedModels, XAIModels)
|
||||
}
|
||||
|
||||
@@ -19,151 +19,163 @@ const (
|
||||
|
||||
var OpenAIModels = map[ModelID]Model{
|
||||
GPT41: {
|
||||
ID: GPT41,
|
||||
Name: "GPT 4.1",
|
||||
Provider: ProviderOpenAI,
|
||||
APIModel: "gpt-4.1",
|
||||
CostPer1MIn: 2.00,
|
||||
CostPer1MInCached: 0.50,
|
||||
CostPer1MOutCached: 0.0,
|
||||
CostPer1MOut: 8.00,
|
||||
ContextWindow: 1_047_576,
|
||||
DefaultMaxTokens: 20000,
|
||||
ID: GPT41,
|
||||
Name: "GPT 4.1",
|
||||
Provider: ProviderOpenAI,
|
||||
APIModel: "gpt-4.1",
|
||||
CostPer1MIn: 2.00,
|
||||
CostPer1MInCached: 0.50,
|
||||
CostPer1MOutCached: 0.0,
|
||||
CostPer1MOut: 8.00,
|
||||
ContextWindow: 1_047_576,
|
||||
DefaultMaxTokens: 20000,
|
||||
SupportsAttachments: true,
|
||||
},
|
||||
GPT41Mini: {
|
||||
ID: GPT41Mini,
|
||||
Name: "GPT 4.1 mini",
|
||||
Provider: ProviderOpenAI,
|
||||
APIModel: "gpt-4.1",
|
||||
CostPer1MIn: 0.40,
|
||||
CostPer1MInCached: 0.10,
|
||||
CostPer1MOutCached: 0.0,
|
||||
CostPer1MOut: 1.60,
|
||||
ContextWindow: 200_000,
|
||||
DefaultMaxTokens: 20000,
|
||||
ID: GPT41Mini,
|
||||
Name: "GPT 4.1 mini",
|
||||
Provider: ProviderOpenAI,
|
||||
APIModel: "gpt-4.1",
|
||||
CostPer1MIn: 0.40,
|
||||
CostPer1MInCached: 0.10,
|
||||
CostPer1MOutCached: 0.0,
|
||||
CostPer1MOut: 1.60,
|
||||
ContextWindow: 200_000,
|
||||
DefaultMaxTokens: 20000,
|
||||
SupportsAttachments: true,
|
||||
},
|
||||
GPT41Nano: {
|
||||
ID: GPT41Nano,
|
||||
Name: "GPT 4.1 nano",
|
||||
Provider: ProviderOpenAI,
|
||||
APIModel: "gpt-4.1-nano",
|
||||
CostPer1MIn: 0.10,
|
||||
CostPer1MInCached: 0.025,
|
||||
CostPer1MOutCached: 0.0,
|
||||
CostPer1MOut: 0.40,
|
||||
ContextWindow: 1_047_576,
|
||||
DefaultMaxTokens: 20000,
|
||||
ID: GPT41Nano,
|
||||
Name: "GPT 4.1 nano",
|
||||
Provider: ProviderOpenAI,
|
||||
APIModel: "gpt-4.1-nano",
|
||||
CostPer1MIn: 0.10,
|
||||
CostPer1MInCached: 0.025,
|
||||
CostPer1MOutCached: 0.0,
|
||||
CostPer1MOut: 0.40,
|
||||
ContextWindow: 1_047_576,
|
||||
DefaultMaxTokens: 20000,
|
||||
SupportsAttachments: true,
|
||||
},
|
||||
GPT45Preview: {
|
||||
ID: GPT45Preview,
|
||||
Name: "GPT 4.5 preview",
|
||||
Provider: ProviderOpenAI,
|
||||
APIModel: "gpt-4.5-preview",
|
||||
CostPer1MIn: 75.00,
|
||||
CostPer1MInCached: 37.50,
|
||||
CostPer1MOutCached: 0.0,
|
||||
CostPer1MOut: 150.00,
|
||||
ContextWindow: 128_000,
|
||||
DefaultMaxTokens: 15000,
|
||||
ID: GPT45Preview,
|
||||
Name: "GPT 4.5 preview",
|
||||
Provider: ProviderOpenAI,
|
||||
APIModel: "gpt-4.5-preview",
|
||||
CostPer1MIn: 75.00,
|
||||
CostPer1MInCached: 37.50,
|
||||
CostPer1MOutCached: 0.0,
|
||||
CostPer1MOut: 150.00,
|
||||
ContextWindow: 128_000,
|
||||
DefaultMaxTokens: 15000,
|
||||
SupportsAttachments: true,
|
||||
},
|
||||
GPT4o: {
|
||||
ID: GPT4o,
|
||||
Name: "GPT 4o",
|
||||
Provider: ProviderOpenAI,
|
||||
APIModel: "gpt-4o",
|
||||
CostPer1MIn: 2.50,
|
||||
CostPer1MInCached: 1.25,
|
||||
CostPer1MOutCached: 0.0,
|
||||
CostPer1MOut: 10.00,
|
||||
ContextWindow: 128_000,
|
||||
DefaultMaxTokens: 4096,
|
||||
ID: GPT4o,
|
||||
Name: "GPT 4o",
|
||||
Provider: ProviderOpenAI,
|
||||
APIModel: "gpt-4o",
|
||||
CostPer1MIn: 2.50,
|
||||
CostPer1MInCached: 1.25,
|
||||
CostPer1MOutCached: 0.0,
|
||||
CostPer1MOut: 10.00,
|
||||
ContextWindow: 128_000,
|
||||
DefaultMaxTokens: 4096,
|
||||
SupportsAttachments: true,
|
||||
},
|
||||
GPT4oMini: {
|
||||
ID: GPT4oMini,
|
||||
Name: "GPT 4o mini",
|
||||
Provider: ProviderOpenAI,
|
||||
APIModel: "gpt-4o-mini",
|
||||
CostPer1MIn: 0.15,
|
||||
CostPer1MInCached: 0.075,
|
||||
CostPer1MOutCached: 0.0,
|
||||
CostPer1MOut: 0.60,
|
||||
ContextWindow: 128_000,
|
||||
ID: GPT4oMini,
|
||||
Name: "GPT 4o mini",
|
||||
Provider: ProviderOpenAI,
|
||||
APIModel: "gpt-4o-mini",
|
||||
CostPer1MIn: 0.15,
|
||||
CostPer1MInCached: 0.075,
|
||||
CostPer1MOutCached: 0.0,
|
||||
CostPer1MOut: 0.60,
|
||||
ContextWindow: 128_000,
|
||||
SupportsAttachments: true,
|
||||
},
|
||||
O1: {
|
||||
ID: O1,
|
||||
Name: "O1",
|
||||
Provider: ProviderOpenAI,
|
||||
APIModel: "o1",
|
||||
CostPer1MIn: 15.00,
|
||||
CostPer1MInCached: 7.50,
|
||||
CostPer1MOutCached: 0.0,
|
||||
CostPer1MOut: 60.00,
|
||||
ContextWindow: 200_000,
|
||||
DefaultMaxTokens: 50000,
|
||||
CanReason: true,
|
||||
ID: O1,
|
||||
Name: "O1",
|
||||
Provider: ProviderOpenAI,
|
||||
APIModel: "o1",
|
||||
CostPer1MIn: 15.00,
|
||||
CostPer1MInCached: 7.50,
|
||||
CostPer1MOutCached: 0.0,
|
||||
CostPer1MOut: 60.00,
|
||||
ContextWindow: 200_000,
|
||||
DefaultMaxTokens: 50000,
|
||||
CanReason: true,
|
||||
SupportsAttachments: true,
|
||||
},
|
||||
O1Pro: {
|
||||
ID: O1Pro,
|
||||
Name: "o1 pro",
|
||||
Provider: ProviderOpenAI,
|
||||
APIModel: "o1-pro",
|
||||
CostPer1MIn: 150.00,
|
||||
CostPer1MInCached: 0.0,
|
||||
CostPer1MOutCached: 0.0,
|
||||
CostPer1MOut: 600.00,
|
||||
ContextWindow: 200_000,
|
||||
DefaultMaxTokens: 50000,
|
||||
CanReason: true,
|
||||
ID: O1Pro,
|
||||
Name: "o1 pro",
|
||||
Provider: ProviderOpenAI,
|
||||
APIModel: "o1-pro",
|
||||
CostPer1MIn: 150.00,
|
||||
CostPer1MInCached: 0.0,
|
||||
CostPer1MOutCached: 0.0,
|
||||
CostPer1MOut: 600.00,
|
||||
ContextWindow: 200_000,
|
||||
DefaultMaxTokens: 50000,
|
||||
CanReason: true,
|
||||
SupportsAttachments: true,
|
||||
},
|
||||
O1Mini: {
|
||||
ID: O1Mini,
|
||||
Name: "o1 mini",
|
||||
Provider: ProviderOpenAI,
|
||||
APIModel: "o1-mini",
|
||||
CostPer1MIn: 1.10,
|
||||
CostPer1MInCached: 0.55,
|
||||
CostPer1MOutCached: 0.0,
|
||||
CostPer1MOut: 4.40,
|
||||
ContextWindow: 128_000,
|
||||
DefaultMaxTokens: 50000,
|
||||
CanReason: true,
|
||||
ID: O1Mini,
|
||||
Name: "o1 mini",
|
||||
Provider: ProviderOpenAI,
|
||||
APIModel: "o1-mini",
|
||||
CostPer1MIn: 1.10,
|
||||
CostPer1MInCached: 0.55,
|
||||
CostPer1MOutCached: 0.0,
|
||||
CostPer1MOut: 4.40,
|
||||
ContextWindow: 128_000,
|
||||
DefaultMaxTokens: 50000,
|
||||
CanReason: true,
|
||||
SupportsAttachments: true,
|
||||
},
|
||||
O3: {
|
||||
ID: O3,
|
||||
Name: "o3",
|
||||
Provider: ProviderOpenAI,
|
||||
APIModel: "o3",
|
||||
CostPer1MIn: 10.00,
|
||||
CostPer1MInCached: 2.50,
|
||||
CostPer1MOutCached: 0.0,
|
||||
CostPer1MOut: 40.00,
|
||||
ContextWindow: 200_000,
|
||||
CanReason: true,
|
||||
ID: O3,
|
||||
Name: "o3",
|
||||
Provider: ProviderOpenAI,
|
||||
APIModel: "o3",
|
||||
CostPer1MIn: 10.00,
|
||||
CostPer1MInCached: 2.50,
|
||||
CostPer1MOutCached: 0.0,
|
||||
CostPer1MOut: 40.00,
|
||||
ContextWindow: 200_000,
|
||||
CanReason: true,
|
||||
SupportsAttachments: true,
|
||||
},
|
||||
O3Mini: {
|
||||
ID: O3Mini,
|
||||
Name: "o3 mini",
|
||||
Provider: ProviderOpenAI,
|
||||
APIModel: "o3-mini",
|
||||
CostPer1MIn: 1.10,
|
||||
CostPer1MInCached: 0.55,
|
||||
CostPer1MOutCached: 0.0,
|
||||
CostPer1MOut: 4.40,
|
||||
ContextWindow: 200_000,
|
||||
DefaultMaxTokens: 50000,
|
||||
CanReason: true,
|
||||
ID: O3Mini,
|
||||
Name: "o3 mini",
|
||||
Provider: ProviderOpenAI,
|
||||
APIModel: "o3-mini",
|
||||
CostPer1MIn: 1.10,
|
||||
CostPer1MInCached: 0.55,
|
||||
CostPer1MOutCached: 0.0,
|
||||
CostPer1MOut: 4.40,
|
||||
ContextWindow: 200_000,
|
||||
DefaultMaxTokens: 50000,
|
||||
CanReason: true,
|
||||
SupportsAttachments: false,
|
||||
},
|
||||
O4Mini: {
|
||||
ID: O4Mini,
|
||||
Name: "o4 mini",
|
||||
Provider: ProviderOpenAI,
|
||||
APIModel: "o4-mini",
|
||||
CostPer1MIn: 1.10,
|
||||
CostPer1MInCached: 0.275,
|
||||
CostPer1MOutCached: 0.0,
|
||||
CostPer1MOut: 4.40,
|
||||
ContextWindow: 128_000,
|
||||
DefaultMaxTokens: 50000,
|
||||
CanReason: true,
|
||||
ID: O4Mini,
|
||||
Name: "o4 mini",
|
||||
Provider: ProviderOpenAI,
|
||||
APIModel: "o4-mini",
|
||||
CostPer1MIn: 1.10,
|
||||
CostPer1MInCached: 0.275,
|
||||
CostPer1MOutCached: 0.0,
|
||||
CostPer1MOut: 4.40,
|
||||
ContextWindow: 128_000,
|
||||
DefaultMaxTokens: 50000,
|
||||
CanReason: true,
|
||||
SupportsAttachments: true,
|
||||
},
|
||||
}
|
||||
|
||||
262
internal/llm/models/openrouter.go
Normal file
262
internal/llm/models/openrouter.go
Normal file
@@ -0,0 +1,262 @@
|
||||
package models
|
||||
|
||||
const (
|
||||
ProviderOpenRouter ModelProvider = "openrouter"
|
||||
|
||||
OpenRouterGPT41 ModelID = "openrouter.gpt-4.1"
|
||||
OpenRouterGPT41Mini ModelID = "openrouter.gpt-4.1-mini"
|
||||
OpenRouterGPT41Nano ModelID = "openrouter.gpt-4.1-nano"
|
||||
OpenRouterGPT45Preview ModelID = "openrouter.gpt-4.5-preview"
|
||||
OpenRouterGPT4o ModelID = "openrouter.gpt-4o"
|
||||
OpenRouterGPT4oMini ModelID = "openrouter.gpt-4o-mini"
|
||||
OpenRouterO1 ModelID = "openrouter.o1"
|
||||
OpenRouterO1Pro ModelID = "openrouter.o1-pro"
|
||||
OpenRouterO1Mini ModelID = "openrouter.o1-mini"
|
||||
OpenRouterO3 ModelID = "openrouter.o3"
|
||||
OpenRouterO3Mini ModelID = "openrouter.o3-mini"
|
||||
OpenRouterO4Mini ModelID = "openrouter.o4-mini"
|
||||
OpenRouterGemini25Flash ModelID = "openrouter.gemini-2.5-flash"
|
||||
OpenRouterGemini25 ModelID = "openrouter.gemini-2.5"
|
||||
OpenRouterClaude35Sonnet ModelID = "openrouter.claude-3.5-sonnet"
|
||||
OpenRouterClaude3Haiku ModelID = "openrouter.claude-3-haiku"
|
||||
OpenRouterClaude37Sonnet ModelID = "openrouter.claude-3.7-sonnet"
|
||||
OpenRouterClaude35Haiku ModelID = "openrouter.claude-3.5-haiku"
|
||||
OpenRouterClaude3Opus ModelID = "openrouter.claude-3-opus"
|
||||
)
|
||||
|
||||
var OpenRouterModels = map[ModelID]Model{
|
||||
OpenRouterGPT41: {
|
||||
ID: OpenRouterGPT41,
|
||||
Name: "OpenRouter – GPT 4.1",
|
||||
Provider: ProviderOpenRouter,
|
||||
APIModel: "openai/gpt-4.1",
|
||||
CostPer1MIn: OpenAIModels[GPT41].CostPer1MIn,
|
||||
CostPer1MInCached: OpenAIModels[GPT41].CostPer1MInCached,
|
||||
CostPer1MOut: OpenAIModels[GPT41].CostPer1MOut,
|
||||
CostPer1MOutCached: OpenAIModels[GPT41].CostPer1MOutCached,
|
||||
ContextWindow: OpenAIModels[GPT41].ContextWindow,
|
||||
DefaultMaxTokens: OpenAIModels[GPT41].DefaultMaxTokens,
|
||||
},
|
||||
OpenRouterGPT41Mini: {
|
||||
ID: OpenRouterGPT41Mini,
|
||||
Name: "OpenRouter – GPT 4.1 mini",
|
||||
Provider: ProviderOpenRouter,
|
||||
APIModel: "openai/gpt-4.1-mini",
|
||||
CostPer1MIn: OpenAIModels[GPT41Mini].CostPer1MIn,
|
||||
CostPer1MInCached: OpenAIModels[GPT41Mini].CostPer1MInCached,
|
||||
CostPer1MOut: OpenAIModels[GPT41Mini].CostPer1MOut,
|
||||
CostPer1MOutCached: OpenAIModels[GPT41Mini].CostPer1MOutCached,
|
||||
ContextWindow: OpenAIModels[GPT41Mini].ContextWindow,
|
||||
DefaultMaxTokens: OpenAIModels[GPT41Mini].DefaultMaxTokens,
|
||||
},
|
||||
OpenRouterGPT41Nano: {
|
||||
ID: OpenRouterGPT41Nano,
|
||||
Name: "OpenRouter – GPT 4.1 nano",
|
||||
Provider: ProviderOpenRouter,
|
||||
APIModel: "openai/gpt-4.1-nano",
|
||||
CostPer1MIn: OpenAIModels[GPT41Nano].CostPer1MIn,
|
||||
CostPer1MInCached: OpenAIModels[GPT41Nano].CostPer1MInCached,
|
||||
CostPer1MOut: OpenAIModels[GPT41Nano].CostPer1MOut,
|
||||
CostPer1MOutCached: OpenAIModels[GPT41Nano].CostPer1MOutCached,
|
||||
ContextWindow: OpenAIModels[GPT41Nano].ContextWindow,
|
||||
DefaultMaxTokens: OpenAIModels[GPT41Nano].DefaultMaxTokens,
|
||||
},
|
||||
OpenRouterGPT45Preview: {
|
||||
ID: OpenRouterGPT45Preview,
|
||||
Name: "OpenRouter – GPT 4.5 preview",
|
||||
Provider: ProviderOpenRouter,
|
||||
APIModel: "openai/gpt-4.5-preview",
|
||||
CostPer1MIn: OpenAIModels[GPT45Preview].CostPer1MIn,
|
||||
CostPer1MInCached: OpenAIModels[GPT45Preview].CostPer1MInCached,
|
||||
CostPer1MOut: OpenAIModels[GPT45Preview].CostPer1MOut,
|
||||
CostPer1MOutCached: OpenAIModels[GPT45Preview].CostPer1MOutCached,
|
||||
ContextWindow: OpenAIModels[GPT45Preview].ContextWindow,
|
||||
DefaultMaxTokens: OpenAIModels[GPT45Preview].DefaultMaxTokens,
|
||||
},
|
||||
OpenRouterGPT4o: {
|
||||
ID: OpenRouterGPT4o,
|
||||
Name: "OpenRouter – GPT 4o",
|
||||
Provider: ProviderOpenRouter,
|
||||
APIModel: "openai/gpt-4o",
|
||||
CostPer1MIn: OpenAIModels[GPT4o].CostPer1MIn,
|
||||
CostPer1MInCached: OpenAIModels[GPT4o].CostPer1MInCached,
|
||||
CostPer1MOut: OpenAIModels[GPT4o].CostPer1MOut,
|
||||
CostPer1MOutCached: OpenAIModels[GPT4o].CostPer1MOutCached,
|
||||
ContextWindow: OpenAIModels[GPT4o].ContextWindow,
|
||||
DefaultMaxTokens: OpenAIModels[GPT4o].DefaultMaxTokens,
|
||||
},
|
||||
OpenRouterGPT4oMini: {
|
||||
ID: OpenRouterGPT4oMini,
|
||||
Name: "OpenRouter – GPT 4o mini",
|
||||
Provider: ProviderOpenRouter,
|
||||
APIModel: "openai/gpt-4o-mini",
|
||||
CostPer1MIn: OpenAIModels[GPT4oMini].CostPer1MIn,
|
||||
CostPer1MInCached: OpenAIModels[GPT4oMini].CostPer1MInCached,
|
||||
CostPer1MOut: OpenAIModels[GPT4oMini].CostPer1MOut,
|
||||
CostPer1MOutCached: OpenAIModels[GPT4oMini].CostPer1MOutCached,
|
||||
ContextWindow: OpenAIModels[GPT4oMini].ContextWindow,
|
||||
},
|
||||
OpenRouterO1: {
|
||||
ID: OpenRouterO1,
|
||||
Name: "OpenRouter – O1",
|
||||
Provider: ProviderOpenRouter,
|
||||
APIModel: "openai/o1",
|
||||
CostPer1MIn: OpenAIModels[O1].CostPer1MIn,
|
||||
CostPer1MInCached: OpenAIModels[O1].CostPer1MInCached,
|
||||
CostPer1MOut: OpenAIModels[O1].CostPer1MOut,
|
||||
CostPer1MOutCached: OpenAIModels[O1].CostPer1MOutCached,
|
||||
ContextWindow: OpenAIModels[O1].ContextWindow,
|
||||
DefaultMaxTokens: OpenAIModels[O1].DefaultMaxTokens,
|
||||
CanReason: OpenAIModels[O1].CanReason,
|
||||
},
|
||||
OpenRouterO1Pro: {
|
||||
ID: OpenRouterO1Pro,
|
||||
Name: "OpenRouter – o1 pro",
|
||||
Provider: ProviderOpenRouter,
|
||||
APIModel: "openai/o1-pro",
|
||||
CostPer1MIn: OpenAIModels[O1Pro].CostPer1MIn,
|
||||
CostPer1MInCached: OpenAIModels[O1Pro].CostPer1MInCached,
|
||||
CostPer1MOut: OpenAIModels[O1Pro].CostPer1MOut,
|
||||
CostPer1MOutCached: OpenAIModels[O1Pro].CostPer1MOutCached,
|
||||
ContextWindow: OpenAIModels[O1Pro].ContextWindow,
|
||||
DefaultMaxTokens: OpenAIModels[O1Pro].DefaultMaxTokens,
|
||||
CanReason: OpenAIModels[O1Pro].CanReason,
|
||||
},
|
||||
OpenRouterO1Mini: {
|
||||
ID: OpenRouterO1Mini,
|
||||
Name: "OpenRouter – o1 mini",
|
||||
Provider: ProviderOpenRouter,
|
||||
APIModel: "openai/o1-mini",
|
||||
CostPer1MIn: OpenAIModels[O1Mini].CostPer1MIn,
|
||||
CostPer1MInCached: OpenAIModels[O1Mini].CostPer1MInCached,
|
||||
CostPer1MOut: OpenAIModels[O1Mini].CostPer1MOut,
|
||||
CostPer1MOutCached: OpenAIModels[O1Mini].CostPer1MOutCached,
|
||||
ContextWindow: OpenAIModels[O1Mini].ContextWindow,
|
||||
DefaultMaxTokens: OpenAIModels[O1Mini].DefaultMaxTokens,
|
||||
CanReason: OpenAIModels[O1Mini].CanReason,
|
||||
},
|
||||
OpenRouterO3: {
|
||||
ID: OpenRouterO3,
|
||||
Name: "OpenRouter – o3",
|
||||
Provider: ProviderOpenRouter,
|
||||
APIModel: "openai/o3",
|
||||
CostPer1MIn: OpenAIModels[O3].CostPer1MIn,
|
||||
CostPer1MInCached: OpenAIModels[O3].CostPer1MInCached,
|
||||
CostPer1MOut: OpenAIModels[O3].CostPer1MOut,
|
||||
CostPer1MOutCached: OpenAIModels[O3].CostPer1MOutCached,
|
||||
ContextWindow: OpenAIModels[O3].ContextWindow,
|
||||
DefaultMaxTokens: OpenAIModels[O3].DefaultMaxTokens,
|
||||
CanReason: OpenAIModels[O3].CanReason,
|
||||
},
|
||||
OpenRouterO3Mini: {
|
||||
ID: OpenRouterO3Mini,
|
||||
Name: "OpenRouter – o3 mini",
|
||||
Provider: ProviderOpenRouter,
|
||||
APIModel: "openai/o3-mini-high",
|
||||
CostPer1MIn: OpenAIModels[O3Mini].CostPer1MIn,
|
||||
CostPer1MInCached: OpenAIModels[O3Mini].CostPer1MInCached,
|
||||
CostPer1MOut: OpenAIModels[O3Mini].CostPer1MOut,
|
||||
CostPer1MOutCached: OpenAIModels[O3Mini].CostPer1MOutCached,
|
||||
ContextWindow: OpenAIModels[O3Mini].ContextWindow,
|
||||
DefaultMaxTokens: OpenAIModels[O3Mini].DefaultMaxTokens,
|
||||
CanReason: OpenAIModels[O3Mini].CanReason,
|
||||
},
|
||||
OpenRouterO4Mini: {
|
||||
ID: OpenRouterO4Mini,
|
||||
Name: "OpenRouter – o4 mini",
|
||||
Provider: ProviderOpenRouter,
|
||||
APIModel: "openai/o4-mini-high",
|
||||
CostPer1MIn: OpenAIModels[O4Mini].CostPer1MIn,
|
||||
CostPer1MInCached: OpenAIModels[O4Mini].CostPer1MInCached,
|
||||
CostPer1MOut: OpenAIModels[O4Mini].CostPer1MOut,
|
||||
CostPer1MOutCached: OpenAIModels[O4Mini].CostPer1MOutCached,
|
||||
ContextWindow: OpenAIModels[O4Mini].ContextWindow,
|
||||
DefaultMaxTokens: OpenAIModels[O4Mini].DefaultMaxTokens,
|
||||
CanReason: OpenAIModels[O4Mini].CanReason,
|
||||
},
|
||||
OpenRouterGemini25Flash: {
|
||||
ID: OpenRouterGemini25Flash,
|
||||
Name: "OpenRouter – Gemini 2.5 Flash",
|
||||
Provider: ProviderOpenRouter,
|
||||
APIModel: "google/gemini-2.5-flash-preview:thinking",
|
||||
CostPer1MIn: GeminiModels[Gemini25Flash].CostPer1MIn,
|
||||
CostPer1MInCached: GeminiModels[Gemini25Flash].CostPer1MInCached,
|
||||
CostPer1MOut: GeminiModels[Gemini25Flash].CostPer1MOut,
|
||||
CostPer1MOutCached: GeminiModels[Gemini25Flash].CostPer1MOutCached,
|
||||
ContextWindow: GeminiModels[Gemini25Flash].ContextWindow,
|
||||
DefaultMaxTokens: GeminiModels[Gemini25Flash].DefaultMaxTokens,
|
||||
},
|
||||
OpenRouterGemini25: {
|
||||
ID: OpenRouterGemini25,
|
||||
Name: "OpenRouter – Gemini 2.5 Pro",
|
||||
Provider: ProviderOpenRouter,
|
||||
APIModel: "google/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,
|
||||
},
|
||||
OpenRouterClaude35Sonnet: {
|
||||
ID: OpenRouterClaude35Sonnet,
|
||||
Name: "OpenRouter – Claude 3.5 Sonnet",
|
||||
Provider: ProviderOpenRouter,
|
||||
APIModel: "anthropic/claude-3.5-sonnet",
|
||||
CostPer1MIn: AnthropicModels[Claude35Sonnet].CostPer1MIn,
|
||||
CostPer1MInCached: AnthropicModels[Claude35Sonnet].CostPer1MInCached,
|
||||
CostPer1MOut: AnthropicModels[Claude35Sonnet].CostPer1MOut,
|
||||
CostPer1MOutCached: AnthropicModels[Claude35Sonnet].CostPer1MOutCached,
|
||||
ContextWindow: AnthropicModels[Claude35Sonnet].ContextWindow,
|
||||
DefaultMaxTokens: AnthropicModels[Claude35Sonnet].DefaultMaxTokens,
|
||||
},
|
||||
OpenRouterClaude3Haiku: {
|
||||
ID: OpenRouterClaude3Haiku,
|
||||
Name: "OpenRouter – Claude 3 Haiku",
|
||||
Provider: ProviderOpenRouter,
|
||||
APIModel: "anthropic/claude-3-haiku",
|
||||
CostPer1MIn: AnthropicModels[Claude3Haiku].CostPer1MIn,
|
||||
CostPer1MInCached: AnthropicModels[Claude3Haiku].CostPer1MInCached,
|
||||
CostPer1MOut: AnthropicModels[Claude3Haiku].CostPer1MOut,
|
||||
CostPer1MOutCached: AnthropicModels[Claude3Haiku].CostPer1MOutCached,
|
||||
ContextWindow: AnthropicModels[Claude3Haiku].ContextWindow,
|
||||
DefaultMaxTokens: AnthropicModels[Claude3Haiku].DefaultMaxTokens,
|
||||
},
|
||||
OpenRouterClaude37Sonnet: {
|
||||
ID: OpenRouterClaude37Sonnet,
|
||||
Name: "OpenRouter – Claude 3.7 Sonnet",
|
||||
Provider: ProviderOpenRouter,
|
||||
APIModel: "anthropic/claude-3.7-sonnet",
|
||||
CostPer1MIn: AnthropicModels[Claude37Sonnet].CostPer1MIn,
|
||||
CostPer1MInCached: AnthropicModels[Claude37Sonnet].CostPer1MInCached,
|
||||
CostPer1MOut: AnthropicModels[Claude37Sonnet].CostPer1MOut,
|
||||
CostPer1MOutCached: AnthropicModels[Claude37Sonnet].CostPer1MOutCached,
|
||||
ContextWindow: AnthropicModels[Claude37Sonnet].ContextWindow,
|
||||
DefaultMaxTokens: AnthropicModels[Claude37Sonnet].DefaultMaxTokens,
|
||||
CanReason: AnthropicModels[Claude37Sonnet].CanReason,
|
||||
},
|
||||
OpenRouterClaude35Haiku: {
|
||||
ID: OpenRouterClaude35Haiku,
|
||||
Name: "OpenRouter – Claude 3.5 Haiku",
|
||||
Provider: ProviderOpenRouter,
|
||||
APIModel: "anthropic/claude-3.5-haiku",
|
||||
CostPer1MIn: AnthropicModels[Claude35Haiku].CostPer1MIn,
|
||||
CostPer1MInCached: AnthropicModels[Claude35Haiku].CostPer1MInCached,
|
||||
CostPer1MOut: AnthropicModels[Claude35Haiku].CostPer1MOut,
|
||||
CostPer1MOutCached: AnthropicModels[Claude35Haiku].CostPer1MOutCached,
|
||||
ContextWindow: AnthropicModels[Claude35Haiku].ContextWindow,
|
||||
DefaultMaxTokens: AnthropicModels[Claude35Haiku].DefaultMaxTokens,
|
||||
},
|
||||
OpenRouterClaude3Opus: {
|
||||
ID: OpenRouterClaude3Opus,
|
||||
Name: "OpenRouter – Claude 3 Opus",
|
||||
Provider: ProviderOpenRouter,
|
||||
APIModel: "anthropic/claude-3-opus",
|
||||
CostPer1MIn: AnthropicModels[Claude3Opus].CostPer1MIn,
|
||||
CostPer1MInCached: AnthropicModels[Claude3Opus].CostPer1MInCached,
|
||||
CostPer1MOut: AnthropicModels[Claude3Opus].CostPer1MOut,
|
||||
CostPer1MOutCached: AnthropicModels[Claude3Opus].CostPer1MOutCached,
|
||||
ContextWindow: AnthropicModels[Claude3Opus].ContextWindow,
|
||||
DefaultMaxTokens: AnthropicModels[Claude3Opus].DefaultMaxTokens,
|
||||
},
|
||||
}
|
||||
61
internal/llm/models/xai.go
Normal file
61
internal/llm/models/xai.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package models
|
||||
|
||||
const (
|
||||
ProviderXAI ModelProvider = "xai"
|
||||
|
||||
XAIGrok3Beta ModelID = "grok-3-beta"
|
||||
XAIGrok3MiniBeta ModelID = "grok-3-mini-beta"
|
||||
XAIGrok3FastBeta ModelID = "grok-3-fast-beta"
|
||||
XAiGrok3MiniFastBeta ModelID = "grok-3-mini-fast-beta"
|
||||
)
|
||||
|
||||
var XAIModels = map[ModelID]Model{
|
||||
XAIGrok3Beta: {
|
||||
ID: XAIGrok3Beta,
|
||||
Name: "Grok3 Beta",
|
||||
Provider: ProviderXAI,
|
||||
APIModel: "grok-3-beta",
|
||||
CostPer1MIn: 3.0,
|
||||
CostPer1MInCached: 0,
|
||||
CostPer1MOut: 15,
|
||||
CostPer1MOutCached: 0,
|
||||
ContextWindow: 131_072,
|
||||
DefaultMaxTokens: 20_000,
|
||||
},
|
||||
XAIGrok3MiniBeta: {
|
||||
ID: XAIGrok3MiniBeta,
|
||||
Name: "Grok3 Mini Beta",
|
||||
Provider: ProviderXAI,
|
||||
APIModel: "grok-3-mini-beta",
|
||||
CostPer1MIn: 0.3,
|
||||
CostPer1MInCached: 0,
|
||||
CostPer1MOut: 0.5,
|
||||
CostPer1MOutCached: 0,
|
||||
ContextWindow: 131_072,
|
||||
DefaultMaxTokens: 20_000,
|
||||
},
|
||||
XAIGrok3FastBeta: {
|
||||
ID: XAIGrok3FastBeta,
|
||||
Name: "Grok3 Fast Beta",
|
||||
Provider: ProviderXAI,
|
||||
APIModel: "grok-3-fast-beta",
|
||||
CostPer1MIn: 5,
|
||||
CostPer1MInCached: 0,
|
||||
CostPer1MOut: 25,
|
||||
CostPer1MOutCached: 0,
|
||||
ContextWindow: 131_072,
|
||||
DefaultMaxTokens: 20_000,
|
||||
},
|
||||
XAiGrok3MiniFastBeta: {
|
||||
ID: XAiGrok3MiniFastBeta,
|
||||
Name: "Grok3 Mini Fast Beta",
|
||||
Provider: ProviderXAI,
|
||||
APIModel: "grok-3-mini-fast-beta",
|
||||
CostPer1MIn: 0.6,
|
||||
CostPer1MInCached: 0,
|
||||
CostPer1MOut: 4.0,
|
||||
CostPer1MOutCached: 0,
|
||||
ContextWindow: 131_072,
|
||||
DefaultMaxTokens: 20_000,
|
||||
},
|
||||
}
|
||||
@@ -8,9 +8,9 @@ import (
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/kujtimiihoxha/opencode/internal/config"
|
||||
"github.com/kujtimiihoxha/opencode/internal/llm/models"
|
||||
"github.com/kujtimiihoxha/opencode/internal/llm/tools"
|
||||
"github.com/opencode-ai/opencode/internal/config"
|
||||
"github.com/opencode-ai/opencode/internal/llm/models"
|
||||
"github.com/opencode-ai/opencode/internal/llm/tools"
|
||||
)
|
||||
|
||||
func CoderPrompt(provider models.ModelProvider) string {
|
||||
|
||||
@@ -4,25 +4,14 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/kujtimiihoxha/opencode/internal/config"
|
||||
"github.com/kujtimiihoxha/opencode/internal/llm/models"
|
||||
"github.com/opencode-ai/opencode/internal/config"
|
||||
"github.com/opencode-ai/opencode/internal/llm/models"
|
||||
"github.com/opencode-ai/opencode/internal/logging"
|
||||
)
|
||||
|
||||
// contextFiles is a list of potential context files to check for
|
||||
var contextFiles = []string{
|
||||
".github/copilot-instructions.md",
|
||||
".cursorrules",
|
||||
"CLAUDE.md",
|
||||
"CLAUDE.local.md",
|
||||
"opencode.md",
|
||||
"opencode.local.md",
|
||||
"OpenCode.md",
|
||||
"OpenCode.local.md",
|
||||
"OPENCODE.md",
|
||||
"OPENCODE.local.md",
|
||||
}
|
||||
|
||||
func GetAgentPrompt(agentName config.AgentName, provider models.ModelProvider) string {
|
||||
basePrompt := ""
|
||||
switch agentName {
|
||||
@@ -38,26 +27,109 @@ func GetAgentPrompt(agentName config.AgentName, provider models.ModelProvider) s
|
||||
|
||||
if agentName == config.AgentCoder || agentName == config.AgentTask {
|
||||
// Add context from project-specific instruction files if they exist
|
||||
contextContent := getContextFromFiles()
|
||||
contextContent := getContextFromPaths()
|
||||
logging.Debug("Context content", "Context", contextContent)
|
||||
if contextContent != "" {
|
||||
return fmt.Sprintf("%s\n\n# Project-Specific Context\n%s", basePrompt, contextContent)
|
||||
return fmt.Sprintf("%s\n\n# Project-Specific Context\n Make sure to follow the instructions in the context below\n%s", basePrompt, contextContent)
|
||||
}
|
||||
}
|
||||
return basePrompt
|
||||
}
|
||||
|
||||
// getContextFromFiles checks for the existence of context files and returns their content
|
||||
func getContextFromFiles() string {
|
||||
workDir := config.WorkingDirectory()
|
||||
var contextContent string
|
||||
var (
|
||||
onceContext sync.Once
|
||||
contextContent string
|
||||
)
|
||||
|
||||
for _, file := range contextFiles {
|
||||
filePath := filepath.Join(workDir, file)
|
||||
content, err := os.ReadFile(filePath)
|
||||
if err == nil {
|
||||
contextContent += fmt.Sprintf("\n%s\n", string(content))
|
||||
}
|
||||
}
|
||||
func getContextFromPaths() string {
|
||||
onceContext.Do(func() {
|
||||
var (
|
||||
cfg = config.Get()
|
||||
workDir = cfg.WorkingDir
|
||||
contextPaths = cfg.ContextPaths
|
||||
)
|
||||
|
||||
contextContent = processContextPaths(workDir, contextPaths)
|
||||
})
|
||||
|
||||
return contextContent
|
||||
}
|
||||
|
||||
func processContextPaths(workDir string, paths []string) string {
|
||||
var (
|
||||
wg sync.WaitGroup
|
||||
resultCh = make(chan string)
|
||||
)
|
||||
|
||||
// Track processed files to avoid duplicates
|
||||
processedFiles := make(map[string]bool)
|
||||
var processedMutex sync.Mutex
|
||||
|
||||
for _, path := range paths {
|
||||
wg.Add(1)
|
||||
go func(p string) {
|
||||
defer wg.Done()
|
||||
|
||||
if strings.HasSuffix(p, "/") {
|
||||
filepath.WalkDir(filepath.Join(workDir, p), func(path string, d os.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !d.IsDir() {
|
||||
// Check if we've already processed this file (case-insensitive)
|
||||
processedMutex.Lock()
|
||||
lowerPath := strings.ToLower(path)
|
||||
if !processedFiles[lowerPath] {
|
||||
processedFiles[lowerPath] = true
|
||||
processedMutex.Unlock()
|
||||
|
||||
if result := processFile(path); result != "" {
|
||||
resultCh <- result
|
||||
}
|
||||
} else {
|
||||
processedMutex.Unlock()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
} else {
|
||||
fullPath := filepath.Join(workDir, p)
|
||||
|
||||
// Check if we've already processed this file (case-insensitive)
|
||||
processedMutex.Lock()
|
||||
lowerPath := strings.ToLower(fullPath)
|
||||
if !processedFiles[lowerPath] {
|
||||
processedFiles[lowerPath] = true
|
||||
processedMutex.Unlock()
|
||||
|
||||
result := processFile(fullPath)
|
||||
if result != "" {
|
||||
resultCh <- result
|
||||
}
|
||||
} else {
|
||||
processedMutex.Unlock()
|
||||
}
|
||||
}
|
||||
}(path)
|
||||
}
|
||||
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(resultCh)
|
||||
}()
|
||||
|
||||
results := make([]string, 0)
|
||||
for result := range resultCh {
|
||||
results = append(results, result)
|
||||
}
|
||||
|
||||
return strings.Join(results, "\n")
|
||||
}
|
||||
|
||||
func processFile(filePath string) string {
|
||||
content, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return "# From:" + filePath + "\n" + string(content)
|
||||
}
|
||||
|
||||
57
internal/llm/prompt/prompt_test.go
Normal file
57
internal/llm/prompt/prompt_test.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package prompt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/opencode-ai/opencode/internal/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGetContextFromPaths(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
_, err := config.Load(tmpDir, false)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to load config: %v", err)
|
||||
}
|
||||
cfg := config.Get()
|
||||
cfg.WorkingDir = tmpDir
|
||||
cfg.ContextPaths = []string{
|
||||
"file.txt",
|
||||
"directory/",
|
||||
}
|
||||
testFiles := []string{
|
||||
"file.txt",
|
||||
"directory/file_a.txt",
|
||||
"directory/file_b.txt",
|
||||
"directory/file_c.txt",
|
||||
}
|
||||
|
||||
createTestFiles(t, tmpDir, testFiles)
|
||||
|
||||
context := getContextFromPaths()
|
||||
expectedContext := fmt.Sprintf("# From:%s/file.txt\nfile.txt: test content\n# From:%s/directory/file_a.txt\ndirectory/file_a.txt: test content\n# From:%s/directory/file_b.txt\ndirectory/file_b.txt: test content\n# From:%s/directory/file_c.txt\ndirectory/file_c.txt: test content", tmpDir, tmpDir, tmpDir, tmpDir)
|
||||
assert.Equal(t, expectedContext, context)
|
||||
}
|
||||
|
||||
func createTestFiles(t *testing.T, tmpDir string, testFiles []string) {
|
||||
t.Helper()
|
||||
for _, path := range testFiles {
|
||||
fullPath := filepath.Join(tmpDir, path)
|
||||
if path[len(path)-1] == '/' {
|
||||
err := os.MkdirAll(fullPath, 0755)
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
dir := filepath.Dir(fullPath)
|
||||
err := os.MkdirAll(dir, 0755)
|
||||
require.NoError(t, err)
|
||||
err = os.WriteFile(fullPath, []byte(path+": test content"), 0644)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ package prompt
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/kujtimiihoxha/opencode/internal/llm/models"
|
||||
"github.com/opencode-ai/opencode/internal/llm/models"
|
||||
)
|
||||
|
||||
func TaskPrompt(_ models.ModelProvider) string {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package prompt
|
||||
|
||||
import "github.com/kujtimiihoxha/opencode/internal/llm/models"
|
||||
import "github.com/opencode-ai/opencode/internal/llm/models"
|
||||
|
||||
func TitlePrompt(_ models.ModelProvider) string {
|
||||
return `you will generate a short title based on the first message a user begins a conversation with
|
||||
@@ -8,5 +8,6 @@ func TitlePrompt(_ models.ModelProvider) string {
|
||||
- the title should be a summary of the user's message
|
||||
- it should be one line long
|
||||
- do not use quotes or colons
|
||||
- the entire text you return will be used as the title`
|
||||
- the entire text you return will be used as the title
|
||||
- never return anything that is more than one sentence (one line) long`
|
||||
}
|
||||
|
||||
@@ -12,10 +12,11 @@ import (
|
||||
"github.com/anthropics/anthropic-sdk-go"
|
||||
"github.com/anthropics/anthropic-sdk-go/bedrock"
|
||||
"github.com/anthropics/anthropic-sdk-go/option"
|
||||
"github.com/kujtimiihoxha/opencode/internal/config"
|
||||
"github.com/kujtimiihoxha/opencode/internal/llm/tools"
|
||||
"github.com/kujtimiihoxha/opencode/internal/logging"
|
||||
"github.com/kujtimiihoxha/opencode/internal/message"
|
||||
"github.com/opencode-ai/opencode/internal/config"
|
||||
"github.com/opencode-ai/opencode/internal/llm/models"
|
||||
"github.com/opencode-ai/opencode/internal/llm/tools"
|
||||
"github.com/opencode-ai/opencode/internal/logging"
|
||||
"github.com/opencode-ai/opencode/internal/message"
|
||||
)
|
||||
|
||||
type anthropicOptions struct {
|
||||
@@ -70,7 +71,14 @@ func (a *anthropicClient) convertMessages(messages []message.Message) (anthropic
|
||||
Type: "ephemeral",
|
||||
}
|
||||
}
|
||||
anthropicMessages = append(anthropicMessages, anthropic.NewUserMessage(content))
|
||||
var contentBlocks []anthropic.ContentBlockParamUnion
|
||||
contentBlocks = append(contentBlocks, content)
|
||||
for _, binaryContent := range msg.BinaryContent() {
|
||||
base64Image := binaryContent.String(models.ProviderAnthropic)
|
||||
imageBlock := anthropic.NewImageBlockBase64(binaryContent.MIMEType, base64Image)
|
||||
contentBlocks = append(contentBlocks, imageBlock)
|
||||
}
|
||||
anthropicMessages = append(anthropicMessages, anthropic.NewUserMessage(contentBlocks...))
|
||||
|
||||
case message.Assistant:
|
||||
blocks := []anthropic.ContentBlockParamUnion{}
|
||||
@@ -196,9 +204,10 @@ func (a *anthropicClient) send(ctx context.Context, messages []message.Message,
|
||||
preparedMessages := a.preparedMessages(a.convertMessages(messages), a.convertTools(tools))
|
||||
cfg := config.Get()
|
||||
if cfg.Debug {
|
||||
// jsonData, _ := json.Marshal(preparedMessages)
|
||||
// logging.Debug("Prepared messages", "messages", string(jsonData))
|
||||
jsonData, _ := json.Marshal(preparedMessages)
|
||||
logging.Debug("Prepared messages", "messages", string(jsonData))
|
||||
}
|
||||
|
||||
attempts := 0
|
||||
for {
|
||||
attempts++
|
||||
@@ -208,12 +217,13 @@ func (a *anthropicClient) send(ctx context.Context, messages []message.Message,
|
||||
)
|
||||
// If there is an error we are going to see if we can retry the call
|
||||
if err != nil {
|
||||
logging.Error("Error in Anthropic API call", "error", err)
|
||||
retry, after, retryErr := a.shouldRetry(attempts, err)
|
||||
if retryErr != nil {
|
||||
return nil, retryErr
|
||||
}
|
||||
if retry {
|
||||
logging.WarnPersist("Retrying due to rate limit... attempt %d of %d", logging.PersistTimeArg, time.Millisecond*time.Duration(after+100))
|
||||
logging.WarnPersist(fmt.Sprintf("Retrying due to rate limit... attempt %d of %d", attempts, maxRetries), logging.PersistTimeArg, time.Millisecond*time.Duration(after+100))
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
@@ -262,7 +272,7 @@ func (a *anthropicClient) stream(ctx context.Context, messages []message.Message
|
||||
event := anthropicStream.Current()
|
||||
err := accumulatedMessage.Accumulate(event)
|
||||
if err != nil {
|
||||
eventChan <- ProviderEvent{Type: EventError, Error: err}
|
||||
logging.Warn("Error accumulating message", "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -351,7 +361,7 @@ func (a *anthropicClient) stream(ctx context.Context, messages []message.Message
|
||||
return
|
||||
}
|
||||
if retry {
|
||||
logging.WarnPersist("Retrying due to rate limit... attempt %d of %d", logging.PersistTimeArg, time.Millisecond*time.Duration(after+100))
|
||||
logging.WarnPersist(fmt.Sprintf("Retrying due to rate limit... attempt %d of %d", attempts, maxRetries), logging.PersistTimeArg, time.Millisecond*time.Duration(after+100))
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
// context cancelled
|
||||
|
||||
47
internal/llm/provider/azure.go
Normal file
47
internal/llm/provider/azure.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
|
||||
"github.com/openai/openai-go"
|
||||
"github.com/openai/openai-go/azure"
|
||||
"github.com/openai/openai-go/option"
|
||||
)
|
||||
|
||||
type azureClient struct {
|
||||
*openaiClient
|
||||
}
|
||||
|
||||
type AzureClient ProviderClient
|
||||
|
||||
func newAzureClient(opts providerClientOptions) AzureClient {
|
||||
|
||||
endpoint := os.Getenv("AZURE_OPENAI_ENDPOINT") // ex: https://foo.openai.azure.com
|
||||
apiVersion := os.Getenv("AZURE_OPENAI_API_VERSION") // ex: 2025-04-01-preview
|
||||
|
||||
if endpoint == "" || apiVersion == "" {
|
||||
return &azureClient{openaiClient: newOpenAIClient(opts).(*openaiClient)}
|
||||
}
|
||||
|
||||
reqOpts := []option.RequestOption{
|
||||
azure.WithEndpoint(endpoint, apiVersion),
|
||||
}
|
||||
|
||||
if opts.apiKey != "" || os.Getenv("AZURE_OPENAI_API_KEY") != "" {
|
||||
key := opts.apiKey
|
||||
if key == "" {
|
||||
key = os.Getenv("AZURE_OPENAI_API_KEY")
|
||||
}
|
||||
reqOpts = append(reqOpts, azure.WithAPIKey(key))
|
||||
} else if cred, err := azidentity.NewDefaultAzureCredential(nil); err == nil {
|
||||
reqOpts = append(reqOpts, azure.WithTokenCredential(cred))
|
||||
}
|
||||
|
||||
base := &openaiClient{
|
||||
providerOptions: opts,
|
||||
client: openai.NewClient(reqOpts...),
|
||||
}
|
||||
|
||||
return &azureClient{openaiClient: base}
|
||||
}
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/kujtimiihoxha/opencode/internal/llm/tools"
|
||||
"github.com/kujtimiihoxha/opencode/internal/message"
|
||||
"github.com/opencode-ai/opencode/internal/llm/tools"
|
||||
"github.com/opencode-ai/opencode/internal/message"
|
||||
)
|
||||
|
||||
type bedrockOptions struct {
|
||||
@@ -55,7 +55,7 @@ func newBedrockClient(opts providerClientOptions) BedrockClient {
|
||||
if strings.Contains(string(opts.model.APIModel), "anthropic") {
|
||||
// Create Anthropic client with Bedrock configuration
|
||||
anthropicOpts := opts
|
||||
anthropicOpts.anthropicOptions = append(anthropicOpts.anthropicOptions,
|
||||
anthropicOpts.anthropicOptions = append(anthropicOpts.anthropicOptions,
|
||||
WithAnthropicBedrock(true),
|
||||
WithAnthropicDisableCache(),
|
||||
)
|
||||
@@ -84,7 +84,7 @@ func (b *bedrockClient) send(ctx context.Context, messages []message.Message, to
|
||||
|
||||
func (b *bedrockClient) stream(ctx context.Context, messages []message.Message, tools []tools.BaseTool) <-chan ProviderEvent {
|
||||
eventChan := make(chan ProviderEvent)
|
||||
|
||||
|
||||
if b.childProvider == nil {
|
||||
go func() {
|
||||
eventChan <- ProviderEvent{
|
||||
@@ -95,6 +95,7 @@ func (b *bedrockClient) stream(ctx context.Context, messages []message.Message,
|
||||
}()
|
||||
return eventChan
|
||||
}
|
||||
|
||||
|
||||
return b.childProvider.stream(ctx, messages, tools)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,14 +9,12 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/generative-ai-go/genai"
|
||||
"github.com/google/uuid"
|
||||
"github.com/kujtimiihoxha/opencode/internal/config"
|
||||
"github.com/kujtimiihoxha/opencode/internal/llm/tools"
|
||||
"github.com/kujtimiihoxha/opencode/internal/logging"
|
||||
"github.com/kujtimiihoxha/opencode/internal/message"
|
||||
"google.golang.org/api/iterator"
|
||||
"google.golang.org/api/option"
|
||||
"github.com/opencode-ai/opencode/internal/config"
|
||||
"github.com/opencode-ai/opencode/internal/llm/tools"
|
||||
"github.com/opencode-ai/opencode/internal/logging"
|
||||
"github.com/opencode-ai/opencode/internal/message"
|
||||
"google.golang.org/genai"
|
||||
)
|
||||
|
||||
type geminiOptions struct {
|
||||
@@ -39,7 +37,7 @@ func newGeminiClient(opts providerClientOptions) GeminiClient {
|
||||
o(&geminiOpts)
|
||||
}
|
||||
|
||||
client, err := genai.NewClient(context.Background(), option.WithAPIKey(opts.apiKey))
|
||||
client, err := genai.NewClient(context.Background(), &genai.ClientConfig{APIKey: opts.apiKey, Backend: genai.BackendGeminiAPI})
|
||||
if err != nil {
|
||||
logging.Error("Failed to create Gemini client", "error", err)
|
||||
return nil
|
||||
@@ -54,43 +52,40 @@ func newGeminiClient(opts providerClientOptions) GeminiClient {
|
||||
|
||||
func (g *geminiClient) convertMessages(messages []message.Message) []*genai.Content {
|
||||
var history []*genai.Content
|
||||
|
||||
// Add system message first
|
||||
history = append(history, &genai.Content{
|
||||
Parts: []genai.Part{genai.Text(g.providerOptions.systemMessage)},
|
||||
Role: "user",
|
||||
})
|
||||
|
||||
// Add a system response to acknowledge the system message
|
||||
history = append(history, &genai.Content{
|
||||
Parts: []genai.Part{genai.Text("I'll help you with that.")},
|
||||
Role: "model",
|
||||
})
|
||||
|
||||
for _, msg := range messages {
|
||||
switch msg.Role {
|
||||
case message.User:
|
||||
var parts []*genai.Part
|
||||
parts = append(parts, &genai.Part{Text: msg.Content().String()})
|
||||
for _, binaryContent := range msg.BinaryContent() {
|
||||
imageFormat := strings.Split(binaryContent.MIMEType, "/")
|
||||
parts = append(parts, &genai.Part{InlineData: &genai.Blob{
|
||||
MIMEType: imageFormat[1],
|
||||
Data: binaryContent.Data,
|
||||
}})
|
||||
}
|
||||
history = append(history, &genai.Content{
|
||||
Parts: []genai.Part{genai.Text(msg.Content().String())},
|
||||
Parts: parts,
|
||||
Role: "user",
|
||||
})
|
||||
|
||||
case message.Assistant:
|
||||
content := &genai.Content{
|
||||
Role: "model",
|
||||
Parts: []genai.Part{},
|
||||
Parts: []*genai.Part{},
|
||||
}
|
||||
|
||||
if msg.Content().String() != "" {
|
||||
content.Parts = append(content.Parts, genai.Text(msg.Content().String()))
|
||||
content.Parts = append(content.Parts, &genai.Part{Text: msg.Content().String()})
|
||||
}
|
||||
|
||||
if len(msg.ToolCalls()) > 0 {
|
||||
for _, call := range msg.ToolCalls() {
|
||||
args, _ := parseJsonToMap(call.Input)
|
||||
content.Parts = append(content.Parts, genai.FunctionCall{
|
||||
Name: call.Name,
|
||||
Args: args,
|
||||
content.Parts = append(content.Parts, &genai.Part{
|
||||
FunctionCall: &genai.FunctionCall{
|
||||
Name: call.Name,
|
||||
Args: args,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -118,10 +113,14 @@ func (g *geminiClient) convertMessages(messages []message.Message) []*genai.Cont
|
||||
}
|
||||
|
||||
history = append(history, &genai.Content{
|
||||
Parts: []genai.Part{genai.FunctionResponse{
|
||||
Name: toolCall.Name,
|
||||
Response: response,
|
||||
}},
|
||||
Parts: []*genai.Part{
|
||||
{
|
||||
FunctionResponse: &genai.FunctionResponse{
|
||||
Name: toolCall.Name,
|
||||
Response: response,
|
||||
},
|
||||
},
|
||||
},
|
||||
Role: "function",
|
||||
})
|
||||
}
|
||||
@@ -132,7 +131,8 @@ func (g *geminiClient) convertMessages(messages []message.Message) []*genai.Cont
|
||||
}
|
||||
|
||||
func (g *geminiClient) convertTools(tools []tools.BaseTool) []*genai.Tool {
|
||||
geminiTools := make([]*genai.Tool, 0, len(tools))
|
||||
geminiTool := &genai.Tool{}
|
||||
geminiTool.FunctionDeclarations = make([]*genai.FunctionDeclaration, 0, len(tools))
|
||||
|
||||
for _, tool := range tools {
|
||||
info := tool.Info()
|
||||
@@ -146,37 +146,24 @@ func (g *geminiClient) convertTools(tools []tools.BaseTool) []*genai.Tool {
|
||||
},
|
||||
}
|
||||
|
||||
geminiTools = append(geminiTools, &genai.Tool{
|
||||
FunctionDeclarations: []*genai.FunctionDeclaration{declaration},
|
||||
})
|
||||
geminiTool.FunctionDeclarations = append(geminiTool.FunctionDeclarations, declaration)
|
||||
}
|
||||
|
||||
return geminiTools
|
||||
return []*genai.Tool{geminiTool}
|
||||
}
|
||||
|
||||
func (g *geminiClient) finishReason(reason genai.FinishReason) message.FinishReason {
|
||||
reasonStr := reason.String()
|
||||
switch {
|
||||
case reasonStr == "STOP":
|
||||
case reason == genai.FinishReasonStop:
|
||||
return message.FinishReasonEndTurn
|
||||
case reasonStr == "MAX_TOKENS":
|
||||
case reason == genai.FinishReasonMaxTokens:
|
||||
return message.FinishReasonMaxTokens
|
||||
case strings.Contains(reasonStr, "FUNCTION") || strings.Contains(reasonStr, "TOOL"):
|
||||
return message.FinishReasonToolUse
|
||||
default:
|
||||
return message.FinishReasonUnknown
|
||||
}
|
||||
}
|
||||
|
||||
func (g *geminiClient) send(ctx context.Context, messages []message.Message, tools []tools.BaseTool) (*ProviderResponse, error) {
|
||||
model := g.client.GenerativeModel(g.providerOptions.model.APIModel)
|
||||
model.SetMaxOutputTokens(int32(g.providerOptions.maxTokens))
|
||||
|
||||
// Convert tools
|
||||
if len(tools) > 0 {
|
||||
model.Tools = g.convertTools(tools)
|
||||
}
|
||||
|
||||
// Convert messages
|
||||
geminiMessages := g.convertMessages(messages)
|
||||
|
||||
@@ -186,22 +173,26 @@ func (g *geminiClient) send(ctx context.Context, messages []message.Message, too
|
||||
logging.Debug("Prepared messages", "messages", string(jsonData))
|
||||
}
|
||||
|
||||
history := geminiMessages[:len(geminiMessages)-1] // All but last message
|
||||
lastMsg := geminiMessages[len(geminiMessages)-1]
|
||||
chat, _ := g.client.Chats.Create(ctx, g.providerOptions.model.APIModel, &genai.GenerateContentConfig{
|
||||
MaxOutputTokens: int32(g.providerOptions.maxTokens),
|
||||
SystemInstruction: &genai.Content{
|
||||
Parts: []*genai.Part{{Text: g.providerOptions.systemMessage}},
|
||||
},
|
||||
Tools: g.convertTools(tools),
|
||||
}, history)
|
||||
|
||||
attempts := 0
|
||||
for {
|
||||
attempts++
|
||||
chat := model.StartChat()
|
||||
chat.History = geminiMessages[:len(geminiMessages)-1] // All but last message
|
||||
var toolCalls []message.ToolCall
|
||||
|
||||
lastMsg := geminiMessages[len(geminiMessages)-1]
|
||||
var lastText string
|
||||
var lastMsgParts []genai.Part
|
||||
for _, part := range lastMsg.Parts {
|
||||
if text, ok := part.(genai.Text); ok {
|
||||
lastText = string(text)
|
||||
break
|
||||
}
|
||||
lastMsgParts = append(lastMsgParts, *part)
|
||||
}
|
||||
|
||||
resp, err := chat.SendMessage(ctx, genai.Text(lastText))
|
||||
resp, err := chat.SendMessage(ctx, lastMsgParts...)
|
||||
// If there is an error we are going to see if we can retry the call
|
||||
if err != nil {
|
||||
retry, after, retryErr := g.shouldRetry(attempts, err)
|
||||
@@ -209,7 +200,7 @@ func (g *geminiClient) send(ctx context.Context, messages []message.Message, too
|
||||
return nil, retryErr
|
||||
}
|
||||
if retry {
|
||||
logging.WarnPersist("Retrying due to rate limit... attempt %d of %d", logging.PersistTimeArg, time.Millisecond*time.Duration(after+100))
|
||||
logging.WarnPersist(fmt.Sprintf("Retrying due to rate limit... attempt %d of %d", attempts, maxRetries), logging.PersistTimeArg, time.Millisecond*time.Duration(after+100))
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
@@ -221,44 +212,43 @@ func (g *geminiClient) send(ctx context.Context, messages []message.Message, too
|
||||
}
|
||||
|
||||
content := ""
|
||||
var toolCalls []message.ToolCall
|
||||
|
||||
if len(resp.Candidates) > 0 && resp.Candidates[0].Content != nil {
|
||||
for _, part := range resp.Candidates[0].Content.Parts {
|
||||
switch p := part.(type) {
|
||||
case genai.Text:
|
||||
content = string(p)
|
||||
case genai.FunctionCall:
|
||||
switch {
|
||||
case part.Text != "":
|
||||
content = string(part.Text)
|
||||
case part.FunctionCall != nil:
|
||||
id := "call_" + uuid.New().String()
|
||||
args, _ := json.Marshal(p.Args)
|
||||
args, _ := json.Marshal(part.FunctionCall.Args)
|
||||
toolCalls = append(toolCalls, message.ToolCall{
|
||||
ID: id,
|
||||
Name: p.Name,
|
||||
Input: string(args),
|
||||
Type: "function",
|
||||
ID: id,
|
||||
Name: part.FunctionCall.Name,
|
||||
Input: string(args),
|
||||
Type: "function",
|
||||
Finished: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
finishReason := message.FinishReasonEndTurn
|
||||
if len(resp.Candidates) > 0 {
|
||||
finishReason = g.finishReason(resp.Candidates[0].FinishReason)
|
||||
}
|
||||
if len(toolCalls) > 0 {
|
||||
finishReason = message.FinishReasonToolUse
|
||||
}
|
||||
|
||||
return &ProviderResponse{
|
||||
Content: content,
|
||||
ToolCalls: toolCalls,
|
||||
Usage: g.usage(resp),
|
||||
FinishReason: g.finishReason(resp.Candidates[0].FinishReason),
|
||||
FinishReason: finishReason,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (g *geminiClient) stream(ctx context.Context, messages []message.Message, tools []tools.BaseTool) <-chan ProviderEvent {
|
||||
model := g.client.GenerativeModel(g.providerOptions.model.APIModel)
|
||||
model.SetMaxOutputTokens(int32(g.providerOptions.maxTokens))
|
||||
|
||||
// Convert tools
|
||||
if len(tools) > 0 {
|
||||
model.Tools = g.convertTools(tools)
|
||||
}
|
||||
|
||||
// Convert messages
|
||||
geminiMessages := g.convertMessages(messages)
|
||||
|
||||
@@ -268,6 +258,16 @@ func (g *geminiClient) stream(ctx context.Context, messages []message.Message, t
|
||||
logging.Debug("Prepared messages", "messages", string(jsonData))
|
||||
}
|
||||
|
||||
history := geminiMessages[:len(geminiMessages)-1] // All but last message
|
||||
lastMsg := geminiMessages[len(geminiMessages)-1]
|
||||
chat, _ := g.client.Chats.Create(ctx, g.providerOptions.model.APIModel, &genai.GenerateContentConfig{
|
||||
MaxOutputTokens: int32(g.providerOptions.maxTokens),
|
||||
SystemInstruction: &genai.Content{
|
||||
Parts: []*genai.Part{{Text: g.providerOptions.systemMessage}},
|
||||
},
|
||||
Tools: g.convertTools(tools),
|
||||
}, history)
|
||||
|
||||
attempts := 0
|
||||
eventChan := make(chan ProviderEvent)
|
||||
|
||||
@@ -276,19 +276,6 @@ func (g *geminiClient) stream(ctx context.Context, messages []message.Message, t
|
||||
|
||||
for {
|
||||
attempts++
|
||||
chat := model.StartChat()
|
||||
chat.History = geminiMessages[:len(geminiMessages)-1] // All but last message
|
||||
|
||||
lastMsg := geminiMessages[len(geminiMessages)-1]
|
||||
var lastText string
|
||||
for _, part := range lastMsg.Parts {
|
||||
if text, ok := part.(genai.Text); ok {
|
||||
lastText = string(text)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
iter := chat.SendMessageStream(ctx, genai.Text(lastText))
|
||||
|
||||
currentContent := ""
|
||||
toolCalls := []message.ToolCall{}
|
||||
@@ -296,11 +283,12 @@ func (g *geminiClient) stream(ctx context.Context, messages []message.Message, t
|
||||
|
||||
eventChan <- ProviderEvent{Type: EventContentStart}
|
||||
|
||||
for {
|
||||
resp, err := iter.Next()
|
||||
if err == iterator.Done {
|
||||
break
|
||||
}
|
||||
var lastMsgParts []genai.Part
|
||||
|
||||
for _, part := range lastMsg.Parts {
|
||||
lastMsgParts = append(lastMsgParts, *part)
|
||||
}
|
||||
for resp, err := range chat.SendMessageStream(ctx, lastMsgParts...) {
|
||||
if err != nil {
|
||||
retry, after, retryErr := g.shouldRetry(attempts, err)
|
||||
if retryErr != nil {
|
||||
@@ -308,7 +296,7 @@ func (g *geminiClient) stream(ctx context.Context, messages []message.Message, t
|
||||
return
|
||||
}
|
||||
if retry {
|
||||
logging.WarnPersist("Retrying due to rate limit... attempt %d of %d", logging.PersistTimeArg, time.Millisecond*time.Duration(after+100))
|
||||
logging.WarnPersist(fmt.Sprintf("Retrying due to rate limit... attempt %d of %d", attempts, maxRetries), logging.PersistTimeArg, time.Millisecond*time.Duration(after+100))
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
if ctx.Err() != nil {
|
||||
@@ -329,25 +317,25 @@ func (g *geminiClient) stream(ctx context.Context, messages []message.Message, t
|
||||
|
||||
if len(resp.Candidates) > 0 && resp.Candidates[0].Content != nil {
|
||||
for _, part := range resp.Candidates[0].Content.Parts {
|
||||
switch p := part.(type) {
|
||||
case genai.Text:
|
||||
newText := string(p)
|
||||
delta := newText[len(currentContent):]
|
||||
switch {
|
||||
case part.Text != "":
|
||||
delta := string(part.Text)
|
||||
if delta != "" {
|
||||
eventChan <- ProviderEvent{
|
||||
Type: EventContentDelta,
|
||||
Content: delta,
|
||||
}
|
||||
currentContent = newText
|
||||
currentContent += delta
|
||||
}
|
||||
case genai.FunctionCall:
|
||||
case part.FunctionCall != nil:
|
||||
id := "call_" + uuid.New().String()
|
||||
args, _ := json.Marshal(p.Args)
|
||||
args, _ := json.Marshal(part.FunctionCall.Args)
|
||||
newCall := message.ToolCall{
|
||||
ID: id,
|
||||
Name: p.Name,
|
||||
Input: string(args),
|
||||
Type: "function",
|
||||
ID: id,
|
||||
Name: part.FunctionCall.Name,
|
||||
Input: string(args),
|
||||
Type: "function",
|
||||
Finished: true,
|
||||
}
|
||||
|
||||
isNew := true
|
||||
@@ -369,37 +357,26 @@ func (g *geminiClient) stream(ctx context.Context, messages []message.Message, t
|
||||
eventChan <- ProviderEvent{Type: EventContentStop}
|
||||
|
||||
if finalResp != nil {
|
||||
|
||||
finishReason := message.FinishReasonEndTurn
|
||||
if len(finalResp.Candidates) > 0 {
|
||||
finishReason = g.finishReason(finalResp.Candidates[0].FinishReason)
|
||||
}
|
||||
if len(toolCalls) > 0 {
|
||||
finishReason = message.FinishReasonToolUse
|
||||
}
|
||||
eventChan <- ProviderEvent{
|
||||
Type: EventComplete,
|
||||
Response: &ProviderResponse{
|
||||
Content: currentContent,
|
||||
ToolCalls: toolCalls,
|
||||
Usage: g.usage(finalResp),
|
||||
FinishReason: g.finishReason(finalResp.Candidates[0].FinishReason),
|
||||
FinishReason: finishReason,
|
||||
},
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// If we get here, we need to retry
|
||||
if attempts > maxRetries {
|
||||
eventChan <- ProviderEvent{
|
||||
Type: EventError,
|
||||
Error: fmt.Errorf("maximum retry attempts reached: %d retries", maxRetries),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Wait before retrying
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
if ctx.Err() != nil {
|
||||
eventChan <- ProviderEvent{Type: EventError, Error: ctx.Err()}
|
||||
}
|
||||
return
|
||||
case <-time.After(time.Duration(2000*(1<<(attempts-1))) * time.Millisecond):
|
||||
continue
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -443,12 +420,12 @@ func (g *geminiClient) toolCalls(resp *genai.GenerateContentResponse) []message.
|
||||
|
||||
if len(resp.Candidates) > 0 && resp.Candidates[0].Content != nil {
|
||||
for _, part := range resp.Candidates[0].Content.Parts {
|
||||
if funcCall, ok := part.(genai.FunctionCall); ok {
|
||||
if part.FunctionCall != nil {
|
||||
id := "call_" + uuid.New().String()
|
||||
args, _ := json.Marshal(funcCall.Args)
|
||||
args, _ := json.Marshal(part.FunctionCall.Args)
|
||||
toolCalls = append(toolCalls, message.ToolCall{
|
||||
ID: id,
|
||||
Name: funcCall.Name,
|
||||
Name: part.FunctionCall.Name,
|
||||
Input: string(args),
|
||||
Type: "function",
|
||||
})
|
||||
|
||||
@@ -8,19 +8,21 @@ import (
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/kujtimiihoxha/opencode/internal/config"
|
||||
"github.com/kujtimiihoxha/opencode/internal/llm/tools"
|
||||
"github.com/kujtimiihoxha/opencode/internal/logging"
|
||||
"github.com/kujtimiihoxha/opencode/internal/message"
|
||||
"github.com/openai/openai-go"
|
||||
"github.com/openai/openai-go/option"
|
||||
"github.com/openai/openai-go/shared"
|
||||
"github.com/opencode-ai/opencode/internal/config"
|
||||
"github.com/opencode-ai/opencode/internal/llm/models"
|
||||
"github.com/opencode-ai/opencode/internal/llm/tools"
|
||||
"github.com/opencode-ai/opencode/internal/logging"
|
||||
"github.com/opencode-ai/opencode/internal/message"
|
||||
)
|
||||
|
||||
type openaiOptions struct {
|
||||
baseURL string
|
||||
disableCache bool
|
||||
reasoningEffort string
|
||||
extraHeaders map[string]string
|
||||
}
|
||||
|
||||
type OpenAIOption func(*openaiOptions)
|
||||
@@ -49,6 +51,12 @@ func newOpenAIClient(opts providerClientOptions) OpenAIClient {
|
||||
openaiClientOptions = append(openaiClientOptions, option.WithBaseURL(openaiOpts.baseURL))
|
||||
}
|
||||
|
||||
if openaiOpts.extraHeaders != nil {
|
||||
for key, value := range openaiOpts.extraHeaders {
|
||||
openaiClientOptions = append(openaiClientOptions, option.WithHeader(key, value))
|
||||
}
|
||||
}
|
||||
|
||||
client := openai.NewClient(openaiClientOptions...)
|
||||
return &openaiClient{
|
||||
providerOptions: opts,
|
||||
@@ -64,7 +72,17 @@ func (o *openaiClient) convertMessages(messages []message.Message) (openaiMessag
|
||||
for _, msg := range messages {
|
||||
switch msg.Role {
|
||||
case message.User:
|
||||
openaiMessages = append(openaiMessages, openai.UserMessage(msg.Content().String()))
|
||||
var content []openai.ChatCompletionContentPartUnionParam
|
||||
textBlock := openai.ChatCompletionContentPartTextParam{Text: msg.Content().String()}
|
||||
content = append(content, openai.ChatCompletionContentPartUnionParam{OfText: &textBlock})
|
||||
for _, binaryContent := range msg.BinaryContent() {
|
||||
imageURL := openai.ChatCompletionContentPartImageImageURLParam{URL: binaryContent.String(models.ProviderOpenAI)}
|
||||
imageBlock := openai.ChatCompletionContentPartImageParam{ImageURL: imageURL}
|
||||
|
||||
content = append(content, openai.ChatCompletionContentPartUnionParam{OfImageURL: &imageBlock})
|
||||
}
|
||||
|
||||
openaiMessages = append(openaiMessages, openai.UserMessage(content))
|
||||
|
||||
case message.Assistant:
|
||||
assistantMsg := openai.ChatCompletionAssistantMessageParam{
|
||||
@@ -188,7 +206,7 @@ func (o *openaiClient) send(ctx context.Context, messages []message.Message, too
|
||||
return nil, retryErr
|
||||
}
|
||||
if retry {
|
||||
logging.WarnPersist("Retrying due to rate limit... attempt %d of %d", logging.PersistTimeArg, time.Millisecond*time.Duration(after+100))
|
||||
logging.WarnPersist(fmt.Sprintf("Retrying due to rate limit... attempt %d of %d", attempts, maxRetries), logging.PersistTimeArg, time.Millisecond*time.Duration(after+100))
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
@@ -204,11 +222,18 @@ func (o *openaiClient) send(ctx context.Context, messages []message.Message, too
|
||||
content = openaiResponse.Choices[0].Message.Content
|
||||
}
|
||||
|
||||
toolCalls := o.toolCalls(*openaiResponse)
|
||||
finishReason := o.finishReason(string(openaiResponse.Choices[0].FinishReason))
|
||||
|
||||
if len(toolCalls) > 0 {
|
||||
finishReason = message.FinishReasonToolUse
|
||||
}
|
||||
|
||||
return &ProviderResponse{
|
||||
Content: content,
|
||||
ToolCalls: o.toolCalls(*openaiResponse),
|
||||
ToolCalls: toolCalls,
|
||||
Usage: o.usage(*openaiResponse),
|
||||
FinishReason: o.finishReason(string(openaiResponse.Choices[0].FinishReason)),
|
||||
FinishReason: finishReason,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
@@ -244,15 +269,6 @@ func (o *openaiClient) stream(ctx context.Context, messages []message.Message, t
|
||||
chunk := openaiStream.Current()
|
||||
acc.AddChunk(chunk)
|
||||
|
||||
if tool, ok := acc.JustFinishedToolCall(); ok {
|
||||
toolCalls = append(toolCalls, message.ToolCall{
|
||||
ID: tool.Id,
|
||||
Name: tool.Name,
|
||||
Input: tool.Arguments,
|
||||
Type: "function",
|
||||
})
|
||||
}
|
||||
|
||||
for _, choice := range chunk.Choices {
|
||||
if choice.Delta.Content != "" {
|
||||
eventChan <- ProviderEvent{
|
||||
@@ -267,13 +283,21 @@ func (o *openaiClient) stream(ctx context.Context, messages []message.Message, t
|
||||
err := openaiStream.Err()
|
||||
if err == nil || errors.Is(err, io.EOF) {
|
||||
// Stream completed successfully
|
||||
finishReason := o.finishReason(string(acc.ChatCompletion.Choices[0].FinishReason))
|
||||
if len(acc.ChatCompletion.Choices[0].Message.ToolCalls) > 0 {
|
||||
toolCalls = append(toolCalls, o.toolCalls(acc.ChatCompletion)...)
|
||||
}
|
||||
if len(toolCalls) > 0 {
|
||||
finishReason = message.FinishReasonToolUse
|
||||
}
|
||||
|
||||
eventChan <- ProviderEvent{
|
||||
Type: EventComplete,
|
||||
Response: &ProviderResponse{
|
||||
Content: currentContent,
|
||||
ToolCalls: toolCalls,
|
||||
Usage: o.usage(acc.ChatCompletion),
|
||||
FinishReason: o.finishReason(string(acc.ChatCompletion.Choices[0].FinishReason)),
|
||||
FinishReason: finishReason,
|
||||
},
|
||||
}
|
||||
close(eventChan)
|
||||
@@ -288,7 +312,7 @@ func (o *openaiClient) stream(ctx context.Context, messages []message.Message, t
|
||||
return
|
||||
}
|
||||
if retry {
|
||||
logging.WarnPersist("Retrying due to rate limit... attempt %d of %d", logging.PersistTimeArg, time.Millisecond*time.Duration(after+100))
|
||||
logging.WarnPersist(fmt.Sprintf("Retrying due to rate limit... attempt %d of %d", attempts, maxRetries), logging.PersistTimeArg, time.Millisecond*time.Duration(after+100))
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
// context cancelled
|
||||
@@ -375,6 +399,12 @@ func WithOpenAIBaseURL(baseURL string) OpenAIOption {
|
||||
}
|
||||
}
|
||||
|
||||
func WithOpenAIExtraHeaders(headers map[string]string) OpenAIOption {
|
||||
return func(options *openaiOptions) {
|
||||
options.extraHeaders = headers
|
||||
}
|
||||
}
|
||||
|
||||
func WithOpenAIDisableCache() OpenAIOption {
|
||||
return func(options *openaiOptions) {
|
||||
options.disableCache = true
|
||||
|
||||
@@ -4,9 +4,9 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/kujtimiihoxha/opencode/internal/llm/models"
|
||||
"github.com/kujtimiihoxha/opencode/internal/llm/tools"
|
||||
"github.com/kujtimiihoxha/opencode/internal/message"
|
||||
"github.com/opencode-ai/opencode/internal/llm/models"
|
||||
"github.com/opencode-ai/opencode/internal/llm/tools"
|
||||
"github.com/opencode-ai/opencode/internal/message"
|
||||
)
|
||||
|
||||
type EventType string
|
||||
@@ -107,6 +107,40 @@ func NewProvider(providerName models.ModelProvider, opts ...ProviderClientOption
|
||||
options: clientOptions,
|
||||
client: newBedrockClient(clientOptions),
|
||||
}, nil
|
||||
case models.ProviderGROQ:
|
||||
clientOptions.openaiOptions = append(clientOptions.openaiOptions,
|
||||
WithOpenAIBaseURL("https://api.groq.com/openai/v1"),
|
||||
)
|
||||
return &baseProvider[OpenAIClient]{
|
||||
options: clientOptions,
|
||||
client: newOpenAIClient(clientOptions),
|
||||
}, nil
|
||||
case models.ProviderAzure:
|
||||
return &baseProvider[AzureClient]{
|
||||
options: clientOptions,
|
||||
client: newAzureClient(clientOptions),
|
||||
}, nil
|
||||
case models.ProviderOpenRouter:
|
||||
clientOptions.openaiOptions = append(clientOptions.openaiOptions,
|
||||
WithOpenAIBaseURL("https://openrouter.ai/api/v1"),
|
||||
WithOpenAIExtraHeaders(map[string]string{
|
||||
"HTTP-Referer": "opencode.ai",
|
||||
"X-Title": "OpenCode",
|
||||
}),
|
||||
)
|
||||
return &baseProvider[OpenAIClient]{
|
||||
options: clientOptions,
|
||||
client: newOpenAIClient(clientOptions),
|
||||
}, nil
|
||||
case models.ProviderXAI:
|
||||
clientOptions.openaiOptions = append(clientOptions.openaiOptions,
|
||||
WithOpenAIBaseURL("https://api.x.ai/v1"),
|
||||
)
|
||||
return &baseProvider[OpenAIClient]{
|
||||
options: clientOptions,
|
||||
client: newOpenAIClient(clientOptions),
|
||||
}, nil
|
||||
|
||||
case models.ProviderMock:
|
||||
// TODO: implement mock client for test
|
||||
panic("not implemented")
|
||||
|
||||
@@ -7,9 +7,9 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/kujtimiihoxha/opencode/internal/config"
|
||||
"github.com/kujtimiihoxha/opencode/internal/llm/tools/shell"
|
||||
"github.com/kujtimiihoxha/opencode/internal/permission"
|
||||
"github.com/opencode-ai/opencode/internal/config"
|
||||
"github.com/opencode-ai/opencode/internal/llm/tools/shell"
|
||||
"github.com/opencode-ai/opencode/internal/permission"
|
||||
)
|
||||
|
||||
type BashParams struct {
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/kujtimiihoxha/opencode/internal/lsp"
|
||||
"github.com/kujtimiihoxha/opencode/internal/lsp/protocol"
|
||||
"github.com/opencode-ai/opencode/internal/lsp"
|
||||
"github.com/opencode-ai/opencode/internal/lsp/protocol"
|
||||
)
|
||||
|
||||
type DiagnosticsParams struct {
|
||||
|
||||
@@ -9,12 +9,12 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/kujtimiihoxha/opencode/internal/config"
|
||||
"github.com/kujtimiihoxha/opencode/internal/diff"
|
||||
"github.com/kujtimiihoxha/opencode/internal/history"
|
||||
"github.com/kujtimiihoxha/opencode/internal/logging"
|
||||
"github.com/kujtimiihoxha/opencode/internal/lsp"
|
||||
"github.com/kujtimiihoxha/opencode/internal/permission"
|
||||
"github.com/opencode-ai/opencode/internal/config"
|
||||
"github.com/opencode-ai/opencode/internal/diff"
|
||||
"github.com/opencode-ai/opencode/internal/history"
|
||||
"github.com/opencode-ai/opencode/internal/logging"
|
||||
"github.com/opencode-ai/opencode/internal/lsp"
|
||||
"github.com/opencode-ai/opencode/internal/permission"
|
||||
)
|
||||
|
||||
type EditParams struct {
|
||||
|
||||
@@ -11,8 +11,8 @@ import (
|
||||
|
||||
md "github.com/JohannesKaufmann/html-to-markdown"
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/kujtimiihoxha/opencode/internal/config"
|
||||
"github.com/kujtimiihoxha/opencode/internal/permission"
|
||||
"github.com/opencode-ai/opencode/internal/config"
|
||||
"github.com/opencode-ai/opencode/internal/permission"
|
||||
)
|
||||
|
||||
type FetchParams struct {
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bmatcuk/doublestar/v4"
|
||||
"github.com/kujtimiihoxha/opencode/internal/config"
|
||||
"github.com/opencode-ai/opencode/internal/config"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -132,14 +134,73 @@ func (g *globTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error)
|
||||
}
|
||||
|
||||
func globFiles(pattern, searchPath string, limit int) ([]string, bool, error) {
|
||||
if !strings.HasPrefix(pattern, "/") && !strings.HasPrefix(pattern, searchPath) {
|
||||
if !strings.HasSuffix(searchPath, "/") {
|
||||
searchPath += "/"
|
||||
}
|
||||
pattern = searchPath + pattern
|
||||
matches, err := globWithRipgrep(pattern, searchPath, limit)
|
||||
if err == nil {
|
||||
return matches, len(matches) >= limit, nil
|
||||
}
|
||||
|
||||
fsys := os.DirFS("/")
|
||||
return globWithDoublestar(pattern, searchPath, limit)
|
||||
}
|
||||
|
||||
func globWithRipgrep(
|
||||
pattern, searchRoot string,
|
||||
limit int,
|
||||
) ([]string, error) {
|
||||
if searchRoot == "" {
|
||||
searchRoot = "."
|
||||
}
|
||||
|
||||
rgBin, err := exec.LookPath("rg")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ripgrep not found in $PATH: %w", err)
|
||||
}
|
||||
|
||||
if !filepath.IsAbs(pattern) && !strings.HasPrefix(pattern, "/") {
|
||||
pattern = "/" + pattern
|
||||
}
|
||||
|
||||
args := []string{
|
||||
"--files",
|
||||
"--null",
|
||||
"--glob", pattern,
|
||||
"-L",
|
||||
}
|
||||
|
||||
cmd := exec.Command(rgBin, args...)
|
||||
cmd.Dir = searchRoot
|
||||
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
if ee, ok := err.(*exec.ExitError); ok && ee.ExitCode() == 1 {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("ripgrep: %w\n%s", err, out)
|
||||
}
|
||||
|
||||
var matches []string
|
||||
for _, p := range bytes.Split(out, []byte{0}) {
|
||||
if len(p) == 0 {
|
||||
continue
|
||||
}
|
||||
abs := filepath.Join(searchRoot, string(p))
|
||||
if skipHidden(abs) {
|
||||
continue
|
||||
}
|
||||
matches = append(matches, abs)
|
||||
}
|
||||
|
||||
sort.SliceStable(matches, func(i, j int) bool {
|
||||
return len(matches[i]) < len(matches[j])
|
||||
})
|
||||
|
||||
if len(matches) > limit {
|
||||
matches = matches[:limit]
|
||||
}
|
||||
return matches, nil
|
||||
}
|
||||
|
||||
func globWithDoublestar(pattern, searchPath string, limit int) ([]string, bool, error) {
|
||||
fsys := os.DirFS(searchPath)
|
||||
|
||||
relPattern := strings.TrimPrefix(pattern, "/")
|
||||
|
||||
@@ -158,7 +219,11 @@ func globFiles(pattern, searchPath string, limit int) ([]string, bool, error) {
|
||||
return nil // Skip files we can't access
|
||||
}
|
||||
|
||||
absPath := "/" + path // Restore absolute path
|
||||
absPath := path // Restore absolute path
|
||||
if !strings.HasPrefix(absPath, searchPath) {
|
||||
absPath = filepath.Join(searchPath, absPath)
|
||||
}
|
||||
|
||||
matches = append(matches, fileInfo{
|
||||
path: absPath,
|
||||
modTime: info.ModTime(),
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/kujtimiihoxha/opencode/internal/config"
|
||||
"github.com/opencode-ai/opencode/internal/config"
|
||||
)
|
||||
|
||||
type GrepParams struct {
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/kujtimiihoxha/opencode/internal/config"
|
||||
"github.com/opencode-ai/opencode/internal/config"
|
||||
)
|
||||
|
||||
type LSParams struct {
|
||||
|
||||
@@ -8,12 +8,12 @@ import (
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/kujtimiihoxha/opencode/internal/config"
|
||||
"github.com/kujtimiihoxha/opencode/internal/diff"
|
||||
"github.com/kujtimiihoxha/opencode/internal/history"
|
||||
"github.com/kujtimiihoxha/opencode/internal/logging"
|
||||
"github.com/kujtimiihoxha/opencode/internal/lsp"
|
||||
"github.com/kujtimiihoxha/opencode/internal/permission"
|
||||
"github.com/opencode-ai/opencode/internal/config"
|
||||
"github.com/opencode-ai/opencode/internal/diff"
|
||||
"github.com/opencode-ai/opencode/internal/history"
|
||||
"github.com/opencode-ai/opencode/internal/logging"
|
||||
"github.com/opencode-ai/opencode/internal/lsp"
|
||||
"github.com/opencode-ai/opencode/internal/permission"
|
||||
)
|
||||
|
||||
type PatchParams struct {
|
||||
|
||||
@@ -47,7 +47,9 @@ func GetPersistentShell(workingDir string) *PersistentShell {
|
||||
shellInstance = newPersistentShell(workingDir)
|
||||
})
|
||||
|
||||
if !shellInstance.isAlive {
|
||||
if shellInstance == nil {
|
||||
shellInstance = newPersistentShell(workingDir)
|
||||
} else if !shellInstance.isAlive {
|
||||
shellInstance = newPersistentShell(shellInstance.cwd)
|
||||
}
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/kujtimiihoxha/opencode/internal/config"
|
||||
"github.com/kujtimiihoxha/opencode/internal/lsp"
|
||||
"github.com/opencode-ai/opencode/internal/config"
|
||||
"github.com/opencode-ai/opencode/internal/lsp"
|
||||
)
|
||||
|
||||
type ViewParams struct {
|
||||
|
||||
@@ -9,12 +9,12 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/kujtimiihoxha/opencode/internal/config"
|
||||
"github.com/kujtimiihoxha/opencode/internal/diff"
|
||||
"github.com/kujtimiihoxha/opencode/internal/history"
|
||||
"github.com/kujtimiihoxha/opencode/internal/logging"
|
||||
"github.com/kujtimiihoxha/opencode/internal/lsp"
|
||||
"github.com/kujtimiihoxha/opencode/internal/permission"
|
||||
"github.com/opencode-ai/opencode/internal/config"
|
||||
"github.com/opencode-ai/opencode/internal/diff"
|
||||
"github.com/opencode-ai/opencode/internal/history"
|
||||
"github.com/opencode-ai/opencode/internal/logging"
|
||||
"github.com/opencode-ai/opencode/internal/lsp"
|
||||
"github.com/opencode-ai/opencode/internal/permission"
|
||||
)
|
||||
|
||||
type WriteParams struct {
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/go-logfmt/logfmt"
|
||||
"github.com/kujtimiihoxha/opencode/internal/pubsub"
|
||||
"github.com/opencode-ai/opencode/internal/pubsub"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -14,9 +14,9 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/kujtimiihoxha/opencode/internal/config"
|
||||
"github.com/kujtimiihoxha/opencode/internal/logging"
|
||||
"github.com/kujtimiihoxha/opencode/internal/lsp/protocol"
|
||||
"github.com/opencode-ai/opencode/internal/config"
|
||||
"github.com/opencode-ai/opencode/internal/logging"
|
||||
"github.com/opencode-ai/opencode/internal/lsp/protocol"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
@@ -389,7 +389,7 @@ func (c *Client) openKeyConfigFiles(ctx context.Context) {
|
||||
filepath.Join(workDir, "package.json"),
|
||||
filepath.Join(workDir, "jsconfig.json"),
|
||||
}
|
||||
|
||||
|
||||
// Also find and open a few TypeScript files to help the server initialize
|
||||
c.openTypeScriptFiles(ctx, workDir)
|
||||
case ServerTypeGo:
|
||||
@@ -547,12 +547,12 @@ func (c *Client) openTypeScriptFiles(ctx context.Context, workDir string) {
|
||||
// shouldSkipDir returns true if the directory should be skipped during file search
|
||||
func shouldSkipDir(path string) bool {
|
||||
dirName := filepath.Base(path)
|
||||
|
||||
|
||||
// Skip hidden directories
|
||||
if strings.HasPrefix(dirName, ".") {
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
// Skip common directories that won't contain relevant source files
|
||||
skipDirs := map[string]bool{
|
||||
"node_modules": true,
|
||||
@@ -562,7 +562,7 @@ func shouldSkipDir(path string) bool {
|
||||
"vendor": true,
|
||||
"target": true,
|
||||
}
|
||||
|
||||
|
||||
return skipDirs[dirName]
|
||||
}
|
||||
|
||||
@@ -776,3 +776,10 @@ func (c *Client) GetDiagnosticsForFile(ctx context.Context, filepath string) ([]
|
||||
|
||||
return diagnostics, nil
|
||||
}
|
||||
|
||||
// ClearDiagnosticsForURI removes diagnostics for a specific URI from the cache
|
||||
func (c *Client) ClearDiagnosticsForURI(uri protocol.DocumentUri) {
|
||||
c.diagnosticsMu.Lock()
|
||||
defer c.diagnosticsMu.Unlock()
|
||||
delete(c.diagnostics, uri)
|
||||
}
|
||||
|
||||
@@ -3,10 +3,10 @@ package lsp
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/kujtimiihoxha/opencode/internal/config"
|
||||
"github.com/kujtimiihoxha/opencode/internal/logging"
|
||||
"github.com/kujtimiihoxha/opencode/internal/lsp/protocol"
|
||||
"github.com/kujtimiihoxha/opencode/internal/lsp/util"
|
||||
"github.com/opencode-ai/opencode/internal/config"
|
||||
"github.com/opencode-ai/opencode/internal/logging"
|
||||
"github.com/opencode-ai/opencode/internal/lsp/protocol"
|
||||
"github.com/opencode-ai/opencode/internal/lsp/util"
|
||||
)
|
||||
|
||||
// Requests
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/kujtimiihoxha/opencode/internal/lsp/protocol"
|
||||
"github.com/opencode-ai/opencode/internal/lsp/protocol"
|
||||
)
|
||||
|
||||
func DetectLanguageID(uri string) protocol.LanguageKind {
|
||||
|
||||
@@ -4,7 +4,7 @@ package lsp
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/kujtimiihoxha/opencode/internal/lsp/protocol"
|
||||
"github.com/opencode-ai/opencode/internal/lsp/protocol"
|
||||
)
|
||||
|
||||
// Implementation sends a textDocument/implementation request to the LSP server.
|
||||
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/kujtimiihoxha/opencode/internal/config"
|
||||
"github.com/kujtimiihoxha/opencode/internal/logging"
|
||||
"github.com/opencode-ai/opencode/internal/config"
|
||||
"github.com/opencode-ai/opencode/internal/logging"
|
||||
)
|
||||
|
||||
// Write writes an LSP message to the given writer
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/kujtimiihoxha/opencode/internal/lsp/protocol"
|
||||
"github.com/opencode-ai/opencode/internal/lsp/protocol"
|
||||
)
|
||||
|
||||
func applyTextEdits(uri protocol.DocumentUri, edits []protocol.TextEdit) error {
|
||||
|
||||
@@ -11,10 +11,10 @@ import (
|
||||
|
||||
"github.com/bmatcuk/doublestar/v4"
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/kujtimiihoxha/opencode/internal/config"
|
||||
"github.com/kujtimiihoxha/opencode/internal/logging"
|
||||
"github.com/kujtimiihoxha/opencode/internal/lsp"
|
||||
"github.com/kujtimiihoxha/opencode/internal/lsp/protocol"
|
||||
"github.com/opencode-ai/opencode/internal/config"
|
||||
"github.com/opencode-ai/opencode/internal/logging"
|
||||
"github.com/opencode-ai/opencode/internal/lsp"
|
||||
"github.com/opencode-ai/opencode/internal/lsp/protocol"
|
||||
)
|
||||
|
||||
// WorkspaceWatcher manages LSP file watching
|
||||
@@ -96,19 +96,19 @@ func (w *WorkspaceWatcher) AddRegistrations(ctx context.Context, id string, watc
|
||||
// Determine server type for specialized handling
|
||||
serverName := getServerNameFromContext(ctx)
|
||||
logging.Debug("Server type detected", "serverName", serverName)
|
||||
|
||||
|
||||
// Check if this server has sent file watchers
|
||||
hasFileWatchers := len(watchers) > 0
|
||||
|
||||
|
||||
// For servers that need file preloading, we'll use a smart approach
|
||||
if shouldPreloadFiles(serverName) || !hasFileWatchers {
|
||||
go func() {
|
||||
startTime := time.Now()
|
||||
filesOpened := 0
|
||||
|
||||
|
||||
// Determine max files to open based on server type
|
||||
maxFilesToOpen := 50 // Default conservative limit
|
||||
|
||||
|
||||
switch serverName {
|
||||
case "typescript", "typescript-language-server", "tsserver", "vtsls":
|
||||
// TypeScript servers benefit from seeing more files
|
||||
@@ -117,17 +117,17 @@ func (w *WorkspaceWatcher) AddRegistrations(ctx context.Context, id string, watc
|
||||
// Java servers need to see many files for project model
|
||||
maxFilesToOpen = 200
|
||||
}
|
||||
|
||||
|
||||
// First, open high-priority files
|
||||
highPriorityFilesOpened := w.openHighPriorityFiles(ctx, serverName)
|
||||
filesOpened += highPriorityFilesOpened
|
||||
|
||||
|
||||
if cnf.DebugLSP {
|
||||
logging.Debug("Opened high-priority files",
|
||||
logging.Debug("Opened high-priority files",
|
||||
"count", highPriorityFilesOpened,
|
||||
"serverName", serverName)
|
||||
}
|
||||
|
||||
|
||||
// If we've already opened enough high-priority files, we might not need more
|
||||
if filesOpened >= maxFilesToOpen {
|
||||
if cnf.DebugLSP {
|
||||
@@ -137,9 +137,9 @@ func (w *WorkspaceWatcher) AddRegistrations(ctx context.Context, id string, watc
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// For the remaining slots, walk the directory and open matching files
|
||||
|
||||
|
||||
err := filepath.WalkDir(w.workspacePath, func(path string, d os.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -199,10 +199,10 @@ func (w *WorkspaceWatcher) AddRegistrations(ctx context.Context, id string, watc
|
||||
func (w *WorkspaceWatcher) openHighPriorityFiles(ctx context.Context, serverName string) int {
|
||||
cnf := config.Get()
|
||||
filesOpened := 0
|
||||
|
||||
|
||||
// Define patterns for high-priority files based on server type
|
||||
var patterns []string
|
||||
|
||||
|
||||
switch serverName {
|
||||
case "typescript", "typescript-language-server", "tsserver", "vtsls":
|
||||
patterns = []string{
|
||||
@@ -256,7 +256,7 @@ func (w *WorkspaceWatcher) openHighPriorityFiles(ctx context.Context, serverName
|
||||
"**/.editorconfig",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// For each pattern, find and open matching files
|
||||
for _, pattern := range patterns {
|
||||
// Use doublestar.Glob to find files matching the pattern (supports ** patterns)
|
||||
@@ -267,17 +267,17 @@ func (w *WorkspaceWatcher) openHighPriorityFiles(ctx context.Context, serverName
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
for _, match := range matches {
|
||||
// Convert relative path to absolute
|
||||
fullPath := filepath.Join(w.workspacePath, match)
|
||||
|
||||
|
||||
// Skip directories and excluded files
|
||||
info, err := os.Stat(fullPath)
|
||||
if err != nil || info.IsDir() || shouldExcludeFile(fullPath) {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
// Open the file
|
||||
if err := w.client.OpenFile(ctx, fullPath); err != nil {
|
||||
if cnf.DebugLSP {
|
||||
@@ -289,17 +289,17 @@ func (w *WorkspaceWatcher) openHighPriorityFiles(ctx context.Context, serverName
|
||||
logging.Debug("Opened high-priority file", "path", fullPath)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Add a small delay to prevent overwhelming the server
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
|
||||
|
||||
// Limit the number of files opened per pattern
|
||||
if filesOpened >= 5 && (serverName != "java" && serverName != "jdtls") {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return filesOpened
|
||||
}
|
||||
|
||||
@@ -310,16 +310,16 @@ func (w *WorkspaceWatcher) WatchWorkspace(ctx context.Context, workspacePath str
|
||||
|
||||
// Store the watcher in the context for later use
|
||||
ctx = context.WithValue(ctx, "workspaceWatcher", w)
|
||||
|
||||
|
||||
// If the server name isn't already in the context, try to detect it
|
||||
if _, ok := ctx.Value("serverName").(string); !ok {
|
||||
serverName := getServerNameFromContext(ctx)
|
||||
ctx = context.WithValue(ctx, "serverName", serverName)
|
||||
}
|
||||
|
||||
|
||||
serverName := getServerNameFromContext(ctx)
|
||||
logging.Debug("Starting workspace watcher", "workspacePath", workspacePath, "serverName", serverName)
|
||||
|
||||
|
||||
// Register handler for file watcher registrations from the server
|
||||
lsp.RegisterFileWatchHandler(func(id string, watchers []protocol.FileSystemWatcher) {
|
||||
w.AddRegistrations(ctx, id, watchers)
|
||||
@@ -414,7 +414,11 @@ func (w *WorkspaceWatcher) WatchWorkspace(ctx context.Context, workspacePath str
|
||||
case event.Op&fsnotify.Create != 0:
|
||||
// Already handled earlier in the event loop
|
||||
// Just send the notification if needed
|
||||
info, _ := os.Stat(event.Name)
|
||||
info, err := os.Stat(event.Name)
|
||||
if err != nil {
|
||||
logging.Error("Error getting file info", "path", event.Name, "error", err)
|
||||
return
|
||||
}
|
||||
if !info.IsDir() && watchKind&protocol.WatchCreate != 0 {
|
||||
w.debounceHandleFileEvent(ctx, uri, protocol.FileChangeType(protocol.Created))
|
||||
}
|
||||
@@ -639,7 +643,9 @@ func (w *WorkspaceWatcher) debounceHandleFileEvent(ctx context.Context, uri stri
|
||||
func (w *WorkspaceWatcher) handleFileEvent(ctx context.Context, uri string, changeType protocol.FileChangeType) {
|
||||
// If the file is open and it's a change event, use didChange notification
|
||||
filePath := uri[7:] // Remove "file://" prefix
|
||||
if changeType == protocol.FileChangeType(protocol.Changed) && w.client.IsFileOpen(filePath) {
|
||||
if changeType == protocol.FileChangeType(protocol.Deleted) {
|
||||
w.client.ClearDiagnosticsForURI(protocol.DocumentUri(uri))
|
||||
} else if changeType == protocol.FileChangeType(protocol.Changed) && w.client.IsFileOpen(filePath) {
|
||||
err := w.client.NotifyChange(ctx, filePath)
|
||||
if err != nil {
|
||||
logging.Error("Error notifying change", "error", err)
|
||||
@@ -682,7 +688,7 @@ func getServerNameFromContext(ctx context.Context) string {
|
||||
if serverName, ok := ctx.Value("serverName").(string); ok && serverName != "" {
|
||||
return strings.ToLower(serverName)
|
||||
}
|
||||
|
||||
|
||||
// Otherwise, try to extract server name from the client command path
|
||||
if w, ok := ctx.Value("workspaceWatcher").(*WorkspaceWatcher); ok && w != nil && w.client != nil && w.client.Cmd != nil {
|
||||
path := strings.ToLower(w.client.Cmd.Path)
|
||||
@@ -865,7 +871,7 @@ func (w *WorkspaceWatcher) openMatchingFile(ctx context.Context, path string) {
|
||||
if watched, _ := w.isPathWatched(path); watched {
|
||||
// Get server name for specialized handling
|
||||
serverName := getServerNameFromContext(ctx)
|
||||
|
||||
|
||||
// Check if the file is a high-priority file that should be opened immediately
|
||||
// This helps with project initialization for certain language servers
|
||||
if isHighPriorityFile(path, serverName) {
|
||||
@@ -881,7 +887,7 @@ func (w *WorkspaceWatcher) openMatchingFile(ctx context.Context, path string) {
|
||||
// For non-high-priority files, we'll use different strategies based on server type
|
||||
if shouldPreloadFiles(serverName) {
|
||||
// For servers that benefit from preloading, open files but with limits
|
||||
|
||||
|
||||
// Check file size - for preloading we're more conservative
|
||||
if info.Size() > (1 * 1024 * 1024) { // 1MB limit for preloaded files
|
||||
if cnf.DebugLSP {
|
||||
@@ -889,13 +895,13 @@ func (w *WorkspaceWatcher) openMatchingFile(ctx context.Context, path string) {
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// Check file extension for common source files
|
||||
ext := strings.ToLower(filepath.Ext(path))
|
||||
|
||||
|
||||
// Only preload source files for the specific language
|
||||
shouldOpen := false
|
||||
|
||||
|
||||
switch serverName {
|
||||
case "typescript", "typescript-language-server", "tsserver", "vtsls":
|
||||
shouldOpen = ext == ".ts" || ext == ".js" || ext == ".tsx" || ext == ".jsx"
|
||||
@@ -913,7 +919,7 @@ func (w *WorkspaceWatcher) openMatchingFile(ctx context.Context, path string) {
|
||||
// For unknown servers, be conservative
|
||||
shouldOpen = false
|
||||
}
|
||||
|
||||
|
||||
if shouldOpen {
|
||||
// Don't need to check if it's already open - the client.OpenFile handles that
|
||||
if err := w.client.OpenFile(ctx, path); err != nil && cnf.DebugLSP {
|
||||
@@ -943,13 +949,13 @@ func isHighPriorityFile(path string, serverName string) bool {
|
||||
fileName == "main.js"
|
||||
case "gopls":
|
||||
// For Go, we want to open go.mod files immediately
|
||||
return fileName == "go.mod" ||
|
||||
return fileName == "go.mod" ||
|
||||
fileName == "go.sum" ||
|
||||
// Also open main.go files
|
||||
fileName == "main.go"
|
||||
case "rust-analyzer":
|
||||
// For Rust, we want to open Cargo.toml files immediately
|
||||
return fileName == "Cargo.toml" ||
|
||||
return fileName == "Cargo.toml" ||
|
||||
fileName == "Cargo.lock" ||
|
||||
// Also open lib.rs and main.rs
|
||||
fileName == "lib.rs" ||
|
||||
|
||||
8
internal/message/attachment.go
Normal file
8
internal/message/attachment.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package message
|
||||
|
||||
type Attachment struct {
|
||||
FilePath string
|
||||
FileName string
|
||||
MimeType string
|
||||
Content []byte
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/kujtimiihoxha/opencode/internal/llm/models"
|
||||
"github.com/opencode-ai/opencode/internal/llm/models"
|
||||
)
|
||||
|
||||
type MessageRole string
|
||||
@@ -66,13 +66,17 @@ func (iuc ImageURLContent) String() string {
|
||||
func (ImageURLContent) isPart() {}
|
||||
|
||||
type BinaryContent struct {
|
||||
Path string
|
||||
MIMEType string
|
||||
Data []byte
|
||||
}
|
||||
|
||||
func (bc BinaryContent) String() string {
|
||||
func (bc BinaryContent) String(provider models.ModelProvider) string {
|
||||
base64Encoded := base64.StdEncoding.EncodeToString(bc.Data)
|
||||
return "data:" + bc.MIMEType + ";base64," + base64Encoded
|
||||
if provider == models.ProviderOpenAI {
|
||||
return "data:" + bc.MIMEType + ";base64," + base64Encoded
|
||||
}
|
||||
return base64Encoded
|
||||
}
|
||||
|
||||
func (BinaryContent) isPart() {}
|
||||
@@ -110,7 +114,6 @@ type Message struct {
|
||||
SessionID string
|
||||
Parts []ContentPart
|
||||
Model models.ModelID
|
||||
|
||||
CreatedAt int64
|
||||
UpdatedAt int64
|
||||
}
|
||||
|
||||
@@ -8,9 +8,9 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/kujtimiihoxha/opencode/internal/db"
|
||||
"github.com/kujtimiihoxha/opencode/internal/llm/models"
|
||||
"github.com/kujtimiihoxha/opencode/internal/pubsub"
|
||||
"github.com/opencode-ai/opencode/internal/db"
|
||||
"github.com/opencode-ai/opencode/internal/llm/models"
|
||||
"github.com/opencode-ai/opencode/internal/pubsub"
|
||||
)
|
||||
|
||||
type CreateMessageParams struct {
|
||||
@@ -64,7 +64,6 @@ func (s *service) Create(ctx context.Context, sessionID string, params CreateMes
|
||||
if err != nil {
|
||||
return Message{}, err
|
||||
}
|
||||
|
||||
dbMessage, err := s.q.CreateMessage(ctx, db.CreateMessageParams{
|
||||
ID: uuid.New().String(),
|
||||
SessionID: sessionID,
|
||||
|
||||
@@ -5,11 +5,10 @@ import (
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/kujtimiihoxha/opencode/internal/config"
|
||||
"github.com/kujtimiihoxha/opencode/internal/pubsub"
|
||||
"github.com/opencode-ai/opencode/internal/config"
|
||||
"github.com/opencode-ai/opencode/internal/pubsub"
|
||||
)
|
||||
|
||||
var ErrorPermissionDenied = errors.New("permission denied")
|
||||
@@ -104,12 +103,8 @@ func (s *permissionService) Request(opts CreatePermissionRequest) bool {
|
||||
s.Publish(pubsub.CreatedEvent, permission)
|
||||
|
||||
// Wait for the response with a timeout
|
||||
select {
|
||||
case resp := <-respCh:
|
||||
return resp
|
||||
case <-time.After(10 * time.Minute):
|
||||
return false
|
||||
}
|
||||
resp := <-respCh
|
||||
return resp
|
||||
}
|
||||
|
||||
func (s *permissionService) AutoApproveSession(sessionID string) {
|
||||
|
||||
@@ -5,8 +5,8 @@ import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/kujtimiihoxha/opencode/internal/db"
|
||||
"github.com/kujtimiihoxha/opencode/internal/pubsub"
|
||||
"github.com/opencode-ai/opencode/internal/db"
|
||||
"github.com/opencode-ai/opencode/internal/pubsub"
|
||||
)
|
||||
|
||||
type Session struct {
|
||||
|
||||
@@ -6,14 +6,17 @@ import (
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/charmbracelet/x/ansi"
|
||||
"github.com/kujtimiihoxha/opencode/internal/config"
|
||||
"github.com/kujtimiihoxha/opencode/internal/session"
|
||||
"github.com/kujtimiihoxha/opencode/internal/tui/styles"
|
||||
"github.com/kujtimiihoxha/opencode/internal/version"
|
||||
"github.com/opencode-ai/opencode/internal/config"
|
||||
"github.com/opencode-ai/opencode/internal/message"
|
||||
"github.com/opencode-ai/opencode/internal/session"
|
||||
"github.com/opencode-ai/opencode/internal/tui/styles"
|
||||
"github.com/opencode-ai/opencode/internal/tui/theme"
|
||||
"github.com/opencode-ai/opencode/internal/version"
|
||||
)
|
||||
|
||||
type SendMsg struct {
|
||||
Text string
|
||||
Text string
|
||||
Attachments []message.Attachment
|
||||
}
|
||||
|
||||
type SessionSelectedMsg = session.Session
|
||||
@@ -22,12 +25,29 @@ type SessionClearedMsg struct{}
|
||||
|
||||
type EditorFocusMsg bool
|
||||
|
||||
func header(width int) string {
|
||||
return lipgloss.JoinVertical(
|
||||
lipgloss.Top,
|
||||
logo(width),
|
||||
repo(width),
|
||||
"",
|
||||
cwd(width),
|
||||
)
|
||||
}
|
||||
|
||||
func lspsConfigured(width int) string {
|
||||
cfg := config.Get()
|
||||
title := "LSP Configuration"
|
||||
title = ansi.Truncate(title, width, "…")
|
||||
|
||||
lsps := styles.BaseStyle.Width(width).Foreground(styles.PrimaryColor).Bold(true).Render(title)
|
||||
t := theme.CurrentTheme()
|
||||
baseStyle := styles.BaseStyle()
|
||||
|
||||
lsps := baseStyle.
|
||||
Width(width).
|
||||
Foreground(t.Primary()).
|
||||
Bold(true).
|
||||
Render(title)
|
||||
|
||||
// Get LSP names and sort them for consistent ordering
|
||||
var lspNames []string
|
||||
@@ -39,16 +59,19 @@ func lspsConfigured(width int) string {
|
||||
var lspViews []string
|
||||
for _, name := range lspNames {
|
||||
lsp := cfg.LSP[name]
|
||||
lspName := styles.BaseStyle.Foreground(styles.Forground).Render(
|
||||
fmt.Sprintf("• %s", name),
|
||||
)
|
||||
lspName := baseStyle.
|
||||
Foreground(t.Text()).
|
||||
Render(fmt.Sprintf("• %s", name))
|
||||
|
||||
cmd := lsp.Command
|
||||
cmd = ansi.Truncate(cmd, width-lipgloss.Width(lspName)-3, "…")
|
||||
lspPath := styles.BaseStyle.Foreground(styles.ForgroundDim).Render(
|
||||
fmt.Sprintf(" (%s)", cmd),
|
||||
)
|
||||
|
||||
lspPath := baseStyle.
|
||||
Foreground(t.TextMuted()).
|
||||
Render(fmt.Sprintf(" (%s)", cmd))
|
||||
|
||||
lspViews = append(lspViews,
|
||||
styles.BaseStyle.
|
||||
baseStyle.
|
||||
Width(width).
|
||||
Render(
|
||||
lipgloss.JoinHorizontal(
|
||||
@@ -59,7 +82,8 @@ func lspsConfigured(width int) string {
|
||||
),
|
||||
)
|
||||
}
|
||||
return styles.BaseStyle.
|
||||
|
||||
return baseStyle.
|
||||
Width(width).
|
||||
Render(
|
||||
lipgloss.JoinVertical(
|
||||
@@ -75,10 +99,14 @@ func lspsConfigured(width int) string {
|
||||
|
||||
func logo(width int) string {
|
||||
logo := fmt.Sprintf("%s %s", styles.OpenCodeIcon, "OpenCode")
|
||||
t := theme.CurrentTheme()
|
||||
baseStyle := styles.BaseStyle()
|
||||
|
||||
version := styles.BaseStyle.Foreground(styles.ForgroundDim).Render(version.Version)
|
||||
versionText := baseStyle.
|
||||
Foreground(t.TextMuted()).
|
||||
Render(version.Version)
|
||||
|
||||
return styles.BaseStyle.
|
||||
return baseStyle.
|
||||
Bold(true).
|
||||
Width(width).
|
||||
Render(
|
||||
@@ -86,34 +114,28 @@ func logo(width int) string {
|
||||
lipgloss.Left,
|
||||
logo,
|
||||
" ",
|
||||
version,
|
||||
versionText,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func repo(width int) string {
|
||||
repo := "https://github.com/kujtimiihoxha/opencode"
|
||||
return styles.BaseStyle.
|
||||
Foreground(styles.ForgroundDim).
|
||||
repo := "https://github.com/opencode-ai/opencode"
|
||||
t := theme.CurrentTheme()
|
||||
|
||||
return styles.BaseStyle().
|
||||
Foreground(t.TextMuted()).
|
||||
Width(width).
|
||||
Render(repo)
|
||||
}
|
||||
|
||||
func cwd(width int) string {
|
||||
cwd := fmt.Sprintf("cwd: %s", config.WorkingDirectory())
|
||||
return styles.BaseStyle.
|
||||
Foreground(styles.ForgroundDim).
|
||||
t := theme.CurrentTheme()
|
||||
|
||||
return styles.BaseStyle().
|
||||
Foreground(t.TextMuted()).
|
||||
Width(width).
|
||||
Render(cwd)
|
||||
}
|
||||
|
||||
func header(width int) string {
|
||||
header := lipgloss.JoinVertical(
|
||||
lipgloss.Top,
|
||||
logo(width),
|
||||
repo(width),
|
||||
"",
|
||||
cwd(width),
|
||||
)
|
||||
return header
|
||||
}
|
||||
|
||||
@@ -1,32 +1,40 @@
|
||||
package chat
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"slices"
|
||||
"unicode"
|
||||
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
"github.com/charmbracelet/bubbles/textarea"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/kujtimiihoxha/opencode/internal/app"
|
||||
"github.com/kujtimiihoxha/opencode/internal/session"
|
||||
"github.com/kujtimiihoxha/opencode/internal/tui/layout"
|
||||
"github.com/kujtimiihoxha/opencode/internal/tui/styles"
|
||||
"github.com/kujtimiihoxha/opencode/internal/tui/util"
|
||||
"github.com/opencode-ai/opencode/internal/app"
|
||||
"github.com/opencode-ai/opencode/internal/logging"
|
||||
"github.com/opencode-ai/opencode/internal/message"
|
||||
"github.com/opencode-ai/opencode/internal/session"
|
||||
"github.com/opencode-ai/opencode/internal/tui/components/dialog"
|
||||
"github.com/opencode-ai/opencode/internal/tui/layout"
|
||||
"github.com/opencode-ai/opencode/internal/tui/styles"
|
||||
"github.com/opencode-ai/opencode/internal/tui/theme"
|
||||
"github.com/opencode-ai/opencode/internal/tui/util"
|
||||
)
|
||||
|
||||
type editorCmp struct {
|
||||
app *app.App
|
||||
session session.Session
|
||||
textarea textarea.Model
|
||||
width int
|
||||
height int
|
||||
app *app.App
|
||||
session session.Session
|
||||
textarea textarea.Model
|
||||
attachments []message.Attachment
|
||||
deleteMode bool
|
||||
}
|
||||
|
||||
type FocusEditorMsg bool
|
||||
|
||||
type focusedEditorKeyMaps struct {
|
||||
type EditorKeyMaps struct {
|
||||
Send key.Binding
|
||||
OpenEditor key.Binding
|
||||
Blur key.Binding
|
||||
}
|
||||
|
||||
type bluredEditorKeyMaps struct {
|
||||
@@ -34,38 +42,43 @@ type bluredEditorKeyMaps struct {
|
||||
Focus key.Binding
|
||||
OpenEditor key.Binding
|
||||
}
|
||||
type DeleteAttachmentKeyMaps struct {
|
||||
AttachmentDeleteMode key.Binding
|
||||
Escape key.Binding
|
||||
DeleteAllAttachments key.Binding
|
||||
}
|
||||
|
||||
var focusedKeyMaps = focusedEditorKeyMaps{
|
||||
var editorMaps = EditorKeyMaps{
|
||||
Send: key.NewBinding(
|
||||
key.WithKeys("ctrl+s"),
|
||||
key.WithHelp("ctrl+s", "send message"),
|
||||
key.WithKeys("enter", "ctrl+s"),
|
||||
key.WithHelp("enter", "send message"),
|
||||
),
|
||||
Blur: key.NewBinding(
|
||||
OpenEditor: key.NewBinding(
|
||||
key.WithKeys("ctrl+e"),
|
||||
key.WithHelp("ctrl+e", "open editor"),
|
||||
),
|
||||
}
|
||||
|
||||
var DeleteKeyMaps = DeleteAttachmentKeyMaps{
|
||||
AttachmentDeleteMode: key.NewBinding(
|
||||
key.WithKeys("ctrl+r"),
|
||||
key.WithHelp("ctrl+r+{i}", "delete attachment at index i"),
|
||||
),
|
||||
Escape: key.NewBinding(
|
||||
key.WithKeys("esc"),
|
||||
key.WithHelp("esc", "focus messages"),
|
||||
key.WithHelp("esc", "cancel delete mode"),
|
||||
),
|
||||
OpenEditor: key.NewBinding(
|
||||
key.WithKeys("ctrl+e"),
|
||||
key.WithHelp("ctrl+e", "open editor"),
|
||||
DeleteAllAttachments: key.NewBinding(
|
||||
key.WithKeys("r"),
|
||||
key.WithHelp("ctrl+r+r", "delete all attchments"),
|
||||
),
|
||||
}
|
||||
|
||||
var bluredKeyMaps = bluredEditorKeyMaps{
|
||||
Send: key.NewBinding(
|
||||
key.WithKeys("ctrl+s", "enter"),
|
||||
key.WithHelp("ctrl+s/enter", "send message"),
|
||||
),
|
||||
Focus: key.NewBinding(
|
||||
key.WithKeys("i"),
|
||||
key.WithHelp("i", "focus editor"),
|
||||
),
|
||||
OpenEditor: key.NewBinding(
|
||||
key.WithKeys("ctrl+e"),
|
||||
key.WithHelp("ctrl+e", "open editor"),
|
||||
),
|
||||
}
|
||||
const (
|
||||
maxAttachments = 5
|
||||
)
|
||||
|
||||
func openEditor() tea.Cmd {
|
||||
func (m *editorCmp) openEditor() tea.Cmd {
|
||||
editor := os.Getenv("EDITOR")
|
||||
if editor == "" {
|
||||
editor = "nvim"
|
||||
@@ -88,9 +101,15 @@ func openEditor() tea.Cmd {
|
||||
if err != nil {
|
||||
return util.ReportError(err)
|
||||
}
|
||||
if len(content) == 0 {
|
||||
return util.ReportWarn("Message is empty")
|
||||
}
|
||||
os.Remove(tmpfile.Name())
|
||||
attachments := m.attachments
|
||||
m.attachments = nil
|
||||
return SendMsg{
|
||||
Text: string(content),
|
||||
Text: string(content),
|
||||
Attachments: attachments,
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -106,13 +125,16 @@ func (m *editorCmp) send() tea.Cmd {
|
||||
|
||||
value := m.textarea.Value()
|
||||
m.textarea.Reset()
|
||||
m.textarea.Blur()
|
||||
attachments := m.attachments
|
||||
|
||||
m.attachments = nil
|
||||
if value == "" {
|
||||
return nil
|
||||
}
|
||||
return tea.Batch(
|
||||
util.CmdHandler(SendMsg{
|
||||
Text: value,
|
||||
Text: value,
|
||||
Attachments: attachments,
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -120,52 +142,100 @@ func (m *editorCmp) send() tea.Cmd {
|
||||
func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
var cmd tea.Cmd
|
||||
switch msg := msg.(type) {
|
||||
case dialog.ThemeChangedMsg:
|
||||
m.textarea = CreateTextArea(&m.textarea)
|
||||
return m, nil
|
||||
case SessionSelectedMsg:
|
||||
if msg.ID != m.session.ID {
|
||||
m.session = msg
|
||||
}
|
||||
return m, nil
|
||||
case FocusEditorMsg:
|
||||
if msg {
|
||||
m.textarea.Focus()
|
||||
return m, tea.Batch(textarea.Blink, util.CmdHandler(EditorFocusMsg(true)))
|
||||
case dialog.AttachmentAddedMsg:
|
||||
if len(m.attachments) >= maxAttachments {
|
||||
logging.ErrorPersist(fmt.Sprintf("cannot add more than %d images", maxAttachments))
|
||||
return m, cmd
|
||||
}
|
||||
m.attachments = append(m.attachments, msg.Attachment)
|
||||
case tea.KeyMsg:
|
||||
if key.Matches(msg, focusedKeyMaps.OpenEditor) {
|
||||
if key.Matches(msg, DeleteKeyMaps.AttachmentDeleteMode) {
|
||||
m.deleteMode = true
|
||||
return m, nil
|
||||
}
|
||||
if key.Matches(msg, DeleteKeyMaps.DeleteAllAttachments) && m.deleteMode {
|
||||
m.deleteMode = false
|
||||
m.attachments = nil
|
||||
return m, nil
|
||||
}
|
||||
if m.deleteMode && len(msg.Runes) > 0 && unicode.IsDigit(msg.Runes[0]) {
|
||||
num := int(msg.Runes[0] - '0')
|
||||
m.deleteMode = false
|
||||
if num < 10 && len(m.attachments) > num {
|
||||
if num == 0 {
|
||||
m.attachments = m.attachments[num+1:]
|
||||
} else {
|
||||
m.attachments = slices.Delete(m.attachments, num, num+1)
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
}
|
||||
if key.Matches(msg, messageKeys.PageUp) || key.Matches(msg, messageKeys.PageDown) ||
|
||||
key.Matches(msg, messageKeys.HalfPageUp) || key.Matches(msg, messageKeys.HalfPageDown) {
|
||||
return m, nil
|
||||
}
|
||||
if key.Matches(msg, editorMaps.OpenEditor) {
|
||||
if m.app.CoderAgent.IsSessionBusy(m.session.ID) {
|
||||
return m, util.ReportWarn("Agent is working, please wait...")
|
||||
}
|
||||
return m, openEditor()
|
||||
return m, m.openEditor()
|
||||
}
|
||||
// if the key does not match any binding, return
|
||||
if m.textarea.Focused() && key.Matches(msg, focusedKeyMaps.Send) {
|
||||
return m, m.send()
|
||||
if key.Matches(msg, DeleteKeyMaps.Escape) {
|
||||
m.deleteMode = false
|
||||
return m, nil
|
||||
}
|
||||
if !m.textarea.Focused() && key.Matches(msg, bluredKeyMaps.Send) {
|
||||
return m, m.send()
|
||||
}
|
||||
if m.textarea.Focused() && key.Matches(msg, focusedKeyMaps.Blur) {
|
||||
m.textarea.Blur()
|
||||
return m, util.CmdHandler(EditorFocusMsg(false))
|
||||
}
|
||||
if !m.textarea.Focused() && key.Matches(msg, bluredKeyMaps.Focus) {
|
||||
m.textarea.Focus()
|
||||
return m, tea.Batch(textarea.Blink, util.CmdHandler(EditorFocusMsg(true)))
|
||||
// Handle Enter key
|
||||
if m.textarea.Focused() && key.Matches(msg, editorMaps.Send) {
|
||||
value := m.textarea.Value()
|
||||
if len(value) > 0 && value[len(value)-1] == '\\' {
|
||||
// If the last character is a backslash, remove it and add a newline
|
||||
m.textarea.SetValue(value[:len(value)-1] + "\n")
|
||||
return m, nil
|
||||
} else {
|
||||
// Otherwise, send the message
|
||||
return m, m.send()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
m.textarea, cmd = m.textarea.Update(msg)
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
func (m *editorCmp) View() string {
|
||||
style := lipgloss.NewStyle().Padding(0, 0, 0, 1).Bold(true)
|
||||
t := theme.CurrentTheme()
|
||||
|
||||
return lipgloss.JoinHorizontal(lipgloss.Top, style.Render(">"), m.textarea.View())
|
||||
// Style the prompt with theme colors
|
||||
style := lipgloss.NewStyle().
|
||||
Padding(0, 0, 0, 1).
|
||||
Bold(true).
|
||||
Foreground(t.Primary())
|
||||
|
||||
if len(m.attachments) == 0 {
|
||||
return lipgloss.JoinHorizontal(lipgloss.Top, style.Render(">"), m.textarea.View())
|
||||
}
|
||||
m.textarea.SetHeight(m.height - 1)
|
||||
return lipgloss.JoinVertical(lipgloss.Top,
|
||||
m.attachmentsContent(),
|
||||
lipgloss.JoinHorizontal(lipgloss.Top, style.Render(">"),
|
||||
m.textarea.View()),
|
||||
)
|
||||
}
|
||||
|
||||
func (m *editorCmp) SetSize(width, height int) tea.Cmd {
|
||||
m.width = width
|
||||
m.height = height
|
||||
m.textarea.SetWidth(width - 3) // account for the prompt and padding right
|
||||
m.textarea.SetHeight(height)
|
||||
m.textarea.SetWidth(width)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -173,35 +243,70 @@ func (m *editorCmp) GetSize() (int, int) {
|
||||
return m.textarea.Width(), m.textarea.Height()
|
||||
}
|
||||
|
||||
func (m *editorCmp) attachmentsContent() string {
|
||||
var styledAttachments []string
|
||||
t := theme.CurrentTheme()
|
||||
attachmentStyles := styles.BaseStyle().
|
||||
MarginLeft(1).
|
||||
Background(t.TextMuted()).
|
||||
Foreground(t.Text())
|
||||
for i, attachment := range m.attachments {
|
||||
var filename string
|
||||
if len(attachment.FileName) > 10 {
|
||||
filename = fmt.Sprintf(" %s %s...", styles.DocumentIcon, attachment.FileName[0:7])
|
||||
} else {
|
||||
filename = fmt.Sprintf(" %s %s", styles.DocumentIcon, attachment.FileName)
|
||||
}
|
||||
if m.deleteMode {
|
||||
filename = fmt.Sprintf("%d%s", i, filename)
|
||||
}
|
||||
styledAttachments = append(styledAttachments, attachmentStyles.Render(filename))
|
||||
}
|
||||
content := lipgloss.JoinHorizontal(lipgloss.Left, styledAttachments...)
|
||||
return content
|
||||
}
|
||||
|
||||
func (m *editorCmp) BindingKeys() []key.Binding {
|
||||
bindings := []key.Binding{}
|
||||
if m.textarea.Focused() {
|
||||
bindings = append(bindings, layout.KeyMapToSlice(focusedKeyMaps)...)
|
||||
} else {
|
||||
bindings = append(bindings, layout.KeyMapToSlice(bluredKeyMaps)...)
|
||||
}
|
||||
|
||||
bindings = append(bindings, layout.KeyMapToSlice(m.textarea.KeyMap)...)
|
||||
bindings = append(bindings, layout.KeyMapToSlice(editorMaps)...)
|
||||
bindings = append(bindings, layout.KeyMapToSlice(DeleteKeyMaps)...)
|
||||
return bindings
|
||||
}
|
||||
|
||||
func NewEditorCmp(app *app.App) tea.Model {
|
||||
ti := textarea.New()
|
||||
ti.Prompt = " "
|
||||
ti.ShowLineNumbers = false
|
||||
ti.BlurredStyle.Base = ti.BlurredStyle.Base.Background(styles.Background)
|
||||
ti.BlurredStyle.CursorLine = ti.BlurredStyle.CursorLine.Background(styles.Background)
|
||||
ti.BlurredStyle.Placeholder = ti.BlurredStyle.Placeholder.Background(styles.Background)
|
||||
ti.BlurredStyle.Text = ti.BlurredStyle.Text.Background(styles.Background)
|
||||
func CreateTextArea(existing *textarea.Model) textarea.Model {
|
||||
t := theme.CurrentTheme()
|
||||
bgColor := t.Background()
|
||||
textColor := t.Text()
|
||||
textMutedColor := t.TextMuted()
|
||||
|
||||
ti.FocusedStyle.Base = ti.FocusedStyle.Base.Background(styles.Background)
|
||||
ti.FocusedStyle.CursorLine = ti.FocusedStyle.CursorLine.Background(styles.Background)
|
||||
ti.FocusedStyle.Placeholder = ti.FocusedStyle.Placeholder.Background(styles.Background)
|
||||
ti.FocusedStyle.Text = ti.BlurredStyle.Text.Background(styles.Background)
|
||||
ti.CharLimit = -1
|
||||
ti.Focus()
|
||||
ta := textarea.New()
|
||||
ta.BlurredStyle.Base = styles.BaseStyle().Background(bgColor).Foreground(textColor)
|
||||
ta.BlurredStyle.CursorLine = styles.BaseStyle().Background(bgColor)
|
||||
ta.BlurredStyle.Placeholder = styles.BaseStyle().Background(bgColor).Foreground(textMutedColor)
|
||||
ta.BlurredStyle.Text = styles.BaseStyle().Background(bgColor).Foreground(textColor)
|
||||
ta.FocusedStyle.Base = styles.BaseStyle().Background(bgColor).Foreground(textColor)
|
||||
ta.FocusedStyle.CursorLine = styles.BaseStyle().Background(bgColor)
|
||||
ta.FocusedStyle.Placeholder = styles.BaseStyle().Background(bgColor).Foreground(textMutedColor)
|
||||
ta.FocusedStyle.Text = styles.BaseStyle().Background(bgColor).Foreground(textColor)
|
||||
|
||||
ta.Prompt = " "
|
||||
ta.ShowLineNumbers = false
|
||||
ta.CharLimit = -1
|
||||
|
||||
if existing != nil {
|
||||
ta.SetValue(existing.Value())
|
||||
ta.SetWidth(existing.Width())
|
||||
ta.SetHeight(existing.Height())
|
||||
}
|
||||
|
||||
ta.Focus()
|
||||
return ta
|
||||
}
|
||||
|
||||
func NewEditorCmp(app *app.App) tea.Model {
|
||||
ta := CreateTextArea(nil)
|
||||
return &editorCmp{
|
||||
app: app,
|
||||
textarea: ti,
|
||||
textarea: ta,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,13 +10,14 @@ import (
|
||||
"github.com/charmbracelet/bubbles/viewport"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/kujtimiihoxha/opencode/internal/app"
|
||||
"github.com/kujtimiihoxha/opencode/internal/message"
|
||||
"github.com/kujtimiihoxha/opencode/internal/pubsub"
|
||||
"github.com/kujtimiihoxha/opencode/internal/session"
|
||||
"github.com/kujtimiihoxha/opencode/internal/tui/layout"
|
||||
"github.com/kujtimiihoxha/opencode/internal/tui/styles"
|
||||
"github.com/kujtimiihoxha/opencode/internal/tui/util"
|
||||
"github.com/opencode-ai/opencode/internal/app"
|
||||
"github.com/opencode-ai/opencode/internal/message"
|
||||
"github.com/opencode-ai/opencode/internal/pubsub"
|
||||
"github.com/opencode-ai/opencode/internal/session"
|
||||
"github.com/opencode-ai/opencode/internal/tui/components/dialog"
|
||||
"github.com/opencode-ai/opencode/internal/tui/styles"
|
||||
"github.com/opencode-ai/opencode/internal/tui/theme"
|
||||
"github.com/opencode-ai/opencode/internal/tui/util"
|
||||
)
|
||||
|
||||
type cacheItem struct {
|
||||
@@ -26,7 +27,6 @@ type cacheItem struct {
|
||||
type messagesCmp struct {
|
||||
app *app.App
|
||||
width, height int
|
||||
writingMode bool
|
||||
viewport viewport.Model
|
||||
session session.Session
|
||||
messages []message.Message
|
||||
@@ -35,9 +35,36 @@ type messagesCmp struct {
|
||||
cachedContent map[string]cacheItem
|
||||
spinner spinner.Model
|
||||
rendering bool
|
||||
attachments viewport.Model
|
||||
}
|
||||
type renderFinishedMsg struct{}
|
||||
|
||||
type MessageKeys struct {
|
||||
PageDown key.Binding
|
||||
PageUp key.Binding
|
||||
HalfPageUp key.Binding
|
||||
HalfPageDown key.Binding
|
||||
}
|
||||
|
||||
var messageKeys = MessageKeys{
|
||||
PageDown: key.NewBinding(
|
||||
key.WithKeys("pgdown"),
|
||||
key.WithHelp("f/pgdn", "page down"),
|
||||
),
|
||||
PageUp: key.NewBinding(
|
||||
key.WithKeys("pgup"),
|
||||
key.WithHelp("b/pgup", "page up"),
|
||||
),
|
||||
HalfPageUp: key.NewBinding(
|
||||
key.WithKeys("ctrl+u"),
|
||||
key.WithHelp("ctrl+u", "½ page up"),
|
||||
),
|
||||
HalfPageDown: key.NewBinding(
|
||||
key.WithKeys("ctrl+d", "ctrl+d"),
|
||||
key.WithHelp("ctrl+d", "½ page down"),
|
||||
),
|
||||
}
|
||||
|
||||
func (m *messagesCmp) Init() tea.Cmd {
|
||||
return tea.Batch(m.viewport.Init(), m.spinner.Tick)
|
||||
}
|
||||
@@ -45,8 +72,9 @@ func (m *messagesCmp) Init() tea.Cmd {
|
||||
func (m *messagesCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
var cmds []tea.Cmd
|
||||
switch msg := msg.(type) {
|
||||
case EditorFocusMsg:
|
||||
m.writingMode = bool(msg)
|
||||
case dialog.ThemeChangedMsg:
|
||||
m.rerender()
|
||||
return m, nil
|
||||
case SessionSelectedMsg:
|
||||
if msg.ID != m.session.ID {
|
||||
cmd := m.SetSession(msg)
|
||||
@@ -60,13 +88,17 @@ func (m *messagesCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.rendering = false
|
||||
return m, nil
|
||||
|
||||
case tea.KeyMsg:
|
||||
if key.Matches(msg, messageKeys.PageUp) || key.Matches(msg, messageKeys.PageDown) ||
|
||||
key.Matches(msg, messageKeys.HalfPageUp) || key.Matches(msg, messageKeys.HalfPageDown) {
|
||||
u, cmd := m.viewport.Update(msg)
|
||||
m.viewport = u
|
||||
cmds = append(cmds, cmd)
|
||||
}
|
||||
|
||||
case renderFinishedMsg:
|
||||
m.rendering = false
|
||||
m.viewport.GotoBottom()
|
||||
case tea.KeyMsg:
|
||||
if m.writingMode {
|
||||
return m, nil
|
||||
}
|
||||
case pubsub.Event[message.Message]:
|
||||
needsRerender := false
|
||||
if msg.Type == pubsub.CreatedEvent {
|
||||
@@ -122,10 +154,6 @@ func (m *messagesCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
}
|
||||
|
||||
u, cmd := m.viewport.Update(msg)
|
||||
m.viewport = u
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
spinner, cmd := m.spinner.Update(msg)
|
||||
m.spinner = spinner
|
||||
cmds = append(cmds, cmd)
|
||||
@@ -151,6 +179,7 @@ func formatTimeDifference(unixTime1, unixTime2 int64) string {
|
||||
func (m *messagesCmp) renderView() {
|
||||
m.uiMessages = make([]uiMessage, 0)
|
||||
pos := 0
|
||||
baseStyle := styles.BaseStyle()
|
||||
|
||||
if m.width == 0 {
|
||||
return
|
||||
@@ -201,16 +230,17 @@ func (m *messagesCmp) renderView() {
|
||||
|
||||
messages := make([]string, 0)
|
||||
for _, v := range m.uiMessages {
|
||||
messages = append(messages, v.content,
|
||||
styles.BaseStyle.
|
||||
messages = append(messages, lipgloss.JoinVertical(lipgloss.Left, v.content),
|
||||
baseStyle.
|
||||
Width(m.width).
|
||||
Render(
|
||||
"",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
m.viewport.SetContent(
|
||||
styles.BaseStyle.
|
||||
baseStyle.
|
||||
Width(m.width).
|
||||
Render(
|
||||
lipgloss.JoinVertical(
|
||||
@@ -222,8 +252,10 @@ func (m *messagesCmp) renderView() {
|
||||
}
|
||||
|
||||
func (m *messagesCmp) View() string {
|
||||
baseStyle := styles.BaseStyle()
|
||||
|
||||
if m.rendering {
|
||||
return styles.BaseStyle.
|
||||
return baseStyle.
|
||||
Width(m.width).
|
||||
Render(
|
||||
lipgloss.JoinVertical(
|
||||
@@ -235,14 +267,14 @@ func (m *messagesCmp) View() string {
|
||||
)
|
||||
}
|
||||
if len(m.messages) == 0 {
|
||||
content := styles.BaseStyle.
|
||||
content := baseStyle.
|
||||
Width(m.width).
|
||||
Height(m.height - 1).
|
||||
Render(
|
||||
m.initialScreen(),
|
||||
)
|
||||
|
||||
return styles.BaseStyle.
|
||||
return baseStyle.
|
||||
Width(m.width).
|
||||
Render(
|
||||
lipgloss.JoinVertical(
|
||||
@@ -254,7 +286,7 @@ func (m *messagesCmp) View() string {
|
||||
)
|
||||
}
|
||||
|
||||
return styles.BaseStyle.
|
||||
return baseStyle.
|
||||
Width(m.width).
|
||||
Render(
|
||||
lipgloss.JoinVertical(
|
||||
@@ -305,6 +337,9 @@ func hasUnfinishedToolCalls(messages []message.Message) bool {
|
||||
func (m *messagesCmp) working() string {
|
||||
text := ""
|
||||
if m.IsAgentWorking() && len(m.messages) > 0 {
|
||||
t := theme.CurrentTheme()
|
||||
baseStyle := styles.BaseStyle()
|
||||
|
||||
task := "Thinking..."
|
||||
lastMessage := m.messages[len(m.messages)-1]
|
||||
if hasToolsWithoutResponse(m.messages) {
|
||||
@@ -315,40 +350,49 @@ func (m *messagesCmp) working() string {
|
||||
task = "Generating..."
|
||||
}
|
||||
if task != "" {
|
||||
text += styles.BaseStyle.Width(m.width).Foreground(styles.PrimaryColor).Bold(true).Render(
|
||||
fmt.Sprintf("%s %s ", m.spinner.View(), task),
|
||||
)
|
||||
text += baseStyle.
|
||||
Width(m.width).
|
||||
Foreground(t.Primary()).
|
||||
Bold(true).
|
||||
Render(fmt.Sprintf("%s %s ", m.spinner.View(), task))
|
||||
}
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
func (m *messagesCmp) help() string {
|
||||
t := theme.CurrentTheme()
|
||||
baseStyle := styles.BaseStyle()
|
||||
|
||||
text := ""
|
||||
|
||||
if m.writingMode {
|
||||
if m.app.CoderAgent.IsBusy() {
|
||||
text += lipgloss.JoinHorizontal(
|
||||
lipgloss.Left,
|
||||
styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render("press "),
|
||||
styles.BaseStyle.Foreground(styles.Forground).Bold(true).Render("esc"),
|
||||
styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render(" to exit writing mode"),
|
||||
baseStyle.Foreground(t.TextMuted()).Bold(true).Render("press "),
|
||||
baseStyle.Foreground(t.Text()).Bold(true).Render("esc"),
|
||||
baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" to exit cancel"),
|
||||
)
|
||||
} else {
|
||||
text += lipgloss.JoinHorizontal(
|
||||
lipgloss.Left,
|
||||
styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render("press "),
|
||||
styles.BaseStyle.Foreground(styles.Forground).Bold(true).Render("i"),
|
||||
styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render(" to start writing"),
|
||||
baseStyle.Foreground(t.TextMuted()).Bold(true).Render("press "),
|
||||
baseStyle.Foreground(t.Text()).Bold(true).Render("enter"),
|
||||
baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" to send the message,"),
|
||||
baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" write"),
|
||||
baseStyle.Foreground(t.Text()).Bold(true).Render(" \\"),
|
||||
baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" and enter to add a new line"),
|
||||
)
|
||||
}
|
||||
|
||||
return styles.BaseStyle.
|
||||
return baseStyle.
|
||||
Width(m.width).
|
||||
Render(text)
|
||||
}
|
||||
|
||||
func (m *messagesCmp) initialScreen() string {
|
||||
return styles.BaseStyle.Width(m.width).Render(
|
||||
baseStyle := styles.BaseStyle()
|
||||
|
||||
return baseStyle.Width(m.width).Render(
|
||||
lipgloss.JoinVertical(
|
||||
lipgloss.Top,
|
||||
header(m.width),
|
||||
@@ -358,6 +402,13 @@ func (m *messagesCmp) initialScreen() string {
|
||||
)
|
||||
}
|
||||
|
||||
func (m *messagesCmp) rerender() {
|
||||
for _, msg := range m.messages {
|
||||
delete(m.cachedContent, msg.ID)
|
||||
}
|
||||
m.renderView()
|
||||
}
|
||||
|
||||
func (m *messagesCmp) SetSize(width, height int) tea.Cmd {
|
||||
if m.width == width && m.height == height {
|
||||
return nil
|
||||
@@ -366,11 +417,9 @@ func (m *messagesCmp) SetSize(width, height int) tea.Cmd {
|
||||
m.height = height
|
||||
m.viewport.Width = width
|
||||
m.viewport.Height = height - 2
|
||||
for _, msg := range m.messages {
|
||||
delete(m.cachedContent, msg.ID)
|
||||
}
|
||||
m.uiMessages = make([]uiMessage, 0)
|
||||
m.renderView()
|
||||
m.attachments.Width = width + 40
|
||||
m.attachments.Height = 3
|
||||
m.rerender()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -388,7 +437,9 @@ func (m *messagesCmp) SetSession(session session.Session) tea.Cmd {
|
||||
return util.ReportError(err)
|
||||
}
|
||||
m.messages = messages
|
||||
m.currentMsgID = m.messages[len(m.messages)-1].ID
|
||||
if len(m.messages) > 0 {
|
||||
m.currentMsgID = m.messages[len(m.messages)-1].ID
|
||||
}
|
||||
delete(m.cachedContent, m.currentMsgID)
|
||||
m.rendering = true
|
||||
return func() tea.Msg {
|
||||
@@ -398,18 +449,28 @@ func (m *messagesCmp) SetSession(session session.Session) tea.Cmd {
|
||||
}
|
||||
|
||||
func (m *messagesCmp) BindingKeys() []key.Binding {
|
||||
bindings := layout.KeyMapToSlice(m.viewport.KeyMap)
|
||||
return bindings
|
||||
return []key.Binding{
|
||||
m.viewport.KeyMap.PageDown,
|
||||
m.viewport.KeyMap.PageUp,
|
||||
m.viewport.KeyMap.HalfPageUp,
|
||||
m.viewport.KeyMap.HalfPageDown,
|
||||
}
|
||||
}
|
||||
|
||||
func NewMessagesCmp(app *app.App) tea.Model {
|
||||
s := spinner.New()
|
||||
s.Spinner = spinner.Pulse
|
||||
vp := viewport.New(0, 0)
|
||||
attachmets := viewport.New(0, 0)
|
||||
vp.KeyMap.PageUp = messageKeys.PageUp
|
||||
vp.KeyMap.PageDown = messageKeys.PageDown
|
||||
vp.KeyMap.HalfPageUp = messageKeys.HalfPageUp
|
||||
vp.KeyMap.HalfPageDown = messageKeys.HalfPageDown
|
||||
return &messagesCmp{
|
||||
app: app,
|
||||
writingMode: true,
|
||||
cachedContent: make(map[string]cacheItem),
|
||||
viewport: viewport.New(0, 0),
|
||||
viewport: vp,
|
||||
spinner: s,
|
||||
attachments: attachmets,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,19 +6,18 @@ import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/glamour"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/charmbracelet/x/ansi"
|
||||
"github.com/kujtimiihoxha/opencode/internal/config"
|
||||
"github.com/kujtimiihoxha/opencode/internal/diff"
|
||||
"github.com/kujtimiihoxha/opencode/internal/llm/agent"
|
||||
"github.com/kujtimiihoxha/opencode/internal/llm/models"
|
||||
"github.com/kujtimiihoxha/opencode/internal/llm/tools"
|
||||
"github.com/kujtimiihoxha/opencode/internal/message"
|
||||
"github.com/kujtimiihoxha/opencode/internal/tui/styles"
|
||||
"github.com/opencode-ai/opencode/internal/config"
|
||||
"github.com/opencode-ai/opencode/internal/diff"
|
||||
"github.com/opencode-ai/opencode/internal/llm/agent"
|
||||
"github.com/opencode-ai/opencode/internal/llm/models"
|
||||
"github.com/opencode-ai/opencode/internal/llm/tools"
|
||||
"github.com/opencode-ai/opencode/internal/message"
|
||||
"github.com/opencode-ai/opencode/internal/tui/styles"
|
||||
"github.com/opencode-ai/opencode/internal/tui/theme"
|
||||
)
|
||||
|
||||
type uiMessageType int
|
||||
@@ -28,11 +27,9 @@ const (
|
||||
assistantMessageType
|
||||
toolMessageType
|
||||
|
||||
maxResultHeight = 15
|
||||
maxResultHeight = 10
|
||||
)
|
||||
|
||||
var diffStyle = diff.NewStyleConfig(diff.WithShowHeader(false), diff.WithShowHunkHeader(false))
|
||||
|
||||
type uiMessage struct {
|
||||
ID string
|
||||
messageType uiMessageType
|
||||
@@ -41,46 +38,37 @@ type uiMessage struct {
|
||||
content string
|
||||
}
|
||||
|
||||
type renderCache struct {
|
||||
mutex sync.Mutex
|
||||
cache map[string][]uiMessage
|
||||
}
|
||||
|
||||
func toMarkdown(content string, focused bool, width int) string {
|
||||
r, _ := glamour.NewTermRenderer(
|
||||
glamour.WithStyles(styles.MarkdownTheme(false)),
|
||||
glamour.WithWordWrap(width),
|
||||
)
|
||||
if focused {
|
||||
r, _ = glamour.NewTermRenderer(
|
||||
glamour.WithStyles(styles.MarkdownTheme(true)),
|
||||
glamour.WithWordWrap(width),
|
||||
)
|
||||
}
|
||||
r := styles.GetMarkdownRenderer(width)
|
||||
rendered, _ := r.Render(content)
|
||||
return rendered
|
||||
}
|
||||
|
||||
func renderMessage(msg string, isUser bool, isFocused bool, width int, info ...string) string {
|
||||
style := styles.BaseStyle.
|
||||
t := theme.CurrentTheme()
|
||||
|
||||
style := styles.BaseStyle().
|
||||
Width(width - 1).
|
||||
BorderLeft(true).
|
||||
Foreground(styles.ForgroundDim).
|
||||
BorderForeground(styles.PrimaryColor).
|
||||
Foreground(t.TextMuted()).
|
||||
BorderForeground(t.Primary()).
|
||||
BorderStyle(lipgloss.ThickBorder())
|
||||
|
||||
if isUser {
|
||||
style = style.
|
||||
BorderForeground(styles.Blue)
|
||||
}
|
||||
parts := []string{
|
||||
styles.ForceReplaceBackgroundWithLipgloss(toMarkdown(msg, isFocused, width), styles.Background),
|
||||
style = style.BorderForeground(t.Secondary())
|
||||
}
|
||||
|
||||
// remove newline at the end
|
||||
// Apply markdown formatting and handle background color
|
||||
parts := []string{
|
||||
styles.ForceReplaceBackgroundWithLipgloss(toMarkdown(msg, isFocused, width), t.Background()),
|
||||
}
|
||||
|
||||
// Remove newline at the end
|
||||
parts[0] = strings.TrimSuffix(parts[0], "\n")
|
||||
if len(info) > 0 {
|
||||
parts = append(parts, info...)
|
||||
}
|
||||
|
||||
rendered := style.Render(
|
||||
lipgloss.JoinVertical(
|
||||
lipgloss.Left,
|
||||
@@ -92,7 +80,29 @@ func renderMessage(msg string, isUser bool, isFocused bool, width int, info ...s
|
||||
}
|
||||
|
||||
func renderUserMessage(msg message.Message, isFocused bool, width int, position int) uiMessage {
|
||||
content := renderMessage(msg.Content().String(), true, isFocused, width)
|
||||
var styledAttachments []string
|
||||
t := theme.CurrentTheme()
|
||||
attachmentStyles := styles.BaseStyle().
|
||||
MarginLeft(1).
|
||||
Background(t.TextMuted()).
|
||||
Foreground(t.Text())
|
||||
for _, attachment := range msg.BinaryContent() {
|
||||
file := filepath.Base(attachment.Path)
|
||||
var filename string
|
||||
if len(file) > 10 {
|
||||
filename = fmt.Sprintf(" %s %s...", styles.DocumentIcon, file[0:7])
|
||||
} else {
|
||||
filename = fmt.Sprintf(" %s %s", styles.DocumentIcon, file)
|
||||
}
|
||||
styledAttachments = append(styledAttachments, attachmentStyles.Render(filename))
|
||||
}
|
||||
content := ""
|
||||
if len(styledAttachments) > 0 {
|
||||
attachmentContent := styles.BaseStyle().Width(width).Render(lipgloss.JoinHorizontal(lipgloss.Left, styledAttachments...))
|
||||
content = renderMessage(msg.Content().String(), true, isFocused, width, attachmentContent)
|
||||
} else {
|
||||
content = renderMessage(msg.Content().String(), true, isFocused, width)
|
||||
}
|
||||
userMsg := uiMessage{
|
||||
ID: msg.ID,
|
||||
messageType: userMessageType,
|
||||
@@ -121,26 +131,37 @@ func renderAssistantMessage(
|
||||
finishData := msg.FinishPart()
|
||||
info := []string{}
|
||||
|
||||
t := theme.CurrentTheme()
|
||||
baseStyle := styles.BaseStyle()
|
||||
|
||||
// Add finish info if available
|
||||
if finished {
|
||||
switch finishData.Reason {
|
||||
case message.FinishReasonEndTurn:
|
||||
took := formatTimeDifference(msg.CreatedAt, finishData.Time)
|
||||
info = append(info, styles.BaseStyle.Width(width-1).Foreground(styles.ForgroundDim).Render(
|
||||
fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, took),
|
||||
))
|
||||
took := formatTimestampDiff(msg.CreatedAt, finishData.Time)
|
||||
info = append(info, baseStyle.
|
||||
Width(width-1).
|
||||
Foreground(t.TextMuted()).
|
||||
Render(fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, took)),
|
||||
)
|
||||
case message.FinishReasonCanceled:
|
||||
info = append(info, styles.BaseStyle.Width(width-1).Foreground(styles.ForgroundDim).Render(
|
||||
fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, "canceled"),
|
||||
))
|
||||
info = append(info, baseStyle.
|
||||
Width(width-1).
|
||||
Foreground(t.TextMuted()).
|
||||
Render(fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, "canceled")),
|
||||
)
|
||||
case message.FinishReasonError:
|
||||
info = append(info, styles.BaseStyle.Width(width-1).Foreground(styles.ForgroundDim).Render(
|
||||
fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, "error"),
|
||||
))
|
||||
info = append(info, baseStyle.
|
||||
Width(width-1).
|
||||
Foreground(t.TextMuted()).
|
||||
Render(fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, "error")),
|
||||
)
|
||||
case message.FinishReasonPermissionDenied:
|
||||
info = append(info, styles.BaseStyle.Width(width-1).Foreground(styles.ForgroundDim).Render(
|
||||
fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, "permission denied"),
|
||||
))
|
||||
info = append(info, baseStyle.
|
||||
Width(width-1).
|
||||
Foreground(t.TextMuted()).
|
||||
Render(fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, "permission denied")),
|
||||
)
|
||||
}
|
||||
}
|
||||
if content != "" || (finished && finishData.Reason == message.FinishReasonEndTurn) {
|
||||
@@ -148,7 +169,7 @@ func renderAssistantMessage(
|
||||
content = "*Finished without output*"
|
||||
}
|
||||
|
||||
content = renderMessage(content, false, msg.ID == focusedUIMessageId, width, info...)
|
||||
content = renderMessage(content, false, true, width, info...)
|
||||
messages = append(messages, uiMessage{
|
||||
ID: msg.ID,
|
||||
messageType: assistantMessageType,
|
||||
@@ -414,32 +435,36 @@ func truncateHeight(content string, height int) string {
|
||||
}
|
||||
|
||||
func renderToolResponse(toolCall message.ToolCall, response message.ToolResult, width int) string {
|
||||
t := theme.CurrentTheme()
|
||||
baseStyle := styles.BaseStyle()
|
||||
|
||||
if response.IsError {
|
||||
errContent := fmt.Sprintf("Error: %s", strings.ReplaceAll(response.Content, "\n", " "))
|
||||
errContent = ansi.Truncate(errContent, width-1, "...")
|
||||
return styles.BaseStyle.
|
||||
return baseStyle.
|
||||
Width(width).
|
||||
Foreground(styles.Error).
|
||||
Foreground(t.Error()).
|
||||
Render(errContent)
|
||||
}
|
||||
|
||||
resultContent := truncateHeight(response.Content, maxResultHeight)
|
||||
switch toolCall.Name {
|
||||
case agent.AgentToolName:
|
||||
return styles.ForceReplaceBackgroundWithLipgloss(
|
||||
toMarkdown(resultContent, false, width),
|
||||
styles.Background,
|
||||
t.Background(),
|
||||
)
|
||||
case tools.BashToolName:
|
||||
resultContent = fmt.Sprintf("```bash\n%s\n```", resultContent)
|
||||
return styles.ForceReplaceBackgroundWithLipgloss(
|
||||
toMarkdown(resultContent, true, width),
|
||||
styles.Background,
|
||||
t.Background(),
|
||||
)
|
||||
case tools.EditToolName:
|
||||
metadata := tools.EditResponseMetadata{}
|
||||
json.Unmarshal([]byte(response.Metadata), &metadata)
|
||||
truncDiff := truncateHeight(metadata.Diff, maxResultHeight)
|
||||
formattedDiff, _ := diff.FormatDiff(truncDiff, diff.WithTotalWidth(width), diff.WithStyle(diffStyle))
|
||||
formattedDiff, _ := diff.FormatDiff(truncDiff, diff.WithTotalWidth(width))
|
||||
return formattedDiff
|
||||
case tools.FetchToolName:
|
||||
var params tools.FetchParams
|
||||
@@ -454,16 +479,16 @@ func renderToolResponse(toolCall message.ToolCall, response message.ToolResult,
|
||||
resultContent = fmt.Sprintf("```%s\n%s\n```", mdFormat, resultContent)
|
||||
return styles.ForceReplaceBackgroundWithLipgloss(
|
||||
toMarkdown(resultContent, true, width),
|
||||
styles.Background,
|
||||
t.Background(),
|
||||
)
|
||||
case tools.GlobToolName:
|
||||
return styles.BaseStyle.Width(width).Foreground(styles.ForgroundMid).Render(resultContent)
|
||||
return baseStyle.Width(width).Foreground(t.TextMuted()).Render(resultContent)
|
||||
case tools.GrepToolName:
|
||||
return styles.BaseStyle.Width(width).Foreground(styles.ForgroundMid).Render(resultContent)
|
||||
return baseStyle.Width(width).Foreground(t.TextMuted()).Render(resultContent)
|
||||
case tools.LSToolName:
|
||||
return styles.BaseStyle.Width(width).Foreground(styles.ForgroundMid).Render(resultContent)
|
||||
return baseStyle.Width(width).Foreground(t.TextMuted()).Render(resultContent)
|
||||
case tools.SourcegraphToolName:
|
||||
return styles.BaseStyle.Width(width).Foreground(styles.ForgroundMid).Render(resultContent)
|
||||
return baseStyle.Width(width).Foreground(t.TextMuted()).Render(resultContent)
|
||||
case tools.ViewToolName:
|
||||
metadata := tools.ViewResponseMetadata{}
|
||||
json.Unmarshal([]byte(response.Metadata), &metadata)
|
||||
@@ -476,7 +501,7 @@ func renderToolResponse(toolCall message.ToolCall, response message.ToolResult,
|
||||
resultContent = fmt.Sprintf("```%s\n%s\n```", ext, truncateHeight(metadata.Content, maxResultHeight))
|
||||
return styles.ForceReplaceBackgroundWithLipgloss(
|
||||
toMarkdown(resultContent, true, width),
|
||||
styles.Background,
|
||||
t.Background(),
|
||||
)
|
||||
case tools.WriteToolName:
|
||||
params := tools.WriteParams{}
|
||||
@@ -492,13 +517,13 @@ func renderToolResponse(toolCall message.ToolCall, response message.ToolResult,
|
||||
resultContent = fmt.Sprintf("```%s\n%s\n```", ext, truncateHeight(params.Content, maxResultHeight))
|
||||
return styles.ForceReplaceBackgroundWithLipgloss(
|
||||
toMarkdown(resultContent, true, width),
|
||||
styles.Background,
|
||||
t.Background(),
|
||||
)
|
||||
default:
|
||||
resultContent = fmt.Sprintf("```text\n%s\n```", resultContent)
|
||||
return styles.ForceReplaceBackgroundWithLipgloss(
|
||||
toMarkdown(resultContent, true, width),
|
||||
styles.Background,
|
||||
t.Background(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -515,39 +540,31 @@ func renderToolMessage(
|
||||
if nested {
|
||||
width = width - 3
|
||||
}
|
||||
style := styles.BaseStyle.
|
||||
|
||||
t := theme.CurrentTheme()
|
||||
baseStyle := styles.BaseStyle()
|
||||
|
||||
style := baseStyle.
|
||||
Width(width - 1).
|
||||
BorderLeft(true).
|
||||
BorderStyle(lipgloss.ThickBorder()).
|
||||
PaddingLeft(1).
|
||||
BorderForeground(styles.ForgroundDim)
|
||||
BorderForeground(t.TextMuted())
|
||||
|
||||
response := findToolResponse(toolCall.ID, allMessages)
|
||||
toolName := styles.BaseStyle.Foreground(styles.ForgroundDim).Render(fmt.Sprintf("%s: ", toolName(toolCall.Name)))
|
||||
toolNameText := baseStyle.Foreground(t.TextMuted()).
|
||||
Render(fmt.Sprintf("%s: ", toolName(toolCall.Name)))
|
||||
|
||||
if !toolCall.Finished {
|
||||
// Get a brief description of what the tool is doing
|
||||
toolAction := getToolAction(toolCall.Name)
|
||||
|
||||
// toolInput := strings.ReplaceAll(toolCall.Input, "\n", " ")
|
||||
// truncatedInput := toolInput
|
||||
// if len(truncatedInput) > 10 {
|
||||
// truncatedInput = truncatedInput[len(truncatedInput)-10:]
|
||||
// }
|
||||
//
|
||||
// truncatedInput = styles.BaseStyle.
|
||||
// Italic(true).
|
||||
// Width(width - 2 - lipgloss.Width(toolName)).
|
||||
// Background(styles.BackgroundDim).
|
||||
// Foreground(styles.ForgroundMid).
|
||||
// Render(truncatedInput)
|
||||
|
||||
progressText := styles.BaseStyle.
|
||||
Width(width - 2 - lipgloss.Width(toolName)).
|
||||
Foreground(styles.ForgroundDim).
|
||||
progressText := baseStyle.
|
||||
Width(width - 2 - lipgloss.Width(toolNameText)).
|
||||
Foreground(t.TextMuted()).
|
||||
Render(fmt.Sprintf("%s", toolAction))
|
||||
|
||||
content := style.Render(lipgloss.JoinHorizontal(lipgloss.Left, toolName, progressText))
|
||||
content := style.Render(lipgloss.JoinHorizontal(lipgloss.Left, toolNameText, progressText))
|
||||
toolMsg := uiMessage{
|
||||
messageType: toolMessageType,
|
||||
position: position,
|
||||
@@ -556,37 +573,39 @@ func renderToolMessage(
|
||||
}
|
||||
return toolMsg
|
||||
}
|
||||
params := renderToolParams(width-2-lipgloss.Width(toolName), toolCall)
|
||||
|
||||
params := renderToolParams(width-2-lipgloss.Width(toolNameText), toolCall)
|
||||
responseContent := ""
|
||||
if response != nil {
|
||||
responseContent = renderToolResponse(toolCall, *response, width-2)
|
||||
responseContent = strings.TrimSuffix(responseContent, "\n")
|
||||
} else {
|
||||
responseContent = styles.BaseStyle.
|
||||
responseContent = baseStyle.
|
||||
Italic(true).
|
||||
Width(width - 2).
|
||||
Foreground(styles.ForgroundDim).
|
||||
Foreground(t.TextMuted()).
|
||||
Render("Waiting for response...")
|
||||
}
|
||||
|
||||
parts := []string{}
|
||||
if !nested {
|
||||
params := styles.BaseStyle.
|
||||
Width(width - 2 - lipgloss.Width(toolName)).
|
||||
Foreground(styles.ForgroundDim).
|
||||
formattedParams := baseStyle.
|
||||
Width(width - 2 - lipgloss.Width(toolNameText)).
|
||||
Foreground(t.TextMuted()).
|
||||
Render(params)
|
||||
|
||||
parts = append(parts, lipgloss.JoinHorizontal(lipgloss.Left, toolName, params))
|
||||
parts = append(parts, lipgloss.JoinHorizontal(lipgloss.Left, toolNameText, formattedParams))
|
||||
} else {
|
||||
prefix := styles.BaseStyle.
|
||||
Foreground(styles.ForgroundDim).
|
||||
prefix := baseStyle.
|
||||
Foreground(t.TextMuted()).
|
||||
Render(" └ ")
|
||||
params := styles.BaseStyle.
|
||||
Width(width - 2 - lipgloss.Width(toolName)).
|
||||
Foreground(styles.ForgroundMid).
|
||||
formattedParams := baseStyle.
|
||||
Width(width - 2 - lipgloss.Width(toolNameText)).
|
||||
Foreground(t.TextMuted()).
|
||||
Render(params)
|
||||
parts = append(parts, lipgloss.JoinHorizontal(lipgloss.Left, prefix, toolName, params))
|
||||
parts = append(parts, lipgloss.JoinHorizontal(lipgloss.Left, prefix, toolNameText, formattedParams))
|
||||
}
|
||||
|
||||
if toolCall.Name == agent.AgentToolName {
|
||||
taskMessages, _ := messagesService.List(context.Background(), toolCall.ID)
|
||||
toolCalls := []message.ToolCall{}
|
||||
@@ -622,3 +641,15 @@ func renderToolMessage(
|
||||
}
|
||||
return toolMsg
|
||||
}
|
||||
|
||||
// Helper function to format the time difference between two Unix timestamps
|
||||
func formatTimestampDiff(start, end int64) string {
|
||||
diffSeconds := float64(end-start) / 1000.0 // Convert to seconds
|
||||
if diffSeconds < 1 {
|
||||
return fmt.Sprintf("%dms", int(diffSeconds*1000))
|
||||
}
|
||||
if diffSeconds < 60 {
|
||||
return fmt.Sprintf("%.1fs", diffSeconds)
|
||||
}
|
||||
return fmt.Sprintf("%.1fm", diffSeconds/60)
|
||||
}
|
||||
|
||||
@@ -8,12 +8,13 @@ import (
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/kujtimiihoxha/opencode/internal/config"
|
||||
"github.com/kujtimiihoxha/opencode/internal/diff"
|
||||
"github.com/kujtimiihoxha/opencode/internal/history"
|
||||
"github.com/kujtimiihoxha/opencode/internal/pubsub"
|
||||
"github.com/kujtimiihoxha/opencode/internal/session"
|
||||
"github.com/kujtimiihoxha/opencode/internal/tui/styles"
|
||||
"github.com/opencode-ai/opencode/internal/config"
|
||||
"github.com/opencode-ai/opencode/internal/diff"
|
||||
"github.com/opencode-ai/opencode/internal/history"
|
||||
"github.com/opencode-ai/opencode/internal/pubsub"
|
||||
"github.com/opencode-ai/opencode/internal/session"
|
||||
"github.com/opencode-ai/opencode/internal/tui/styles"
|
||||
"github.com/opencode-ai/opencode/internal/tui/theme"
|
||||
)
|
||||
|
||||
type sidebarCmp struct {
|
||||
@@ -81,7 +82,9 @@ func (m *sidebarCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
|
||||
func (m *sidebarCmp) View() string {
|
||||
return styles.BaseStyle.
|
||||
baseStyle := styles.BaseStyle()
|
||||
|
||||
return baseStyle.
|
||||
Width(m.width).
|
||||
PaddingLeft(4).
|
||||
PaddingRight(2).
|
||||
@@ -101,11 +104,19 @@ func (m *sidebarCmp) View() string {
|
||||
}
|
||||
|
||||
func (m *sidebarCmp) sessionSection() string {
|
||||
sessionKey := styles.BaseStyle.Foreground(styles.PrimaryColor).Bold(true).Render("Session")
|
||||
sessionValue := styles.BaseStyle.
|
||||
Foreground(styles.Forground).
|
||||
t := theme.CurrentTheme()
|
||||
baseStyle := styles.BaseStyle()
|
||||
|
||||
sessionKey := baseStyle.
|
||||
Foreground(t.Primary()).
|
||||
Bold(true).
|
||||
Render("Session")
|
||||
|
||||
sessionValue := baseStyle.
|
||||
Foreground(t.Text()).
|
||||
Width(m.width - lipgloss.Width(sessionKey)).
|
||||
Render(fmt.Sprintf(": %s", m.session.Title))
|
||||
|
||||
return lipgloss.JoinHorizontal(
|
||||
lipgloss.Left,
|
||||
sessionKey,
|
||||
@@ -114,17 +125,40 @@ func (m *sidebarCmp) sessionSection() string {
|
||||
}
|
||||
|
||||
func (m *sidebarCmp) modifiedFile(filePath string, additions, removals int) string {
|
||||
t := theme.CurrentTheme()
|
||||
baseStyle := styles.BaseStyle()
|
||||
|
||||
stats := ""
|
||||
if additions > 0 && removals > 0 {
|
||||
stats = styles.BaseStyle.Foreground(styles.ForgroundDim).Render(fmt.Sprintf(" %d additions and %d removals", additions, removals))
|
||||
} else if additions > 0 {
|
||||
stats = styles.BaseStyle.Foreground(styles.ForgroundDim).Render(fmt.Sprintf(" %d additions", additions))
|
||||
} else if removals > 0 {
|
||||
stats = styles.BaseStyle.Foreground(styles.ForgroundDim).Render(fmt.Sprintf(" %d removals", removals))
|
||||
}
|
||||
filePathStr := styles.BaseStyle.Foreground(styles.Forground).Render(filePath)
|
||||
additionsStr := baseStyle.
|
||||
Foreground(t.Success()).
|
||||
PaddingLeft(1).
|
||||
Render(fmt.Sprintf("+%d", additions))
|
||||
|
||||
return styles.BaseStyle.
|
||||
removalsStr := baseStyle.
|
||||
Foreground(t.Error()).
|
||||
PaddingLeft(1).
|
||||
Render(fmt.Sprintf("-%d", removals))
|
||||
|
||||
content := lipgloss.JoinHorizontal(lipgloss.Left, additionsStr, removalsStr)
|
||||
stats = baseStyle.Width(lipgloss.Width(content)).Render(content)
|
||||
} else if additions > 0 {
|
||||
additionsStr := fmt.Sprintf(" %s", baseStyle.
|
||||
PaddingLeft(1).
|
||||
Foreground(t.Success()).
|
||||
Render(fmt.Sprintf("+%d", additions)))
|
||||
stats = baseStyle.Width(lipgloss.Width(additionsStr)).Render(additionsStr)
|
||||
} else if removals > 0 {
|
||||
removalsStr := fmt.Sprintf(" %s", baseStyle.
|
||||
PaddingLeft(1).
|
||||
Foreground(t.Error()).
|
||||
Render(fmt.Sprintf("-%d", removals)))
|
||||
stats = baseStyle.Width(lipgloss.Width(removalsStr)).Render(removalsStr)
|
||||
}
|
||||
|
||||
filePathStr := baseStyle.Render(filePath)
|
||||
|
||||
return baseStyle.
|
||||
Width(m.width).
|
||||
Render(
|
||||
lipgloss.JoinHorizontal(
|
||||
@@ -136,7 +170,14 @@ func (m *sidebarCmp) modifiedFile(filePath string, additions, removals int) stri
|
||||
}
|
||||
|
||||
func (m *sidebarCmp) modifiedFiles() string {
|
||||
modifiedFiles := styles.BaseStyle.Width(m.width).Foreground(styles.PrimaryColor).Bold(true).Render("Modified Files:")
|
||||
t := theme.CurrentTheme()
|
||||
baseStyle := styles.BaseStyle()
|
||||
|
||||
modifiedFiles := baseStyle.
|
||||
Width(m.width).
|
||||
Foreground(t.Primary()).
|
||||
Bold(true).
|
||||
Render("Modified Files:")
|
||||
|
||||
// If no modified files, show a placeholder message
|
||||
if m.modFiles == nil || len(m.modFiles) == 0 {
|
||||
@@ -145,13 +186,13 @@ func (m *sidebarCmp) modifiedFiles() string {
|
||||
if remainingWidth > 0 {
|
||||
message += strings.Repeat(" ", remainingWidth)
|
||||
}
|
||||
return styles.BaseStyle.
|
||||
return baseStyle.
|
||||
Width(m.width).
|
||||
Render(
|
||||
lipgloss.JoinVertical(
|
||||
lipgloss.Top,
|
||||
modifiedFiles,
|
||||
styles.BaseStyle.Foreground(styles.ForgroundDim).Render(message),
|
||||
baseStyle.Foreground(t.TextMuted()).Render(message),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -170,7 +211,7 @@ func (m *sidebarCmp) modifiedFiles() string {
|
||||
fileViews = append(fileViews, m.modifiedFile(path, stats.additions, stats.removals))
|
||||
}
|
||||
|
||||
return styles.BaseStyle.
|
||||
return baseStyle.
|
||||
Width(m.width).
|
||||
Render(
|
||||
lipgloss.JoinVertical(
|
||||
|
||||
@@ -7,20 +7,21 @@ import (
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/kujtimiihoxha/opencode/internal/config"
|
||||
"github.com/kujtimiihoxha/opencode/internal/llm/models"
|
||||
"github.com/kujtimiihoxha/opencode/internal/lsp"
|
||||
"github.com/kujtimiihoxha/opencode/internal/lsp/protocol"
|
||||
"github.com/kujtimiihoxha/opencode/internal/pubsub"
|
||||
"github.com/kujtimiihoxha/opencode/internal/session"
|
||||
"github.com/kujtimiihoxha/opencode/internal/tui/components/chat"
|
||||
"github.com/kujtimiihoxha/opencode/internal/tui/styles"
|
||||
"github.com/kujtimiihoxha/opencode/internal/tui/util"
|
||||
"github.com/opencode-ai/opencode/internal/config"
|
||||
"github.com/opencode-ai/opencode/internal/llm/models"
|
||||
"github.com/opencode-ai/opencode/internal/lsp"
|
||||
"github.com/opencode-ai/opencode/internal/lsp/protocol"
|
||||
"github.com/opencode-ai/opencode/internal/pubsub"
|
||||
"github.com/opencode-ai/opencode/internal/session"
|
||||
"github.com/opencode-ai/opencode/internal/tui/components/chat"
|
||||
"github.com/opencode-ai/opencode/internal/tui/styles"
|
||||
"github.com/opencode-ai/opencode/internal/tui/theme"
|
||||
"github.com/opencode-ai/opencode/internal/tui/util"
|
||||
)
|
||||
|
||||
type StatusCmp interface {
|
||||
tea.Model
|
||||
SetHelpMsg(string)
|
||||
SetHelpWidgetMsg(string)
|
||||
}
|
||||
|
||||
type statusCmp struct {
|
||||
@@ -70,7 +71,21 @@ func (m statusCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
var helpWidget = styles.Padded.Background(styles.ForgroundMid).Foreground(styles.BackgroundDarker).Bold(true).Render("ctrl+? help")
|
||||
var helpWidget = ""
|
||||
|
||||
// getHelpWidget returns the help widget with current theme colors
|
||||
func getHelpWidget(helpText string) string {
|
||||
t := theme.CurrentTheme()
|
||||
if helpText == "" {
|
||||
helpText = "ctrl+? help"
|
||||
}
|
||||
|
||||
return styles.Padded().
|
||||
Background(t.TextMuted()).
|
||||
Foreground(t.BackgroundDarker()).
|
||||
Bold(true).
|
||||
Render(helpText)
|
||||
}
|
||||
|
||||
func formatTokensAndCost(tokens int64, cost float64) string {
|
||||
// Format tokens in human-readable format (e.g., 110K, 1.2M)
|
||||
@@ -99,29 +114,38 @@ func formatTokensAndCost(tokens int64, cost float64) string {
|
||||
}
|
||||
|
||||
func (m statusCmp) View() string {
|
||||
status := helpWidget
|
||||
t := theme.CurrentTheme()
|
||||
|
||||
// Initialize the help widget
|
||||
status := getHelpWidget("")
|
||||
|
||||
if m.session.ID != "" {
|
||||
tokens := formatTokensAndCost(m.session.PromptTokens+m.session.CompletionTokens, m.session.Cost)
|
||||
tokensStyle := styles.Padded.
|
||||
Background(styles.Forground).
|
||||
Foreground(styles.BackgroundDim).
|
||||
tokensStyle := styles.Padded().
|
||||
Background(t.Text()).
|
||||
Foreground(t.BackgroundSecondary()).
|
||||
Render(tokens)
|
||||
status += tokensStyle
|
||||
}
|
||||
|
||||
diagnostics := styles.Padded.Background(styles.BackgroundDarker).Render(m.projectDiagnostics())
|
||||
diagnostics := styles.Padded().
|
||||
Background(t.BackgroundDarker()).
|
||||
Render(m.projectDiagnostics())
|
||||
|
||||
if m.info.Msg != "" {
|
||||
infoStyle := styles.Padded.
|
||||
Foreground(styles.Base).
|
||||
infoStyle := styles.Padded().
|
||||
Foreground(t.Background()).
|
||||
Width(m.availableFooterMsgWidth(diagnostics))
|
||||
|
||||
switch m.info.Type {
|
||||
case util.InfoTypeInfo:
|
||||
infoStyle = infoStyle.Background(styles.BorderColor)
|
||||
infoStyle = infoStyle.Background(t.Info())
|
||||
case util.InfoTypeWarn:
|
||||
infoStyle = infoStyle.Background(styles.Peach)
|
||||
infoStyle = infoStyle.Background(t.Warning())
|
||||
case util.InfoTypeError:
|
||||
infoStyle = infoStyle.Background(styles.Red)
|
||||
infoStyle = infoStyle.Background(t.Error())
|
||||
}
|
||||
|
||||
// Truncate message if it's longer than available width
|
||||
msg := m.info.Msg
|
||||
availWidth := m.availableFooterMsgWidth(diagnostics) - 10
|
||||
@@ -130,9 +154,9 @@ func (m statusCmp) View() string {
|
||||
}
|
||||
status += infoStyle.Render(msg)
|
||||
} else {
|
||||
status += styles.Padded.
|
||||
Foreground(styles.Base).
|
||||
Background(styles.BackgroundDim).
|
||||
status += styles.Padded().
|
||||
Foreground(t.Text()).
|
||||
Background(t.BackgroundSecondary()).
|
||||
Width(m.availableFooterMsgWidth(diagnostics)).
|
||||
Render("")
|
||||
}
|
||||
@@ -143,6 +167,8 @@ func (m statusCmp) View() string {
|
||||
}
|
||||
|
||||
func (m *statusCmp) projectDiagnostics() string {
|
||||
t := theme.CurrentTheme()
|
||||
|
||||
// Check if any LSP server is still initializing
|
||||
initializing := false
|
||||
for _, client := range m.lspClients {
|
||||
@@ -155,8 +181,8 @@ func (m *statusCmp) projectDiagnostics() string {
|
||||
// If any server is initializing, show that status
|
||||
if initializing {
|
||||
return lipgloss.NewStyle().
|
||||
Background(styles.BackgroundDarker).
|
||||
Foreground(styles.Peach).
|
||||
Background(t.BackgroundDarker()).
|
||||
Foreground(t.Warning()).
|
||||
Render(fmt.Sprintf("%s Initializing LSP...", styles.SpinnerIcon))
|
||||
}
|
||||
|
||||
@@ -189,29 +215,29 @@ func (m *statusCmp) projectDiagnostics() string {
|
||||
|
||||
if len(errorDiagnostics) > 0 {
|
||||
errStr := lipgloss.NewStyle().
|
||||
Background(styles.BackgroundDarker).
|
||||
Foreground(styles.Error).
|
||||
Background(t.BackgroundDarker()).
|
||||
Foreground(t.Error()).
|
||||
Render(fmt.Sprintf("%s %d", styles.ErrorIcon, len(errorDiagnostics)))
|
||||
diagnostics = append(diagnostics, errStr)
|
||||
}
|
||||
if len(warnDiagnostics) > 0 {
|
||||
warnStr := lipgloss.NewStyle().
|
||||
Background(styles.BackgroundDarker).
|
||||
Foreground(styles.Warning).
|
||||
Background(t.BackgroundDarker()).
|
||||
Foreground(t.Warning()).
|
||||
Render(fmt.Sprintf("%s %d", styles.WarningIcon, len(warnDiagnostics)))
|
||||
diagnostics = append(diagnostics, warnStr)
|
||||
}
|
||||
if len(hintDiagnostics) > 0 {
|
||||
hintStr := lipgloss.NewStyle().
|
||||
Background(styles.BackgroundDarker).
|
||||
Foreground(styles.Text).
|
||||
Background(t.BackgroundDarker()).
|
||||
Foreground(t.Text()).
|
||||
Render(fmt.Sprintf("%s %d", styles.HintIcon, len(hintDiagnostics)))
|
||||
diagnostics = append(diagnostics, hintStr)
|
||||
}
|
||||
if len(infoDiagnostics) > 0 {
|
||||
infoStr := lipgloss.NewStyle().
|
||||
Background(styles.BackgroundDarker).
|
||||
Foreground(styles.Peach).
|
||||
Background(t.BackgroundDarker()).
|
||||
Foreground(t.Info()).
|
||||
Render(fmt.Sprintf("%s %d", styles.InfoIcon, len(infoDiagnostics)))
|
||||
diagnostics = append(diagnostics, infoStr)
|
||||
}
|
||||
@@ -230,6 +256,8 @@ func (m statusCmp) availableFooterMsgWidth(diagnostics string) int {
|
||||
}
|
||||
|
||||
func (m statusCmp) model() string {
|
||||
t := theme.CurrentTheme()
|
||||
|
||||
cfg := config.Get()
|
||||
|
||||
coder, ok := cfg.Agents[config.AgentCoder]
|
||||
@@ -237,14 +265,22 @@ func (m statusCmp) model() string {
|
||||
return "Unknown"
|
||||
}
|
||||
model := models.SupportedModels[coder.Model]
|
||||
return styles.Padded.Background(styles.Grey).Foreground(styles.Text).Render(model.Name)
|
||||
|
||||
return styles.Padded().
|
||||
Background(t.Secondary()).
|
||||
Foreground(t.Background()).
|
||||
Render(model.Name)
|
||||
}
|
||||
|
||||
func (m statusCmp) SetHelpMsg(s string) {
|
||||
helpWidget = styles.Padded.Background(styles.Forground).Foreground(styles.BackgroundDarker).Bold(true).Render(s)
|
||||
func (m statusCmp) SetHelpWidgetMsg(s string) {
|
||||
// Update the help widget text using the getHelpWidget function
|
||||
helpWidget = getHelpWidget(s)
|
||||
}
|
||||
|
||||
func NewStatusCmp(lspClients map[string]*lsp.Client) StatusCmp {
|
||||
// Initialize the help widget with default text
|
||||
helpWidget = getHelpWidget("")
|
||||
|
||||
return &statusCmp{
|
||||
messageTTL: 10 * time.Second,
|
||||
lspClients: lspClients,
|
||||
|
||||
173
internal/tui/components/dialog/arguments.go
Normal file
173
internal/tui/components/dialog/arguments.go
Normal file
@@ -0,0 +1,173 @@
|
||||
package dialog
|
||||
|
||||
import (
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
|
||||
"github.com/opencode-ai/opencode/internal/tui/styles"
|
||||
"github.com/opencode-ai/opencode/internal/tui/theme"
|
||||
"github.com/opencode-ai/opencode/internal/tui/util"
|
||||
)
|
||||
|
||||
// ArgumentsDialogCmp is a component that asks the user for command arguments.
|
||||
type ArgumentsDialogCmp struct {
|
||||
width, height int
|
||||
textInput textinput.Model
|
||||
keys argumentsDialogKeyMap
|
||||
commandID string
|
||||
content string
|
||||
}
|
||||
|
||||
// NewArgumentsDialogCmp creates a new ArgumentsDialogCmp.
|
||||
func NewArgumentsDialogCmp(commandID, content string) ArgumentsDialogCmp {
|
||||
t := theme.CurrentTheme()
|
||||
ti := textinput.New()
|
||||
ti.Placeholder = "Enter arguments..."
|
||||
ti.Focus()
|
||||
ti.Width = 40
|
||||
ti.Prompt = ""
|
||||
ti.PlaceholderStyle = ti.PlaceholderStyle.Background(t.Background())
|
||||
ti.PromptStyle = ti.PromptStyle.Background(t.Background())
|
||||
ti.TextStyle = ti.TextStyle.Background(t.Background())
|
||||
|
||||
return ArgumentsDialogCmp{
|
||||
textInput: ti,
|
||||
keys: argumentsDialogKeyMap{},
|
||||
commandID: commandID,
|
||||
content: content,
|
||||
}
|
||||
}
|
||||
|
||||
type argumentsDialogKeyMap struct {
|
||||
Enter key.Binding
|
||||
Escape key.Binding
|
||||
}
|
||||
|
||||
// ShortHelp implements key.Map.
|
||||
func (k argumentsDialogKeyMap) ShortHelp() []key.Binding {
|
||||
return []key.Binding{
|
||||
key.NewBinding(
|
||||
key.WithKeys("enter"),
|
||||
key.WithHelp("enter", "confirm"),
|
||||
),
|
||||
key.NewBinding(
|
||||
key.WithKeys("esc"),
|
||||
key.WithHelp("esc", "cancel"),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// FullHelp implements key.Map.
|
||||
func (k argumentsDialogKeyMap) FullHelp() [][]key.Binding {
|
||||
return [][]key.Binding{k.ShortHelp()}
|
||||
}
|
||||
|
||||
// Init implements tea.Model.
|
||||
func (m ArgumentsDialogCmp) Init() tea.Cmd {
|
||||
return tea.Batch(
|
||||
textinput.Blink,
|
||||
m.textInput.Focus(),
|
||||
)
|
||||
}
|
||||
|
||||
// Update implements tea.Model.
|
||||
func (m ArgumentsDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
var cmd tea.Cmd
|
||||
var cmds []tea.Cmd
|
||||
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch {
|
||||
case key.Matches(msg, key.NewBinding(key.WithKeys("esc"))):
|
||||
return m, util.CmdHandler(CloseArgumentsDialogMsg{})
|
||||
case key.Matches(msg, key.NewBinding(key.WithKeys("enter"))):
|
||||
return m, util.CmdHandler(CloseArgumentsDialogMsg{
|
||||
Submit: true,
|
||||
CommandID: m.commandID,
|
||||
Content: m.content,
|
||||
Arguments: m.textInput.Value(),
|
||||
})
|
||||
}
|
||||
case tea.WindowSizeMsg:
|
||||
m.width = msg.Width
|
||||
m.height = msg.Height
|
||||
}
|
||||
|
||||
m.textInput, cmd = m.textInput.Update(msg)
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
return m, tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
// View implements tea.Model.
|
||||
func (m ArgumentsDialogCmp) View() string {
|
||||
t := theme.CurrentTheme()
|
||||
baseStyle := styles.BaseStyle()
|
||||
|
||||
// Calculate width needed for content
|
||||
maxWidth := 60 // Width for explanation text
|
||||
|
||||
title := baseStyle.
|
||||
Foreground(t.Primary()).
|
||||
Bold(true).
|
||||
Width(maxWidth).
|
||||
Padding(0, 1).
|
||||
Render("Command Arguments")
|
||||
|
||||
explanation := baseStyle.
|
||||
Foreground(t.Text()).
|
||||
Width(maxWidth).
|
||||
Padding(0, 1).
|
||||
Render("This command requires arguments. Please enter the text to replace $ARGUMENTS with:")
|
||||
|
||||
inputField := baseStyle.
|
||||
Foreground(t.Text()).
|
||||
Width(maxWidth).
|
||||
Padding(1, 1).
|
||||
Render(m.textInput.View())
|
||||
|
||||
maxWidth = min(maxWidth, m.width-10)
|
||||
|
||||
content := lipgloss.JoinVertical(
|
||||
lipgloss.Left,
|
||||
title,
|
||||
explanation,
|
||||
inputField,
|
||||
)
|
||||
|
||||
return baseStyle.Padding(1, 2).
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderBackground(t.Background()).
|
||||
BorderForeground(t.TextMuted()).
|
||||
Background(t.Background()).
|
||||
Width(lipgloss.Width(content) + 4).
|
||||
Render(content)
|
||||
}
|
||||
|
||||
// SetSize sets the size of the component.
|
||||
func (m *ArgumentsDialogCmp) SetSize(width, height int) {
|
||||
m.width = width
|
||||
m.height = height
|
||||
}
|
||||
|
||||
// Bindings implements layout.Bindings.
|
||||
func (m ArgumentsDialogCmp) Bindings() []key.Binding {
|
||||
return m.keys.ShortHelp()
|
||||
}
|
||||
|
||||
// CloseArgumentsDialogMsg is a message that is sent when the arguments dialog is closed.
|
||||
type CloseArgumentsDialogMsg struct {
|
||||
Submit bool
|
||||
CommandID string
|
||||
Content string
|
||||
Arguments string
|
||||
}
|
||||
|
||||
// ShowArgumentsDialogMsg is a message that is sent to show the arguments dialog.
|
||||
type ShowArgumentsDialogMsg struct {
|
||||
CommandID string
|
||||
Content string
|
||||
}
|
||||
|
||||
@@ -4,9 +4,10 @@ import (
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/kujtimiihoxha/opencode/internal/tui/layout"
|
||||
"github.com/kujtimiihoxha/opencode/internal/tui/styles"
|
||||
"github.com/kujtimiihoxha/opencode/internal/tui/util"
|
||||
"github.com/opencode-ai/opencode/internal/tui/layout"
|
||||
"github.com/opencode-ai/opencode/internal/tui/styles"
|
||||
"github.com/opencode-ai/opencode/internal/tui/theme"
|
||||
"github.com/opencode-ai/opencode/internal/tui/util"
|
||||
)
|
||||
|
||||
// Command represents a command that can be executed
|
||||
@@ -112,11 +113,14 @@ func (c *commandDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
|
||||
func (c *commandDialogCmp) View() string {
|
||||
t := theme.CurrentTheme()
|
||||
baseStyle := styles.BaseStyle()
|
||||
|
||||
if len(c.commands) == 0 {
|
||||
return styles.BaseStyle.Padding(1, 2).
|
||||
return baseStyle.Padding(1, 2).
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderBackground(styles.Background).
|
||||
BorderForeground(styles.ForgroundDim).
|
||||
BorderBackground(t.Background()).
|
||||
BorderForeground(t.TextMuted()).
|
||||
Width(40).
|
||||
Render("No commands available")
|
||||
}
|
||||
@@ -154,17 +158,17 @@ func (c *commandDialogCmp) View() string {
|
||||
|
||||
for i := startIdx; i < endIdx; i++ {
|
||||
cmd := c.commands[i]
|
||||
itemStyle := styles.BaseStyle.Width(maxWidth)
|
||||
descStyle := styles.BaseStyle.Width(maxWidth).Foreground(styles.ForgroundDim)
|
||||
itemStyle := baseStyle.Width(maxWidth)
|
||||
descStyle := baseStyle.Width(maxWidth).Foreground(t.TextMuted())
|
||||
|
||||
if i == c.selectedIdx {
|
||||
itemStyle = itemStyle.
|
||||
Background(styles.PrimaryColor).
|
||||
Foreground(styles.Background).
|
||||
Background(t.Primary()).
|
||||
Foreground(t.Background()).
|
||||
Bold(true)
|
||||
descStyle = descStyle.
|
||||
Background(styles.PrimaryColor).
|
||||
Foreground(styles.Background)
|
||||
Background(t.Primary()).
|
||||
Foreground(t.Background())
|
||||
}
|
||||
|
||||
title := itemStyle.Padding(0, 1).Render(cmd.Title)
|
||||
@@ -177,8 +181,8 @@ func (c *commandDialogCmp) View() string {
|
||||
}
|
||||
}
|
||||
|
||||
title := styles.BaseStyle.
|
||||
Foreground(styles.PrimaryColor).
|
||||
title := baseStyle.
|
||||
Foreground(t.Primary()).
|
||||
Bold(true).
|
||||
Width(maxWidth).
|
||||
Padding(0, 1).
|
||||
@@ -187,16 +191,15 @@ func (c *commandDialogCmp) View() string {
|
||||
content := lipgloss.JoinVertical(
|
||||
lipgloss.Left,
|
||||
title,
|
||||
styles.BaseStyle.Width(maxWidth).Render(""),
|
||||
styles.BaseStyle.Width(maxWidth).Render(lipgloss.JoinVertical(lipgloss.Left, commandItems...)),
|
||||
styles.BaseStyle.Width(maxWidth).Render(""),
|
||||
styles.BaseStyle.Width(maxWidth).Padding(0, 1).Foreground(styles.ForgroundDim).Render("↑/k: up ↓/j: down enter: select esc: cancel"),
|
||||
baseStyle.Width(maxWidth).Render(""),
|
||||
baseStyle.Width(maxWidth).Render(lipgloss.JoinVertical(lipgloss.Left, commandItems...)),
|
||||
baseStyle.Width(maxWidth).Render(""),
|
||||
)
|
||||
|
||||
return styles.BaseStyle.Padding(1, 2).
|
||||
return baseStyle.Padding(1, 2).
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderBackground(styles.Background).
|
||||
BorderForeground(styles.ForgroundDim).
|
||||
BorderBackground(t.Background()).
|
||||
BorderForeground(t.TextMuted()).
|
||||
Width(lipgloss.Width(content) + 4).
|
||||
Render(content)
|
||||
}
|
||||
@@ -244,4 +247,3 @@ func NewCommandDialogCmp() CommandDialog {
|
||||
selectedCommandID: "",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
166
internal/tui/components/dialog/custom_commands.go
Normal file
166
internal/tui/components/dialog/custom_commands.go
Normal file
@@ -0,0 +1,166 @@
|
||||
package dialog
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/opencode-ai/opencode/internal/config"
|
||||
"github.com/opencode-ai/opencode/internal/tui/util"
|
||||
)
|
||||
|
||||
// Command prefix constants
|
||||
const (
|
||||
UserCommandPrefix = "user:"
|
||||
ProjectCommandPrefix = "project:"
|
||||
)
|
||||
|
||||
// LoadCustomCommands loads custom commands from both XDG_CONFIG_HOME and project data directory
|
||||
func LoadCustomCommands() ([]Command, error) {
|
||||
cfg := config.Get()
|
||||
if cfg == nil {
|
||||
return nil, fmt.Errorf("config not loaded")
|
||||
}
|
||||
|
||||
var commands []Command
|
||||
|
||||
// Load user commands from XDG_CONFIG_HOME/opencode/commands
|
||||
xdgConfigHome := os.Getenv("XDG_CONFIG_HOME")
|
||||
if xdgConfigHome == "" {
|
||||
// Default to ~/.config if XDG_CONFIG_HOME is not set
|
||||
home, err := os.UserHomeDir()
|
||||
if err == nil {
|
||||
xdgConfigHome = filepath.Join(home, ".config")
|
||||
}
|
||||
}
|
||||
|
||||
if xdgConfigHome != "" {
|
||||
userCommandsDir := filepath.Join(xdgConfigHome, "opencode", "commands")
|
||||
userCommands, err := loadCommandsFromDir(userCommandsDir, UserCommandPrefix)
|
||||
if err != nil {
|
||||
// Log error but continue - we'll still try to load other commands
|
||||
fmt.Printf("Warning: failed to load user commands from XDG_CONFIG_HOME: %v\n", err)
|
||||
} else {
|
||||
commands = append(commands, userCommands...)
|
||||
}
|
||||
}
|
||||
|
||||
// Load commands from $HOME/.opencode/commands
|
||||
home, err := os.UserHomeDir()
|
||||
if err == nil {
|
||||
homeCommandsDir := filepath.Join(home, ".opencode", "commands")
|
||||
homeCommands, err := loadCommandsFromDir(homeCommandsDir, UserCommandPrefix)
|
||||
if err != nil {
|
||||
// Log error but continue - we'll still try to load other commands
|
||||
fmt.Printf("Warning: failed to load home commands: %v\n", err)
|
||||
} else {
|
||||
commands = append(commands, homeCommands...)
|
||||
}
|
||||
}
|
||||
|
||||
// Load project commands from data directory
|
||||
projectCommandsDir := filepath.Join(cfg.Data.Directory, "commands")
|
||||
projectCommands, err := loadCommandsFromDir(projectCommandsDir, ProjectCommandPrefix)
|
||||
if err != nil {
|
||||
// Log error but return what we have so far
|
||||
fmt.Printf("Warning: failed to load project commands: %v\n", err)
|
||||
} else {
|
||||
commands = append(commands, projectCommands...)
|
||||
}
|
||||
|
||||
return commands, nil
|
||||
}
|
||||
|
||||
// loadCommandsFromDir loads commands from a specific directory with the given prefix
|
||||
func loadCommandsFromDir(commandsDir string, prefix string) ([]Command, error) {
|
||||
// Check if the commands directory exists
|
||||
if _, err := os.Stat(commandsDir); os.IsNotExist(err) {
|
||||
// Create the commands directory if it doesn't exist
|
||||
if err := os.MkdirAll(commandsDir, 0755); err != nil {
|
||||
return nil, fmt.Errorf("failed to create commands directory %s: %w", commandsDir, err)
|
||||
}
|
||||
// Return empty list since we just created the directory
|
||||
return []Command{}, nil
|
||||
}
|
||||
|
||||
var commands []Command
|
||||
|
||||
// Walk through the commands directory and load all .md files
|
||||
err := filepath.Walk(commandsDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Skip directories
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Only process markdown files
|
||||
if !strings.HasSuffix(strings.ToLower(info.Name()), ".md") {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Read the file content
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read command file %s: %w", path, err)
|
||||
}
|
||||
|
||||
// Get the command ID from the file name without the .md extension
|
||||
commandID := strings.TrimSuffix(info.Name(), filepath.Ext(info.Name()))
|
||||
|
||||
// Get relative path from commands directory
|
||||
relPath, err := filepath.Rel(commandsDir, path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get relative path for %s: %w", path, err)
|
||||
}
|
||||
|
||||
// Create the command ID from the relative path
|
||||
// Replace directory separators with colons
|
||||
commandIDPath := strings.ReplaceAll(filepath.Dir(relPath), string(filepath.Separator), ":")
|
||||
if commandIDPath != "." {
|
||||
commandID = commandIDPath + ":" + commandID
|
||||
}
|
||||
|
||||
// Create a command
|
||||
command := Command{
|
||||
ID: prefix + commandID,
|
||||
Title: prefix + commandID,
|
||||
Description: fmt.Sprintf("Custom command from %s", relPath),
|
||||
Handler: func(cmd Command) tea.Cmd {
|
||||
commandContent := string(content)
|
||||
|
||||
// Check if the command contains $ARGUMENTS placeholder
|
||||
if strings.Contains(commandContent, "$ARGUMENTS") {
|
||||
// Show arguments dialog
|
||||
return util.CmdHandler(ShowArgumentsDialogMsg{
|
||||
CommandID: cmd.ID,
|
||||
Content: commandContent,
|
||||
})
|
||||
}
|
||||
|
||||
// No arguments needed, run command directly
|
||||
return util.CmdHandler(CommandRunCustomMsg{
|
||||
Content: commandContent,
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
commands = append(commands, command)
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load custom commands from %s: %w", commandsDir, err)
|
||||
}
|
||||
|
||||
return commands, nil
|
||||
}
|
||||
|
||||
// CommandRunCustomMsg is sent when a custom command is executed
|
||||
type CommandRunCustomMsg struct {
|
||||
Content string
|
||||
}
|
||||
477
internal/tui/components/dialog/filepicker.go
Normal file
477
internal/tui/components/dialog/filepicker.go
Normal file
@@ -0,0 +1,477 @@
|
||||
package dialog
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
"github.com/charmbracelet/bubbles/textinput"
|
||||
"github.com/charmbracelet/bubbles/viewport"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/opencode-ai/opencode/internal/app"
|
||||
"github.com/opencode-ai/opencode/internal/config"
|
||||
"github.com/opencode-ai/opencode/internal/logging"
|
||||
"github.com/opencode-ai/opencode/internal/message"
|
||||
"github.com/opencode-ai/opencode/internal/tui/image"
|
||||
"github.com/opencode-ai/opencode/internal/tui/styles"
|
||||
"github.com/opencode-ai/opencode/internal/tui/theme"
|
||||
"github.com/opencode-ai/opencode/internal/tui/util"
|
||||
)
|
||||
|
||||
const (
|
||||
maxAttachmentSize = int64(5 * 1024 * 1024) // 5MB
|
||||
downArrow = "down"
|
||||
upArrow = "up"
|
||||
)
|
||||
|
||||
type FilePrickerKeyMap struct {
|
||||
Enter key.Binding
|
||||
Down key.Binding
|
||||
Up key.Binding
|
||||
Forward key.Binding
|
||||
Backward key.Binding
|
||||
OpenFilePicker key.Binding
|
||||
Esc key.Binding
|
||||
InsertCWD key.Binding
|
||||
}
|
||||
|
||||
var filePickerKeyMap = FilePrickerKeyMap{
|
||||
Enter: key.NewBinding(
|
||||
key.WithKeys("enter"),
|
||||
key.WithHelp("enter", "select file/enter directory"),
|
||||
),
|
||||
Down: key.NewBinding(
|
||||
key.WithKeys("j", downArrow),
|
||||
key.WithHelp("↓/j", "down"),
|
||||
),
|
||||
Up: key.NewBinding(
|
||||
key.WithKeys("k", upArrow),
|
||||
key.WithHelp("↑/k", "up"),
|
||||
),
|
||||
Forward: key.NewBinding(
|
||||
key.WithKeys("l"),
|
||||
key.WithHelp("l", "enter directory"),
|
||||
),
|
||||
Backward: key.NewBinding(
|
||||
key.WithKeys("h", "backspace"),
|
||||
key.WithHelp("h/backspace", "go back"),
|
||||
),
|
||||
OpenFilePicker: key.NewBinding(
|
||||
key.WithKeys("ctrl+f"),
|
||||
key.WithHelp("ctrl+f", "open file picker"),
|
||||
),
|
||||
Esc: key.NewBinding(
|
||||
key.WithKeys("esc"),
|
||||
key.WithHelp("esc", "close/exit"),
|
||||
),
|
||||
InsertCWD: key.NewBinding(
|
||||
key.WithKeys("i"),
|
||||
key.WithHelp("i", "manual path input"),
|
||||
),
|
||||
}
|
||||
|
||||
type filepickerCmp struct {
|
||||
basePath string
|
||||
width int
|
||||
height int
|
||||
cursor int
|
||||
err error
|
||||
cursorChain stack
|
||||
viewport viewport.Model
|
||||
dirs []os.DirEntry
|
||||
cwdDetails *DirNode
|
||||
selectedFile string
|
||||
cwd textinput.Model
|
||||
ShowFilePicker bool
|
||||
app *app.App
|
||||
}
|
||||
|
||||
type DirNode struct {
|
||||
parent *DirNode
|
||||
child *DirNode
|
||||
directory string
|
||||
}
|
||||
type stack []int
|
||||
|
||||
func (s stack) Push(v int) stack {
|
||||
return append(s, v)
|
||||
}
|
||||
|
||||
func (s stack) Pop() (stack, int) {
|
||||
l := len(s)
|
||||
return s[:l-1], s[l-1]
|
||||
}
|
||||
|
||||
type AttachmentAddedMsg struct {
|
||||
Attachment message.Attachment
|
||||
}
|
||||
|
||||
func (f *filepickerCmp) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *filepickerCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
var cmd tea.Cmd
|
||||
switch msg := msg.(type) {
|
||||
case tea.WindowSizeMsg:
|
||||
f.width = 60
|
||||
f.height = 20
|
||||
f.viewport.Width = 80
|
||||
f.viewport.Height = 22
|
||||
f.cursor = 0
|
||||
f.getCurrentFileBelowCursor()
|
||||
case tea.KeyMsg:
|
||||
switch {
|
||||
case key.Matches(msg, filePickerKeyMap.InsertCWD):
|
||||
f.cwd.Focus()
|
||||
return f, cmd
|
||||
case key.Matches(msg, filePickerKeyMap.Esc):
|
||||
if f.cwd.Focused() {
|
||||
f.cwd.Blur()
|
||||
}
|
||||
case key.Matches(msg, filePickerKeyMap.Down):
|
||||
if !f.cwd.Focused() || msg.String() == downArrow {
|
||||
if f.cursor < len(f.dirs)-1 {
|
||||
f.cursor++
|
||||
f.getCurrentFileBelowCursor()
|
||||
}
|
||||
}
|
||||
case key.Matches(msg, filePickerKeyMap.Up):
|
||||
if !f.cwd.Focused() || msg.String() == upArrow {
|
||||
if f.cursor > 0 {
|
||||
f.cursor--
|
||||
f.getCurrentFileBelowCursor()
|
||||
}
|
||||
}
|
||||
case key.Matches(msg, filePickerKeyMap.Enter):
|
||||
var path string
|
||||
var isPathDir bool
|
||||
if f.cwd.Focused() {
|
||||
path = f.cwd.Value()
|
||||
fileInfo, err := os.Stat(path)
|
||||
if err != nil {
|
||||
logging.ErrorPersist("Invalid path")
|
||||
return f, cmd
|
||||
}
|
||||
isPathDir = fileInfo.IsDir()
|
||||
} else {
|
||||
path = filepath.Join(f.cwdDetails.directory, "/", f.dirs[f.cursor].Name())
|
||||
isPathDir = f.dirs[f.cursor].IsDir()
|
||||
}
|
||||
if isPathDir {
|
||||
path := filepath.Join(f.cwdDetails.directory, "/", f.dirs[f.cursor].Name())
|
||||
newWorkingDir := DirNode{parent: f.cwdDetails, directory: path}
|
||||
f.cwdDetails.child = &newWorkingDir
|
||||
f.cwdDetails = f.cwdDetails.child
|
||||
f.cursorChain = f.cursorChain.Push(f.cursor)
|
||||
f.dirs = readDir(f.cwdDetails.directory, false)
|
||||
f.cursor = 0
|
||||
f.cwd.SetValue(f.cwdDetails.directory)
|
||||
f.getCurrentFileBelowCursor()
|
||||
} else {
|
||||
f.selectedFile = path
|
||||
return f.addAttachmentToMessage()
|
||||
}
|
||||
case key.Matches(msg, filePickerKeyMap.Esc):
|
||||
if !f.cwd.Focused() {
|
||||
f.cursorChain = make(stack, 0)
|
||||
f.cursor = 0
|
||||
} else {
|
||||
f.cwd.Blur()
|
||||
}
|
||||
case key.Matches(msg, filePickerKeyMap.Forward):
|
||||
if !f.cwd.Focused() {
|
||||
if f.dirs[f.cursor].IsDir() {
|
||||
path := filepath.Join(f.cwdDetails.directory, "/", f.dirs[f.cursor].Name())
|
||||
newWorkingDir := DirNode{parent: f.cwdDetails, directory: path}
|
||||
f.cwdDetails.child = &newWorkingDir
|
||||
f.cwdDetails = f.cwdDetails.child
|
||||
f.cursorChain = f.cursorChain.Push(f.cursor)
|
||||
f.dirs = readDir(f.cwdDetails.directory, false)
|
||||
f.cursor = 0
|
||||
f.cwd.SetValue(f.cwdDetails.directory)
|
||||
f.getCurrentFileBelowCursor()
|
||||
}
|
||||
}
|
||||
case key.Matches(msg, filePickerKeyMap.Backward):
|
||||
if !f.cwd.Focused() {
|
||||
if len(f.cursorChain) != 0 && f.cwdDetails.parent != nil {
|
||||
f.cursorChain, f.cursor = f.cursorChain.Pop()
|
||||
f.cwdDetails = f.cwdDetails.parent
|
||||
f.cwdDetails.child = nil
|
||||
f.dirs = readDir(f.cwdDetails.directory, false)
|
||||
f.cwd.SetValue(f.cwdDetails.directory)
|
||||
f.getCurrentFileBelowCursor()
|
||||
}
|
||||
}
|
||||
case key.Matches(msg, filePickerKeyMap.OpenFilePicker):
|
||||
f.dirs = readDir(f.cwdDetails.directory, false)
|
||||
f.cursor = 0
|
||||
f.getCurrentFileBelowCursor()
|
||||
}
|
||||
}
|
||||
if f.cwd.Focused() {
|
||||
f.cwd, cmd = f.cwd.Update(msg)
|
||||
}
|
||||
return f, cmd
|
||||
}
|
||||
|
||||
func (f *filepickerCmp) addAttachmentToMessage() (tea.Model, tea.Cmd) {
|
||||
modeInfo := GetSelectedModel(config.Get())
|
||||
if !modeInfo.SupportsAttachments {
|
||||
logging.ErrorPersist(fmt.Sprintf("Model %s doesn't support attachments", modeInfo.Name))
|
||||
return f, nil
|
||||
}
|
||||
if isExtSupported(f.dirs[f.cursor].Name()) {
|
||||
f.selectedFile = f.dirs[f.cursor].Name()
|
||||
selectedFilePath := filepath.Join(f.cwdDetails.directory, "/", f.selectedFile)
|
||||
isFileLarge, err := image.ValidateFileSize(selectedFilePath, maxAttachmentSize)
|
||||
if err != nil {
|
||||
logging.ErrorPersist("unable to read the image")
|
||||
return f, nil
|
||||
}
|
||||
if isFileLarge {
|
||||
logging.ErrorPersist("file too large, max 5MB")
|
||||
return f, nil
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(selectedFilePath)
|
||||
if err != nil {
|
||||
logging.ErrorPersist("Unable read selected file")
|
||||
return f, nil
|
||||
}
|
||||
|
||||
mimeBufferSize := min(512, len(content))
|
||||
mimeType := http.DetectContentType(content[:mimeBufferSize])
|
||||
fileName := f.selectedFile
|
||||
attachment := message.Attachment{FilePath: selectedFilePath, FileName: fileName, MimeType: mimeType, Content: content}
|
||||
f.selectedFile = ""
|
||||
return f, util.CmdHandler(AttachmentAddedMsg{attachment})
|
||||
}
|
||||
if !isExtSupported(f.selectedFile) {
|
||||
logging.ErrorPersist("Unsupported file")
|
||||
return f, nil
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func (f *filepickerCmp) View() string {
|
||||
t := theme.CurrentTheme()
|
||||
const maxVisibleDirs = 20
|
||||
const maxWidth = 80
|
||||
|
||||
adjustedWidth := maxWidth
|
||||
for _, file := range f.dirs {
|
||||
if len(file.Name()) > adjustedWidth-4 { // Account for padding
|
||||
adjustedWidth = len(file.Name()) + 4
|
||||
}
|
||||
}
|
||||
adjustedWidth = max(30, min(adjustedWidth, f.width-15)) + 1
|
||||
|
||||
files := make([]string, 0, maxVisibleDirs)
|
||||
startIdx := 0
|
||||
|
||||
if len(f.dirs) > maxVisibleDirs {
|
||||
halfVisible := maxVisibleDirs / 2
|
||||
if f.cursor >= halfVisible && f.cursor < len(f.dirs)-halfVisible {
|
||||
startIdx = f.cursor - halfVisible
|
||||
} else if f.cursor >= len(f.dirs)-halfVisible {
|
||||
startIdx = len(f.dirs) - maxVisibleDirs
|
||||
}
|
||||
}
|
||||
|
||||
endIdx := min(startIdx+maxVisibleDirs, len(f.dirs))
|
||||
|
||||
for i := startIdx; i < endIdx; i++ {
|
||||
file := f.dirs[i]
|
||||
itemStyle := styles.BaseStyle().Width(adjustedWidth)
|
||||
|
||||
if i == f.cursor {
|
||||
itemStyle = itemStyle.
|
||||
Background(t.Primary()).
|
||||
Foreground(t.Background()).
|
||||
Bold(true)
|
||||
}
|
||||
filename := file.Name()
|
||||
|
||||
if len(filename) > adjustedWidth-4 {
|
||||
filename = filename[:adjustedWidth-7] + "..."
|
||||
}
|
||||
if file.IsDir() {
|
||||
filename = filename + "/"
|
||||
} else if isExtSupported(file.Name()) {
|
||||
filename = filename
|
||||
} else {
|
||||
filename = filename
|
||||
}
|
||||
|
||||
files = append(files, itemStyle.Padding(0, 1).Render(filename))
|
||||
}
|
||||
|
||||
// Pad to always show exactly 21 lines
|
||||
for len(files) < maxVisibleDirs {
|
||||
files = append(files, styles.BaseStyle().Width(adjustedWidth).Render(""))
|
||||
}
|
||||
|
||||
currentPath := styles.BaseStyle().
|
||||
Height(1).
|
||||
Width(adjustedWidth).
|
||||
Render(f.cwd.View())
|
||||
|
||||
viewportstyle := lipgloss.NewStyle().
|
||||
Width(f.viewport.Width).
|
||||
Background(t.Background()).
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderForeground(t.TextMuted()).
|
||||
BorderBackground(t.Background()).
|
||||
Padding(2).
|
||||
Render(f.viewport.View())
|
||||
var insertExitText string
|
||||
if f.IsCWDFocused() {
|
||||
insertExitText = "Press esc to exit typing path"
|
||||
} else {
|
||||
insertExitText = "Press i to start typing path"
|
||||
}
|
||||
|
||||
content := lipgloss.JoinVertical(
|
||||
lipgloss.Left,
|
||||
currentPath,
|
||||
styles.BaseStyle().Width(adjustedWidth).Render(""),
|
||||
styles.BaseStyle().Width(adjustedWidth).Render(lipgloss.JoinVertical(lipgloss.Left, files...)),
|
||||
styles.BaseStyle().Width(adjustedWidth).Render(""),
|
||||
styles.BaseStyle().Foreground(t.TextMuted()).Width(adjustedWidth).Render(insertExitText),
|
||||
)
|
||||
|
||||
f.cwd.SetValue(f.cwd.Value())
|
||||
contentStyle := styles.BaseStyle().Padding(1, 2).
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderBackground(t.Background()).
|
||||
BorderForeground(t.TextMuted()).
|
||||
Width(lipgloss.Width(content) + 4)
|
||||
|
||||
return lipgloss.JoinHorizontal(lipgloss.Center, contentStyle.Render(content), viewportstyle)
|
||||
}
|
||||
|
||||
type FilepickerCmp interface {
|
||||
tea.Model
|
||||
ToggleFilepicker(showFilepicker bool)
|
||||
IsCWDFocused() bool
|
||||
}
|
||||
|
||||
func (f *filepickerCmp) ToggleFilepicker(showFilepicker bool) {
|
||||
f.ShowFilePicker = showFilepicker
|
||||
}
|
||||
|
||||
func (f *filepickerCmp) IsCWDFocused() bool {
|
||||
return f.cwd.Focused()
|
||||
}
|
||||
|
||||
func NewFilepickerCmp(app *app.App) FilepickerCmp {
|
||||
homepath, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
logging.Error("error loading user files")
|
||||
return nil
|
||||
}
|
||||
baseDir := DirNode{parent: nil, directory: homepath}
|
||||
dirs := readDir(homepath, false)
|
||||
viewport := viewport.New(0, 0)
|
||||
currentDirectory := textinput.New()
|
||||
currentDirectory.CharLimit = 200
|
||||
currentDirectory.Width = 44
|
||||
currentDirectory.Cursor.Blink = true
|
||||
currentDirectory.SetValue(baseDir.directory)
|
||||
return &filepickerCmp{cwdDetails: &baseDir, dirs: dirs, cursorChain: make(stack, 0), viewport: viewport, cwd: currentDirectory, app: app}
|
||||
}
|
||||
|
||||
func (f *filepickerCmp) getCurrentFileBelowCursor() {
|
||||
if len(f.dirs) == 0 || f.cursor < 0 || f.cursor >= len(f.dirs) {
|
||||
logging.Error(fmt.Sprintf("Invalid cursor position. Dirs length: %d, Cursor: %d", len(f.dirs), f.cursor))
|
||||
f.viewport.SetContent("Preview unavailable")
|
||||
return
|
||||
}
|
||||
|
||||
dir := f.dirs[f.cursor]
|
||||
filename := dir.Name()
|
||||
if !dir.IsDir() && isExtSupported(filename) {
|
||||
fullPath := f.cwdDetails.directory + "/" + dir.Name()
|
||||
|
||||
go func() {
|
||||
imageString, err := image.ImagePreview(f.viewport.Width-4, fullPath)
|
||||
if err != nil {
|
||||
logging.Error(err.Error())
|
||||
f.viewport.SetContent("Preview unavailable")
|
||||
return
|
||||
}
|
||||
|
||||
f.viewport.SetContent(imageString)
|
||||
}()
|
||||
} else {
|
||||
f.viewport.SetContent("Preview unavailable")
|
||||
}
|
||||
}
|
||||
|
||||
func readDir(path string, showHidden bool) []os.DirEntry {
|
||||
logging.Info(fmt.Sprintf("Reading directory: %s", path))
|
||||
|
||||
entriesChan := make(chan []os.DirEntry, 1)
|
||||
errChan := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
dirEntries, err := os.ReadDir(path)
|
||||
if err != nil {
|
||||
logging.ErrorPersist(err.Error())
|
||||
errChan <- err
|
||||
return
|
||||
}
|
||||
entriesChan <- dirEntries
|
||||
}()
|
||||
|
||||
select {
|
||||
case dirEntries := <-entriesChan:
|
||||
sort.Slice(dirEntries, func(i, j int) bool {
|
||||
if dirEntries[i].IsDir() == dirEntries[j].IsDir() {
|
||||
return dirEntries[i].Name() < dirEntries[j].Name()
|
||||
}
|
||||
return dirEntries[i].IsDir()
|
||||
})
|
||||
|
||||
if showHidden {
|
||||
return dirEntries
|
||||
}
|
||||
|
||||
var sanitizedDirEntries []os.DirEntry
|
||||
for _, dirEntry := range dirEntries {
|
||||
isHidden, _ := IsHidden(dirEntry.Name())
|
||||
if !isHidden {
|
||||
if dirEntry.IsDir() || isExtSupported(dirEntry.Name()) {
|
||||
sanitizedDirEntries = append(sanitizedDirEntries, dirEntry)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sanitizedDirEntries
|
||||
|
||||
case err := <-errChan:
|
||||
logging.ErrorPersist(fmt.Sprintf("Error reading directory %s", path), err)
|
||||
return []os.DirEntry{}
|
||||
|
||||
case <-time.After(5 * time.Second):
|
||||
logging.ErrorPersist(fmt.Sprintf("Timeout reading directory %s", path), nil)
|
||||
return []os.DirEntry{}
|
||||
}
|
||||
}
|
||||
|
||||
func IsHidden(file string) (bool, error) {
|
||||
return strings.HasPrefix(file, "."), nil
|
||||
}
|
||||
|
||||
func isExtSupported(path string) bool {
|
||||
ext := strings.ToLower(filepath.Ext(path))
|
||||
return (ext == ".jpg" || ext == ".jpeg" || ext == ".webp" || ext == ".png")
|
||||
}
|
||||
@@ -6,7 +6,8 @@ import (
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/kujtimiihoxha/opencode/internal/tui/styles"
|
||||
"github.com/opencode-ai/opencode/internal/tui/styles"
|
||||
"github.com/opencode-ai/opencode/internal/tui/theme"
|
||||
)
|
||||
|
||||
type helpCmp struct {
|
||||
@@ -53,17 +54,29 @@ func removeDuplicateBindings(bindings []key.Binding) []key.Binding {
|
||||
}
|
||||
|
||||
func (h *helpCmp) render() string {
|
||||
helpKeyStyle := styles.Bold.Background(styles.Background).Foreground(styles.Forground).Padding(0, 1, 0, 0)
|
||||
helpDescStyle := styles.Regular.Background(styles.Background).Foreground(styles.ForgroundMid)
|
||||
t := theme.CurrentTheme()
|
||||
baseStyle := styles.BaseStyle()
|
||||
|
||||
helpKeyStyle := styles.Bold().
|
||||
Background(t.Background()).
|
||||
Foreground(t.Text()).
|
||||
Padding(0, 1, 0, 0)
|
||||
|
||||
helpDescStyle := styles.Regular().
|
||||
Background(t.Background()).
|
||||
Foreground(t.TextMuted())
|
||||
|
||||
// Compile list of bindings to render
|
||||
bindings := removeDuplicateBindings(h.keys)
|
||||
|
||||
// Enumerate through each group of bindings, populating a series of
|
||||
// pairs of columns, one for keys, one for descriptions
|
||||
var (
|
||||
pairs []string
|
||||
width int
|
||||
rows = 14 - 2
|
||||
rows = 12 - 2
|
||||
)
|
||||
|
||||
for i := 0; i < len(bindings); i += rows {
|
||||
var (
|
||||
keys []string
|
||||
@@ -73,11 +86,12 @@ func (h *helpCmp) render() string {
|
||||
keys = append(keys, helpKeyStyle.Render(bindings[j].Help().Key))
|
||||
descs = append(descs, helpDescStyle.Render(bindings[j].Help().Desc))
|
||||
}
|
||||
|
||||
// Render pair of columns; beyond the first pair, render a three space
|
||||
// left margin, in order to visually separate the pairs.
|
||||
var cols []string
|
||||
if len(pairs) > 0 {
|
||||
cols = []string{styles.BaseStyle.Render(" ")}
|
||||
cols = []string{baseStyle.Render(" ")}
|
||||
}
|
||||
|
||||
maxDescWidth := 0
|
||||
@@ -89,7 +103,7 @@ func (h *helpCmp) render() string {
|
||||
for i := range descs {
|
||||
remainingWidth := maxDescWidth - lipgloss.Width(descs[i])
|
||||
if remainingWidth > 0 {
|
||||
descs[i] = descs[i] + styles.BaseStyle.Render(strings.Repeat(" ", remainingWidth))
|
||||
descs[i] = descs[i] + baseStyle.Render(strings.Repeat(" ", remainingWidth))
|
||||
}
|
||||
}
|
||||
maxKeyWidth := 0
|
||||
@@ -101,7 +115,7 @@ func (h *helpCmp) render() string {
|
||||
for i := range keys {
|
||||
remainingWidth := maxKeyWidth - lipgloss.Width(keys[i])
|
||||
if remainingWidth > 0 {
|
||||
keys[i] = keys[i] + styles.BaseStyle.Render(strings.Repeat(" ", remainingWidth))
|
||||
keys[i] = keys[i] + baseStyle.Render(strings.Repeat(" ", remainingWidth))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,7 +124,7 @@ func (h *helpCmp) render() string {
|
||||
strings.Join(descs, "\n"),
|
||||
)
|
||||
|
||||
pair := styles.BaseStyle.Render(lipgloss.JoinHorizontal(lipgloss.Top, cols...))
|
||||
pair := baseStyle.Render(lipgloss.JoinHorizontal(lipgloss.Top, cols...))
|
||||
// check whether it exceeds the maximum width avail (the width of the
|
||||
// terminal, subtracting 2 for the borders).
|
||||
width += lipgloss.Width(pair)
|
||||
@@ -130,9 +144,9 @@ func (h *helpCmp) render() string {
|
||||
lipgloss.Left, // x
|
||||
lipgloss.Top, // y
|
||||
lastPair, // content
|
||||
lipgloss.WithWhitespaceBackground(styles.Background), // background
|
||||
lipgloss.WithWhitespaceBackground(t.Background()),
|
||||
))
|
||||
content := styles.BaseStyle.Width(h.width).Render(
|
||||
content := baseStyle.Width(h.width).Render(
|
||||
lipgloss.JoinHorizontal(
|
||||
lipgloss.Top,
|
||||
prefix...,
|
||||
@@ -140,8 +154,9 @@ func (h *helpCmp) render() string {
|
||||
)
|
||||
return content
|
||||
}
|
||||
|
||||
// Join pairs of columns and enclose in a border
|
||||
content := styles.BaseStyle.Width(h.width).Render(
|
||||
content := baseStyle.Width(h.width).Render(
|
||||
lipgloss.JoinHorizontal(
|
||||
lipgloss.Top,
|
||||
pairs...,
|
||||
@@ -151,22 +166,25 @@ func (h *helpCmp) render() string {
|
||||
}
|
||||
|
||||
func (h *helpCmp) View() string {
|
||||
t := theme.CurrentTheme()
|
||||
baseStyle := styles.BaseStyle()
|
||||
|
||||
content := h.render()
|
||||
header := styles.BaseStyle.
|
||||
header := baseStyle.
|
||||
Bold(true).
|
||||
Width(lipgloss.Width(content)).
|
||||
Foreground(styles.PrimaryColor).
|
||||
Foreground(t.Primary()).
|
||||
Render("Keyboard Shortcuts")
|
||||
|
||||
return styles.BaseStyle.Padding(1).
|
||||
return baseStyle.Padding(1).
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderForeground(styles.ForgroundDim).
|
||||
BorderForeground(t.TextMuted()).
|
||||
Width(h.width).
|
||||
BorderBackground(styles.Background).
|
||||
BorderBackground(t.Background()).
|
||||
Render(
|
||||
lipgloss.JoinVertical(lipgloss.Center,
|
||||
header,
|
||||
styles.BaseStyle.Render(strings.Repeat(" ", lipgloss.Width(header))),
|
||||
baseStyle.Render(strings.Repeat(" ", lipgloss.Width(header))),
|
||||
content,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -5,8 +5,9 @@ import (
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
|
||||
"github.com/kujtimiihoxha/opencode/internal/tui/styles"
|
||||
"github.com/kujtimiihoxha/opencode/internal/tui/util"
|
||||
"github.com/opencode-ai/opencode/internal/tui/styles"
|
||||
"github.com/opencode-ai/opencode/internal/tui/theme"
|
||||
"github.com/opencode-ai/opencode/internal/tui/util"
|
||||
)
|
||||
|
||||
// InitDialogCmp is a component that asks the user if they want to initialize the project.
|
||||
@@ -46,8 +47,8 @@ func (k initDialogKeyMap) ShortHelp() []key.Binding {
|
||||
key.WithHelp("enter", "confirm"),
|
||||
),
|
||||
key.NewBinding(
|
||||
key.WithKeys("esc"),
|
||||
key.WithHelp("esc", "cancel"),
|
||||
key.WithKeys("esc", "q"),
|
||||
key.WithHelp("esc/q", "cancel"),
|
||||
),
|
||||
key.NewBinding(
|
||||
key.WithKeys("y", "n"),
|
||||
@@ -92,79 +93,76 @@ func (m InitDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
|
||||
// View implements tea.Model.
|
||||
func (m InitDialogCmp) View() string {
|
||||
t := theme.CurrentTheme()
|
||||
baseStyle := styles.BaseStyle()
|
||||
|
||||
// Calculate width needed for content
|
||||
maxWidth := 60 // Width for explanation text
|
||||
|
||||
title := styles.BaseStyle.
|
||||
Foreground(styles.PrimaryColor).
|
||||
title := baseStyle.
|
||||
Foreground(t.Primary()).
|
||||
Bold(true).
|
||||
Width(maxWidth).
|
||||
Padding(0, 1).
|
||||
Render("Initialize Project")
|
||||
|
||||
explanation := styles.BaseStyle.
|
||||
Foreground(styles.Forground).
|
||||
explanation := baseStyle.
|
||||
Foreground(t.Text()).
|
||||
Width(maxWidth).
|
||||
Padding(0, 1).
|
||||
Render("Initialization generates a new OpenCode.md file that contains information about your codebase, this file serves as memory for each project, you can freely add to it to help the agents be better at their job.")
|
||||
|
||||
question := styles.BaseStyle.
|
||||
Foreground(styles.Forground).
|
||||
question := baseStyle.
|
||||
Foreground(t.Text()).
|
||||
Width(maxWidth).
|
||||
Padding(1, 1).
|
||||
Render("Would you like to initialize this project?")
|
||||
|
||||
yesStyle := styles.BaseStyle
|
||||
noStyle := styles.BaseStyle
|
||||
maxWidth = min(maxWidth, m.width-10)
|
||||
yesStyle := baseStyle
|
||||
noStyle := baseStyle
|
||||
|
||||
if m.selected == 0 {
|
||||
yesStyle = yesStyle.
|
||||
Background(styles.PrimaryColor).
|
||||
Foreground(styles.Background).
|
||||
Background(t.Primary()).
|
||||
Foreground(t.Background()).
|
||||
Bold(true)
|
||||
noStyle = noStyle.
|
||||
Background(styles.Background).
|
||||
Foreground(styles.PrimaryColor)
|
||||
Background(t.Background()).
|
||||
Foreground(t.Primary())
|
||||
} else {
|
||||
noStyle = noStyle.
|
||||
Background(styles.PrimaryColor).
|
||||
Foreground(styles.Background).
|
||||
Background(t.Primary()).
|
||||
Foreground(t.Background()).
|
||||
Bold(true)
|
||||
yesStyle = yesStyle.
|
||||
Background(styles.Background).
|
||||
Foreground(styles.PrimaryColor)
|
||||
Background(t.Background()).
|
||||
Foreground(t.Primary())
|
||||
}
|
||||
|
||||
yes := yesStyle.Padding(0, 3).Render("Yes")
|
||||
no := noStyle.Padding(0, 3).Render("No")
|
||||
|
||||
buttons := lipgloss.JoinHorizontal(lipgloss.Center, yes, styles.BaseStyle.Render(" "), no)
|
||||
buttons = styles.BaseStyle.
|
||||
buttons := lipgloss.JoinHorizontal(lipgloss.Center, yes, baseStyle.Render(" "), no)
|
||||
buttons = baseStyle.
|
||||
Width(maxWidth).
|
||||
Padding(1, 0).
|
||||
Render(buttons)
|
||||
|
||||
help := styles.BaseStyle.
|
||||
Width(maxWidth).
|
||||
Padding(0, 1).
|
||||
Foreground(styles.ForgroundDim).
|
||||
Render("tab/←/→: toggle y/n: yes/no enter: confirm esc: cancel")
|
||||
|
||||
content := lipgloss.JoinVertical(
|
||||
lipgloss.Left,
|
||||
title,
|
||||
styles.BaseStyle.Width(maxWidth).Render(""),
|
||||
baseStyle.Width(maxWidth).Render(""),
|
||||
explanation,
|
||||
question,
|
||||
buttons,
|
||||
styles.BaseStyle.Width(maxWidth).Render(""),
|
||||
help,
|
||||
baseStyle.Width(maxWidth).Render(""),
|
||||
)
|
||||
|
||||
return styles.BaseStyle.Padding(1, 2).
|
||||
return baseStyle.Padding(1, 2).
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderBackground(styles.Background).
|
||||
BorderForeground(styles.ForgroundDim).
|
||||
BorderBackground(t.Background()).
|
||||
BorderForeground(t.TextMuted()).
|
||||
Width(lipgloss.Width(content) + 4).
|
||||
Render(content)
|
||||
}
|
||||
|
||||
373
internal/tui/components/dialog/models.go
Normal file
373
internal/tui/components/dialog/models.go
Normal file
@@ -0,0 +1,373 @@
|
||||
package dialog
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/opencode-ai/opencode/internal/config"
|
||||
"github.com/opencode-ai/opencode/internal/llm/models"
|
||||
"github.com/opencode-ai/opencode/internal/tui/layout"
|
||||
"github.com/opencode-ai/opencode/internal/tui/styles"
|
||||
"github.com/opencode-ai/opencode/internal/tui/theme"
|
||||
"github.com/opencode-ai/opencode/internal/tui/util"
|
||||
)
|
||||
|
||||
const (
|
||||
numVisibleModels = 10
|
||||
maxDialogWidth = 40
|
||||
)
|
||||
|
||||
// ModelSelectedMsg is sent when a model is selected
|
||||
type ModelSelectedMsg struct {
|
||||
Model models.Model
|
||||
}
|
||||
|
||||
// CloseModelDialogMsg is sent when a model is selected
|
||||
type CloseModelDialogMsg struct{}
|
||||
|
||||
// ModelDialog interface for the model selection dialog
|
||||
type ModelDialog interface {
|
||||
tea.Model
|
||||
layout.Bindings
|
||||
}
|
||||
|
||||
type modelDialogCmp struct {
|
||||
models []models.Model
|
||||
provider models.ModelProvider
|
||||
availableProviders []models.ModelProvider
|
||||
|
||||
selectedIdx int
|
||||
width int
|
||||
height int
|
||||
scrollOffset int
|
||||
hScrollOffset int
|
||||
hScrollPossible bool
|
||||
}
|
||||
|
||||
type modelKeyMap struct {
|
||||
Up key.Binding
|
||||
Down key.Binding
|
||||
Left key.Binding
|
||||
Right key.Binding
|
||||
Enter key.Binding
|
||||
Escape key.Binding
|
||||
J key.Binding
|
||||
K key.Binding
|
||||
H key.Binding
|
||||
L key.Binding
|
||||
}
|
||||
|
||||
var modelKeys = modelKeyMap{
|
||||
Up: key.NewBinding(
|
||||
key.WithKeys("up"),
|
||||
key.WithHelp("↑", "previous model"),
|
||||
),
|
||||
Down: key.NewBinding(
|
||||
key.WithKeys("down"),
|
||||
key.WithHelp("↓", "next model"),
|
||||
),
|
||||
Left: key.NewBinding(
|
||||
key.WithKeys("left"),
|
||||
key.WithHelp("←", "scroll left"),
|
||||
),
|
||||
Right: key.NewBinding(
|
||||
key.WithKeys("right"),
|
||||
key.WithHelp("→", "scroll right"),
|
||||
),
|
||||
Enter: key.NewBinding(
|
||||
key.WithKeys("enter"),
|
||||
key.WithHelp("enter", "select model"),
|
||||
),
|
||||
Escape: key.NewBinding(
|
||||
key.WithKeys("esc"),
|
||||
key.WithHelp("esc", "close"),
|
||||
),
|
||||
J: key.NewBinding(
|
||||
key.WithKeys("j"),
|
||||
key.WithHelp("j", "next model"),
|
||||
),
|
||||
K: key.NewBinding(
|
||||
key.WithKeys("k"),
|
||||
key.WithHelp("k", "previous model"),
|
||||
),
|
||||
H: key.NewBinding(
|
||||
key.WithKeys("h"),
|
||||
key.WithHelp("h", "scroll left"),
|
||||
),
|
||||
L: key.NewBinding(
|
||||
key.WithKeys("l"),
|
||||
key.WithHelp("l", "scroll right"),
|
||||
),
|
||||
}
|
||||
|
||||
func (m *modelDialogCmp) Init() tea.Cmd {
|
||||
m.setupModels()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *modelDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch {
|
||||
case key.Matches(msg, modelKeys.Up) || key.Matches(msg, modelKeys.K):
|
||||
m.moveSelectionUp()
|
||||
case key.Matches(msg, modelKeys.Down) || key.Matches(msg, modelKeys.J):
|
||||
m.moveSelectionDown()
|
||||
case key.Matches(msg, modelKeys.Left) || key.Matches(msg, modelKeys.H):
|
||||
if m.hScrollPossible {
|
||||
m.switchProvider(-1)
|
||||
}
|
||||
case key.Matches(msg, modelKeys.Right) || key.Matches(msg, modelKeys.L):
|
||||
if m.hScrollPossible {
|
||||
m.switchProvider(1)
|
||||
}
|
||||
case key.Matches(msg, modelKeys.Enter):
|
||||
util.ReportInfo(fmt.Sprintf("selected model: %s", m.models[m.selectedIdx].Name))
|
||||
return m, util.CmdHandler(ModelSelectedMsg{Model: m.models[m.selectedIdx]})
|
||||
case key.Matches(msg, modelKeys.Escape):
|
||||
return m, util.CmdHandler(CloseModelDialogMsg{})
|
||||
}
|
||||
case tea.WindowSizeMsg:
|
||||
m.width = msg.Width
|
||||
m.height = msg.Height
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// moveSelectionUp moves the selection up or wraps to bottom
|
||||
func (m *modelDialogCmp) moveSelectionUp() {
|
||||
if m.selectedIdx > 0 {
|
||||
m.selectedIdx--
|
||||
} else {
|
||||
m.selectedIdx = len(m.models) - 1
|
||||
m.scrollOffset = max(0, len(m.models)-numVisibleModels)
|
||||
}
|
||||
|
||||
// Keep selection visible
|
||||
if m.selectedIdx < m.scrollOffset {
|
||||
m.scrollOffset = m.selectedIdx
|
||||
}
|
||||
}
|
||||
|
||||
// moveSelectionDown moves the selection down or wraps to top
|
||||
func (m *modelDialogCmp) moveSelectionDown() {
|
||||
if m.selectedIdx < len(m.models)-1 {
|
||||
m.selectedIdx++
|
||||
} else {
|
||||
m.selectedIdx = 0
|
||||
m.scrollOffset = 0
|
||||
}
|
||||
|
||||
// Keep selection visible
|
||||
if m.selectedIdx >= m.scrollOffset+numVisibleModels {
|
||||
m.scrollOffset = m.selectedIdx - (numVisibleModels - 1)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *modelDialogCmp) switchProvider(offset int) {
|
||||
newOffset := m.hScrollOffset + offset
|
||||
|
||||
// Ensure we stay within bounds
|
||||
if newOffset < 0 {
|
||||
newOffset = len(m.availableProviders) - 1
|
||||
}
|
||||
if newOffset >= len(m.availableProviders) {
|
||||
newOffset = 0
|
||||
}
|
||||
|
||||
m.hScrollOffset = newOffset
|
||||
m.provider = m.availableProviders[m.hScrollOffset]
|
||||
m.setupModelsForProvider(m.provider)
|
||||
}
|
||||
|
||||
func (m *modelDialogCmp) View() string {
|
||||
t := theme.CurrentTheme()
|
||||
baseStyle := styles.BaseStyle()
|
||||
|
||||
// Capitalize first letter of provider name
|
||||
providerName := strings.ToUpper(string(m.provider)[:1]) + string(m.provider[1:])
|
||||
title := baseStyle.
|
||||
Foreground(t.Primary()).
|
||||
Bold(true).
|
||||
Width(maxDialogWidth).
|
||||
Padding(0, 0, 1).
|
||||
Render(fmt.Sprintf("Select %s Model", providerName))
|
||||
|
||||
// Render visible models
|
||||
endIdx := min(m.scrollOffset+numVisibleModels, len(m.models))
|
||||
modelItems := make([]string, 0, endIdx-m.scrollOffset)
|
||||
|
||||
for i := m.scrollOffset; i < endIdx; i++ {
|
||||
itemStyle := baseStyle.Width(maxDialogWidth)
|
||||
if i == m.selectedIdx {
|
||||
itemStyle = itemStyle.Background(t.Primary()).
|
||||
Foreground(t.Background()).Bold(true)
|
||||
}
|
||||
modelItems = append(modelItems, itemStyle.Render(m.models[i].Name))
|
||||
}
|
||||
|
||||
scrollIndicator := m.getScrollIndicators(maxDialogWidth)
|
||||
|
||||
content := lipgloss.JoinVertical(
|
||||
lipgloss.Left,
|
||||
title,
|
||||
baseStyle.Width(maxDialogWidth).Render(lipgloss.JoinVertical(lipgloss.Left, modelItems...)),
|
||||
scrollIndicator,
|
||||
)
|
||||
|
||||
return baseStyle.Padding(1, 2).
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderBackground(t.Background()).
|
||||
BorderForeground(t.TextMuted()).
|
||||
Width(lipgloss.Width(content) + 4).
|
||||
Render(content)
|
||||
}
|
||||
|
||||
func (m *modelDialogCmp) getScrollIndicators(maxWidth int) string {
|
||||
var indicator string
|
||||
|
||||
if len(m.models) > numVisibleModels {
|
||||
if m.scrollOffset > 0 {
|
||||
indicator += "↑ "
|
||||
}
|
||||
if m.scrollOffset+numVisibleModels < len(m.models) {
|
||||
indicator += "↓ "
|
||||
}
|
||||
}
|
||||
|
||||
if m.hScrollPossible {
|
||||
if m.hScrollOffset > 0 {
|
||||
indicator = "← " + indicator
|
||||
}
|
||||
if m.hScrollOffset < len(m.availableProviders)-1 {
|
||||
indicator += "→"
|
||||
}
|
||||
}
|
||||
|
||||
if indicator == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
t := theme.CurrentTheme()
|
||||
baseStyle := styles.BaseStyle()
|
||||
|
||||
return baseStyle.
|
||||
Foreground(t.Primary()).
|
||||
Width(maxWidth).
|
||||
Align(lipgloss.Right).
|
||||
Bold(true).
|
||||
Render(indicator)
|
||||
}
|
||||
|
||||
func (m *modelDialogCmp) BindingKeys() []key.Binding {
|
||||
return layout.KeyMapToSlice(modelKeys)
|
||||
}
|
||||
|
||||
func (m *modelDialogCmp) setupModels() {
|
||||
cfg := config.Get()
|
||||
modelInfo := GetSelectedModel(cfg)
|
||||
m.availableProviders = getEnabledProviders(cfg)
|
||||
m.hScrollPossible = len(m.availableProviders) > 1
|
||||
|
||||
m.provider = modelInfo.Provider
|
||||
m.hScrollOffset = findProviderIndex(m.availableProviders, m.provider)
|
||||
|
||||
m.setupModelsForProvider(m.provider)
|
||||
}
|
||||
|
||||
func GetSelectedModel(cfg *config.Config) models.Model {
|
||||
|
||||
agentCfg := cfg.Agents[config.AgentCoder]
|
||||
selectedModelId := agentCfg.Model
|
||||
return models.SupportedModels[selectedModelId]
|
||||
}
|
||||
|
||||
func getEnabledProviders(cfg *config.Config) []models.ModelProvider {
|
||||
var providers []models.ModelProvider
|
||||
for providerId, provider := range cfg.Providers {
|
||||
if !provider.Disabled {
|
||||
providers = append(providers, providerId)
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by provider popularity
|
||||
slices.SortFunc(providers, func(a, b models.ModelProvider) int {
|
||||
rA := models.ProviderPopularity[a]
|
||||
rB := models.ProviderPopularity[b]
|
||||
|
||||
// models not included in popularity ranking default to last
|
||||
if rA == 0 {
|
||||
rA = 999
|
||||
}
|
||||
if rB == 0 {
|
||||
rB = 999
|
||||
}
|
||||
return rA - rB
|
||||
})
|
||||
return providers
|
||||
}
|
||||
|
||||
// findProviderIndex returns the index of the provider in the list, or -1 if not found
|
||||
func findProviderIndex(providers []models.ModelProvider, provider models.ModelProvider) int {
|
||||
for i, p := range providers {
|
||||
if p == provider {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func (m *modelDialogCmp) setupModelsForProvider(provider models.ModelProvider) {
|
||||
cfg := config.Get()
|
||||
agentCfg := cfg.Agents[config.AgentCoder]
|
||||
selectedModelId := agentCfg.Model
|
||||
|
||||
m.provider = provider
|
||||
m.models = getModelsForProvider(provider)
|
||||
m.selectedIdx = 0
|
||||
m.scrollOffset = 0
|
||||
|
||||
// Try to select the current model if it belongs to this provider
|
||||
if provider == models.SupportedModels[selectedModelId].Provider {
|
||||
for i, model := range m.models {
|
||||
if model.ID == selectedModelId {
|
||||
m.selectedIdx = i
|
||||
// Adjust scroll position to keep selected model visible
|
||||
if m.selectedIdx >= numVisibleModels {
|
||||
m.scrollOffset = m.selectedIdx - (numVisibleModels - 1)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getModelsForProvider(provider models.ModelProvider) []models.Model {
|
||||
var providerModels []models.Model
|
||||
for _, model := range models.SupportedModels {
|
||||
if model.Provider == provider {
|
||||
providerModels = append(providerModels, model)
|
||||
}
|
||||
}
|
||||
|
||||
// reverse alphabetical order (if llm naming was consistent latest would appear first)
|
||||
slices.SortFunc(providerModels, func(a, b models.Model) int {
|
||||
if a.Name > b.Name {
|
||||
return -1
|
||||
} else if a.Name < b.Name {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
|
||||
return providerModels
|
||||
}
|
||||
|
||||
func NewModelDialogCmp() ModelDialog {
|
||||
return &modelDialogCmp{}
|
||||
}
|
||||
@@ -2,19 +2,18 @@ package dialog
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
"github.com/charmbracelet/bubbles/viewport"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/glamour"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/kujtimiihoxha/opencode/internal/diff"
|
||||
"github.com/kujtimiihoxha/opencode/internal/llm/tools"
|
||||
"github.com/kujtimiihoxha/opencode/internal/permission"
|
||||
"github.com/kujtimiihoxha/opencode/internal/tui/layout"
|
||||
"github.com/kujtimiihoxha/opencode/internal/tui/styles"
|
||||
"github.com/kujtimiihoxha/opencode/internal/tui/util"
|
||||
"github.com/opencode-ai/opencode/internal/diff"
|
||||
"github.com/opencode-ai/opencode/internal/llm/tools"
|
||||
"github.com/opencode-ai/opencode/internal/permission"
|
||||
"github.com/opencode-ai/opencode/internal/tui/layout"
|
||||
"github.com/opencode-ai/opencode/internal/tui/styles"
|
||||
"github.com/opencode-ai/opencode/internal/tui/theme"
|
||||
"github.com/opencode-ai/opencode/internal/tui/util"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type PermissionAction string
|
||||
@@ -67,8 +66,8 @@ var permissionsKeys = permissionsMapping{
|
||||
key.WithHelp("a", "allow"),
|
||||
),
|
||||
AllowSession: key.NewBinding(
|
||||
key.WithKeys("A"),
|
||||
key.WithHelp("A", "allow for session"),
|
||||
key.WithKeys("s"),
|
||||
key.WithHelp("s", "allow for session"),
|
||||
),
|
||||
Deny: key.NewBinding(
|
||||
key.WithKeys("d"),
|
||||
@@ -149,29 +148,32 @@ func (p *permissionDialogCmp) selectCurrentOption() tea.Cmd {
|
||||
}
|
||||
|
||||
func (p *permissionDialogCmp) renderButtons() string {
|
||||
allowStyle := styles.BaseStyle
|
||||
allowSessionStyle := styles.BaseStyle
|
||||
denyStyle := styles.BaseStyle
|
||||
spacerStyle := styles.BaseStyle.Background(styles.Background)
|
||||
t := theme.CurrentTheme()
|
||||
baseStyle := styles.BaseStyle()
|
||||
|
||||
allowStyle := baseStyle
|
||||
allowSessionStyle := baseStyle
|
||||
denyStyle := baseStyle
|
||||
spacerStyle := baseStyle.Background(t.Background())
|
||||
|
||||
// Style the selected button
|
||||
switch p.selectedOption {
|
||||
case 0:
|
||||
allowStyle = allowStyle.Background(styles.PrimaryColor).Foreground(styles.Background)
|
||||
allowSessionStyle = allowSessionStyle.Background(styles.Background).Foreground(styles.PrimaryColor)
|
||||
denyStyle = denyStyle.Background(styles.Background).Foreground(styles.PrimaryColor)
|
||||
allowStyle = allowStyle.Background(t.Primary()).Foreground(t.Background())
|
||||
allowSessionStyle = allowSessionStyle.Background(t.Background()).Foreground(t.Primary())
|
||||
denyStyle = denyStyle.Background(t.Background()).Foreground(t.Primary())
|
||||
case 1:
|
||||
allowStyle = allowStyle.Background(styles.Background).Foreground(styles.PrimaryColor)
|
||||
allowSessionStyle = allowSessionStyle.Background(styles.PrimaryColor).Foreground(styles.Background)
|
||||
denyStyle = denyStyle.Background(styles.Background).Foreground(styles.PrimaryColor)
|
||||
allowStyle = allowStyle.Background(t.Background()).Foreground(t.Primary())
|
||||
allowSessionStyle = allowSessionStyle.Background(t.Primary()).Foreground(t.Background())
|
||||
denyStyle = denyStyle.Background(t.Background()).Foreground(t.Primary())
|
||||
case 2:
|
||||
allowStyle = allowStyle.Background(styles.Background).Foreground(styles.PrimaryColor)
|
||||
allowSessionStyle = allowSessionStyle.Background(styles.Background).Foreground(styles.PrimaryColor)
|
||||
denyStyle = denyStyle.Background(styles.PrimaryColor).Foreground(styles.Background)
|
||||
allowStyle = allowStyle.Background(t.Background()).Foreground(t.Primary())
|
||||
allowSessionStyle = allowSessionStyle.Background(t.Background()).Foreground(t.Primary())
|
||||
denyStyle = denyStyle.Background(t.Primary()).Foreground(t.Background())
|
||||
}
|
||||
|
||||
allowButton := allowStyle.Padding(0, 1).Render("Allow (a)")
|
||||
allowSessionButton := allowSessionStyle.Padding(0, 1).Render("Allow for session (A)")
|
||||
allowSessionButton := allowSessionStyle.Padding(0, 1).Render("Allow for session (s)")
|
||||
denyButton := denyStyle.Padding(0, 1).Render("Deny (d)")
|
||||
|
||||
content := lipgloss.JoinHorizontal(
|
||||
@@ -192,15 +194,18 @@ func (p *permissionDialogCmp) renderButtons() string {
|
||||
}
|
||||
|
||||
func (p *permissionDialogCmp) renderHeader() string {
|
||||
toolKey := styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render("Tool")
|
||||
toolValue := styles.BaseStyle.
|
||||
Foreground(styles.Forground).
|
||||
t := theme.CurrentTheme()
|
||||
baseStyle := styles.BaseStyle()
|
||||
|
||||
toolKey := baseStyle.Foreground(t.TextMuted()).Bold(true).Render("Tool")
|
||||
toolValue := baseStyle.
|
||||
Foreground(t.Text()).
|
||||
Width(p.width - lipgloss.Width(toolKey)).
|
||||
Render(fmt.Sprintf(": %s", p.permission.ToolName))
|
||||
|
||||
pathKey := styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render("Path")
|
||||
pathValue := styles.BaseStyle.
|
||||
Foreground(styles.Forground).
|
||||
pathKey := baseStyle.Foreground(t.TextMuted()).Bold(true).Render("Path")
|
||||
pathValue := baseStyle.
|
||||
Foreground(t.Text()).
|
||||
Width(p.width - lipgloss.Width(pathKey)).
|
||||
Render(fmt.Sprintf(": %s", p.permission.Path))
|
||||
|
||||
@@ -210,45 +215,45 @@ func (p *permissionDialogCmp) renderHeader() string {
|
||||
toolKey,
|
||||
toolValue,
|
||||
),
|
||||
styles.BaseStyle.Render(strings.Repeat(" ", p.width)),
|
||||
baseStyle.Render(strings.Repeat(" ", p.width)),
|
||||
lipgloss.JoinHorizontal(
|
||||
lipgloss.Left,
|
||||
pathKey,
|
||||
pathValue,
|
||||
),
|
||||
styles.BaseStyle.Render(strings.Repeat(" ", p.width)),
|
||||
baseStyle.Render(strings.Repeat(" ", p.width)),
|
||||
}
|
||||
|
||||
// Add tool-specific header information
|
||||
switch p.permission.ToolName {
|
||||
case tools.BashToolName:
|
||||
headerParts = append(headerParts, styles.BaseStyle.Foreground(styles.ForgroundDim).Width(p.width).Bold(true).Render("Command"))
|
||||
headerParts = append(headerParts, baseStyle.Foreground(t.TextMuted()).Width(p.width).Bold(true).Render("Command"))
|
||||
case tools.EditToolName:
|
||||
headerParts = append(headerParts, styles.BaseStyle.Foreground(styles.ForgroundDim).Width(p.width).Bold(true).Render("Diff"))
|
||||
headerParts = append(headerParts, baseStyle.Foreground(t.TextMuted()).Width(p.width).Bold(true).Render("Diff"))
|
||||
case tools.WriteToolName:
|
||||
headerParts = append(headerParts, styles.BaseStyle.Foreground(styles.ForgroundDim).Width(p.width).Bold(true).Render("Diff"))
|
||||
headerParts = append(headerParts, baseStyle.Foreground(t.TextMuted()).Width(p.width).Bold(true).Render("Diff"))
|
||||
case tools.FetchToolName:
|
||||
headerParts = append(headerParts, styles.BaseStyle.Foreground(styles.ForgroundDim).Width(p.width).Bold(true).Render("URL"))
|
||||
headerParts = append(headerParts, baseStyle.Foreground(t.TextMuted()).Width(p.width).Bold(true).Render("URL"))
|
||||
}
|
||||
|
||||
return lipgloss.NewStyle().Render(lipgloss.JoinVertical(lipgloss.Left, headerParts...))
|
||||
return lipgloss.NewStyle().Background(t.Background()).Render(lipgloss.JoinVertical(lipgloss.Left, headerParts...))
|
||||
}
|
||||
|
||||
func (p *permissionDialogCmp) renderBashContent() string {
|
||||
t := theme.CurrentTheme()
|
||||
baseStyle := styles.BaseStyle()
|
||||
|
||||
if pr, ok := p.permission.Params.(tools.BashPermissionsParams); ok {
|
||||
content := fmt.Sprintf("```bash\n%s\n```", pr.Command)
|
||||
|
||||
// Use the cache for markdown rendering
|
||||
renderedContent := p.GetOrSetMarkdown(p.permission.ID, func() (string, error) {
|
||||
r, _ := glamour.NewTermRenderer(
|
||||
glamour.WithStyles(styles.MarkdownTheme(true)),
|
||||
glamour.WithWordWrap(p.width-10),
|
||||
)
|
||||
r := styles.GetMarkdownRenderer(p.width-10)
|
||||
s, err := r.Render(content)
|
||||
return styles.ForceReplaceBackgroundWithLipgloss(s, styles.Background), err
|
||||
return styles.ForceReplaceBackgroundWithLipgloss(s, t.Background()), err
|
||||
})
|
||||
|
||||
finalContent := styles.BaseStyle.
|
||||
finalContent := baseStyle.
|
||||
Width(p.contentViewPort.Width).
|
||||
Render(renderedContent)
|
||||
p.contentViewPort.SetContent(finalContent)
|
||||
@@ -295,39 +300,45 @@ func (p *permissionDialogCmp) renderWriteContent() string {
|
||||
}
|
||||
|
||||
func (p *permissionDialogCmp) renderFetchContent() string {
|
||||
t := theme.CurrentTheme()
|
||||
baseStyle := styles.BaseStyle()
|
||||
|
||||
if pr, ok := p.permission.Params.(tools.FetchPermissionsParams); ok {
|
||||
content := fmt.Sprintf("```bash\n%s\n```", pr.URL)
|
||||
|
||||
// Use the cache for markdown rendering
|
||||
renderedContent := p.GetOrSetMarkdown(p.permission.ID, func() (string, error) {
|
||||
r, _ := glamour.NewTermRenderer(
|
||||
glamour.WithStyles(styles.MarkdownTheme(true)),
|
||||
glamour.WithWordWrap(p.width-10),
|
||||
)
|
||||
r := styles.GetMarkdownRenderer(p.width-10)
|
||||
s, err := r.Render(content)
|
||||
return styles.ForceReplaceBackgroundWithLipgloss(s, styles.Background), err
|
||||
return styles.ForceReplaceBackgroundWithLipgloss(s, t.Background()), err
|
||||
})
|
||||
|
||||
p.contentViewPort.SetContent(renderedContent)
|
||||
finalContent := baseStyle.
|
||||
Width(p.contentViewPort.Width).
|
||||
Render(renderedContent)
|
||||
p.contentViewPort.SetContent(finalContent)
|
||||
return p.styleViewport()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (p *permissionDialogCmp) renderDefaultContent() string {
|
||||
t := theme.CurrentTheme()
|
||||
baseStyle := styles.BaseStyle()
|
||||
|
||||
content := p.permission.Description
|
||||
|
||||
// Use the cache for markdown rendering
|
||||
renderedContent := p.GetOrSetMarkdown(p.permission.ID, func() (string, error) {
|
||||
r, _ := glamour.NewTermRenderer(
|
||||
glamour.WithStyles(styles.CatppuccinMarkdownStyle()),
|
||||
glamour.WithWordWrap(p.width-10),
|
||||
)
|
||||
r := styles.GetMarkdownRenderer(p.width-10)
|
||||
s, err := r.Render(content)
|
||||
return styles.ForceReplaceBackgroundWithLipgloss(s, styles.Background), err
|
||||
return styles.ForceReplaceBackgroundWithLipgloss(s, t.Background()), err
|
||||
})
|
||||
|
||||
p.contentViewPort.SetContent(renderedContent)
|
||||
finalContent := baseStyle.
|
||||
Width(p.contentViewPort.Width).
|
||||
Render(renderedContent)
|
||||
p.contentViewPort.SetContent(finalContent)
|
||||
|
||||
if renderedContent == "" {
|
||||
return ""
|
||||
@@ -337,17 +348,21 @@ func (p *permissionDialogCmp) renderDefaultContent() string {
|
||||
}
|
||||
|
||||
func (p *permissionDialogCmp) styleViewport() string {
|
||||
t := theme.CurrentTheme()
|
||||
contentStyle := lipgloss.NewStyle().
|
||||
Background(styles.Background)
|
||||
Background(t.Background())
|
||||
|
||||
return contentStyle.Render(p.contentViewPort.View())
|
||||
}
|
||||
|
||||
func (p *permissionDialogCmp) render() string {
|
||||
title := styles.BaseStyle.
|
||||
t := theme.CurrentTheme()
|
||||
baseStyle := styles.BaseStyle()
|
||||
|
||||
title := baseStyle.
|
||||
Bold(true).
|
||||
Width(p.width - 4).
|
||||
Foreground(styles.PrimaryColor).
|
||||
Foreground(t.Primary()).
|
||||
Render("Permission Required")
|
||||
// Render header
|
||||
headerContent := p.renderHeader()
|
||||
@@ -375,25 +390,21 @@ func (p *permissionDialogCmp) render() string {
|
||||
contentFinal = p.renderDefaultContent()
|
||||
}
|
||||
|
||||
// Add help text
|
||||
helpText := styles.BaseStyle.Width(p.width - 4).Padding(0, 1).Foreground(styles.ForgroundDim).Render("←/→/tab: switch options a: allow A: allow for session d: deny enter/space: confirm")
|
||||
|
||||
content := lipgloss.JoinVertical(
|
||||
lipgloss.Top,
|
||||
title,
|
||||
styles.BaseStyle.Render(strings.Repeat(" ", lipgloss.Width(title))),
|
||||
baseStyle.Render(strings.Repeat(" ", lipgloss.Width(title))),
|
||||
headerContent,
|
||||
contentFinal,
|
||||
buttons,
|
||||
styles.BaseStyle.Render(strings.Repeat(" ", p.width - 4)),
|
||||
helpText,
|
||||
baseStyle.Render(strings.Repeat(" ", p.width-4)),
|
||||
)
|
||||
|
||||
return styles.BaseStyle.
|
||||
return baseStyle.
|
||||
Padding(1, 0, 0, 1).
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderBackground(styles.Background).
|
||||
BorderForeground(styles.ForgroundDim).
|
||||
BorderBackground(t.Background()).
|
||||
BorderForeground(t.TextMuted()).
|
||||
Width(p.width).
|
||||
Height(p.height).
|
||||
Render(
|
||||
|
||||
@@ -6,9 +6,10 @@ import (
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/kujtimiihoxha/opencode/internal/tui/layout"
|
||||
"github.com/kujtimiihoxha/opencode/internal/tui/styles"
|
||||
"github.com/kujtimiihoxha/opencode/internal/tui/util"
|
||||
"github.com/opencode-ai/opencode/internal/tui/layout"
|
||||
"github.com/opencode-ai/opencode/internal/tui/styles"
|
||||
"github.com/opencode-ai/opencode/internal/tui/theme"
|
||||
"github.com/opencode-ai/opencode/internal/tui/util"
|
||||
)
|
||||
|
||||
const question = "Are you sure you want to quit?"
|
||||
@@ -81,16 +82,19 @@ func (q *quitDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
|
||||
func (q *quitDialogCmp) View() string {
|
||||
yesStyle := styles.BaseStyle
|
||||
noStyle := styles.BaseStyle
|
||||
spacerStyle := styles.BaseStyle.Background(styles.Background)
|
||||
t := theme.CurrentTheme()
|
||||
baseStyle := styles.BaseStyle()
|
||||
|
||||
yesStyle := baseStyle
|
||||
noStyle := baseStyle
|
||||
spacerStyle := baseStyle.Background(t.Background())
|
||||
|
||||
if q.selectedNo {
|
||||
noStyle = noStyle.Background(styles.PrimaryColor).Foreground(styles.Background)
|
||||
yesStyle = yesStyle.Background(styles.Background).Foreground(styles.PrimaryColor)
|
||||
noStyle = noStyle.Background(t.Primary()).Foreground(t.Background())
|
||||
yesStyle = yesStyle.Background(t.Background()).Foreground(t.Primary())
|
||||
} else {
|
||||
yesStyle = yesStyle.Background(styles.PrimaryColor).Foreground(styles.Background)
|
||||
noStyle = noStyle.Background(styles.Background).Foreground(styles.PrimaryColor)
|
||||
yesStyle = yesStyle.Background(t.Primary()).Foreground(t.Background())
|
||||
noStyle = noStyle.Background(t.Background()).Foreground(t.Primary())
|
||||
}
|
||||
|
||||
yesButton := yesStyle.Padding(0, 1).Render("Yes")
|
||||
@@ -104,7 +108,7 @@ func (q *quitDialogCmp) View() string {
|
||||
buttons = spacerStyle.Render(strings.Repeat(" ", remainingWidth)) + buttons
|
||||
}
|
||||
|
||||
content := styles.BaseStyle.Render(
|
||||
content := baseStyle.Render(
|
||||
lipgloss.JoinVertical(
|
||||
lipgloss.Center,
|
||||
question,
|
||||
@@ -113,10 +117,10 @@ func (q *quitDialogCmp) View() string {
|
||||
),
|
||||
)
|
||||
|
||||
return styles.BaseStyle.Padding(1, 2).
|
||||
return baseStyle.Padding(1, 2).
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderBackground(styles.Background).
|
||||
BorderForeground(styles.ForgroundDim).
|
||||
BorderBackground(t.Background()).
|
||||
BorderForeground(t.TextMuted()).
|
||||
Width(lipgloss.Width(content) + 4).
|
||||
Render(content)
|
||||
}
|
||||
|
||||
@@ -4,10 +4,11 @@ import (
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/kujtimiihoxha/opencode/internal/session"
|
||||
"github.com/kujtimiihoxha/opencode/internal/tui/layout"
|
||||
"github.com/kujtimiihoxha/opencode/internal/tui/styles"
|
||||
"github.com/kujtimiihoxha/opencode/internal/tui/util"
|
||||
"github.com/opencode-ai/opencode/internal/session"
|
||||
"github.com/opencode-ai/opencode/internal/tui/layout"
|
||||
"github.com/opencode-ai/opencode/internal/tui/styles"
|
||||
"github.com/opencode-ai/opencode/internal/tui/theme"
|
||||
"github.com/opencode-ai/opencode/internal/tui/util"
|
||||
)
|
||||
|
||||
// SessionSelectedMsg is sent when a session is selected
|
||||
@@ -105,11 +106,14 @@ func (s *sessionDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
|
||||
func (s *sessionDialogCmp) View() string {
|
||||
t := theme.CurrentTheme()
|
||||
baseStyle := styles.BaseStyle()
|
||||
|
||||
if len(s.sessions) == 0 {
|
||||
return styles.BaseStyle.Padding(1, 2).
|
||||
return baseStyle.Padding(1, 2).
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderBackground(styles.Background).
|
||||
BorderForeground(styles.ForgroundDim).
|
||||
BorderBackground(t.Background()).
|
||||
BorderForeground(t.TextMuted()).
|
||||
Width(40).
|
||||
Render("No sessions available")
|
||||
}
|
||||
@@ -122,6 +126,8 @@ func (s *sessionDialogCmp) View() string {
|
||||
}
|
||||
}
|
||||
|
||||
maxWidth = max(30, min(maxWidth, s.width-15)) // Limit width to avoid overflow
|
||||
|
||||
// Limit height to avoid taking up too much screen space
|
||||
maxVisibleSessions := min(10, len(s.sessions))
|
||||
|
||||
@@ -144,20 +150,20 @@ func (s *sessionDialogCmp) View() string {
|
||||
|
||||
for i := startIdx; i < endIdx; i++ {
|
||||
sess := s.sessions[i]
|
||||
itemStyle := styles.BaseStyle.Width(maxWidth)
|
||||
itemStyle := baseStyle.Width(maxWidth)
|
||||
|
||||
if i == s.selectedIdx {
|
||||
itemStyle = itemStyle.
|
||||
Background(styles.PrimaryColor).
|
||||
Foreground(styles.Background).
|
||||
Background(t.Primary()).
|
||||
Foreground(t.Background()).
|
||||
Bold(true)
|
||||
}
|
||||
|
||||
sessionItems = append(sessionItems, itemStyle.Padding(0, 1).Render(sess.Title))
|
||||
}
|
||||
|
||||
title := styles.BaseStyle.
|
||||
Foreground(styles.PrimaryColor).
|
||||
title := baseStyle.
|
||||
Foreground(t.Primary()).
|
||||
Bold(true).
|
||||
Width(maxWidth).
|
||||
Padding(0, 1).
|
||||
@@ -166,16 +172,15 @@ func (s *sessionDialogCmp) View() string {
|
||||
content := lipgloss.JoinVertical(
|
||||
lipgloss.Left,
|
||||
title,
|
||||
styles.BaseStyle.Width(maxWidth).Render(""),
|
||||
styles.BaseStyle.Width(maxWidth).Render(lipgloss.JoinVertical(lipgloss.Left, sessionItems...)),
|
||||
styles.BaseStyle.Width(maxWidth).Render(""),
|
||||
styles.BaseStyle.Width(maxWidth).Padding(0, 1).Foreground(styles.ForgroundDim).Render("↑/k: up ↓/j: down enter: select esc: cancel"),
|
||||
baseStyle.Width(maxWidth).Render(""),
|
||||
baseStyle.Width(maxWidth).Render(lipgloss.JoinVertical(lipgloss.Left, sessionItems...)),
|
||||
baseStyle.Width(maxWidth).Render(""),
|
||||
)
|
||||
|
||||
return styles.BaseStyle.Padding(1, 2).
|
||||
return baseStyle.Padding(1, 2).
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderBackground(styles.Background).
|
||||
BorderForeground(styles.ForgroundDim).
|
||||
BorderBackground(t.Background()).
|
||||
BorderForeground(t.TextMuted()).
|
||||
Width(lipgloss.Width(content) + 4).
|
||||
Render(content)
|
||||
}
|
||||
@@ -223,4 +228,3 @@ func NewSessionDialogCmp() SessionDialog {
|
||||
selectedSessionID: "",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
198
internal/tui/components/dialog/theme.go
Normal file
198
internal/tui/components/dialog/theme.go
Normal file
@@ -0,0 +1,198 @@
|
||||
package dialog
|
||||
|
||||
import (
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/opencode-ai/opencode/internal/tui/layout"
|
||||
"github.com/opencode-ai/opencode/internal/tui/styles"
|
||||
"github.com/opencode-ai/opencode/internal/tui/theme"
|
||||
"github.com/opencode-ai/opencode/internal/tui/util"
|
||||
)
|
||||
|
||||
// ThemeChangedMsg is sent when the theme is changed
|
||||
type ThemeChangedMsg struct {
|
||||
ThemeName string
|
||||
}
|
||||
|
||||
// CloseThemeDialogMsg is sent when the theme dialog is closed
|
||||
type CloseThemeDialogMsg struct{}
|
||||
|
||||
// ThemeDialog interface for the theme switching dialog
|
||||
type ThemeDialog interface {
|
||||
tea.Model
|
||||
layout.Bindings
|
||||
}
|
||||
|
||||
type themeDialogCmp struct {
|
||||
themes []string
|
||||
selectedIdx int
|
||||
width int
|
||||
height int
|
||||
currentTheme string
|
||||
}
|
||||
|
||||
type themeKeyMap struct {
|
||||
Up key.Binding
|
||||
Down key.Binding
|
||||
Enter key.Binding
|
||||
Escape key.Binding
|
||||
J key.Binding
|
||||
K key.Binding
|
||||
}
|
||||
|
||||
var themeKeys = themeKeyMap{
|
||||
Up: key.NewBinding(
|
||||
key.WithKeys("up"),
|
||||
key.WithHelp("↑", "previous theme"),
|
||||
),
|
||||
Down: key.NewBinding(
|
||||
key.WithKeys("down"),
|
||||
key.WithHelp("↓", "next theme"),
|
||||
),
|
||||
Enter: key.NewBinding(
|
||||
key.WithKeys("enter"),
|
||||
key.WithHelp("enter", "select theme"),
|
||||
),
|
||||
Escape: key.NewBinding(
|
||||
key.WithKeys("esc"),
|
||||
key.WithHelp("esc", "close"),
|
||||
),
|
||||
J: key.NewBinding(
|
||||
key.WithKeys("j"),
|
||||
key.WithHelp("j", "next theme"),
|
||||
),
|
||||
K: key.NewBinding(
|
||||
key.WithKeys("k"),
|
||||
key.WithHelp("k", "previous theme"),
|
||||
),
|
||||
}
|
||||
|
||||
func (t *themeDialogCmp) Init() tea.Cmd {
|
||||
// Load available themes and update selectedIdx based on current theme
|
||||
t.themes = theme.AvailableThemes()
|
||||
t.currentTheme = theme.CurrentThemeName()
|
||||
|
||||
// Find the current theme in the list
|
||||
for i, name := range t.themes {
|
||||
if name == t.currentTheme {
|
||||
t.selectedIdx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *themeDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch {
|
||||
case key.Matches(msg, themeKeys.Up) || key.Matches(msg, themeKeys.K):
|
||||
if t.selectedIdx > 0 {
|
||||
t.selectedIdx--
|
||||
}
|
||||
return t, nil
|
||||
case key.Matches(msg, themeKeys.Down) || key.Matches(msg, themeKeys.J):
|
||||
if t.selectedIdx < len(t.themes)-1 {
|
||||
t.selectedIdx++
|
||||
}
|
||||
return t, nil
|
||||
case key.Matches(msg, themeKeys.Enter):
|
||||
if len(t.themes) > 0 {
|
||||
previousTheme := theme.CurrentThemeName()
|
||||
selectedTheme := t.themes[t.selectedIdx]
|
||||
if previousTheme == selectedTheme {
|
||||
return t, util.CmdHandler(CloseThemeDialogMsg{})
|
||||
}
|
||||
if err := theme.SetTheme(selectedTheme); err != nil {
|
||||
return t, util.ReportError(err)
|
||||
}
|
||||
return t, util.CmdHandler(ThemeChangedMsg{
|
||||
ThemeName: selectedTheme,
|
||||
})
|
||||
}
|
||||
case key.Matches(msg, themeKeys.Escape):
|
||||
return t, util.CmdHandler(CloseThemeDialogMsg{})
|
||||
}
|
||||
case tea.WindowSizeMsg:
|
||||
t.width = msg.Width
|
||||
t.height = msg.Height
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func (t *themeDialogCmp) View() string {
|
||||
currentTheme := theme.CurrentTheme()
|
||||
baseStyle := styles.BaseStyle()
|
||||
|
||||
if len(t.themes) == 0 {
|
||||
return baseStyle.Padding(1, 2).
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderBackground(currentTheme.Background()).
|
||||
BorderForeground(currentTheme.TextMuted()).
|
||||
Width(40).
|
||||
Render("No themes available")
|
||||
}
|
||||
|
||||
// Calculate max width needed for theme names
|
||||
maxWidth := 40 // Minimum width
|
||||
for _, themeName := range t.themes {
|
||||
if len(themeName) > maxWidth-4 { // Account for padding
|
||||
maxWidth = len(themeName) + 4
|
||||
}
|
||||
}
|
||||
|
||||
maxWidth = max(30, min(maxWidth, t.width-15)) // Limit width to avoid overflow
|
||||
|
||||
// Build the theme list
|
||||
themeItems := make([]string, 0, len(t.themes))
|
||||
for i, themeName := range t.themes {
|
||||
itemStyle := baseStyle.Width(maxWidth)
|
||||
|
||||
if i == t.selectedIdx {
|
||||
itemStyle = itemStyle.
|
||||
Background(currentTheme.Primary()).
|
||||
Foreground(currentTheme.Background()).
|
||||
Bold(true)
|
||||
}
|
||||
|
||||
themeItems = append(themeItems, itemStyle.Padding(0, 1).Render(themeName))
|
||||
}
|
||||
|
||||
title := baseStyle.
|
||||
Foreground(currentTheme.Primary()).
|
||||
Bold(true).
|
||||
Width(maxWidth).
|
||||
Padding(0, 1).
|
||||
Render("Select Theme")
|
||||
|
||||
content := lipgloss.JoinVertical(
|
||||
lipgloss.Left,
|
||||
title,
|
||||
baseStyle.Width(maxWidth).Render(""),
|
||||
baseStyle.Width(maxWidth).Render(lipgloss.JoinVertical(lipgloss.Left, themeItems...)),
|
||||
baseStyle.Width(maxWidth).Render(""),
|
||||
)
|
||||
|
||||
return baseStyle.Padding(1, 2).
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderBackground(currentTheme.Background()).
|
||||
BorderForeground(currentTheme.TextMuted()).
|
||||
Width(lipgloss.Width(content) + 4).
|
||||
Render(content)
|
||||
}
|
||||
|
||||
func (t *themeDialogCmp) BindingKeys() []key.Binding {
|
||||
return layout.KeyMapToSlice(themeKeys)
|
||||
}
|
||||
|
||||
// NewThemeDialogCmp creates a new theme switching dialog
|
||||
func NewThemeDialogCmp() ThemeDialog {
|
||||
return &themeDialogCmp{
|
||||
themes: []string{},
|
||||
selectedIdx: 0,
|
||||
currentTheme: "",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,9 +9,10 @@ import (
|
||||
"github.com/charmbracelet/bubbles/viewport"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/kujtimiihoxha/opencode/internal/logging"
|
||||
"github.com/kujtimiihoxha/opencode/internal/tui/layout"
|
||||
"github.com/kujtimiihoxha/opencode/internal/tui/styles"
|
||||
"github.com/opencode-ai/opencode/internal/logging"
|
||||
"github.com/opencode-ai/opencode/internal/tui/layout"
|
||||
"github.com/opencode-ai/opencode/internal/tui/styles"
|
||||
"github.com/opencode-ai/opencode/internal/tui/theme"
|
||||
)
|
||||
|
||||
type DetailComponent interface {
|
||||
@@ -49,9 +50,10 @@ func (i *detailCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
|
||||
func (i *detailCmp) updateContent() {
|
||||
var content strings.Builder
|
||||
t := theme.CurrentTheme()
|
||||
|
||||
// Format the header with timestamp and level
|
||||
timeStyle := lipgloss.NewStyle().Foreground(styles.SubText0)
|
||||
timeStyle := lipgloss.NewStyle().Foreground(t.TextMuted())
|
||||
levelStyle := getLevelStyle(i.currentLog.Level)
|
||||
|
||||
header := lipgloss.JoinHorizontal(
|
||||
@@ -65,7 +67,7 @@ func (i *detailCmp) updateContent() {
|
||||
content.WriteString("\n\n")
|
||||
|
||||
// Message with styling
|
||||
messageStyle := lipgloss.NewStyle().Bold(true).Foreground(styles.Text)
|
||||
messageStyle := lipgloss.NewStyle().Bold(true).Foreground(t.Text())
|
||||
content.WriteString(messageStyle.Render("Message:"))
|
||||
content.WriteString("\n")
|
||||
content.WriteString(lipgloss.NewStyle().Padding(0, 2).Render(i.currentLog.Message))
|
||||
@@ -73,13 +75,13 @@ func (i *detailCmp) updateContent() {
|
||||
|
||||
// Attributes section
|
||||
if len(i.currentLog.Attributes) > 0 {
|
||||
attrHeaderStyle := lipgloss.NewStyle().Bold(true).Foreground(styles.Text)
|
||||
attrHeaderStyle := lipgloss.NewStyle().Bold(true).Foreground(t.Text())
|
||||
content.WriteString(attrHeaderStyle.Render("Attributes:"))
|
||||
content.WriteString("\n")
|
||||
|
||||
// Create a table-like display for attributes
|
||||
keyStyle := lipgloss.NewStyle().Foreground(styles.Primary).Bold(true)
|
||||
valueStyle := lipgloss.NewStyle().Foreground(styles.Text)
|
||||
keyStyle := lipgloss.NewStyle().Foreground(t.Primary()).Bold(true)
|
||||
valueStyle := lipgloss.NewStyle().Foreground(t.Text())
|
||||
|
||||
for _, attr := range i.currentLog.Attributes {
|
||||
attrLine := fmt.Sprintf("%s: %s",
|
||||
@@ -96,23 +98,25 @@ func (i *detailCmp) updateContent() {
|
||||
|
||||
func getLevelStyle(level string) lipgloss.Style {
|
||||
style := lipgloss.NewStyle().Bold(true)
|
||||
|
||||
t := theme.CurrentTheme()
|
||||
|
||||
switch strings.ToLower(level) {
|
||||
case "info":
|
||||
return style.Foreground(styles.Blue)
|
||||
return style.Foreground(t.Info())
|
||||
case "warn", "warning":
|
||||
return style.Foreground(styles.Warning)
|
||||
return style.Foreground(t.Warning())
|
||||
case "error", "err":
|
||||
return style.Foreground(styles.Error)
|
||||
return style.Foreground(t.Error())
|
||||
case "debug":
|
||||
return style.Foreground(styles.Green)
|
||||
return style.Foreground(t.Success())
|
||||
default:
|
||||
return style.Foreground(styles.Text)
|
||||
return style.Foreground(t.Text())
|
||||
}
|
||||
}
|
||||
|
||||
func (i *detailCmp) View() string {
|
||||
return styles.ForceReplaceBackgroundWithLipgloss(i.viewport.View(), styles.Background)
|
||||
t := theme.CurrentTheme()
|
||||
return styles.ForceReplaceBackgroundWithLipgloss(i.viewport.View(), t.Background())
|
||||
}
|
||||
|
||||
func (i *detailCmp) GetSize() (int, int) {
|
||||
|
||||
@@ -7,11 +7,12 @@ import (
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
"github.com/charmbracelet/bubbles/table"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/kujtimiihoxha/opencode/internal/logging"
|
||||
"github.com/kujtimiihoxha/opencode/internal/pubsub"
|
||||
"github.com/kujtimiihoxha/opencode/internal/tui/layout"
|
||||
"github.com/kujtimiihoxha/opencode/internal/tui/styles"
|
||||
"github.com/kujtimiihoxha/opencode/internal/tui/util"
|
||||
"github.com/opencode-ai/opencode/internal/logging"
|
||||
"github.com/opencode-ai/opencode/internal/pubsub"
|
||||
"github.com/opencode-ai/opencode/internal/tui/layout"
|
||||
"github.com/opencode-ai/opencode/internal/tui/styles"
|
||||
"github.com/opencode-ai/opencode/internal/tui/theme"
|
||||
"github.com/opencode-ai/opencode/internal/tui/util"
|
||||
)
|
||||
|
||||
type TableComponent interface {
|
||||
@@ -61,7 +62,11 @@ func (i *tableCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
|
||||
func (i *tableCmp) View() string {
|
||||
return styles.ForceReplaceBackgroundWithLipgloss(i.table.View(), styles.Background)
|
||||
t := theme.CurrentTheme()
|
||||
defaultStyles := table.DefaultStyles()
|
||||
defaultStyles.Selected = defaultStyles.Selected.Foreground(t.Primary())
|
||||
i.table.SetStyles(defaultStyles)
|
||||
return styles.ForceReplaceBackgroundWithLipgloss(i.table.View(), t.Background())
|
||||
}
|
||||
|
||||
func (i *tableCmp) GetSize() (int, int) {
|
||||
@@ -121,11 +126,9 @@ func NewLogsTable() TableComponent {
|
||||
{Title: "Message", Width: 10},
|
||||
{Title: "Attributes", Width: 10},
|
||||
}
|
||||
defaultStyles := table.DefaultStyles()
|
||||
defaultStyles.Selected = defaultStyles.Selected.Foreground(styles.Primary)
|
||||
|
||||
tableModel := table.New(
|
||||
table.WithColumns(columns),
|
||||
table.WithStyles(defaultStyles),
|
||||
)
|
||||
tableModel.Focus()
|
||||
return &tableCmp{
|
||||
|
||||
72
internal/tui/image/images.go
Normal file
72
internal/tui/image/images.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package image
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/disintegration/imaging"
|
||||
"github.com/lucasb-eyer/go-colorful"
|
||||
)
|
||||
|
||||
func ValidateFileSize(filePath string, sizeLimit int64) (bool, error) {
|
||||
fileInfo, err := os.Stat(filePath)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("error getting file info: %w", err)
|
||||
}
|
||||
|
||||
if fileInfo.Size() > sizeLimit {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func ToString(width int, img image.Image) string {
|
||||
img = imaging.Resize(img, width, 0, imaging.Lanczos)
|
||||
b := img.Bounds()
|
||||
imageWidth := b.Max.X
|
||||
h := b.Max.Y
|
||||
str := strings.Builder{}
|
||||
|
||||
for heightCounter := 0; heightCounter < h; heightCounter += 2 {
|
||||
for x := range imageWidth {
|
||||
c1, _ := colorful.MakeColor(img.At(x, heightCounter))
|
||||
color1 := lipgloss.Color(c1.Hex())
|
||||
|
||||
var color2 lipgloss.Color
|
||||
if heightCounter+1 < h {
|
||||
c2, _ := colorful.MakeColor(img.At(x, heightCounter+1))
|
||||
color2 = lipgloss.Color(c2.Hex())
|
||||
} else {
|
||||
color2 = color1
|
||||
}
|
||||
|
||||
str.WriteString(lipgloss.NewStyle().Foreground(color1).
|
||||
Background(color2).Render("▀"))
|
||||
}
|
||||
|
||||
str.WriteString("\n")
|
||||
}
|
||||
|
||||
return str.String()
|
||||
}
|
||||
|
||||
func ImagePreview(width int, filename string) (string, error) {
|
||||
imageContent, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer imageContent.Close()
|
||||
|
||||
img, _, err := image.Decode(imageContent)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
imageString := ToString(width, img)
|
||||
|
||||
return imageString, nil
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/kujtimiihoxha/opencode/internal/tui/styles"
|
||||
"github.com/opencode-ai/opencode/internal/tui/theme"
|
||||
)
|
||||
|
||||
type Container interface {
|
||||
@@ -29,9 +29,6 @@ type container struct {
|
||||
borderBottom bool
|
||||
borderLeft bool
|
||||
borderStyle lipgloss.Border
|
||||
borderColor lipgloss.TerminalColor
|
||||
|
||||
backgroundColor lipgloss.TerminalColor
|
||||
}
|
||||
|
||||
func (c *container) Init() tea.Cmd {
|
||||
@@ -45,13 +42,12 @@ func (c *container) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
|
||||
func (c *container) View() string {
|
||||
t := theme.CurrentTheme()
|
||||
style := lipgloss.NewStyle()
|
||||
width := c.width
|
||||
height := c.height
|
||||
// Apply background color if specified
|
||||
if c.backgroundColor != nil {
|
||||
style = style.Background(c.backgroundColor)
|
||||
}
|
||||
|
||||
style = style.Background(t.Background())
|
||||
|
||||
// Apply border if any side is enabled
|
||||
if c.borderTop || c.borderRight || c.borderBottom || c.borderLeft {
|
||||
@@ -69,11 +65,7 @@ func (c *container) View() string {
|
||||
width--
|
||||
}
|
||||
style = style.Border(c.borderStyle, c.borderTop, c.borderRight, c.borderBottom, c.borderLeft)
|
||||
|
||||
// Apply border color if specified
|
||||
if c.borderColor != nil {
|
||||
style = style.BorderBackground(c.backgroundColor).BorderForeground(c.borderColor)
|
||||
}
|
||||
style = style.BorderBackground(t.Background()).BorderForeground(t.BorderNormal())
|
||||
}
|
||||
style = style.
|
||||
Width(width).
|
||||
@@ -132,11 +124,10 @@ func (c *container) BindingKeys() []key.Binding {
|
||||
type ContainerOption func(*container)
|
||||
|
||||
func NewContainer(content tea.Model, options ...ContainerOption) Container {
|
||||
|
||||
c := &container{
|
||||
content: content,
|
||||
borderColor: styles.BorderColor,
|
||||
borderStyle: lipgloss.NormalBorder(),
|
||||
backgroundColor: styles.Background,
|
||||
content: content,
|
||||
borderStyle: lipgloss.NormalBorder(),
|
||||
}
|
||||
|
||||
for _, option := range options {
|
||||
@@ -201,12 +192,6 @@ func WithBorderStyle(style lipgloss.Border) ContainerOption {
|
||||
}
|
||||
}
|
||||
|
||||
func WithBorderColor(color lipgloss.TerminalColor) ContainerOption {
|
||||
return func(c *container) {
|
||||
c.borderColor = color
|
||||
}
|
||||
}
|
||||
|
||||
func WithRoundedBorder() ContainerOption {
|
||||
return WithBorderStyle(lipgloss.RoundedBorder())
|
||||
}
|
||||
@@ -218,9 +203,3 @@ func WithThickBorder() ContainerOption {
|
||||
func WithDoubleBorder() ContainerOption {
|
||||
return WithBorderStyle(lipgloss.DoubleBorder())
|
||||
}
|
||||
|
||||
func WithBackgroundColor(color lipgloss.TerminalColor) ContainerOption {
|
||||
return func(c *container) {
|
||||
c.backgroundColor = color
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
package layout
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/kujtimiihoxha/opencode/internal/tui/styles"
|
||||
"github.com/kujtimiihoxha/opencode/internal/tui/util"
|
||||
"github.com/mattn/go-runewidth"
|
||||
chAnsi "github.com/charmbracelet/x/ansi"
|
||||
"github.com/muesli/ansi"
|
||||
"github.com/muesli/reflow/truncate"
|
||||
"github.com/muesli/termenv"
|
||||
"github.com/opencode-ai/opencode/internal/tui/styles"
|
||||
"github.com/opencode-ai/opencode/internal/tui/theme"
|
||||
"github.com/opencode-ai/opencode/internal/tui/util"
|
||||
)
|
||||
|
||||
// Most of this code is borrowed from
|
||||
@@ -44,12 +44,15 @@ func PlaceOverlay(
|
||||
fgHeight := len(fgLines)
|
||||
|
||||
if shadow {
|
||||
t := theme.CurrentTheme()
|
||||
baseStyle := styles.BaseStyle()
|
||||
|
||||
var shadowbg string = ""
|
||||
shadowchar := lipgloss.NewStyle().
|
||||
Background(styles.BackgroundDarker).
|
||||
Foreground(styles.Background).
|
||||
Background(t.BackgroundDarker()).
|
||||
Foreground(t.Background()).
|
||||
Render("░")
|
||||
bgchar := styles.BaseStyle.Render(" ")
|
||||
bgchar := baseStyle.Render(" ")
|
||||
for i := 0; i <= fgHeight; i++ {
|
||||
if i == 0 {
|
||||
shadowbg += bgchar + strings.Repeat(bgchar, fgWidth) + "\n"
|
||||
@@ -117,42 +120,7 @@ func PlaceOverlay(
|
||||
// cutLeft cuts printable characters from the left.
|
||||
// This function is heavily based on muesli's ansi and truncate packages.
|
||||
func cutLeft(s string, cutWidth int) string {
|
||||
var (
|
||||
pos int
|
||||
isAnsi bool
|
||||
ab bytes.Buffer
|
||||
b bytes.Buffer
|
||||
)
|
||||
for _, c := range s {
|
||||
var w int
|
||||
if c == ansi.Marker || isAnsi {
|
||||
isAnsi = true
|
||||
ab.WriteRune(c)
|
||||
if ansi.IsTerminator(c) {
|
||||
isAnsi = false
|
||||
if bytes.HasSuffix(ab.Bytes(), []byte("[0m")) {
|
||||
ab.Reset()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
w = runewidth.RuneWidth(c)
|
||||
}
|
||||
|
||||
if pos >= cutWidth {
|
||||
if b.Len() == 0 {
|
||||
if ab.Len() > 0 {
|
||||
b.Write(ab.Bytes())
|
||||
}
|
||||
if pos-cutWidth > 1 {
|
||||
b.WriteByte(' ')
|
||||
continue
|
||||
}
|
||||
}
|
||||
b.WriteRune(c)
|
||||
}
|
||||
pos += w
|
||||
}
|
||||
return b.String()
|
||||
return chAnsi.Cut(s, cutWidth, lipgloss.Width(s))
|
||||
}
|
||||
|
||||
func max(a, b int) int {
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/kujtimiihoxha/opencode/internal/tui/styles"
|
||||
"github.com/opencode-ai/opencode/internal/tui/theme"
|
||||
)
|
||||
|
||||
type SplitPaneLayout interface {
|
||||
@@ -29,8 +29,6 @@ type splitPaneLayout struct {
|
||||
rightPanel Container
|
||||
leftPanel Container
|
||||
bottomPanel Container
|
||||
|
||||
backgroundColor lipgloss.TerminalColor
|
||||
}
|
||||
|
||||
type SplitPaneOption func(*splitPaneLayout)
|
||||
@@ -113,11 +111,13 @@ func (s *splitPaneLayout) View() string {
|
||||
finalView = topSection
|
||||
}
|
||||
|
||||
if s.backgroundColor != nil && finalView != "" {
|
||||
if finalView != "" {
|
||||
t := theme.CurrentTheme()
|
||||
|
||||
style := lipgloss.NewStyle().
|
||||
Width(s.width).
|
||||
Height(s.height).
|
||||
Background(s.backgroundColor)
|
||||
Background(t.Background())
|
||||
|
||||
return style.Render(finalView)
|
||||
}
|
||||
@@ -241,10 +241,10 @@ func (s *splitPaneLayout) BindingKeys() []key.Binding {
|
||||
}
|
||||
|
||||
func NewSplitPane(options ...SplitPaneOption) SplitPaneLayout {
|
||||
|
||||
layout := &splitPaneLayout{
|
||||
ratio: 0.7,
|
||||
verticalRatio: 0.9, // Default 80% for top section, 20% for bottom
|
||||
backgroundColor: styles.Background,
|
||||
ratio: 0.7,
|
||||
verticalRatio: 0.9, // Default 90% for top section, 10% for bottom
|
||||
}
|
||||
for _, option := range options {
|
||||
option(layout)
|
||||
@@ -270,12 +270,6 @@ func WithRatio(ratio float64) SplitPaneOption {
|
||||
}
|
||||
}
|
||||
|
||||
func WithSplitBackgroundColor(color lipgloss.TerminalColor) SplitPaneOption {
|
||||
return func(s *splitPaneLayout) {
|
||||
s.backgroundColor = color
|
||||
}
|
||||
}
|
||||
|
||||
func WithBottomPanel(panel Container) SplitPaneOption {
|
||||
return func(s *splitPaneLayout) {
|
||||
s.bottomPanel = panel
|
||||
|
||||
@@ -5,22 +5,23 @@ import (
|
||||
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/kujtimiihoxha/opencode/internal/app"
|
||||
"github.com/kujtimiihoxha/opencode/internal/session"
|
||||
"github.com/kujtimiihoxha/opencode/internal/tui/components/chat"
|
||||
"github.com/kujtimiihoxha/opencode/internal/tui/layout"
|
||||
"github.com/kujtimiihoxha/opencode/internal/tui/util"
|
||||
"github.com/opencode-ai/opencode/internal/app"
|
||||
"github.com/opencode-ai/opencode/internal/message"
|
||||
"github.com/opencode-ai/opencode/internal/session"
|
||||
"github.com/opencode-ai/opencode/internal/tui/components/chat"
|
||||
"github.com/opencode-ai/opencode/internal/tui/components/dialog"
|
||||
"github.com/opencode-ai/opencode/internal/tui/layout"
|
||||
"github.com/opencode-ai/opencode/internal/tui/util"
|
||||
)
|
||||
|
||||
var ChatPage PageID = "chat"
|
||||
|
||||
type chatPage struct {
|
||||
app *app.App
|
||||
editor layout.Container
|
||||
messages layout.Container
|
||||
layout layout.SplitPaneLayout
|
||||
session session.Session
|
||||
editingMode bool
|
||||
app *app.App
|
||||
editor layout.Container
|
||||
messages layout.Container
|
||||
layout layout.SplitPaneLayout
|
||||
session session.Session
|
||||
}
|
||||
|
||||
type ChatKeyMap struct {
|
||||
@@ -34,8 +35,8 @@ var keyMap = ChatKeyMap{
|
||||
key.WithHelp("ctrl+n", "new session"),
|
||||
),
|
||||
Cancel: key.NewBinding(
|
||||
key.WithKeys("ctrl+x"),
|
||||
key.WithHelp("ctrl+x", "cancel"),
|
||||
key.WithKeys("esc"),
|
||||
key.WithHelp("esc", "cancel"),
|
||||
),
|
||||
}
|
||||
|
||||
@@ -53,7 +54,17 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
cmd := p.layout.SetSize(msg.Width, msg.Height)
|
||||
cmds = append(cmds, cmd)
|
||||
case chat.SendMsg:
|
||||
cmd := p.sendMessage(msg.Text)
|
||||
cmd := p.sendMessage(msg.Text, msg.Attachments)
|
||||
if cmd != nil {
|
||||
return p, cmd
|
||||
}
|
||||
case dialog.CommandRunCustomMsg:
|
||||
// Check if the agent is busy before executing custom commands
|
||||
if p.app.CoderAgent.IsBusy() {
|
||||
return p, util.ReportWarn("Agent is busy, please wait before executing a command...")
|
||||
}
|
||||
// Handle custom command execution
|
||||
cmd := p.sendMessage(msg.Content)
|
||||
if cmd != nil {
|
||||
return p, cmd
|
||||
}
|
||||
@@ -65,8 +76,6 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
}
|
||||
p.session = msg
|
||||
case chat.EditorFocusMsg:
|
||||
p.editingMode = bool(msg)
|
||||
case tea.KeyMsg:
|
||||
switch {
|
||||
case key.Matches(msg, keyMap.NewSession):
|
||||
@@ -102,7 +111,7 @@ func (p *chatPage) clearSidebar() tea.Cmd {
|
||||
return p.layout.ClearRightPanel()
|
||||
}
|
||||
|
||||
func (p *chatPage) sendMessage(text string) tea.Cmd {
|
||||
func (p *chatPage) sendMessage(text string, attachments []message.Attachment) tea.Cmd {
|
||||
var cmds []tea.Cmd
|
||||
if p.session.ID == "" {
|
||||
session, err := p.app.Sessions.Create(context.Background(), "New Session")
|
||||
@@ -118,7 +127,10 @@ func (p *chatPage) sendMessage(text string) tea.Cmd {
|
||||
cmds = append(cmds, util.CmdHandler(chat.SessionSelectedMsg(session)))
|
||||
}
|
||||
|
||||
p.app.CoderAgent.Run(context.Background(), p.session.ID, text)
|
||||
_, err := p.app.CoderAgent.Run(context.Background(), p.session.ID, text, attachments...)
|
||||
if err != nil {
|
||||
return util.ReportError(err)
|
||||
}
|
||||
return tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
@@ -136,11 +148,8 @@ func (p *chatPage) View() string {
|
||||
|
||||
func (p *chatPage) BindingKeys() []key.Binding {
|
||||
bindings := layout.KeyMapToSlice(keyMap)
|
||||
if p.editingMode {
|
||||
bindings = append(bindings, p.editor.BindingKeys()...)
|
||||
} else {
|
||||
bindings = append(bindings, p.messages.BindingKeys()...)
|
||||
}
|
||||
bindings = append(bindings, p.messages.BindingKeys()...)
|
||||
bindings = append(bindings, p.editor.BindingKeys()...)
|
||||
return bindings
|
||||
}
|
||||
|
||||
@@ -149,16 +158,14 @@ func NewChatPage(app *app.App) tea.Model {
|
||||
chat.NewMessagesCmp(app),
|
||||
layout.WithPadding(1, 1, 0, 1),
|
||||
)
|
||||
|
||||
editorContainer := layout.NewContainer(
|
||||
chat.NewEditorCmp(app),
|
||||
layout.WithBorder(true, false, false, false),
|
||||
)
|
||||
return &chatPage{
|
||||
app: app,
|
||||
editor: editorContainer,
|
||||
messages: messagesContainer,
|
||||
editingMode: true,
|
||||
app: app,
|
||||
editor: editorContainer,
|
||||
messages: messagesContainer,
|
||||
layout: layout.NewSplitPane(
|
||||
layout.WithLeftPanel(messagesContainer),
|
||||
layout.WithBottomPanel(editorContainer),
|
||||
|
||||
@@ -4,9 +4,9 @@ import (
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/kujtimiihoxha/opencode/internal/tui/components/logs"
|
||||
"github.com/kujtimiihoxha/opencode/internal/tui/layout"
|
||||
"github.com/kujtimiihoxha/opencode/internal/tui/styles"
|
||||
"github.com/opencode-ai/opencode/internal/tui/components/logs"
|
||||
"github.com/opencode-ai/opencode/internal/tui/layout"
|
||||
"github.com/opencode-ai/opencode/internal/tui/styles"
|
||||
)
|
||||
|
||||
var LogsPage PageID = "logs"
|
||||
@@ -42,7 +42,7 @@ func (p *logsPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
|
||||
func (p *logsPage) View() string {
|
||||
style := styles.BaseStyle.Width(p.width).Height(p.height)
|
||||
style := styles.BaseStyle().Width(p.width).Height(p.height)
|
||||
return style.Render(lipgloss.JoinVertical(lipgloss.Top,
|
||||
p.table.View(),
|
||||
p.details.View(),
|
||||
@@ -77,7 +77,7 @@ func (p *logsPage) Init() tea.Cmd {
|
||||
|
||||
func NewLogsPage() LogPage {
|
||||
return &logsPage{
|
||||
table: layout.NewContainer(logs.NewLogsTable(), layout.WithBorderAll(), layout.WithBorderColor(styles.ForgroundDim)),
|
||||
details: layout.NewContainer(logs.NewLogsDetails(), layout.WithBorderAll(), layout.WithBorderColor(styles.ForgroundDim)),
|
||||
table: layout.NewContainer(logs.NewLogsTable(), layout.WithBorderAll()),
|
||||
details: layout.NewContainer(logs.NewLogsDetails(), layout.WithBorderAll()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
package styles
|
||||
|
||||
import (
|
||||
"github.com/charmbracelet/huh"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
func HuhTheme() *huh.Theme {
|
||||
t := huh.ThemeBase()
|
||||
|
||||
t.Focused.Base = t.Focused.Base.BorderStyle(lipgloss.HiddenBorder())
|
||||
t.Focused.Title = t.Focused.Title.Foreground(Text)
|
||||
t.Focused.NoteTitle = t.Focused.NoteTitle.Foreground(Text)
|
||||
t.Focused.Directory = t.Focused.Directory.Foreground(Text)
|
||||
t.Focused.Description = t.Focused.Description.Foreground(SubText0)
|
||||
t.Focused.ErrorIndicator = t.Focused.ErrorIndicator.Foreground(Red)
|
||||
t.Focused.ErrorMessage = t.Focused.ErrorMessage.Foreground(Red)
|
||||
t.Focused.SelectSelector = t.Focused.SelectSelector.Foreground(Blue)
|
||||
t.Focused.NextIndicator = t.Focused.NextIndicator.Foreground(Blue)
|
||||
t.Focused.PrevIndicator = t.Focused.PrevIndicator.Foreground(Blue)
|
||||
t.Focused.Option = t.Focused.Option.Foreground(Text)
|
||||
t.Focused.MultiSelectSelector = t.Focused.MultiSelectSelector.Foreground(Blue)
|
||||
t.Focused.SelectedOption = t.Focused.SelectedOption.Foreground(Green)
|
||||
t.Focused.SelectedPrefix = t.Focused.SelectedPrefix.Foreground(Green)
|
||||
t.Focused.UnselectedPrefix = t.Focused.UnselectedPrefix.Foreground(Text)
|
||||
t.Focused.UnselectedOption = t.Focused.UnselectedOption.Foreground(Text)
|
||||
t.Focused.FocusedButton = t.Focused.FocusedButton.Foreground(Base).Background(Blue)
|
||||
t.Focused.BlurredButton = t.Focused.BlurredButton.Foreground(Text).Background(Base)
|
||||
|
||||
t.Focused.TextInput.Cursor = t.Focused.TextInput.Cursor.Foreground(Teal)
|
||||
t.Focused.TextInput.Placeholder = t.Focused.TextInput.Placeholder.Foreground(Overlay0)
|
||||
t.Focused.TextInput.Prompt = t.Focused.TextInput.Prompt.Foreground(Blue)
|
||||
|
||||
t.Blurred = t.Focused
|
||||
t.Blurred.Base = t.Blurred.Base.BorderStyle(lipgloss.HiddenBorder())
|
||||
|
||||
t.Help.Ellipsis = t.Help.Ellipsis.Foreground(SubText0)
|
||||
t.Help.ShortKey = t.Help.ShortKey.Foreground(SubText0)
|
||||
t.Help.ShortDesc = t.Help.ShortDesc.Foreground(Ovelay1)
|
||||
t.Help.ShortSeparator = t.Help.ShortSeparator.Foreground(SubText0)
|
||||
t.Help.FullKey = t.Help.FullKey.Foreground(SubText0)
|
||||
t.Help.FullDesc = t.Help.FullDesc.Foreground(Ovelay1)
|
||||
t.Help.FullSeparator = t.Help.FullSeparator.Foreground(SubText0)
|
||||
|
||||
return t
|
||||
}
|
||||
@@ -3,11 +3,12 @@ package styles
|
||||
const (
|
||||
OpenCodeIcon string = "⌬"
|
||||
|
||||
CheckIcon string = "✓"
|
||||
ErrorIcon string = "✖"
|
||||
WarningIcon string = "⚠"
|
||||
InfoIcon string = ""
|
||||
HintIcon string = "i"
|
||||
SpinnerIcon string = "..."
|
||||
LoadingIcon string = "⟳"
|
||||
)
|
||||
CheckIcon string = "✓"
|
||||
ErrorIcon string = "✖"
|
||||
WarningIcon string = "⚠"
|
||||
InfoIcon string = ""
|
||||
HintIcon string = "i"
|
||||
SpinnerIcon string = "..."
|
||||
LoadingIcon string = "⟳"
|
||||
DocumentIcon string = "🖼"
|
||||
)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,177 +1,155 @@
|
||||
package styles
|
||||
|
||||
import (
|
||||
catppuccin "github.com/catppuccin/go"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/opencode-ai/opencode/internal/tui/theme"
|
||||
)
|
||||
|
||||
var (
|
||||
light = catppuccin.Latte
|
||||
dark = catppuccin.Mocha
|
||||
ImageBakcground = "#212121"
|
||||
)
|
||||
|
||||
// NEW STYLES
|
||||
var (
|
||||
Background = lipgloss.AdaptiveColor{
|
||||
Dark: "#212121",
|
||||
Light: "#212121",
|
||||
}
|
||||
BackgroundDim = lipgloss.AdaptiveColor{
|
||||
Dark: "#2c2c2c",
|
||||
Light: "#2c2c2c",
|
||||
}
|
||||
BackgroundDarker = lipgloss.AdaptiveColor{
|
||||
Dark: "#181818",
|
||||
Light: "#181818",
|
||||
}
|
||||
BorderColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#4b4c5c",
|
||||
Light: "#4b4c5c",
|
||||
}
|
||||
// Style generation functions that use the current theme
|
||||
|
||||
Forground = lipgloss.AdaptiveColor{
|
||||
Dark: "#d3d3d3",
|
||||
Light: "#d3d3d3",
|
||||
}
|
||||
// BaseStyle returns the base style with background and foreground colors
|
||||
func BaseStyle() lipgloss.Style {
|
||||
t := theme.CurrentTheme()
|
||||
return lipgloss.NewStyle().
|
||||
Background(t.Background()).
|
||||
Foreground(t.Text())
|
||||
}
|
||||
|
||||
ForgroundMid = lipgloss.AdaptiveColor{
|
||||
Dark: "#a0a0a0",
|
||||
Light: "#a0a0a0",
|
||||
}
|
||||
// Regular returns a basic unstyled lipgloss.Style
|
||||
func Regular() lipgloss.Style {
|
||||
return lipgloss.NewStyle()
|
||||
}
|
||||
|
||||
ForgroundDim = lipgloss.AdaptiveColor{
|
||||
Dark: "#737373",
|
||||
Light: "#737373",
|
||||
}
|
||||
// Bold returns a bold style
|
||||
func Bold() lipgloss.Style {
|
||||
return Regular().Bold(true)
|
||||
}
|
||||
|
||||
BaseStyle = lipgloss.NewStyle().
|
||||
Background(Background).
|
||||
Foreground(Forground)
|
||||
// Padded returns a style with horizontal padding
|
||||
func Padded() lipgloss.Style {
|
||||
return Regular().Padding(0, 1)
|
||||
}
|
||||
|
||||
PrimaryColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#fab283",
|
||||
Light: "#fab283",
|
||||
}
|
||||
)
|
||||
// Border returns a style with a normal border
|
||||
func Border() lipgloss.Style {
|
||||
t := theme.CurrentTheme()
|
||||
return Regular().
|
||||
Border(lipgloss.NormalBorder()).
|
||||
BorderForeground(t.BorderNormal())
|
||||
}
|
||||
|
||||
var (
|
||||
Regular = lipgloss.NewStyle()
|
||||
Bold = Regular.Bold(true)
|
||||
Padded = Regular.Padding(0, 1)
|
||||
// ThickBorder returns a style with a thick border
|
||||
func ThickBorder() lipgloss.Style {
|
||||
t := theme.CurrentTheme()
|
||||
return Regular().
|
||||
Border(lipgloss.ThickBorder()).
|
||||
BorderForeground(t.BorderNormal())
|
||||
}
|
||||
|
||||
Border = Regular.Border(lipgloss.NormalBorder())
|
||||
ThickBorder = Regular.Border(lipgloss.ThickBorder())
|
||||
DoubleBorder = Regular.Border(lipgloss.DoubleBorder())
|
||||
// DoubleBorder returns a style with a double border
|
||||
func DoubleBorder() lipgloss.Style {
|
||||
t := theme.CurrentTheme()
|
||||
return Regular().
|
||||
Border(lipgloss.DoubleBorder()).
|
||||
BorderForeground(t.BorderNormal())
|
||||
}
|
||||
|
||||
// Colors
|
||||
White = lipgloss.Color("#ffffff")
|
||||
Surface0 = lipgloss.AdaptiveColor{
|
||||
Dark: dark.Surface0().Hex,
|
||||
Light: light.Surface0().Hex,
|
||||
}
|
||||
// FocusedBorder returns a style with a border using the focused border color
|
||||
func FocusedBorder() lipgloss.Style {
|
||||
t := theme.CurrentTheme()
|
||||
return Regular().
|
||||
Border(lipgloss.NormalBorder()).
|
||||
BorderForeground(t.BorderFocused())
|
||||
}
|
||||
|
||||
Overlay0 = lipgloss.AdaptiveColor{
|
||||
Dark: dark.Overlay0().Hex,
|
||||
Light: light.Overlay0().Hex,
|
||||
}
|
||||
// DimBorder returns a style with a border using the dim border color
|
||||
func DimBorder() lipgloss.Style {
|
||||
t := theme.CurrentTheme()
|
||||
return Regular().
|
||||
Border(lipgloss.NormalBorder()).
|
||||
BorderForeground(t.BorderDim())
|
||||
}
|
||||
|
||||
Ovelay1 = lipgloss.AdaptiveColor{
|
||||
Dark: dark.Overlay1().Hex,
|
||||
Light: light.Overlay1().Hex,
|
||||
}
|
||||
// PrimaryColor returns the primary color from the current theme
|
||||
func PrimaryColor() lipgloss.AdaptiveColor {
|
||||
return theme.CurrentTheme().Primary()
|
||||
}
|
||||
|
||||
Text = lipgloss.AdaptiveColor{
|
||||
Dark: dark.Text().Hex,
|
||||
Light: light.Text().Hex,
|
||||
}
|
||||
// SecondaryColor returns the secondary color from the current theme
|
||||
func SecondaryColor() lipgloss.AdaptiveColor {
|
||||
return theme.CurrentTheme().Secondary()
|
||||
}
|
||||
|
||||
SubText0 = lipgloss.AdaptiveColor{
|
||||
Dark: dark.Subtext0().Hex,
|
||||
Light: light.Subtext0().Hex,
|
||||
}
|
||||
// AccentColor returns the accent color from the current theme
|
||||
func AccentColor() lipgloss.AdaptiveColor {
|
||||
return theme.CurrentTheme().Accent()
|
||||
}
|
||||
|
||||
SubText1 = lipgloss.AdaptiveColor{
|
||||
Dark: dark.Subtext1().Hex,
|
||||
Light: light.Subtext1().Hex,
|
||||
}
|
||||
// ErrorColor returns the error color from the current theme
|
||||
func ErrorColor() lipgloss.AdaptiveColor {
|
||||
return theme.CurrentTheme().Error()
|
||||
}
|
||||
|
||||
LightGrey = lipgloss.AdaptiveColor{
|
||||
Dark: dark.Surface0().Hex,
|
||||
Light: light.Surface0().Hex,
|
||||
}
|
||||
Grey = lipgloss.AdaptiveColor{
|
||||
Dark: dark.Surface1().Hex,
|
||||
Light: light.Surface1().Hex,
|
||||
}
|
||||
// WarningColor returns the warning color from the current theme
|
||||
func WarningColor() lipgloss.AdaptiveColor {
|
||||
return theme.CurrentTheme().Warning()
|
||||
}
|
||||
|
||||
DarkGrey = lipgloss.AdaptiveColor{
|
||||
Dark: dark.Surface2().Hex,
|
||||
Light: light.Surface2().Hex,
|
||||
}
|
||||
// SuccessColor returns the success color from the current theme
|
||||
func SuccessColor() lipgloss.AdaptiveColor {
|
||||
return theme.CurrentTheme().Success()
|
||||
}
|
||||
|
||||
Base = lipgloss.AdaptiveColor{
|
||||
Dark: dark.Base().Hex,
|
||||
Light: light.Base().Hex,
|
||||
}
|
||||
// InfoColor returns the info color from the current theme
|
||||
func InfoColor() lipgloss.AdaptiveColor {
|
||||
return theme.CurrentTheme().Info()
|
||||
}
|
||||
|
||||
Crust = lipgloss.AdaptiveColor{
|
||||
Dark: dark.Crust().Hex,
|
||||
Light: light.Crust().Hex,
|
||||
}
|
||||
// TextColor returns the text color from the current theme
|
||||
func TextColor() lipgloss.AdaptiveColor {
|
||||
return theme.CurrentTheme().Text()
|
||||
}
|
||||
|
||||
Blue = lipgloss.AdaptiveColor{
|
||||
Dark: dark.Blue().Hex,
|
||||
Light: light.Blue().Hex,
|
||||
}
|
||||
// TextMutedColor returns the muted text color from the current theme
|
||||
func TextMutedColor() lipgloss.AdaptiveColor {
|
||||
return theme.CurrentTheme().TextMuted()
|
||||
}
|
||||
|
||||
Red = lipgloss.AdaptiveColor{
|
||||
Dark: dark.Red().Hex,
|
||||
Light: light.Red().Hex,
|
||||
}
|
||||
// TextEmphasizedColor returns the emphasized text color from the current theme
|
||||
func TextEmphasizedColor() lipgloss.AdaptiveColor {
|
||||
return theme.CurrentTheme().TextEmphasized()
|
||||
}
|
||||
|
||||
Green = lipgloss.AdaptiveColor{
|
||||
Dark: dark.Green().Hex,
|
||||
Light: light.Green().Hex,
|
||||
}
|
||||
// BackgroundColor returns the background color from the current theme
|
||||
func BackgroundColor() lipgloss.AdaptiveColor {
|
||||
return theme.CurrentTheme().Background()
|
||||
}
|
||||
|
||||
Mauve = lipgloss.AdaptiveColor{
|
||||
Dark: dark.Mauve().Hex,
|
||||
Light: light.Mauve().Hex,
|
||||
}
|
||||
// BackgroundSecondaryColor returns the secondary background color from the current theme
|
||||
func BackgroundSecondaryColor() lipgloss.AdaptiveColor {
|
||||
return theme.CurrentTheme().BackgroundSecondary()
|
||||
}
|
||||
|
||||
Teal = lipgloss.AdaptiveColor{
|
||||
Dark: dark.Teal().Hex,
|
||||
Light: light.Teal().Hex,
|
||||
}
|
||||
// BackgroundDarkerColor returns the darker background color from the current theme
|
||||
func BackgroundDarkerColor() lipgloss.AdaptiveColor {
|
||||
return theme.CurrentTheme().BackgroundDarker()
|
||||
}
|
||||
|
||||
Rosewater = lipgloss.AdaptiveColor{
|
||||
Dark: dark.Rosewater().Hex,
|
||||
Light: light.Rosewater().Hex,
|
||||
}
|
||||
// BorderNormalColor returns the normal border color from the current theme
|
||||
func BorderNormalColor() lipgloss.AdaptiveColor {
|
||||
return theme.CurrentTheme().BorderNormal()
|
||||
}
|
||||
|
||||
Flamingo = lipgloss.AdaptiveColor{
|
||||
Dark: dark.Flamingo().Hex,
|
||||
Light: light.Flamingo().Hex,
|
||||
}
|
||||
// BorderFocusedColor returns the focused border color from the current theme
|
||||
func BorderFocusedColor() lipgloss.AdaptiveColor {
|
||||
return theme.CurrentTheme().BorderFocused()
|
||||
}
|
||||
|
||||
Lavender = lipgloss.AdaptiveColor{
|
||||
Dark: dark.Lavender().Hex,
|
||||
Light: light.Lavender().Hex,
|
||||
}
|
||||
|
||||
Peach = lipgloss.AdaptiveColor{
|
||||
Dark: dark.Peach().Hex,
|
||||
Light: light.Peach().Hex,
|
||||
}
|
||||
|
||||
Yellow = lipgloss.AdaptiveColor{
|
||||
Dark: dark.Yellow().Hex,
|
||||
Light: light.Yellow().Hex,
|
||||
}
|
||||
|
||||
Primary = Blue
|
||||
Secondary = Mauve
|
||||
|
||||
Warning = Peach
|
||||
Error = Red
|
||||
)
|
||||
// BorderDimColor returns the dim border color from the current theme
|
||||
func BorderDimColor() lipgloss.AdaptiveColor {
|
||||
return theme.CurrentTheme().BorderDim()
|
||||
}
|
||||
|
||||
248
internal/tui/theme/catppuccin.go
Normal file
248
internal/tui/theme/catppuccin.go
Normal file
@@ -0,0 +1,248 @@
|
||||
package theme
|
||||
|
||||
import (
|
||||
catppuccin "github.com/catppuccin/go"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
// CatppuccinTheme implements the Theme interface with Catppuccin colors.
|
||||
// It provides both dark (Mocha) and light (Latte) variants.
|
||||
type CatppuccinTheme struct {
|
||||
BaseTheme
|
||||
}
|
||||
|
||||
// NewCatppuccinTheme creates a new instance of the Catppuccin theme.
|
||||
func NewCatppuccinTheme() *CatppuccinTheme {
|
||||
// Get the Catppuccin palettes
|
||||
mocha := catppuccin.Mocha
|
||||
latte := catppuccin.Latte
|
||||
|
||||
theme := &CatppuccinTheme{}
|
||||
|
||||
// Base colors
|
||||
theme.PrimaryColor = lipgloss.AdaptiveColor{
|
||||
Dark: mocha.Blue().Hex,
|
||||
Light: latte.Blue().Hex,
|
||||
}
|
||||
theme.SecondaryColor = lipgloss.AdaptiveColor{
|
||||
Dark: mocha.Mauve().Hex,
|
||||
Light: latte.Mauve().Hex,
|
||||
}
|
||||
theme.AccentColor = lipgloss.AdaptiveColor{
|
||||
Dark: mocha.Peach().Hex,
|
||||
Light: latte.Peach().Hex,
|
||||
}
|
||||
|
||||
// Status colors
|
||||
theme.ErrorColor = lipgloss.AdaptiveColor{
|
||||
Dark: mocha.Red().Hex,
|
||||
Light: latte.Red().Hex,
|
||||
}
|
||||
theme.WarningColor = lipgloss.AdaptiveColor{
|
||||
Dark: mocha.Peach().Hex,
|
||||
Light: latte.Peach().Hex,
|
||||
}
|
||||
theme.SuccessColor = lipgloss.AdaptiveColor{
|
||||
Dark: mocha.Green().Hex,
|
||||
Light: latte.Green().Hex,
|
||||
}
|
||||
theme.InfoColor = lipgloss.AdaptiveColor{
|
||||
Dark: mocha.Blue().Hex,
|
||||
Light: latte.Blue().Hex,
|
||||
}
|
||||
|
||||
// Text colors
|
||||
theme.TextColor = lipgloss.AdaptiveColor{
|
||||
Dark: mocha.Text().Hex,
|
||||
Light: latte.Text().Hex,
|
||||
}
|
||||
theme.TextMutedColor = lipgloss.AdaptiveColor{
|
||||
Dark: mocha.Subtext0().Hex,
|
||||
Light: latte.Subtext0().Hex,
|
||||
}
|
||||
theme.TextEmphasizedColor = lipgloss.AdaptiveColor{
|
||||
Dark: mocha.Lavender().Hex,
|
||||
Light: latte.Lavender().Hex,
|
||||
}
|
||||
|
||||
// Background colors
|
||||
theme.BackgroundColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#212121", // From existing styles
|
||||
Light: "#EEEEEE", // Light equivalent
|
||||
}
|
||||
theme.BackgroundSecondaryColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#2c2c2c", // From existing styles
|
||||
Light: "#E0E0E0", // Light equivalent
|
||||
}
|
||||
theme.BackgroundDarkerColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#181818", // From existing styles
|
||||
Light: "#F5F5F5", // Light equivalent
|
||||
}
|
||||
|
||||
// Border colors
|
||||
theme.BorderNormalColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#4b4c5c", // From existing styles
|
||||
Light: "#BDBDBD", // Light equivalent
|
||||
}
|
||||
theme.BorderFocusedColor = lipgloss.AdaptiveColor{
|
||||
Dark: mocha.Blue().Hex,
|
||||
Light: latte.Blue().Hex,
|
||||
}
|
||||
theme.BorderDimColor = lipgloss.AdaptiveColor{
|
||||
Dark: mocha.Surface0().Hex,
|
||||
Light: latte.Surface0().Hex,
|
||||
}
|
||||
|
||||
// Diff view colors
|
||||
theme.DiffAddedColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#478247", // From existing diff.go
|
||||
Light: "#2E7D32", // Light equivalent
|
||||
}
|
||||
theme.DiffRemovedColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#7C4444", // From existing diff.go
|
||||
Light: "#C62828", // Light equivalent
|
||||
}
|
||||
theme.DiffContextColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#a0a0a0", // From existing diff.go
|
||||
Light: "#757575", // Light equivalent
|
||||
}
|
||||
theme.DiffHunkHeaderColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#a0a0a0", // From existing diff.go
|
||||
Light: "#757575", // Light equivalent
|
||||
}
|
||||
theme.DiffHighlightAddedColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#DAFADA", // From existing diff.go
|
||||
Light: "#A5D6A7", // Light equivalent
|
||||
}
|
||||
theme.DiffHighlightRemovedColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#FADADD", // From existing diff.go
|
||||
Light: "#EF9A9A", // Light equivalent
|
||||
}
|
||||
theme.DiffAddedBgColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#303A30", // From existing diff.go
|
||||
Light: "#E8F5E9", // Light equivalent
|
||||
}
|
||||
theme.DiffRemovedBgColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#3A3030", // From existing diff.go
|
||||
Light: "#FFEBEE", // Light equivalent
|
||||
}
|
||||
theme.DiffContextBgColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#212121", // From existing diff.go
|
||||
Light: "#F5F5F5", // Light equivalent
|
||||
}
|
||||
theme.DiffLineNumberColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#888888", // From existing diff.go
|
||||
Light: "#9E9E9E", // Light equivalent
|
||||
}
|
||||
theme.DiffAddedLineNumberBgColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#293229", // From existing diff.go
|
||||
Light: "#C8E6C9", // Light equivalent
|
||||
}
|
||||
theme.DiffRemovedLineNumberBgColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#332929", // From existing diff.go
|
||||
Light: "#FFCDD2", // Light equivalent
|
||||
}
|
||||
|
||||
// Markdown colors
|
||||
theme.MarkdownTextColor = lipgloss.AdaptiveColor{
|
||||
Dark: mocha.Text().Hex,
|
||||
Light: latte.Text().Hex,
|
||||
}
|
||||
theme.MarkdownHeadingColor = lipgloss.AdaptiveColor{
|
||||
Dark: mocha.Mauve().Hex,
|
||||
Light: latte.Mauve().Hex,
|
||||
}
|
||||
theme.MarkdownLinkColor = lipgloss.AdaptiveColor{
|
||||
Dark: mocha.Sky().Hex,
|
||||
Light: latte.Sky().Hex,
|
||||
}
|
||||
theme.MarkdownLinkTextColor = lipgloss.AdaptiveColor{
|
||||
Dark: mocha.Pink().Hex,
|
||||
Light: latte.Pink().Hex,
|
||||
}
|
||||
theme.MarkdownCodeColor = lipgloss.AdaptiveColor{
|
||||
Dark: mocha.Green().Hex,
|
||||
Light: latte.Green().Hex,
|
||||
}
|
||||
theme.MarkdownBlockQuoteColor = lipgloss.AdaptiveColor{
|
||||
Dark: mocha.Yellow().Hex,
|
||||
Light: latte.Yellow().Hex,
|
||||
}
|
||||
theme.MarkdownEmphColor = lipgloss.AdaptiveColor{
|
||||
Dark: mocha.Yellow().Hex,
|
||||
Light: latte.Yellow().Hex,
|
||||
}
|
||||
theme.MarkdownStrongColor = lipgloss.AdaptiveColor{
|
||||
Dark: mocha.Peach().Hex,
|
||||
Light: latte.Peach().Hex,
|
||||
}
|
||||
theme.MarkdownHorizontalRuleColor = lipgloss.AdaptiveColor{
|
||||
Dark: mocha.Overlay0().Hex,
|
||||
Light: latte.Overlay0().Hex,
|
||||
}
|
||||
theme.MarkdownListItemColor = lipgloss.AdaptiveColor{
|
||||
Dark: mocha.Blue().Hex,
|
||||
Light: latte.Blue().Hex,
|
||||
}
|
||||
theme.MarkdownListEnumerationColor = lipgloss.AdaptiveColor{
|
||||
Dark: mocha.Sky().Hex,
|
||||
Light: latte.Sky().Hex,
|
||||
}
|
||||
theme.MarkdownImageColor = lipgloss.AdaptiveColor{
|
||||
Dark: mocha.Sapphire().Hex,
|
||||
Light: latte.Sapphire().Hex,
|
||||
}
|
||||
theme.MarkdownImageTextColor = lipgloss.AdaptiveColor{
|
||||
Dark: mocha.Pink().Hex,
|
||||
Light: latte.Pink().Hex,
|
||||
}
|
||||
theme.MarkdownCodeBlockColor = lipgloss.AdaptiveColor{
|
||||
Dark: mocha.Text().Hex,
|
||||
Light: latte.Text().Hex,
|
||||
}
|
||||
|
||||
// Syntax highlighting colors
|
||||
theme.SyntaxCommentColor = lipgloss.AdaptiveColor{
|
||||
Dark: mocha.Overlay1().Hex,
|
||||
Light: latte.Overlay1().Hex,
|
||||
}
|
||||
theme.SyntaxKeywordColor = lipgloss.AdaptiveColor{
|
||||
Dark: mocha.Pink().Hex,
|
||||
Light: latte.Pink().Hex,
|
||||
}
|
||||
theme.SyntaxFunctionColor = lipgloss.AdaptiveColor{
|
||||
Dark: mocha.Green().Hex,
|
||||
Light: latte.Green().Hex,
|
||||
}
|
||||
theme.SyntaxVariableColor = lipgloss.AdaptiveColor{
|
||||
Dark: mocha.Sky().Hex,
|
||||
Light: latte.Sky().Hex,
|
||||
}
|
||||
theme.SyntaxStringColor = lipgloss.AdaptiveColor{
|
||||
Dark: mocha.Yellow().Hex,
|
||||
Light: latte.Yellow().Hex,
|
||||
}
|
||||
theme.SyntaxNumberColor = lipgloss.AdaptiveColor{
|
||||
Dark: mocha.Teal().Hex,
|
||||
Light: latte.Teal().Hex,
|
||||
}
|
||||
theme.SyntaxTypeColor = lipgloss.AdaptiveColor{
|
||||
Dark: mocha.Sky().Hex,
|
||||
Light: latte.Sky().Hex,
|
||||
}
|
||||
theme.SyntaxOperatorColor = lipgloss.AdaptiveColor{
|
||||
Dark: mocha.Pink().Hex,
|
||||
Light: latte.Pink().Hex,
|
||||
}
|
||||
theme.SyntaxPunctuationColor = lipgloss.AdaptiveColor{
|
||||
Dark: mocha.Text().Hex,
|
||||
Light: latte.Text().Hex,
|
||||
}
|
||||
|
||||
return theme
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Register the Catppuccin theme with the theme manager
|
||||
RegisterTheme("catppuccin", NewCatppuccinTheme())
|
||||
}
|
||||
274
internal/tui/theme/dracula.go
Normal file
274
internal/tui/theme/dracula.go
Normal file
@@ -0,0 +1,274 @@
|
||||
package theme
|
||||
|
||||
import (
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
// DraculaTheme implements the Theme interface with Dracula colors.
|
||||
// It provides both dark and light variants, though Dracula is primarily a dark theme.
|
||||
type DraculaTheme struct {
|
||||
BaseTheme
|
||||
}
|
||||
|
||||
// NewDraculaTheme creates a new instance of the Dracula theme.
|
||||
func NewDraculaTheme() *DraculaTheme {
|
||||
// Dracula color palette
|
||||
// Official colors from https://draculatheme.com/
|
||||
darkBackground := "#282a36"
|
||||
darkCurrentLine := "#44475a"
|
||||
darkSelection := "#44475a"
|
||||
darkForeground := "#f8f8f2"
|
||||
darkComment := "#6272a4"
|
||||
darkCyan := "#8be9fd"
|
||||
darkGreen := "#50fa7b"
|
||||
darkOrange := "#ffb86c"
|
||||
darkPink := "#ff79c6"
|
||||
darkPurple := "#bd93f9"
|
||||
darkRed := "#ff5555"
|
||||
darkYellow := "#f1fa8c"
|
||||
darkBorder := "#44475a"
|
||||
|
||||
// Light mode approximation (Dracula is primarily a dark theme)
|
||||
lightBackground := "#f8f8f2"
|
||||
lightCurrentLine := "#e6e6e6"
|
||||
lightSelection := "#d8d8d8"
|
||||
lightForeground := "#282a36"
|
||||
lightComment := "#6272a4"
|
||||
lightCyan := "#0097a7"
|
||||
lightGreen := "#388e3c"
|
||||
lightOrange := "#f57c00"
|
||||
lightPink := "#d81b60"
|
||||
lightPurple := "#7e57c2"
|
||||
lightRed := "#e53935"
|
||||
lightYellow := "#fbc02d"
|
||||
lightBorder := "#d8d8d8"
|
||||
|
||||
theme := &DraculaTheme{}
|
||||
|
||||
// Base colors
|
||||
theme.PrimaryColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkPurple,
|
||||
Light: lightPurple,
|
||||
}
|
||||
theme.SecondaryColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkPink,
|
||||
Light: lightPink,
|
||||
}
|
||||
theme.AccentColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkCyan,
|
||||
Light: lightCyan,
|
||||
}
|
||||
|
||||
// Status colors
|
||||
theme.ErrorColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkRed,
|
||||
Light: lightRed,
|
||||
}
|
||||
theme.WarningColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkOrange,
|
||||
Light: lightOrange,
|
||||
}
|
||||
theme.SuccessColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkGreen,
|
||||
Light: lightGreen,
|
||||
}
|
||||
theme.InfoColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkCyan,
|
||||
Light: lightCyan,
|
||||
}
|
||||
|
||||
// Text colors
|
||||
theme.TextColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkForeground,
|
||||
Light: lightForeground,
|
||||
}
|
||||
theme.TextMutedColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkComment,
|
||||
Light: lightComment,
|
||||
}
|
||||
theme.TextEmphasizedColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkYellow,
|
||||
Light: lightYellow,
|
||||
}
|
||||
|
||||
// Background colors
|
||||
theme.BackgroundColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkBackground,
|
||||
Light: lightBackground,
|
||||
}
|
||||
theme.BackgroundSecondaryColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkCurrentLine,
|
||||
Light: lightCurrentLine,
|
||||
}
|
||||
theme.BackgroundDarkerColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#21222c", // Slightly darker than background
|
||||
Light: "#ffffff", // Slightly lighter than background
|
||||
}
|
||||
|
||||
// Border colors
|
||||
theme.BorderNormalColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkBorder,
|
||||
Light: lightBorder,
|
||||
}
|
||||
theme.BorderFocusedColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkPurple,
|
||||
Light: lightPurple,
|
||||
}
|
||||
theme.BorderDimColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkSelection,
|
||||
Light: lightSelection,
|
||||
}
|
||||
|
||||
// Diff view colors
|
||||
theme.DiffAddedColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkGreen,
|
||||
Light: lightGreen,
|
||||
}
|
||||
theme.DiffRemovedColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkRed,
|
||||
Light: lightRed,
|
||||
}
|
||||
theme.DiffContextColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkComment,
|
||||
Light: lightComment,
|
||||
}
|
||||
theme.DiffHunkHeaderColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkPurple,
|
||||
Light: lightPurple,
|
||||
}
|
||||
theme.DiffHighlightAddedColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#50fa7b",
|
||||
Light: "#a5d6a7",
|
||||
}
|
||||
theme.DiffHighlightRemovedColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#ff5555",
|
||||
Light: "#ef9a9a",
|
||||
}
|
||||
theme.DiffAddedBgColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#2c3b2c",
|
||||
Light: "#e8f5e9",
|
||||
}
|
||||
theme.DiffRemovedBgColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#3b2c2c",
|
||||
Light: "#ffebee",
|
||||
}
|
||||
theme.DiffContextBgColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkBackground,
|
||||
Light: lightBackground,
|
||||
}
|
||||
theme.DiffLineNumberColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkComment,
|
||||
Light: lightComment,
|
||||
}
|
||||
theme.DiffAddedLineNumberBgColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#253025",
|
||||
Light: "#c8e6c9",
|
||||
}
|
||||
theme.DiffRemovedLineNumberBgColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#302525",
|
||||
Light: "#ffcdd2",
|
||||
}
|
||||
|
||||
// Markdown colors
|
||||
theme.MarkdownTextColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkForeground,
|
||||
Light: lightForeground,
|
||||
}
|
||||
theme.MarkdownHeadingColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkPink,
|
||||
Light: lightPink,
|
||||
}
|
||||
theme.MarkdownLinkColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkPurple,
|
||||
Light: lightPurple,
|
||||
}
|
||||
theme.MarkdownLinkTextColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkCyan,
|
||||
Light: lightCyan,
|
||||
}
|
||||
theme.MarkdownCodeColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkGreen,
|
||||
Light: lightGreen,
|
||||
}
|
||||
theme.MarkdownBlockQuoteColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkYellow,
|
||||
Light: lightYellow,
|
||||
}
|
||||
theme.MarkdownEmphColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkYellow,
|
||||
Light: lightYellow,
|
||||
}
|
||||
theme.MarkdownStrongColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkOrange,
|
||||
Light: lightOrange,
|
||||
}
|
||||
theme.MarkdownHorizontalRuleColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkComment,
|
||||
Light: lightComment,
|
||||
}
|
||||
theme.MarkdownListItemColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkPurple,
|
||||
Light: lightPurple,
|
||||
}
|
||||
theme.MarkdownListEnumerationColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkCyan,
|
||||
Light: lightCyan,
|
||||
}
|
||||
theme.MarkdownImageColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkPurple,
|
||||
Light: lightPurple,
|
||||
}
|
||||
theme.MarkdownImageTextColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkCyan,
|
||||
Light: lightCyan,
|
||||
}
|
||||
theme.MarkdownCodeBlockColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkForeground,
|
||||
Light: lightForeground,
|
||||
}
|
||||
|
||||
// Syntax highlighting colors
|
||||
theme.SyntaxCommentColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkComment,
|
||||
Light: lightComment,
|
||||
}
|
||||
theme.SyntaxKeywordColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkPink,
|
||||
Light: lightPink,
|
||||
}
|
||||
theme.SyntaxFunctionColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkGreen,
|
||||
Light: lightGreen,
|
||||
}
|
||||
theme.SyntaxVariableColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkOrange,
|
||||
Light: lightOrange,
|
||||
}
|
||||
theme.SyntaxStringColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkYellow,
|
||||
Light: lightYellow,
|
||||
}
|
||||
theme.SyntaxNumberColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkPurple,
|
||||
Light: lightPurple,
|
||||
}
|
||||
theme.SyntaxTypeColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkCyan,
|
||||
Light: lightCyan,
|
||||
}
|
||||
theme.SyntaxOperatorColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkPink,
|
||||
Light: lightPink,
|
||||
}
|
||||
theme.SyntaxPunctuationColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkForeground,
|
||||
Light: lightForeground,
|
||||
}
|
||||
|
||||
return theme
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Register the Dracula theme with the theme manager
|
||||
RegisterTheme("dracula", NewDraculaTheme())
|
||||
}
|
||||
282
internal/tui/theme/flexoki.go
Normal file
282
internal/tui/theme/flexoki.go
Normal file
@@ -0,0 +1,282 @@
|
||||
package theme
|
||||
|
||||
import (
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
// Flexoki color palette constants
|
||||
const (
|
||||
// Base colors
|
||||
flexokiPaper = "#FFFCF0" // Paper (lightest)
|
||||
flexokiBase50 = "#F2F0E5" // bg-2 (light)
|
||||
flexokiBase100 = "#E6E4D9" // ui (light)
|
||||
flexokiBase150 = "#DAD8CE" // ui-2 (light)
|
||||
flexokiBase200 = "#CECDC3" // ui-3 (light)
|
||||
flexokiBase300 = "#B7B5AC" // tx-3 (light)
|
||||
flexokiBase500 = "#878580" // tx-2 (light)
|
||||
flexokiBase600 = "#6F6E69" // tx (light)
|
||||
flexokiBase700 = "#575653" // tx-3 (dark)
|
||||
flexokiBase800 = "#403E3C" // ui-3 (dark)
|
||||
flexokiBase850 = "#343331" // ui-2 (dark)
|
||||
flexokiBase900 = "#282726" // ui (dark)
|
||||
flexokiBase950 = "#1C1B1A" // bg-2 (dark)
|
||||
flexokiBlack = "#100F0F" // bg (darkest)
|
||||
|
||||
// Accent colors - Light theme (600)
|
||||
flexokiRed600 = "#AF3029"
|
||||
flexokiOrange600 = "#BC5215"
|
||||
flexokiYellow600 = "#AD8301"
|
||||
flexokiGreen600 = "#66800B"
|
||||
flexokiCyan600 = "#24837B"
|
||||
flexokiBlue600 = "#205EA6"
|
||||
flexokiPurple600 = "#5E409D"
|
||||
flexokiMagenta600 = "#A02F6F"
|
||||
|
||||
// Accent colors - Dark theme (400)
|
||||
flexokiRed400 = "#D14D41"
|
||||
flexokiOrange400 = "#DA702C"
|
||||
flexokiYellow400 = "#D0A215"
|
||||
flexokiGreen400 = "#879A39"
|
||||
flexokiCyan400 = "#3AA99F"
|
||||
flexokiBlue400 = "#4385BE"
|
||||
flexokiPurple400 = "#8B7EC8"
|
||||
flexokiMagenta400 = "#CE5D97"
|
||||
)
|
||||
|
||||
// FlexokiTheme implements the Theme interface with Flexoki colors.
|
||||
// It provides both dark and light variants.
|
||||
type FlexokiTheme struct {
|
||||
BaseTheme
|
||||
}
|
||||
|
||||
// NewFlexokiTheme creates a new instance of the Flexoki theme.
|
||||
func NewFlexokiTheme() *FlexokiTheme {
|
||||
theme := &FlexokiTheme{}
|
||||
|
||||
// Base colors
|
||||
theme.PrimaryColor = lipgloss.AdaptiveColor{
|
||||
Dark: flexokiBlue400,
|
||||
Light: flexokiBlue600,
|
||||
}
|
||||
theme.SecondaryColor = lipgloss.AdaptiveColor{
|
||||
Dark: flexokiPurple400,
|
||||
Light: flexokiPurple600,
|
||||
}
|
||||
theme.AccentColor = lipgloss.AdaptiveColor{
|
||||
Dark: flexokiOrange400,
|
||||
Light: flexokiOrange600,
|
||||
}
|
||||
|
||||
// Status colors
|
||||
theme.ErrorColor = lipgloss.AdaptiveColor{
|
||||
Dark: flexokiRed400,
|
||||
Light: flexokiRed600,
|
||||
}
|
||||
theme.WarningColor = lipgloss.AdaptiveColor{
|
||||
Dark: flexokiYellow400,
|
||||
Light: flexokiYellow600,
|
||||
}
|
||||
theme.SuccessColor = lipgloss.AdaptiveColor{
|
||||
Dark: flexokiGreen400,
|
||||
Light: flexokiGreen600,
|
||||
}
|
||||
theme.InfoColor = lipgloss.AdaptiveColor{
|
||||
Dark: flexokiCyan400,
|
||||
Light: flexokiCyan600,
|
||||
}
|
||||
|
||||
// Text colors
|
||||
theme.TextColor = lipgloss.AdaptiveColor{
|
||||
Dark: flexokiBase300,
|
||||
Light: flexokiBase600,
|
||||
}
|
||||
theme.TextMutedColor = lipgloss.AdaptiveColor{
|
||||
Dark: flexokiBase700,
|
||||
Light: flexokiBase500,
|
||||
}
|
||||
theme.TextEmphasizedColor = lipgloss.AdaptiveColor{
|
||||
Dark: flexokiYellow400,
|
||||
Light: flexokiYellow600,
|
||||
}
|
||||
|
||||
// Background colors
|
||||
theme.BackgroundColor = lipgloss.AdaptiveColor{
|
||||
Dark: flexokiBlack,
|
||||
Light: flexokiPaper,
|
||||
}
|
||||
theme.BackgroundSecondaryColor = lipgloss.AdaptiveColor{
|
||||
Dark: flexokiBase950,
|
||||
Light: flexokiBase50,
|
||||
}
|
||||
theme.BackgroundDarkerColor = lipgloss.AdaptiveColor{
|
||||
Dark: flexokiBase900,
|
||||
Light: flexokiBase100,
|
||||
}
|
||||
|
||||
// Border colors
|
||||
theme.BorderNormalColor = lipgloss.AdaptiveColor{
|
||||
Dark: flexokiBase900,
|
||||
Light: flexokiBase100,
|
||||
}
|
||||
theme.BorderFocusedColor = lipgloss.AdaptiveColor{
|
||||
Dark: flexokiBlue400,
|
||||
Light: flexokiBlue600,
|
||||
}
|
||||
theme.BorderDimColor = lipgloss.AdaptiveColor{
|
||||
Dark: flexokiBase850,
|
||||
Light: flexokiBase150,
|
||||
}
|
||||
|
||||
// Diff view colors
|
||||
theme.DiffAddedColor = lipgloss.AdaptiveColor{
|
||||
Dark: flexokiGreen400,
|
||||
Light: flexokiGreen600,
|
||||
}
|
||||
theme.DiffRemovedColor = lipgloss.AdaptiveColor{
|
||||
Dark: flexokiRed400,
|
||||
Light: flexokiRed600,
|
||||
}
|
||||
theme.DiffContextColor = lipgloss.AdaptiveColor{
|
||||
Dark: flexokiBase700,
|
||||
Light: flexokiBase500,
|
||||
}
|
||||
theme.DiffHunkHeaderColor = lipgloss.AdaptiveColor{
|
||||
Dark: flexokiBase700,
|
||||
Light: flexokiBase500,
|
||||
}
|
||||
theme.DiffHighlightAddedColor = lipgloss.AdaptiveColor{
|
||||
Dark: flexokiGreen400,
|
||||
Light: flexokiGreen600,
|
||||
}
|
||||
theme.DiffHighlightRemovedColor = lipgloss.AdaptiveColor{
|
||||
Dark: flexokiRed400,
|
||||
Light: flexokiRed600,
|
||||
}
|
||||
theme.DiffAddedBgColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#1D2419", // Darker green background
|
||||
Light: "#EFF2E2", // Light green background
|
||||
}
|
||||
theme.DiffRemovedBgColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#241919", // Darker red background
|
||||
Light: "#F2E2E2", // Light red background
|
||||
}
|
||||
theme.DiffContextBgColor = lipgloss.AdaptiveColor{
|
||||
Dark: flexokiBlack,
|
||||
Light: flexokiPaper,
|
||||
}
|
||||
theme.DiffLineNumberColor = lipgloss.AdaptiveColor{
|
||||
Dark: flexokiBase700,
|
||||
Light: flexokiBase500,
|
||||
}
|
||||
theme.DiffAddedLineNumberBgColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#1A2017", // Slightly darker green
|
||||
Light: "#E5EBD9", // Light green
|
||||
}
|
||||
theme.DiffRemovedLineNumberBgColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#201717", // Slightly darker red
|
||||
Light: "#EBD9D9", // Light red
|
||||
}
|
||||
|
||||
// Markdown colors
|
||||
theme.MarkdownTextColor = lipgloss.AdaptiveColor{
|
||||
Dark: flexokiBase300,
|
||||
Light: flexokiBase600,
|
||||
}
|
||||
theme.MarkdownHeadingColor = lipgloss.AdaptiveColor{
|
||||
Dark: flexokiYellow400,
|
||||
Light: flexokiYellow600,
|
||||
}
|
||||
theme.MarkdownLinkColor = lipgloss.AdaptiveColor{
|
||||
Dark: flexokiCyan400,
|
||||
Light: flexokiCyan600,
|
||||
}
|
||||
theme.MarkdownLinkTextColor = lipgloss.AdaptiveColor{
|
||||
Dark: flexokiMagenta400,
|
||||
Light: flexokiMagenta600,
|
||||
}
|
||||
theme.MarkdownCodeColor = lipgloss.AdaptiveColor{
|
||||
Dark: flexokiGreen400,
|
||||
Light: flexokiGreen600,
|
||||
}
|
||||
theme.MarkdownBlockQuoteColor = lipgloss.AdaptiveColor{
|
||||
Dark: flexokiCyan400,
|
||||
Light: flexokiCyan600,
|
||||
}
|
||||
theme.MarkdownEmphColor = lipgloss.AdaptiveColor{
|
||||
Dark: flexokiYellow400,
|
||||
Light: flexokiYellow600,
|
||||
}
|
||||
theme.MarkdownStrongColor = lipgloss.AdaptiveColor{
|
||||
Dark: flexokiOrange400,
|
||||
Light: flexokiOrange600,
|
||||
}
|
||||
theme.MarkdownHorizontalRuleColor = lipgloss.AdaptiveColor{
|
||||
Dark: flexokiBase800,
|
||||
Light: flexokiBase200,
|
||||
}
|
||||
theme.MarkdownListItemColor = lipgloss.AdaptiveColor{
|
||||
Dark: flexokiBlue400,
|
||||
Light: flexokiBlue600,
|
||||
}
|
||||
theme.MarkdownListEnumerationColor = lipgloss.AdaptiveColor{
|
||||
Dark: flexokiBlue400,
|
||||
Light: flexokiBlue600,
|
||||
}
|
||||
theme.MarkdownImageColor = lipgloss.AdaptiveColor{
|
||||
Dark: flexokiPurple400,
|
||||
Light: flexokiPurple600,
|
||||
}
|
||||
theme.MarkdownImageTextColor = lipgloss.AdaptiveColor{
|
||||
Dark: flexokiMagenta400,
|
||||
Light: flexokiMagenta600,
|
||||
}
|
||||
theme.MarkdownCodeBlockColor = lipgloss.AdaptiveColor{
|
||||
Dark: flexokiBase300,
|
||||
Light: flexokiBase600,
|
||||
}
|
||||
|
||||
// Syntax highlighting colors (based on Flexoki's mappings)
|
||||
theme.SyntaxCommentColor = lipgloss.AdaptiveColor{
|
||||
Dark: flexokiBase700, // tx-3
|
||||
Light: flexokiBase300, // tx-3
|
||||
}
|
||||
theme.SyntaxKeywordColor = lipgloss.AdaptiveColor{
|
||||
Dark: flexokiGreen400, // gr
|
||||
Light: flexokiGreen600, // gr
|
||||
}
|
||||
theme.SyntaxFunctionColor = lipgloss.AdaptiveColor{
|
||||
Dark: flexokiOrange400, // or
|
||||
Light: flexokiOrange600, // or
|
||||
}
|
||||
theme.SyntaxVariableColor = lipgloss.AdaptiveColor{
|
||||
Dark: flexokiBlue400, // bl
|
||||
Light: flexokiBlue600, // bl
|
||||
}
|
||||
theme.SyntaxStringColor = lipgloss.AdaptiveColor{
|
||||
Dark: flexokiCyan400, // cy
|
||||
Light: flexokiCyan600, // cy
|
||||
}
|
||||
theme.SyntaxNumberColor = lipgloss.AdaptiveColor{
|
||||
Dark: flexokiPurple400, // pu
|
||||
Light: flexokiPurple600, // pu
|
||||
}
|
||||
theme.SyntaxTypeColor = lipgloss.AdaptiveColor{
|
||||
Dark: flexokiYellow400, // ye
|
||||
Light: flexokiYellow600, // ye
|
||||
}
|
||||
theme.SyntaxOperatorColor = lipgloss.AdaptiveColor{
|
||||
Dark: flexokiBase500, // tx-2
|
||||
Light: flexokiBase500, // tx-2
|
||||
}
|
||||
theme.SyntaxPunctuationColor = lipgloss.AdaptiveColor{
|
||||
Dark: flexokiBase500, // tx-2
|
||||
Light: flexokiBase500, // tx-2
|
||||
}
|
||||
|
||||
return theme
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Register the Flexoki theme with the theme manager
|
||||
RegisterTheme("flexoki", NewFlexokiTheme())
|
||||
}
|
||||
302
internal/tui/theme/gruvbox.go
Normal file
302
internal/tui/theme/gruvbox.go
Normal file
@@ -0,0 +1,302 @@
|
||||
package theme
|
||||
|
||||
import (
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
// Gruvbox color palette constants
|
||||
const (
|
||||
// Dark theme colors
|
||||
gruvboxDarkBg0 = "#282828"
|
||||
gruvboxDarkBg0Soft = "#32302f"
|
||||
gruvboxDarkBg1 = "#3c3836"
|
||||
gruvboxDarkBg2 = "#504945"
|
||||
gruvboxDarkBg3 = "#665c54"
|
||||
gruvboxDarkBg4 = "#7c6f64"
|
||||
gruvboxDarkFg0 = "#fbf1c7"
|
||||
gruvboxDarkFg1 = "#ebdbb2"
|
||||
gruvboxDarkFg2 = "#d5c4a1"
|
||||
gruvboxDarkFg3 = "#bdae93"
|
||||
gruvboxDarkFg4 = "#a89984"
|
||||
gruvboxDarkGray = "#928374"
|
||||
gruvboxDarkRed = "#cc241d"
|
||||
gruvboxDarkRedBright = "#fb4934"
|
||||
gruvboxDarkGreen = "#98971a"
|
||||
gruvboxDarkGreenBright = "#b8bb26"
|
||||
gruvboxDarkYellow = "#d79921"
|
||||
gruvboxDarkYellowBright = "#fabd2f"
|
||||
gruvboxDarkBlue = "#458588"
|
||||
gruvboxDarkBlueBright = "#83a598"
|
||||
gruvboxDarkPurple = "#b16286"
|
||||
gruvboxDarkPurpleBright = "#d3869b"
|
||||
gruvboxDarkAqua = "#689d6a"
|
||||
gruvboxDarkAquaBright = "#8ec07c"
|
||||
gruvboxDarkOrange = "#d65d0e"
|
||||
gruvboxDarkOrangeBright = "#fe8019"
|
||||
|
||||
// Light theme colors
|
||||
gruvboxLightBg0 = "#fbf1c7"
|
||||
gruvboxLightBg0Soft = "#f2e5bc"
|
||||
gruvboxLightBg1 = "#ebdbb2"
|
||||
gruvboxLightBg2 = "#d5c4a1"
|
||||
gruvboxLightBg3 = "#bdae93"
|
||||
gruvboxLightBg4 = "#a89984"
|
||||
gruvboxLightFg0 = "#282828"
|
||||
gruvboxLightFg1 = "#3c3836"
|
||||
gruvboxLightFg2 = "#504945"
|
||||
gruvboxLightFg3 = "#665c54"
|
||||
gruvboxLightFg4 = "#7c6f64"
|
||||
gruvboxLightGray = "#928374"
|
||||
gruvboxLightRed = "#9d0006"
|
||||
gruvboxLightRedBright = "#cc241d"
|
||||
gruvboxLightGreen = "#79740e"
|
||||
gruvboxLightGreenBright = "#98971a"
|
||||
gruvboxLightYellow = "#b57614"
|
||||
gruvboxLightYellowBright = "#d79921"
|
||||
gruvboxLightBlue = "#076678"
|
||||
gruvboxLightBlueBright = "#458588"
|
||||
gruvboxLightPurple = "#8f3f71"
|
||||
gruvboxLightPurpleBright = "#b16286"
|
||||
gruvboxLightAqua = "#427b58"
|
||||
gruvboxLightAquaBright = "#689d6a"
|
||||
gruvboxLightOrange = "#af3a03"
|
||||
gruvboxLightOrangeBright = "#d65d0e"
|
||||
)
|
||||
|
||||
// GruvboxTheme implements the Theme interface with Gruvbox colors.
|
||||
// It provides both dark and light variants.
|
||||
type GruvboxTheme struct {
|
||||
BaseTheme
|
||||
}
|
||||
|
||||
// NewGruvboxTheme creates a new instance of the Gruvbox theme.
|
||||
func NewGruvboxTheme() *GruvboxTheme {
|
||||
theme := &GruvboxTheme{}
|
||||
|
||||
// Base colors
|
||||
theme.PrimaryColor = lipgloss.AdaptiveColor{
|
||||
Dark: gruvboxDarkBlueBright,
|
||||
Light: gruvboxLightBlueBright,
|
||||
}
|
||||
theme.SecondaryColor = lipgloss.AdaptiveColor{
|
||||
Dark: gruvboxDarkPurpleBright,
|
||||
Light: gruvboxLightPurpleBright,
|
||||
}
|
||||
theme.AccentColor = lipgloss.AdaptiveColor{
|
||||
Dark: gruvboxDarkOrangeBright,
|
||||
Light: gruvboxLightOrangeBright,
|
||||
}
|
||||
|
||||
// Status colors
|
||||
theme.ErrorColor = lipgloss.AdaptiveColor{
|
||||
Dark: gruvboxDarkRedBright,
|
||||
Light: gruvboxLightRedBright,
|
||||
}
|
||||
theme.WarningColor = lipgloss.AdaptiveColor{
|
||||
Dark: gruvboxDarkYellowBright,
|
||||
Light: gruvboxLightYellowBright,
|
||||
}
|
||||
theme.SuccessColor = lipgloss.AdaptiveColor{
|
||||
Dark: gruvboxDarkGreenBright,
|
||||
Light: gruvboxLightGreenBright,
|
||||
}
|
||||
theme.InfoColor = lipgloss.AdaptiveColor{
|
||||
Dark: gruvboxDarkBlueBright,
|
||||
Light: gruvboxLightBlueBright,
|
||||
}
|
||||
|
||||
// Text colors
|
||||
theme.TextColor = lipgloss.AdaptiveColor{
|
||||
Dark: gruvboxDarkFg1,
|
||||
Light: gruvboxLightFg1,
|
||||
}
|
||||
theme.TextMutedColor = lipgloss.AdaptiveColor{
|
||||
Dark: gruvboxDarkFg4,
|
||||
Light: gruvboxLightFg4,
|
||||
}
|
||||
theme.TextEmphasizedColor = lipgloss.AdaptiveColor{
|
||||
Dark: gruvboxDarkYellowBright,
|
||||
Light: gruvboxLightYellowBright,
|
||||
}
|
||||
|
||||
// Background colors
|
||||
theme.BackgroundColor = lipgloss.AdaptiveColor{
|
||||
Dark: gruvboxDarkBg0,
|
||||
Light: gruvboxLightBg0,
|
||||
}
|
||||
theme.BackgroundSecondaryColor = lipgloss.AdaptiveColor{
|
||||
Dark: gruvboxDarkBg1,
|
||||
Light: gruvboxLightBg1,
|
||||
}
|
||||
theme.BackgroundDarkerColor = lipgloss.AdaptiveColor{
|
||||
Dark: gruvboxDarkBg0Soft,
|
||||
Light: gruvboxLightBg0Soft,
|
||||
}
|
||||
|
||||
// Border colors
|
||||
theme.BorderNormalColor = lipgloss.AdaptiveColor{
|
||||
Dark: gruvboxDarkBg2,
|
||||
Light: gruvboxLightBg2,
|
||||
}
|
||||
theme.BorderFocusedColor = lipgloss.AdaptiveColor{
|
||||
Dark: gruvboxDarkBlueBright,
|
||||
Light: gruvboxLightBlueBright,
|
||||
}
|
||||
theme.BorderDimColor = lipgloss.AdaptiveColor{
|
||||
Dark: gruvboxDarkBg1,
|
||||
Light: gruvboxLightBg1,
|
||||
}
|
||||
|
||||
// Diff view colors
|
||||
theme.DiffAddedColor = lipgloss.AdaptiveColor{
|
||||
Dark: gruvboxDarkGreenBright,
|
||||
Light: gruvboxLightGreenBright,
|
||||
}
|
||||
theme.DiffRemovedColor = lipgloss.AdaptiveColor{
|
||||
Dark: gruvboxDarkRedBright,
|
||||
Light: gruvboxLightRedBright,
|
||||
}
|
||||
theme.DiffContextColor = lipgloss.AdaptiveColor{
|
||||
Dark: gruvboxDarkFg4,
|
||||
Light: gruvboxLightFg4,
|
||||
}
|
||||
theme.DiffHunkHeaderColor = lipgloss.AdaptiveColor{
|
||||
Dark: gruvboxDarkFg3,
|
||||
Light: gruvboxLightFg3,
|
||||
}
|
||||
theme.DiffHighlightAddedColor = lipgloss.AdaptiveColor{
|
||||
Dark: gruvboxDarkGreenBright,
|
||||
Light: gruvboxLightGreenBright,
|
||||
}
|
||||
theme.DiffHighlightRemovedColor = lipgloss.AdaptiveColor{
|
||||
Dark: gruvboxDarkRedBright,
|
||||
Light: gruvboxLightRedBright,
|
||||
}
|
||||
theme.DiffAddedBgColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#3C4C3C", // Darker green background
|
||||
Light: "#E8F5E9", // Light green background
|
||||
}
|
||||
theme.DiffRemovedBgColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#4C3C3C", // Darker red background
|
||||
Light: "#FFEBEE", // Light red background
|
||||
}
|
||||
theme.DiffContextBgColor = lipgloss.AdaptiveColor{
|
||||
Dark: gruvboxDarkBg0,
|
||||
Light: gruvboxLightBg0,
|
||||
}
|
||||
theme.DiffLineNumberColor = lipgloss.AdaptiveColor{
|
||||
Dark: gruvboxDarkFg4,
|
||||
Light: gruvboxLightFg4,
|
||||
}
|
||||
theme.DiffAddedLineNumberBgColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#32432F", // Slightly darker green
|
||||
Light: "#C8E6C9", // Light green
|
||||
}
|
||||
theme.DiffRemovedLineNumberBgColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#43322F", // Slightly darker red
|
||||
Light: "#FFCDD2", // Light red
|
||||
}
|
||||
|
||||
// Markdown colors
|
||||
theme.MarkdownTextColor = lipgloss.AdaptiveColor{
|
||||
Dark: gruvboxDarkFg1,
|
||||
Light: gruvboxLightFg1,
|
||||
}
|
||||
theme.MarkdownHeadingColor = lipgloss.AdaptiveColor{
|
||||
Dark: gruvboxDarkYellowBright,
|
||||
Light: gruvboxLightYellowBright,
|
||||
}
|
||||
theme.MarkdownLinkColor = lipgloss.AdaptiveColor{
|
||||
Dark: gruvboxDarkBlueBright,
|
||||
Light: gruvboxLightBlueBright,
|
||||
}
|
||||
theme.MarkdownLinkTextColor = lipgloss.AdaptiveColor{
|
||||
Dark: gruvboxDarkAquaBright,
|
||||
Light: gruvboxLightAquaBright,
|
||||
}
|
||||
theme.MarkdownCodeColor = lipgloss.AdaptiveColor{
|
||||
Dark: gruvboxDarkGreenBright,
|
||||
Light: gruvboxLightGreenBright,
|
||||
}
|
||||
theme.MarkdownBlockQuoteColor = lipgloss.AdaptiveColor{
|
||||
Dark: gruvboxDarkAquaBright,
|
||||
Light: gruvboxLightAquaBright,
|
||||
}
|
||||
theme.MarkdownEmphColor = lipgloss.AdaptiveColor{
|
||||
Dark: gruvboxDarkYellowBright,
|
||||
Light: gruvboxLightYellowBright,
|
||||
}
|
||||
theme.MarkdownStrongColor = lipgloss.AdaptiveColor{
|
||||
Dark: gruvboxDarkOrangeBright,
|
||||
Light: gruvboxLightOrangeBright,
|
||||
}
|
||||
theme.MarkdownHorizontalRuleColor = lipgloss.AdaptiveColor{
|
||||
Dark: gruvboxDarkBg3,
|
||||
Light: gruvboxLightBg3,
|
||||
}
|
||||
theme.MarkdownListItemColor = lipgloss.AdaptiveColor{
|
||||
Dark: gruvboxDarkBlueBright,
|
||||
Light: gruvboxLightBlueBright,
|
||||
}
|
||||
theme.MarkdownListEnumerationColor = lipgloss.AdaptiveColor{
|
||||
Dark: gruvboxDarkBlueBright,
|
||||
Light: gruvboxLightBlueBright,
|
||||
}
|
||||
theme.MarkdownImageColor = lipgloss.AdaptiveColor{
|
||||
Dark: gruvboxDarkPurpleBright,
|
||||
Light: gruvboxLightPurpleBright,
|
||||
}
|
||||
theme.MarkdownImageTextColor = lipgloss.AdaptiveColor{
|
||||
Dark: gruvboxDarkAquaBright,
|
||||
Light: gruvboxLightAquaBright,
|
||||
}
|
||||
theme.MarkdownCodeBlockColor = lipgloss.AdaptiveColor{
|
||||
Dark: gruvboxDarkFg1,
|
||||
Light: gruvboxLightFg1,
|
||||
}
|
||||
|
||||
// Syntax highlighting colors
|
||||
theme.SyntaxCommentColor = lipgloss.AdaptiveColor{
|
||||
Dark: gruvboxDarkGray,
|
||||
Light: gruvboxLightGray,
|
||||
}
|
||||
theme.SyntaxKeywordColor = lipgloss.AdaptiveColor{
|
||||
Dark: gruvboxDarkRedBright,
|
||||
Light: gruvboxLightRedBright,
|
||||
}
|
||||
theme.SyntaxFunctionColor = lipgloss.AdaptiveColor{
|
||||
Dark: gruvboxDarkGreenBright,
|
||||
Light: gruvboxLightGreenBright,
|
||||
}
|
||||
theme.SyntaxVariableColor = lipgloss.AdaptiveColor{
|
||||
Dark: gruvboxDarkBlueBright,
|
||||
Light: gruvboxLightBlueBright,
|
||||
}
|
||||
theme.SyntaxStringColor = lipgloss.AdaptiveColor{
|
||||
Dark: gruvboxDarkYellowBright,
|
||||
Light: gruvboxLightYellowBright,
|
||||
}
|
||||
theme.SyntaxNumberColor = lipgloss.AdaptiveColor{
|
||||
Dark: gruvboxDarkPurpleBright,
|
||||
Light: gruvboxLightPurpleBright,
|
||||
}
|
||||
theme.SyntaxTypeColor = lipgloss.AdaptiveColor{
|
||||
Dark: gruvboxDarkYellow,
|
||||
Light: gruvboxLightYellow,
|
||||
}
|
||||
theme.SyntaxOperatorColor = lipgloss.AdaptiveColor{
|
||||
Dark: gruvboxDarkAquaBright,
|
||||
Light: gruvboxLightAquaBright,
|
||||
}
|
||||
theme.SyntaxPunctuationColor = lipgloss.AdaptiveColor{
|
||||
Dark: gruvboxDarkFg1,
|
||||
Light: gruvboxLightFg1,
|
||||
}
|
||||
|
||||
return theme
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Register the Gruvbox theme with the theme manager
|
||||
RegisterTheme("gruvbox", NewGruvboxTheme())
|
||||
}
|
||||
118
internal/tui/theme/manager.go
Normal file
118
internal/tui/theme/manager.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package theme
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/alecthomas/chroma/v2/styles"
|
||||
"github.com/opencode-ai/opencode/internal/config"
|
||||
"github.com/opencode-ai/opencode/internal/logging"
|
||||
)
|
||||
|
||||
// Manager handles theme registration, selection, and retrieval.
|
||||
// It maintains a registry of available themes and tracks the currently active theme.
|
||||
type Manager struct {
|
||||
themes map[string]Theme
|
||||
currentName string
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// Global instance of the theme manager
|
||||
var globalManager = &Manager{
|
||||
themes: make(map[string]Theme),
|
||||
currentName: "",
|
||||
}
|
||||
|
||||
// RegisterTheme adds a new theme to the registry.
|
||||
// If this is the first theme registered, it becomes the default.
|
||||
func RegisterTheme(name string, theme Theme) {
|
||||
globalManager.mu.Lock()
|
||||
defer globalManager.mu.Unlock()
|
||||
|
||||
globalManager.themes[name] = theme
|
||||
|
||||
// If this is the first theme, make it the default
|
||||
if globalManager.currentName == "" {
|
||||
globalManager.currentName = name
|
||||
}
|
||||
}
|
||||
|
||||
// SetTheme changes the active theme to the one with the specified name.
|
||||
// Returns an error if the theme doesn't exist.
|
||||
func SetTheme(name string) error {
|
||||
globalManager.mu.Lock()
|
||||
defer globalManager.mu.Unlock()
|
||||
|
||||
delete(styles.Registry, "charm")
|
||||
if _, exists := globalManager.themes[name]; !exists {
|
||||
return fmt.Errorf("theme '%s' not found", name)
|
||||
}
|
||||
|
||||
globalManager.currentName = name
|
||||
|
||||
// Update the config file using viper
|
||||
if err := updateConfigTheme(name); err != nil {
|
||||
// Log the error but don't fail the theme change
|
||||
logging.Warn("Warning: Failed to update config file with new theme", "err", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CurrentTheme returns the currently active theme.
|
||||
// If no theme is set, it returns nil.
|
||||
func CurrentTheme() Theme {
|
||||
globalManager.mu.RLock()
|
||||
defer globalManager.mu.RUnlock()
|
||||
|
||||
if globalManager.currentName == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
return globalManager.themes[globalManager.currentName]
|
||||
}
|
||||
|
||||
// CurrentThemeName returns the name of the currently active theme.
|
||||
func CurrentThemeName() string {
|
||||
globalManager.mu.RLock()
|
||||
defer globalManager.mu.RUnlock()
|
||||
|
||||
return globalManager.currentName
|
||||
}
|
||||
|
||||
// AvailableThemes returns a list of all registered theme names.
|
||||
func AvailableThemes() []string {
|
||||
globalManager.mu.RLock()
|
||||
defer globalManager.mu.RUnlock()
|
||||
|
||||
names := make([]string, 0, len(globalManager.themes))
|
||||
for name := range globalManager.themes {
|
||||
names = append(names, name)
|
||||
}
|
||||
slices.SortFunc(names, func(a, b string) int {
|
||||
if a == "opencode" {
|
||||
return -1
|
||||
} else if b == "opencode" {
|
||||
return 1
|
||||
}
|
||||
return strings.Compare(a, b)
|
||||
})
|
||||
return names
|
||||
}
|
||||
|
||||
// GetTheme returns a specific theme by name.
|
||||
// Returns nil if the theme doesn't exist.
|
||||
func GetTheme(name string) Theme {
|
||||
globalManager.mu.RLock()
|
||||
defer globalManager.mu.RUnlock()
|
||||
|
||||
return globalManager.themes[name]
|
||||
}
|
||||
|
||||
// updateConfigTheme updates the theme setting in the configuration file
|
||||
func updateConfigTheme(themeName string) error {
|
||||
// Use the config package to update the theme
|
||||
return config.UpdateTheme(themeName)
|
||||
}
|
||||
273
internal/tui/theme/monokai.go
Normal file
273
internal/tui/theme/monokai.go
Normal file
@@ -0,0 +1,273 @@
|
||||
package theme
|
||||
|
||||
import (
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
// MonokaiProTheme implements the Theme interface with Monokai Pro colors.
|
||||
// It provides both dark and light variants.
|
||||
type MonokaiProTheme struct {
|
||||
BaseTheme
|
||||
}
|
||||
|
||||
// NewMonokaiProTheme creates a new instance of the Monokai Pro theme.
|
||||
func NewMonokaiProTheme() *MonokaiProTheme {
|
||||
// Monokai Pro color palette (dark mode)
|
||||
darkBackground := "#2d2a2e"
|
||||
darkCurrentLine := "#403e41"
|
||||
darkSelection := "#5b595c"
|
||||
darkForeground := "#fcfcfa"
|
||||
darkComment := "#727072"
|
||||
darkRed := "#ff6188"
|
||||
darkOrange := "#fc9867"
|
||||
darkYellow := "#ffd866"
|
||||
darkGreen := "#a9dc76"
|
||||
darkCyan := "#78dce8"
|
||||
darkBlue := "#ab9df2"
|
||||
darkPurple := "#ab9df2"
|
||||
darkBorder := "#403e41"
|
||||
|
||||
// Light mode colors (adapted from dark)
|
||||
lightBackground := "#fafafa"
|
||||
lightCurrentLine := "#f0f0f0"
|
||||
lightSelection := "#e5e5e6"
|
||||
lightForeground := "#2d2a2e"
|
||||
lightComment := "#939293"
|
||||
lightRed := "#f92672"
|
||||
lightOrange := "#fd971f"
|
||||
lightYellow := "#e6db74"
|
||||
lightGreen := "#9bca65"
|
||||
lightCyan := "#66d9ef"
|
||||
lightBlue := "#7e75db"
|
||||
lightPurple := "#ae81ff"
|
||||
lightBorder := "#d3d3d3"
|
||||
|
||||
theme := &MonokaiProTheme{}
|
||||
|
||||
// Base colors
|
||||
theme.PrimaryColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkCyan,
|
||||
Light: lightCyan,
|
||||
}
|
||||
theme.SecondaryColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkPurple,
|
||||
Light: lightPurple,
|
||||
}
|
||||
theme.AccentColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkOrange,
|
||||
Light: lightOrange,
|
||||
}
|
||||
|
||||
// Status colors
|
||||
theme.ErrorColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkRed,
|
||||
Light: lightRed,
|
||||
}
|
||||
theme.WarningColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkOrange,
|
||||
Light: lightOrange,
|
||||
}
|
||||
theme.SuccessColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkGreen,
|
||||
Light: lightGreen,
|
||||
}
|
||||
theme.InfoColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkBlue,
|
||||
Light: lightBlue,
|
||||
}
|
||||
|
||||
// Text colors
|
||||
theme.TextColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkForeground,
|
||||
Light: lightForeground,
|
||||
}
|
||||
theme.TextMutedColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkComment,
|
||||
Light: lightComment,
|
||||
}
|
||||
theme.TextEmphasizedColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkYellow,
|
||||
Light: lightYellow,
|
||||
}
|
||||
|
||||
// Background colors
|
||||
theme.BackgroundColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkBackground,
|
||||
Light: lightBackground,
|
||||
}
|
||||
theme.BackgroundSecondaryColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkCurrentLine,
|
||||
Light: lightCurrentLine,
|
||||
}
|
||||
theme.BackgroundDarkerColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#221f22", // Slightly darker than background
|
||||
Light: "#ffffff", // Slightly lighter than background
|
||||
}
|
||||
|
||||
// Border colors
|
||||
theme.BorderNormalColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkBorder,
|
||||
Light: lightBorder,
|
||||
}
|
||||
theme.BorderFocusedColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkCyan,
|
||||
Light: lightCyan,
|
||||
}
|
||||
theme.BorderDimColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkSelection,
|
||||
Light: lightSelection,
|
||||
}
|
||||
|
||||
// Diff view colors
|
||||
theme.DiffAddedColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#a9dc76",
|
||||
Light: "#9bca65",
|
||||
}
|
||||
theme.DiffRemovedColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#ff6188",
|
||||
Light: "#f92672",
|
||||
}
|
||||
theme.DiffContextColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#a0a0a0",
|
||||
Light: "#757575",
|
||||
}
|
||||
theme.DiffHunkHeaderColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#a0a0a0",
|
||||
Light: "#757575",
|
||||
}
|
||||
theme.DiffHighlightAddedColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#c2e7a9",
|
||||
Light: "#c5e0b4",
|
||||
}
|
||||
theme.DiffHighlightRemovedColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#ff8ca6",
|
||||
Light: "#ffb3c8",
|
||||
}
|
||||
theme.DiffAddedBgColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#3a4a35",
|
||||
Light: "#e8f5e9",
|
||||
}
|
||||
theme.DiffRemovedBgColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#4a3439",
|
||||
Light: "#ffebee",
|
||||
}
|
||||
theme.DiffContextBgColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkBackground,
|
||||
Light: lightBackground,
|
||||
}
|
||||
theme.DiffLineNumberColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#888888",
|
||||
Light: "#9e9e9e",
|
||||
}
|
||||
theme.DiffAddedLineNumberBgColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#2d3a28",
|
||||
Light: "#c8e6c9",
|
||||
}
|
||||
theme.DiffRemovedLineNumberBgColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#3d2a2e",
|
||||
Light: "#ffcdd2",
|
||||
}
|
||||
|
||||
// Markdown colors
|
||||
theme.MarkdownTextColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkForeground,
|
||||
Light: lightForeground,
|
||||
}
|
||||
theme.MarkdownHeadingColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkPurple,
|
||||
Light: lightPurple,
|
||||
}
|
||||
theme.MarkdownLinkColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkCyan,
|
||||
Light: lightCyan,
|
||||
}
|
||||
theme.MarkdownLinkTextColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkBlue,
|
||||
Light: lightBlue,
|
||||
}
|
||||
theme.MarkdownCodeColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkGreen,
|
||||
Light: lightGreen,
|
||||
}
|
||||
theme.MarkdownBlockQuoteColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkYellow,
|
||||
Light: lightYellow,
|
||||
}
|
||||
theme.MarkdownEmphColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkYellow,
|
||||
Light: lightYellow,
|
||||
}
|
||||
theme.MarkdownStrongColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkOrange,
|
||||
Light: lightOrange,
|
||||
}
|
||||
theme.MarkdownHorizontalRuleColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkComment,
|
||||
Light: lightComment,
|
||||
}
|
||||
theme.MarkdownListItemColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkCyan,
|
||||
Light: lightCyan,
|
||||
}
|
||||
theme.MarkdownListEnumerationColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkBlue,
|
||||
Light: lightBlue,
|
||||
}
|
||||
theme.MarkdownImageColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkCyan,
|
||||
Light: lightCyan,
|
||||
}
|
||||
theme.MarkdownImageTextColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkBlue,
|
||||
Light: lightBlue,
|
||||
}
|
||||
theme.MarkdownCodeBlockColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkForeground,
|
||||
Light: lightForeground,
|
||||
}
|
||||
|
||||
// Syntax highlighting colors
|
||||
theme.SyntaxCommentColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkComment,
|
||||
Light: lightComment,
|
||||
}
|
||||
theme.SyntaxKeywordColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkRed,
|
||||
Light: lightRed,
|
||||
}
|
||||
theme.SyntaxFunctionColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkGreen,
|
||||
Light: lightGreen,
|
||||
}
|
||||
theme.SyntaxVariableColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkForeground,
|
||||
Light: lightForeground,
|
||||
}
|
||||
theme.SyntaxStringColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkYellow,
|
||||
Light: lightYellow,
|
||||
}
|
||||
theme.SyntaxNumberColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkPurple,
|
||||
Light: lightPurple,
|
||||
}
|
||||
theme.SyntaxTypeColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkBlue,
|
||||
Light: lightBlue,
|
||||
}
|
||||
theme.SyntaxOperatorColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkCyan,
|
||||
Light: lightCyan,
|
||||
}
|
||||
theme.SyntaxPunctuationColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkForeground,
|
||||
Light: lightForeground,
|
||||
}
|
||||
|
||||
return theme
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Register the Monokai Pro theme with the theme manager
|
||||
RegisterTheme("monokai", NewMonokaiProTheme())
|
||||
}
|
||||
274
internal/tui/theme/onedark.go
Normal file
274
internal/tui/theme/onedark.go
Normal file
@@ -0,0 +1,274 @@
|
||||
package theme
|
||||
|
||||
import (
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
// OneDarkTheme implements the Theme interface with Atom's One Dark colors.
|
||||
// It provides both dark and light variants.
|
||||
type OneDarkTheme struct {
|
||||
BaseTheme
|
||||
}
|
||||
|
||||
// NewOneDarkTheme creates a new instance of the One Dark theme.
|
||||
func NewOneDarkTheme() *OneDarkTheme {
|
||||
// One Dark color palette
|
||||
// Dark mode colors from Atom One Dark
|
||||
darkBackground := "#282c34"
|
||||
darkCurrentLine := "#2c313c"
|
||||
darkSelection := "#3e4451"
|
||||
darkForeground := "#abb2bf"
|
||||
darkComment := "#5c6370"
|
||||
darkRed := "#e06c75"
|
||||
darkOrange := "#d19a66"
|
||||
darkYellow := "#e5c07b"
|
||||
darkGreen := "#98c379"
|
||||
darkCyan := "#56b6c2"
|
||||
darkBlue := "#61afef"
|
||||
darkPurple := "#c678dd"
|
||||
darkBorder := "#3b4048"
|
||||
|
||||
// Light mode colors from Atom One Light
|
||||
lightBackground := "#fafafa"
|
||||
lightCurrentLine := "#f0f0f0"
|
||||
lightSelection := "#e5e5e6"
|
||||
lightForeground := "#383a42"
|
||||
lightComment := "#a0a1a7"
|
||||
lightRed := "#e45649"
|
||||
lightOrange := "#da8548"
|
||||
lightYellow := "#c18401"
|
||||
lightGreen := "#50a14f"
|
||||
lightCyan := "#0184bc"
|
||||
lightBlue := "#4078f2"
|
||||
lightPurple := "#a626a4"
|
||||
lightBorder := "#d3d3d3"
|
||||
|
||||
theme := &OneDarkTheme{}
|
||||
|
||||
// Base colors
|
||||
theme.PrimaryColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkBlue,
|
||||
Light: lightBlue,
|
||||
}
|
||||
theme.SecondaryColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkPurple,
|
||||
Light: lightPurple,
|
||||
}
|
||||
theme.AccentColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkOrange,
|
||||
Light: lightOrange,
|
||||
}
|
||||
|
||||
// Status colors
|
||||
theme.ErrorColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkRed,
|
||||
Light: lightRed,
|
||||
}
|
||||
theme.WarningColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkOrange,
|
||||
Light: lightOrange,
|
||||
}
|
||||
theme.SuccessColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkGreen,
|
||||
Light: lightGreen,
|
||||
}
|
||||
theme.InfoColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkBlue,
|
||||
Light: lightBlue,
|
||||
}
|
||||
|
||||
// Text colors
|
||||
theme.TextColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkForeground,
|
||||
Light: lightForeground,
|
||||
}
|
||||
theme.TextMutedColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkComment,
|
||||
Light: lightComment,
|
||||
}
|
||||
theme.TextEmphasizedColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkYellow,
|
||||
Light: lightYellow,
|
||||
}
|
||||
|
||||
// Background colors
|
||||
theme.BackgroundColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkBackground,
|
||||
Light: lightBackground,
|
||||
}
|
||||
theme.BackgroundSecondaryColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkCurrentLine,
|
||||
Light: lightCurrentLine,
|
||||
}
|
||||
theme.BackgroundDarkerColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#21252b", // Slightly darker than background
|
||||
Light: "#ffffff", // Slightly lighter than background
|
||||
}
|
||||
|
||||
// Border colors
|
||||
theme.BorderNormalColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkBorder,
|
||||
Light: lightBorder,
|
||||
}
|
||||
theme.BorderFocusedColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkBlue,
|
||||
Light: lightBlue,
|
||||
}
|
||||
theme.BorderDimColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkSelection,
|
||||
Light: lightSelection,
|
||||
}
|
||||
|
||||
// Diff view colors
|
||||
theme.DiffAddedColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#478247",
|
||||
Light: "#2E7D32",
|
||||
}
|
||||
theme.DiffRemovedColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#7C4444",
|
||||
Light: "#C62828",
|
||||
}
|
||||
theme.DiffContextColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#a0a0a0",
|
||||
Light: "#757575",
|
||||
}
|
||||
theme.DiffHunkHeaderColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#a0a0a0",
|
||||
Light: "#757575",
|
||||
}
|
||||
theme.DiffHighlightAddedColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#DAFADA",
|
||||
Light: "#A5D6A7",
|
||||
}
|
||||
theme.DiffHighlightRemovedColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#FADADD",
|
||||
Light: "#EF9A9A",
|
||||
}
|
||||
theme.DiffAddedBgColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#303A30",
|
||||
Light: "#E8F5E9",
|
||||
}
|
||||
theme.DiffRemovedBgColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#3A3030",
|
||||
Light: "#FFEBEE",
|
||||
}
|
||||
theme.DiffContextBgColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkBackground,
|
||||
Light: lightBackground,
|
||||
}
|
||||
theme.DiffLineNumberColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#888888",
|
||||
Light: "#9E9E9E",
|
||||
}
|
||||
theme.DiffAddedLineNumberBgColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#293229",
|
||||
Light: "#C8E6C9",
|
||||
}
|
||||
theme.DiffRemovedLineNumberBgColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#332929",
|
||||
Light: "#FFCDD2",
|
||||
}
|
||||
|
||||
// Markdown colors
|
||||
theme.MarkdownTextColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkForeground,
|
||||
Light: lightForeground,
|
||||
}
|
||||
theme.MarkdownHeadingColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkPurple,
|
||||
Light: lightPurple,
|
||||
}
|
||||
theme.MarkdownLinkColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkBlue,
|
||||
Light: lightBlue,
|
||||
}
|
||||
theme.MarkdownLinkTextColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkCyan,
|
||||
Light: lightCyan,
|
||||
}
|
||||
theme.MarkdownCodeColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkGreen,
|
||||
Light: lightGreen,
|
||||
}
|
||||
theme.MarkdownBlockQuoteColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkYellow,
|
||||
Light: lightYellow,
|
||||
}
|
||||
theme.MarkdownEmphColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkYellow,
|
||||
Light: lightYellow,
|
||||
}
|
||||
theme.MarkdownStrongColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkOrange,
|
||||
Light: lightOrange,
|
||||
}
|
||||
theme.MarkdownHorizontalRuleColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkComment,
|
||||
Light: lightComment,
|
||||
}
|
||||
theme.MarkdownListItemColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkBlue,
|
||||
Light: lightBlue,
|
||||
}
|
||||
theme.MarkdownListEnumerationColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkCyan,
|
||||
Light: lightCyan,
|
||||
}
|
||||
theme.MarkdownImageColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkBlue,
|
||||
Light: lightBlue,
|
||||
}
|
||||
theme.MarkdownImageTextColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkCyan,
|
||||
Light: lightCyan,
|
||||
}
|
||||
theme.MarkdownCodeBlockColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkForeground,
|
||||
Light: lightForeground,
|
||||
}
|
||||
|
||||
// Syntax highlighting colors
|
||||
theme.SyntaxCommentColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkComment,
|
||||
Light: lightComment,
|
||||
}
|
||||
theme.SyntaxKeywordColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkPurple,
|
||||
Light: lightPurple,
|
||||
}
|
||||
theme.SyntaxFunctionColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkBlue,
|
||||
Light: lightBlue,
|
||||
}
|
||||
theme.SyntaxVariableColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkRed,
|
||||
Light: lightRed,
|
||||
}
|
||||
theme.SyntaxStringColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkGreen,
|
||||
Light: lightGreen,
|
||||
}
|
||||
theme.SyntaxNumberColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkOrange,
|
||||
Light: lightOrange,
|
||||
}
|
||||
theme.SyntaxTypeColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkYellow,
|
||||
Light: lightYellow,
|
||||
}
|
||||
theme.SyntaxOperatorColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkCyan,
|
||||
Light: lightCyan,
|
||||
}
|
||||
theme.SyntaxPunctuationColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkForeground,
|
||||
Light: lightForeground,
|
||||
}
|
||||
|
||||
return theme
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Register the One Dark theme with the theme manager
|
||||
RegisterTheme("onedark", NewOneDarkTheme())
|
||||
}
|
||||
277
internal/tui/theme/opencode.go
Normal file
277
internal/tui/theme/opencode.go
Normal file
@@ -0,0 +1,277 @@
|
||||
package theme
|
||||
|
||||
import (
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
// OpenCodeTheme implements the Theme interface with OpenCode brand colors.
|
||||
// It provides both dark and light variants.
|
||||
type OpenCodeTheme struct {
|
||||
BaseTheme
|
||||
}
|
||||
|
||||
// NewOpenCodeTheme creates a new instance of the OpenCode theme.
|
||||
func NewOpenCodeTheme() *OpenCodeTheme {
|
||||
// OpenCode color palette
|
||||
// Dark mode colors
|
||||
darkBackground := "#212121"
|
||||
darkCurrentLine := "#252525"
|
||||
darkSelection := "#303030"
|
||||
darkForeground := "#e0e0e0"
|
||||
darkComment := "#6a6a6a"
|
||||
darkPrimary := "#fab283" // Primary orange/gold
|
||||
darkSecondary := "#5c9cf5" // Secondary blue
|
||||
darkAccent := "#9d7cd8" // Accent purple
|
||||
darkRed := "#e06c75" // Error red
|
||||
darkOrange := "#f5a742" // Warning orange
|
||||
darkGreen := "#7fd88f" // Success green
|
||||
darkCyan := "#56b6c2" // Info cyan
|
||||
darkYellow := "#e5c07b" // Emphasized text
|
||||
darkBorder := "#4b4c5c" // Border color
|
||||
|
||||
// Light mode colors
|
||||
lightBackground := "#f8f8f8"
|
||||
lightCurrentLine := "#f0f0f0"
|
||||
lightSelection := "#e5e5e6"
|
||||
lightForeground := "#2a2a2a"
|
||||
lightComment := "#8a8a8a"
|
||||
lightPrimary := "#3b7dd8" // Primary blue
|
||||
lightSecondary := "#7b5bb6" // Secondary purple
|
||||
lightAccent := "#d68c27" // Accent orange/gold
|
||||
lightRed := "#d1383d" // Error red
|
||||
lightOrange := "#d68c27" // Warning orange
|
||||
lightGreen := "#3d9a57" // Success green
|
||||
lightCyan := "#318795" // Info cyan
|
||||
lightYellow := "#b0851f" // Emphasized text
|
||||
lightBorder := "#d3d3d3" // Border color
|
||||
|
||||
theme := &OpenCodeTheme{}
|
||||
|
||||
// Base colors
|
||||
theme.PrimaryColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkPrimary,
|
||||
Light: lightPrimary,
|
||||
}
|
||||
theme.SecondaryColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkSecondary,
|
||||
Light: lightSecondary,
|
||||
}
|
||||
theme.AccentColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkAccent,
|
||||
Light: lightAccent,
|
||||
}
|
||||
|
||||
// Status colors
|
||||
theme.ErrorColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkRed,
|
||||
Light: lightRed,
|
||||
}
|
||||
theme.WarningColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkOrange,
|
||||
Light: lightOrange,
|
||||
}
|
||||
theme.SuccessColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkGreen,
|
||||
Light: lightGreen,
|
||||
}
|
||||
theme.InfoColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkCyan,
|
||||
Light: lightCyan,
|
||||
}
|
||||
|
||||
// Text colors
|
||||
theme.TextColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkForeground,
|
||||
Light: lightForeground,
|
||||
}
|
||||
theme.TextMutedColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkComment,
|
||||
Light: lightComment,
|
||||
}
|
||||
theme.TextEmphasizedColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkYellow,
|
||||
Light: lightYellow,
|
||||
}
|
||||
|
||||
// Background colors
|
||||
theme.BackgroundColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkBackground,
|
||||
Light: lightBackground,
|
||||
}
|
||||
theme.BackgroundSecondaryColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkCurrentLine,
|
||||
Light: lightCurrentLine,
|
||||
}
|
||||
theme.BackgroundDarkerColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#121212", // Slightly darker than background
|
||||
Light: "#ffffff", // Slightly lighter than background
|
||||
}
|
||||
|
||||
// Border colors
|
||||
theme.BorderNormalColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkBorder,
|
||||
Light: lightBorder,
|
||||
}
|
||||
theme.BorderFocusedColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkPrimary,
|
||||
Light: lightPrimary,
|
||||
}
|
||||
theme.BorderDimColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkSelection,
|
||||
Light: lightSelection,
|
||||
}
|
||||
|
||||
// Diff view colors
|
||||
theme.DiffAddedColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#478247",
|
||||
Light: "#2E7D32",
|
||||
}
|
||||
theme.DiffRemovedColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#7C4444",
|
||||
Light: "#C62828",
|
||||
}
|
||||
theme.DiffContextColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#a0a0a0",
|
||||
Light: "#757575",
|
||||
}
|
||||
theme.DiffHunkHeaderColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#a0a0a0",
|
||||
Light: "#757575",
|
||||
}
|
||||
theme.DiffHighlightAddedColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#DAFADA",
|
||||
Light: "#A5D6A7",
|
||||
}
|
||||
theme.DiffHighlightRemovedColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#FADADD",
|
||||
Light: "#EF9A9A",
|
||||
}
|
||||
theme.DiffAddedBgColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#303A30",
|
||||
Light: "#E8F5E9",
|
||||
}
|
||||
theme.DiffRemovedBgColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#3A3030",
|
||||
Light: "#FFEBEE",
|
||||
}
|
||||
theme.DiffContextBgColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkBackground,
|
||||
Light: lightBackground,
|
||||
}
|
||||
theme.DiffLineNumberColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#888888",
|
||||
Light: "#9E9E9E",
|
||||
}
|
||||
theme.DiffAddedLineNumberBgColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#293229",
|
||||
Light: "#C8E6C9",
|
||||
}
|
||||
theme.DiffRemovedLineNumberBgColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#332929",
|
||||
Light: "#FFCDD2",
|
||||
}
|
||||
|
||||
// Markdown colors
|
||||
theme.MarkdownTextColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkForeground,
|
||||
Light: lightForeground,
|
||||
}
|
||||
theme.MarkdownHeadingColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkSecondary,
|
||||
Light: lightSecondary,
|
||||
}
|
||||
theme.MarkdownLinkColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkPrimary,
|
||||
Light: lightPrimary,
|
||||
}
|
||||
theme.MarkdownLinkTextColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkCyan,
|
||||
Light: lightCyan,
|
||||
}
|
||||
theme.MarkdownCodeColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkGreen,
|
||||
Light: lightGreen,
|
||||
}
|
||||
theme.MarkdownBlockQuoteColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkYellow,
|
||||
Light: lightYellow,
|
||||
}
|
||||
theme.MarkdownEmphColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkYellow,
|
||||
Light: lightYellow,
|
||||
}
|
||||
theme.MarkdownStrongColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkAccent,
|
||||
Light: lightAccent,
|
||||
}
|
||||
theme.MarkdownHorizontalRuleColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkComment,
|
||||
Light: lightComment,
|
||||
}
|
||||
theme.MarkdownListItemColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkPrimary,
|
||||
Light: lightPrimary,
|
||||
}
|
||||
theme.MarkdownListEnumerationColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkCyan,
|
||||
Light: lightCyan,
|
||||
}
|
||||
theme.MarkdownImageColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkPrimary,
|
||||
Light: lightPrimary,
|
||||
}
|
||||
theme.MarkdownImageTextColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkCyan,
|
||||
Light: lightCyan,
|
||||
}
|
||||
theme.MarkdownCodeBlockColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkForeground,
|
||||
Light: lightForeground,
|
||||
}
|
||||
|
||||
// Syntax highlighting colors
|
||||
theme.SyntaxCommentColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkComment,
|
||||
Light: lightComment,
|
||||
}
|
||||
theme.SyntaxKeywordColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkSecondary,
|
||||
Light: lightSecondary,
|
||||
}
|
||||
theme.SyntaxFunctionColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkPrimary,
|
||||
Light: lightPrimary,
|
||||
}
|
||||
theme.SyntaxVariableColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkRed,
|
||||
Light: lightRed,
|
||||
}
|
||||
theme.SyntaxStringColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkGreen,
|
||||
Light: lightGreen,
|
||||
}
|
||||
theme.SyntaxNumberColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkAccent,
|
||||
Light: lightAccent,
|
||||
}
|
||||
theme.SyntaxTypeColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkYellow,
|
||||
Light: lightYellow,
|
||||
}
|
||||
theme.SyntaxOperatorColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkCyan,
|
||||
Light: lightCyan,
|
||||
}
|
||||
theme.SyntaxPunctuationColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkForeground,
|
||||
Light: lightForeground,
|
||||
}
|
||||
|
||||
return theme
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Register the OpenCode theme with the theme manager
|
||||
RegisterTheme("opencode", NewOpenCodeTheme())
|
||||
}
|
||||
|
||||
208
internal/tui/theme/theme.go
Normal file
208
internal/tui/theme/theme.go
Normal file
@@ -0,0 +1,208 @@
|
||||
package theme
|
||||
|
||||
import (
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
// Theme defines the interface for all UI themes in the application.
|
||||
// All colors must be defined as lipgloss.AdaptiveColor to support
|
||||
// both light and dark terminal backgrounds.
|
||||
type Theme interface {
|
||||
// Base colors
|
||||
Primary() lipgloss.AdaptiveColor
|
||||
Secondary() lipgloss.AdaptiveColor
|
||||
Accent() lipgloss.AdaptiveColor
|
||||
|
||||
// Status colors
|
||||
Error() lipgloss.AdaptiveColor
|
||||
Warning() lipgloss.AdaptiveColor
|
||||
Success() lipgloss.AdaptiveColor
|
||||
Info() lipgloss.AdaptiveColor
|
||||
|
||||
// Text colors
|
||||
Text() lipgloss.AdaptiveColor
|
||||
TextMuted() lipgloss.AdaptiveColor
|
||||
TextEmphasized() lipgloss.AdaptiveColor
|
||||
|
||||
// Background colors
|
||||
Background() lipgloss.AdaptiveColor
|
||||
BackgroundSecondary() lipgloss.AdaptiveColor
|
||||
BackgroundDarker() lipgloss.AdaptiveColor
|
||||
|
||||
// Border colors
|
||||
BorderNormal() lipgloss.AdaptiveColor
|
||||
BorderFocused() lipgloss.AdaptiveColor
|
||||
BorderDim() lipgloss.AdaptiveColor
|
||||
|
||||
// Diff view colors
|
||||
DiffAdded() lipgloss.AdaptiveColor
|
||||
DiffRemoved() lipgloss.AdaptiveColor
|
||||
DiffContext() lipgloss.AdaptiveColor
|
||||
DiffHunkHeader() lipgloss.AdaptiveColor
|
||||
DiffHighlightAdded() lipgloss.AdaptiveColor
|
||||
DiffHighlightRemoved() lipgloss.AdaptiveColor
|
||||
DiffAddedBg() lipgloss.AdaptiveColor
|
||||
DiffRemovedBg() lipgloss.AdaptiveColor
|
||||
DiffContextBg() lipgloss.AdaptiveColor
|
||||
DiffLineNumber() lipgloss.AdaptiveColor
|
||||
DiffAddedLineNumberBg() lipgloss.AdaptiveColor
|
||||
DiffRemovedLineNumberBg() lipgloss.AdaptiveColor
|
||||
|
||||
// Markdown colors
|
||||
MarkdownText() lipgloss.AdaptiveColor
|
||||
MarkdownHeading() lipgloss.AdaptiveColor
|
||||
MarkdownLink() lipgloss.AdaptiveColor
|
||||
MarkdownLinkText() lipgloss.AdaptiveColor
|
||||
MarkdownCode() lipgloss.AdaptiveColor
|
||||
MarkdownBlockQuote() lipgloss.AdaptiveColor
|
||||
MarkdownEmph() lipgloss.AdaptiveColor
|
||||
MarkdownStrong() lipgloss.AdaptiveColor
|
||||
MarkdownHorizontalRule() lipgloss.AdaptiveColor
|
||||
MarkdownListItem() lipgloss.AdaptiveColor
|
||||
MarkdownListEnumeration() lipgloss.AdaptiveColor
|
||||
MarkdownImage() lipgloss.AdaptiveColor
|
||||
MarkdownImageText() lipgloss.AdaptiveColor
|
||||
MarkdownCodeBlock() lipgloss.AdaptiveColor
|
||||
|
||||
// Syntax highlighting colors
|
||||
SyntaxComment() lipgloss.AdaptiveColor
|
||||
SyntaxKeyword() lipgloss.AdaptiveColor
|
||||
SyntaxFunction() lipgloss.AdaptiveColor
|
||||
SyntaxVariable() lipgloss.AdaptiveColor
|
||||
SyntaxString() lipgloss.AdaptiveColor
|
||||
SyntaxNumber() lipgloss.AdaptiveColor
|
||||
SyntaxType() lipgloss.AdaptiveColor
|
||||
SyntaxOperator() lipgloss.AdaptiveColor
|
||||
SyntaxPunctuation() lipgloss.AdaptiveColor
|
||||
}
|
||||
|
||||
// BaseTheme provides a default implementation of the Theme interface
|
||||
// that can be embedded in concrete theme implementations.
|
||||
type BaseTheme struct {
|
||||
// Base colors
|
||||
PrimaryColor lipgloss.AdaptiveColor
|
||||
SecondaryColor lipgloss.AdaptiveColor
|
||||
AccentColor lipgloss.AdaptiveColor
|
||||
|
||||
// Status colors
|
||||
ErrorColor lipgloss.AdaptiveColor
|
||||
WarningColor lipgloss.AdaptiveColor
|
||||
SuccessColor lipgloss.AdaptiveColor
|
||||
InfoColor lipgloss.AdaptiveColor
|
||||
|
||||
// Text colors
|
||||
TextColor lipgloss.AdaptiveColor
|
||||
TextMutedColor lipgloss.AdaptiveColor
|
||||
TextEmphasizedColor lipgloss.AdaptiveColor
|
||||
|
||||
// Background colors
|
||||
BackgroundColor lipgloss.AdaptiveColor
|
||||
BackgroundSecondaryColor lipgloss.AdaptiveColor
|
||||
BackgroundDarkerColor lipgloss.AdaptiveColor
|
||||
|
||||
// Border colors
|
||||
BorderNormalColor lipgloss.AdaptiveColor
|
||||
BorderFocusedColor lipgloss.AdaptiveColor
|
||||
BorderDimColor lipgloss.AdaptiveColor
|
||||
|
||||
// Diff view colors
|
||||
DiffAddedColor lipgloss.AdaptiveColor
|
||||
DiffRemovedColor lipgloss.AdaptiveColor
|
||||
DiffContextColor lipgloss.AdaptiveColor
|
||||
DiffHunkHeaderColor lipgloss.AdaptiveColor
|
||||
DiffHighlightAddedColor lipgloss.AdaptiveColor
|
||||
DiffHighlightRemovedColor lipgloss.AdaptiveColor
|
||||
DiffAddedBgColor lipgloss.AdaptiveColor
|
||||
DiffRemovedBgColor lipgloss.AdaptiveColor
|
||||
DiffContextBgColor lipgloss.AdaptiveColor
|
||||
DiffLineNumberColor lipgloss.AdaptiveColor
|
||||
DiffAddedLineNumberBgColor lipgloss.AdaptiveColor
|
||||
DiffRemovedLineNumberBgColor lipgloss.AdaptiveColor
|
||||
|
||||
// Markdown colors
|
||||
MarkdownTextColor lipgloss.AdaptiveColor
|
||||
MarkdownHeadingColor lipgloss.AdaptiveColor
|
||||
MarkdownLinkColor lipgloss.AdaptiveColor
|
||||
MarkdownLinkTextColor lipgloss.AdaptiveColor
|
||||
MarkdownCodeColor lipgloss.AdaptiveColor
|
||||
MarkdownBlockQuoteColor lipgloss.AdaptiveColor
|
||||
MarkdownEmphColor lipgloss.AdaptiveColor
|
||||
MarkdownStrongColor lipgloss.AdaptiveColor
|
||||
MarkdownHorizontalRuleColor lipgloss.AdaptiveColor
|
||||
MarkdownListItemColor lipgloss.AdaptiveColor
|
||||
MarkdownListEnumerationColor lipgloss.AdaptiveColor
|
||||
MarkdownImageColor lipgloss.AdaptiveColor
|
||||
MarkdownImageTextColor lipgloss.AdaptiveColor
|
||||
MarkdownCodeBlockColor lipgloss.AdaptiveColor
|
||||
|
||||
// Syntax highlighting colors
|
||||
SyntaxCommentColor lipgloss.AdaptiveColor
|
||||
SyntaxKeywordColor lipgloss.AdaptiveColor
|
||||
SyntaxFunctionColor lipgloss.AdaptiveColor
|
||||
SyntaxVariableColor lipgloss.AdaptiveColor
|
||||
SyntaxStringColor lipgloss.AdaptiveColor
|
||||
SyntaxNumberColor lipgloss.AdaptiveColor
|
||||
SyntaxTypeColor lipgloss.AdaptiveColor
|
||||
SyntaxOperatorColor lipgloss.AdaptiveColor
|
||||
SyntaxPunctuationColor lipgloss.AdaptiveColor
|
||||
}
|
||||
|
||||
// Implement the Theme interface for BaseTheme
|
||||
func (t *BaseTheme) Primary() lipgloss.AdaptiveColor { return t.PrimaryColor }
|
||||
func (t *BaseTheme) Secondary() lipgloss.AdaptiveColor { return t.SecondaryColor }
|
||||
func (t *BaseTheme) Accent() lipgloss.AdaptiveColor { return t.AccentColor }
|
||||
|
||||
func (t *BaseTheme) Error() lipgloss.AdaptiveColor { return t.ErrorColor }
|
||||
func (t *BaseTheme) Warning() lipgloss.AdaptiveColor { return t.WarningColor }
|
||||
func (t *BaseTheme) Success() lipgloss.AdaptiveColor { return t.SuccessColor }
|
||||
func (t *BaseTheme) Info() lipgloss.AdaptiveColor { return t.InfoColor }
|
||||
|
||||
func (t *BaseTheme) Text() lipgloss.AdaptiveColor { return t.TextColor }
|
||||
func (t *BaseTheme) TextMuted() lipgloss.AdaptiveColor { return t.TextMutedColor }
|
||||
func (t *BaseTheme) TextEmphasized() lipgloss.AdaptiveColor { return t.TextEmphasizedColor }
|
||||
|
||||
func (t *BaseTheme) Background() lipgloss.AdaptiveColor { return t.BackgroundColor }
|
||||
func (t *BaseTheme) BackgroundSecondary() lipgloss.AdaptiveColor { return t.BackgroundSecondaryColor }
|
||||
func (t *BaseTheme) BackgroundDarker() lipgloss.AdaptiveColor { return t.BackgroundDarkerColor }
|
||||
|
||||
func (t *BaseTheme) BorderNormal() lipgloss.AdaptiveColor { return t.BorderNormalColor }
|
||||
func (t *BaseTheme) BorderFocused() lipgloss.AdaptiveColor { return t.BorderFocusedColor }
|
||||
func (t *BaseTheme) BorderDim() lipgloss.AdaptiveColor { return t.BorderDimColor }
|
||||
|
||||
func (t *BaseTheme) DiffAdded() lipgloss.AdaptiveColor { return t.DiffAddedColor }
|
||||
func (t *BaseTheme) DiffRemoved() lipgloss.AdaptiveColor { return t.DiffRemovedColor }
|
||||
func (t *BaseTheme) DiffContext() lipgloss.AdaptiveColor { return t.DiffContextColor }
|
||||
func (t *BaseTheme) DiffHunkHeader() lipgloss.AdaptiveColor { return t.DiffHunkHeaderColor }
|
||||
func (t *BaseTheme) DiffHighlightAdded() lipgloss.AdaptiveColor { return t.DiffHighlightAddedColor }
|
||||
func (t *BaseTheme) DiffHighlightRemoved() lipgloss.AdaptiveColor { return t.DiffHighlightRemovedColor }
|
||||
func (t *BaseTheme) DiffAddedBg() lipgloss.AdaptiveColor { return t.DiffAddedBgColor }
|
||||
func (t *BaseTheme) DiffRemovedBg() lipgloss.AdaptiveColor { return t.DiffRemovedBgColor }
|
||||
func (t *BaseTheme) DiffContextBg() lipgloss.AdaptiveColor { return t.DiffContextBgColor }
|
||||
func (t *BaseTheme) DiffLineNumber() lipgloss.AdaptiveColor { return t.DiffLineNumberColor }
|
||||
func (t *BaseTheme) DiffAddedLineNumberBg() lipgloss.AdaptiveColor { return t.DiffAddedLineNumberBgColor }
|
||||
func (t *BaseTheme) DiffRemovedLineNumberBg() lipgloss.AdaptiveColor { return t.DiffRemovedLineNumberBgColor }
|
||||
|
||||
func (t *BaseTheme) MarkdownText() lipgloss.AdaptiveColor { return t.MarkdownTextColor }
|
||||
func (t *BaseTheme) MarkdownHeading() lipgloss.AdaptiveColor { return t.MarkdownHeadingColor }
|
||||
func (t *BaseTheme) MarkdownLink() lipgloss.AdaptiveColor { return t.MarkdownLinkColor }
|
||||
func (t *BaseTheme) MarkdownLinkText() lipgloss.AdaptiveColor { return t.MarkdownLinkTextColor }
|
||||
func (t *BaseTheme) MarkdownCode() lipgloss.AdaptiveColor { return t.MarkdownCodeColor }
|
||||
func (t *BaseTheme) MarkdownBlockQuote() lipgloss.AdaptiveColor { return t.MarkdownBlockQuoteColor }
|
||||
func (t *BaseTheme) MarkdownEmph() lipgloss.AdaptiveColor { return t.MarkdownEmphColor }
|
||||
func (t *BaseTheme) MarkdownStrong() lipgloss.AdaptiveColor { return t.MarkdownStrongColor }
|
||||
func (t *BaseTheme) MarkdownHorizontalRule() lipgloss.AdaptiveColor { return t.MarkdownHorizontalRuleColor }
|
||||
func (t *BaseTheme) MarkdownListItem() lipgloss.AdaptiveColor { return t.MarkdownListItemColor }
|
||||
func (t *BaseTheme) MarkdownListEnumeration() lipgloss.AdaptiveColor { return t.MarkdownListEnumerationColor }
|
||||
func (t *BaseTheme) MarkdownImage() lipgloss.AdaptiveColor { return t.MarkdownImageColor }
|
||||
func (t *BaseTheme) MarkdownImageText() lipgloss.AdaptiveColor { return t.MarkdownImageTextColor }
|
||||
func (t *BaseTheme) MarkdownCodeBlock() lipgloss.AdaptiveColor { return t.MarkdownCodeBlockColor }
|
||||
|
||||
func (t *BaseTheme) SyntaxComment() lipgloss.AdaptiveColor { return t.SyntaxCommentColor }
|
||||
func (t *BaseTheme) SyntaxKeyword() lipgloss.AdaptiveColor { return t.SyntaxKeywordColor }
|
||||
func (t *BaseTheme) SyntaxFunction() lipgloss.AdaptiveColor { return t.SyntaxFunctionColor }
|
||||
func (t *BaseTheme) SyntaxVariable() lipgloss.AdaptiveColor { return t.SyntaxVariableColor }
|
||||
func (t *BaseTheme) SyntaxString() lipgloss.AdaptiveColor { return t.SyntaxStringColor }
|
||||
func (t *BaseTheme) SyntaxNumber() lipgloss.AdaptiveColor { return t.SyntaxNumberColor }
|
||||
func (t *BaseTheme) SyntaxType() lipgloss.AdaptiveColor { return t.SyntaxTypeColor }
|
||||
func (t *BaseTheme) SyntaxOperator() lipgloss.AdaptiveColor { return t.SyntaxOperatorColor }
|
||||
func (t *BaseTheme) SyntaxPunctuation() lipgloss.AdaptiveColor { return t.SyntaxPunctuationColor }
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user