mirror of
https://fastgit.cc/https://github.com/anomalyco/opencode
synced 2026-05-04 07:40:39 +08:00
Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
89
README.md
89
README.md
@@ -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,8 +22,35 @@ OpenCode is a Go-based CLI application that brings AI assistance to your termina
|
||||
|
||||
## Installation
|
||||
|
||||
### Using the Install Script
|
||||
|
||||
```bash
|
||||
# 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
|
||||
# Coming soon
|
||||
go install github.com/opencode-ai/opencode@latest
|
||||
```
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"time"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
zone "github.com/lrstanley/bubblezone"
|
||||
"github.com/opencode-ai/opencode/internal/app"
|
||||
"github.com/opencode-ai/opencode/internal/config"
|
||||
"github.com/opencode-ai/opencode/internal/db"
|
||||
@@ -16,7 +17,6 @@ import (
|
||||
"github.com/opencode-ai/opencode/internal/pubsub"
|
||||
"github.com/opencode-ai/opencode/internal/tui"
|
||||
"github.com/opencode-ai/opencode/internal/version"
|
||||
zone "github.com/lrstanley/bubblezone"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -79,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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
25
go.mod
25
go.mod
@@ -5,10 +5,12 @@ 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
|
||||
@@ -18,7 +20,6 @@ require (
|
||||
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/google/generative-ai-go v0.19.0
|
||||
github.com/google/uuid v1.6.0
|
||||
@@ -44,9 +45,9 @@ require (
|
||||
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
|
||||
@@ -69,27 +70,21 @@ require (
|
||||
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/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/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/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||
@@ -100,12 +95,12 @@ require (
|
||||
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/sethvargo/go-retry v0.3.0 // indirect
|
||||
github.com/skeema/knownhosts v1.3.1 // 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
|
||||
@@ -116,7 +111,6 @@ require (
|
||||
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
|
||||
@@ -140,6 +134,5 @@ require (
|
||||
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/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
71
go.sum
71
go.sum
@@ -10,17 +10,18 @@ cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4
|
||||
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 +32,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=
|
||||
@@ -99,11 +96,7 @@ github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko
|
||||
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=
|
||||
@@ -111,10 +104,6 @@ github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yA
|
||||
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 +112,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,8 +121,8 @@ 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/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
||||
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/generative-ai-go v0.19.0 h1:R71szggh8wHMCUlEMsW2A/3T+5LdEIkiaHSYgSpUgdg=
|
||||
@@ -164,10 +143,6 @@ github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUq
|
||||
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=
|
||||
@@ -175,6 +150,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/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=
|
||||
@@ -208,17 +185,13 @@ github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdh
|
||||
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/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
|
||||
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
|
||||
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=
|
||||
@@ -242,9 +215,6 @@ github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||
github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE=
|
||||
github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas=
|
||||
github.com/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/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=
|
||||
@@ -258,7 +228,6 @@ 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=
|
||||
@@ -277,8 +246,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=
|
||||
@@ -309,7 +276,6 @@ 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=
|
||||
@@ -321,7 +287,6 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91
|
||||
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=
|
||||
@@ -339,15 +304,12 @@ 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=
|
||||
@@ -369,7 +331,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=
|
||||
@@ -398,8 +359,6 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8
|
||||
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=
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"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
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/opencode-ai/opencode/internal/llm/models"
|
||||
@@ -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 viper.Get("providers.anthropic.apiKey") != "" {
|
||||
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 viper.Get("providers.openai.apiKey") != "" {
|
||||
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 viper.Get("providers.google.gemini.apiKey") != "" {
|
||||
viper.SetDefault("agents.coder.model", models.Gemini25)
|
||||
viper.SetDefault("agents.task.model", models.Gemini25Flash)
|
||||
viper.SetDefault("agents.title.model", models.Gemini25Flash)
|
||||
@@ -231,14 +284,28 @@ func setProviderDefaults() {
|
||||
}
|
||||
|
||||
// Groq configuration
|
||||
if apiKey := os.Getenv("GROQ_API_KEY"); apiKey != "" {
|
||||
viper.SetDefault("providers.groq.apiKey", apiKey)
|
||||
if viper.Get("providers.groq.apiKey") != "" {
|
||||
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 viper.Get("providers.openrouter.apiKey") != "" {
|
||||
viper.SetDefault("agents.coder.model", models.OpenRouterClaude37Sonnet)
|
||||
viper.SetDefault("agents.task.model", models.OpenRouterClaude37Sonnet)
|
||||
viper.SetDefault("agents.title.model", models.OpenRouterClaude35Haiku)
|
||||
return
|
||||
}
|
||||
|
||||
if viper.Get("providers.xai.apiKey") != "" {
|
||||
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 +313,13 @@ func setProviderDefaults() {
|
||||
viper.SetDefault("agents.title.model", models.BedrockClaude37Sonnet)
|
||||
return
|
||||
}
|
||||
|
||||
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 +386,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 +423,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 +562,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 +617,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 +706,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
|
||||
}
|
||||
|
||||
@@ -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/opencode-ai/opencode/internal/config"
|
||||
"github.com/opencode-ai/opencode/internal/logging"
|
||||
"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
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ type Service interface {
|
||||
Cancel(sessionID string)
|
||||
IsSessionBusy(sessionID string) bool
|
||||
IsBusy() bool
|
||||
Update(agentName config.AgentName, modelID models.ModelID) (models.Model, error)
|
||||
}
|
||||
|
||||
type agent struct {
|
||||
@@ -436,6 +437,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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -11,8 +11,8 @@ 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",
|
||||
@@ -29,13 +29,13 @@ var AnthropicModels = map[ModelID]Model{
|
||||
ID: Claude3Haiku,
|
||||
Name: "Claude 3 Haiku",
|
||||
Provider: ProviderAnthropic,
|
||||
APIModel: "claude-3-haiku-latest",
|
||||
APIModel: "claude-3-haiku-20240307", // doesn't support "-latest"
|
||||
CostPer1MIn: 0.25,
|
||||
CostPer1MInCached: 0.30,
|
||||
CostPer1MOutCached: 0.03,
|
||||
CostPer1MOut: 1.25,
|
||||
ContextWindow: 200000,
|
||||
DefaultMaxTokens: 5000,
|
||||
DefaultMaxTokens: 4096,
|
||||
},
|
||||
Claude37Sonnet: {
|
||||
ID: Claude37Sonnet,
|
||||
|
||||
157
internal/llm/models/azure.go
Normal file
157
internal/llm/models/azure.go
Normal file
@@ -0,0 +1,157 @@
|
||||
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,
|
||||
},
|
||||
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,
|
||||
},
|
||||
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,
|
||||
},
|
||||
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,
|
||||
},
|
||||
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,
|
||||
},
|
||||
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,
|
||||
},
|
||||
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,
|
||||
},
|
||||
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,
|
||||
},
|
||||
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,
|
||||
},
|
||||
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,
|
||||
},
|
||||
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,
|
||||
},
|
||||
}
|
||||
82
internal/llm/models/groq.go
Normal file
82
internal/llm/models/groq.go
Normal file
@@ -0,0 +1,82 @@
|
||||
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,
|
||||
},
|
||||
|
||||
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?
|
||||
},
|
||||
|
||||
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,
|
||||
},
|
||||
|
||||
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,
|
||||
},
|
||||
|
||||
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,
|
||||
},
|
||||
}
|
||||
@@ -23,21 +23,27 @@ type Model struct {
|
||||
|
||||
// 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 +69,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 +86,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)
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
}
|
||||
@@ -4,25 +4,14 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -213,7 +213,7 @@ func (a *anthropicClient) send(ctx context.Context, messages []message.Message,
|
||||
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 +262,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 +351,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}
|
||||
}
|
||||
@@ -54,19 +54,6 @@ 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:
|
||||
@@ -132,7 +119,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,23 +134,18 @@ 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
|
||||
}
|
||||
@@ -171,7 +154,11 @@ func (g *geminiClient) finishReason(reason genai.FinishReason) message.FinishRea
|
||||
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))
|
||||
|
||||
model.SystemInstruction = &genai.Content{
|
||||
Parts: []genai.Part{
|
||||
genai.Text(g.providerOptions.systemMessage),
|
||||
},
|
||||
}
|
||||
// Convert tools
|
||||
if len(tools) > 0 {
|
||||
model.Tools = g.convertTools(tools)
|
||||
@@ -189,19 +176,13 @@ func (g *geminiClient) send(ctx context.Context, messages []message.Message, too
|
||||
attempts := 0
|
||||
for {
|
||||
attempts++
|
||||
var toolCalls []message.ToolCall
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := chat.SendMessage(ctx, genai.Text(lastText))
|
||||
resp, err := chat.SendMessage(ctx, lastMsg.Parts...)
|
||||
// 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 +190,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,7 +202,6 @@ 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 {
|
||||
@@ -232,20 +212,28 @@ func (g *geminiClient) send(ctx context.Context, messages []message.Message, too
|
||||
id := "call_" + uuid.New().String()
|
||||
args, _ := json.Marshal(p.Args)
|
||||
toolCalls = append(toolCalls, message.ToolCall{
|
||||
ID: id,
|
||||
Name: p.Name,
|
||||
Input: string(args),
|
||||
Type: "function",
|
||||
ID: id,
|
||||
Name: p.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
|
||||
}
|
||||
}
|
||||
@@ -253,7 +241,11 @@ func (g *geminiClient) send(ctx context.Context, messages []message.Message, too
|
||||
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))
|
||||
|
||||
model.SystemInstruction = &genai.Content{
|
||||
Parts: []genai.Part{
|
||||
genai.Text(g.providerOptions.systemMessage),
|
||||
},
|
||||
}
|
||||
// Convert tools
|
||||
if len(tools) > 0 {
|
||||
model.Tools = g.convertTools(tools)
|
||||
@@ -277,18 +269,10 @@ 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
|
||||
|
||||
chat.History = geminiMessages[:len(geminiMessages)-1]
|
||||
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))
|
||||
iter := chat.SendMessageStream(ctx, lastMsg.Parts...)
|
||||
|
||||
currentContent := ""
|
||||
toolCalls := []message.ToolCall{}
|
||||
@@ -308,7 +292,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 {
|
||||
@@ -331,23 +315,23 @@ func (g *geminiClient) stream(ctx context.Context, messages []message.Message, t
|
||||
for _, part := range resp.Candidates[0].Content.Parts {
|
||||
switch p := part.(type) {
|
||||
case genai.Text:
|
||||
newText := string(p)
|
||||
delta := newText[len(currentContent):]
|
||||
delta := string(p)
|
||||
if delta != "" {
|
||||
eventChan <- ProviderEvent{
|
||||
Type: EventContentDelta,
|
||||
Content: delta,
|
||||
}
|
||||
currentContent = newText
|
||||
currentContent += delta
|
||||
}
|
||||
case genai.FunctionCall:
|
||||
id := "call_" + uuid.New().String()
|
||||
args, _ := json.Marshal(p.Args)
|
||||
newCall := message.ToolCall{
|
||||
ID: id,
|
||||
Name: p.Name,
|
||||
Input: string(args),
|
||||
Type: "function",
|
||||
ID: id,
|
||||
Name: p.Name,
|
||||
Input: string(args),
|
||||
Type: "function",
|
||||
Finished: true,
|
||||
}
|
||||
|
||||
isNew := true
|
||||
@@ -369,37 +353,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
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
|
||||
@@ -8,19 +8,20 @@ import (
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"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/tools"
|
||||
"github.com/opencode-ai/opencode/internal/logging"
|
||||
"github.com/opencode-ai/opencode/internal/message"
|
||||
"github.com/openai/openai-go"
|
||||
"github.com/openai/openai-go/option"
|
||||
"github.com/openai/openai-go/shared"
|
||||
)
|
||||
|
||||
type openaiOptions struct {
|
||||
baseURL string
|
||||
disableCache bool
|
||||
reasoningEffort string
|
||||
extraHeaders map[string]string
|
||||
}
|
||||
|
||||
type OpenAIOption func(*openaiOptions)
|
||||
@@ -49,6 +50,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,
|
||||
@@ -188,7 +195,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 +211,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 +258,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 +272,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 +301,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 +388,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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -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(),
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -643,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)
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/opencode-ai/opencode/internal/config"
|
||||
@@ -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) {
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/opencode-ai/opencode/internal/config"
|
||||
"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"
|
||||
)
|
||||
|
||||
@@ -22,12 +23,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 +57,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 +80,8 @@ func lspsConfigured(width int) string {
|
||||
),
|
||||
)
|
||||
}
|
||||
return styles.BaseStyle.
|
||||
|
||||
return baseStyle.
|
||||
Width(width).
|
||||
Render(
|
||||
lipgloss.JoinVertical(
|
||||
@@ -75,10 +97,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 +112,28 @@ func logo(width int) string {
|
||||
lipgloss.Left,
|
||||
logo,
|
||||
" ",
|
||||
version,
|
||||
versionText,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
func repo(width int) string {
|
||||
repo := "https://github.com/opencode-ai/opencode"
|
||||
return styles.BaseStyle.
|
||||
Foreground(styles.ForgroundDim).
|
||||
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
|
||||
}
|
||||
|
||||
@@ -10,8 +10,10 @@ import (
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/opencode-ai/opencode/internal/app"
|
||||
"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"
|
||||
)
|
||||
|
||||
@@ -100,6 +102,9 @@ 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
|
||||
@@ -134,7 +139,13 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
|
||||
func (m *editorCmp) View() string {
|
||||
style := lipgloss.NewStyle().Padding(0, 0, 0, 1).Bold(true)
|
||||
t := theme.CurrentTheme()
|
||||
|
||||
// Style the prompt with theme colors
|
||||
style := lipgloss.NewStyle().
|
||||
Padding(0, 0, 0, 1).
|
||||
Bold(true).
|
||||
Foreground(t.Primary())
|
||||
|
||||
return lipgloss.JoinHorizontal(lipgloss.Top, style.Render(">"), m.textarea.View())
|
||||
}
|
||||
@@ -155,23 +166,42 @@ func (m *editorCmp) BindingKeys() []key.Binding {
|
||||
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()
|
||||
|
||||
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)
|
||||
|
||||
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()
|
||||
return &editorCmp{
|
||||
app: app,
|
||||
textarea: ti,
|
||||
textarea: ta,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,9 @@ import (
|
||||
"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"
|
||||
)
|
||||
|
||||
@@ -69,7 +71,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 dialog.ThemeChangedMsg:
|
||||
m.rerender()
|
||||
return m, nil
|
||||
case SessionSelectedMsg:
|
||||
if msg.ID != m.session.ID {
|
||||
cmd := m.SetSession(msg)
|
||||
@@ -174,6 +178,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
|
||||
@@ -225,15 +230,13 @@ func (m *messagesCmp) renderView() {
|
||||
messages := make([]string, 0)
|
||||
for _, v := range m.uiMessages {
|
||||
messages = append(messages, v.content,
|
||||
styles.BaseStyle.
|
||||
baseStyle.
|
||||
Width(m.width).
|
||||
Render(
|
||||
"",
|
||||
),
|
||||
Render(""),
|
||||
)
|
||||
}
|
||||
m.viewport.SetContent(
|
||||
styles.BaseStyle.
|
||||
baseStyle.
|
||||
Width(m.width).
|
||||
Render(
|
||||
lipgloss.JoinVertical(
|
||||
@@ -245,8 +248,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(
|
||||
@@ -258,14 +263,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(
|
||||
@@ -277,7 +282,7 @@ func (m *messagesCmp) View() string {
|
||||
)
|
||||
}
|
||||
|
||||
return styles.BaseStyle.
|
||||
return baseStyle.
|
||||
Width(m.width).
|
||||
Render(
|
||||
lipgloss.JoinVertical(
|
||||
@@ -328,6 +333,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) {
|
||||
@@ -338,42 +346,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.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 cancel"),
|
||||
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("enter"),
|
||||
styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render(" to send the message,"),
|
||||
styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render(" write"),
|
||||
styles.BaseStyle.Foreground(styles.Forground).Bold(true).Render(" \\"),
|
||||
styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render(" and enter to add a new line"),
|
||||
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),
|
||||
@@ -383,6 +398,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
|
||||
@@ -391,11 +413,7 @@ 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.rerender()
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -6,10 +6,8 @@ import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/glamour"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/charmbracelet/x/ansi"
|
||||
"github.com/opencode-ai/opencode/internal/config"
|
||||
@@ -19,6 +17,7 @@ import (
|
||||
"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
|
||||
@@ -31,8 +30,6 @@ const (
|
||||
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,
|
||||
@@ -121,26 +109,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) {
|
||||
@@ -414,32 +413,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 +457,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 +479,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 +495,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 +518,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 +551,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 +619,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)
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"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,22 +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 {
|
||||
additions := styles.BaseStyle.Foreground(styles.Green).PaddingLeft(1).Render(fmt.Sprintf("+%d", additions))
|
||||
removals := styles.BaseStyle.Foreground(styles.Red).PaddingLeft(1).Render(fmt.Sprintf("-%d", removals))
|
||||
content := lipgloss.JoinHorizontal(lipgloss.Left, additions, removals)
|
||||
stats = styles.BaseStyle.Width(lipgloss.Width(content)).Render(content)
|
||||
} else if additions > 0 {
|
||||
additions := fmt.Sprintf(" %s", styles.BaseStyle.PaddingLeft(1).Foreground(styles.Green).Render(fmt.Sprintf("+%d", additions)))
|
||||
stats = styles.BaseStyle.Width(lipgloss.Width(additions)).Render(additions)
|
||||
} else if removals > 0 {
|
||||
removals := fmt.Sprintf(" %s", styles.BaseStyle.PaddingLeft(1).Foreground(styles.Red).Render(fmt.Sprintf("-%d", removals)))
|
||||
stats = styles.BaseStyle.Width(lipgloss.Width(removals)).Render(removals)
|
||||
}
|
||||
filePathStr := styles.BaseStyle.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(
|
||||
@@ -141,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 {
|
||||
@@ -150,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),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -175,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(
|
||||
|
||||
@@ -15,12 +15,13 @@ import (
|
||||
"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,
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"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"
|
||||
)
|
||||
|
||||
@@ -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,15 +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(""),
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
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"
|
||||
)
|
||||
|
||||
type helpCmp struct {
|
||||
@@ -53,10 +54,21 @@ 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 (
|
||||
@@ -64,6 +76,7 @@ func (h *helpCmp) render() string {
|
||||
width int
|
||||
rows = 10 - 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,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"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"
|
||||
)
|
||||
|
||||
@@ -92,55 +93,58 @@ 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?")
|
||||
|
||||
maxWidth = min(maxWidth, m.width-10)
|
||||
yesStyle := styles.BaseStyle
|
||||
noStyle := styles.BaseStyle
|
||||
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)
|
||||
@@ -148,17 +152,17 @@ func (m InitDialogCmp) View() string {
|
||||
content := lipgloss.JoinVertical(
|
||||
lipgloss.Left,
|
||||
title,
|
||||
styles.BaseStyle.Width(maxWidth).Render(""),
|
||||
baseStyle.Width(maxWidth).Render(""),
|
||||
explanation,
|
||||
question,
|
||||
buttons,
|
||||
styles.BaseStyle.Width(maxWidth).Render(""),
|
||||
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)
|
||||
}
|
||||
|
||||
370
internal/tui/components/dialog/models.go
Normal file
370
internal/tui/components/dialog/models.go
Normal file
@@ -0,0 +1,370 @@
|
||||
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()
|
||||
|
||||
m.availableProviders = getEnabledProviders(cfg)
|
||||
m.hScrollPossible = len(m.availableProviders) > 1
|
||||
|
||||
agentCfg := cfg.Agents[config.AgentCoder]
|
||||
selectedModelId := agentCfg.Model
|
||||
modelInfo := models.SupportedModels[selectedModelId]
|
||||
|
||||
m.provider = modelInfo.Provider
|
||||
m.hScrollOffset = findProviderIndex(m.availableProviders, m.provider)
|
||||
|
||||
m.setupModelsForProvider(m.provider)
|
||||
}
|
||||
|
||||
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{}
|
||||
}
|
||||
@@ -7,13 +7,13 @@ import (
|
||||
"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/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"
|
||||
)
|
||||
|
||||
@@ -149,25 +149,28 @@ 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)")
|
||||
@@ -192,15 +195,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 +216,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 +301,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 +349,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()
|
||||
@@ -378,18 +394,18 @@ func (p *permissionDialogCmp) render() string {
|
||||
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)),
|
||||
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(
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"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"
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"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"
|
||||
)
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
@@ -146,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).
|
||||
@@ -168,15 +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(""),
|
||||
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)
|
||||
}
|
||||
|
||||
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: "",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"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) {
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"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"
|
||||
)
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
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"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"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"
|
||||
)
|
||||
|
||||
@@ -43,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"
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
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"
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
@@ -142,7 +142,6 @@ 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),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,177 +1,152 @@
|
||||
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
|
||||
)
|
||||
// Style generation functions that use the current theme
|
||||
|
||||
// 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",
|
||||
}
|
||||
// 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())
|
||||
}
|
||||
|
||||
Forground = lipgloss.AdaptiveColor{
|
||||
Dark: "#d3d3d3",
|
||||
Light: "#d3d3d3",
|
||||
}
|
||||
// Regular returns a basic unstyled lipgloss.Style
|
||||
func Regular() lipgloss.Style {
|
||||
return lipgloss.NewStyle()
|
||||
}
|
||||
|
||||
ForgroundMid = lipgloss.AdaptiveColor{
|
||||
Dark: "#a0a0a0",
|
||||
Light: "#a0a0a0",
|
||||
}
|
||||
// Bold returns a bold style
|
||||
func Bold() lipgloss.Style {
|
||||
return Regular().Bold(true)
|
||||
}
|
||||
|
||||
ForgroundDim = lipgloss.AdaptiveColor{
|
||||
Dark: "#737373",
|
||||
Light: "#737373",
|
||||
}
|
||||
// Padded returns a style with horizontal padding
|
||||
func Padded() lipgloss.Style {
|
||||
return Regular().Padding(0, 1)
|
||||
}
|
||||
|
||||
BaseStyle = lipgloss.NewStyle().
|
||||
Background(Background).
|
||||
Foreground(Forground)
|
||||
// Border returns a style with a normal border
|
||||
func Border() lipgloss.Style {
|
||||
t := theme.CurrentTheme()
|
||||
return Regular().
|
||||
Border(lipgloss.NormalBorder()).
|
||||
BorderForeground(t.BorderNormal())
|
||||
}
|
||||
|
||||
PrimaryColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#fab283",
|
||||
Light: "#fab283",
|
||||
}
|
||||
)
|
||||
// ThickBorder returns a style with a thick border
|
||||
func ThickBorder() lipgloss.Style {
|
||||
t := theme.CurrentTheme()
|
||||
return Regular().
|
||||
Border(lipgloss.ThickBorder()).
|
||||
BorderForeground(t.BorderNormal())
|
||||
}
|
||||
|
||||
var (
|
||||
Regular = lipgloss.NewStyle()
|
||||
Bold = Regular.Bold(true)
|
||||
Padded = Regular.Padding(0, 1)
|
||||
// DoubleBorder returns a style with a double border
|
||||
func DoubleBorder() lipgloss.Style {
|
||||
t := theme.CurrentTheme()
|
||||
return Regular().
|
||||
Border(lipgloss.DoubleBorder()).
|
||||
BorderForeground(t.BorderNormal())
|
||||
}
|
||||
|
||||
Border = Regular.Border(lipgloss.NormalBorder())
|
||||
ThickBorder = Regular.Border(lipgloss.ThickBorder())
|
||||
DoubleBorder = Regular.Border(lipgloss.DoubleBorder())
|
||||
// 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())
|
||||
}
|
||||
|
||||
// Colors
|
||||
White = lipgloss.Color("#ffffff")
|
||||
Surface0 = lipgloss.AdaptiveColor{
|
||||
Dark: dark.Surface0().Hex,
|
||||
Light: light.Surface0().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())
|
||||
}
|
||||
|
||||
Overlay0 = lipgloss.AdaptiveColor{
|
||||
Dark: dark.Overlay0().Hex,
|
||||
Light: light.Overlay0().Hex,
|
||||
}
|
||||
// PrimaryColor returns the primary color from the current theme
|
||||
func PrimaryColor() lipgloss.AdaptiveColor {
|
||||
return theme.CurrentTheme().Primary()
|
||||
}
|
||||
|
||||
Ovelay1 = lipgloss.AdaptiveColor{
|
||||
Dark: dark.Overlay1().Hex,
|
||||
Light: light.Overlay1().Hex,
|
||||
}
|
||||
// SecondaryColor returns the secondary color from the current theme
|
||||
func SecondaryColor() lipgloss.AdaptiveColor {
|
||||
return theme.CurrentTheme().Secondary()
|
||||
}
|
||||
|
||||
Text = lipgloss.AdaptiveColor{
|
||||
Dark: dark.Text().Hex,
|
||||
Light: light.Text().Hex,
|
||||
}
|
||||
// AccentColor returns the accent color from the current theme
|
||||
func AccentColor() lipgloss.AdaptiveColor {
|
||||
return theme.CurrentTheme().Accent()
|
||||
}
|
||||
|
||||
SubText0 = lipgloss.AdaptiveColor{
|
||||
Dark: dark.Subtext0().Hex,
|
||||
Light: light.Subtext0().Hex,
|
||||
}
|
||||
// ErrorColor returns the error color from the current theme
|
||||
func ErrorColor() lipgloss.AdaptiveColor {
|
||||
return theme.CurrentTheme().Error()
|
||||
}
|
||||
|
||||
SubText1 = lipgloss.AdaptiveColor{
|
||||
Dark: dark.Subtext1().Hex,
|
||||
Light: light.Subtext1().Hex,
|
||||
}
|
||||
// WarningColor returns the warning color from the current theme
|
||||
func WarningColor() lipgloss.AdaptiveColor {
|
||||
return theme.CurrentTheme().Warning()
|
||||
}
|
||||
|
||||
LightGrey = lipgloss.AdaptiveColor{
|
||||
Dark: dark.Surface0().Hex,
|
||||
Light: light.Surface0().Hex,
|
||||
}
|
||||
Grey = lipgloss.AdaptiveColor{
|
||||
Dark: dark.Surface1().Hex,
|
||||
Light: light.Surface1().Hex,
|
||||
}
|
||||
// SuccessColor returns the success color from the current theme
|
||||
func SuccessColor() lipgloss.AdaptiveColor {
|
||||
return theme.CurrentTheme().Success()
|
||||
}
|
||||
|
||||
DarkGrey = lipgloss.AdaptiveColor{
|
||||
Dark: dark.Surface2().Hex,
|
||||
Light: light.Surface2().Hex,
|
||||
}
|
||||
// InfoColor returns the info color from the current theme
|
||||
func InfoColor() lipgloss.AdaptiveColor {
|
||||
return theme.CurrentTheme().Info()
|
||||
}
|
||||
|
||||
Base = lipgloss.AdaptiveColor{
|
||||
Dark: dark.Base().Hex,
|
||||
Light: light.Base().Hex,
|
||||
}
|
||||
// TextColor returns the text color from the current theme
|
||||
func TextColor() lipgloss.AdaptiveColor {
|
||||
return theme.CurrentTheme().Text()
|
||||
}
|
||||
|
||||
Crust = lipgloss.AdaptiveColor{
|
||||
Dark: dark.Crust().Hex,
|
||||
Light: light.Crust().Hex,
|
||||
}
|
||||
// TextMutedColor returns the muted text color from the current theme
|
||||
func TextMutedColor() lipgloss.AdaptiveColor {
|
||||
return theme.CurrentTheme().TextMuted()
|
||||
}
|
||||
|
||||
Blue = lipgloss.AdaptiveColor{
|
||||
Dark: dark.Blue().Hex,
|
||||
Light: light.Blue().Hex,
|
||||
}
|
||||
// TextEmphasizedColor returns the emphasized text color from the current theme
|
||||
func TextEmphasizedColor() lipgloss.AdaptiveColor {
|
||||
return theme.CurrentTheme().TextEmphasized()
|
||||
}
|
||||
|
||||
Red = lipgloss.AdaptiveColor{
|
||||
Dark: dark.Red().Hex,
|
||||
Light: light.Red().Hex,
|
||||
}
|
||||
// BackgroundColor returns the background color from the current theme
|
||||
func BackgroundColor() lipgloss.AdaptiveColor {
|
||||
return theme.CurrentTheme().Background()
|
||||
}
|
||||
|
||||
Green = lipgloss.AdaptiveColor{
|
||||
Dark: dark.Green().Hex,
|
||||
Light: light.Green().Hex,
|
||||
}
|
||||
// BackgroundSecondaryColor returns the secondary background color from the current theme
|
||||
func BackgroundSecondaryColor() lipgloss.AdaptiveColor {
|
||||
return theme.CurrentTheme().BackgroundSecondary()
|
||||
}
|
||||
|
||||
Mauve = lipgloss.AdaptiveColor{
|
||||
Dark: dark.Mauve().Hex,
|
||||
Light: light.Mauve().Hex,
|
||||
}
|
||||
// BackgroundDarkerColor returns the darker background color from the current theme
|
||||
func BackgroundDarkerColor() lipgloss.AdaptiveColor {
|
||||
return theme.CurrentTheme().BackgroundDarker()
|
||||
}
|
||||
|
||||
Teal = lipgloss.AdaptiveColor{
|
||||
Dark: dark.Teal().Hex,
|
||||
Light: light.Teal().Hex,
|
||||
}
|
||||
// BorderNormalColor returns the normal border color from the current theme
|
||||
func BorderNormalColor() lipgloss.AdaptiveColor {
|
||||
return theme.CurrentTheme().BorderNormal()
|
||||
}
|
||||
|
||||
Rosewater = lipgloss.AdaptiveColor{
|
||||
Dark: dark.Rosewater().Hex,
|
||||
Light: light.Rosewater().Hex,
|
||||
}
|
||||
// BorderFocusedColor returns the focused border color from the current theme
|
||||
func BorderFocusedColor() lipgloss.AdaptiveColor {
|
||||
return theme.CurrentTheme().BorderFocused()
|
||||
}
|
||||
|
||||
Flamingo = lipgloss.AdaptiveColor{
|
||||
Dark: dark.Flamingo().Hex,
|
||||
Light: light.Flamingo().Hex,
|
||||
}
|
||||
// BorderDimColor returns the dim border color from the current theme
|
||||
func BorderDimColor() lipgloss.AdaptiveColor {
|
||||
return theme.CurrentTheme().BorderDim()
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
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 }
|
||||
89
internal/tui/theme/theme_test.go
Normal file
89
internal/tui/theme/theme_test.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package theme
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestThemeRegistration(t *testing.T) {
|
||||
// Get list of available themes
|
||||
availableThemes := AvailableThemes()
|
||||
|
||||
// Check if "catppuccin" theme is registered
|
||||
catppuccinFound := false
|
||||
for _, themeName := range availableThemes {
|
||||
if themeName == "catppuccin" {
|
||||
catppuccinFound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !catppuccinFound {
|
||||
t.Errorf("Catppuccin theme is not registered")
|
||||
}
|
||||
|
||||
// Check if "gruvbox" theme is registered
|
||||
gruvboxFound := false
|
||||
for _, themeName := range availableThemes {
|
||||
if themeName == "gruvbox" {
|
||||
gruvboxFound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !gruvboxFound {
|
||||
t.Errorf("Gruvbox theme is not registered")
|
||||
}
|
||||
|
||||
// Check if "monokai" theme is registered
|
||||
monokaiFound := false
|
||||
for _, themeName := range availableThemes {
|
||||
if themeName == "monokai" {
|
||||
monokaiFound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !monokaiFound {
|
||||
t.Errorf("Monokai theme is not registered")
|
||||
}
|
||||
|
||||
// Try to get the themes and make sure they're not nil
|
||||
catppuccin := GetTheme("catppuccin")
|
||||
if catppuccin == nil {
|
||||
t.Errorf("Catppuccin theme is nil")
|
||||
}
|
||||
|
||||
gruvbox := GetTheme("gruvbox")
|
||||
if gruvbox == nil {
|
||||
t.Errorf("Gruvbox theme is nil")
|
||||
}
|
||||
|
||||
monokai := GetTheme("monokai")
|
||||
if monokai == nil {
|
||||
t.Errorf("Monokai theme is nil")
|
||||
}
|
||||
|
||||
// Test switching theme
|
||||
originalTheme := CurrentThemeName()
|
||||
|
||||
err := SetTheme("gruvbox")
|
||||
if err != nil {
|
||||
t.Errorf("Failed to set theme to gruvbox: %v", err)
|
||||
}
|
||||
|
||||
if CurrentThemeName() != "gruvbox" {
|
||||
t.Errorf("Theme not properly switched to gruvbox")
|
||||
}
|
||||
|
||||
err = SetTheme("monokai")
|
||||
if err != nil {
|
||||
t.Errorf("Failed to set theme to monokai: %v", err)
|
||||
}
|
||||
|
||||
if CurrentThemeName() != "monokai" {
|
||||
t.Errorf("Theme not properly switched to monokai")
|
||||
}
|
||||
|
||||
// Switch back to original theme
|
||||
_ = SetTheme(originalTheme)
|
||||
}
|
||||
274
internal/tui/theme/tokyonight.go
Normal file
274
internal/tui/theme/tokyonight.go
Normal file
@@ -0,0 +1,274 @@
|
||||
package theme
|
||||
|
||||
import (
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
// TokyoNightTheme implements the Theme interface with Tokyo Night colors.
|
||||
// It provides both dark and light variants.
|
||||
type TokyoNightTheme struct {
|
||||
BaseTheme
|
||||
}
|
||||
|
||||
// NewTokyoNightTheme creates a new instance of the Tokyo Night theme.
|
||||
func NewTokyoNightTheme() *TokyoNightTheme {
|
||||
// Tokyo Night color palette
|
||||
// Dark mode colors
|
||||
darkBackground := "#222436"
|
||||
darkCurrentLine := "#1e2030"
|
||||
darkSelection := "#2f334d"
|
||||
darkForeground := "#c8d3f5"
|
||||
darkComment := "#636da6"
|
||||
darkRed := "#ff757f"
|
||||
darkOrange := "#ff966c"
|
||||
darkYellow := "#ffc777"
|
||||
darkGreen := "#c3e88d"
|
||||
darkCyan := "#86e1fc"
|
||||
darkBlue := "#82aaff"
|
||||
darkPurple := "#c099ff"
|
||||
darkBorder := "#3b4261"
|
||||
|
||||
// Light mode colors (Tokyo Night Day)
|
||||
lightBackground := "#e1e2e7"
|
||||
lightCurrentLine := "#d5d6db"
|
||||
lightSelection := "#c8c9ce"
|
||||
lightForeground := "#3760bf"
|
||||
lightComment := "#848cb5"
|
||||
lightRed := "#f52a65"
|
||||
lightOrange := "#b15c00"
|
||||
lightYellow := "#8c6c3e"
|
||||
lightGreen := "#587539"
|
||||
lightCyan := "#007197"
|
||||
lightBlue := "#2e7de9"
|
||||
lightPurple := "#9854f1"
|
||||
lightBorder := "#a8aecb"
|
||||
|
||||
theme := &TokyoNightTheme{}
|
||||
|
||||
// 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: "#191B29", // Darker background from palette
|
||||
Light: "#f0f0f5", // 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: "#4fd6be", // teal from palette
|
||||
Light: "#1e725c",
|
||||
}
|
||||
theme.DiffRemovedColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#c53b53", // red1 from palette
|
||||
Light: "#c53b53",
|
||||
}
|
||||
theme.DiffContextColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#828bb8", // fg_dark from palette
|
||||
Light: "#7086b5",
|
||||
}
|
||||
theme.DiffHunkHeaderColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#828bb8", // fg_dark from palette
|
||||
Light: "#7086b5",
|
||||
}
|
||||
theme.DiffHighlightAddedColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#b8db87", // git.add from palette
|
||||
Light: "#4db380",
|
||||
}
|
||||
theme.DiffHighlightRemovedColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#e26a75", // git.delete from palette
|
||||
Light: "#f52a65",
|
||||
}
|
||||
theme.DiffAddedBgColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#20303b",
|
||||
Light: "#d5e5d5",
|
||||
}
|
||||
theme.DiffRemovedBgColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#37222c",
|
||||
Light: "#f7d8db",
|
||||
}
|
||||
theme.DiffContextBgColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkBackground,
|
||||
Light: lightBackground,
|
||||
}
|
||||
theme.DiffLineNumberColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#545c7e", // dark3 from palette
|
||||
Light: "#848cb5",
|
||||
}
|
||||
theme.DiffAddedLineNumberBgColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#1b2b34",
|
||||
Light: "#c5d5c5",
|
||||
}
|
||||
theme.DiffRemovedLineNumberBgColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#2d1f26",
|
||||
Light: "#e7c8cb",
|
||||
}
|
||||
|
||||
// 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 Tokyo Night theme with the theme manager
|
||||
RegisterTheme("tokyonight", NewTokyoNightTheme())
|
||||
}
|
||||
276
internal/tui/theme/tron.go
Normal file
276
internal/tui/theme/tron.go
Normal file
@@ -0,0 +1,276 @@
|
||||
package theme
|
||||
|
||||
import (
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
// TronTheme implements the Theme interface with Tron-inspired colors.
|
||||
// It provides both dark and light variants, though Tron is primarily a dark theme.
|
||||
type TronTheme struct {
|
||||
BaseTheme
|
||||
}
|
||||
|
||||
// NewTronTheme creates a new instance of the Tron theme.
|
||||
func NewTronTheme() *TronTheme {
|
||||
// Tron color palette
|
||||
// Inspired by the Tron movie's neon aesthetic
|
||||
darkBackground := "#0c141f"
|
||||
darkCurrentLine := "#1a2633"
|
||||
darkSelection := "#1a2633"
|
||||
darkForeground := "#caf0ff"
|
||||
darkComment := "#4d6b87"
|
||||
darkCyan := "#00d9ff"
|
||||
darkBlue := "#007fff"
|
||||
darkOrange := "#ff9000"
|
||||
darkPink := "#ff00a0"
|
||||
darkPurple := "#b73fff"
|
||||
darkRed := "#ff3333"
|
||||
darkYellow := "#ffcc00"
|
||||
darkGreen := "#00ff8f"
|
||||
darkBorder := "#1a2633"
|
||||
|
||||
// Light mode approximation
|
||||
lightBackground := "#f0f8ff"
|
||||
lightCurrentLine := "#e0f0ff"
|
||||
lightSelection := "#d0e8ff"
|
||||
lightForeground := "#0c141f"
|
||||
lightComment := "#4d6b87"
|
||||
lightCyan := "#0097b3"
|
||||
lightBlue := "#0066cc"
|
||||
lightOrange := "#cc7300"
|
||||
lightPink := "#cc0080"
|
||||
lightPurple := "#9932cc"
|
||||
lightRed := "#cc2929"
|
||||
lightYellow := "#cc9900"
|
||||
lightGreen := "#00cc72"
|
||||
lightBorder := "#d0e8ff"
|
||||
|
||||
theme := &TronTheme{}
|
||||
|
||||
// Base colors
|
||||
theme.PrimaryColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkCyan,
|
||||
Light: lightCyan,
|
||||
}
|
||||
theme.SecondaryColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkBlue,
|
||||
Light: lightBlue,
|
||||
}
|
||||
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: 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: "#070d14", // 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: darkGreen,
|
||||
Light: lightGreen,
|
||||
}
|
||||
theme.DiffRemovedColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkRed,
|
||||
Light: lightRed,
|
||||
}
|
||||
theme.DiffContextColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkComment,
|
||||
Light: lightComment,
|
||||
}
|
||||
theme.DiffHunkHeaderColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkBlue,
|
||||
Light: lightBlue,
|
||||
}
|
||||
theme.DiffHighlightAddedColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#00ff8f",
|
||||
Light: "#a5d6a7",
|
||||
}
|
||||
theme.DiffHighlightRemovedColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#ff3333",
|
||||
Light: "#ef9a9a",
|
||||
}
|
||||
theme.DiffAddedBgColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#0a2a1a",
|
||||
Light: "#e8f5e9",
|
||||
}
|
||||
theme.DiffRemovedBgColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#2a0a0a",
|
||||
Light: "#ffebee",
|
||||
}
|
||||
theme.DiffContextBgColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkBackground,
|
||||
Light: lightBackground,
|
||||
}
|
||||
theme.DiffLineNumberColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkComment,
|
||||
Light: lightComment,
|
||||
}
|
||||
theme.DiffAddedLineNumberBgColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#082015",
|
||||
Light: "#c8e6c9",
|
||||
}
|
||||
theme.DiffRemovedLineNumberBgColor = lipgloss.AdaptiveColor{
|
||||
Dark: "#200808",
|
||||
Light: "#ffcdd2",
|
||||
}
|
||||
|
||||
// Markdown colors
|
||||
theme.MarkdownTextColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkForeground,
|
||||
Light: lightForeground,
|
||||
}
|
||||
theme.MarkdownHeadingColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkCyan,
|
||||
Light: lightCyan,
|
||||
}
|
||||
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: darkCyan,
|
||||
Light: lightCyan,
|
||||
}
|
||||
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: darkBlue,
|
||||
Light: lightBlue,
|
||||
}
|
||||
theme.SyntaxTypeColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkPurple,
|
||||
Light: lightPurple,
|
||||
}
|
||||
theme.SyntaxOperatorColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkPink,
|
||||
Light: lightPink,
|
||||
}
|
||||
theme.SyntaxPunctuationColor = lipgloss.AdaptiveColor{
|
||||
Dark: darkForeground,
|
||||
Light: lightForeground,
|
||||
}
|
||||
|
||||
return theme
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Register the Tron theme with the theme manager
|
||||
RegisterTheme("tron", NewTronTheme())
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package tui
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
@@ -25,6 +26,8 @@ type keyMap struct {
|
||||
Help key.Binding
|
||||
SwitchSession key.Binding
|
||||
Commands key.Binding
|
||||
Models key.Binding
|
||||
SwitchTheme key.Binding
|
||||
}
|
||||
|
||||
var keys = keyMap{
|
||||
@@ -43,14 +46,24 @@ var keys = keyMap{
|
||||
),
|
||||
|
||||
SwitchSession: key.NewBinding(
|
||||
key.WithKeys("ctrl+a"),
|
||||
key.WithHelp("ctrl+a", "switch session"),
|
||||
key.WithKeys("ctrl+s"),
|
||||
key.WithHelp("ctrl+s", "switch session"),
|
||||
),
|
||||
|
||||
Commands: key.NewBinding(
|
||||
key.WithKeys("ctrl+k"),
|
||||
key.WithHelp("ctrl+k", "commands"),
|
||||
),
|
||||
|
||||
Models: key.NewBinding(
|
||||
key.WithKeys("ctrl+o"),
|
||||
key.WithHelp("ctrl+o", "model selection"),
|
||||
),
|
||||
|
||||
SwitchTheme: key.NewBinding(
|
||||
key.WithKeys("ctrl+t"),
|
||||
key.WithHelp("ctrl+t", "switch theme"),
|
||||
),
|
||||
}
|
||||
|
||||
var helpEsc = key.NewBinding(
|
||||
@@ -64,8 +77,8 @@ var returnKey = key.NewBinding(
|
||||
)
|
||||
|
||||
var logsKeyReturnKey = key.NewBinding(
|
||||
key.WithKeys("backspace", "q"),
|
||||
key.WithHelp("backspace/q", "go back"),
|
||||
key.WithKeys("esc", "backspace", "q"),
|
||||
key.WithHelp("esc/q", "go back"),
|
||||
)
|
||||
|
||||
type appModel struct {
|
||||
@@ -93,8 +106,14 @@ type appModel struct {
|
||||
commandDialog dialog.CommandDialog
|
||||
commands []dialog.Command
|
||||
|
||||
showModelDialog bool
|
||||
modelDialog dialog.ModelDialog
|
||||
|
||||
showInitDialog bool
|
||||
initDialog dialog.InitDialogCmp
|
||||
|
||||
showThemeDialog bool
|
||||
themeDialog dialog.ThemeDialog
|
||||
}
|
||||
|
||||
func (a appModel) Init() tea.Cmd {
|
||||
@@ -112,8 +131,12 @@ func (a appModel) Init() tea.Cmd {
|
||||
cmds = append(cmds, cmd)
|
||||
cmd = a.commandDialog.Init()
|
||||
cmds = append(cmds, cmd)
|
||||
cmd = a.modelDialog.Init()
|
||||
cmds = append(cmds, cmd)
|
||||
cmd = a.initDialog.Init()
|
||||
cmds = append(cmds, cmd)
|
||||
cmd = a.themeDialog.Init()
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
// Check if we should show the init dialog
|
||||
cmds = append(cmds, func() tea.Msg {
|
||||
@@ -243,6 +266,29 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
a.showCommandDialog = false
|
||||
return a, nil
|
||||
|
||||
case dialog.CloseThemeDialogMsg:
|
||||
a.showThemeDialog = false
|
||||
return a, nil
|
||||
|
||||
case dialog.ThemeChangedMsg:
|
||||
a.pages[a.currentPage], cmd = a.pages[a.currentPage].Update(msg)
|
||||
a.showThemeDialog = false
|
||||
return a, tea.Batch(cmd, util.ReportInfo("Theme changed to: "+msg.ThemeName))
|
||||
|
||||
case dialog.CloseModelDialogMsg:
|
||||
a.showModelDialog = false
|
||||
return a, nil
|
||||
|
||||
case dialog.ModelSelectedMsg:
|
||||
a.showModelDialog = false
|
||||
|
||||
model, err := a.app.CoderAgent.Update(config.AgentCoder, msg.Model.ID)
|
||||
if err != nil {
|
||||
return a, util.ReportError(err)
|
||||
}
|
||||
|
||||
return a, util.ReportInfo(fmt.Sprintf("Model changed to %s", model.Name))
|
||||
|
||||
case dialog.ShowInitDialogMsg:
|
||||
a.showInitDialog = msg.Show
|
||||
return a, nil
|
||||
@@ -298,6 +344,9 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if a.showCommandDialog {
|
||||
a.showCommandDialog = false
|
||||
}
|
||||
if a.showModelDialog {
|
||||
a.showModelDialog = false
|
||||
}
|
||||
return a, nil
|
||||
case key.Matches(msg, keys.SwitchSession):
|
||||
if a.currentPage == page.ChatPage && !a.showQuit && !a.showPermissions && !a.showCommandDialog {
|
||||
@@ -315,7 +364,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
return a, nil
|
||||
case key.Matches(msg, keys.Commands):
|
||||
if a.currentPage == page.ChatPage && !a.showQuit && !a.showPermissions && !a.showSessionDialog {
|
||||
if a.currentPage == page.ChatPage && !a.showQuit && !a.showPermissions && !a.showSessionDialog && !a.showThemeDialog {
|
||||
// Show commands dialog
|
||||
if len(a.commands) == 0 {
|
||||
return a, util.ReportWarn("No commands available")
|
||||
@@ -325,6 +374,24 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return a, nil
|
||||
}
|
||||
return a, nil
|
||||
case key.Matches(msg, keys.Models):
|
||||
if a.showModelDialog {
|
||||
a.showModelDialog = false
|
||||
return a, nil
|
||||
}
|
||||
if a.currentPage == page.ChatPage && !a.showQuit && !a.showPermissions && !a.showSessionDialog && !a.showCommandDialog {
|
||||
a.showModelDialog = true
|
||||
return a, nil
|
||||
}
|
||||
return a, nil
|
||||
case key.Matches(msg, keys.SwitchTheme):
|
||||
if !a.showQuit && !a.showPermissions && !a.showSessionDialog && !a.showCommandDialog {
|
||||
// Show theme switcher dialog
|
||||
a.showThemeDialog = true
|
||||
// Theme list is dynamically loaded by the dialog component
|
||||
return a, a.themeDialog.Init()
|
||||
}
|
||||
return a, nil
|
||||
case key.Matches(msg, logsKeyReturnKey):
|
||||
if a.currentPage == page.LogsPage {
|
||||
return a, a.moveToPage(page.ChatPage)
|
||||
@@ -405,6 +472,16 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
}
|
||||
|
||||
if a.showModelDialog {
|
||||
d, modelCmd := a.modelDialog.Update(msg)
|
||||
a.modelDialog = d.(dialog.ModelDialog)
|
||||
cmds = append(cmds, modelCmd)
|
||||
// Only block key messages send all other messages down
|
||||
if _, ok := msg.(tea.KeyMsg); ok {
|
||||
return a, tea.Batch(cmds...)
|
||||
}
|
||||
}
|
||||
|
||||
if a.showInitDialog {
|
||||
d, initCmd := a.initDialog.Update(msg)
|
||||
a.initDialog = d.(dialog.InitDialogCmp)
|
||||
@@ -415,6 +492,16 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
}
|
||||
|
||||
if a.showThemeDialog {
|
||||
d, themeCmd := a.themeDialog.Update(msg)
|
||||
a.themeDialog = d.(dialog.ThemeDialog)
|
||||
cmds = append(cmds, themeCmd)
|
||||
// Only block key messages send all other messages down
|
||||
if _, ok := msg.(tea.KeyMsg); ok {
|
||||
return a, tea.Batch(cmds...)
|
||||
}
|
||||
}
|
||||
|
||||
s, _ := a.status.Update(msg)
|
||||
a.status = s.(core.StatusCmp)
|
||||
a.pages[a.currentPage], cmd = a.pages[a.currentPage].Update(msg)
|
||||
@@ -473,9 +560,9 @@ func (a appModel) View() string {
|
||||
}
|
||||
|
||||
if !a.app.CoderAgent.IsBusy() {
|
||||
a.status.SetHelpMsg("ctrl+? help")
|
||||
a.status.SetHelpWidgetMsg("ctrl+? help")
|
||||
} else {
|
||||
a.status.SetHelpMsg("? help")
|
||||
a.status.SetHelpWidgetMsg("? help")
|
||||
}
|
||||
|
||||
if a.showHelp {
|
||||
@@ -538,6 +625,21 @@ func (a appModel) View() string {
|
||||
)
|
||||
}
|
||||
|
||||
if a.showModelDialog {
|
||||
overlay := a.modelDialog.View()
|
||||
row := lipgloss.Height(appView) / 2
|
||||
row -= lipgloss.Height(overlay) / 2
|
||||
col := lipgloss.Width(appView) / 2
|
||||
col -= lipgloss.Width(overlay) / 2
|
||||
appView = layout.PlaceOverlay(
|
||||
col,
|
||||
row,
|
||||
overlay,
|
||||
appView,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
if a.showCommandDialog {
|
||||
overlay := a.commandDialog.View()
|
||||
row := lipgloss.Height(appView) / 2
|
||||
@@ -564,6 +666,21 @@ func (a appModel) View() string {
|
||||
)
|
||||
}
|
||||
|
||||
if a.showThemeDialog {
|
||||
overlay := a.themeDialog.View()
|
||||
row := lipgloss.Height(appView) / 2
|
||||
row -= lipgloss.Height(overlay) / 2
|
||||
col := lipgloss.Width(appView) / 2
|
||||
col -= lipgloss.Width(overlay) / 2
|
||||
appView = layout.PlaceOverlay(
|
||||
col,
|
||||
row,
|
||||
overlay,
|
||||
appView,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
return appView
|
||||
}
|
||||
|
||||
@@ -577,8 +694,10 @@ func New(app *app.App) tea.Model {
|
||||
quit: dialog.NewQuitCmp(),
|
||||
sessionDialog: dialog.NewSessionDialogCmp(),
|
||||
commandDialog: dialog.NewCommandDialogCmp(),
|
||||
modelDialog: dialog.NewModelDialogCmp(),
|
||||
permissions: dialog.NewPermissionDialogCmp(),
|
||||
initDialog: dialog.NewInitDialogCmp(),
|
||||
themeDialog: dialog.NewThemeDialogCmp(),
|
||||
app: app,
|
||||
commands: []dialog.Command{},
|
||||
pages: map[page.PageID]tea.Model{
|
||||
|
||||
@@ -12,28 +12,63 @@
|
||||
"model": {
|
||||
"description": "Model ID for the agent",
|
||||
"enum": [
|
||||
"gemini-2.0-flash",
|
||||
"bedrock.claude-3.7-sonnet",
|
||||
"claude-3-opus",
|
||||
"claude-3.5-sonnet",
|
||||
"gpt-4o-mini",
|
||||
"o1",
|
||||
"o3-mini",
|
||||
"o1-pro",
|
||||
"o4-mini",
|
||||
"claude-3-haiku",
|
||||
"gpt-4o",
|
||||
"o3",
|
||||
"gpt-4.1-mini",
|
||||
"azure.gpt-4o-mini",
|
||||
"openrouter.gpt-4.1-mini",
|
||||
"openrouter.o1-mini",
|
||||
"bedrock.claude-3.7-sonnet",
|
||||
"meta-llama/llama-4-scout-17b-16e-instruct",
|
||||
"openrouter.gpt-4o-mini",
|
||||
"gemini-2.0-flash",
|
||||
"deepseek-r1-distill-llama-70b",
|
||||
"openrouter.claude-3.7-sonnet",
|
||||
"openrouter.gpt-4.5-preview",
|
||||
"azure.o3-mini",
|
||||
"openrouter.claude-3.5-haiku",
|
||||
"azure.o1-mini",
|
||||
"openrouter.o1",
|
||||
"openrouter.gemini-2.5",
|
||||
"llama-3.3-70b-versatile",
|
||||
"gpt-4.5-preview",
|
||||
"gemini-2.5-flash",
|
||||
"claude-3.5-haiku",
|
||||
"gpt-4.1",
|
||||
"openrouter.claude-3-opus",
|
||||
"openrouter.claude-3.5-sonnet",
|
||||
"o4-mini",
|
||||
"gemini-2.0-flash-lite",
|
||||
"claude-3.7-sonnet",
|
||||
"o1-mini",
|
||||
"azure.gpt-4.5-preview",
|
||||
"openrouter.gpt-4o",
|
||||
"o1",
|
||||
"azure.gpt-4o",
|
||||
"openrouter.gpt-4.1-nano",
|
||||
"o3",
|
||||
"gpt-4.1",
|
||||
"azure.o1",
|
||||
"claude-3-haiku",
|
||||
"claude-3-opus",
|
||||
"gpt-4.1-mini",
|
||||
"openrouter.o4-mini",
|
||||
"openrouter.gemini-2.5-flash",
|
||||
"claude-3.5-haiku",
|
||||
"o3-mini",
|
||||
"azure.o3",
|
||||
"gpt-4o",
|
||||
"azure.gpt-4.1",
|
||||
"openrouter.claude-3-haiku",
|
||||
"gpt-4.1-nano",
|
||||
"gemini-2.5"
|
||||
"azure.gpt-4.1-nano",
|
||||
"claude-3.7-sonnet",
|
||||
"gemini-2.5",
|
||||
"azure.o4-mini",
|
||||
"o1-mini",
|
||||
"qwen-qwq",
|
||||
"meta-llama/llama-4-maverick-17b-128e-instruct",
|
||||
"openrouter.gpt-4.1",
|
||||
"openrouter.o1-pro",
|
||||
"openrouter.o3",
|
||||
"claude-3.5-sonnet",
|
||||
"gemini-2.5-flash",
|
||||
"azure.gpt-4.1-mini",
|
||||
"openrouter.o3-mini"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
@@ -67,28 +102,63 @@
|
||||
"model": {
|
||||
"description": "Model ID for the agent",
|
||||
"enum": [
|
||||
"gemini-2.0-flash",
|
||||
"bedrock.claude-3.7-sonnet",
|
||||
"claude-3-opus",
|
||||
"claude-3.5-sonnet",
|
||||
"gpt-4o-mini",
|
||||
"o1",
|
||||
"o3-mini",
|
||||
"o1-pro",
|
||||
"o4-mini",
|
||||
"claude-3-haiku",
|
||||
"gpt-4o",
|
||||
"o3",
|
||||
"gpt-4.1-mini",
|
||||
"azure.gpt-4o-mini",
|
||||
"openrouter.gpt-4.1-mini",
|
||||
"openrouter.o1-mini",
|
||||
"bedrock.claude-3.7-sonnet",
|
||||
"meta-llama/llama-4-scout-17b-16e-instruct",
|
||||
"openrouter.gpt-4o-mini",
|
||||
"gemini-2.0-flash",
|
||||
"deepseek-r1-distill-llama-70b",
|
||||
"openrouter.claude-3.7-sonnet",
|
||||
"openrouter.gpt-4.5-preview",
|
||||
"azure.o3-mini",
|
||||
"openrouter.claude-3.5-haiku",
|
||||
"azure.o1-mini",
|
||||
"openrouter.o1",
|
||||
"openrouter.gemini-2.5",
|
||||
"llama-3.3-70b-versatile",
|
||||
"gpt-4.5-preview",
|
||||
"gemini-2.5-flash",
|
||||
"claude-3.5-haiku",
|
||||
"gpt-4.1",
|
||||
"openrouter.claude-3-opus",
|
||||
"openrouter.claude-3.5-sonnet",
|
||||
"o4-mini",
|
||||
"gemini-2.0-flash-lite",
|
||||
"claude-3.7-sonnet",
|
||||
"o1-mini",
|
||||
"azure.gpt-4.5-preview",
|
||||
"openrouter.gpt-4o",
|
||||
"o1",
|
||||
"azure.gpt-4o",
|
||||
"openrouter.gpt-4.1-nano",
|
||||
"o3",
|
||||
"gpt-4.1",
|
||||
"azure.o1",
|
||||
"claude-3-haiku",
|
||||
"claude-3-opus",
|
||||
"gpt-4.1-mini",
|
||||
"openrouter.o4-mini",
|
||||
"openrouter.gemini-2.5-flash",
|
||||
"claude-3.5-haiku",
|
||||
"o3-mini",
|
||||
"azure.o3",
|
||||
"gpt-4o",
|
||||
"azure.gpt-4.1",
|
||||
"openrouter.claude-3-haiku",
|
||||
"gpt-4.1-nano",
|
||||
"gemini-2.5"
|
||||
"azure.gpt-4.1-nano",
|
||||
"claude-3.7-sonnet",
|
||||
"gemini-2.5",
|
||||
"azure.o4-mini",
|
||||
"o1-mini",
|
||||
"qwen-qwq",
|
||||
"meta-llama/llama-4-maverick-17b-128e-instruct",
|
||||
"openrouter.gpt-4.1",
|
||||
"openrouter.o1-pro",
|
||||
"openrouter.o3",
|
||||
"claude-3.5-sonnet",
|
||||
"gemini-2.5-flash",
|
||||
"azure.gpt-4.1-mini",
|
||||
"openrouter.o3-mini"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
@@ -121,6 +191,26 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"contextPaths": {
|
||||
"default": [
|
||||
".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"
|
||||
],
|
||||
"description": "Context paths for the application",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"data": {
|
||||
"description": "Storage configuration",
|
||||
"properties": {
|
||||
@@ -249,7 +339,9 @@
|
||||
"openai",
|
||||
"gemini",
|
||||
"groq",
|
||||
"bedrock"
|
||||
"openrouter",
|
||||
"bedrock",
|
||||
"azure"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
@@ -259,6 +351,28 @@
|
||||
"description": "LLM provider configurations",
|
||||
"type": "object"
|
||||
},
|
||||
"tui": {
|
||||
"description": "Terminal User Interface configuration",
|
||||
"properties": {
|
||||
"theme": {
|
||||
"default": "opencode",
|
||||
"description": "TUI theme name",
|
||||
"enum": [
|
||||
"opencode",
|
||||
"catppuccin",
|
||||
"dracula",
|
||||
"flexoki",
|
||||
"gruvbox",
|
||||
"monokai",
|
||||
"onedark",
|
||||
"tokyonight",
|
||||
"tron"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"wd": {
|
||||
"description": "Working directory for the application",
|
||||
"type": "string"
|
||||
|
||||
Reference in New Issue
Block a user