Compare commits

..

82 Commits

Author SHA1 Message Date
Dax Raad
ffc231bd8b docs: contributing 2025-06-14 12:45:26 -04:00
Dax Raad
3cf4ef56fb sync 2025-06-14 12:32:41 -04:00
Dax Raad
c738e26438 docs: mcp 2025-06-14 12:25:26 -04:00
Dax Raad
9c6aa82ac1 docs: config schema 2025-06-14 12:22:07 -04:00
Dax Raad
ef74d97491 ci: update publish script 2025-06-14 12:13:59 -04:00
Dax Raad
af892e5432 docs: readme 2025-06-14 12:13:46 -04:00
Dax Raad
d7aca6230d naming fixes 2025-06-14 01:54:28 -04:00
Dax Raad
0f9c2c5c27 Add flag system and auto-share functionality
- Add Flag module for environment variable configuration
- Implement OPENCODE_AUTO_SHARE flag to automatically share new sessions
- Update session creation to conditionally auto-share based on flag

🤖 Generated with [OpenCode](https://opencode.ai)

Co-Authored-By: OpenCode <noreply@opencode.ai>
2025-06-14 01:51:04 -04:00
Dax Raad
6a261dedb4 Improve logging and simplify fzf implementation
- Refactor fzf search to use Bun's $ syntax for cleaner command execution
- Add request/response duration logging to server middleware
- Set default service name for logging to improve log clarity

🤖 Generated with [OpenCode](https://opencode.ai)

Co-Authored-By: OpenCode <noreply@opencode.ai>
2025-06-14 01:51:04 -04:00
Alireza Bahrami
ec928d88b5 fix(install): check if the path export command already exists (#28) 2025-06-13 23:28:33 -04:00
Dax Raad
59a5f120c0 Clean up workflows and enhance file discovery tools to include dot files
🤖 Generated with [OpenCode](https://opencode.ai)

Co-Authored-By: OpenCode <noreply@opencode.ai>
2025-06-13 23:24:46 -04:00
Dax Raad
ce07f80b19 sync 2025-06-13 17:42:56 -04:00
Dax Raad
168fd9b2e3 screenshot 2025-06-13 17:42:14 -04:00
Dax Raad
df13b155f9 disable autoshare 2025-06-13 17:30:17 -04:00
Dax Raad
eeed5b8718 sync 2025-06-13 17:24:45 -04:00
Dax Raad
148ef90210 sync 2025-06-13 17:23:22 -04:00
adamdottv
67023bb007 wip: refactoring tui 2025-06-13 15:56:33 -05:00
Dax Raad
a316aed4fe sync 2025-06-13 16:47:15 -04:00
Dax Raad
9f7c0bd599 sync 2025-06-13 16:46:48 -04:00
Dax Raad
c7e1068f90 sync 2025-06-13 16:45:58 -04:00
Dax Raad
e2052d790b sync 2025-06-13 16:43:53 -04:00
Dax Raad
d3b2763c14 commit and push 2025-06-13 16:42:31 -04:00
Dax Raad
c6492de7ac sync 2025-06-13 16:37:58 -04:00
Dax Raad
d8fa0fb50c sync 2025-06-13 16:29:57 -04:00
Dax Raad
18ab8faa1d reset readme 2025-06-13 16:26:34 -04:00
Dax Raad
f35ce180e2 ci 2025-06-13 16:23:38 -04:00
Dax Raad
2bee48a9bc homebrew 2025-06-13 16:17:27 -04:00
adamdottv
10ddd654cf wip: refactoring tui 2025-06-13 11:27:05 -05:00
adamdottv
61396b93ed wip: refactoring tui 2025-06-13 11:18:46 -05:00
adamdottv
62b9a30a9c wip: refactoring tui 2025-06-13 10:47:51 -05:00
adamdottv
5706c6ad3a wip: refactoring tui 2025-06-13 09:57:54 -05:00
adamdottv
e8e03c895a wip: refactoring tui 2025-06-13 09:44:09 -05:00
adamdottv
38667682a7 wip: refactoring tui 2025-06-13 09:19:51 -05:00
adamdottv
d7d5fc39fb wip: refactoring tui 2025-06-13 08:30:57 -05:00
adamdottv
0caf25adee wip: refactoring tui 2025-06-13 08:30:56 -05:00
Dax Raad
37febc6873 do not strip aur package 2025-06-13 08:27:17 -04:00
adamdottv
4169f0c412 wip: refactoring tui 2025-06-13 07:01:26 -05:00
adamdottv
b7f06bbc1f wip: refactoring tui 2025-06-13 06:56:12 -05:00
adamdottv
1b8cfe9e99 wip: refactoring tui 2025-06-13 06:49:59 -05:00
adamdottv
97837d2d23 wip: refactoring tui 2025-06-13 06:23:12 -05:00
Dax Raad
9abc2a0cf8 load API keys 2025-06-13 00:53:46 -04:00
Dax Raad
9fb47bc855 Enhance auth command with dynamic provider selection
- Add support for dynamically loading providers from ModelsDev
- Prioritize anthropic as recommended provider
- Add "other" provider option for manual entry
- Include special handling for amazon-bedrock with AWS config guidance
- Expand provider selection UI to show up to 8 providers

🤖 Generated with [OpenCode](https://opencode.ai)

Co-Authored-By: OpenCode <noreply@opencode.ai>
2025-06-13 00:33:54 -04:00
Dax Raad
73e9fb53d5 sync 2025-06-13 00:06:15 -04:00
Dax Raad
f03637b1fc Refactor AI SDK provider loading to use BunProc.install
Simplifies provider installation by using BunProc.install() instead of manual path construction and file system checks.

🤖 Generated with [OpenCode](https://opencode.ai)

Co-Authored-By: OpenCode <noreply@opencode.ai>
2025-06-12 23:50:26 -04:00
Dax Raad
2c376c5abc bedrock loader 2025-06-12 23:39:52 -04:00
Dax Raad
442e1b52ad Update provider configuration and server handling
🤖 Generated with [OpenCode](https://opencode.ai)

Co-Authored-By: OpenCode <noreply@opencode.ai>
2025-06-12 23:10:03 -04:00
Thomas Meire
e8c3abc369 Update error message to say opencode instead of sst (#81) 2025-06-12 18:38:59 -04:00
Dax Raad
c8648baba2 ci 2025-06-12 18:30:19 -04:00
Dax Raad
7b3a799856 ci 2025-06-12 18:21:08 -04:00
Dax Raad
9356b6c35a sync 2025-06-12 18:14:04 -04:00
Dax Raad
29a6603a89 Update CLI run command and session handling
🤖 Generated with [OpenCode](https://opencode.ai)

Co-Authored-By: OpenCode <noreply@opencode.ai>
2025-06-12 18:07:31 -04:00
Dax Raad
a454ba8895 subagent 2025-06-12 18:07:31 -04:00
Jay V
5eae7aef0e updating logo 2025-06-12 17:30:24 -04:00
adamdottv
1031bceef7 wip: refactoring tui 2025-06-12 16:04:45 -05:00
adamdottv
653965ef59 wip: refactoring tui 2025-06-12 16:00:26 -05:00
adamdottv
ca0ea3f94d wip: refactoring tui 2025-06-12 16:00:25 -05:00
adamdottv
98bd5109c2 wip: refactoring tui 2025-06-12 16:00:25 -05:00
adamdottv
78f65e4789 wip: refactoring tui 2025-06-12 16:00:25 -05:00
adamdottv
75dd2f75aa wip: refactoring tui 2025-06-12 16:00:25 -05:00
adamdottv
fe86e58bbb wip: refactoring tui 2025-06-12 16:00:24 -05:00
adamdottv
ae339015fc wip: refactoring tui 2025-06-12 16:00:24 -05:00
adamdottv
cce2e4ad75 wip: refactoring tui 2025-06-12 16:00:24 -05:00
Dax Raad
a1ce35c208 ci 2025-06-12 14:15:44 -04:00
Dax Raad
69d6709a19 sync 2025-06-12 14:11:01 -04:00
Dax Raad
52ec134b2d Update publish workflow to support snapshot releases on dontlook branch
🤖 Generated with [OpenCode](https://opencode.ai)

Co-Authored-By: OpenCode <noreply@opencode.ai>
2025-06-12 14:10:29 -04:00
Dax Raad
db88bede05 sync 2025-06-12 14:06:06 -04:00
Dax Raad
d4d218d7d6 Update index.ts
🤖 Generated with [OpenCode](https://opencode.ai)

Co-Authored-By: OpenCode <noreply@opencode.ai>
2025-06-12 13:59:42 -04:00
Dax Raad
3e086e3ab9 sync 2025-06-12 13:49:43 -04:00
Jay V
2f5faae34b fix share page edit 2025-06-12 13:42:10 -04:00
Dax Raad
e3ad6a0698 do not output bunproc 2025-06-12 13:39:03 -04:00
Dax Raad
b536b45536 Fix AUR SSH key path handling in publish script
Quote and trim AUR_KEY environment variable to handle paths with spaces and multiline content properly.

🤖 Generated with [OpenCode](https://opencode.ai)

Co-Authored-By: OpenCode <noreply@opencode.ai>
2025-06-12 13:37:12 -04:00
Dax Raad
81c245035f Simplify BunProc.which() to use process.execPath directly
🤖 Generated with [OpenCode](https://opencode.ai)

Co-Authored-By: OpenCode <noreply@opencode.ai>
2025-06-12 13:32:31 -04:00
Dax Raad
dda7059e57 update bun integration
🤖 Generated with [OpenCode](https://opencode.ai)

Co-Authored-By: OpenCode <noreply@opencode.ai>
2025-06-12 13:29:14 -04:00
Dax Raad
0cca75ef48 sync 2025-06-12 13:19:24 -04:00
Dax Raad
ee1f55dbe2 token 2025-06-12 13:17:06 -04:00
Dax Raad
2fa50190e5 skip nil values 2025-06-12 13:13:34 -04:00
Jay V
662b6b1258 share page handle undefined 2025-06-12 13:11:34 -04:00
Dax Raad
f0dbe40522 sync 2025-06-12 13:04:01 -04:00
Dax Raad
41c54f629c sync 2025-06-12 12:48:38 -04:00
Dax Raad
4503201b15 npm token 2025-06-12 12:09:13 -04:00
Dax Raad
120151ee38 sync 2025-06-12 11:58:41 -04:00
Dax Raad
4d2e556713 sync 2025-06-12 11:57:57 -04:00
99 changed files with 3643 additions and 7915 deletions

View File

@@ -1,37 +0,0 @@
name: build
on:
workflow_dispatch:
push:
branches:
- dev
concurrency: ${{ github.workflow }}-${{ github.ref }}
permissions:
contents: write
packages: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- run: git fetch --force --tags
- uses: actions/setup-go@v5
with:
go-version: ">=1.23.2"
cache: true
cache-dependency-path: go.sum
- run: go mod download
- uses: goreleaser/goreleaser-action@v6
with:
distribution: goreleaser
version: latest
args: build --snapshot --clean

63
.github/workflows/publish.yml vendored Normal file
View File

@@ -0,0 +1,63 @@
name: publish
on:
workflow_dispatch:
push:
branches:
- dev
tags:
- "*"
concurrency: ${{ github.workflow }}-${{ github.ref }}
permissions:
contents: write
packages: write
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- run: git fetch --force --tags
- uses: actions/setup-go@v5
with:
go-version: ">=1.24.0"
cache: true
cache-dependency-path: go.sum
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.2.16
- name: Install makepkg
run: |
sudo apt-get update
sudo apt-get install -y pacman-package-manager
- name: Setup SSH for AUR
run: |
mkdir -p ~/.ssh
echo "${{ secrets.AUR_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
ssh-keyscan -H aur.archlinux.org >> ~/.ssh/known_hosts
git config --global user.email "opencode@sst.dev"
git config --global user.name "opencode"
- name: Publish
run: |
bun install
if [ "${{ startsWith(github.ref, 'refs/tags/') }}" = "true" ]; then
./script/publish.ts
else
./script/publish.ts --snapshot
fi
working-directory: ./packages/opencode
env:
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
AUR_KEY: ${{ secrets.AUR_KEY }}
NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -1,39 +0,0 @@
name: release
on:
workflow_dispatch:
push:
tags:
- "*"
concurrency: ${{ github.workflow }}-${{ github.ref }}
permissions:
contents: write
packages: write
jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- run: git fetch --force --tags
- uses: actions/setup-go@v5
with:
go-version: ">=1.23.2"
cache: true
cache-dependency-path: go.sum
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.2.16
- run: ./script/publish.ts
working-directory: ./packages/opencode
env:
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
AUR_KEY: ${{ secrets.AUR_KEY }}

680
README.md
View File

@@ -1,658 +1,92 @@
opencode
[![OpenCode Terminal UI](screenshot.png)](https://github.com/sst/opencode)
![OpenCode Terminal UI](screenshot.png)
AI coding agent, built for the terminal.
> **⚠️ Notice:** We are in progress of a complete overhaul in the `dontlook` branch - should be released mid June. The README below is for the current version
⚠️ **Note:** version 0.1.x is a full rewrite and we do not have proper documentation for it yet. Should have this out week of June 17th 2025 📚
A powerful terminal-based AI assistant for developers, providing intelligent coding assistance directly in your terminal.
## Overview
OpenCode is a Go-based CLI application that brings AI assistance to your terminal. It provides a TUI (Terminal User Interface) for interacting with various AI models to help with coding tasks, debugging, and more.
## Features
- **Interactive TUI**: Built with [Bubble Tea](https://github.com/charmbracelet/bubbletea) for a smooth terminal experience
- **Multiple AI Providers**: Support for OpenAI, Anthropic Claude, Google Gemini, AWS Bedrock, Groq, Azure OpenAI, and OpenRouter
- **Session Management**: Save and manage multiple conversation sessions
- **Tool Integration**: AI can execute commands, search files, and modify code
- **Vim-like Editor**: Integrated editor with text input capabilities
- **Persistent Storage**: SQLite database for storing conversations and sessions
- **LSP Integration**: Language Server Protocol support for code intelligence
- **File Change Tracking**: Track and visualize file changes during sessions
- **External Editor Support**: Open your preferred editor for composing messages
- **Named Arguments for Custom Commands**: Create powerful custom commands with multiple named placeholders
## Installation
### Using the Install Script
### Installation
```bash
# Install the latest version
# YOLO
curl -fsSL https://opencode.ai/install | bash
# Install a specific version
curl -fsSL https://opencode.ai/install | VERSION=0.1.0 bash
# Package managers
npm i -g opencode-ai@latest # or bun/pnpm/yarn
brew install sst/tap/opencode # macOS
paru -S opencode-bin # Arch Linux
```
### Using Homebrew (macOS and Linux)
> **Note:** Remove previous versions < 0.1.x first if installed
### Providers
The recommended approach is to sign up for claude pro or max and do `opencode auth login` and select Anthropic. It is the most cost effective way to use this tool.
Additionally opencode is powered by the provider list at [models.dev](https://models.dev) so you can use `opencode auth login` to configure api keys for any provider you'd like to use. This is stored in `~/.local/share/opencode/auth.json`
```bash
brew install sst/tap/opencode
$ opencode auth login
┌ Add credential
◆ Select provider
│ ● Anthropic (recommended)
│ ○ OpenAI
│ ○ Google
│ ○ Amazon Bedrock
│ ○ Azure
│ ○ DeepSeek
│ ○ Groq
│ ...
```
### Using AUR (Arch Linux)
The models.dev dataset is also used to detect common environment variables like `OPENAI_API_KEY` to autoload that provider.
```bash
# Using yay
yay -S opencode-bin
If there are additional providers you want to use you can submit a PR to the [models.dev repo](https://github.com/sst/models.dev). If configuring just for yourself check out the Config section below
# Using paru
paru -S opencode-bin
```
### Project Config
### Using Go
Project configuration is optional. You can place an `opencode.json` file in the root of your repo and it will be loaded.
```bash
go install github.com/sst/opencode@latest
```
## Configuration
OpenCode looks for configuration in the following locations:
- `$HOME/.opencode.json`
- `$XDG_CONFIG_HOME/opencode/.opencode.json`
- `./.opencode.json` (local directory)
### Environment Variables
You can configure OpenCode using environment variables:
| Environment Variable | Purpose |
| -------------------------- | ------------------------------------------------------ |
| `ANTHROPIC_API_KEY` | For Claude models |
| `OPENAI_API_KEY` | For OpenAI models |
| `GEMINI_API_KEY` | For Google Gemini models |
| `VERTEXAI_PROJECT` | For Google Cloud VertexAI (Gemini) |
| `VERTEXAI_LOCATION` | For Google Cloud VertexAI (Gemini) |
| `GROQ_API_KEY` | For Groq models |
| `AWS_ACCESS_KEY_ID` | For AWS Bedrock (Claude) |
| `AWS_SECRET_ACCESS_KEY` | For AWS Bedrock (Claude) |
| `AWS_REGION` | For AWS Bedrock (Claude) |
| `AZURE_OPENAI_ENDPOINT` | For Azure OpenAI models |
| `AZURE_OPENAI_API_KEY` | For Azure OpenAI models (optional when using Entra ID) |
| `AZURE_OPENAI_API_VERSION` | For Azure OpenAI models |
### Configuration File Structure
```json
```json title="opencode.json"
{
"data": {
"directory": ".opencode"
},
"providers": {
"openai": {
"apiKey": "your-api-key",
"disabled": false
},
"anthropic": {
"apiKey": "your-api-key",
"disabled": false
},
"groq": {
"apiKey": "your-api-key",
"disabled": false
},
"openrouter": {
"apiKey": "your-api-key",
"disabled": false
}
},
"agents": {
"primary": {
"model": "claude-3.7-sonnet",
"maxTokens": 5000
},
"task": {
"model": "claude-3.7-sonnet",
"maxTokens": 5000
},
"title": {
"model": "claude-3.7-sonnet",
"maxTokens": 80
}
},
"mcpServers": {
"example": {
"type": "stdio",
"command": "path/to/mcp-server",
"env": [],
"args": []
}
},
"lsp": {
"go": {
"disabled": false,
"command": "gopls"
}
},
"shell": {
"path": "/bin/zsh",
"args": ["-l"]
},
"debug": false,
"debugLSP": false
"$schema": "http://opencode.ai/config.json"
}
```
## Supported AI Models
#### MCP
OpenCode supports a variety of AI models from different providers:
### OpenAI
- GPT-4.1 family (gpt-4.1, gpt-4.1-mini, gpt-4.1-nano)
- GPT-4.5 Preview
- GPT-4o family (gpt-4o, gpt-4o-mini)
- O1 family (o1, o1-pro, o1-mini)
- O3 family (o3, o3-mini)
- O4 Mini
### Anthropic
- Claude 3.5 Sonnet
- Claude 3.5 Haiku
- Claude 3.7 Sonnet
- Claude 3 Haiku
- Claude 3 Opus
### Google
- Gemini 2.5
- Gemini 2.5 Flash
- Gemini 2.0 Flash
- Gemini 2.0 Flash Lite
### AWS Bedrock
- Claude 3.7 Sonnet
### Groq
- Llama 4 Maverick (17b-128e-instruct)
- Llama 4 Scout (17b-16e-instruct)
- QWEN QWQ-32b
- Deepseek R1 distill Llama 70b
- Llama 3.3 70b Versatile
### Azure OpenAI
- GPT-4.1 family (gpt-4.1, gpt-4.1-mini, gpt-4.1-nano)
- GPT-4.5 Preview
- GPT-4o family (gpt-4o, gpt-4o-mini)
- O1 family (o1, o1-mini)
- O3 family (o3, o3-mini)
- O4 Mini
### Google Cloud VertexAI
- Gemini 2.5
- Gemini 2.5 Flash
## Using Bedrock Models
To use bedrock models with OpenCode you need three things.
1. Valid AWS credentials (the env vars: `AWS_SECRET_KEY_ID`, `AWS_SECRET_ACCESS_KEY` and `AWS_REGION`)
2. Access to the corresponding model in AWS Bedrock in your region.
a. You can request access in the AWS console on the Bedrock -> "Model access" page.
3. A correct configuration file. You don't need the `providers` key. Instead you have to prefix your models per agent with `bedrock.` and then a valid model. For now only Claude 3.7 is supported.
```json
```json title="opencode.json"
{
"agents": {
"primary": {
"model": "bedrock.claude-3.7-sonnet",
"maxTokens": 5000,
"reasoningEffort": ""
},
"task": {
"model": "bedrock.claude-3.7-sonnet",
"maxTokens": 5000,
"reasoningEffort": ""
},
"title": {
"model": "bedrock.claude-3.7-sonnet",
"maxTokens": 80,
"reasoningEffort": ""
}
}
}
```
## Interactive Mode Usage
```bash
# Start OpenCode
opencode
# Start with debug logging
opencode -d
# Start with a specific working directory
opencode -c /path/to/project
```
## Non-interactive Prompt Mode
You can run OpenCode in non-interactive mode by passing a prompt directly as a command-line argument or by piping text into the command. This is useful for scripting, automation, or when you want a quick answer without launching the full TUI.
```bash
# Run a single prompt and print the AI's response to the terminal
opencode -p "Explain the use of context in Go"
# Pipe input to OpenCode (equivalent to using -p flag)
echo "Explain the use of context in Go" | opencode
# Get response in JSON format
opencode -p "Explain the use of context in Go" -f json
# Or with piped input
echo "Explain the use of context in Go" | opencode -f json
# Run without showing the spinner
opencode -p "Explain the use of context in Go" -q
# Or with piped input
echo "Explain the use of context in Go" | opencode -q
# Enable verbose logging to stderr
opencode -p "Explain the use of context in Go" --verbose
# Or with piped input
echo "Explain the use of context in Go" | opencode --verbose
# Restrict the agent to only use specific tools
opencode -p "Explain the use of context in Go" --allowedTools=view,ls,glob
# Or with piped input
echo "Explain the use of context in Go" | opencode --allowedTools=view,ls,glob
# Prevent the agent from using specific tools
opencode -p "Explain the use of context in Go" --excludedTools=bash,edit
# Or with piped input
echo "Explain the use of context in Go" | opencode --excludedTools=bash,edit
```
In this mode, OpenCode will process your prompt, print the result to standard output, and then exit. All permissions are auto-approved for the session.
### Tool Restrictions
You can control which tools the AI assistant has access to in non-interactive mode:
- `--allowedTools`: Comma-separated list of tools that the agent is allowed to use. Only these tools will be available.
- `--excludedTools`: Comma-separated list of tools that the agent is not allowed to use. All other tools will be available.
These flags are mutually exclusive - you can use either `--allowedTools` or `--excludedTools`, but not both at the same time.
### Output Formats
OpenCode supports the following output formats in non-interactive mode:
| Format | Description |
| ------ | ------------------------------- |
| `text` | Plain text output (default) |
| `json` | Output wrapped in a JSON object |
The output format is implemented as a strongly-typed `OutputFormat` in the codebase, ensuring type safety and validation when processing outputs.
## Command-line Flags
| Flag | Short | Description |
| ----------------- | ----- | --------------------------------------------------- |
| `--help` | `-h` | Display help information |
| `--debug` | `-d` | Enable debug mode |
| `--cwd` | `-c` | Set current working directory |
| `--prompt` | `-p` | Run a single prompt in non-interactive mode |
| `--output-format` | `-f` | Output format for non-interactive mode (text, json) |
| `--quiet` | `-q` | Hide spinner in non-interactive mode |
| `--verbose` | | Display logs to stderr in non-interactive mode |
| `--allowedTools` | | Restrict the agent to only use specified tools |
| `--excludedTools` | | Prevent the agent from using specified tools |
## Keyboard Shortcuts
### Global Shortcuts
| Shortcut | Action |
| -------- | ------------------------------------------------------- |
| `Ctrl+C` | Quit application |
| `Ctrl+?` | Toggle help dialog |
| `?` | Toggle help dialog (when not in editing mode) |
| `Ctrl+L` | View logs |
| `Ctrl+A` | Switch session |
| `Ctrl+K` | Command dialog |
| `Ctrl+O` | Toggle model selection dialog |
| `Esc` | Close current overlay/dialog or return to previous mode |
### Chat Page Shortcuts
| Shortcut | Action |
| -------- | --------------------------------------- |
| `Ctrl+N` | Create new session |
| `Ctrl+X` | Cancel current operation/generation |
| `i` | Focus editor (when not in writing mode) |
| `Esc` | Exit writing mode and focus messages |
### Editor Shortcuts
| Shortcut | Action |
| ------------------- | ----------------------------------------- |
| `Ctrl+S` | Send message (when editor is focused) |
| `Enter` or `Ctrl+S` | Send message (when editor is not focused) |
| `Ctrl+E` | Open external editor |
| `Esc` | Blur editor and focus messages |
### Session Dialog Shortcuts
| Shortcut | Action |
| ---------- | ---------------- |
| `↑` or `k` | Previous session |
| `↓` or `j` | Next session |
| `Enter` | Select session |
| `Esc` | Close dialog |
### Model Dialog Shortcuts
| Shortcut | Action |
| ---------- | ----------------- |
| `↑` or `k` | Move up |
| `↓` or `j` | Move down |
| `←` or `h` | Previous provider |
| `→` or `l` | Next provider |
| `Esc` | Close dialog |
### Permission Dialog Shortcuts
| Shortcut | Action |
| ----------------------- | ---------------------------- |
| `←` or `left` | Switch options left |
| `→` or `right` or `tab` | Switch options right |
| `Enter` or `space` | Confirm selection |
| `a` | Allow permission |
| `A` | Allow permission for session |
| `d` | Deny permission |
### Logs Page Shortcuts
| Shortcut | Action |
| ------------------ | ------------------- |
| `Backspace` or `q` | Return to chat page |
## AI Assistant Tools
OpenCode's AI assistant has access to various tools to help with coding tasks:
### File and Code Tools
| Tool | Description | Parameters |
| ------------- | --------------------------- | ---------------------------------------------------------------------------------------- |
| `glob` | Find files by pattern | `pattern` (required), `path` (optional) |
| `grep` | Search file contents | `pattern` (required), `path` (optional), `include` (optional), `literal_text` (optional) |
| `ls` | List directory contents | `path` (optional), `ignore` (optional array of patterns) |
| `view` | View file contents | `file_path` (required), `offset` (optional), `limit` (optional) |
| `write` | Write to files | `file_path` (required), `content` (required) |
| `edit` | Edit files | Various parameters for file editing |
| `patch` | Apply patches to files | `file_path` (required), `diff` (required) |
| `diagnostics` | Get diagnostics information | `file_path` (optional) |
### Other Tools
| Tool | Description | Parameters |
| ------- | ------------------------------- | ----------------------------------------------------------- |
| `bash` | Execute shell commands | `command` (required), `timeout` (optional) |
| `fetch` | Fetch data from URLs | `url` (required), `format` (required), `timeout` (optional) |
| `agent` | Run sub-tasks with the AI agent | `prompt` (required) |
### Shell Configuration
OpenCode allows you to configure the shell used by the `bash` tool. By default, it uses:
1. The shell specified in the config file (if provided)
2. The shell from the `$SHELL` environment variable (if available)
3. Falls back to `/bin/bash` if neither of the above is available
To configure a custom shell, add a `shell` section to your `.opencode.json` configuration file:
```json
{
"shell": {
"path": "/bin/zsh",
"args": ["-l"]
}
}
```
You can specify any shell executable and custom arguments:
```json
{
"shell": {
"path": "/usr/bin/fish",
"args": []
}
}
```
## Architecture
OpenCode is built with a modular architecture:
- **cmd**: Command-line interface using Cobra
- **internal/app**: Core application services
- **internal/config**: Configuration management
- **internal/db**: Database operations and migrations
- **internal/llm**: LLM providers and tools integration
- **internal/tui**: Terminal UI components and layouts
- **internal/logging**: Logging infrastructure
- **internal/message**: Message handling
- **internal/session**: Session management
- **internal/lsp**: Language Server Protocol integration
## Custom Commands
OpenCode supports custom commands that can be created by users to quickly send predefined prompts to the AI assistant.
### Creating Custom Commands
Custom commands are predefined prompts stored as Markdown files in one of three locations:
1. **User Commands** (prefixed with `user:`):
```
$XDG_CONFIG_HOME/opencode/commands/
```
(typically `~/.config/opencode/commands/` on Linux/macOS)
or
```
$HOME/.opencode/commands/
```
2. **Project Commands** (prefixed with `project:`):
```
<PROJECT DIR>/.opencode/commands/
```
Each `.md` file in these directories becomes a custom command. The file name (without extension) becomes the command ID.
For example, creating a file at `~/.config/opencode/commands/prime-context.md` with content:
```markdown
RUN git ls-files
READ README.md
```
This creates a command called `user:prime-context`.
### Command Arguments
OpenCode supports named arguments in custom commands using placeholders in the format `$NAME` (where NAME consists of uppercase letters, numbers, and underscores, and must start with a letter).
For example:
```markdown
# Fetch Context for Issue $ISSUE_NUMBER
RUN gh issue view $ISSUE_NUMBER --json title,body,comments
RUN git grep --author="$AUTHOR_NAME" -n .
RUN grep -R "$SEARCH_PATTERN" $DIRECTORY
```
When you run a command with arguments, OpenCode will prompt you to enter values for each unique placeholder. Named arguments provide several benefits:
- Clear identification of what each argument represents
- Ability to use the same argument multiple times
- Better organization for commands with multiple inputs
### Organizing Commands
You can organize commands in subdirectories:
```
~/.config/opencode/commands/git/commit.md
```
This creates a command with ID `user:git:commit`.
### Using Custom Commands
1. Press `Ctrl+K` to open the command dialog
2. Select your custom command (prefixed with either `user:` or `project:`)
3. Press Enter to execute the command
The content of the command file will be sent as a message to the AI assistant.
## MCP (Model Context Protocol)
OpenCode implements the Model Context Protocol (MCP) to extend its capabilities through external tools. MCP provides a standardized way for the AI assistant to interact with external services and tools.
### MCP Features
- **External Tool Integration**: Connect to external tools and services via a standardized protocol
- **Tool Discovery**: Automatically discover available tools from MCP servers
- **Multiple Connection Types**:
- **Stdio**: Communicate with tools via standard input/output
- **SSE**: Communicate with tools via Server-Sent Events
- **Security**: Permission system for controlling access to MCP tools
### Configuring MCP Servers
MCP servers are defined in the configuration file under the `mcpServers` section:
```json
{
"mcpServers": {
"example": {
"type": "stdio",
"command": "path/to/mcp-server",
"env": [],
"args": []
},
"web-example": {
"type": "sse",
"url": "https://example.com/mcp",
"headers": {
"Authorization": "Bearer token"
"$schema": "http://opencode.ai/config.json",
"mcp": {
"localmcp": {
"type": "local",
"command": ["bun", "x", "my-mcp-command"],
"environment": {
"MY_ENV_VAR": "my_env_var_value"
}
}
}
}
```
### MCP Tool Usage
Once configured, MCP tools are automatically available to the AI assistant alongside built-in tools. They follow the same permission model as other tools, requiring user approval before execution.
## LSP (Language Server Protocol)
OpenCode integrates with Language Server Protocol to provide code intelligence features across multiple programming languages.
### LSP Features
- **Multi-language Support**: Connect to language servers for different programming languages
- **Diagnostics**: Receive error checking and linting information
- **File Watching**: Automatically notify language servers of file changes
### Configuring LSP
Language servers are configured in the configuration file under the `lsp` section:
```json
{
"lsp": {
"go": {
"disabled": false,
"command": "gopls"
},
"typescript": {
"disabled": false,
"command": "typescript-language-server",
"args": ["--stdio"]
"remotemcp": {
"type": "remote",
"url": "https://my-mcp-server.com"
}
}
}
```
### LSP Integration with AI
### Contributing
The AI assistant can access LSP features through the `diagnostics` tool, allowing it to:
To run opencode locally you need
- Check for errors in your code
- Suggest fixes based on diagnostics
- bun
- golang 1.24.x
While the LSP client implementation supports the full LSP protocol (including completions, hover, definition, etc.), currently only diagnostics are exposed to the AI assistant.
To run
## Development
### Prerequisites
- Go 1.24.0 or higher
### Building from Source
```bash
# Clone the repository
git clone https://github.com/sst/opencode.git
cd opencode
# Build
go build -o opencode
# Run
./opencode
```
## Acknowledgments
OpenCode gratefully acknowledges the contributions and support from these key individuals:
- [@isaacphi](https://github.com/isaacphi) - For the [mcp-language-server](https://github.com/isaacphi/mcp-language-server) project which provided the foundation for our LSP client implementation
- [@adamdottv](https://github.com/adamdottv) - For the design direction and UI/UX architecture
Special thanks to the broader open source community whose tools and libraries have made this project possible.
## License
OpenCode is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
## Contributing
Contributions are welcome! Here's how you can contribute:
1. Fork the repository
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request
Please make sure to update tests as appropriate and follow the existing code style.
$ bun install
$ cd packages/opencode
$ bun run src/index.ts
```

View File

@@ -50,6 +50,7 @@
"@types/turndown": "5.0.5",
"@types/yargs": "17.0.33",
"typescript": "catalog:",
"zod-to-json-schema": "3.24.5",
},
},
"packages/web": {
@@ -425,7 +426,7 @@
"@types/babel__traverse": ["@types/babel__traverse@7.20.7", "", { "dependencies": { "@babel/types": "^7.20.7" } }, "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng=="],
"@types/bun": ["@types/bun@1.2.15", "", { "dependencies": { "bun-types": "1.2.15" } }, "sha512-U1ljPdBEphF0nw1MIk0hI7kPg7dFdPyM7EenHsp6W5loNHl7zqy6JQf/RKCgnUn2KDzUpkBwHPnEJEjII594bA=="],
"@types/bun": ["@types/bun@1.2.16", "", { "dependencies": { "bun-types": "1.2.16" } }, "sha512-1aCZJ/6nSiViw339RsaNhkNoEloLaPzZhxMOYEa7OzRzO41IGg5n/7I43/ZIAW/c+Q6cT12Vf7fOZOoVIzb5BQ=="],
"@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
@@ -561,7 +562,7 @@
"buffer": ["buffer@4.9.2", "", { "dependencies": { "base64-js": "^1.0.2", "ieee754": "^1.1.4", "isarray": "^1.0.0" } }, "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg=="],
"bun-types": ["bun-types@1.2.15", "", { "dependencies": { "@types/node": "*" } }, "sha512-NarRIaS+iOaQU1JPfyKhZm4AsUOrwUOqRNHY0XxI8GI8jYxiLXLcdjYMG9UKS+fwWasc1uw1htV9AX24dD+p4w=="],
"bun-types": ["bun-types@1.2.16", "", { "dependencies": { "@types/node": "*" } }, "sha512-ciXLrHV4PXax9vHvUrkvun9VPVGOVwbbbBF/Ev1cXz12lyEZMoJpIJABOfPcN9gDJRaiKF9MVbSygLg4NXu3/A=="],
"bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],

25
install
View File

@@ -12,23 +12,28 @@ requested_version=${VERSION:-}
os=$(uname -s | tr '[:upper:]' '[:lower:]')
if [[ "$os" == "darwin" ]]; then
os="mac"
os="darwin"
fi
arch=$(uname -m)
if [[ "$arch" == "aarch64" ]]; then
arch="arm64"
elif [[ "$arch" == "x86_64" ]]; then
arch="x64"
fi
filename="$APP-$os-$arch.tar.gz"
filename="$APP-$os-$arch.zip"
case "$filename" in
*"-linux-"*)
[[ "$arch" == "x86_64" || "$arch" == "arm64" || "$arch" == "i386" ]] || exit 1
[[ "$arch" == "x64" || "$arch" == "arm64" ]] || exit 1
;;
*"-mac-"*)
[[ "$arch" == "x86_64" || "$arch" == "arm64" ]] || exit 1
*"-darwin-"*)
[[ "$arch" == "x64" || "$arch" == "arm64" ]] || exit 1
;;
*"-windows-"*)
[[ "$arch" == "x64" ]] || exit 1
;;
*)
echo "${RED}Unsupported OS/Arch: $os/$arch${NC}"
@@ -88,8 +93,9 @@ check_version() {
download_and_install() {
print_message info "Downloading ${ORANGE}opencode ${GREEN}version: ${YELLOW}$specific_version ${GREEN}..."
mkdir -p opencodetmp && cd opencodetmp
curl -# -L $url | tar xz
mv opencode $INSTALL_DIR
curl -# -L -o "$filename" "$url"
unzip -q "$filename"
mv opencode "$INSTALL_DIR"
cd .. && rm -rf opencodetmp
}
@@ -101,7 +107,9 @@ add_to_path() {
local config_file=$1
local command=$2
if [[ -w $config_file ]]; then
if grep -Fxq "$command" "$config_file"; then
print_message info "Command already exists in $config_file, skipping write."
elif [[ -w $config_file ]]; then
echo -e "\n# opencode" >> "$config_file"
echo "$command" >> "$config_file"
print_message info "Successfully added ${ORANGE}opencode ${GREEN}to \$PATH in $config_file"
@@ -167,6 +175,7 @@ if [[ ":$PATH:" != *":$INSTALL_DIR:"* ]]; then
add_to_path "$config_file" "export PATH=$INSTALL_DIR:\$PATH"
;;
*)
export PATH=$INSTALL_DIR:$PATH
print_message warning "Manually add the directory to $config_file (or similar):"
print_message info " export PATH=$INSTALL_DIR:\$PATH"
;;

View File

@@ -49,7 +49,7 @@ else
done
if [ -z "$resolved" ]; then
printf "It seems that your package manager failed to install the right version of the SST CLI for your platform. You can try manually installing the \"%s\" package\n" "$name" >&2
printf "It seems that your package manager failed to install the right version of the OpenCode CLI for your platform. You can try manually installing the \"%s\" package\n" "$name" >&2
exit 1
fi
fi

View File

@@ -0,0 +1,152 @@
{
"type": "object",
"properties": {
"$schema": {
"type": "string"
},
"provider": {
"type": "object",
"additionalProperties": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"env": {
"type": "array",
"items": {
"type": "string"
}
},
"id": {
"type": "string"
},
"models": {
"type": "object",
"additionalProperties": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"attachment": {
"type": "boolean"
},
"reasoning": {
"type": "boolean"
},
"temperature": {
"type": "boolean"
},
"cost": {
"type": "object",
"properties": {
"input": {
"type": "number"
},
"output": {
"type": "number"
},
"inputCached": {
"type": "number"
},
"outputCached": {
"type": "number"
}
},
"required": [
"input",
"output",
"inputCached",
"outputCached"
],
"additionalProperties": false
},
"limit": {
"type": "object",
"properties": {
"context": {
"type": "number"
},
"output": {
"type": "number"
}
},
"required": [
"context",
"output"
],
"additionalProperties": false
},
"id": {
"type": "string"
}
},
"additionalProperties": false
}
},
"options": {
"type": "object",
"additionalProperties": {}
}
},
"required": [
"models"
],
"additionalProperties": false
}
},
"mcp": {
"type": "object",
"additionalProperties": {
"anyOf": [
{
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "local"
},
"command": {
"type": "array",
"items": {
"type": "string"
}
},
"environment": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"required": [
"type",
"command"
],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "remote"
},
"url": {
"type": "string"
}
},
"required": [
"type",
"url"
],
"additionalProperties": false
}
]
}
}
},
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}

View File

@@ -5,7 +5,8 @@
"type": "module",
"private": true,
"scripts": {
"typecheck": "tsc --noEmit"
"typecheck": "tsc --noEmit",
"dev": "bun run ./src/index.ts"
},
"exports": {
"./*": [
@@ -18,7 +19,8 @@
"@types/bun": "latest",
"@types/turndown": "5.0.5",
"@types/yargs": "17.0.33",
"typescript": "catalog:"
"typescript": "catalog:",
"zod-to-json-schema": "3.24.5"
},
"dependencies": {
"@clack/prompts": "0.11.0",

View File

@@ -1,23 +0,0 @@
# Maintainer: dax
# Maintainer: adam
pkgname='opencode-bin'
pkgver={{VERSION}}
pkgrel=1
pkgdesc='The AI coding agent built for the terminal.'
url='https://github.com/sst/opencode'
arch=('aarch64' 'x86_64')
license=('MIT')
provides=('opencode')
conflicts=('opencode')
depends=('fzf' 'ripgrep')
source_aarch64=("${pkgname}_${pkgver}_aarch64.zip::{{ARM64_URL}}")
sha256sums_aarch64=('{{ARM64_SHA}}')
source_x86_64=("${pkgname}_${pkgver}_x86_64.zip::{{X64_URL}}")
sha256sums_x86_64=('{{X64_SHA}}')
package() {
install -Dm755 ./opencode "${pkgdir}/usr/bin/opencode"
}

View File

@@ -58,7 +58,7 @@ for (const [os, arch] of targets) {
),
)
if (!dry)
await $`cd dist/${name} && npm publish --access public --tag ${npmTag}`
await $`cd dist/${name} && bun publish --access public --tag ${npmTag}`
optionalDependencies[name] = version
}
@@ -83,7 +83,7 @@ await Bun.file(`./dist/${pkg.name}/package.json`).write(
),
)
if (!dry)
await $`cd ./dist/${pkg.name} && npm publish --access public --tag ${npmTag}`
await $`cd ./dist/${pkg.name} && bun publish --access public --tag ${npmTag}`
if (!snapshot) {
// Github Release
@@ -110,7 +110,8 @@ if (!snapshot) {
return (
!lower.includes("chore:") &&
!lower.includes("ci:") &&
!lower.includes("docs:")
!lower.includes("docs:") &&
!lower.includes("doc:")
)
})
.join("\n")
@@ -118,44 +119,117 @@ if (!snapshot) {
if (!dry)
await $`gh release create v${version} --title "v${version}" --notes ${notes} ./dist/*.zip`
// Calculate SHA values
const arm64Sha =
await $`sha256sum ./dist/opencode-linux-arm64.zip | cut -d' ' -f1`
.text()
.then((x) => x.trim())
const x64Sha =
await $`sha256sum ./dist/opencode-linux-x64.zip | cut -d' ' -f1`
.text()
.then((x) => x.trim())
const macX64Sha =
await $`sha256sum ./dist/opencode-darwin-x64.zip | cut -d' ' -f1`
.text()
.then((x) => x.trim())
const macArm64Sha =
await $`sha256sum ./dist/opencode-darwin-arm64.zip | cut -d' ' -f1`
.text()
.then((x) => x.trim())
// AUR package
const pkgbuildTemplate = await Bun.file("./script/PKGBUILD.template").text()
const pkgbuild = pkgbuildTemplate
.replace("{{VERSION}}", version.split("-")[0])
.replace(
"{{ARM64_URL}}",
`https://github.com/sst/opencode/releases/download/v${version}/opencode-linux-arm64.zip`,
)
.replace(
"{{ARM64_SHA}}",
await $`sha256sum ./dist/opencode-linux-arm64.zip | cut -d' ' -f1`
.text()
.then((x) => x.trim()),
)
.replace(
"{{X64_URL}}",
`https://github.com/sst/opencode/releases/download/v${version}/opencode-linux-x64.zip`,
)
.replace(
"{{X64_SHA}}",
await $`sha256sum ./dist/opencode-linux-x64.zip | cut -d' ' -f1`
.text()
.then((x) => x.trim()),
)
const pkgbuild = [
"# Maintainer: dax",
"# Maintainer: adam",
"",
"pkgname='opencode-bin'",
`pkgver=${version.split("-")[0]}`,
"options=('!debug' '!strip')",
"pkgrel=1",
"pkgdesc='The AI coding agent built for the terminal.'",
"url='https://github.com/sst/opencode'",
"arch=('aarch64' 'x86_64')",
"license=('MIT')",
"provides=('opencode')",
"conflicts=('opencode')",
"depends=('fzf' 'ripgrep')",
"",
`source_aarch64=("\${pkgname}_\${pkgver}_aarch64.zip::https://github.com/sst/opencode/releases/download/v${version}/opencode-linux-arm64.zip")`,
`sha256sums_aarch64=('${arm64Sha}')`,
"",
`source_x86_64=("\${pkgname}_\${pkgver}_x86_64.zip::https://github.com/sst/opencode/releases/download/v${version}/opencode-linux-x64.zip")`,
`sha256sums_x86_64=('${x64Sha}')`,
"",
"package() {",
' install -Dm755 ./opencode "${pkgdir}/usr/bin/opencode"',
"}",
"",
].join("\n")
await $`rm -rf ./dist/aur-opencode-bin`
const gitEnv: Record<string, string> = process.env["AUR_KEY"]
? { GIT_SSH_COMMAND: `ssh -i ${process.env["AUR_KEY"]}` }
: {}
await $`git clone ssh://aur@aur.archlinux.org/opencode-bin.git ./dist/aur-opencode-bin`.env(
gitEnv,
)
await $`git clone ssh://aur@aur.archlinux.org/opencode-bin.git ./dist/aur-opencode-bin`
await Bun.file("./dist/aur-opencode-bin/PKGBUILD").write(pkgbuild)
await $`cd ./dist/aur-opencode-bin && makepkg --printsrcinfo > .SRCINFO`
await $`cd ./dist/aur-opencode-bin && git add PKGBUILD .SRCINFO`.env(gitEnv)
await $`cd ./dist/aur-opencode-bin && git commit -m "Update to v${version}"`.env(
gitEnv,
)
if (!dry) await $`cd ./dist/aur-opencode-bin && git push`.env(gitEnv)
await $`cd ./dist/aur-opencode-bin && git add PKGBUILD .SRCINFO`
await $`cd ./dist/aur-opencode-bin && git commit -m "Update to v${version}"`
if (!dry) await $`cd ./dist/aur-opencode-bin && git push`
// Homebrew formula
const homebrewFormula = [
"# typed: false",
"# frozen_string_literal: true",
"",
"# This file was generated by GoReleaser. DO NOT EDIT.",
"class Opencode < Formula",
` desc "The AI coding agent built for the terminal."`,
` homepage "https://github.com/sst/opencode"`,
` version "${version.split("-")[0]}"`,
"",
" on_macos do",
" if Hardware::CPU.intel?",
` url "https://github.com/sst/opencode/releases/download/v${version}/opencode-darwin-x64.zip"`,
` sha256 "${macX64Sha}"`,
"",
" def install",
' bin.install "opencode"',
" end",
" end",
" if Hardware::CPU.arm?",
` url "https://github.com/sst/opencode/releases/download/v${version}/opencode-darwin-arm64.zip"`,
` sha256 "${macArm64Sha}"`,
"",
" def install",
' bin.install "opencode"',
" end",
" end",
" end",
"",
" on_linux do",
" if Hardware::CPU.intel? and Hardware::CPU.is_64_bit?",
` url "https://github.com/sst/opencode/releases/download/v${version}/opencode-linux-x64.zip"`,
` sha256 "${x64Sha}"`,
" def install",
' bin.install "opencode"',
" end",
" end",
" if Hardware::CPU.arm? and Hardware::CPU.is_64_bit?",
` url "https://github.com/sst/opencode/releases/download/v${version}/opencode-linux-arm64.zip"`,
` sha256 "${arm64Sha}"`,
" def install",
' bin.install "opencode"',
" end",
" end",
" end",
"end",
"",
"",
].join("\n")
await $`rm -rf ./dist/homebrew-tap`
await $`git clone https://${process.env["GITHUB_TOKEN"]}@github.com/sst/homebrew-tap.git ./dist/homebrew-tap`
await Bun.file("./dist/homebrew-tap/opencode.rb").write(homebrewFormula)
await $`cd ./dist/homebrew-tap && git add opencode.rb`
await $`cd ./dist/homebrew-tap && git commit -m "Update to v${version}"`
if (!dry) await $`cd ./dist/homebrew-tap && git push`
}

View File

@@ -0,0 +1,8 @@
#!/usr/bin/env bun
import "zod-openapi/extend"
import { Config } from "../src/config/config"
import { zodToJsonSchema } from "zod-to-json-schema"
const result = zodToJsonSchema(Config.Info)
await Bun.write("config.schema.json", JSON.stringify(result, null, 2))

View File

@@ -1,5 +1,9 @@
import path from "path"
import { z } from "zod"
import { Global } from "../global"
import { Log } from "../util/log"
import path from "path"
import { NamedError } from "../util/error"
export namespace BunProc {
const log = Log.create({ service: "bun" })
@@ -13,6 +17,8 @@ export namespace BunProc {
})
const result = Bun.spawn([which(), ...cmd], {
...options,
stdout: "pipe",
stderr: "pipe",
env: {
...process.env,
...options?.env,
@@ -21,15 +27,32 @@ export namespace BunProc {
})
const code = await result.exited
if (code !== 0) {
console.error(result.stderr?.toString("utf8") ?? "")
throw new Error(`Command failed with exit code ${result.exitCode}`)
}
return result
}
export function which() {
return process.argv0 !== "bun"
? path.resolve(process.cwd(), process.argv0)
: "bun"
return process.execPath
}
export const InstallFailedError = NamedError.create(
"BunInstallFailedError",
z.object({
pkg: z.string(),
version: z.string(),
}),
)
export async function install(pkg: string, version = "latest") {
const dir = path.join(Global.Path.cache, `node_modules`, pkg)
if (!(await Bun.file(path.join(dir, "package.json")).exists())) {
log.info("installing", { pkg })
await BunProc.run(["add", `${pkg}@${version}`], {
cwd: Global.Path.cache,
}).catch(() => {
throw new InstallFailedError({ pkg, version })
})
}
return dir
}
}

View File

@@ -5,6 +5,7 @@ import * as prompts from "@clack/prompts"
import open from "open"
import { UI } from "../ui"
import { ModelsDev } from "../../provider/models"
import { map, pipe, sort, sortBy, values } from "remeda"
export const AuthCommand = cmd({
command: "auth",
@@ -43,26 +44,53 @@ export const AuthLoginCommand = cmd({
async handler() {
UI.empty()
prompts.intro("Add credential")
const provider = await prompts.select({
const providers = await ModelsDev.get()
const priority: Record<string, number> = {
anthropic: 0,
openai: 1,
google: 2,
}
let provider = await prompts.select({
message: "Select provider",
maxItems: 2,
maxItems: 8,
options: [
...pipe(
providers,
values(),
sortBy(
(x) => priority[x.id] ?? 99,
(x) => x.name ?? x.id,
),
map((x) => ({
label: x.name,
value: x.id,
hint: priority[x.id] === 0 ? "recommended" : undefined,
})),
),
{
label: "Anthropic",
value: "anthropic",
},
{
label: "OpenAI",
value: "openai",
},
{
label: "Google",
value: "google",
value: "other",
label: "Other",
},
],
})
if (prompts.isCancel(provider)) throw new UI.CancelledError()
if (provider === "other") {
provider = await prompts.text({
message: "Enter provider - must match @ai-sdk/<provider>",
})
if (prompts.isCancel(provider)) throw new UI.CancelledError()
}
if (provider === "amazon-bedrock") {
prompts.log.info(
"Amazon bedrock can be configured with standard AWS environment variables like AWS_PROFILE or AWS_ACCESS_KEY_ID",
)
prompts.outro("Done")
return
}
if (provider === "anthropic") {
const method = await prompts.select({
message: "Login method",

View File

@@ -2,6 +2,7 @@ import { Server } from "../../server/server"
import fs from "fs/promises"
import path from "path"
import type { CommandModule } from "yargs"
import { Config } from "../../config/config"
export const GenerateCommand = {
command: "generate",

View File

@@ -83,6 +83,7 @@ export const RunCommand = {
}
Bus.subscribe(Message.Event.PartUpdated, async (evt) => {
if (evt.properties.sessionID !== session.id) return
const part = evt.properties.part
const message = await Session.getMessage(
evt.properties.sessionID,

View File

@@ -1,4 +1,5 @@
import { App } from "../../app/app"
import { LSP } from "../../lsp"
import { VERSION } from "../version"
import { cmd } from "./cmd"
@@ -6,9 +7,10 @@ export const ScrapCommand = cmd({
command: "scrap <file>",
builder: (yargs) =>
yargs.positional("file", { type: "string", demandOption: true }),
async handler() {
async handler(args) {
await App.provide({ cwd: process.cwd(), version: VERSION }, async (app) => {
Bun.resolveSync("typescript/lib/tsserver.js", app.path.cwd)
await LSP.touchFile(args.file, true)
console.log(await LSP.diagnostics())
})
},
})

View File

@@ -2,6 +2,7 @@ import { Log } from "../util/log"
import { z } from "zod"
import { App } from "../app/app"
import { Filesystem } from "../util/filesystem"
import { ModelsDev } from "../provider/models"
export namespace Config {
const log = Log.create({ service: "config" })
@@ -49,11 +50,14 @@ export namespace Config {
export const Info = z
.object({
provider: z.record(z.string(), z.record(z.string(), z.any())).optional(),
tool: z
.object({
provider: z.record(z.string(), z.string().array()).optional(),
})
$schema: z.string().optional(),
provider: z
.record(
ModelsDev.Provider.partial().extend({
models: z.record(ModelsDev.Model.partial()),
options: z.record(z.any()).optional(),
}),
)
.optional(),
mcp: z.record(z.string(), Mcp).optional(),
})

View File

@@ -1,4 +1,3 @@
import { App } from "../app/app"
import path from "path"
import { Global } from "../global"
import fs from "fs/promises"
@@ -6,6 +5,7 @@ import { z } from "zod"
import { NamedError } from "../util/error"
import { lazy } from "../util/lazy"
import { Log } from "../util/log"
import { $ } from "bun"
export namespace Fzf {
const log = Log.create({ service: "fzf" })
@@ -117,18 +117,18 @@ export namespace Fzf {
}
export async function search(cwd: string, query: string) {
const process = Bun.spawn({
cwd,
stdin: "inherit",
stdout: "pipe",
stderr: "pipe",
cmd: [await filepath(), "--filter", query],
})
await process.exited
const stdout = await Bun.readableStreamToText(process.stdout)
return stdout
const results = await $`${await filepath()} --filter ${query}`
.quiet()
.throws(false)
.cwd(cwd)
.text()
const split = results
.trim()
.split("\n")
.filter((line) => line.length > 0)
log.info("results", {
count: split.length,
})
return split
}
}

View File

@@ -0,0 +1,8 @@
export namespace Flag {
export const OPENCODE_AUTO_SHARE = truthy("OPENCODE_AUTO_SHARE")
function truthy(key: string) {
const value = process.env[key]?.toLowerCase()
return value === "true" || value === "1"
}
}

View File

@@ -35,12 +35,19 @@ const cli = yargs(hideBin(process.argv))
})
.usage("\n" + UI.logo())
.command({
command: "$0",
command: "$0 [project]",
describe: "Start OpenCode TUI",
builder: (yargs) =>
yargs.positional("project", {
type: "string",
describe: "path to start opencode in",
}),
handler: async (args) => {
while (true) {
const cwd = args.project ? path.resolve(args.project) : process.cwd()
process.chdir(cwd)
const result = await App.provide(
{ cwd: process.cwd(), version: VERSION },
{ cwd, version: VERSION },
async () => {
const providers = await Provider.list()
if (Object.keys(providers).length === 0) {
@@ -65,7 +72,7 @@ const cli = yargs(hideBin(process.argv))
cmd = [binary]
}
const proc = Bun.spawn({
cmd,
cmd: [...cmd, ...process.argv.slice(2)],
cwd,
stdout: "inherit",
stderr: "inherit",

View File

@@ -1,17 +1,54 @@
import { Global } from "../global"
import { Log } from "../util/log"
import path from "path"
import { z } from "zod"
export namespace ModelsDev {
const log = Log.create({ service: "models.dev" })
const filepath = path.join(Global.Path.cache, "models.json")
export const Model = z
.object({
name: z.string(),
attachment: z.boolean(),
reasoning: z.boolean(),
temperature: z.boolean(),
cost: z.object({
input: z.number(),
output: z.number(),
inputCached: z.number(),
outputCached: z.number(),
}),
limit: z.object({
context: z.number(),
output: z.number(),
}),
id: z.string(),
})
.openapi({
ref: "Model.Info",
})
export type Model = z.infer<typeof Model>
export const Provider = z
.object({
name: z.string(),
env: z.array(z.string()),
id: z.string(),
models: z.record(Model),
})
.openapi({
ref: "Provider.Info",
})
export type Provider = z.infer<typeof Provider>
export async function get() {
const file = Bun.file(filepath)
const result = await file.json().catch(() => {})
if (result) {
refresh()
return result
return result as Record<string, Provider>
}
await refresh()
return get()

View File

@@ -4,8 +4,6 @@ import { Config } from "../config/config"
import { mergeDeep, sortBy } from "remeda"
import { NoSuchModelError, type LanguageModel, type Provider as SDK } from "ai"
import { Log } from "../util/log"
import path from "path"
import { Global } from "../global"
import { BunProc } from "../bun"
import { BashTool } from "../tool/bash"
import { EditTool } from "../tool/edit"
@@ -24,110 +22,64 @@ import { AuthAnthropic } from "../auth/anthropic"
import { ModelsDev } from "./models"
import { NamedError } from "../util/error"
import { Auth } from "../auth"
import { TaskTool } from "../tool/task"
export namespace Provider {
const log = Log.create({ service: "provider" })
export const Model = z
.object({
id: z.string(),
name: z.string().optional(),
attachment: z.boolean(),
reasoning: z.boolean().optional(),
cost: z.object({
input: z.number(),
inputCached: z.number(),
output: z.number(),
outputCached: z.number(),
}),
limit: z.object({
context: z.number(),
output: z.number(),
}),
})
.openapi({
ref: "Provider.Model",
})
export type Model = z.output<typeof Model>
type CustomLoader = (
provider: ModelsDev.Provider,
) => Promise<Record<string, any> | false>
export const Info = z
.object({
id: z.string(),
name: z.string(),
models: z.record(z.string(), Model),
})
.openapi({
ref: "Provider.Info",
})
export type Info = z.output<typeof Info>
type Source = "env" | "config" | "custom" | "api"
type Autodetector = (provider: Info) => Promise<
| {
source: Source
options: Record<string, any>
}
| false
>
function env(...keys: string[]) {
const result: Autodetector = async () => {
for (const key of keys) {
if (process.env[key])
return {
source: "env",
options: {},
}
}
return false
}
return result
}
type Source = "oauth" | "env" | "config" | "api"
const AUTODETECT: Record<string, Autodetector> = {
const CUSTOM_LOADERS: Record<string, CustomLoader> = {
async anthropic(provider) {
const access = await AuthAnthropic.access()
if (access) {
// claude sub doesn't have usage cost
for (const model of Object.values(provider.models)) {
model.cost = {
input: 0,
inputCached: 0,
output: 0,
outputCached: 0,
}
}
return {
source: "oauth",
options: {
apiKey: "",
headers: {
authorization: `Bearer ${access}`,
"anthropic-beta": "oauth-2025-04-20",
},
},
if (!access) return false
for (const model of Object.values(provider.models)) {
model.cost = {
input: 0,
inputCached: 0,
output: 0,
outputCached: 0,
}
}
return env("ANTHROPIC_API_KEY")(provider)
return {
apiKey: "",
headers: {
authorization: `Bearer ${access}`,
"anthropic-beta": "oauth-2025-04-20",
},
}
},
"amazon-bedrock": async () => {
if (!process.env["AWS_PROFILE"]) return false
const { fromNodeProviderChain } = await import(
await BunProc.install("@aws-sdk/credential-providers")
)
return {
region: process.env["AWS_REGION"] ?? "us-east-1",
credentialProvider: fromNodeProviderChain(),
}
},
google: env("GOOGLE_GENERATIVE_AI_API_KEY"),
openai: env("OPENAI_API_KEY"),
}
const state = App.state("provider", async () => {
const config = await Config.get()
const database: Record<string, Provider.Info> = await ModelsDev.get()
const database = await ModelsDev.get()
const providers: {
[providerID: string]: {
source: Source
info: Provider.Info
info: ModelsDev.Provider
options: Record<string, any>
}
} = {}
const models = new Map<string, { info: Model; language: LanguageModel }>()
const models = new Map<
string,
{ info: ModelsDev.Model; language: LanguageModel }
>()
const sdk = new Map<string, SDK>()
log.info("loading")
@@ -141,11 +93,7 @@ export namespace Provider {
if (!provider) {
providers[id] = {
source,
info: database[id] ?? {
id,
name: id,
models: [],
},
info: database[id],
options,
}
return
@@ -154,22 +102,68 @@ export namespace Provider {
provider.source = source
}
for (const [providerID, fn] of Object.entries(AUTODETECT)) {
const provider = database[providerID]
if (!provider) continue
const result = await fn(provider)
if (!result) continue
mergeProvider(providerID, result.options, result.source)
for (const [providerID, provider] of Object.entries(
config.provider ?? {},
)) {
const existing = database[providerID]
const parsed: ModelsDev.Provider = {
id: providerID,
name: provider.name ?? existing?.name ?? providerID,
env: provider.env ?? existing?.env ?? [],
models: existing?.models ?? {},
}
for (const [modelID, model] of Object.entries(provider.models ?? {})) {
const existing = parsed.models[modelID]
const parsedModel: ModelsDev.Model = {
id: modelID,
name: model.name ?? existing?.name ?? modelID,
attachment: model.attachment ?? existing?.attachment ?? false,
reasoning: model.reasoning ?? existing?.reasoning ?? false,
temperature: model.temperature ?? existing?.temperature ?? false,
cost: model.cost ??
existing?.cost ?? {
input: 0,
output: 0,
inputCached: 0,
outputCached: 0,
},
limit: model.limit ??
existing?.limit ?? {
context: 0,
output: 0,
},
}
parsed.models[modelID] = parsedModel
}
database[providerID] = parsed
}
for (const [providerID, info] of Object.entries(await Auth.all())) {
if (info.type === "api") {
mergeProvider(providerID, { apiKey: info.key }, "api")
// load env
for (const [providerID, provider] of Object.entries(database)) {
if (provider.env.some((item) => process.env[item])) {
mergeProvider(providerID, {}, "env")
}
}
for (const [providerID, options] of Object.entries(config.provider ?? {})) {
mergeProvider(providerID, options, "config")
// load apikeys
for (const [providerID, provider] of Object.entries(await Auth.all())) {
if (provider.type === "api") {
mergeProvider(providerID, { apiKey: provider.key }, "api")
}
}
// load custom
for (const [providerID, fn] of Object.entries(CUSTOM_LOADERS)) {
const result = await fn(database[providerID])
if (result) mergeProvider(providerID, result, "custom")
}
// load config
for (const [providerID, provider] of Object.entries(
config.provider ?? {},
)) {
mergeProvider(providerID, provider.options ?? {}, "config")
}
for (const providerID of Object.keys(providers)) {
@@ -192,21 +186,9 @@ export namespace Provider {
const s = await state()
const existing = s.sdk.get(providerID)
if (existing) return existing
const dir = path.join(
Global.Path.cache,
`node_modules`,
`@ai-sdk`,
providerID,
const mod = await import(
await BunProc.install(`@ai-sdk/${providerID}`, "alpha")
)
if (!(await Bun.file(path.join(dir, "package.json")).exists())) {
log.info("installing", {
providerID,
})
await BunProc.run(["add", `@ai-sdk/${providerID}@alpha`], {
cwd: Global.Path.cache,
})
}
const mod = await import(path.join(dir))
const fn = mod[Object.keys(mod).find((key) => key.startsWith("create"))!]
const loaded = fn(s.providers[providerID]?.options)
s.sdk.set(providerID, loaded)
@@ -260,7 +242,7 @@ export namespace Provider {
}
const priority = ["gemini-2.5-pro-preview", "codex-mini", "claude-sonnet-4"]
export function sort(models: Model[]) {
export function sort(models: ModelsDev.Model[]) {
return sortBy(
models,
[
@@ -298,6 +280,7 @@ export namespace Provider {
// MultiEditTool,
WriteTool,
TodoWriteTool,
TaskTool,
TodoReadTool,
]
const TOOL_MAPPING: Record<string, Tool.Info[]> = {
@@ -306,11 +289,13 @@ export namespace Provider {
google: TOOLS,
}
export async function tools(providerID: string) {
/*
const cfg = await Config.get()
if (cfg.tool?.provider?.[providerID])
return cfg.tool.provider[providerID].map(
(id) => TOOLS.find((t) => t.id === id)!,
)
*/
return TOOL_MAPPING[providerID] ?? TOOLS
}

View File

@@ -13,6 +13,7 @@ import { Global } from "../global"
import { mapValues } from "remeda"
import { NamedError } from "../util/error"
import { Fzf } from "../external/fzf"
import { ModelsDev } from "../provider/models"
const ERRORS = {
400: {
@@ -55,12 +56,16 @@ export namespace Server {
},
)
})
.use((c, next) => {
.use(async (c, next) => {
log.info("request", {
method: c.req.method,
path: c.req.path,
})
return next()
const start = Date.now()
await next()
log.info("response", {
duration: Date.now() - start,
})
})
.get(
"/openapi",
@@ -406,7 +411,7 @@ export namespace Server {
"application/json": {
schema: resolver(
z.object({
providers: Provider.Info.array(),
providers: ModelsDev.Provider.array(),
default: z.record(z.string(), z.string()),
}),
),
@@ -421,7 +426,7 @@ export namespace Server {
)
return c.json({
providers: Object.values(providers),
defaults: mapValues(
default: mapValues(
providers,
(item) => Provider.sort(Object.values(item.models))[0].id,
),

View File

@@ -12,24 +12,22 @@ import {
tool,
type Tool as AITool,
type LanguageModelUsage,
type UIMessage,
} from "ai"
import { z, ZodSchema } from "zod"
import { Decimal } from "decimal.js"
import PROMPT_ANTHROPIC from "./prompt/anthropic.txt"
import PROMPT_ANTHROPIC_SPOOF from "./prompt/anthropic_spoof.txt"
import PROMPT_TITLE from "./prompt/title.txt"
import PROMPT_SUMMARIZE from "./prompt/summarize.txt"
import PROMPT_INITIALIZE from "../session/prompt/initialize.txt"
import { Share } from "../share/share"
import { Message } from "./message"
import { Bus } from "../bus"
import { Provider } from "../provider/provider"
import { SessionContext } from "./context"
import { ListTool } from "../tool/ls"
import { MCP } from "../mcp"
import { NamedError } from "../util/error"
import type { Tool } from "../tool/tool"
import { SystemPrompt } from "./system"
import { Flag } from "../flag/flag"
export namespace Session {
const log = Log.create({ service: "session" })
@@ -37,6 +35,7 @@ export namespace Session {
export const Info = z
.object({
id: Identifier.schema("session"),
parentID: Identifier.schema("session").optional(),
share: z
.object({
secret: z.string(),
@@ -79,10 +78,13 @@ export namespace Session {
}
})
export async function create() {
export async function create(parentID?: string) {
const result: Info = {
id: Identifier.descending("session"),
title: "New Session - " + new Date().toISOString(),
parentID,
title:
(parentID ? "Child session - " : "New Session - ") +
new Date().toISOString(),
time: {
created: Date.now(),
updated: Date.now(),
@@ -91,11 +93,12 @@ export namespace Session {
log.info("created", result)
state().sessions.set(result.id, result)
await Storage.writeJSON("session/info/" + result.id, result)
share(result.id).then((share) => {
update(result.id, (draft) => {
draft.share = share
if (!result.parentID && Flag.OPENCODE_AUTO_SHARE)
share(result.id).then((share) => {
update(result.id, (draft) => {
draft.share = share
})
})
})
Bus.publish(Event.Updated, {
info: result,
})
@@ -186,12 +189,16 @@ export namespace Session {
providerID: string
modelID: string
parts: Message.Part[]
system?: string[]
tools?: Tool.Info[]
}) {
const l = log.clone().tag("session", input.sessionID)
l.info("chatting")
const model = await Provider.getModel(input.providerID, input.modelID)
let msgs = await messages(input.sessionID)
const previous = msgs.at(-1)
// auto summarize if too long
if (previous?.metadata.assistant) {
const tokens =
previous.metadata.assistant.tokens.input +
@@ -214,95 +221,26 @@ export namespace Session {
const lastSummary = msgs.findLast(
(msg) => msg.metadata.assistant?.summary === true,
)
if (lastSummary)
msgs = msgs.filter(
(msg) => msg.role === "system" || msg.id >= lastSummary.id,
)
if (lastSummary) msgs = msgs.filter((msg) => msg.id >= lastSummary.id)
if (msgs.length === 0) {
const app = App.info()
if (input.providerID === "anthropic") {
const claude: Message.Info = {
id: Identifier.ascending("message"),
role: "system",
parts: [
{
type: "text",
text: PROMPT_ANTHROPIC_SPOOF.trim(),
},
],
metadata: {
sessionID: input.sessionID,
time: {
created: Date.now(),
},
tool: {},
},
}
await updateMessage(claude)
msgs.push(claude)
}
const system: Message.Info = {
id: Identifier.ascending("message"),
role: "system",
parts: [
{
type: "text",
text: PROMPT_ANTHROPIC,
},
{
type: "text",
text: [
`Here is some useful information about the environment you are running in:`,
`<env>`,
`Working directory: ${app.path.cwd}`,
`Is directory a git repo: ${app.git ? "yes" : "no"}`,
`Platform: ${process.platform}`,
`Today's date: ${new Date().toISOString()}`,
`</env>`,
`<project>`,
`${app.git ? await ListTool.execute({ path: app.path.cwd, ignore: [] }, { sessionID: input.sessionID, abort: abort.signal }).then((x) => x.output) : ""}`,
`</project>`,
].join("\n"),
},
],
metadata: {
sessionID: input.sessionID,
time: {
created: Date.now(),
},
tool: {},
},
}
const context = await SessionContext.find()
if (context) {
system.parts.push({
type: "text",
text: context,
})
}
msgs.push(system)
const app = App.info()
const session = await get(input.sessionID)
if (msgs.length === 0 && !session.parentID) {
generateText({
maxOutputTokens: 20,
messages: convertToModelMessages([
{
role: "system",
parts: [
{
type: "text",
text: PROMPT_ANTHROPIC_SPOOF.trim(),
},
],
},
{
role: "system",
parts: [
{
type: "text",
text: PROMPT_TITLE,
},
],
},
...SystemPrompt.title(input.providerID).map(
(x): UIMessage => ({
id: Identifier.ascending("message"),
role: "system",
parts: [
{
type: "text",
text: x,
},
],
}),
),
{
role: "user",
parts: input.parts,
@@ -317,7 +255,6 @@ export namespace Session {
})
})
.catch(() => {})
await updateMessage(system)
}
const msg: Message.Info = {
role: "user",
@@ -334,12 +271,21 @@ export namespace Session {
await updateMessage(msg)
msgs.push(msg)
const system = input.system ?? SystemPrompt.provider(input.providerID)
system.push(...(await SystemPrompt.environment(input.sessionID)))
system.push(...(await SystemPrompt.custom()))
const next: Message.Info = {
id: Identifier.ascending("message"),
role: "assistant",
parts: [],
metadata: {
assistant: {
system,
path: {
cwd: app.path.cwd,
root: app.path.root,
},
cost: 0,
tokens: {
input: 0,
@@ -358,6 +304,7 @@ export namespace Session {
}
await updateMessage(next)
const tools: Record<string, AITool> = {}
for (const item of await Provider.tools(input.providerID)) {
tools[item.id.replaceAll(".", "_")] = tool({
id: item.id as any,
@@ -369,6 +316,7 @@ export namespace Session {
const result = await item.execute(args, {
sessionID: input.sessionID,
abort: abort.signal,
messageID: next.id,
})
next.metadata!.tool![opts.toolCallId] = {
...result.metadata,
@@ -395,6 +343,7 @@ export namespace Session {
},
})
}
for (const [key, item] of Object.entries(await MCP.tools())) {
const execute = item.execute
if (!execute) continue
@@ -576,7 +525,21 @@ export namespace Session {
toolCallStreaming: true,
abortSignal: abort.signal,
stopWhen: stepCountIs(1000),
messages: convertToModelMessages(msgs),
messages: convertToModelMessages([
...system.map(
(x): UIMessage => ({
id: Identifier.ascending("message"),
role: "system",
parts: [
{
type: "text",
text: x,
},
],
}),
),
...msgs,
]),
temperature: model.info.id === "codex-mini-latest" ? undefined : 0,
tools: {
...(await MCP.tools()),
@@ -618,10 +581,11 @@ export namespace Session {
const lastSummary = msgs.findLast(
(msg) => msg.metadata.assistant?.summary === true,
)?.id
const filtered = msgs.filter(
(msg) => msg.role !== "system" && (!lastSummary || msg.id >= lastSummary),
)
const filtered = msgs.filter((msg) => !lastSummary || msg.id >= lastSummary)
const model = await Provider.getModel(input.providerID, input.modelID)
const app = App.info()
const system = SystemPrompt.summarize(input.providerID)
const next: Message.Info = {
id: Identifier.ascending("message"),
role: "assistant",
@@ -630,6 +594,11 @@ export namespace Session {
tool: {},
sessionID: input.sessionID,
assistant: {
system,
path: {
cwd: app.path.cwd,
root: app.path.root,
},
summary: true,
cost: 0,
modelID: input.modelID,
@@ -650,15 +619,18 @@ export namespace Session {
abortSignal: abort.signal,
model: model.language,
messages: convertToModelMessages([
{
role: "system",
parts: [
{
type: "text",
text: PROMPT_SUMMARIZE,
},
],
},
...system.map(
(x): UIMessage => ({
id: Identifier.ascending("message"),
role: "system",
parts: [
{
type: "text",
text: x,
},
],
}),
),
...filtered,
{
role: "user",

View File

@@ -133,7 +133,7 @@ export namespace Message {
export const Info = z
.object({
id: z.string(),
role: z.enum(["system", "user", "assistant"]),
role: z.enum(["user", "assistant"]),
parts: z.array(Part),
metadata: z.object({
time: z.object({
@@ -161,8 +161,13 @@ export namespace Message {
),
assistant: z
.object({
system: z.string().array(),
modelID: z.string(),
providerID: z.string(),
path: z.object({
cwd: z.string(),
root: z.string(),
}),
cost: z.number(),
summary: z.boolean().optional(),
tokens: z.object({

View File

@@ -1,14 +1,14 @@
You are OpenCode, an interactive CLI tool that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user.
You are opencode, an interactive CLI tool that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user.
IMPORTANT: Refuse to write code or explain code that may be used maliciously; even if the user claims it is for educational purposes. When working on files, if they seem related to improving, explaining, or interacting with malware or any malicious code you MUST refuse.
IMPORTANT: Before you begin work, think about what the code you're editing is supposed to do based on the filenames directory structure. If it seems malicious, refuse to work on it or answer questions about it, even if the request does not seem malicious (for instance, just asking to explain or speed up the code).
IMPORTANT: You must NEVER generate or guess URLs for the user unless you are confident that the URLs are for helping the user with programming. You may use URLs provided by the user in their messages or local files.
If the user asks for help or wants to give feedback inform them of the following:
- /help: Get help with using OpenCode
- /help: Get help with using opencode
- To give feedback, users should report the issue at https://github.com/sst/opencode/issues
When the user directly asks about OpenCode (eg 'can OpenCode do...', 'does OpenCode have...') or asks in second person (eg 'are you able...', 'can you do...'), first use the WebFetch tool to gather information to answer the question from OpenCode docs at https://opencode.ai
When the user directly asks about opencode (eg 'can opencode do...', 'does opencode have...') or asks in second person (eg 'are you able...', 'can you do...'), first use the WebFetch tool to gather information to answer the question from opencode docs at https://opencode.ai
# Tone and style
You should be concise, direct, and to the point. When you run a non-trivial bash command, you should explain what the command does and why you are running it, to make sure the user understands what you are doing (this is especially important when you are running a command that will make changes to the user's system).

View File

@@ -0,0 +1,75 @@
import { App } from "../app/app"
import { ListTool } from "../tool/ls"
import { Filesystem } from "../util/filesystem"
import PROMPT_ANTHROPIC from "./prompt/anthropic.txt"
import PROMPT_ANTHROPIC_SPOOF from "./prompt/anthropic_spoof.txt"
import PROMPT_SUMMARIZE from "./prompt/summarize.txt"
import PROMPT_TITLE from "./prompt/title.txt"
export namespace SystemPrompt {
export function provider(providerID: string) {
const result = []
switch (providerID) {
case "anthropic":
result.push(PROMPT_ANTHROPIC_SPOOF.trim())
result.push(PROMPT_ANTHROPIC)
break
default:
result.push(PROMPT_ANTHROPIC)
break
}
return result
}
export async function environment(sessionID: string) {
const app = App.info()
return [
[
`Here is some useful information about the environment you are running in:`,
`<env>`,
` Working directory: ${app.path.cwd}`,
` Is directory a git repo: ${app.git ? "yes" : "no"}`,
` Platform: ${process.platform}`,
` Today's date: ${new Date().toDateString()}`,
`</env>`,
`<project>`,
` ${app.git ? await ListTool.execute({ path: app.path.cwd, ignore: [] }, { sessionID: sessionID, messageID: "", abort: AbortSignal.any([]) }).then((x) => x.output) : ""}`,
`</project>`,
].join("\n"),
]
}
const CUSTOM_FILES = [
"AGENTS.md",
"CLAUDE.md",
"CONTEXT.md", // deprecated
]
export async function custom() {
const { cwd, root } = App.info().path
const found = []
for (const item of CUSTOM_FILES) {
const matches = await Filesystem.findUp(item, cwd, root)
found.push(...matches.map((x) => Bun.file(x).text()))
}
return Promise.all(found)
}
export function summarize(providerID: string) {
switch (providerID) {
case "anthropic":
return [PROMPT_ANTHROPIC_SPOOF.trim(), PROMPT_SUMMARIZE]
default:
return [PROMPT_SUMMARIZE]
}
}
export function title(providerID: string) {
switch (providerID) {
case "anthropic":
return [PROMPT_ANTHROPIC_SPOOF.trim(), PROMPT_TITLE]
default:
return [PROMPT_TITLE]
}
}
}

View File

@@ -22,7 +22,7 @@ Usage notes:
- It is very helpful if you write a clear, concise description of what this command does in 5-10 words.
- If the output exceeds 30000 characters, output will be truncated before being returned to you.
- VERY IMPORTANT: You MUST avoid using search commands like `find` and `grep`. Instead use Grep, Glob, or Task to search. You MUST avoid read tools like `cat`, `head`, `tail`, and `ls`, and use Read and LS to read files.
- If you _still_ need to run `grep`, STOP. ALWAYS USE ripgrep at `rg` (or /usr/bin/rg) first, which all OpenCode users have pre-installed.
- If you _still_ need to run `grep`, STOP. ALWAYS USE ripgrep at `rg` (or /usr/bin/rg) first, which all opencode users have pre-installed.
- When issuing multiple commands, use the ';' or '&&' operator to separate them. DO NOT use newlines (newlines are ok in quoted strings).
- Try to maintain your current working directory throughout the session by using absolute paths and avoiding usage of `cd`. You may use `cd` if the User explicitly requests it.
<good-example>
@@ -60,9 +60,9 @@ When the user asks you to create a new git commit, follow these steps carefully:
3. You have the capability to call multiple tools in a single response. When multiple independent pieces of information are requested, batch your tool calls together for optimal performance. ALWAYS run the following commands in parallel:
- Add relevant untracked files to the staging area.
- Create the commit with a message ending with:
🤖 Generated with [OpenCode](https://opencode.ai)
🤖 Generated with [opencode](https://opencode.ai)
Co-Authored-By: OpenCode <noreply@opencode.ai>
Co-Authored-By: opencode <noreply@opencode.ai>
- Run git status to make sure the commit succeeded.
4. If the commit fails due to pre-commit hook changes, retry the commit ONCE to include these automated changes. If it fails again, it usually means a pre-commit hook is preventing the commit. If the commit succeeds but you notice that files were modified by the pre-commit hook, you MUST amend your commit to include them.
@@ -81,9 +81,9 @@ Important notes:
git commit -m "$(cat <<'EOF'
Commit message here.
🤖 Generated with [OpenCode](https://opencode.ai)
🤖 Generated with [opencode](https://opencode.ai)
Co-Authored-By: OpenCode <noreply@opencode.ai>
Co-Authored-By: opencode <noreply@opencode.ai>
EOF
)"
</example>
@@ -128,7 +128,7 @@ gh pr create --title "the pr title" --body "$(cat <<'EOF'
## Test plan
[Checklist of TODOs for testing the pull request...]
🤖 Generated with [OpenCode](https://opencode.ai)
🤖 Generated with [opencode](https://opencode.ai)
EOF
)"
</example>

View File

@@ -27,7 +27,7 @@ export const GlobTool = Tool.define({
const glob = new Bun.Glob(params.pattern)
const files = []
let truncated = false
for await (const file of glob.scan({ cwd: search })) {
for await (const file of glob.scan({ cwd: search, dot: true })) {
if (files.length >= limit) {
truncated = true
break

View File

@@ -40,7 +40,7 @@ export const ListTool = Tool.define({
const glob = new Bun.Glob("**/*")
const files = []
for await (const file of glob.scan({ cwd: searchPath })) {
for await (const file of glob.scan({ cwd: searchPath, dot: true })) {
if (file.startsWith(".") || IGNORE_PATTERNS.some((p) => file.includes(p)))
continue
if (params.ignore?.some((pattern) => new Bun.Glob(pattern).match(file)))

View File

@@ -0,0 +1,39 @@
import { Tool } from "./tool"
import DESCRIPTION from "./task.txt"
import { z } from "zod"
import { Session } from "../session"
export const TaskTool = Tool.define({
id: "opencode.task",
description: DESCRIPTION,
parameters: z.object({
description: z
.string()
.describe("A short (3-5 words) description of the task"),
prompt: z.string().describe("The task for the agent to perform"),
}),
async execute(params, ctx) {
const session = await Session.create(ctx.sessionID)
const msg = await Session.getMessage(ctx.sessionID, ctx.messageID)
const metadata = msg.metadata.assistant!
const result = await Session.chat({
sessionID: session.id,
modelID: metadata.modelID,
providerID: metadata.providerID,
parts: [
{
type: "text",
text: params.prompt,
},
],
})
return {
metadata: {
title: params.description,
},
output: result.parts.findLast((x) => x.type === "text")!.text,
}
},
})

View File

@@ -7,6 +7,7 @@ export namespace Tool {
}
export type Context = {
sessionID: string
messageID: string
abort: AbortSignal
}
export interface Info<

View File

@@ -2,7 +2,7 @@ import path from "path"
import fs from "fs/promises"
import { Global } from "../global"
export namespace Log {
export const Default = create()
export const Default = create({ service: "default" })
export interface Options {
print: boolean

View File

@@ -9,7 +9,7 @@ import (
"sync"
"time"
tea "github.com/charmbracelet/bubbletea"
tea "github.com/charmbracelet/bubbletea/v2"
zone "github.com/lrstanley/bubblezone"
"github.com/sst/opencode/internal/app"
"github.com/sst/opencode/internal/pubsub"
@@ -65,6 +65,8 @@ func main() {
zone.NewGlobal()
program := tea.NewProgram(
tui.NewModel(app_),
// tea.WithMouseCellMotion(),
tea.WithKeyboardEnhancements(),
tea.WithAltScreen(),
)
@@ -121,9 +123,6 @@ func main() {
// Cancel subscriptions first
cancelSubs()
// Then shutdown the app
app_.Shutdown()
// Then cancel TUI message handler
tuiCancel()

View File

@@ -4,13 +4,12 @@ go 1.24.0
require (
github.com/BurntSushi/toml v1.5.0
github.com/alecthomas/chroma/v2 v2.15.0
github.com/alecthomas/chroma/v2 v2.18.0
github.com/bmatcuk/doublestar/v4 v4.8.1
github.com/catppuccin/go v0.3.0
github.com/charmbracelet/bubbles v0.21.0
github.com/charmbracelet/bubbletea v1.3.4
github.com/charmbracelet/glamour v0.9.1
github.com/charmbracelet/lipgloss v1.1.0
github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1
github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.3
github.com/charmbracelet/glamour v0.10.0
github.com/charmbracelet/lipgloss/v2 v2.0.0-beta1
github.com/charmbracelet/x/ansi v0.8.0
github.com/lithammer/fuzzysearch v1.1.8
github.com/lrstanley/bubblezone v0.0.0-20250315020633-c249a3fe1231
@@ -28,6 +27,11 @@ require (
dario.cat/mergo v1.0.2 // indirect
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
github.com/atombender/go-jsonschema v0.20.0 // indirect
github.com/charmbracelet/bubbletea v1.3.4 // indirect
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 // indirect
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect
github.com/charmbracelet/x/input v0.3.5-0.20250424101541-abb4d9a9b197 // indirect
github.com/charmbracelet/x/windows v0.2.1 // indirect
github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect
github.com/fsnotify/fsnotify v1.8.0 // indirect
github.com/getkin/kin-openapi v0.127.0 // indirect
@@ -57,11 +61,11 @@ require (
github.com/atotto/clipboard v0.1.4
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
github.com/charmbracelet/colorprofile v0.3.1 // indirect
github.com/charmbracelet/x/cellbuf v0.0.14-0.20250501183327-ad3bc78c6a81 // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/disintegration/imaging v1.6.2
github.com/dlclark/regexp2 v1.11.4 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/gorilla/css v1.0.1 // indirect

View File

@@ -7,8 +7,8 @@ github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6
github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk=
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.15.0 h1:LxXTQHFoYrstG2nnV9y2X5O94sOBzf0CIUpSTbpxvMc=
github.com/alecthomas/chroma/v2 v2.15.0/go.mod h1:gUhVLrPDXPtp/f+L1jo9xepo9gL4eLwRuGAunSZMkio=
github.com/alecthomas/chroma/v2 v2.18.0 h1:6h53Q4hW83SuF+jcsp7CVhLsMozzvQvO8HBbKQW+gn4=
github.com/alecthomas/chroma/v2 v2.18.0/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3aWCNE14TFlepBk=
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/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ=
@@ -26,26 +26,34 @@ github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd3
github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38=
github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY=
github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1 h1:swACzss0FjnyPz1enfX56GKkLiuKg5FlyVmOLIlU2kE=
github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1/go.mod h1:6HamsBKWqEC/FVHuQMHgQL+knPyvHH55HwJDHl/adMw=
github.com/charmbracelet/bubbletea v1.3.4 h1:kCg7B+jSCFPLYRA52SDZjr51kG/fMUEoPoZrkaDHyoI=
github.com/charmbracelet/bubbletea v1.3.4/go.mod h1:dtcUCyCGEX3g9tosuYiut3MXgY/Jsv9nKVdibKKRRXo=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
github.com/charmbracelet/glamour v0.9.1 h1:11dEfiGP8q1BEqvGoIjivuc2rBk+5qEXdPtaQ2WoiCM=
github.com/charmbracelet/glamour v0.9.1/go.mod h1:+SHvIS8qnwhgTpVMiXwn7OfGomSqff1cHBCI8jLOetk=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.3 h1:5A2e3myxXMpCES+kjEWgGsaf9VgZXjZbLi5iMTH7j40=
github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.3/go.mod h1:ZFDg5oPjyRYrPAa3iFrtP1DO8xy+LUQxd9JFHEcuwJY=
github.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL3m+AxCzDz40=
github.com/charmbracelet/colorprofile v0.3.1/go.mod h1:/GkGusxNs8VB/RSOh3fu0TJmQ4ICMMPApIIVn0KszZ0=
github.com/charmbracelet/glamour v0.10.0 h1:MtZvfwsYCx8jEPFJm3rIBFIMZUfUJ765oX8V6kXldcY=
github.com/charmbracelet/glamour v0.10.0/go.mod h1:f+uf+I/ChNmqo087elLnVdCiVgjSKWuXa/l6NU2ndYk=
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE=
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA=
github.com/charmbracelet/lipgloss/v2 v2.0.0-beta1 h1:SOylT6+BQzPHEjn15TIzawBPVD0QmhKXbcb3jY0ZIKU=
github.com/charmbracelet/lipgloss/v2 v2.0.0-beta1/go.mod h1:tRlx/Hu0lo/j9viunCN2H+Ze6JrmdjQlXUQvvArgaOc=
github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
github.com/charmbracelet/x/cellbuf v0.0.14-0.20250501183327-ad3bc78c6a81 h1:iGrflaL5jQW6crML+pZx/ulWAVZQR3CQoRGvFsr2Tyg=
github.com/charmbracelet/x/cellbuf v0.0.14-0.20250501183327-ad3bc78c6a81/go.mod h1:poPFOXFTsJsnLbkV3H2KxAAXT7pdjxxLujLocWjkyzM=
github.com/charmbracelet/x/exp/golden v0.0.0-20250207160936-21c02780d27a h1:FsHEJ52OC4VuTzU8t+n5frMjLvpYWEznSr/u8tnkCYw=
github.com/charmbracelet/x/exp/golden v0.0.0-20250207160936-21c02780d27a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf h1:rLG0Yb6MQSDKdB52aGX55JT1oi0P0Kuaj7wi1bLUpnI=
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf/go.mod h1:B3UgsnsBZS/eX42BlaNiJkD1pPOUa+oF1IYC6Yd2CEU=
github.com/charmbracelet/x/input v0.3.5-0.20250424101541-abb4d9a9b197 h1:fsWj8NF5njyMVzELc7++HsvRDvgz3VcgGAUgWBDWWWM=
github.com/charmbracelet/x/input v0.3.5-0.20250424101541-abb4d9a9b197/go.mod h1:xseGeVftoP9rVI+/8WKYrJFH6ior6iERGvklwwHz5+s=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/charmbracelet/x/windows v0.2.1 h1:3x7vnbpQrjpuq/4L+I4gNsG5htYoCiA5oe9hLjAij5I=
github.com/charmbracelet/x/windows v0.2.1/go.mod h1:ptZp16h40gDYqs5TSawSVW+yiLB13j4kSMA0lSCHL0M=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
@@ -56,8 +64,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dprotaso/go-yit v0.0.0-20191028211022-135eb7262960/go.mod h1:9HQzr9D/0PGwMEbC3d5AB7oi67+h4TsQqItC1GVYG58=
github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 h1:PRxIJD8XjimM5aTknUK9w6DHLDox2r2M3DI4i2pnd3w=
github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936/go.mod h1:ttYvX5qlB+mlV1okblJqcSMtR4c52UKxDiX9GRBS8+Q=

View File

@@ -8,9 +8,9 @@ import (
"log/slog"
tea "github.com/charmbracelet/bubbletea"
tea "github.com/charmbracelet/bubbletea/v2"
"github.com/sst/opencode/internal/commands"
"github.com/sst/opencode/internal/config"
"github.com/sst/opencode/internal/fileutil"
"github.com/sst/opencode/internal/state"
"github.com/sst/opencode/internal/status"
"github.com/sst/opencode/internal/theme"
@@ -23,14 +23,11 @@ type App struct {
Config *config.Config
Client *client.ClientWithResponses
Provider *client.ProviderInfo
Model *client.ProviderModel
Model *client.ModelInfo
Session *client.SessionInfo
Messages []client.MessageInfo
Status status.Service
// UI state
filepickerOpen bool
completionDialogOpen bool
Commands commands.Registry
}
type AppInfo struct {
@@ -61,20 +58,25 @@ func New(ctx context.Context, version string, httpClient *client.ClientWithRespo
}
providers := []client.ProviderInfo{}
var defaultProvider *client.ProviderInfo
var defaultModel *client.ProviderModel
var defaultModel *client.ModelInfo
for i, provider := range providersResponse.JSON200.Providers {
if i == 0 || provider.Id == "anthropic" {
defaultProvider = &providersResponse.JSON200.Providers[i]
if match, ok := providersResponse.JSON200.Default[provider.Id]; ok {
model := defaultProvider.Models[match]
defaultModel = &model
} else {
for _, model := range provider.Models {
defaultModel = &model
break
}
}
var anthropic *client.ProviderInfo
for _, provider := range providersResponse.JSON200.Providers {
if provider.Id == "anthropic" {
anthropic = &provider
}
}
// default to anthropic if available
if anthropic != nil {
defaultProvider = anthropic
defaultModel = getDefaultModel(providersResponse, *anthropic)
}
for _, provider := range providersResponse.JSON200.Providers {
if defaultProvider == nil || defaultModel == nil {
defaultProvider = &provider
defaultModel = getDefaultModel(providersResponse, provider)
}
providers = append(providers, provider)
}
@@ -82,7 +84,7 @@ func New(ctx context.Context, version string, httpClient *client.ClientWithRespo
return nil, fmt.Errorf("no providers found")
}
appConfigPath := filepath.Join(Info.Path.Config, "tui.toml")
appConfigPath := filepath.Join(Info.Path.Config, "config")
appConfig, err := config.LoadConfig(appConfigPath)
if err != nil {
slog.Info("No TUI config found, using default values", "error", err)
@@ -91,7 +93,7 @@ func New(ctx context.Context, version string, httpClient *client.ClientWithRespo
}
var currentProvider *client.ProviderInfo
var currentModel *client.ProviderModel
var currentModel *client.ModelInfo
for _, provider := range providers {
if provider.Id == appConfig.Provider {
currentProvider = &provider
@@ -113,14 +115,26 @@ func New(ctx context.Context, version string, httpClient *client.ClientWithRespo
Session: &client.SessionInfo{},
Messages: []client.MessageInfo{},
Status: status.GetService(),
Commands: commands.NewCommandRegistry(),
}
theme.SetTheme(appConfig.Theme)
fileutil.Init()
return app, nil
}
func getDefaultModel(response *client.PostProviderListResponse, provider client.ProviderInfo) *client.ModelInfo {
if match, ok := response.JSON200.Default[provider.Id]; ok {
model := provider.Models[match]
return &model
} else {
for _, model := range provider.Models {
return &model
}
}
return nil
}
type Attachment struct {
FilePath string
FileName string
@@ -309,28 +323,3 @@ func (a *App) ListProviders(ctx context.Context) ([]client.ProviderInfo, error)
providers := *resp.JSON200
return providers.Providers, nil
}
// IsFilepickerOpen returns whether the filepicker is currently open
func (app *App) IsFilepickerOpen() bool {
return app.filepickerOpen
}
// SetFilepickerOpen sets the state of the filepicker
func (app *App) SetFilepickerOpen(open bool) {
app.filepickerOpen = open
}
// IsCompletionDialogOpen returns whether the completion dialog is currently open
func (app *App) IsCompletionDialogOpen() bool {
return app.completionDialogOpen
}
// SetCompletionDialogOpen sets the state of the completion dialog
func (app *App) SetCompletionDialogOpen(open bool) {
app.completionDialogOpen = open
}
// Shutdown performs a clean shutdown of the application
func (app *App) Shutdown() {
// TODO: cleanup?
}

View File

@@ -0,0 +1,71 @@
package commands
import (
"github.com/charmbracelet/bubbles/v2/key"
)
// Command represents a user-triggerable action.
type Command struct {
// Name is the identifier used for slash commands (e.g., "new").
Name string
// Description is a short explanation of what the command does.
Description string
// KeyBinding is the keyboard shortcut to trigger this command.
KeyBinding key.Binding
}
// Registry holds all the available commands.
type Registry map[string]Command
// ExecuteCommandMsg is a message sent when a command should be executed.
type ExecuteCommandMsg struct {
Name string
}
func NewCommandRegistry() Registry {
return Registry{
"help": {
Name: "help",
Description: "show help",
KeyBinding: key.NewBinding(
key.WithKeys("f1", "super+/", "super+h"),
),
},
"new": {
Name: "new",
Description: "new session",
KeyBinding: key.NewBinding(
key.WithKeys("f2", "super+n"),
),
},
"sessions": {
Name: "sessions",
Description: "switch session",
KeyBinding: key.NewBinding(
key.WithKeys("f3", "super+s"),
),
},
"model": {
Name: "model",
Description: "switch model",
KeyBinding: key.NewBinding(
key.WithKeys("f4", "super+m"),
),
},
"theme": {
Name: "theme",
Description: "switch theme",
KeyBinding: key.NewBinding(
key.WithKeys("f5", "super+t"),
),
},
"quit": {
Name: "quit",
Description: "quit",
KeyBinding: key.NewBinding(
key.WithKeys("f10", "ctrl+c", "super+q"),
),
},
}
}

View File

@@ -0,0 +1,71 @@
package completions
import (
"sort"
"github.com/lithammer/fuzzysearch/fuzzy"
"github.com/sst/opencode/internal/app"
"github.com/sst/opencode/internal/components/dialog"
)
type CommandCompletionProvider struct {
app *app.App
}
func NewCommandCompletionProvider(app *app.App) dialog.CompletionProvider {
return &CommandCompletionProvider{app: app}
}
func (c *CommandCompletionProvider) GetId() string {
return "commands"
}
func (c *CommandCompletionProvider) GetEntry() dialog.CompletionItemI {
return dialog.NewCompletionItem(dialog.CompletionItem{
Title: "Commands",
Value: "commands",
})
}
func (c *CommandCompletionProvider) GetChildEntries(query string) ([]dialog.CompletionItemI, error) {
if query == "" {
// If no query, return all commands
items := []dialog.CompletionItemI{}
for _, cmd := range c.app.Commands {
items = append(items, dialog.NewCompletionItem(dialog.CompletionItem{
Title: " /" + cmd.Name,
Value: "/" + cmd.Name,
}))
}
return items, nil
}
// Use fuzzy matching for commands
var commandNames []string
commandMap := make(map[string]dialog.CompletionItemI)
for _, cmd := range c.app.Commands {
commandNames = append(commandNames, cmd.Name)
commandMap[cmd.Name] = dialog.NewCompletionItem(dialog.CompletionItem{
Title: " /" + cmd.Name,
Value: "/" + cmd.Name,
})
}
// Find fuzzy matches
matches := fuzzy.RankFind(query, commandNames)
// Sort by score (best matches first)
sort.Sort(matches)
// Convert matches to completion items
items := []dialog.CompletionItemI{}
for _, match := range matches {
if item, ok := commandMap[match.Target]; ok {
items = append(items, item)
}
}
return items, nil
}

View File

@@ -1,18 +1,15 @@
package completions
import (
"bytes"
"fmt"
"os/exec"
"path/filepath"
"context"
"github.com/lithammer/fuzzysearch/fuzzy"
"github.com/sst/opencode/internal/fileutil"
"github.com/sst/opencode/internal/status"
"github.com/sst/opencode/internal/app"
"github.com/sst/opencode/internal/components/dialog"
"github.com/sst/opencode/pkg/client"
)
type filesAndFoldersContextGroup struct {
app *app.App
prefix string
}
@@ -27,143 +24,18 @@ func (cg *filesAndFoldersContextGroup) GetEntry() dialog.CompletionItemI {
})
}
func processNullTerminatedOutput(outputBytes []byte) []string {
if len(outputBytes) > 0 && outputBytes[len(outputBytes)-1] == 0 {
outputBytes = outputBytes[:len(outputBytes)-1]
}
if len(outputBytes) == 0 {
return []string{}
}
split := bytes.Split(outputBytes, []byte{0})
matches := make([]string, 0, len(split))
for _, p := range split {
if len(p) == 0 {
continue
}
path := string(p)
path = filepath.Join(".", path)
if !fileutil.SkipHidden(path) {
matches = append(matches, path)
}
}
return matches
}
func (cg *filesAndFoldersContextGroup) getFiles(query string) ([]string, error) {
cmdRg := fileutil.GetRgCmd("") // No glob pattern for this use case
cmdFzf := fileutil.GetFzfCmd(query)
var matches []string
// Case 1: Both rg and fzf available
if cmdRg != nil && cmdFzf != nil {
rgPipe, err := cmdRg.StdoutPipe()
if err != nil {
return nil, fmt.Errorf("failed to get rg stdout pipe: %w", err)
}
defer rgPipe.Close()
cmdFzf.Stdin = rgPipe
var fzfOut bytes.Buffer
var fzfErr bytes.Buffer
cmdFzf.Stdout = &fzfOut
cmdFzf.Stderr = &fzfErr
if err := cmdFzf.Start(); err != nil {
return nil, fmt.Errorf("failed to start fzf: %w", err)
}
errRg := cmdRg.Run()
errFzf := cmdFzf.Wait()
if errRg != nil {
status.Warn(fmt.Sprintf("rg command failed during pipe: %v", errRg))
}
if errFzf != nil {
if exitErr, ok := errFzf.(*exec.ExitError); ok && exitErr.ExitCode() == 1 {
return []string{}, nil // No matches from fzf
}
return nil, fmt.Errorf("fzf command failed: %w\nStderr: %s", errFzf, fzfErr.String())
}
matches = processNullTerminatedOutput(fzfOut.Bytes())
// Case 2: Only rg available
} else if cmdRg != nil {
status.Debug("Using Ripgrep with fuzzy match fallback for file completions")
var rgOut bytes.Buffer
var rgErr bytes.Buffer
cmdRg.Stdout = &rgOut
cmdRg.Stderr = &rgErr
if err := cmdRg.Run(); err != nil {
return nil, fmt.Errorf("rg command failed: %w\nStderr: %s", err, rgErr.String())
}
allFiles := processNullTerminatedOutput(rgOut.Bytes())
matches = fuzzy.Find(query, allFiles)
// Case 3: Only fzf available
} else if cmdFzf != nil {
status.Debug("Using FZF with doublestar fallback for file completions")
files, _, err := fileutil.GlobWithDoublestar("**/*", ".", 0)
if err != nil {
return nil, fmt.Errorf("failed to list files for fzf: %w", err)
}
allFiles := make([]string, 0, len(files))
for _, file := range files {
if !fileutil.SkipHidden(file) {
allFiles = append(allFiles, file)
}
}
var fzfIn bytes.Buffer
for _, file := range allFiles {
fzfIn.WriteString(file)
fzfIn.WriteByte(0)
}
cmdFzf.Stdin = &fzfIn
var fzfOut bytes.Buffer
var fzfErr bytes.Buffer
cmdFzf.Stdout = &fzfOut
cmdFzf.Stderr = &fzfErr
if err := cmdFzf.Run(); err != nil {
if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 1 {
return []string{}, nil
}
return nil, fmt.Errorf("fzf command failed: %w\nStderr: %s", err, fzfErr.String())
}
matches = processNullTerminatedOutput(fzfOut.Bytes())
// Case 4: Fallback to doublestar with fuzzy match
} else {
status.Debug("Using doublestar with fuzzy match for file completions")
allFiles, _, err := fileutil.GlobWithDoublestar("**/*", ".", 0)
if err != nil {
return nil, fmt.Errorf("failed to glob files: %w", err)
}
filteredFiles := make([]string, 0, len(allFiles))
for _, file := range allFiles {
if !fileutil.SkipHidden(file) {
filteredFiles = append(filteredFiles, file)
}
}
matches = fuzzy.Find(query, filteredFiles)
response, err := cg.app.Client.PostFileSearchWithResponse(context.Background(), client.PostFileSearchJSONRequestBody{
Query: query,
})
if err != nil {
return []string{}, err
}
if response.JSON200 == nil {
return []string{}, nil
}
return matches, nil
return *response.JSON200, nil
}
func (cg *filesAndFoldersContextGroup) GetChildEntries(query string) ([]dialog.CompletionItemI, error) {
@@ -184,8 +56,9 @@ func (cg *filesAndFoldersContextGroup) GetChildEntries(query string) ([]dialog.C
return items, nil
}
func NewFileAndFolderContextGroup() dialog.CompletionProvider {
func NewFileAndFolderContextGroup(app *app.App) dialog.CompletionProvider {
return &filesAndFoldersContextGroup{
app: app,
prefix: "file",
}
}

View File

@@ -0,0 +1,29 @@
package completions
import (
"strings"
"github.com/sst/opencode/internal/app"
"github.com/sst/opencode/internal/components/dialog"
)
type CompletionManager struct {
providers map[string]dialog.CompletionProvider
}
func NewCompletionManager(app *app.App) *CompletionManager {
return &CompletionManager{
providers: map[string]dialog.CompletionProvider{
"files": NewFileAndFolderContextGroup(app),
"commands": NewCommandCompletionProvider(app),
},
}
}
func (m *CompletionManager) GetProvider(input string) dialog.CompletionProvider {
if strings.HasPrefix(input, "/") {
return m.providers["commands"]
}
return m.providers["files"]
}

View File

@@ -5,16 +5,15 @@ import (
"log/slog"
"os"
"os/exec"
"slices"
"strings"
"unicode"
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/spinner"
"github.com/charmbracelet/bubbles/textarea"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/bubbles/v2/key"
"github.com/charmbracelet/bubbles/v2/spinner"
"github.com/charmbracelet/bubbles/v2/textarea"
tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/lipgloss/v2"
"github.com/sst/opencode/internal/app"
"github.com/sst/opencode/internal/commands"
"github.com/sst/opencode/internal/components/dialog"
"github.com/sst/opencode/internal/image"
"github.com/sst/opencode/internal/layout"
@@ -45,11 +44,6 @@ type EditorKeyMaps struct {
HistoryDown key.Binding
}
type bluredEditorKeyMaps struct {
Send key.Binding
Focus key.Binding
OpenEditor key.Binding
}
type DeleteAttachmentKeyMaps struct {
AttachmentDeleteMode key.Binding
Escape key.Binding
@@ -58,12 +52,12 @@ type DeleteAttachmentKeyMaps struct {
var editorMaps = EditorKeyMaps{
Send: key.NewBinding(
key.WithKeys("enter", "ctrl+s"),
key.WithKeys("enter"),
key.WithHelp("enter", "send message"),
),
OpenEditor: key.NewBinding(
key.WithKeys("ctrl+e"),
key.WithHelp("ctrl+e", "open editor"),
key.WithKeys("f12"),
key.WithHelp("f12", "open editor"),
),
Paste: key.NewBinding(
key.WithKeys("ctrl+v"),
@@ -109,17 +103,33 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case dialog.ThemeChangedMsg:
m.textarea = createTextArea(&m.textarea)
case dialog.CompletionSelectedMsg:
existingValue := m.textarea.Value()
modifiedValue := strings.Replace(existingValue, msg.SearchString, msg.CompletionValue, 1)
m.textarea.SetValue(modifiedValue)
return m, nil
case dialog.AttachmentAddedMsg:
if len(m.attachments) >= maxAttachments {
status.Error(fmt.Sprintf("cannot add more than %d images", maxAttachments))
return m, cmd
if msg.IsCommand {
// Execute the command directly
commandName := strings.TrimPrefix(msg.CompletionValue, "/")
m.textarea.Reset()
return m, util.CmdHandler(commands.ExecuteCommandMsg{Name: commandName})
} else {
// For files, replace the text in the editor
existingValue := m.textarea.Value()
modifiedValue := strings.Replace(existingValue, msg.SearchString, msg.CompletionValue, 1)
m.textarea.SetValue(modifiedValue)
return m, nil
}
m.attachments = append(m.attachments, msg.Attachment)
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c":
if m.textarea.Value() != "" {
m.textarea.Reset()
return m, func() tea.Msg {
return nil
}
}
case "shift+enter":
value := m.textarea.Value()
m.textarea.SetValue(value + "\n")
return m, nil
}
if key.Matches(msg, DeleteKeyMaps.AttachmentDeleteMode) {
m.deleteMode = true
return m, nil
@@ -129,18 +139,18 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.attachments = nil
return m, nil
}
if m.deleteMode && len(msg.Runes) > 0 && unicode.IsDigit(msg.Runes[0]) {
num := int(msg.Runes[0] - '0')
m.deleteMode = false
if num < 10 && len(m.attachments) > num {
if num == 0 {
m.attachments = m.attachments[num+1:]
} else {
m.attachments = slices.Delete(m.attachments, num, num+1)
}
return m, nil
}
}
// if m.deleteMode && len(msg.Runes) > 0 && unicode.IsDigit(msg.Runes[0]) {
// num := int(msg.Runes[0] - '0')
// m.deleteMode = false
// if num < 10 && len(m.attachments) > num {
// if num == 0 {
// m.attachments = m.attachments[num+1:]
// } else {
// m.attachments = slices.Delete(m.attachments, num, num+1)
// }
// return m, nil
// }
// }
if key.Matches(msg, messageKeys.PageUp) || key.Matches(msg, messageKeys.PageDown) ||
key.Matches(msg, messageKeys.HalfPageUp) || key.Matches(msg, messageKeys.HalfPageDown) {
return m, nil
@@ -177,7 +187,9 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// Handle history navigation with up/down arrow keys
// Only handle history navigation if the filepicker is not open and completion dialog is not open
if m.textarea.Focused() && key.Matches(msg, editorMaps.HistoryUp) && !m.app.IsFilepickerOpen() && !m.app.IsCompletionDialogOpen() {
if m.textarea.Focused() && key.Matches(msg, editorMaps.HistoryUp) {
// TODO: fix this
// && !m.app.IsFilepickerOpen() && !m.app.IsCompletionDialogOpen() {
// Get the current line number
currentLine := m.textarea.Line()
@@ -197,7 +209,9 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
}
if m.textarea.Focused() && key.Matches(msg, editorMaps.HistoryDown) && !m.app.IsFilepickerOpen() && !m.app.IsCompletionDialogOpen() {
if m.textarea.Focused() && key.Matches(msg, editorMaps.HistoryDown) {
// TODO: fix this
// && !m.app.IsFilepickerOpen() && !m.app.IsCompletionDialogOpen() {
// Get the current line number and total lines
currentLine := m.textarea.Line()
value := m.textarea.Value()
@@ -258,9 +272,13 @@ func (m *editorComponent) View() string {
m.textarea.View(),
)
textarea = styles.BaseStyle().
Width(m.width-2).
Border(lipgloss.NormalBorder(), true, true).
BorderForeground(t.Border()).
Width(m.width).
PaddingTop(1).
PaddingBottom(1).
Background(t.BackgroundElement()).
Border(lipgloss.ThickBorder(), false, true).
BorderForeground(t.BackgroundSubtle()).
BorderBackground(t.Background()).
Render(textarea)
hint := base("enter") + muted(" send ") + base("shift") + muted("+") + base("enter") + muted(" newline")
@@ -270,7 +288,7 @@ func (m *editorComponent) View() string {
model := ""
if m.app.Model != nil {
model = base(*m.app.Model.Name) + muted(" • /model")
model = base(m.app.Model.Name) + muted(" • /model")
}
space := m.width - 2 - lipgloss.Width(model) - lipgloss.Width(hint)
@@ -282,21 +300,19 @@ func (m *editorComponent) View() string {
content := lipgloss.JoinVertical(
lipgloss.Top,
// m.attachmentsContent(),
"",
textarea,
info,
)
return styles.ForceReplaceBackgroundWithLipgloss(
content,
t.Background(),
)
return content
}
func (m *editorComponent) SetSize(width, height int) tea.Cmd {
m.width = width
m.height = height
m.textarea.SetWidth(width - 5) // account for the prompt and padding right
m.textarea.SetHeight(height - 3) // account for info underneath
m.textarea.SetHeight(height - 4) // account for info underneath
return nil
}
@@ -304,13 +320,6 @@ func (m *editorComponent) GetSize() (int, int) {
return m.width, m.height
}
func (m *editorComponent) BindingKeys() []key.Binding {
bindings := []key.Binding{}
bindings = append(bindings, layout.KeyMapToSlice(editorMaps)...)
bindings = append(bindings, layout.KeyMapToSlice(DeleteKeyMaps)...)
return bindings
}
func (m *editorComponent) openEditor(value string) tea.Cmd {
editor := os.Getenv("EDITOR")
if editor == "" {
@@ -353,7 +362,7 @@ func (m *editorComponent) openEditor(value string) tea.Cmd {
}
func (m *editorComponent) send() tea.Cmd {
value := m.textarea.Value()
value := strings.TrimSpace(m.textarea.Value())
m.textarea.Reset()
attachments := m.attachments
@@ -370,6 +379,16 @@ func (m *editorComponent) send() tea.Cmd {
if value == "" {
return nil
}
// Check for slash command
// if strings.HasPrefix(value, "/") {
// commandName := strings.TrimPrefix(value, "/")
// if _, ok := m.app.Commands[commandName]; ok {
// return util.CmdHandler(commands.ExecuteCommandMsg{Name: commandName})
// }
// }
slog.Info("Send message", "value", value)
return tea.Batch(
util.CmdHandler(SendMsg{
Text: value,
@@ -407,21 +426,21 @@ func (m *editorComponent) attachmentsContent() string {
func createTextArea(existing *textarea.Model) textarea.Model {
t := theme.CurrentTheme()
bgColor := t.Background()
bgColor := t.BackgroundElement()
textColor := t.Text()
textMutedColor := t.TextMuted()
ta := textarea.New()
ta.Placeholder = "It's prompting time..."
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.Styles.Blurred.Base = lipgloss.NewStyle().Background(bgColor).Foreground(textColor)
ta.Styles.Blurred.CursorLine = lipgloss.NewStyle().Background(bgColor)
ta.Styles.Blurred.Placeholder = lipgloss.NewStyle().Background(bgColor).Foreground(textMutedColor)
ta.Styles.Blurred.Text = lipgloss.NewStyle().Background(bgColor).Foreground(textColor)
ta.Styles.Focused.Base = lipgloss.NewStyle().Background(bgColor).Foreground(textColor)
ta.Styles.Focused.CursorLine = lipgloss.NewStyle().Background(bgColor)
ta.Styles.Focused.Placeholder = lipgloss.NewStyle().Background(bgColor).Foreground(textMutedColor)
ta.Styles.Focused.Text = lipgloss.NewStyle().Background(bgColor).Foreground(textColor)
ta.Styles.Cursor.Color = t.Primary()
ta.Prompt = " "
ta.ShowLineNumbers = false
@@ -437,7 +456,11 @@ func createTextArea(existing *textarea.Model) textarea.Model {
return ta
}
func NewEditorComponent(app *app.App) tea.Model {
func (m *editorComponent) GetValue() string {
return m.textarea.Value()
}
func NewEditorComponent(app *app.App) layout.ModelWithView {
s := spinner.New(spinner.WithSpinner(spinner.Ellipsis), spinner.WithStyle(styles.Muted().Width(3)))
ta := createTextArea(nil)

View File

@@ -9,7 +9,8 @@ import (
"time"
"unicode"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/lipgloss/v2"
"github.com/charmbracelet/lipgloss/v2/compat"
"github.com/charmbracelet/x/ansi"
"github.com/sst/opencode/internal/app"
"github.com/sst/opencode/internal/components/diff"
@@ -21,8 +22,8 @@ import (
"golang.org/x/text/language"
)
func toMarkdown(content string, width int) string {
r := styles.GetMarkdownRenderer(width)
func toMarkdown(content string, width int, backgroundColor compat.AdaptiveColor) string {
r := styles.GetMarkdownRenderer(width, backgroundColor)
content = strings.ReplaceAll(content, app.Info.Path.Root+"/", "")
rendered, _ := r.Render(content)
lines := strings.Split(rendered, "\n")
@@ -50,10 +51,14 @@ func toMarkdown(content string, width int) string {
type blockRenderer struct {
align *lipgloss.Position
borderColor *lipgloss.AdaptiveColor
borderColor *compat.AdaptiveColor
fullWidth bool
paddingTop int
paddingBottom int
paddingLeft int
paddingRight int
marginTop int
marginBottom int
}
type renderingOption func(*blockRenderer)
@@ -70,12 +75,36 @@ func WithAlign(align lipgloss.Position) renderingOption {
}
}
func WithBorderColor(color lipgloss.AdaptiveColor) renderingOption {
func WithBorderColor(color compat.AdaptiveColor) renderingOption {
return func(c *blockRenderer) {
c.borderColor = &color
}
}
func WithMarginTop(padding int) renderingOption {
return func(c *blockRenderer) {
c.marginTop = padding
}
}
func WithMarginBottom(padding int) renderingOption {
return func(c *blockRenderer) {
c.marginBottom = padding
}
}
func WithPaddingLeft(padding int) renderingOption {
return func(c *blockRenderer) {
c.paddingLeft = padding
}
}
func WithPaddingRight(padding int) renderingOption {
return func(c *blockRenderer) {
c.paddingRight = padding
}
}
func WithPaddingTop(padding int) renderingOption {
return func(c *blockRenderer) {
c.paddingTop = padding
@@ -91,17 +120,23 @@ func WithPaddingBottom(padding int) renderingOption {
func renderContentBlock(content string, options ...renderingOption) string {
t := theme.CurrentTheme()
renderer := &blockRenderer{
fullWidth: false,
fullWidth: false,
paddingTop: 1,
paddingBottom: 1,
paddingLeft: 2,
paddingRight: 2,
}
for _, option := range options {
option(renderer)
}
style := styles.BaseStyle().
PaddingTop(1).
PaddingBottom(1).
PaddingLeft(2).
PaddingRight(2).
MarginTop(renderer.marginTop).
MarginBottom(renderer.marginBottom).
PaddingTop(renderer.paddingTop).
PaddingBottom(renderer.paddingBottom).
PaddingLeft(renderer.paddingLeft).
PaddingRight(renderer.paddingRight).
Background(t.BackgroundSubtle()).
Foreground(t.TextMuted()).
BorderStyle(lipgloss.ThickBorder())
@@ -137,28 +172,19 @@ func renderContentBlock(content string, options ...renderingOption) string {
BorderLeftBackground(t.Background())
}
content = styles.ForceReplaceBackgroundWithLipgloss(content, t.BackgroundSubtle())
if renderer.fullWidth {
style = style.Width(layout.Current.Container.Width - 2)
style = style.Width(layout.Current.Container.Width)
}
content = style.Render(content)
if renderer.paddingTop > 0 {
content = strings.Repeat("\n", renderer.paddingTop) + content
}
if renderer.paddingBottom > 0 {
content = content + strings.Repeat("\n", renderer.paddingBottom)
}
content = lipgloss.PlaceHorizontal(
layout.Current.Container.Width,
align,
content,
lipgloss.WithWhitespaceBackground(t.Background()),
)
content = lipgloss.PlaceHorizontal(
layout.Current.Viewport.Width,
lipgloss.Center,
content,
lipgloss.WithWhitespaceBackground(t.Background()),
)
return content
}
@@ -167,13 +193,12 @@ func renderText(message client.MessageInfo, text string, author string) string {
t := theme.CurrentTheme()
width := layout.Current.Container.Width
padding := 0
switch layout.Current.Size {
case layout.LayoutSizeSmall:
if layout.Current.Viewport.Width < 80 {
padding = 5
case layout.LayoutSizeNormal:
padding = 10
case layout.LayoutSizeLarge:
} else if layout.Current.Viewport.Width < 120 {
padding = 15
} else {
padding = 20
}
timestamp := time.UnixMilli(int64(message.Metadata.Time.Created)).Local().Format("02 Jan 2006 03:04 PM")
@@ -181,9 +206,7 @@ func renderText(message client.MessageInfo, text string, author string) string {
// don't show the date if it's today
timestamp = timestamp[12:]
}
info := styles.BaseStyle().
Foreground(t.TextMuted()).
Render(fmt.Sprintf("%s (%s)", author, timestamp))
info := fmt.Sprintf("%s (%s)", author, timestamp)
align := lipgloss.Left
switch message.Role {
@@ -193,9 +216,9 @@ func renderText(message client.MessageInfo, text string, author string) string {
align = lipgloss.Left
}
textWidth := lipgloss.Width(text)
textWidth := max(lipgloss.Width(text), lipgloss.Width(info))
markdownWidth := min(textWidth, width-padding-4) // -4 for the border and padding
content := toMarkdown(text, markdownWidth)
content := toMarkdown(text, markdownWidth, t.BackgroundSubtle())
content = lipgloss.JoinVertical(align, content, info)
switch message.Role {
@@ -207,7 +230,7 @@ func renderText(message client.MessageInfo, text string, author string) string {
case client.Assistant:
return renderContentBlock(content,
WithAlign(lipgloss.Left),
WithBorderColor(t.Primary()),
WithBorderColor(t.Accent()),
)
}
return ""
@@ -216,7 +239,7 @@ func renderText(message client.MessageInfo, text string, author string) string {
func renderToolInvocation(
toolCall client.MessageToolInvocationToolCall,
result *string,
metadata map[string]any,
metadata client.MessageInfo_Metadata_Tool_AdditionalProperties,
showResult bool,
) string {
ignoredTools := []string{"opencode_todoread"}
@@ -224,16 +247,28 @@ func renderToolInvocation(
return ""
}
padding := 1
outerWidth := layout.Current.Container.Width - 1 // subtract 1 for the border
innerWidth := outerWidth - padding - 4 // -4 for the border and padding
outerWidth := layout.Current.Container.Width
innerWidth := outerWidth - 6
paddingTop := 0
paddingBottom := 0
if showResult {
paddingTop = 1
if result == nil || *result == "" {
paddingBottom = 1
}
}
t := theme.CurrentTheme()
style := styles.Muted().
Width(outerWidth).
PaddingLeft(padding).
Background(t.BackgroundSubtle()).
PaddingTop(paddingTop).
PaddingBottom(paddingBottom).
PaddingLeft(2).
PaddingRight(2).
BorderLeft(true).
BorderForeground(t.BorderSubtle()).
BorderRight(true).
BorderForeground(t.BackgroundSubtle()).
BorderStyle(lipgloss.ThickBorder())
if toolCall.State == "partial-call" {
@@ -262,88 +297,119 @@ func renderToolInvocation(
}
body := ""
error := ""
finished := result != nil && *result != ""
if finished {
body = *result
}
if metadata["error"] != nil && metadata["message"] != nil {
body = styles.BaseStyle().
Width(outerWidth).
Foreground(t.Error()).
Render(metadata["message"].(string))
if e, ok := metadata.Get("error"); ok && e.(bool) == true {
if m, ok := metadata.Get("message"); ok {
style = style.BorderLeftForeground(t.Error())
error = styles.BaseStyle().
Background(t.BackgroundSubtle()).
Foreground(t.Error()).
Render(m.(string))
error = renderContentBlock(
error,
WithFullWidth(),
WithBorderColor(t.Error()),
WithMarginBottom(1),
)
}
}
elapsed := ""
if metadata["time"] != nil {
timeMap := metadata["time"].(map[string]any)
start := timeMap["start"].(float64)
end := timeMap["end"].(float64)
durationMs := end - start
duration := time.Duration(durationMs * float64(time.Millisecond))
roundedDuration := time.Duration(duration.Round(time.Millisecond))
if durationMs > 1000 {
roundedDuration = time.Duration(duration.Round(time.Second))
}
elapsed = styles.Muted().Render(roundedDuration.String())
start := metadata.Time.Start
end := metadata.Time.End
durationMs := end - start
duration := time.Duration(durationMs * float32(time.Millisecond))
roundedDuration := time.Duration(duration.Round(time.Millisecond))
if durationMs > 1000 {
roundedDuration = time.Duration(duration.Round(time.Second))
}
elapsed = styles.Muted().Render(roundedDuration.String())
title := ""
switch toolCall.ToolName {
case "opencode_read":
toolArgs = renderArgs(&toolArgsMap, "filePath")
title = fmt.Sprintf("Read: %s %s", toolArgs, elapsed)
body = ""
filename := toolArgsMap["filePath"].(string)
if metadata["preview"] != nil {
body = metadata["preview"].(string)
if preview, ok := metadata.Get("preview"); ok && toolArgsMap["filePath"] != nil {
filename := toolArgsMap["filePath"].(string)
body = preview.(string)
body = renderFile(filename, body, WithTruncate(6))
}
case "opencode_edit":
filename := toolArgsMap["filePath"].(string)
title = fmt.Sprintf("Edit: %s %s", relative(filename), elapsed)
if metadata["diff"] != nil {
patch := metadata["diff"].(string)
diffWidth := min(layout.Current.Viewport.Width, 120)
formattedDiff, _ := diff.FormatDiff(filename, patch, diff.WithTotalWidth(diffWidth))
body = strings.TrimSpace(formattedDiff)
body = lipgloss.Place(
layout.Current.Viewport.Width,
lipgloss.Height(body)+2,
lipgloss.Center,
lipgloss.Center,
body,
lipgloss.WithWhitespaceBackground(t.Background()),
)
if filename, ok := toolArgsMap["filePath"].(string); ok {
title = fmt.Sprintf("Edit: %s %s", relative(filename), elapsed)
if d, ok := metadata.Get("diff"); ok {
patch := d.(string)
var formattedDiff string
if layout.Current.Viewport.Width < 80 {
formattedDiff, _ = diff.FormatUnifiedDiff(
filename,
patch,
diff.WithWidth(layout.Current.Container.Width-2),
)
} else {
diffWidth := min(layout.Current.Viewport.Width-2, 120)
formattedDiff, _ = diff.FormatDiff(filename, patch, diff.WithTotalWidth(diffWidth))
}
formattedDiff = strings.TrimSpace(formattedDiff)
formattedDiff = lipgloss.NewStyle().
BorderStyle(lipgloss.ThickBorder()).
BorderForeground(t.BackgroundSubtle()).
BorderLeft(true).
BorderRight(true).
Render(formattedDiff)
if showResult {
style = style.Width(lipgloss.Width(formattedDiff))
title += "\n"
}
body = strings.TrimSpace(formattedDiff)
body = lipgloss.Place(
layout.Current.Viewport.Width,
lipgloss.Height(body)+1,
lipgloss.Center,
lipgloss.Top,
body,
)
}
}
case "opencode_write":
filename := toolArgsMap["filePath"].(string)
title = fmt.Sprintf("Write: %s %s", relative(filename), elapsed)
content := toolArgsMap["content"].(string)
body = renderFile(filename, content)
if filename, ok := toolArgsMap["filePath"].(string); ok {
title = fmt.Sprintf("Write: %s %s", relative(filename), elapsed)
if content, ok := toolArgsMap["content"].(string); ok {
body = renderFile(filename, content)
}
}
case "opencode_bash":
description := toolArgsMap["description"].(string)
title = fmt.Sprintf("Shell: %s %s", description, elapsed)
if metadata["stdout"] != nil {
if description, ok := toolArgsMap["description"].(string); ok {
title = fmt.Sprintf("Shell: %s %s", description, elapsed)
}
if stdout, ok := metadata.Get("stdout"); ok {
command := toolArgsMap["command"].(string)
stdout := metadata["stdout"].(string)
stdout := stdout.(string)
body = fmt.Sprintf("```console\n> %s\n%s```", command, stdout)
body = toMarkdown(body, innerWidth)
body = renderContentBlock(body, WithFullWidth(), WithPaddingTop(1), WithPaddingBottom(1))
body = toMarkdown(body, innerWidth, t.BackgroundSubtle())
body = renderContentBlock(body, WithFullWidth(), WithMarginBottom(1))
}
case "opencode_webfetch":
toolArgs = renderArgs(&toolArgsMap, "url")
title = fmt.Sprintf("Fetching: %s %s", toolArgs, elapsed)
format := toolArgsMap["format"].(string)
body = truncateHeight(body, 10)
if format == "html" || format == "markdown" {
body = toMarkdown(body, innerWidth)
if format, ok := toolArgsMap["format"].(string); ok {
body = *result
body = truncateHeight(body, 10)
if format == "html" || format == "markdown" {
body = toMarkdown(body, innerWidth, t.BackgroundSubtle())
}
body = renderContentBlock(body, WithFullWidth(), WithMarginBottom(1))
}
body = renderContentBlock(body, WithFullWidth(), WithPaddingTop(1), WithPaddingBottom(1))
case "opencode_todowrite":
title = fmt.Sprintf("Planning... %s", elapsed)
if finished && metadata["todos"] != nil {
body = ""
todos := metadata["todos"].([]any)
title = fmt.Sprintf("Planning %s", elapsed)
if to, ok := metadata.Get("todos"); ok && finished {
todos := to.([]any)
for _, todo := range todos {
t := todo.(map[string]any)
content := t["content"].(string)
@@ -351,27 +417,36 @@ func renderToolInvocation(
case "completed":
body += fmt.Sprintf("- [x] %s\n", content)
// case "in-progress":
// body += fmt.Sprintf("- [ ] _%s_\n", content)
// body += fmt.Sprintf("- [ ] %s\n", content)
default:
body += fmt.Sprintf("- [ ] %s\n", content)
}
}
body = toMarkdown(body, innerWidth)
body = renderContentBlock(body, WithFullWidth(), WithPaddingTop(1), WithPaddingBottom(1))
body = toMarkdown(body, innerWidth, t.BackgroundSubtle())
body = renderContentBlock(body, WithFullWidth(), WithMarginBottom(1))
}
default:
toolName := renderToolName(toolCall.ToolName)
title = fmt.Sprintf("%s: %s %s", toolName, toolArgs, elapsed)
body = *result
body = truncateHeight(body, 10)
body = renderContentBlock(body, WithFullWidth(), WithPaddingTop(1), WithPaddingBottom(1))
body = renderContentBlock(body, WithFullWidth(), WithMarginBottom(1))
}
if body == "" && error == "" {
body = *result
body = truncateHeight(body, 10)
body = renderContentBlock(body, WithFullWidth(), WithMarginBottom(1))
}
content := style.Render(title)
content = lipgloss.PlaceHorizontal(layout.Current.Viewport.Width, lipgloss.Center, content)
content = styles.ForceReplaceBackgroundWithLipgloss(content, t.Background())
if showResult && body != "" {
if showResult && body != "" && error == "" {
content += "\n" + body
}
if showResult && error != "" {
content += "\n" + error
}
return content
}
@@ -411,6 +486,7 @@ func WithTruncate(height int) fileRenderingOption {
}
func renderFile(filename string, content string, options ...fileRenderingOption) string {
t := theme.CurrentTheme()
renderer := &fileRenderer{
filename: filename,
content: content,
@@ -419,7 +495,6 @@ func renderFile(filename string, content string, options ...fileRenderingOption)
option(renderer)
}
// TODO: is this even needed?
lines := []string{}
for line := range strings.SplitSeq(content, "\n") {
line = strings.TrimRightFunc(line, unicode.IsSpace)
@@ -428,25 +503,14 @@ func renderFile(filename string, content string, options ...fileRenderingOption)
}
content = strings.Join(lines, "\n")
width := layout.Current.Container.Width - 6
width := layout.Current.Container.Width - 8
if renderer.height > 0 {
content = truncateHeight(content, renderer.height)
}
content = fmt.Sprintf("```%s\n%s\n```", extension(renderer.filename), content)
content = toMarkdown(content, width)
content = toMarkdown(content, width, t.BackgroundSubtle())
// ensure no line is wider than the width
// truncated := []string{}
// for line := range strings.SplitSeq(content, "\n") {
// line = strings.TrimRightFunc(line, unicode.IsSpace)
// // if lipgloss.Width(line) > width-3 {
// line = ansi.Truncate(line, width-3, "")
// // }
// truncated = append(truncated, line)
// }
// content = strings.Join(truncated, "\n")
return renderContentBlock(content, WithFullWidth(), WithPaddingTop(1), WithPaddingBottom(1))
return renderContentBlock(content, WithFullWidth(), WithMarginBottom(1))
}
func renderToolAction(name string) string {
@@ -484,6 +548,9 @@ func renderArgs(args *map[string]any, titleKey string) string {
title := ""
parts := []string{}
for key, value := range *args {
if value == nil {
continue
}
if key == "filePath" || key == "path" {
value = relative(value.(string))
}

View File

@@ -4,11 +4,11 @@ import (
"strings"
"time"
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/spinner"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/bubbles/v2/key"
"github.com/charmbracelet/bubbles/v2/spinner"
"github.com/charmbracelet/bubbles/v2/viewport"
tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/lipgloss/v2"
"github.com/sst/opencode/internal/app"
"github.com/sst/opencode/internal/components/dialog"
"github.com/sst/opencode/internal/layout"
@@ -118,10 +118,10 @@ type blockType int
const (
none blockType = iota
systemTextBlock
userTextBlock
assistantTextBlock
toolInvocationBlock
errorBlock
)
func (m *messagesComponent) renderView() {
@@ -129,13 +129,10 @@ func (m *messagesComponent) renderView() {
return
}
t := theme.CurrentTheme()
blocks := make([]string, 0)
previousBlockType := none
for _, message := range m.app.Messages {
if message.Role == client.System {
continue // ignoring system messages for now
}
var content string
var cached bool
@@ -172,15 +169,13 @@ func (m *messagesComponent) renderView() {
previousBlockType = userTextBlock
} else if message.Role == client.Assistant {
previousBlockType = assistantTextBlock
} else if message.Role == client.System {
previousBlockType = systemTextBlock
}
case client.MessagePartToolInvocation:
toolInvocationPart := part.(client.MessagePartToolInvocation)
toolCall, _ := toolInvocationPart.ToolInvocation.AsMessageToolInvocationToolCall()
metadata := map[string]any{}
metadata := client.MessageInfo_Metadata_Tool_AdditionalProperties{}
if _, ok := message.Metadata.Tool[toolCall.ToolCallId]; ok {
metadata = message.Metadata.Tool[toolCall.ToolCallId].(map[string]any)
metadata = message.Metadata.Tool[toolCall.ToolCallId]
}
var result *string
resultPart, resultError := toolInvocationPart.ToolInvocation.AsMessageToolInvocationToolResult()
@@ -211,21 +206,33 @@ func (m *messagesComponent) renderView() {
previousBlockType = toolInvocationBlock
}
}
error := ""
if message.Metadata.Error != nil {
errorValue, _ := message.Metadata.Error.ValueByDiscriminator()
switch errorValue.(type) {
case client.UnknownError:
clientError := errorValue.(client.UnknownError)
error = clientError.Data.Message
error = renderContentBlock(error, WithBorderColor(t.Error()), WithFullWidth(), WithMarginTop(1), WithMarginBottom(1))
blocks = append(blocks, error)
previousBlockType = errorBlock
}
}
}
t := theme.CurrentTheme()
centered := []string{}
for _, block := range blocks {
centered = append(centered, lipgloss.PlaceHorizontal(
m.width,
lipgloss.Center,
block,
lipgloss.WithWhitespaceBackground(t.Background()),
lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
))
}
m.viewport.Height = m.height - lipgloss.Height(m.header())
m.viewport.SetContent(strings.Join(centered, "\n"))
m.viewport.SetHeight(m.height - lipgloss.Height(m.header()))
m.viewport.SetContent("\n" + strings.Join(centered, "\n") + "\n")
}
func (m *messagesComponent) header() string {
@@ -238,7 +245,7 @@ func (m *messagesComponent) header() string {
base := styles.BaseStyle().Render
muted := styles.Muted().Render
headerLines := []string{}
headerLines = append(headerLines, toMarkdown("# "+m.app.Session.Title, width))
headerLines = append(headerLines, toMarkdown("# "+m.app.Session.Title, width-6, t.Background()))
if m.app.Session.Share != nil && m.app.Session.Share.Url != "" {
headerLines = append(headerLines, muted(m.app.Session.Share.Url))
} else {
@@ -248,20 +255,26 @@ func (m *messagesComponent) header() string {
header = styles.BaseStyle().
Width(width).
PaddingTop(1).
BorderBottom(true).
BorderForeground(t.BorderSubtle()).
BorderStyle(lipgloss.NormalBorder()).
Background(t.Background()).
PaddingLeft(2).
PaddingRight(2).
// Background(t.BackgroundElement()).
BorderLeft(true).
BorderRight(true).
BorderBackground(t.Background()).
BorderForeground(t.BackgroundSubtle()).
BorderStyle(lipgloss.ThickBorder()).
Render(header)
return styles.ForceReplaceBackgroundWithLipgloss(header, t.Background())
return "\n" + header + "\n"
}
func (m *messagesComponent) View() string {
if len(m.app.Messages) == 0 || m.rendering {
if len(m.app.Messages) == 0 {
return m.home()
}
if m.rendering {
return m.viewport.View()
}
return lipgloss.JoinVertical(
lipgloss.Left,
lipgloss.PlaceHorizontal(m.width, lipgloss.Center, m.header()),
@@ -269,73 +282,8 @@ func (m *messagesComponent) View() string {
)
}
// func hasToolsWithoutResponse(messages []message.Message) bool {
// toolCalls := make([]message.ToolCall, 0)
// toolResults := make([]message.ToolResult, 0)
// for _, m := range messages {
// toolCalls = append(toolCalls, m.ToolCalls()...)
// toolResults = append(toolResults, m.ToolResults()...)
// }
//
// for _, v := range toolCalls {
// found := false
// for _, r := range toolResults {
// if v.ID == r.ToolCallID {
// found = true
// break
// }
// }
// if !found && v.Finished {
// return true
// }
// }
// return false
// }
// func hasUnfinishedToolCalls(messages []message.Message) bool {
// toolCalls := make([]message.ToolCall, 0)
// for _, m := range messages {
// toolCalls = append(toolCalls, m.ToolCalls()...)
// }
// for _, v := range toolCalls {
// if !v.Finished {
// return true
// }
// }
// return false
// }
func (m *messagesComponent) help() string {
t := theme.CurrentTheme()
baseStyle := styles.BaseStyle()
text := ""
if m.app.IsBusy() {
text += lipgloss.JoinHorizontal(
lipgloss.Left,
baseStyle.Foreground(t.TextMuted()).Bold(true).Render("press "),
baseStyle.Foreground(t.Text()).Bold(true).Render("esc"),
baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" to interrupt"),
)
} else {
text += lipgloss.JoinHorizontal(
lipgloss.Left,
baseStyle.Foreground(t.Text()).Bold(true).Render("enter"),
baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" to send,"),
baseStyle.Foreground(t.Text()).Bold(true).Render(" \\"),
baseStyle.Foreground(t.TextMuted()).Bold(true).Render("+"),
baseStyle.Foreground(t.Text()).Bold(true).Render("enter"),
baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" for newline"),
)
}
return baseStyle.
Width(m.width).
Render(text)
}
func (m *messagesComponent) home() string {
t := theme.CurrentTheme()
// t := theme.CurrentTheme()
baseStyle := styles.BaseStyle()
base := baseStyle.Render
muted := styles.Muted().Render
@@ -359,8 +307,8 @@ func (m *messagesComponent) home() string {
styles.Muted().Render(open),
styles.BaseStyle().Render(code),
)
cwd := app.Info.Path.Cwd
config := app.Info.Path.Config
// cwd := app.Info.Path.Cwd
// config := app.Info.Path.Config
commands := [][]string{
{"/help", "show help"},
@@ -387,9 +335,9 @@ func (m *messagesComponent) home() string {
lines = append(lines, "")
lines = append(lines, logoAndVersion)
lines = append(lines, "")
lines = append(lines, base("cwd ")+muted(cwd))
lines = append(lines, base("config ")+muted(config))
lines = append(lines, "")
// lines = append(lines, base("cwd ")+muted(cwd))
// lines = append(lines, base("config ")+muted(config))
// lines = append(lines, "")
lines = append(lines, commandLines...)
lines = append(lines, "")
if m.rendering {
@@ -398,16 +346,13 @@ func (m *messagesComponent) home() string {
lines = append(lines, "")
}
return styles.ForceReplaceBackgroundWithLipgloss(
lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center,
baseStyle.Width(lipgloss.Width(logoAndVersion)).Render(
lipgloss.JoinVertical(
lipgloss.Top,
lines...,
),
)),
t.Background(),
)
return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center,
baseStyle.Width(lipgloss.Width(logoAndVersion)).Render(
lipgloss.JoinVertical(
lipgloss.Top,
lines...,
),
))
}
func (m *messagesComponent) SetSize(width, height int) tea.Cmd {
@@ -420,10 +365,10 @@ func (m *messagesComponent) SetSize(width, height int) tea.Cmd {
}
m.width = width
m.height = height
m.viewport.Width = width
m.viewport.Height = height - lipgloss.Height(m.header())
m.attachments.Width = width + 40
m.attachments.Height = 3
m.viewport.SetWidth(width)
m.viewport.SetHeight(height - lipgloss.Height(m.header()))
m.attachments.SetWidth(width + 40)
m.attachments.SetHeight(3)
m.renderView()
return nil
}
@@ -440,24 +385,15 @@ func (m *messagesComponent) Reload() tea.Cmd {
}
}
func (m *messagesComponent) BindingKeys() []key.Binding {
return []key.Binding{
m.viewport.KeyMap.PageDown,
m.viewport.KeyMap.PageUp,
m.viewport.KeyMap.HalfPageUp,
m.viewport.KeyMap.HalfPageDown,
}
}
func NewMessagesComponent(app *app.App) tea.Model {
func NewMessagesComponent(app *app.App) layout.ModelWithView {
customSpinner := spinner.Spinner{
Frames: []string{" ", "┃", "┃"},
FPS: time.Second / 3,
}
s := spinner.New(spinner.WithSpinner(customSpinner))
vp := viewport.New(0, 0)
attachments := viewport.New(0, 0)
vp := viewport.New() //(0, 0)
attachments := viewport.New() //(0, 0)
vp.KeyMap.PageUp = messageKeys.PageUp
vp.KeyMap.PageDown = messageKeys.PageDown
vp.KeyMap.HalfPageUp = messageKeys.HalfPageUp

View File

@@ -5,20 +5,21 @@ import (
"strings"
"time"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/lipgloss/v2"
"github.com/sst/opencode/internal/app"
"github.com/sst/opencode/internal/layout"
"github.com/sst/opencode/internal/pubsub"
"github.com/sst/opencode/internal/status"
"github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme"
)
type StatusCmp interface {
tea.Model
type StatusComponent interface {
layout.ModelWithView
}
type statusCmp struct {
type statusComponent struct {
app *app.App
queue []status.StatusMessage
width int
@@ -27,7 +28,7 @@ type statusCmp struct {
}
// clearMessageCmd is a command that clears status messages after a timeout
func (m statusCmp) clearMessageCmd() tea.Cmd {
func (m statusComponent) clearMessageCmd() tea.Cmd {
return tea.Tick(time.Second, func(t time.Time) tea.Msg {
return statusCleanupMsg{time: t}
})
@@ -38,11 +39,11 @@ type statusCleanupMsg struct {
time time.Time
}
func (m statusCmp) Init() tea.Cmd {
func (m statusComponent) Init() tea.Cmd {
return m.clearMessageCmd()
}
func (m statusCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
func (m statusComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.width = msg.Width
@@ -100,14 +101,15 @@ func (m statusCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
func logo() string {
t := theme.CurrentTheme()
mark := styles.Bold().Foreground(t.Primary()).Render("◧ ")
open := styles.Muted().Render("open")
code := styles.BaseStyle().Bold(true).Render("code")
version := styles.Muted().Render(app.Info.Version)
return styles.ForceReplaceBackgroundWithLipgloss(
styles.Padded().Render(mark+open+code+" "+version),
t.BackgroundElement(),
)
base := lipgloss.NewStyle().Background(t.BackgroundElement()).Foreground(t.TextMuted()).Render
emphasis := lipgloss.NewStyle().Bold(true).Background(t.BackgroundElement()).Foreground(t.Text()).Render
open := base("open")
code := emphasis("code ")
version := base(app.Info.Version)
return styles.Padded().
Background(t.BackgroundElement()).
Render(open + code + version)
}
func formatTokensAndCost(tokens float32, contextWindow float32, cost float32) string {
@@ -137,7 +139,7 @@ func formatTokensAndCost(tokens float32, contextWindow float32, cost float32) st
return fmt.Sprintf("Tokens: %s (%d%%), Cost: %s", formattedTokens, int(percentage), formattedCost)
}
func (m statusCmp) View() string {
func (m statusComponent) View() string {
if m.app.Session.Id == "" {
return styles.BaseStyle().
Width(m.width).
@@ -250,107 +252,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.app.LSPClients {
// if client.GetServerState() == lsp.StateStarting {
// initializing = true
// break
// }
// }
// If any server is initializing, show that status
if initializing {
return lipgloss.NewStyle().
Foreground(t.Warning()).
Render(fmt.Sprintf("%s Initializing LSP...", styles.SpinnerIcon))
}
// errorDiagnostics := []protocol.Diagnostic{}
// warnDiagnostics := []protocol.Diagnostic{}
// hintDiagnostics := []protocol.Diagnostic{}
// infoDiagnostics := []protocol.Diagnostic{}
// for _, client := range m.app.LSPClients {
// for _, d := range client.GetDiagnostics() {
// for _, diag := range d {
// switch diag.Severity {
// case protocol.SeverityError:
// errorDiagnostics = append(errorDiagnostics, diag)
// case protocol.SeverityWarning:
// warnDiagnostics = append(warnDiagnostics, diag)
// case protocol.SeverityHint:
// hintDiagnostics = append(hintDiagnostics, diag)
// case protocol.SeverityInformation:
// infoDiagnostics = append(infoDiagnostics, diag)
// }
// }
// }
// }
return styles.ForceReplaceBackgroundWithLipgloss(
styles.Padded().Render("No diagnostics"),
t.BackgroundElement(),
)
// if len(errorDiagnostics) == 0 &&
// len(warnDiagnostics) == 0 &&
// len(infoDiagnostics) == 0 &&
// len(hintDiagnostics) == 0 {
// return styles.ForceReplaceBackgroundWithLipgloss(
// styles.Padded().Render("No diagnostics"),
// t.BackgroundDarker(),
// )
// }
// diagnostics := []string{}
//
// errStr := lipgloss.NewStyle().
// Background(t.BackgroundDarker()).
// Foreground(t.Error()).
// Render(fmt.Sprintf("%s %d", styles.ErrorIcon, len(errorDiagnostics)))
// diagnostics = append(diagnostics, errStr)
//
// warnStr := lipgloss.NewStyle().
// Background(t.BackgroundDarker()).
// Foreground(t.Warning()).
// Render(fmt.Sprintf("%s %d", styles.WarningIcon, len(warnDiagnostics)))
// diagnostics = append(diagnostics, warnStr)
//
// infoStr := lipgloss.NewStyle().
// Background(t.BackgroundDarker()).
// Foreground(t.Info()).
// Render(fmt.Sprintf("%s %d", styles.InfoIcon, len(infoDiagnostics)))
// diagnostics = append(diagnostics, infoStr)
//
// hintStr := lipgloss.NewStyle().
// Background(t.BackgroundDarker()).
// Foreground(t.Text()).
// Render(fmt.Sprintf("%s %d", styles.HintIcon, len(hintDiagnostics)))
// diagnostics = append(diagnostics, hintStr)
//
// return styles.ForceReplaceBackgroundWithLipgloss(
// styles.Padded().Render(strings.Join(diagnostics, " ")),
// t.BackgroundDarker(),
// )
}
func (m statusCmp) model() string {
t := theme.CurrentTheme()
model := "None"
if m.app.Model != nil {
model = *m.app.Model.Name
}
return styles.Padded().
Background(t.Secondary()).
Foreground(t.Background()).
Render(model)
}
func NewStatusCmp(app *app.App) StatusCmp {
statusComponent := &statusCmp{
func NewStatusCmp(app *app.App) StatusComponent {
statusComponent := &statusComponent{
app: app,
queue: []status.StatusMessage{},
messageTTL: 4 * time.Second,

View File

@@ -1,257 +0,0 @@
package dialog
import (
"fmt"
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme"
"github.com/sst/opencode/internal/util"
)
type argumentsDialogKeyMap struct {
Enter key.Binding
Escape key.Binding
}
// ShortHelp implements key.Map.
func (k argumentsDialogKeyMap) ShortHelp() []key.Binding {
return []key.Binding{
key.NewBinding(
key.WithKeys("enter"),
key.WithHelp("enter", "confirm"),
),
key.NewBinding(
key.WithKeys("esc"),
key.WithHelp("esc", "cancel"),
),
}
}
// FullHelp implements key.Map.
func (k argumentsDialogKeyMap) FullHelp() [][]key.Binding {
return [][]key.Binding{k.ShortHelp()}
}
// ShowMultiArgumentsDialogMsg is a message that is sent to show the multi-arguments dialog.
type ShowMultiArgumentsDialogMsg struct {
CommandID string
Content string
ArgNames []string
}
// CloseMultiArgumentsDialogMsg is a message that is sent when the multi-arguments dialog is closed.
type CloseMultiArgumentsDialogMsg struct {
Submit bool
CommandID string
Content string
Args map[string]string
}
// MultiArgumentsDialogCmp is a component that asks the user for multiple command arguments.
type MultiArgumentsDialogCmp struct {
width, height int
inputs []textinput.Model
focusIndex int
keys argumentsDialogKeyMap
commandID string
content string
argNames []string
}
// NewMultiArgumentsDialogCmp creates a new MultiArgumentsDialogCmp.
func NewMultiArgumentsDialogCmp(commandID, content string, argNames []string) MultiArgumentsDialogCmp {
t := theme.CurrentTheme()
inputs := make([]textinput.Model, len(argNames))
for i, name := range argNames {
ti := textinput.New()
ti.Placeholder = fmt.Sprintf("Enter value for %s...", name)
ti.Width = 40
ti.Prompt = ""
ti.PlaceholderStyle = ti.PlaceholderStyle.Background(t.Background())
ti.PromptStyle = ti.PromptStyle.Background(t.Background())
ti.TextStyle = ti.TextStyle.Background(t.Background())
// Only focus the first input initially
if i == 0 {
ti.Focus()
ti.PromptStyle = ti.PromptStyle.Foreground(t.Primary())
ti.TextStyle = ti.TextStyle.Foreground(t.Primary())
} else {
ti.Blur()
}
inputs[i] = ti
}
return MultiArgumentsDialogCmp{
inputs: inputs,
keys: argumentsDialogKeyMap{},
commandID: commandID,
content: content,
argNames: argNames,
focusIndex: 0,
}
}
// Init implements tea.Model.
func (m MultiArgumentsDialogCmp) Init() tea.Cmd {
// Make sure only the first input is focused
for i := range m.inputs {
if i == 0 {
m.inputs[i].Focus()
} else {
m.inputs[i].Blur()
}
}
return textinput.Blink
}
// Update implements tea.Model.
func (m MultiArgumentsDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmds []tea.Cmd
t := theme.CurrentTheme()
switch msg := msg.(type) {
case tea.KeyMsg:
switch {
case key.Matches(msg, key.NewBinding(key.WithKeys("esc"))):
return m, util.CmdHandler(CloseMultiArgumentsDialogMsg{
Submit: false,
CommandID: m.commandID,
Content: m.content,
Args: nil,
})
case key.Matches(msg, key.NewBinding(key.WithKeys("enter"))):
// If we're on the last input, submit the form
if m.focusIndex == len(m.inputs)-1 {
args := make(map[string]string)
for i, name := range m.argNames {
args[name] = m.inputs[i].Value()
}
return m, util.CmdHandler(CloseMultiArgumentsDialogMsg{
Submit: true,
CommandID: m.commandID,
Content: m.content,
Args: args,
})
}
// Otherwise, move to the next input
m.inputs[m.focusIndex].Blur()
m.focusIndex++
m.inputs[m.focusIndex].Focus()
m.inputs[m.focusIndex].PromptStyle = m.inputs[m.focusIndex].PromptStyle.Foreground(t.Primary())
m.inputs[m.focusIndex].TextStyle = m.inputs[m.focusIndex].TextStyle.Foreground(t.Primary())
case key.Matches(msg, key.NewBinding(key.WithKeys("tab"))):
// Move to the next input
m.inputs[m.focusIndex].Blur()
m.focusIndex = (m.focusIndex + 1) % len(m.inputs)
m.inputs[m.focusIndex].Focus()
m.inputs[m.focusIndex].PromptStyle = m.inputs[m.focusIndex].PromptStyle.Foreground(t.Primary())
m.inputs[m.focusIndex].TextStyle = m.inputs[m.focusIndex].TextStyle.Foreground(t.Primary())
case key.Matches(msg, key.NewBinding(key.WithKeys("shift+tab"))):
// Move to the previous input
m.inputs[m.focusIndex].Blur()
m.focusIndex = (m.focusIndex - 1 + len(m.inputs)) % len(m.inputs)
m.inputs[m.focusIndex].Focus()
m.inputs[m.focusIndex].PromptStyle = m.inputs[m.focusIndex].PromptStyle.Foreground(t.Primary())
m.inputs[m.focusIndex].TextStyle = m.inputs[m.focusIndex].TextStyle.Foreground(t.Primary())
}
case tea.WindowSizeMsg:
m.width = msg.Width
m.height = msg.Height
}
// Update the focused input
var cmd tea.Cmd
m.inputs[m.focusIndex], cmd = m.inputs[m.focusIndex].Update(msg)
cmds = append(cmds, cmd)
return m, tea.Batch(cmds...)
}
// View implements tea.Model.
func (m MultiArgumentsDialogCmp) View() string {
t := theme.CurrentTheme()
baseStyle := styles.BaseStyle()
// Calculate width needed for content
maxWidth := 60 // Width for explanation text
title := lipgloss.NewStyle().
Foreground(t.Primary()).
Bold(true).
Width(maxWidth).
Padding(0, 1).
Background(t.Background()).
Render("Command Arguments")
explanation := lipgloss.NewStyle().
Foreground(t.Text()).
Width(maxWidth).
Padding(0, 1).
Background(t.Background()).
Render("This command requires multiple arguments. Please enter values for each:")
// Create input fields for each argument
inputFields := make([]string, len(m.inputs))
for i, input := range m.inputs {
// Highlight the label of the focused input
labelStyle := lipgloss.NewStyle().
Width(maxWidth).
Padding(1, 1, 0, 1).
Background(t.Background())
if i == m.focusIndex {
labelStyle = labelStyle.Foreground(t.Primary()).Bold(true)
} else {
labelStyle = labelStyle.Foreground(t.TextMuted())
}
label := labelStyle.Render(m.argNames[i] + ":")
field := lipgloss.NewStyle().
Foreground(t.Text()).
Width(maxWidth).
Padding(0, 1).
Background(t.Background()).
Render(input.View())
inputFields[i] = lipgloss.JoinVertical(lipgloss.Left, label, field)
}
maxWidth = min(maxWidth, m.width-10)
// Join all elements vertically
elements := []string{title, explanation}
elements = append(elements, inputFields...)
content := lipgloss.JoinVertical(
lipgloss.Left,
elements...,
)
return baseStyle.Padding(1, 2).
Border(lipgloss.RoundedBorder()).
BorderBackground(t.Background()).
BorderForeground(t.TextMuted()).
Background(t.Background()).
Width(lipgloss.Width(content) + 4).
Render(content)
}
// SetSize sets the size of the component.
func (m *MultiArgumentsDialogCmp) SetSize(width, height int) {
m.width = width
m.height = height
}
// Bindings implements layout.Bindings.
func (m MultiArgumentsDialogCmp) Bindings() []key.Binding {
return m.keys.ShortHelp()
}

View File

@@ -1,180 +0,0 @@
package dialog
import (
"github.com/charmbracelet/bubbles/key"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
utilComponents "github.com/sst/opencode/internal/components/util"
"github.com/sst/opencode/internal/layout"
"github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme"
"github.com/sst/opencode/internal/util"
)
// Command represents a command that can be executed
type Command struct {
ID string
Title string
Description string
Handler func(cmd Command) tea.Cmd
}
func (ci Command) Render(selected bool, width int) string {
t := theme.CurrentTheme()
baseStyle := styles.BaseStyle()
descStyle := baseStyle.Width(width).Foreground(t.TextMuted())
itemStyle := baseStyle.Width(width).
Foreground(t.Text()).
Background(t.Background())
if selected {
itemStyle = itemStyle.
Background(t.Primary()).
Foreground(t.Background()).
Bold(true)
descStyle = descStyle.
Background(t.Primary()).
Foreground(t.Background())
}
title := itemStyle.Padding(0, 1).Render(ci.Title)
if ci.Description != "" {
description := descStyle.Padding(0, 1).Render(ci.Description)
return lipgloss.JoinVertical(lipgloss.Left, title, description)
}
return title
}
// CommandSelectedMsg is sent when a command is selected
type CommandSelectedMsg struct {
Command Command
}
// CloseCommandDialogMsg is sent when the command dialog is closed
type CloseCommandDialogMsg struct{}
// CommandDialog interface for the command selection dialog
type CommandDialog interface {
tea.Model
layout.Bindings
SetCommands(commands []Command)
}
type commandDialogCmp struct {
listView utilComponents.SimpleList[Command]
width int
height int
}
type commandKeyMap struct {
Enter key.Binding
Escape key.Binding
}
var commandKeys = commandKeyMap{
Enter: key.NewBinding(
key.WithKeys("enter"),
key.WithHelp("enter", "select command"),
),
Escape: key.NewBinding(
key.WithKeys("esc"),
key.WithHelp("esc", "close"),
),
}
func (c *commandDialogCmp) Init() tea.Cmd {
return c.listView.Init()
}
func (c *commandDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmds []tea.Cmd
switch msg := msg.(type) {
case tea.KeyMsg:
switch {
case key.Matches(msg, commandKeys.Enter):
selectedItem, idx := c.listView.GetSelectedItem()
if idx != -1 {
return c, util.CmdHandler(CommandSelectedMsg{
Command: selectedItem,
})
}
case key.Matches(msg, commandKeys.Escape):
return c, util.CmdHandler(CloseCommandDialogMsg{})
}
case tea.WindowSizeMsg:
c.width = msg.Width
c.height = msg.Height
}
u, cmd := c.listView.Update(msg)
c.listView = u.(utilComponents.SimpleList[Command])
cmds = append(cmds, cmd)
return c, tea.Batch(cmds...)
}
func (c *commandDialogCmp) View() string {
t := theme.CurrentTheme()
baseStyle := styles.BaseStyle()
maxWidth := 40
commands := c.listView.GetItems()
for _, cmd := range commands {
if len(cmd.Title) > maxWidth-4 {
maxWidth = len(cmd.Title) + 4
}
if cmd.Description != "" {
if len(cmd.Description) > maxWidth-4 {
maxWidth = len(cmd.Description) + 4
}
}
}
c.listView.SetMaxWidth(maxWidth)
title := baseStyle.
Foreground(t.Primary()).
Bold(true).
Width(maxWidth).
Padding(0, 1).
Render("Commands")
content := lipgloss.JoinVertical(
lipgloss.Left,
title,
baseStyle.Width(maxWidth).Render(""),
baseStyle.Width(maxWidth).Render(c.listView.View()),
baseStyle.Width(maxWidth).Render(""),
)
return baseStyle.Padding(1, 2).
Border(lipgloss.RoundedBorder()).
BorderBackground(t.Background()).
BorderForeground(t.TextMuted()).
Width(lipgloss.Width(content) + 4).
Render(content)
}
func (c *commandDialogCmp) BindingKeys() []key.Binding {
return layout.KeyMapToSlice(commandKeys)
}
func (c *commandDialogCmp) SetCommands(commands []Command) {
c.listView.SetItems(commands)
}
// NewCommandDialogCmp creates a new command selection dialog
func NewCommandDialogCmp() CommandDialog {
listView := utilComponents.NewSimpleList[Command](
[]Command{},
10,
"No commands available",
true,
)
return &commandDialogCmp{
listView: listView,
}
}

View File

@@ -1,13 +1,13 @@
package dialog
import (
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/textarea"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/sst/opencode/internal/status"
utilComponents "github.com/sst/opencode/internal/components/util"
"github.com/charmbracelet/bubbles/v2/key"
"github.com/charmbracelet/bubbles/v2/textarea"
tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/lipgloss/v2"
"github.com/sst/opencode/internal/components/list"
"github.com/sst/opencode/internal/layout"
"github.com/sst/opencode/internal/status"
"github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme"
"github.com/sst/opencode/internal/util"
@@ -20,7 +20,7 @@ type CompletionItem struct {
}
type CompletionItemI interface {
utilComponents.SimpleListItem
list.ListItem
GetValue() string
DisplayValue() string
}
@@ -30,18 +30,18 @@ func (ci *CompletionItem) Render(selected bool, width int) string {
baseStyle := styles.BaseStyle()
itemStyle := baseStyle.
Background(t.BackgroundElement()).
Width(width).
Padding(0, 1)
if selected {
itemStyle = itemStyle.
Background(t.Background()).
Foreground(t.Primary()).
Bold(true)
}
title := itemStyle.Render(
ci.GetValue(),
ci.DisplayValue(),
)
return title
@@ -68,6 +68,7 @@ type CompletionProvider interface {
type CompletionSelectedMsg struct {
SearchString string
CompletionValue string
IsCommand bool
}
type CompletionDialogCompleteItemMsg struct {
@@ -77,18 +78,19 @@ type CompletionDialogCompleteItemMsg struct {
type CompletionDialogCloseMsg struct{}
type CompletionDialog interface {
tea.Model
layout.Bindings
layout.ModelWithView
SetWidth(width int)
IsEmpty() bool
SetProvider(provider CompletionProvider)
}
type completionDialogCmp struct {
type completionDialogComponent struct {
query string
completionProvider CompletionProvider
width int
height int
pseudoSearchTextArea textarea.Model
listView utilComponents.SimpleList[CompletionItemI]
list list.List[CompletionItemI]
}
type completionDialogKeyMap struct {
@@ -105,42 +107,44 @@ var completionDialogKeys = completionDialogKeyMap{
),
}
func (c *completionDialogCmp) Init() tea.Cmd {
func (c *completionDialogComponent) Init() tea.Cmd {
return nil
}
func (c *completionDialogCmp) complete(item CompletionItemI) tea.Cmd {
func (c *completionDialogComponent) complete(item CompletionItemI) tea.Cmd {
value := c.pseudoSearchTextArea.Value()
if value == "" {
return nil
}
// Check if this is a command completion
isCommand := c.completionProvider.GetId() == "commands"
return tea.Batch(
util.CmdHandler(CompletionSelectedMsg{
SearchString: value,
CompletionValue: item.GetValue(),
IsCommand: isCommand,
}),
c.close(),
)
}
func (c *completionDialogCmp) close() tea.Cmd {
c.listView.SetItems([]CompletionItemI{})
func (c *completionDialogComponent) close() tea.Cmd {
c.list.SetItems([]CompletionItemI{})
c.pseudoSearchTextArea.Reset()
c.pseudoSearchTextArea.Blur()
return util.CmdHandler(CompletionDialogCloseMsg{})
}
func (c *completionDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
func (c *completionDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmds []tea.Cmd
switch msg := msg.(type) {
case tea.KeyMsg:
if c.pseudoSearchTextArea.Focused() {
if !key.Matches(msg, completionDialogKeys.Complete) {
var cmd tea.Cmd
c.pseudoSearchTextArea, cmd = c.pseudoSearchTextArea.Update(msg)
cmds = append(cmds, cmd)
@@ -157,26 +161,23 @@ func (c *completionDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
status.Error(err.Error())
}
c.listView.SetItems(items)
c.list.SetItems(items)
c.query = query
}
u, cmd := c.listView.Update(msg)
c.listView = u.(utilComponents.SimpleList[CompletionItemI])
u, cmd := c.list.Update(msg)
c.list = u.(list.List[CompletionItemI])
cmds = append(cmds, cmd)
}
switch {
case key.Matches(msg, completionDialogKeys.Complete):
item, i := c.listView.GetSelectedItem()
item, i := c.list.GetSelectedItem()
if i == -1 {
return c, nil
}
cmd := c.complete(item)
return c, cmd
return c, c.complete(item)
case key.Matches(msg, completionDialogKeys.Cancel):
// Only close on backspace when there are no characters left
if msg.String() != "backspace" || len(c.pseudoSearchTextArea.Value()) <= 0 {
@@ -191,7 +192,7 @@ func (c *completionDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
status.Error(err.Error())
}
c.listView.SetItems(items)
c.list.SetItems(items)
c.pseudoSearchTextArea.SetValue(msg.String())
return c, c.pseudoSearchTextArea.Focus()
}
@@ -203,13 +204,12 @@ func (c *completionDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return c, tea.Batch(cmds...)
}
func (c *completionDialogCmp) View() string {
func (c *completionDialogComponent) View() string {
t := theme.CurrentTheme()
baseStyle := styles.BaseStyle()
maxWidth := 40
completions := c.listView.GetItems()
completions := c.list.GetItems()
for _, cmd := range completions {
title := cmd.DisplayValue()
@@ -218,28 +218,40 @@ func (c *completionDialogCmp) View() string {
}
}
c.listView.SetMaxWidth(maxWidth)
c.list.SetMaxWidth(maxWidth)
return baseStyle.Padding(0, 0).
Border(lipgloss.NormalBorder()).
Background(t.BackgroundElement()).
Border(lipgloss.ThickBorder()).
BorderTop(false).
BorderBottom(false).
BorderRight(false).
BorderLeft(false).
BorderBackground(t.Background()).
BorderForeground(t.TextMuted()).
BorderRight(true).
BorderLeft(true).
BorderForeground(t.BackgroundSubtle()).
Width(c.width).
Render(c.listView.View())
Render(c.list.View())
}
func (c *completionDialogCmp) SetWidth(width int) {
func (c *completionDialogComponent) SetWidth(width int) {
c.width = width
}
func (c *completionDialogCmp) BindingKeys() []key.Binding {
return layout.KeyMapToSlice(completionDialogKeys)
func (c *completionDialogComponent) IsEmpty() bool {
return c.list.IsEmpty()
}
func NewCompletionDialogCmp(completionProvider CompletionProvider) CompletionDialog {
func (c *completionDialogComponent) SetProvider(provider CompletionProvider) {
if c.completionProvider.GetId() != provider.GetId() {
c.completionProvider = provider
items, err := provider.GetChildEntries("")
if err != nil {
status.Error(err.Error())
}
c.list.SetItems(items)
}
}
func NewCompletionDialogComponent(completionProvider CompletionProvider) CompletionDialog {
ti := textarea.New()
items, err := completionProvider.GetChildEntries("")
@@ -247,17 +259,17 @@ func NewCompletionDialogCmp(completionProvider CompletionProvider) CompletionDia
status.Error(err.Error())
}
li := utilComponents.NewSimpleList(
li := list.NewListComponent(
items,
7,
"No file matches found",
"No matches",
false,
)
return &completionDialogCmp{
return &completionDialogComponent{
query: "",
completionProvider: completionProvider,
pseudoSearchTextArea: ti,
listView: li,
list: li,
}
}

View File

@@ -1,155 +0,0 @@
package dialog
import (
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
tea "github.com/charmbracelet/bubbletea"
"github.com/sst/opencode/internal/app"
"github.com/sst/opencode/internal/util"
)
// Command prefix constants
const (
UserCommandPrefix = "user:"
ProjectCommandPrefix = "project:"
)
// namedArgPattern is a regex pattern to find named arguments in the format $NAME
var namedArgPattern = regexp.MustCompile(`\$([A-Z][A-Z0-9_]*)`)
// LoadCustomCommands loads custom commands from both XDG_CONFIG_HOME and project data directory
func LoadCustomCommands() ([]Command, error) {
var commands []Command
homeCommandsDir := filepath.Join(app.Info.Path.Config, "commands")
homeCommands, err := loadCommandsFromDir(homeCommandsDir, UserCommandPrefix)
if err != nil {
// Log error but continue - we'll still try to load other commands
fmt.Printf("Warning: failed to load home commands: %v\n", err)
} else {
commands = append(commands, homeCommands...)
}
projectCommandsDir := filepath.Join(app.Info.Path.Root, ".opencode", "commands")
projectCommands, err := loadCommandsFromDir(projectCommandsDir, ProjectCommandPrefix)
if err != nil {
// Log error but return what we have so far
fmt.Printf("Warning: failed to load project commands: %v\n", err)
} else {
commands = append(commands, projectCommands...)
}
return commands, nil
}
// loadCommandsFromDir loads commands from a specific directory with the given prefix
func loadCommandsFromDir(commandsDir string, prefix string) ([]Command, error) {
// Check if the commands directory exists
if _, err := os.Stat(commandsDir); os.IsNotExist(err) {
// Create the commands directory if it doesn't exist
if err := os.MkdirAll(commandsDir, 0755); err != nil {
return nil, fmt.Errorf("failed to create commands directory %s: %w", commandsDir, err)
}
// Return empty list since we just created the directory
return []Command{}, nil
}
var commands []Command
// Walk through the commands directory and load all .md files
err := filepath.Walk(commandsDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// Skip directories
if info.IsDir() {
return nil
}
// Only process markdown files
if !strings.HasSuffix(strings.ToLower(info.Name()), ".md") {
return nil
}
// Read the file content
content, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("failed to read command file %s: %w", path, err)
}
// Get the command ID from the file name without the .md extension
commandID := strings.TrimSuffix(info.Name(), filepath.Ext(info.Name()))
// Get relative path from commands directory
relPath, err := filepath.Rel(commandsDir, path)
if err != nil {
return fmt.Errorf("failed to get relative path for %s: %w", path, err)
}
// Create the command ID from the relative path
// Replace directory separators with colons
commandIDPath := strings.ReplaceAll(filepath.Dir(relPath), string(filepath.Separator), ":")
if commandIDPath != "." {
commandID = commandIDPath + ":" + commandID
}
// Create a command
command := Command{
ID: prefix + commandID,
Title: prefix + commandID,
Description: fmt.Sprintf("Custom command from %s", relPath),
Handler: func(cmd Command) tea.Cmd {
commandContent := string(content)
// Check for named arguments
matches := namedArgPattern.FindAllStringSubmatch(commandContent, -1)
if len(matches) > 0 {
// Extract unique argument names
argNames := make([]string, 0)
argMap := make(map[string]bool)
for _, match := range matches {
argName := match[1] // Group 1 is the name without $
if !argMap[argName] {
argMap[argName] = true
argNames = append(argNames, argName)
}
}
// Show multi-arguments dialog for all named arguments
return util.CmdHandler(ShowMultiArgumentsDialogMsg{
CommandID: cmd.ID,
Content: commandContent,
ArgNames: argNames,
})
}
// No arguments needed, run command directly
return util.CmdHandler(CommandRunCustomMsg{
Content: commandContent,
Args: nil, // No arguments
})
},
}
commands = append(commands, command)
return nil
})
if err != nil {
return nil, fmt.Errorf("failed to load custom commands from %s: %w", commandsDir, err)
}
return commands, nil
}
// CommandRunCustomMsg is sent when a custom command is executed
type CommandRunCustomMsg struct {
Content string
Args map[string]string // Map of argument names to values
}

View File

@@ -1,106 +0,0 @@
package dialog
import (
"testing"
"regexp"
)
func TestNamedArgPattern(t *testing.T) {
testCases := []struct {
input string
expected []string
}{
{
input: "This is a test with $ARGUMENTS placeholder",
expected: []string{"ARGUMENTS"},
},
{
input: "This is a test with $FOO and $BAR placeholders",
expected: []string{"FOO", "BAR"},
},
{
input: "This is a test with $FOO_BAR and $BAZ123 placeholders",
expected: []string{"FOO_BAR", "BAZ123"},
},
{
input: "This is a test with no placeholders",
expected: []string{},
},
{
input: "This is a test with $FOO appearing twice: $FOO",
expected: []string{"FOO"},
},
{
input: "This is a test with $1INVALID placeholder",
expected: []string{},
},
}
for _, tc := range testCases {
matches := namedArgPattern.FindAllStringSubmatch(tc.input, -1)
// Extract unique argument names
argNames := make([]string, 0)
argMap := make(map[string]bool)
for _, match := range matches {
argName := match[1] // Group 1 is the name without $
if !argMap[argName] {
argMap[argName] = true
argNames = append(argNames, argName)
}
}
// Check if we got the expected number of arguments
if len(argNames) != len(tc.expected) {
t.Errorf("Expected %d arguments, got %d for input: %s", len(tc.expected), len(argNames), tc.input)
continue
}
// Check if we got the expected argument names
for _, expectedArg := range tc.expected {
found := false
for _, actualArg := range argNames {
if actualArg == expectedArg {
found = true
break
}
}
if !found {
t.Errorf("Expected argument %s not found in %v for input: %s", expectedArg, argNames, tc.input)
}
}
}
}
func TestRegexPattern(t *testing.T) {
pattern := regexp.MustCompile(`\$([A-Z][A-Z0-9_]*)`)
validMatches := []string{
"$FOO",
"$BAR",
"$FOO_BAR",
"$BAZ123",
"$ARGUMENTS",
}
invalidMatches := []string{
"$foo",
"$1BAR",
"$_FOO",
"FOO",
"$",
}
for _, valid := range validMatches {
if !pattern.MatchString(valid) {
t.Errorf("Expected %s to match, but it didn't", valid)
}
}
for _, invalid := range invalidMatches {
if pattern.MatchString(invalid) {
t.Errorf("Expected %s not to match, but it did", invalid)
}
}
}

View File

@@ -1,485 +0,0 @@
package dialog
import (
"fmt"
"net/http"
"os"
"path/filepath"
"sort"
"strings"
"time"
"log/slog"
"github.com/atotto/clipboard"
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/textinput"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/sst/opencode/internal/app"
"github.com/sst/opencode/internal/image"
"github.com/sst/opencode/internal/status"
"github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme"
"github.com/sst/opencode/internal/util"
)
const (
maxAttachmentSize = int64(5 * 1024 * 1024) // 5MB
downArrow = "down"
upArrow = "up"
)
type FilePrickerKeyMap struct {
Enter key.Binding
Down key.Binding
Up key.Binding
Forward key.Binding
Backward key.Binding
OpenFilePicker key.Binding
Esc key.Binding
InsertCWD key.Binding
Paste key.Binding
}
var filePickerKeyMap = FilePrickerKeyMap{
Enter: key.NewBinding(
key.WithKeys("enter"),
key.WithHelp("enter", "select file/enter directory"),
),
Down: key.NewBinding(
key.WithKeys("j", downArrow),
key.WithHelp("↓/j", "down"),
),
Up: key.NewBinding(
key.WithKeys("k", upArrow),
key.WithHelp("↑/k", "up"),
),
Forward: key.NewBinding(
key.WithKeys("l"),
key.WithHelp("l", "enter directory"),
),
Backward: key.NewBinding(
key.WithKeys("h", "backspace"),
key.WithHelp("h/backspace", "go back"),
),
OpenFilePicker: key.NewBinding(
key.WithKeys("ctrl+f"),
key.WithHelp("ctrl+f", "open file picker"),
),
Esc: key.NewBinding(
key.WithKeys("esc"),
key.WithHelp("esc", "close/exit"),
),
InsertCWD: key.NewBinding(
key.WithKeys("i"),
key.WithHelp("i", "manual path input"),
),
Paste: key.NewBinding(
key.WithKeys("ctrl+v"),
key.WithHelp("ctrl+v", "paste file/directory path"),
),
}
type filepickerCmp struct {
basePath string
width int
height int
cursor int
err error
cursorChain stack
viewport viewport.Model
dirs []os.DirEntry
cwdDetails *DirNode
selectedFile string
cwd textinput.Model
ShowFilePicker bool
app *app.App
}
type DirNode struct {
parent *DirNode
child *DirNode
directory string
}
type stack []int
func (s stack) Push(v int) stack {
return append(s, v)
}
func (s stack) Pop() (stack, int) {
l := len(s)
return s[:l-1], s[l-1]
}
type AttachmentAddedMsg struct {
Attachment app.Attachment
}
func (f *filepickerCmp) Init() tea.Cmd {
return nil
}
func (f *filepickerCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
switch msg := msg.(type) {
case tea.WindowSizeMsg:
f.width = 60
f.height = 20
f.viewport.Width = 80
f.viewport.Height = 22
f.cursor = 0
f.getCurrentFileBelowCursor()
case tea.KeyMsg:
if f.cwd.Focused() {
f.cwd, cmd = f.cwd.Update(msg)
}
switch {
case key.Matches(msg, filePickerKeyMap.InsertCWD):
f.cwd.Focus()
return f, cmd
case key.Matches(msg, filePickerKeyMap.Esc):
if f.cwd.Focused() {
f.cwd.Blur()
}
case key.Matches(msg, filePickerKeyMap.Down):
if !f.cwd.Focused() || msg.String() == downArrow {
if f.cursor < len(f.dirs)-1 {
f.cursor++
f.getCurrentFileBelowCursor()
}
}
case key.Matches(msg, filePickerKeyMap.Up):
if !f.cwd.Focused() || msg.String() == upArrow {
if f.cursor > 0 {
f.cursor--
f.getCurrentFileBelowCursor()
}
}
case key.Matches(msg, filePickerKeyMap.Enter):
var path string
var isPathDir bool
if f.cwd.Focused() {
path = f.cwd.Value()
fileInfo, err := os.Stat(path)
if err != nil {
status.Error("Invalid path")
return f, cmd
}
isPathDir = fileInfo.IsDir()
} else {
path = filepath.Join(f.cwdDetails.directory, "/", f.dirs[f.cursor].Name())
isPathDir = f.dirs[f.cursor].IsDir()
}
if isPathDir {
newWorkingDir := DirNode{parent: f.cwdDetails, directory: path}
f.cwdDetails.child = &newWorkingDir
f.cwdDetails = f.cwdDetails.child
f.cursorChain = f.cursorChain.Push(f.cursor)
f.dirs = readDir(f.cwdDetails.directory, false)
f.cursor = 0
f.cwd.SetValue(f.cwdDetails.directory)
f.getCurrentFileBelowCursor()
} else {
f.selectedFile = path
return f.addAttachmentToMessage()
}
case key.Matches(msg, filePickerKeyMap.Esc):
if !f.cwd.Focused() {
f.cursorChain = make(stack, 0)
f.cursor = 0
} else {
f.cwd.Blur()
}
case key.Matches(msg, filePickerKeyMap.Forward):
if !f.cwd.Focused() {
if f.dirs[f.cursor].IsDir() {
path := filepath.Join(f.cwdDetails.directory, "/", f.dirs[f.cursor].Name())
newWorkingDir := DirNode{parent: f.cwdDetails, directory: path}
f.cwdDetails.child = &newWorkingDir
f.cwdDetails = f.cwdDetails.child
f.cursorChain = f.cursorChain.Push(f.cursor)
f.dirs = readDir(f.cwdDetails.directory, false)
f.cursor = 0
f.cwd.SetValue(f.cwdDetails.directory)
f.getCurrentFileBelowCursor()
}
}
case key.Matches(msg, filePickerKeyMap.Backward):
if !f.cwd.Focused() {
if len(f.cursorChain) != 0 && f.cwdDetails.parent != nil {
f.cursorChain, f.cursor = f.cursorChain.Pop()
f.cwdDetails = f.cwdDetails.parent
f.cwdDetails.child = nil
f.dirs = readDir(f.cwdDetails.directory, false)
f.cwd.SetValue(f.cwdDetails.directory)
f.getCurrentFileBelowCursor()
}
}
case key.Matches(msg, filePickerKeyMap.Paste):
if f.cwd.Focused() {
val, err := clipboard.ReadAll()
if err != nil {
slog.Error("failed to read clipboard")
return f, cmd
}
f.cwd.SetValue(f.cwd.Value() + val)
}
case key.Matches(msg, filePickerKeyMap.OpenFilePicker):
f.dirs = readDir(f.cwdDetails.directory, false)
f.cursor = 0
f.getCurrentFileBelowCursor()
}
}
return f, cmd
}
func (f *filepickerCmp) addAttachmentToMessage() (tea.Model, tea.Cmd) {
// modeInfo := GetSelectedModel(config.Get())
// if !modeInfo.SupportsAttachments {
// status.Error(fmt.Sprintf("Model %s doesn't support attachments", modeInfo.Name))
// return f, nil
// }
selectedFilePath := f.selectedFile
if !isExtSupported(selectedFilePath) {
status.Error("Unsupported file")
return f, nil
}
isFileLarge, err := image.ValidateFileSize(selectedFilePath, maxAttachmentSize)
if err != nil {
status.Error("unable to read the image")
return f, nil
}
if isFileLarge {
status.Error("file too large, max 5MB")
return f, nil
}
content, err := os.ReadFile(selectedFilePath)
if err != nil {
status.Error("Unable read selected file")
return f, nil
}
mimeBufferSize := min(512, len(content))
mimeType := http.DetectContentType(content[:mimeBufferSize])
fileName := filepath.Base(selectedFilePath)
attachment := app.Attachment{FilePath: selectedFilePath, FileName: fileName, MimeType: mimeType, Content: content}
f.selectedFile = ""
return f, util.CmdHandler(AttachmentAddedMsg{attachment})
}
func (f *filepickerCmp) View() string {
t := theme.CurrentTheme()
const maxVisibleDirs = 20
const maxWidth = 80
adjustedWidth := maxWidth
for _, file := range f.dirs {
if len(file.Name()) > adjustedWidth-4 { // Account for padding
adjustedWidth = len(file.Name()) + 4
}
}
adjustedWidth = max(30, min(adjustedWidth, f.width-15)) + 1
files := make([]string, 0, maxVisibleDirs)
startIdx := 0
if len(f.dirs) > maxVisibleDirs {
halfVisible := maxVisibleDirs / 2
if f.cursor >= halfVisible && f.cursor < len(f.dirs)-halfVisible {
startIdx = f.cursor - halfVisible
} else if f.cursor >= len(f.dirs)-halfVisible {
startIdx = len(f.dirs) - maxVisibleDirs
}
}
endIdx := min(startIdx+maxVisibleDirs, len(f.dirs))
for i := startIdx; i < endIdx; i++ {
file := f.dirs[i]
itemStyle := styles.BaseStyle().Width(adjustedWidth)
if i == f.cursor {
itemStyle = itemStyle.
Background(t.Primary()).
Foreground(t.Background()).
Bold(true)
}
filename := file.Name()
if len(filename) > adjustedWidth-4 {
filename = filename[:adjustedWidth-7] + "..."
}
if file.IsDir() {
filename = filename + "/"
}
files = append(files, itemStyle.Padding(0, 1).Render(filename))
}
// Pad to always show exactly 21 lines
for len(files) < maxVisibleDirs {
files = append(files, styles.BaseStyle().Width(adjustedWidth).Render(""))
}
currentPath := styles.BaseStyle().
Height(1).
Width(adjustedWidth).
Render(f.cwd.View())
viewportstyle := lipgloss.NewStyle().
Width(f.viewport.Width).
Background(t.Background()).
Border(lipgloss.RoundedBorder()).
BorderForeground(t.TextMuted()).
BorderBackground(t.Background()).
Padding(2).
Render(f.viewport.View())
var insertExitText string
if f.IsCWDFocused() {
insertExitText = "Press esc to exit typing path"
} else {
insertExitText = "Press i to start typing path"
}
content := lipgloss.JoinVertical(
lipgloss.Left,
currentPath,
styles.BaseStyle().Width(adjustedWidth).Render(""),
styles.BaseStyle().Width(adjustedWidth).Render(lipgloss.JoinVertical(lipgloss.Left, files...)),
styles.BaseStyle().Width(adjustedWidth).Render(""),
styles.BaseStyle().Foreground(t.TextMuted()).Width(adjustedWidth).Render(insertExitText),
)
f.cwd.SetValue(f.cwd.Value())
contentStyle := styles.BaseStyle().Padding(1, 2).
Border(lipgloss.RoundedBorder()).
BorderBackground(t.Background()).
BorderForeground(t.TextMuted()).
Width(lipgloss.Width(content) + 4)
return lipgloss.JoinHorizontal(lipgloss.Center, contentStyle.Render(content), viewportstyle)
}
type FilepickerCmp interface {
tea.Model
ToggleFilepicker(showFilepicker bool)
IsCWDFocused() bool
}
func (f *filepickerCmp) ToggleFilepicker(showFilepicker bool) {
f.ShowFilePicker = showFilepicker
}
func (f *filepickerCmp) IsCWDFocused() bool {
return f.cwd.Focused()
}
func NewFilepickerCmp(app *app.App) FilepickerCmp {
homepath, err := os.UserHomeDir()
if err != nil {
slog.Error("error loading user files")
return nil
}
baseDir := DirNode{parent: nil, directory: homepath}
dirs := readDir(homepath, false)
viewport := viewport.New(0, 0)
currentDirectory := textinput.New()
currentDirectory.CharLimit = 200
currentDirectory.Width = 44
currentDirectory.Cursor.Blink = true
currentDirectory.SetValue(baseDir.directory)
return &filepickerCmp{cwdDetails: &baseDir, dirs: dirs, cursorChain: make(stack, 0), viewport: viewport, cwd: currentDirectory, app: app}
}
func (f *filepickerCmp) getCurrentFileBelowCursor() {
if len(f.dirs) == 0 || f.cursor < 0 || f.cursor >= len(f.dirs) {
slog.Error(fmt.Sprintf("Invalid cursor position. Dirs length: %d, Cursor: %d", len(f.dirs), f.cursor))
f.viewport.SetContent("Preview unavailable")
return
}
dir := f.dirs[f.cursor]
filename := dir.Name()
if !dir.IsDir() && isExtSupported(filename) {
fullPath := f.cwdDetails.directory + "/" + dir.Name()
go func() {
imageString, err := image.ImagePreview(f.viewport.Width-4, fullPath)
if err != nil {
slog.Error(err.Error())
f.viewport.SetContent("Preview unavailable")
return
}
f.viewport.SetContent(imageString)
}()
} else {
f.viewport.SetContent("Preview unavailable")
}
}
func readDir(path string, showHidden bool) []os.DirEntry {
slog.Info(fmt.Sprintf("Reading directory: %s", path))
entriesChan := make(chan []os.DirEntry, 1)
errChan := make(chan error, 1)
go func() {
dirEntries, err := os.ReadDir(path)
if err != nil {
status.Error(err.Error())
errChan <- err
return
}
entriesChan <- dirEntries
}()
select {
case dirEntries := <-entriesChan:
sort.Slice(dirEntries, func(i, j int) bool {
if dirEntries[i].IsDir() == dirEntries[j].IsDir() {
return dirEntries[i].Name() < dirEntries[j].Name()
}
return dirEntries[i].IsDir()
})
if showHidden {
return dirEntries
}
var sanitizedDirEntries []os.DirEntry
for _, dirEntry := range dirEntries {
isHidden, _ := IsHidden(dirEntry.Name())
if !isHidden {
if dirEntry.IsDir() || isExtSupported(dirEntry.Name()) {
sanitizedDirEntries = append(sanitizedDirEntries, dirEntry)
}
}
}
return sanitizedDirEntries
case <-errChan:
status.Error(fmt.Sprintf("Error reading directory %s", path))
return []os.DirEntry{}
case <-time.After(5 * time.Second):
status.Error(fmt.Sprintf("Timeout reading directory %s", path))
return []os.DirEntry{}
}
}
func IsHidden(file string) (bool, error) {
return strings.HasPrefix(file, "."), nil
}
func isExtSupported(path string) bool {
ext := strings.ToLower(filepath.Ext(path))
return (ext == ".jpg" || ext == ".jpeg" || ext == ".webp" || ext == ".png")
}

View File

@@ -3,198 +3,98 @@ package dialog
import (
"strings"
"github.com/charmbracelet/bubbles/key"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/sst/opencode/internal/styles"
"github.com/charmbracelet/bubbles/v2/key"
tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/lipgloss/v2"
"github.com/sst/opencode/internal/components/modal"
"github.com/sst/opencode/internal/layout"
"github.com/sst/opencode/internal/theme"
)
type helpCmp struct {
width int
height int
keys []key.Binding
type helpDialog struct {
width int
height int
modal *modal.Modal
bindings []key.Binding
}
func (h *helpCmp) Init() tea.Cmd {
// func (i bindingItem) Render(selected bool, width int) string {
// t := theme.CurrentTheme()
// baseStyle := styles.BaseStyle().
// Width(width - 2).
// Background(t.BackgroundElement())
//
// if selected {
// baseStyle = baseStyle.
// Background(t.Primary()).
// Foreground(t.BackgroundElement()).
// Bold(true)
// } else {
// baseStyle = baseStyle.
// Foreground(t.Text())
// }
//
// return baseStyle.Padding(0, 1).Render(i.binding.Help().Desc)
// }
func (h *helpDialog) Init() tea.Cmd {
return nil
}
func (h *helpCmp) SetBindings(k []key.Binding) {
h.keys = k
}
func (h *helpCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
func (h *helpDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
h.width = 90
h.width = msg.Width
h.height = msg.Height
}
return h, nil
}
func removeDuplicateBindings(bindings []key.Binding) []key.Binding {
seen := make(map[string]struct{})
result := make([]key.Binding, 0, len(bindings))
// Process bindings in reverse order
for i := len(bindings) - 1; i >= 0; i-- {
b := bindings[i]
k := strings.Join(b.Keys(), " ")
if _, ok := seen[k]; ok {
// duplicate, skip
continue
}
seen[k] = struct{}{}
// Add to the beginning of result to maintain original order
result = append([]key.Binding{b}, result...)
}
return result
}
func (h *helpCmp) render() string {
func (h *helpDialog) View() string {
t := theme.CurrentTheme()
baseStyle := styles.BaseStyle()
helpKeyStyle := styles.Bold().
Background(t.Background()).
keyStyle := lipgloss.NewStyle().
Background(t.BackgroundElement()).
Foreground(t.Text()).
Padding(0, 1, 0, 0)
helpDescStyle := styles.Regular().
Background(t.Background()).
Bold(true)
descStyle := lipgloss.NewStyle().
Background(t.BackgroundElement()).
Foreground(t.TextMuted())
// Compile list of bindings to render
bindings := removeDuplicateBindings(h.keys)
// Enumerate through each group of bindings, populating a series of
// pairs of columns, one for keys, one for descriptions
var (
pairs []string
width int
rows = 12 - 2
)
for i := 0; i < len(bindings); i += rows {
var (
keys []string
descs []string
)
for j := i; j < min(i+rows, len(bindings)); j++ {
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{baseStyle.Render(" ")}
}
maxDescWidth := 0
for _, desc := range descs {
if maxDescWidth < lipgloss.Width(desc) {
maxDescWidth = lipgloss.Width(desc)
}
}
for i := range descs {
remainingWidth := maxDescWidth - lipgloss.Width(descs[i])
if remainingWidth > 0 {
descs[i] = descs[i] + baseStyle.Render(strings.Repeat(" ", remainingWidth))
}
}
maxKeyWidth := 0
for _, key := range keys {
if maxKeyWidth < lipgloss.Width(key) {
maxKeyWidth = lipgloss.Width(key)
}
}
for i := range keys {
remainingWidth := maxKeyWidth - lipgloss.Width(keys[i])
if remainingWidth > 0 {
keys[i] = keys[i] + baseStyle.Render(strings.Repeat(" ", remainingWidth))
lines := []string{}
for _, b := range h.bindings {
content := keyStyle.Render(b.Help().Key)
content += descStyle.Render(" " + b.Help().Desc)
for i, key := range b.Keys() {
if i == 0 {
keyString := " (" + strings.ToUpper(key) + ")"
// space := max(h.width-lipgloss.Width(content)-lipgloss.Width(keyString), 0)
// spacer := strings.Repeat(" ", space)
// content += descStyle.Render(spacer)
content += descStyle.Render(keyString)
}
}
cols = append(cols,
strings.Join(keys, "\n"),
strings.Join(descs, "\n"),
)
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)
if width > h.width-2 {
break
}
pairs = append(pairs, pair)
lines = append(lines, content)
}
// https://github.com/charmbracelet/lipgloss/issues/209
if len(pairs) > 1 {
prefix := pairs[:len(pairs)-1]
lastPair := pairs[len(pairs)-1]
prefix = append(prefix, lipgloss.Place(
lipgloss.Width(lastPair), // width
lipgloss.Height(prefix[0]), // height
lipgloss.Left, // x
lipgloss.Top, // y
lastPair, // content
lipgloss.WithWhitespaceBackground(t.Background()),
))
content := baseStyle.Width(h.width).Render(
lipgloss.JoinHorizontal(
lipgloss.Top,
prefix...,
),
)
return content
return strings.Join(lines, "\n")
}
func (h *helpDialog) Render(background string) string {
return h.modal.Render(h.View(), background)
}
func (h *helpDialog) Close() tea.Cmd {
return nil
}
type HelpDialog interface {
layout.Modal
}
func NewHelpDialog(bindings ...key.Binding) HelpDialog {
return &helpDialog{
bindings: bindings,
modal: modal.New(),
}
// Join pairs of columns and enclose in a border
content := baseStyle.Width(h.width).Render(
lipgloss.JoinHorizontal(
lipgloss.Top,
pairs...,
),
)
return content
}
func (h *helpCmp) View() string {
t := theme.CurrentTheme()
baseStyle := styles.BaseStyle()
content := h.render()
header := baseStyle.
Bold(true).
Width(lipgloss.Width(content)).
Foreground(t.Primary()).
Render("Keyboard Shortcuts")
return baseStyle.Padding(1).
Border(lipgloss.RoundedBorder()).
BorderForeground(t.TextMuted()).
Width(h.width).
BorderBackground(t.Background()).
Render(
lipgloss.JoinVertical(lipgloss.Center,
header,
baseStyle.Render(strings.Repeat(" ", lipgloss.Width(header))),
content,
),
)
}
type HelpCmp interface {
tea.Model
SetBindings([]key.Binding)
}
func NewHelpCmp() HelpCmp {
return &helpCmp{}
}

View File

@@ -1,9 +1,9 @@
package dialog
import (
"github.com/charmbracelet/bubbles/key"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/bubbles/v2/key"
tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/lipgloss/v2"
"github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme"
@@ -173,11 +173,6 @@ func (m *InitDialogCmp) SetSize(width, height int) {
m.height = height
}
// Bindings implements layout.Bindings.
func (m InitDialogCmp) Bindings() []key.Binding {
return m.keys.ShortHelp()
}
// CloseInitDialogMsg is a message that is sent when the init dialog is closed.
type CloseInitDialogMsg struct {
Initialize bool

View File

@@ -7,11 +7,13 @@ import (
"slices"
"strings"
"github.com/charmbracelet/bubbles/key"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/bubbles/v2/key"
tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/lipgloss/v2"
"github.com/sst/opencode/internal/app"
"github.com/sst/opencode/internal/components/modal"
"github.com/sst/opencode/internal/layout"
"github.com/sst/opencode/internal/state"
"github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme"
"github.com/sst/opencode/internal/util"
@@ -19,25 +21,16 @@ import (
)
const (
numVisibleModels = 10
numVisibleModels = 6
maxDialogWidth = 40
)
// CloseModelDialogMsg is sent when a model is selected
type CloseModelDialogMsg struct {
Provider *client.ProviderInfo
Model *client.ProviderModel
}
// ModelDialog interface for the model selection dialog
type ModelDialog interface {
tea.Model
layout.Bindings
SetProviders(providers []client.ProviderInfo)
layout.Modal
}
type modelDialogCmp struct {
type modelDialog struct {
app *app.App
availableProviders []client.ProviderInfo
provider client.ProviderInfo
@@ -48,6 +41,8 @@ type modelDialogCmp struct {
scrollOffset int
hScrollOffset int
hScrollPossible bool
modal *modal.Modal
}
type modelKeyMap struct {
@@ -57,27 +52,23 @@ type modelKeyMap struct {
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.WithKeys("up", "k"),
key.WithHelp("↑", "previous model"),
),
Down: key.NewBinding(
key.WithKeys("down"),
key.WithKeys("down", "j"),
key.WithHelp("↓", "next model"),
),
Left: key.NewBinding(
key.WithKeys("left"),
key.WithKeys("left", "h"),
key.WithHelp("←", "scroll left"),
),
Right: key.NewBinding(
key.WithKeys("right"),
key.WithKeys("right", "l"),
key.WithHelp("→", "scroll right"),
),
Enter: key.NewBinding(
@@ -88,25 +79,9 @@ var modelKeys = modelKeyMap{
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 {
func (m *modelDialog) Init() tea.Cmd {
// cfg := config.Get()
// modelInfo := GetSelectedModel(cfg)
// m.availableProviders = getEnabledProviders(cfg)
@@ -116,40 +91,37 @@ func (m *modelDialogCmp) Init() tea.Cmd {
// m.hScrollOffset = findProviderIndex(m.availableProviders, m.provider)
// m.setupModelsForProvider(m.provider)
m.availableProviders, _ = m.app.ListProviders(context.Background())
m.hScrollOffset = 0
m.hScrollPossible = len(m.availableProviders) > 1
m.provider = m.availableProviders[m.hScrollOffset]
return nil
}
func (m *modelDialogCmp) SetProviders(providers []client.ProviderInfo) {
m.availableProviders = providers
}
func (m *modelDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
func (m *modelDialog) 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):
case key.Matches(msg, modelKeys.Up):
m.moveSelectionUp()
case key.Matches(msg, modelKeys.Down) || key.Matches(msg, modelKeys.J):
case key.Matches(msg, modelKeys.Down):
m.moveSelectionDown()
case key.Matches(msg, modelKeys.Left) || key.Matches(msg, modelKeys.H):
case key.Matches(msg, modelKeys.Left):
if m.hScrollPossible {
m.switchProvider(-1)
}
case key.Matches(msg, modelKeys.Right) || key.Matches(msg, modelKeys.L):
case key.Matches(msg, modelKeys.Right):
if m.hScrollPossible {
m.switchProvider(1)
}
case key.Matches(msg, modelKeys.Enter):
models := m.models()
return m, util.CmdHandler(CloseModelDialogMsg{Provider: &m.provider, Model: &models[m.selectedIdx]})
return m, tea.Sequence(
util.CmdHandler(modal.CloseModalMsg{}),
util.CmdHandler(
state.ModelSelectedMsg{
Provider: m.provider,
Model: models[m.selectedIdx],
}),
)
case key.Matches(msg, modelKeys.Escape):
return m, util.CmdHandler(CloseModelDialogMsg{})
return m, util.CmdHandler(modal.CloseModalMsg{})
}
case tea.WindowSizeMsg:
m.width = msg.Width
@@ -159,15 +131,15 @@ func (m *modelDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil
}
func (m *modelDialogCmp) models() []client.ProviderModel {
models := slices.SortedFunc(maps.Values(m.provider.Models), func(a, b client.ProviderModel) int {
return strings.Compare(*a.Name, *b.Name)
func (m *modelDialog) models() []client.ModelInfo {
models := slices.SortedFunc(maps.Values(m.provider.Models), func(a, b client.ModelInfo) int {
return strings.Compare(a.Name, b.Name)
})
return models
}
// moveSelectionUp moves the selection up or wraps to bottom
func (m *modelDialogCmp) moveSelectionUp() {
func (m *modelDialog) moveSelectionUp() {
if m.selectedIdx > 0 {
m.selectedIdx--
} else {
@@ -182,7 +154,7 @@ func (m *modelDialogCmp) moveSelectionUp() {
}
// moveSelectionDown moves the selection down or wraps to top
func (m *modelDialogCmp) moveSelectionDown() {
func (m *modelDialog) moveSelectionDown() {
if m.selectedIdx < len(m.provider.Models)-1 {
m.selectedIdx++
} else {
@@ -196,7 +168,7 @@ func (m *modelDialogCmp) moveSelectionDown() {
}
}
func (m *modelDialogCmp) switchProvider(offset int) {
func (m *modelDialog) switchProvider(offset int) {
newOffset := m.hScrollOffset + offset
// Ensure we stay within bounds
@@ -212,9 +184,11 @@ func (m *modelDialogCmp) switchProvider(offset int) {
m.setupModelsForProvider(m.provider.Id)
}
func (m *modelDialogCmp) View() string {
func (m *modelDialog) View() string {
t := theme.CurrentTheme()
baseStyle := styles.BaseStyle()
baseStyle := lipgloss.NewStyle().
Background(t.BackgroundElement()).
Foreground(t.Text())
// Capitalize first letter of provider name
title := baseStyle.
@@ -232,10 +206,12 @@ func (m *modelDialogCmp) View() string {
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)
itemStyle = itemStyle.
Background(t.Primary()).
Foreground(t.BackgroundElement()).
Bold(true)
}
modelItems = append(modelItems, itemStyle.Render(*models[i].Name))
modelItems = append(modelItems, itemStyle.Render(models[i].Name))
}
scrollIndicator := m.getScrollIndicators(maxDialogWidth)
@@ -247,15 +223,10 @@ func (m *modelDialogCmp) View() string {
scrollIndicator,
)
return baseStyle.Padding(1, 2).
Border(lipgloss.RoundedBorder()).
BorderBackground(t.Background()).
BorderForeground(t.TextMuted()).
Width(lipgloss.Width(content) + 4).
Render(content)
return content
}
func (m *modelDialogCmp) getScrollIndicators(maxWidth int) string {
func (m *modelDialog) getScrollIndicators(maxWidth int) string {
var indicator string
if len(m.provider.Models) > numVisibleModels {
@@ -291,10 +262,6 @@ func (m *modelDialogCmp) getScrollIndicators(maxWidth int) string {
Render(indicator)
}
func (m *modelDialogCmp) BindingKeys() []key.Binding {
return layout.KeyMapToSlice(modelKeys)
}
// findProviderIndex returns the index of the provider in the list, or -1 if not found
// func findProviderIndex(providers []string, provider string) int {
// for i, p := range providers {
@@ -305,7 +272,7 @@ func (m *modelDialogCmp) BindingKeys() []key.Binding {
// return -1
// }
func (m *modelDialogCmp) setupModelsForProvider(_ string) {
func (m *modelDialog) setupModelsForProvider(_ string) {
m.selectedIdx = 0
m.scrollOffset = 0
@@ -331,8 +298,22 @@ func (m *modelDialogCmp) setupModelsForProvider(_ string) {
// }
}
func NewModelDialogCmp(app *app.App) ModelDialog {
return &modelDialogCmp{
app: app,
func (m *modelDialog) Render(background string) string {
return m.modal.Render(m.View(), background)
}
func (s *modelDialog) Close() tea.Cmd {
return nil
}
func NewModelDialog(app *app.App) ModelDialog {
availableProviders, _ := app.ListProviders(context.Background())
return &modelDialog{
availableProviders: availableProviders,
hScrollOffset: 0,
hScrollPossible: len(availableProviders) > 1,
provider: availableProviders[0],
modal: modal.New(),
}
}

View File

@@ -2,10 +2,10 @@ package dialog
import (
"fmt"
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/bubbles/v2/key"
"github.com/charmbracelet/bubbles/v2/viewport"
tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/lipgloss/v2"
"github.com/sst/opencode/internal/layout"
"github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme"
@@ -28,10 +28,9 @@ type PermissionResponseMsg struct {
Action PermissionAction
}
// PermissionDialogCmp interface for permission dialog component
type PermissionDialogCmp interface {
tea.Model
layout.Bindings
// PermissionDialogComponent interface for permission dialog component
type PermissionDialogComponent interface {
layout.ModelWithView
// SetPermissions(permission permission.PermissionRequest) tea.Cmd
}
@@ -76,8 +75,8 @@ var permissionsKeys = permissionsMapping{
),
}
// permissionDialogCmp is the implementation of PermissionDialog
type permissionDialogCmp struct {
// permissionDialogComponent is the implementation of PermissionDialog
type permissionDialogComponent struct {
width int
height int
// permission permission.PermissionRequest
@@ -89,11 +88,11 @@ type permissionDialogCmp struct {
markdownCache map[string]string
}
func (p *permissionDialogCmp) Init() tea.Cmd {
func (p *permissionDialogComponent) Init() tea.Cmd {
return p.contentViewPort.Init()
}
func (p *permissionDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
func (p *permissionDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmds []tea.Cmd
switch msg := msg.(type) {
@@ -129,7 +128,7 @@ func (p *permissionDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return p, tea.Batch(cmds...)
}
func (p *permissionDialogCmp) selectCurrentOption() tea.Cmd {
func (p *permissionDialogComponent) selectCurrentOption() tea.Cmd {
var action PermissionAction
switch p.selectedOption {
@@ -144,7 +143,7 @@ func (p *permissionDialogCmp) selectCurrentOption() tea.Cmd {
return util.CmdHandler(PermissionResponseMsg{Action: action}) // , Permission: p.permission})
}
func (p *permissionDialogCmp) renderButtons() string {
func (p *permissionDialogComponent) renderButtons() string {
t := theme.CurrentTheme()
baseStyle := styles.BaseStyle()
@@ -190,7 +189,7 @@ func (p *permissionDialogCmp) renderButtons() string {
return content
}
func (p *permissionDialogCmp) renderHeader() string {
func (p *permissionDialogComponent) renderHeader() string {
return "NOT IMPLEMENTED"
// t := theme.CurrentTheme()
// baseStyle := styles.BaseStyle()
@@ -246,7 +245,7 @@ func (p *permissionDialogCmp) renderHeader() string {
// return lipgloss.NewStyle().Background(t.Background()).Render(lipgloss.JoinVertical(lipgloss.Left, headerParts...))
}
func (p *permissionDialogCmp) renderBashContent() string {
func (p *permissionDialogComponent) renderBashContent() string {
// t := theme.CurrentTheme()
// baseStyle := styles.BaseStyle()
//
@@ -257,7 +256,7 @@ func (p *permissionDialogCmp) renderBashContent() string {
// renderedContent := p.GetOrSetMarkdown(p.permission.ID, func() (string, error) {
// r := styles.GetMarkdownRenderer(p.width - 10)
// s, err := r.Render(content)
// return styles.ForceReplaceBackgroundWithLipgloss(s, t.Background()), err
// return s
// })
//
// finalContent := baseStyle.
@@ -269,7 +268,7 @@ func (p *permissionDialogCmp) renderBashContent() string {
return ""
}
func (p *permissionDialogCmp) renderEditContent() string {
func (p *permissionDialogComponent) renderEditContent() string {
// if pr, ok := p.permission.Params.(tools.EditPermissionsParams); ok {
// diff := p.GetOrSetDiff(p.permission.ID, func() (string, error) {
// return diff.FormatDiff(pr.Diff, diff.WithTotalWidth(p.contentViewPort.Width))
@@ -281,7 +280,7 @@ func (p *permissionDialogCmp) renderEditContent() string {
return ""
}
func (p *permissionDialogCmp) renderPatchContent() string {
func (p *permissionDialogComponent) renderPatchContent() string {
// if pr, ok := p.permission.Params.(tools.EditPermissionsParams); ok {
// diff := p.GetOrSetDiff(p.permission.ID, func() (string, error) {
// return diff.FormatDiff(pr.Diff, diff.WithTotalWidth(p.contentViewPort.Width))
@@ -293,7 +292,7 @@ func (p *permissionDialogCmp) renderPatchContent() string {
return ""
}
func (p *permissionDialogCmp) renderWriteContent() string {
func (p *permissionDialogComponent) renderWriteContent() string {
// if pr, ok := p.permission.Params.(tools.WritePermissionsParams); ok {
// // Use the cache for diff rendering
// diff := p.GetOrSetDiff(p.permission.ID, func() (string, error) {
@@ -306,7 +305,7 @@ func (p *permissionDialogCmp) renderWriteContent() string {
return ""
}
func (p *permissionDialogCmp) renderFetchContent() string {
func (p *permissionDialogComponent) renderFetchContent() string {
// t := theme.CurrentTheme()
// baseStyle := styles.BaseStyle()
//
@@ -317,7 +316,7 @@ func (p *permissionDialogCmp) renderFetchContent() string {
// renderedContent := p.GetOrSetMarkdown(p.permission.ID, func() (string, error) {
// r := styles.GetMarkdownRenderer(p.width - 10)
// s, err := r.Render(content)
// return styles.ForceReplaceBackgroundWithLipgloss(s, t.Background()), err
// return s
// })
//
// finalContent := baseStyle.
@@ -329,7 +328,7 @@ func (p *permissionDialogCmp) renderFetchContent() string {
return ""
}
func (p *permissionDialogCmp) renderDefaultContent() string {
func (p *permissionDialogComponent) renderDefaultContent() string {
// t := theme.CurrentTheme()
// baseStyle := styles.BaseStyle()
//
@@ -339,7 +338,7 @@ func (p *permissionDialogCmp) renderDefaultContent() string {
// renderedContent := p.GetOrSetMarkdown(p.permission.ID, func() (string, error) {
// r := styles.GetMarkdownRenderer(p.width - 10)
// s, err := r.Render(content)
// return styles.ForceReplaceBackgroundWithLipgloss(s, t.Background()), err
// return s
// })
//
// finalContent := baseStyle.
@@ -354,7 +353,7 @@ func (p *permissionDialogCmp) renderDefaultContent() string {
return p.styleViewport()
}
func (p *permissionDialogCmp) styleViewport() string {
func (p *permissionDialogComponent) styleViewport() string {
t := theme.CurrentTheme()
contentStyle := lipgloss.NewStyle().
Background(t.Background())
@@ -362,7 +361,7 @@ func (p *permissionDialogCmp) styleViewport() string {
return contentStyle.Render(p.contentViewPort.View())
}
func (p *permissionDialogCmp) render() string {
func (p *permissionDialogComponent) render() string {
return "NOT IMPLEMENTED"
// t := theme.CurrentTheme()
// baseStyle := styles.BaseStyle()
@@ -420,15 +419,11 @@ func (p *permissionDialogCmp) render() string {
// )
}
func (p *permissionDialogCmp) View() string {
func (p *permissionDialogComponent) View() string {
return p.render()
}
func (p *permissionDialogCmp) BindingKeys() []key.Binding {
return layout.KeyMapToSlice(permissionsKeys)
}
func (p *permissionDialogCmp) SetSize() tea.Cmd {
func (p *permissionDialogComponent) SetSize() tea.Cmd {
// if p.permission.ID == "" {
// return nil
// }
@@ -458,7 +453,7 @@ func (p *permissionDialogCmp) SetSize() tea.Cmd {
// }
// Helper to get or set cached diff content
func (c *permissionDialogCmp) GetOrSetDiff(key string, generator func() (string, error)) string {
func (c *permissionDialogComponent) GetOrSetDiff(key string, generator func() (string, error)) string {
if cached, ok := c.diffCache[key]; ok {
return cached
}
@@ -474,7 +469,7 @@ func (c *permissionDialogCmp) GetOrSetDiff(key string, generator func() (string,
}
// Helper to get or set cached markdown content
func (c *permissionDialogCmp) GetOrSetMarkdown(key string, generator func() (string, error)) string {
func (c *permissionDialogComponent) GetOrSetMarkdown(key string, generator func() (string, error)) string {
if cached, ok := c.markdownCache[key]; ok {
return cached
}
@@ -489,11 +484,11 @@ func (c *permissionDialogCmp) GetOrSetMarkdown(key string, generator func() (str
return content
}
func NewPermissionDialogCmp() PermissionDialogCmp {
func NewPermissionDialogCmp() PermissionDialogComponent {
// Create viewport for content
contentViewport := viewport.New(0, 0)
contentViewport := viewport.New() // (0, 0)
return &permissionDialogCmp{
return &permissionDialogComponent{
contentViewPort: contentViewport,
selectedOption: 0, // Default to "Allow"
diffCache: make(map[string]string),

View File

@@ -3,9 +3,10 @@ package dialog
import (
"strings"
"github.com/charmbracelet/bubbles/key"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/bubbles/v2/key"
tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/lipgloss/v2"
"github.com/sst/opencode/internal/components/modal"
"github.com/sst/opencode/internal/layout"
"github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme"
@@ -14,14 +15,17 @@ import (
const question = "Are you sure you want to quit?"
type CloseQuitMsg struct{}
// QuitDialog interface for the quit confirmation dialog
type QuitDialog interface {
tea.Model
layout.Bindings
layout.Modal
IsQuitDialog() bool
}
type quitDialogCmp struct {
type quitDialog struct {
width int
height int
modal *modal.Modal
selectedNo bool
}
@@ -30,12 +34,11 @@ type helpMapping struct {
EnterSpace key.Binding
Yes key.Binding
No key.Binding
Tab key.Binding
}
var helpKeys = helpMapping{
LeftRight: key.NewBinding(
key.WithKeys("left", "right"),
key.WithKeys("left", "right", "h", "l", "tab"),
key.WithHelp("←/→", "switch options"),
),
EnterSpace: key.NewBinding(
@@ -43,58 +46,57 @@ var helpKeys = helpMapping{
key.WithHelp("enter/space", "confirm"),
),
Yes: key.NewBinding(
key.WithKeys("y", "Y"),
key.WithKeys("y", "Y", "ctrl+c"),
key.WithHelp("y/Y", "yes"),
),
No: key.NewBinding(
key.WithKeys("n", "N"),
key.WithHelp("n/N", "no"),
),
Tab: key.NewBinding(
key.WithKeys("tab"),
key.WithHelp("tab", "switch options"),
),
}
func (q *quitDialogCmp) Init() tea.Cmd {
func (q *quitDialog) Init() tea.Cmd {
return nil
}
func (q *quitDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
func (q *quitDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
q.width = msg.Width
q.height = msg.Height
case tea.KeyMsg:
switch {
case key.Matches(msg, helpKeys.LeftRight) || key.Matches(msg, helpKeys.Tab):
case key.Matches(msg, helpKeys.LeftRight):
q.selectedNo = !q.selectedNo
return q, nil
case key.Matches(msg, helpKeys.EnterSpace):
if !q.selectedNo {
return q, tea.Quit
}
return q, util.CmdHandler(CloseQuitMsg{})
return q, util.CmdHandler(modal.CloseModalMsg{})
case key.Matches(msg, helpKeys.Yes):
return q, tea.Quit
case key.Matches(msg, helpKeys.No):
return q, util.CmdHandler(CloseQuitMsg{})
return q, util.CmdHandler(modal.CloseModalMsg{})
}
}
return q, nil
}
func (q *quitDialogCmp) View() string {
func (q *quitDialog) Render(background string) string {
t := theme.CurrentTheme()
baseStyle := styles.BaseStyle()
yesStyle := baseStyle
noStyle := baseStyle
spacerStyle := baseStyle.Background(t.Background())
spacerStyle := baseStyle.Background(t.BackgroundElement())
if q.selectedNo {
noStyle = noStyle.Background(t.Primary()).Foreground(t.Background())
yesStyle = yesStyle.Background(t.Background()).Foreground(t.Primary())
noStyle = noStyle.Background(t.Primary()).Foreground(t.BackgroundElement())
yesStyle = yesStyle.Background(t.BackgroundElement()).Foreground(t.Primary())
} else {
yesStyle = yesStyle.Background(t.Primary()).Foreground(t.Background())
noStyle = noStyle.Background(t.Background()).Foreground(t.Primary())
yesStyle = yesStyle.Background(t.Primary()).Foreground(t.BackgroundElement())
noStyle = noStyle.Background(t.BackgroundElement()).Foreground(t.Primary())
}
yesButton := yesStyle.Padding(0, 1).Render("Yes")
@@ -117,20 +119,21 @@ func (q *quitDialogCmp) View() string {
),
)
return baseStyle.Padding(1, 2).
Border(lipgloss.RoundedBorder()).
BorderBackground(t.Background()).
BorderForeground(t.TextMuted()).
Width(lipgloss.Width(content) + 4).
Render(content)
return q.modal.Render(content, background)
}
func (q *quitDialogCmp) BindingKeys() []key.Binding {
return layout.KeyMapToSlice(helpKeys)
func (q *quitDialog) Close() tea.Cmd {
return nil
}
func NewQuitCmp() QuitDialog {
return &quitDialogCmp{
func (q *quitDialog) IsQuitDialog() bool {
return true
}
// NewQuitDialog creates a new quit confirmation dialog
func NewQuitDialog() QuitDialog {
return &quitDialog{
selectedNo: true,
modal: modal.New(),
}
}

View File

@@ -1,230 +1,112 @@
package dialog
import (
"github.com/charmbracelet/bubbles/key"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"context"
tea "github.com/charmbracelet/bubbletea/v2"
"github.com/sst/opencode/internal/app"
"github.com/sst/opencode/internal/components/list"
"github.com/sst/opencode/internal/components/modal"
"github.com/sst/opencode/internal/layout"
"github.com/sst/opencode/internal/state"
"github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme"
"github.com/sst/opencode/internal/util"
"github.com/sst/opencode/pkg/client"
)
// CloseSessionDialogMsg is sent when the session dialog is closed
type CloseSessionDialogMsg struct {
Session *client.SessionInfo
}
// SessionDialog interface for the session switching dialog
type SessionDialog interface {
tea.Model
layout.Bindings
SetSessions(sessions []client.SessionInfo)
SetSelectedSession(sessionID string)
layout.Modal
}
type sessionDialogCmp struct {
sessions []client.SessionInfo
selectedIdx int
type sessionItem struct {
session client.SessionInfo
}
func (s sessionItem) Render(selected bool, width int) string {
t := theme.CurrentTheme()
baseStyle := styles.BaseStyle().
Width(width - 2).
Background(t.BackgroundElement())
if selected {
baseStyle = baseStyle.
Background(t.Primary()).
Foreground(t.BackgroundElement()).
Bold(true)
} else {
baseStyle = baseStyle.
Foreground(t.Text())
}
return baseStyle.Padding(0, 1).Render(s.session.Title)
}
type sessionDialog struct {
width int
height int
modal *modal.Modal
selectedSessionID string
list list.List[sessionItem]
}
type sessionKeyMap struct {
Up key.Binding
Down key.Binding
Enter key.Binding
Escape key.Binding
J key.Binding
K key.Binding
}
var sessionKeys = sessionKeyMap{
Up: key.NewBinding(
key.WithKeys("up"),
key.WithHelp("↑", "previous session"),
),
Down: key.NewBinding(
key.WithKeys("down"),
key.WithHelp("↓", "next session"),
),
Enter: key.NewBinding(
key.WithKeys("enter"),
key.WithHelp("enter", "select session"),
),
Escape: key.NewBinding(
key.WithKeys("esc"),
key.WithHelp("esc", "close"),
),
J: key.NewBinding(
key.WithKeys("j"),
key.WithHelp("j", "next session"),
),
K: key.NewBinding(
key.WithKeys("k"),
key.WithHelp("k", "previous session"),
),
}
func (s *sessionDialogCmp) Init() tea.Cmd {
func (s *sessionDialog) Init() tea.Cmd {
return nil
}
func (s *sessionDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
func (s *sessionDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
s.width = msg.Width
s.height = msg.Height
s.list.SetMaxWidth(layout.Current.Container.Width - 12)
case tea.KeyMsg:
switch {
case key.Matches(msg, sessionKeys.Up) || key.Matches(msg, sessionKeys.K):
if s.selectedIdx > 0 {
s.selectedIdx--
}
return s, nil
case key.Matches(msg, sessionKeys.Down) || key.Matches(msg, sessionKeys.J):
if s.selectedIdx < len(s.sessions)-1 {
s.selectedIdx++
}
return s, nil
case key.Matches(msg, sessionKeys.Enter):
if len(s.sessions) > 0 {
selectedSession := s.sessions[s.selectedIdx]
switch msg.String() {
case "enter":
if item, idx := s.list.GetSelectedItem(); idx >= 0 {
selectedSession := item.session
s.selectedSessionID = selectedSession.Id
return s, util.CmdHandler(CloseSessionDialogMsg{
Session: &selectedSession,
})
return s, tea.Sequence(
util.CmdHandler(modal.CloseModalMsg{}),
util.CmdHandler(state.SessionSelectedMsg(&selectedSession)),
)
}
case key.Matches(msg, sessionKeys.Escape):
return s, util.CmdHandler(CloseSessionDialogMsg{})
}
}
return s, nil
var cmd tea.Cmd
listModel, cmd := s.list.Update(msg)
s.list = listModel.(list.List[sessionItem])
return s, cmd
}
func (s *sessionDialogCmp) View() string {
t := theme.CurrentTheme()
baseStyle := styles.BaseStyle()
func (s *sessionDialog) Render(background string) string {
return s.modal.Render(s.list.View(), background)
}
if len(s.sessions) == 0 {
return baseStyle.Padding(1, 2).
Border(lipgloss.RoundedBorder()).
BorderBackground(t.Background()).
BorderForeground(t.TextMuted()).
Width(40).
Render("No sessions available")
func (s *sessionDialog) Close() tea.Cmd {
return nil
}
// NewSessionDialog creates a new session switching dialog
func NewSessionDialog(app *app.App) SessionDialog {
sessions, _ := app.ListSessions(context.Background())
var sessionItems []sessionItem
for _, sess := range sessions {
sessionItems = append(sessionItems, sessionItem{session: sess})
}
// Calculate max width needed for session titles
maxWidth := 40 // Minimum width
for _, sess := range s.sessions {
if len(sess.Title) > maxWidth-4 { // Account for padding
maxWidth = len(sess.Title) + 4
}
}
maxWidth = max(30, min(maxWidth, s.width-15)) // Limit width to avoid overflow
// Limit height to avoid taking up too much screen space
maxVisibleSessions := min(10, len(s.sessions))
// Build the session list
sessionItems := make([]string, 0, maxVisibleSessions)
startIdx := 0
// If we have more sessions than can be displayed, adjust the start index
if len(s.sessions) > maxVisibleSessions {
// Center the selected item when possible
halfVisible := maxVisibleSessions / 2
if s.selectedIdx >= halfVisible && s.selectedIdx < len(s.sessions)-halfVisible {
startIdx = s.selectedIdx - halfVisible
} else if s.selectedIdx >= len(s.sessions)-halfVisible {
startIdx = len(s.sessions) - maxVisibleSessions
}
}
endIdx := min(startIdx+maxVisibleSessions, len(s.sessions))
for i := startIdx; i < endIdx; i++ {
sess := s.sessions[i]
itemStyle := baseStyle.Width(maxWidth)
if i == s.selectedIdx {
itemStyle = itemStyle.
Background(t.Primary()).
Foreground(t.Background()).
Bold(true)
}
sessionItems = append(sessionItems, itemStyle.Padding(0, 1).Render(sess.Title))
}
title := baseStyle.
Foreground(t.Primary()).
Bold(true).
Width(maxWidth).
Padding(0, 1).
Render("Switch Session")
content := lipgloss.JoinVertical(
lipgloss.Left,
title,
baseStyle.Width(maxWidth).Render(""),
baseStyle.Width(maxWidth).Render(lipgloss.JoinVertical(lipgloss.Left, sessionItems...)),
baseStyle.Width(maxWidth).Render(""),
list := list.NewListComponent(
sessionItems,
10, // maxVisibleSessions
"No sessions available",
true, // useAlphaNumericKeys
)
return baseStyle.Padding(1, 2).
Border(lipgloss.RoundedBorder()).
BorderBackground(t.Background()).
BorderForeground(t.TextMuted()).
Width(lipgloss.Width(content) + 4).
Render(content)
}
func (s *sessionDialogCmp) BindingKeys() []key.Binding {
return layout.KeyMapToSlice(sessionKeys)
}
func (s *sessionDialogCmp) SetSessions(sessions []client.SessionInfo) {
s.sessions = sessions
// If we have a selected session ID, find its index
if s.selectedSessionID != "" {
for i, sess := range sessions {
if sess.Id == s.selectedSessionID {
s.selectedIdx = i
return
}
}
}
// Default to first session if selected not found
s.selectedIdx = 0
}
func (s *sessionDialogCmp) SetSelectedSession(sessionID string) {
s.selectedSessionID = sessionID
// Update the selected index if sessions are already loaded
if len(s.sessions) > 0 {
for i, sess := range s.sessions {
if sess.Id == sessionID {
s.selectedIdx = i
return
}
}
}
}
// NewSessionDialogCmp creates a new session switching dialog
func NewSessionDialogCmp() SessionDialog {
return &sessionDialogCmp{
sessions: []client.SessionInfo{},
selectedIdx: 0,
selectedSessionID: "",
return &sessionDialog{
list: list,
modal: modal.New(modal.WithTitle("Switch Session"), modal.WithMaxWidth(80)),
}
}

View File

@@ -1,9 +1,9 @@
package dialog
import (
"github.com/charmbracelet/bubbles/key"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
tea "github.com/charmbracelet/bubbletea/v2"
list "github.com/sst/opencode/internal/components/list"
"github.com/sst/opencode/internal/components/modal"
"github.com/sst/opencode/internal/layout"
"github.com/sst/opencode/internal/status"
"github.com/sst/opencode/internal/styles"
@@ -16,184 +16,112 @@ 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
layout.Modal
}
type themeDialogCmp struct {
themes []string
selectedIdx int
width int
height int
currentTheme string
type themeItem struct {
name string
}
type themeKeyMap struct {
Up key.Binding
Down key.Binding
Enter key.Binding
Escape key.Binding
J key.Binding
K key.Binding
}
func (t themeItem) Render(selected bool, width int) string {
th := theme.CurrentTheme()
baseStyle := styles.BaseStyle().
Width(width - 2).
Background(th.BackgroundElement())
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
}
if selected {
baseStyle = baseStyle.
Background(th.Primary()).
Foreground(th.BackgroundElement()).
Bold(true)
} else {
baseStyle = baseStyle.
Foreground(th.Text())
}
return baseStyle.Padding(0, 1).Render(t.name)
}
type themeDialog struct {
width int
height int
modal *modal.Modal
list list.List[themeItem]
}
func (t *themeDialog) Init() tea.Cmd {
return nil
}
func (t *themeDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
func (t *themeDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
t.width = msg.Width
t.height = msg.Height
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 {
switch msg.String() {
case "enter":
if item, idx := t.list.GetSelectedItem(); idx >= 0 {
previousTheme := theme.CurrentThemeName()
selectedTheme := t.themes[t.selectedIdx]
selectedTheme := item.name
if previousTheme == selectedTheme {
return t, util.CmdHandler(CloseThemeDialogMsg{})
return t, util.CmdHandler(modal.CloseModalMsg{})
}
if err := theme.SetTheme(selectedTheme); err != nil {
status.Error(err.Error())
return t, nil
}
return t, util.CmdHandler(ThemeChangedMsg{
ThemeName: selectedTheme,
})
return t, tea.Sequence(
util.CmdHandler(modal.CloseModalMsg{}),
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
var cmd tea.Cmd
listModel, cmd := t.list.Update(msg)
t.list = listModel.(list.List[themeItem])
return t, cmd
}
func (t *themeDialogCmp) View() string {
currentTheme := theme.CurrentTheme()
baseStyle := styles.BaseStyle()
func (t *themeDialog) Render(background string) string {
return t.modal.Render(t.list.View(), background)
}
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")
}
func (t *themeDialog) Close() tea.Cmd {
return nil
}
// 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
// NewThemeDialog creates a new theme switching dialog
func NewThemeDialog() ThemeDialog {
themes := theme.AvailableThemes()
currentTheme := theme.CurrentThemeName()
var themeItems []themeItem
var selectedIdx int
for i, name := range themes {
themeItems = append(themeItems, themeItem{name: name})
if name == currentTheme {
selectedIdx = i
}
}
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(""),
list := list.NewListComponent(
themeItems,
10, // maxVisibleThemes
"No themes available",
true,
)
return baseStyle.Padding(1, 2).
Border(lipgloss.RoundedBorder()).
BorderBackground(currentTheme.Background()).
BorderForeground(currentTheme.TextMuted()).
Width(lipgloss.Width(content) + 4).
Render(content)
}
// Set the initial selection to the current theme
list.SetSelectedIndex(selectedIdx)
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: "",
return &themeDialog{
list: list,
modal: modal.New(modal.WithTitle("Select Theme"), modal.WithMaxWidth(40)),
}
}

View File

@@ -1,178 +0,0 @@
package dialog
import (
"github.com/charmbracelet/bubbles/key"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
utilComponents "github.com/sst/opencode/internal/components/util"
"github.com/sst/opencode/internal/layout"
"github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme"
)
const (
maxToolsDialogWidth = 60
maxVisibleTools = 15
)
// ToolsDialog interface for the tools list dialog
type ToolsDialog interface {
tea.Model
layout.Bindings
SetTools(tools []string)
}
// ShowToolsDialogMsg is sent to show the tools dialog
type ShowToolsDialogMsg struct {
Show bool
}
// CloseToolsDialogMsg is sent when the tools dialog is closed
type CloseToolsDialogMsg struct{}
type toolItem struct {
name string
}
func (t toolItem) Render(selected bool, width int) string {
th := theme.CurrentTheme()
baseStyle := styles.BaseStyle().
Width(width).
Background(th.Background())
if selected {
baseStyle = baseStyle.
Background(th.Primary()).
Foreground(th.Background()).
Bold(true)
} else {
baseStyle = baseStyle.
Foreground(th.Text())
}
return baseStyle.Render(t.name)
}
type toolsDialogCmp struct {
tools []toolItem
width int
height int
list utilComponents.SimpleList[toolItem]
}
type toolsKeyMap struct {
Up key.Binding
Down key.Binding
Escape key.Binding
J key.Binding
K key.Binding
}
var toolsKeys = toolsKeyMap{
Up: key.NewBinding(
key.WithKeys("up"),
key.WithHelp("↑", "previous tool"),
),
Down: key.NewBinding(
key.WithKeys("down"),
key.WithHelp("↓", "next tool"),
),
Escape: key.NewBinding(
key.WithKeys("esc"),
key.WithHelp("esc", "close"),
),
J: key.NewBinding(
key.WithKeys("j"),
key.WithHelp("j", "next tool"),
),
K: key.NewBinding(
key.WithKeys("k"),
key.WithHelp("k", "previous tool"),
),
}
func (m *toolsDialogCmp) Init() tea.Cmd {
return nil
}
func (m *toolsDialogCmp) SetTools(tools []string) {
var toolItems []toolItem
for _, name := range tools {
toolItems = append(toolItems, toolItem{name: name})
}
m.tools = toolItems
m.list.SetItems(toolItems)
}
func (m *toolsDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch {
case key.Matches(msg, toolsKeys.Escape):
return m, func() tea.Msg { return CloseToolsDialogMsg{} }
// Pass other key messages to the list component
default:
var cmd tea.Cmd
listModel, cmd := m.list.Update(msg)
m.list = listModel.(utilComponents.SimpleList[toolItem])
return m, cmd
}
case tea.WindowSizeMsg:
m.width = msg.Width
m.height = msg.Height
}
// For non-key messages
var cmd tea.Cmd
listModel, cmd := m.list.Update(msg)
m.list = listModel.(utilComponents.SimpleList[toolItem])
return m, cmd
}
func (m *toolsDialogCmp) View() string {
t := theme.CurrentTheme()
baseStyle := styles.BaseStyle().Background(t.Background())
title := baseStyle.
Foreground(t.Primary()).
Bold(true).
Width(maxToolsDialogWidth).
Padding(0, 0, 1).
Render("Available Tools")
// Calculate dialog width based on content
dialogWidth := min(maxToolsDialogWidth, m.width/2)
m.list.SetMaxWidth(dialogWidth)
content := lipgloss.JoinVertical(
lipgloss.Left,
title,
m.list.View(),
)
return baseStyle.Padding(1, 2).
Border(lipgloss.RoundedBorder()).
BorderBackground(t.Background()).
BorderForeground(t.TextMuted()).
Background(t.Background()).
Width(lipgloss.Width(content) + 4).
Render(content)
}
func (m *toolsDialogCmp) BindingKeys() []key.Binding {
return layout.KeyMapToSlice(toolsKeys)
}
func NewToolsDialogCmp() ToolsDialog {
list := utilComponents.NewSimpleList[toolItem](
[]toolItem{},
maxVisibleTools,
"No tools available",
true,
)
return &toolsDialogCmp{
list: list,
}
}

View File

@@ -3,6 +3,7 @@ package diff
import (
"bytes"
"fmt"
"image/color"
"io"
"regexp"
"strconv"
@@ -12,9 +13,11 @@ import (
"github.com/alecthomas/chroma/v2/formatters"
"github.com/alecthomas/chroma/v2/lexers"
"github.com/alecthomas/chroma/v2/styles"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/lipgloss/v2"
"github.com/charmbracelet/lipgloss/v2/compat"
"github.com/charmbracelet/x/ansi"
"github.com/sergi/go-diff/diffmatchpatch"
stylesi "github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme"
)
@@ -101,6 +104,40 @@ func WithTotalWidth(width int) SideBySideOption {
}
}
// -------------------------------------------------------------------------
// Unified Configuration
// -------------------------------------------------------------------------
// UnifiedConfig configures the rendering of unified diffs
type UnifiedConfig struct {
Width int
}
// UnifiedOption modifies a UnifiedConfig
type UnifiedOption func(*UnifiedConfig)
// NewUnifiedConfig creates a UnifiedConfig with default values
func NewUnifiedConfig(opts ...UnifiedOption) UnifiedConfig {
config := UnifiedConfig{
Width: 80, // Default width for unified view
}
for _, opt := range opts {
opt(&config)
}
return config
}
// WithWidth sets the width for unified view
func WithWidth(width int) UnifiedOption {
return func(u *UnifiedConfig) {
if width > 0 {
u.Width = width
}
}
}
// -------------------------------------------------------------------------
// Diff Parsing
// -------------------------------------------------------------------------
@@ -300,7 +337,7 @@ 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 {
func SyntaxHighlight(w io.Writer, source, fileName, formatter string, bg color.Color) error {
t := theme.CurrentTheme()
// Determine the language lexer to use
@@ -509,15 +546,12 @@ func SyntaxHighlight(w io.Writer, source, fileName, formatter string, bg lipglos
}
// 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
func getColor(adaptiveColor compat.AdaptiveColor) string {
return stylesi.AdaptiveColorToString(adaptiveColor)
}
// highlightLine applies syntax highlighting to a single line
func highlightLine(fileName string, line string, bg lipgloss.TerminalColor) string {
func highlightLine(fileName string, line string, bg color.Color) string {
var buf bytes.Buffer
err := SyntaxHighlight(&buf, line, fileName, "terminal16m", bg)
if err != nil {
@@ -540,7 +574,7 @@ func createStyles(t theme.Theme) (removedLineStyle, addedLineStyle, contextLineS
// -------------------------------------------------------------------------
// applyHighlighting applies intra-line highlighting to a piece of text
func applyHighlighting(content string, segments []Segment, segmentType LineType, highlightBg lipgloss.AdaptiveColor) string {
func applyHighlighting(content string, segments []Segment, segmentType LineType, highlightBg compat.AdaptiveColor) string {
// Find all ANSI sequences in the content
ansiRegex := regexp.MustCompile(`\x1b(?:[@-Z\\-_]|\[[0-9?]*(?:;[0-9?]*)*[@-~])`)
ansiMatches := ansiRegex.FindAllStringIndex(content, -1)
@@ -642,6 +676,101 @@ func applyHighlighting(content string, segments []Segment, segmentType LineType,
return sb.String()
}
// renderLinePrefix renders the line number and marker prefix for a diff line
func renderLinePrefix(dl DiffLine, lineNum string, marker string, lineNumberStyle lipgloss.Style, t theme.Theme) string {
// Style the marker based on line type
var styledMarker string
switch dl.Kind {
case LineRemoved:
styledMarker = lipgloss.NewStyle().Background(t.DiffRemovedBg()).Foreground(t.DiffRemoved()).Render(marker)
case LineAdded:
styledMarker = lipgloss.NewStyle().Background(t.DiffAddedBg()).Foreground(t.DiffAdded()).Render(marker)
case LineContext:
styledMarker = lipgloss.NewStyle().Background(t.DiffContextBg()).Foreground(t.TextMuted()).Render(marker)
default:
styledMarker = marker
}
return lineNumberStyle.Render(lineNum + " " + styledMarker)
}
// renderLineContent renders the content of a diff line with syntax and intra-line highlighting
func renderLineContent(fileName string, dl DiffLine, bgStyle lipgloss.Style, highlightColor compat.AdaptiveColor, width int, t theme.Theme) string {
// Apply syntax highlighting
content := highlightLine(fileName, dl.Content, bgStyle.GetBackground())
// Apply intra-line highlighting if needed
if len(dl.Segments) > 0 && (dl.Kind == LineRemoved || dl.Kind == LineAdded) {
content = applyHighlighting(content, dl.Segments, dl.Kind, highlightColor)
}
// Add a padding space for added/removed lines
if dl.Kind == LineRemoved || dl.Kind == LineAdded {
content = bgStyle.Render(" ") + content
}
// Create the final line and truncate if needed
return bgStyle.MaxHeight(1).Width(width).Render(
ansi.Truncate(
content,
width,
lipgloss.NewStyle().Background(bgStyle.GetBackground()).Foreground(t.TextMuted()).Render("..."),
),
)
}
// renderUnifiedLine renders a single line in unified diff format
func renderUnifiedLine(fileName string, dl DiffLine, width int, t theme.Theme) string {
removedLineStyle, addedLineStyle, contextLineStyle, lineNumberStyle := createStyles(t)
// Determine line style and marker based on line type
var marker string
var bgStyle lipgloss.Style
var lineNum string
var highlightColor compat.AdaptiveColor
switch dl.Kind {
case LineRemoved:
marker = "-"
bgStyle = removedLineStyle
lineNumberStyle = lineNumberStyle.Foreground(t.DiffRemoved()).Background(t.DiffRemovedLineNumberBg())
highlightColor = t.DiffHighlightRemoved()
if dl.OldLineNo > 0 {
lineNum = fmt.Sprintf("%6d ", dl.OldLineNo)
} else {
lineNum = " "
}
case LineAdded:
marker = "+"
bgStyle = addedLineStyle
lineNumberStyle = lineNumberStyle.Foreground(t.DiffAdded()).Background(t.DiffAddedLineNumberBg())
highlightColor = t.DiffHighlightAdded()
if dl.NewLineNo > 0 {
lineNum = fmt.Sprintf(" %7d", dl.NewLineNo)
} else {
lineNum = " "
}
case LineContext:
marker = " "
bgStyle = contextLineStyle
if dl.OldLineNo > 0 && dl.NewLineNo > 0 {
lineNum = fmt.Sprintf("%6d %6d", dl.OldLineNo, dl.NewLineNo)
} else {
lineNum = " "
}
}
// Create the line prefix
prefix := renderLinePrefix(dl, lineNum, marker, lineNumberStyle, t)
// Render the content
prefixWidth := ansi.StringWidth(prefix)
contentWidth := width - prefixWidth
content := renderLineContent(fileName, dl, bgStyle, highlightColor, contentWidth, t)
return prefix + content
}
// renderDiffColumnLine is a helper function that handles the common logic for rendering diff columns
func renderDiffColumnLine(
fileName string,
@@ -661,8 +790,7 @@ func renderDiffColumnLine(
var marker string
var bgStyle lipgloss.Style
var lineNum string
var highlightType LineType
var highlightColor lipgloss.AdaptiveColor
var highlightColor compat.AdaptiveColor
if isLeftColumn {
// Left column logic
@@ -671,7 +799,6 @@ func renderDiffColumnLine(
marker = "-"
bgStyle = removedLineStyle
lineNumberStyle = lineNumberStyle.Foreground(t.DiffRemoved()).Background(t.DiffRemovedLineNumberBg())
highlightType = LineRemoved
highlightColor = t.DiffHighlightRemoved()
case LineAdded:
marker = "?"
@@ -692,7 +819,6 @@ func renderDiffColumnLine(
marker = "+"
bgStyle = addedLineStyle
lineNumberStyle = lineNumberStyle.Foreground(t.DiffAdded()).Background(t.DiffAddedLineNumberBg())
highlightType = LineAdded
highlightColor = t.DiffHighlightAdded()
case LineRemoved:
marker = "?"
@@ -708,44 +834,24 @@ func renderDiffColumnLine(
}
}
// Style the marker based on line type
var styledMarker string
switch dl.Kind {
case LineRemoved:
styledMarker = removedLineStyle.Foreground(t.DiffRemoved()).Render(marker)
case LineAdded:
styledMarker = addedLineStyle.Foreground(t.DiffAdded()).Render(marker)
case LineContext:
styledMarker = contextLineStyle.Foreground(t.TextMuted()).Render(marker)
default:
styledMarker = marker
}
// Create the line prefix
prefix := lineNumberStyle.Render(lineNum + " " + styledMarker)
prefix := renderLinePrefix(*dl, lineNum, marker, lineNumberStyle, t)
// Apply syntax highlighting
content := highlightLine(fileName, dl.Content, bgStyle.GetBackground())
// Determine if we should render content
shouldRenderContent := (dl.Kind == LineRemoved && isLeftColumn) ||
(dl.Kind == LineAdded && !isLeftColumn) ||
dl.Kind == LineContext
// Apply intra-line highlighting if needed
if (dl.Kind == LineRemoved && isLeftColumn || dl.Kind == LineAdded && !isLeftColumn) && len(dl.Segments) > 0 {
content = applyHighlighting(content, dl.Segments, highlightType, highlightColor)
if !shouldRenderContent {
return bgStyle.Width(colWidth).Render("")
}
// Add a padding space for added/removed lines
if (dl.Kind == LineRemoved && isLeftColumn) || (dl.Kind == LineAdded && !isLeftColumn) {
content = bgStyle.Render(" ") + content
}
// Render the content
prefixWidth := ansi.StringWidth(prefix)
contentWidth := colWidth - prefixWidth
content := renderLineContent(fileName, *dl, bgStyle, highlightColor, contentWidth, t)
// Create the final line and truncate if needed
lineText := prefix + content
return bgStyle.MaxHeight(1).Width(colWidth).Render(
ansi.Truncate(
lineText,
colWidth,
lipgloss.NewStyle().Background(bgStyle.GetBackground()).Foreground(t.TextMuted()).Render("..."),
),
)
return prefix + content
}
// renderLeftColumn formats the left side of a side-by-side diff
@@ -762,6 +868,27 @@ func renderRightColumn(fileName string, dl *DiffLine, colWidth int) string {
// Public API
// -------------------------------------------------------------------------
// RenderUnifiedHunk formats a hunk for unified display
func RenderUnifiedHunk(fileName string, h Hunk, opts ...UnifiedOption) string {
// Apply options to create the configuration
config := NewUnifiedConfig(opts...)
// Make a copy of the hunk so we don't modify the original
hunkCopy := Hunk{Lines: make([]DiffLine, len(h.Lines))}
copy(hunkCopy.Lines, h.Lines)
// Highlight changes within lines
HighlightIntralineChanges(&hunkCopy)
var sb strings.Builder
for _, line := range hunkCopy.Lines {
sb.WriteString(renderUnifiedLine(fileName, line, config.Width, theme.CurrentTheme()))
sb.WriteString("\n")
}
return sb.String()
}
// RenderSideBySideHunk formats a hunk for side-by-side display
func RenderSideBySideHunk(fileName string, h Hunk, opts ...SideBySideOption) string {
// Apply options to create the configuration
@@ -792,6 +919,21 @@ func RenderSideBySideHunk(fileName string, h Hunk, opts ...SideBySideOption) str
return sb.String()
}
// FormatUnifiedDiff creates a unified formatted view of a diff
func FormatUnifiedDiff(filename string, diffText string, opts ...UnifiedOption) (string, error) {
diffResult, err := ParseUnifiedDiff(diffText)
if err != nil {
return "", err
}
var sb strings.Builder
for _, h := range diffResult.Hunks {
sb.WriteString(RenderUnifiedHunk(filename, h, opts...))
}
return sb.String(), nil
}
// FormatDiff creates a side-by-side formatted view of a diff
func FormatDiff(filename string, diffText string, opts ...SideBySideOption) (string, error) {
// t := theme.CurrentTheme()

View File

@@ -1,28 +1,27 @@
package utilComponents
package list
import (
"github.com/charmbracelet/bubbles/key"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/bubbles/v2/key"
tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/lipgloss/v2"
"github.com/sst/opencode/internal/layout"
"github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme"
)
type SimpleListItem interface {
type ListItem interface {
Render(selected bool, width int) string
}
type SimpleList[T SimpleListItem] interface {
tea.Model
layout.Bindings
type List[T ListItem] interface {
layout.ModelWithView
SetMaxWidth(maxWidth int)
GetSelectedItem() (item T, idx int)
SetItems(items []T)
GetItems() []T
SetSelectedIndex(idx int)
IsEmpty() bool
}
type simpleListCmp[T SimpleListItem] struct {
type listComponent[T ListItem] struct {
fallbackMsg string
items []T
selectedIdx int
@@ -33,14 +32,14 @@ type simpleListCmp[T SimpleListItem] struct {
height int
}
type simpleListKeyMap struct {
type listKeyMap struct {
Up key.Binding
Down key.Binding
UpAlpha key.Binding
DownAlpha key.Binding
}
var simpleListKeys = simpleListKeyMap{
var simpleListKeys = listKeyMap{
Up: key.NewBinding(
key.WithKeys("up"),
key.WithHelp("↑", "previous list item"),
@@ -59,11 +58,11 @@ var simpleListKeys = simpleListKeyMap{
),
}
func (c *simpleListCmp[T]) Init() tea.Cmd {
func (c *listComponent[T]) Init() tea.Cmd {
return nil
}
func (c *simpleListCmp[T]) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
func (c *listComponent[T]) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch {
@@ -83,11 +82,7 @@ func (c *simpleListCmp[T]) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return c, nil
}
func (c *simpleListCmp[T]) BindingKeys() []key.Binding {
return layout.KeyMapToSlice(simpleListKeys)
}
func (c *simpleListCmp[T]) GetSelectedItem() (T, int) {
func (c *listComponent[T]) GetSelectedItem() (T, int) {
if len(c.items) > 0 {
return c.items[c.selectedIdx], c.selectedIdx
}
@@ -96,34 +91,37 @@ func (c *simpleListCmp[T]) GetSelectedItem() (T, int) {
return zero, -1
}
func (c *simpleListCmp[T]) SetItems(items []T) {
func (c *listComponent[T]) SetItems(items []T) {
c.selectedIdx = 0
c.items = items
}
func (c *simpleListCmp[T]) GetItems() []T {
func (c *listComponent[T]) GetItems() []T {
return c.items
}
func (c *simpleListCmp[T]) SetMaxWidth(width int) {
func (c *listComponent[T]) IsEmpty() bool {
return len(c.items) == 0
}
func (c *listComponent[T]) SetMaxWidth(width int) {
c.maxWidth = width
}
func (c *simpleListCmp[T]) View() string {
t := theme.CurrentTheme()
baseStyle := styles.BaseStyle()
func (c *listComponent[T]) SetSelectedIndex(idx int) {
if idx >= 0 && idx < len(c.items) {
c.selectedIdx = idx
}
}
func (c *listComponent[T]) View() string {
items := c.items
maxWidth := c.maxWidth
maxVisibleItems := min(c.maxVisibleItems, len(items))
startIdx := 0
if len(items) <= 0 {
return baseStyle.
Background(t.Background()).
Padding(0, 1).
Width(maxWidth).
Render(c.fallbackMsg)
return c.fallbackMsg
}
if len(items) > maxVisibleItems {
@@ -148,8 +146,8 @@ func (c *simpleListCmp[T]) View() string {
return lipgloss.JoinVertical(lipgloss.Left, listItems...)
}
func NewSimpleList[T SimpleListItem](items []T, maxVisibleItems int, fallbackMsg string, useAlphaNumericKeys bool) SimpleList[T] {
return &simpleListCmp[T]{
func NewListComponent[T ListItem](items []T, maxVisibleItems int, fallbackMsg string, useAlphaNumericKeys bool) List[T] {
return &listComponent[T]{
fallbackMsg: fallbackMsg,
items: items,
maxVisibleItems: maxVisibleItems,

View File

@@ -0,0 +1,143 @@
package modal
import (
"github.com/charmbracelet/lipgloss/v2"
"github.com/sst/opencode/internal/layout"
"github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme"
)
// CloseModalMsg is a message to signal that the active modal should be closed.
type CloseModalMsg struct{}
// Modal is a reusable modal component that handles frame rendering and overlay placement
type Modal struct {
width int
height int
title string
maxWidth int
maxHeight int
fitContent bool
}
// ModalOption is a function that configures a Modal
type ModalOption func(*Modal)
// WithTitle sets the modal title
func WithTitle(title string) ModalOption {
return func(m *Modal) {
m.title = title
}
}
// WithMaxWidth sets the maximum width
func WithMaxWidth(width int) ModalOption {
return func(m *Modal) {
m.maxWidth = width
m.fitContent = false
}
}
// WithMaxHeight sets the maximum height
func WithMaxHeight(height int) ModalOption {
return func(m *Modal) {
m.maxHeight = height
}
}
func WithFitContent(fit bool) ModalOption {
return func(m *Modal) {
m.fitContent = fit
}
}
// New creates a new Modal with the given options
func New(opts ...ModalOption) *Modal {
m := &Modal{
maxWidth: 0,
maxHeight: 0,
fitContent: true,
}
for _, opt := range opts {
opt(m)
}
return m
}
// Render renders the modal centered on the screen
func (m *Modal) Render(contentView string, background string) string {
t := theme.CurrentTheme()
outerWidth := layout.Current.Container.Width - 8
if m.maxWidth > 0 && outerWidth > m.maxWidth {
outerWidth = m.maxWidth
}
if m.fitContent {
titleWidth := lipgloss.Width(m.title)
contentWidth := lipgloss.Width(contentView)
largestWidth := max(titleWidth+2, contentWidth)
outerWidth = largestWidth + 6
}
innerWidth := outerWidth - 4
// Base style for the modal
baseStyle := styles.BaseStyle().
Background(t.BackgroundElement()).
Foreground(t.TextMuted())
// Add title if provided
var finalContent string
if m.title != "" {
titleStyle := baseStyle.
Foreground(t.Primary()).
Bold(true).
Width(innerWidth).
Padding(0, 1)
titleView := titleStyle.Render(m.title)
finalContent = lipgloss.JoinVertical(
lipgloss.Left,
titleView,
contentView,
)
} else {
finalContent = contentView
}
modalStyle := baseStyle.
PaddingTop(1).
PaddingBottom(1).
PaddingLeft(2).
PaddingRight(2).
BorderStyle(lipgloss.ThickBorder()).
BorderLeft(true).
BorderRight(true).
BorderLeftForeground(t.BackgroundSubtle()).
BorderLeftBackground(t.Background()).
BorderRightForeground(t.BackgroundSubtle()).
BorderRightBackground(t.Background())
modalView := modalStyle.
Width(outerWidth).
Render(finalContent)
// Calculate position for centering
bgHeight := lipgloss.Height(background)
bgWidth := lipgloss.Width(background)
modalHeight := lipgloss.Height(modalView)
modalWidth := lipgloss.Width(modalView)
row := (bgHeight - modalHeight) / 2
col := (bgWidth - modalWidth) / 2
return layout.PlaceOverlay(
col,
row,
modalView,
background,
)
}

View File

@@ -3,7 +3,7 @@ package qr
import (
"strings"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/lipgloss/v2"
"github.com/sst/opencode/internal/theme"
"rsc.io/qr"
)

View File

@@ -10,9 +10,9 @@ import (
)
type Config struct {
Theme string `toml:"Theme"`
Provider string `toml:"Provider"`
Model string `toml:"Model"`
Theme string `toml:"theme"`
Provider string `toml:"provider"`
Model string `toml:"model"`
}
// NewConfig creates a new Config instance with default values.

View File

@@ -1,163 +0,0 @@
package fileutil
import (
"fmt"
"io/fs"
"os"
"os/exec"
"path/filepath"
"sort"
"strings"
"time"
"github.com/bmatcuk/doublestar/v4"
"github.com/sst/opencode/internal/status"
)
var (
rgPath string
fzfPath string
)
func Init() {
var err error
rgPath, err = exec.LookPath("rg")
if err != nil {
status.Warn("Ripgrep (rg) not found in $PATH. Some features might be limited or slower.")
rgPath = ""
}
fzfPath, err = exec.LookPath("fzf")
if err != nil {
status.Warn("FZF not found in $PATH. Some features might be limited or slower.")
fzfPath = ""
}
}
func GetRgCmd(globPattern string) *exec.Cmd {
if rgPath == "" {
return nil
}
rgArgs := []string{
"--files",
"-L",
"--null",
}
if globPattern != "" {
if !filepath.IsAbs(globPattern) && !strings.HasPrefix(globPattern, "/") {
globPattern = "/" + globPattern
}
rgArgs = append(rgArgs, "--glob", globPattern)
}
cmd := exec.Command(rgPath, rgArgs...)
cmd.Dir = "."
return cmd
}
func GetFzfCmd(query string) *exec.Cmd {
if fzfPath == "" {
return nil
}
fzfArgs := []string{
"--filter",
query,
"--read0",
"--print0",
}
cmd := exec.Command(fzfPath, fzfArgs...)
cmd.Dir = "."
return cmd
}
type FileInfo struct {
Path string
ModTime time.Time
}
func SkipHidden(path string) bool {
// Check for hidden files (starting with a dot)
base := filepath.Base(path)
if base != "." && strings.HasPrefix(base, ".") {
return true
}
commonIgnoredDirs := map[string]bool{
".opencode": true,
"node_modules": true,
"vendor": true,
"dist": true,
"build": true,
"target": true,
".git": true,
".idea": true,
".vscode": true,
"__pycache__": true,
"bin": true,
"obj": true,
"out": true,
"coverage": true,
"tmp": true,
"temp": true,
"logs": true,
"generated": true,
"bower_components": true,
"jspm_packages": true,
}
parts := strings.Split(path, string(os.PathSeparator))
for _, part := range parts {
if commonIgnoredDirs[part] {
return true
}
}
return false
}
func GlobWithDoublestar(pattern, searchPath string, limit int) ([]string, bool, error) {
fsys := os.DirFS(searchPath)
relPattern := strings.TrimPrefix(pattern, "/")
var matches []FileInfo
err := doublestar.GlobWalk(fsys, relPattern, func(path string, d fs.DirEntry) error {
if d.IsDir() {
return nil
}
if SkipHidden(path) {
return nil
}
info, err := d.Info()
if err != nil {
return nil
}
absPath := path
if !strings.HasPrefix(absPath, searchPath) && searchPath != "." {
absPath = filepath.Join(searchPath, absPath)
} else if !strings.HasPrefix(absPath, "/") && searchPath == "." {
absPath = filepath.Join(searchPath, absPath) // Ensure relative paths are joined correctly
}
matches = append(matches, FileInfo{Path: absPath, ModTime: info.ModTime()})
if limit > 0 && len(matches) >= limit*2 {
return fs.SkipAll
}
return nil
})
if err != nil {
return nil, false, fmt.Errorf("glob walk error: %w", err)
}
sort.Slice(matches, func(i, j int) bool {
return matches[i].ModTime.After(matches[j].ModTime)
})
truncated := false
if limit > 0 && len(matches) > limit {
matches = matches[:limit]
truncated = true
}
results := make([]string, len(matches))
for i, m := range matches {
results[i] = m.Path
}
return results, truncated, nil
}

View File

@@ -4,11 +4,12 @@ import (
"bytes"
"fmt"
"image"
"image/color"
"image/png"
"os"
"strings"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/lipgloss/v2"
"github.com/disintegration/imaging"
"github.com/lucasb-eyer/go-colorful"
_ "golang.org/x/image/webp"
@@ -39,7 +40,7 @@ func ToString(width int, img image.Image) string {
c1, _ := colorful.MakeColor(img.At(x, heightCounter))
color1 := lipgloss.Color(c1.Hex())
var color2 lipgloss.Color
var color2 color.Color
if heightCounter+1 < h {
c2, _ := colorful.MakeColor(img.At(x, heightCounter+1))
color2 = lipgloss.Color(c2.Hex())
@@ -76,10 +77,10 @@ func ImagePreview(width int, filename string) (string, error) {
func ImageToBytes(image image.Image) ([]byte, error) {
buf := new(bytes.Buffer)
err := png.Encode(buf, image)
if err != nil {
return nil, err
}
return buf.Bytes(), nil
err := png.Encode(buf, image)
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}

View File

@@ -1,27 +1,34 @@
package layout
import (
"github.com/charmbracelet/bubbles/key"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/lipgloss/v2"
"github.com/sst/opencode/internal/theme"
)
type Container interface {
type ModelWithView interface {
tea.Model
tea.ViewModel
}
type Container interface {
ModelWithView
Sizeable
Bindings
Focus()
Blur()
MaxWidth() int
Alignment() lipgloss.Position
GetPosition() (x, y int)
GetContent() ModelWithView
}
type container struct {
width int
height int
x int
y int
content tea.Model
content ModelWithView
paddingTop int
paddingRight int
@@ -46,7 +53,7 @@ func (c *container) Init() tea.Cmd {
func (c *container) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
u, cmd := c.content.Update(msg)
c.content = u
c.content = u.(ModelWithView)
return c, cmd
}
@@ -137,7 +144,7 @@ func (c *container) SetSize(width, height int) tea.Cmd {
}
func (c *container) GetSize() (int, int) {
return c.width, c.height
return min(c.width, c.maxWidth), c.height
}
func (c *container) MaxWidth() int {
@@ -148,13 +155,6 @@ func (c *container) Alignment() lipgloss.Position {
return c.align
}
func (c *container) BindingKeys() []key.Binding {
if b, ok := c.content.(Bindings); ok {
return b.BindingKeys()
}
return []key.Binding{}
}
// Focus sets the container as focused
func (c *container) Focus() {
c.focused = true
@@ -173,9 +173,19 @@ func (c *container) Blur() {
}
}
// GetPosition returns the x, y coordinates of the container
func (c *container) GetPosition() (x, y int) {
return c.x, c.y
}
// GetContent returns the content of the container
func (c *container) GetContent() ModelWithView {
return c.content
}
type ContainerOption func(*container)
func NewContainer(content tea.Model, options ...ContainerOption) Container {
func NewContainer(content ModelWithView, options ...ContainerOption) Container {
c := &container{
content: content,
borderStyle: lipgloss.NormalBorder(),

View File

@@ -1,10 +1,8 @@
package layout
import (
"github.com/charmbracelet/bubbles/key"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/sst/opencode/internal/theme"
tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/lipgloss/v2"
)
type FlexDirection int
@@ -26,9 +24,8 @@ func FlexPaneSizeFixed(size int) FlexPaneSize {
}
type FlexLayout interface {
tea.Model
ModelWithView
Sizeable
Bindings
SetPanes(panes []Container) tea.Cmd
SetPaneSizes(sizes []FlexPaneSize) tea.Cmd
SetDirection(direction FlexDirection) tea.Cmd
@@ -75,8 +72,6 @@ func (f *flexLayout) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
func (f *flexLayout) View() string {
t := theme.CurrentTheme()
if len(f.panes) == 0 {
return ""
}
@@ -94,7 +89,6 @@ func (f *flexLayout) View() string {
paneWidth,
pane.Alignment(),
pane.View(),
lipgloss.WithWhitespaceBackground(t.Background()),
)
views = append(views, view)
} else {
@@ -105,7 +99,6 @@ func (f *flexLayout) View() string {
lipgloss.Center,
pane.Alignment(),
pane.View(),
lipgloss.WithWhitespaceBackground(t.Background()),
)
views = append(views, view)
}
@@ -166,11 +159,51 @@ func (f *flexLayout) SetSize(width, height int) tea.Cmd {
f.height = height
var cmds []tea.Cmd
currentX, currentY := 0, 0
for i, pane := range f.panes {
if pane != nil {
paneWidth, paneHeight := f.calculatePaneSize(i)
// Calculate actual position based on alignment
actualX, actualY := currentX, currentY
if f.direction == FlexDirectionHorizontal {
// In horizontal layout, vertical alignment affects Y position
// (lipgloss.Center is used for vertical alignment in JoinHorizontal)
actualY = (f.height - paneHeight) / 2
} else {
// In vertical layout, horizontal alignment affects X position
contentWidth := paneWidth
if pane.MaxWidth() > 0 && contentWidth > pane.MaxWidth() {
contentWidth = pane.MaxWidth()
}
switch pane.Alignment() {
case lipgloss.Center:
actualX = (f.width - contentWidth) / 2
case lipgloss.Right:
actualX = f.width - contentWidth
case lipgloss.Left:
actualX = 0
}
}
// Set position if the pane is a *container
if c, ok := pane.(*container); ok {
c.x = actualX
c.y = actualY
}
cmd := pane.SetSize(paneWidth, paneHeight)
cmds = append(cmds, cmd)
// Update position for next pane
if f.direction == FlexDirectionHorizontal {
currentX += paneWidth
} else {
currentY += paneHeight
}
}
}
return tea.Batch(cmds...)
@@ -204,18 +237,6 @@ func (f *flexLayout) SetDirection(direction FlexDirection) tea.Cmd {
return nil
}
func (f *flexLayout) BindingKeys() []key.Binding {
keys := []key.Binding{}
for _, pane := range f.panes {
if pane != nil {
if b, ok := pane.(Bindings); ok {
keys = append(keys, b.BindingKeys()...)
}
}
}
return keys
}
func NewFlexLayout(options ...FlexLayoutOption) FlexLayout {
layout := &flexLayout{
direction: FlexDirectionHorizontal,
@@ -245,4 +266,3 @@ func WithPaneSizes(sizes ...FlexPaneSize) FlexLayoutOption {
f.sizes = sizes
}
}

View File

@@ -3,15 +3,14 @@ package layout
import (
"reflect"
"github.com/charmbracelet/bubbles/key"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/bubbles/v2/key"
tea "github.com/charmbracelet/bubbletea/v2"
)
var Current *LayoutInfo
func init() {
Current = &LayoutInfo{
Size: LayoutSizeNormal,
Viewport: Dimensions{Width: 80, Height: 25},
Container: Dimensions{Width: 80, Height: 25},
}
@@ -19,23 +18,22 @@ func init() {
type LayoutSize string
const (
LayoutSizeSmall LayoutSize = "small"
LayoutSizeNormal LayoutSize = "normal"
LayoutSizeLarge LayoutSize = "large"
)
type Dimensions struct {
Width int
Height int
}
type LayoutInfo struct {
Size LayoutSize
Viewport Dimensions
Container Dimensions
}
type Modal interface {
tea.Model
Render(background string) string
Close() tea.Cmd
}
type Focusable interface {
Focus() tea.Cmd
Blur() tea.Cmd
@@ -47,10 +45,6 @@ type Sizeable interface {
GetSize() (int, int)
}
type Bindings interface {
BindingKeys() []key.Binding
}
func KeyMapToSlice(t any) (bindings []key.Binding) {
typ := reflect.TypeOf(t)
if typ.Kind() != reflect.Struct {

View File

@@ -3,20 +3,14 @@ package layout
import (
"strings"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/lipgloss/v2"
chAnsi "github.com/charmbracelet/x/ansi"
"github.com/muesli/ansi"
"github.com/muesli/reflow/truncate"
"github.com/muesli/termenv"
"github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme"
"github.com/sst/opencode/internal/util"
)
// Most of this code is borrowed from
// https://github.com/charmbracelet/lipgloss/pull/102
// as well as the lipgloss library, with some modification for what I needed.
// Split a string into lines, additionally returning the size of the widest line.
func getLines(s string) (lines []string, widest int) {
lines = strings.Split(s, "\n")
@@ -33,40 +27,18 @@ func getLines(s string) (lines []string, widest int) {
func PlaceOverlay(
x, y int,
fg, bg string,
shadow bool, opts ...WhitespaceOption,
opts ...WhitespaceOption,
) string {
fgLines, fgWidth := getLines(fg)
bgLines, bgWidth := getLines(bg)
bgHeight := len(bgLines)
fgHeight := len(fgLines)
if shadow {
t := theme.CurrentTheme()
baseStyle := styles.BaseStyle()
var shadowbg string = ""
shadowchar := lipgloss.NewStyle().
Background(t.BackgroundElement()).
Foreground(t.Background()).
Render("░")
bgchar := baseStyle.Render(" ")
for i := 0; i <= fgHeight; i++ {
if i == 0 {
shadowbg += bgchar + strings.Repeat(bgchar, fgWidth) + "\n"
} else {
shadowbg += bgchar + strings.Repeat(shadowchar, fgWidth) + "\n"
}
}
fg = PlaceOverlay(0, 0, fg, shadowbg, false, opts...)
fgLines, fgWidth = getLines(fg)
fgHeight = len(fgLines)
}
if fgWidth >= bgWidth && fgHeight >= bgHeight {
// FIXME: return fg or bg?
return fg
}
// TODO: allow placement outside of the bg box?
x = util.Clamp(x, 0, bgWidth-fgWidth)
y = util.Clamp(y, 0, bgHeight-fgHeight)
@@ -120,13 +92,6 @@ func cutLeft(s string, cutWidth int) string {
return chAnsi.Cut(s, cutWidth, lipgloss.Width(s))
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
type whitespace struct {
style termenv.Style
chars string

View File

@@ -2,20 +2,16 @@ package page
import (
"context"
"strings"
"github.com/charmbracelet/bubbles/key"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/bubbles/v2/key"
tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/lipgloss/v2"
"github.com/sst/opencode/internal/app"
"github.com/sst/opencode/internal/completions"
"github.com/sst/opencode/internal/components/chat"
"github.com/sst/opencode/internal/components/dialog"
"github.com/sst/opencode/internal/layout"
"github.com/sst/opencode/internal/state"
"github.com/sst/opencode/internal/status"
"github.com/sst/opencode/internal/util"
"github.com/sst/opencode/pkg/client"
)
var ChatPage PageID = "chat"
@@ -26,21 +22,17 @@ type chatPage struct {
messages layout.Container
layout layout.FlexLayout
completionDialog dialog.CompletionDialog
completionManager *completions.CompletionManager
showCompletionDialog bool
}
type ChatKeyMap struct {
NewSession key.Binding
Cancel key.Binding
ToggleTools key.Binding
ShowCompletionDialog key.Binding
}
var keyMap = ChatKeyMap{
NewSession: key.NewBinding(
key.WithKeys("ctrl+n"),
key.WithHelp("ctrl+n", "new session"),
),
Cancel: key.NewBinding(
key.WithKeys("esc"),
key.WithHelp("esc", "cancel"),
@@ -70,48 +62,26 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
cmd := p.layout.SetSize(msg.Width, msg.Height)
cmds = append(cmds, cmd)
case chat.SendMsg:
p.showCompletionDialog = false
cmd := p.sendMessage(msg.Text, msg.Attachments)
if cmd != nil {
return p, cmd
}
case dialog.CommandRunCustomMsg:
// Check if the agent is busy before executing custom commands
if p.app.IsBusy() {
status.Warn("Agent is busy, please wait before executing a command...")
return p, nil
}
// Process the command content with arguments if any
content := msg.Content
if msg.Args != nil {
// Replace all named arguments with their values
for name, value := range msg.Args {
placeholder := "$" + name
content = strings.ReplaceAll(content, placeholder, value)
case dialog.CompletionDialogCloseMsg:
p.showCompletionDialog = false
case tea.KeyMsg:
switch msg.String() {
case "ctrl+c":
_, cmd := p.editor.Update(msg)
if cmd != nil {
return p, cmd
}
}
// Handle custom command execution
cmd := p.sendMessage(content, nil)
if cmd != nil {
return p, cmd
}
case dialog.CompletionDialogCloseMsg:
p.showCompletionDialog = false
p.app.SetCompletionDialogOpen(false)
case tea.KeyMsg:
switch {
case key.Matches(msg, keyMap.ShowCompletionDialog):
p.showCompletionDialog = true
p.app.SetCompletionDialogOpen(true)
// Continue sending keys to layout->chat
case key.Matches(msg, keyMap.NewSession):
p.app.Session = &client.SessionInfo{}
p.app.Messages = []client.MessageInfo{}
return p, tea.Batch(
util.CmdHandler(state.SessionClearedMsg{}),
)
case key.Matches(msg, keyMap.Cancel):
if p.app.Session.Id != "" {
// Cancel the current session's generation process
@@ -123,14 +93,22 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return p, util.CmdHandler(chat.ToggleToolMessagesMsg{})
}
}
if p.showCompletionDialog {
// Get the current text from the editor to determine which provider to use
editorModel := p.editor.GetContent().(interface{ GetValue() string })
currentInput := editorModel.GetValue()
provider := p.completionManager.GetProvider(currentInput)
p.completionDialog.SetProvider(provider)
context, contextCmd := p.completionDialog.Update(msg)
p.completionDialog = context.(dialog.CompletionDialog)
cmds = append(cmds, contextCmd)
// Doesn't forward event if enter key is pressed
// Doesn't forward event if enter key is pressed and there are completions
if keyMsg, ok := msg.(tea.KeyMsg); ok {
if keyMsg.String() == "enter" {
if keyMsg.String() == "enter" { // && !p.completionDialog.IsEmpty() {
return p, tea.Batch(cmds...)
}
}
@@ -160,36 +138,29 @@ func (p *chatPage) GetSize() (int, int) {
func (p *chatPage) View() string {
layoutView := p.layout.View()
// TODO: Fix this with our new layout
if p.showCompletionDialog {
_, layoutHeight := p.layout.GetSize()
editorWidth, editorHeight := p.editor.GetSize()
editorWidth, _ := p.editor.GetSize()
editorX, editorY := p.editor.GetPosition()
p.completionDialog.SetWidth(editorWidth)
overlay := p.completionDialog.View()
layoutView = layout.PlaceOverlay(
0,
layoutHeight-editorHeight-lipgloss.Height(overlay),
editorX,
editorY-lipgloss.Height(overlay)+2,
overlay,
layoutView,
false,
)
}
return layoutView
}
func (p *chatPage) BindingKeys() []key.Binding {
bindings := layout.KeyMapToSlice(keyMap)
bindings = append(bindings, p.messages.BindingKeys()...)
bindings = append(bindings, p.editor.BindingKeys()...)
return bindings
}
func NewChatPage(app *app.App) layout.ModelWithView {
completionManager := completions.NewCompletionManager(app)
initialProvider := completionManager.GetProvider("")
completionDialog := dialog.NewCompletionDialogComponent(initialProvider)
func NewChatPage(app *app.App) tea.Model {
cg := completions.NewFileAndFolderContextGroup()
completionDialog := dialog.NewCompletionDialogCmp(cg)
messagesContainer := layout.NewContainer(
chat.NewMessagesComponent(app),
)
@@ -198,11 +169,13 @@ func NewChatPage(app *app.App) tea.Model {
layout.WithMaxWidth(layout.Current.Container.Width),
layout.WithAlignCenter(),
)
return &chatPage{
app: app,
editor: editorContainer,
messages: messagesContainer,
completionDialog: completionDialog,
app: app,
editor: editorContainer,
messages: messagesContainer,
completionDialog: completionDialog,
completionManager: completionManager,
layout: layout.NewFlexLayout(
layout.WithPanes(messagesContainer, editorContainer),
layout.WithDirection(layout.FlexDirectionVertical),

View File

@@ -7,7 +7,7 @@ import (
type SessionSelectedMsg = *client.SessionInfo
type ModelSelectedMsg struct {
Provider client.ProviderInfo
Model client.ProviderModel
Model client.ModelInfo
}
type SessionClearedMsg struct{}
@@ -17,5 +17,3 @@ type CompactSessionMsg struct{}
type StateUpdatedMsg struct {
State map[string]any
}
// TODO: store in CONFIG/tui.yaml

View File

@@ -1,123 +1,13 @@
package styles
import (
"fmt"
"regexp"
"strings"
type TerminalInfo struct {
BackgroundIsDark bool
}
"github.com/charmbracelet/lipgloss"
)
var Terminal *TerminalInfo
var ansiEscape = regexp.MustCompile("\x1b\\[[0-9;]*m")
func getColorRGB(c lipgloss.TerminalColor) (uint8, uint8, uint8) {
r, g, b, a := c.RGBA()
// Un-premultiply alpha if needed
if a > 0 && a < 0xffff {
r = (r * 0xffff) / a
g = (g * 0xffff) / a
b = (b * 0xffff) / a
func init() {
Terminal = &TerminalInfo{
BackgroundIsDark: false,
}
// Convert from 16-bit to 8-bit color
return uint8(r >> 8), uint8(g >> 8), uint8(b >> 8)
}
// ForceReplaceBackgroundWithLipgloss replaces any ANSI background color codes
// in `input` with a single 24bit background (48;2;R;G;B).
func ForceReplaceBackgroundWithLipgloss(input string, newBgColor lipgloss.TerminalColor) string {
// Precompute our new-bg sequence once
r, g, b := getColorRGB(newBgColor)
newBg := fmt.Sprintf("48;2;%d;%d;%d", r, g, b)
return ansiEscape.ReplaceAllStringFunc(input, func(seq string) string {
const (
escPrefixLen = 2 // "\x1b["
escSuffixLen = 1 // "m"
)
raw := seq
start := escPrefixLen
end := len(raw) - escSuffixLen
var sb strings.Builder
// reserve enough space: original content minus bg codes + our newBg
sb.Grow((end - start) + len(newBg) + 2)
// scan from start..end, token by token
for i := start; i < end; {
// find the next ';' or end
j := i
for j < end && raw[j] != ';' {
j++
}
token := raw[i:j]
// fastpath: skip "48;5;N" or "48;2;R;G;B"
if len(token) == 2 && token[0] == '4' && token[1] == '8' {
k := j + 1
if k < end {
// find next token
l := k
for l < end && raw[l] != ';' {
l++
}
next := raw[k:l]
if next == "5" {
// skip "48;5;N"
m := l + 1
for m < end && raw[m] != ';' {
m++
}
i = m + 1
continue
} else if next == "2" {
// skip "48;2;R;G;B"
m := l + 1
for count := 0; count < 3 && m < end; count++ {
for m < end && raw[m] != ';' {
m++
}
m++
}
i = m
continue
}
}
}
// decide whether to keep this token
// manually parse ASCII digits to int
isNum := true
val := 0
for p := i; p < j; p++ {
c := raw[p]
if c < '0' || c > '9' {
isNum = false
break
}
val = val*10 + int(c-'0')
}
keep := !isNum ||
((val < 40 || val > 47) && (val < 100 || val > 107) && val != 49)
if keep {
if sb.Len() > 0 {
sb.WriteByte(';')
}
sb.WriteString(token)
}
// advance past this token (and the semicolon)
i = j + 1
}
// append our new background
if sb.Len() > 0 {
sb.WriteByte(';')
}
sb.WriteString(newBg)
return "\x1b[" + sb.String() + "m"
})
}

View File

@@ -3,7 +3,8 @@ package styles
import (
"github.com/charmbracelet/glamour"
"github.com/charmbracelet/glamour/ansi"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/lipgloss/v2/compat"
"github.com/lucasb-eyer/go-colorful"
"github.com/sst/opencode/internal/theme"
)
@@ -15,234 +16,267 @@ func stringPtr(s string) *string { return &s }
func uintPtr(u uint) *uint { return &u }
// returns a glamour TermRenderer configured with the current theme
func GetMarkdownRenderer(width int) *glamour.TermRenderer {
func GetMarkdownRenderer(width int, backgroundColor compat.AdaptiveColor) *glamour.TermRenderer {
r, _ := glamour.NewTermRenderer(
glamour.WithStyles(generateMarkdownStyleConfig()),
glamour.WithStyles(generateMarkdownStyleConfig(backgroundColor)),
glamour.WithWordWrap(width),
glamour.WithChromaFormatter("terminal16m"),
)
return r
}
// creates an ansi.StyleConfig for markdown rendering
// using adaptive colors from the provided theme.
func generateMarkdownStyleConfig() ansi.StyleConfig {
func generateMarkdownStyleConfig(backgroundColor compat.AdaptiveColor) ansi.StyleConfig {
t := theme.CurrentTheme()
background := stringPtr(AdaptiveColorToString(backgroundColor))
return ansi.StyleConfig{
Document: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{
BlockPrefix: "",
BlockSuffix: "",
Color: stringPtr(adaptiveColorToString(t.MarkdownText())),
BlockPrefix: "",
BlockSuffix: "",
BackgroundColor: background,
Color: stringPtr(AdaptiveColorToString(t.MarkdownText())),
},
},
BlockQuote: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{
Color: stringPtr(adaptiveColorToString(t.MarkdownBlockQuote())),
Color: stringPtr(AdaptiveColorToString(t.MarkdownBlockQuote())),
Italic: boolPtr(true),
Prefix: "┃ ",
},
Indent: uintPtr(1),
IndentToken: stringPtr(BaseStyle().Render(" ")),
IndentToken: stringPtr(" "),
},
List: ansi.StyleList{
LevelIndent: defaultMargin,
StyleBlock: ansi.StyleBlock{
IndentToken: stringPtr(BaseStyle().Render(" ")),
IndentToken: stringPtr(" "),
StylePrimitive: ansi.StylePrimitive{
Color: stringPtr(adaptiveColorToString(t.MarkdownText())),
Color: stringPtr(AdaptiveColorToString(t.MarkdownText())),
},
},
},
Heading: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{
BlockSuffix: "\n",
Color: stringPtr(adaptiveColorToString(t.MarkdownHeading())),
Color: stringPtr(AdaptiveColorToString(t.MarkdownHeading())),
Bold: boolPtr(true),
},
},
H1: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{
Prefix: "# ",
Color: stringPtr(adaptiveColorToString(t.MarkdownHeading())),
Color: stringPtr(AdaptiveColorToString(t.MarkdownHeading())),
Bold: boolPtr(true),
},
},
H2: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{
Prefix: "## ",
Color: stringPtr(adaptiveColorToString(t.MarkdownHeading())),
Color: stringPtr(AdaptiveColorToString(t.MarkdownHeading())),
Bold: boolPtr(true),
},
},
H3: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{
Prefix: "### ",
Color: stringPtr(adaptiveColorToString(t.MarkdownHeading())),
Color: stringPtr(AdaptiveColorToString(t.MarkdownHeading())),
Bold: boolPtr(true),
},
},
H4: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{
Prefix: "#### ",
Color: stringPtr(adaptiveColorToString(t.MarkdownHeading())),
Color: stringPtr(AdaptiveColorToString(t.MarkdownHeading())),
Bold: boolPtr(true),
},
},
H5: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{
Prefix: "##### ",
Color: stringPtr(adaptiveColorToString(t.MarkdownHeading())),
Color: stringPtr(AdaptiveColorToString(t.MarkdownHeading())),
Bold: boolPtr(true),
},
},
H6: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{
Prefix: "###### ",
Color: stringPtr(adaptiveColorToString(t.MarkdownHeading())),
Color: stringPtr(AdaptiveColorToString(t.MarkdownHeading())),
Bold: boolPtr(true),
},
},
Strikethrough: ansi.StylePrimitive{
CrossedOut: boolPtr(true),
Color: stringPtr(adaptiveColorToString(t.TextMuted())),
Color: stringPtr(AdaptiveColorToString(t.TextMuted())),
},
Emph: ansi.StylePrimitive{
Color: stringPtr(adaptiveColorToString(t.MarkdownEmph())),
Color: stringPtr(AdaptiveColorToString(t.MarkdownEmph())),
Italic: boolPtr(true),
},
Strong: ansi.StylePrimitive{
Bold: boolPtr(true),
Color: stringPtr(adaptiveColorToString(t.MarkdownStrong())),
Color: stringPtr(AdaptiveColorToString(t.MarkdownStrong())),
},
HorizontalRule: ansi.StylePrimitive{
Color: stringPtr(adaptiveColorToString(t.MarkdownHorizontalRule())),
Color: stringPtr(AdaptiveColorToString(t.MarkdownHorizontalRule())),
Format: "\n─────────────────────────────────────────\n",
},
Item: ansi.StylePrimitive{
BlockPrefix: "• ",
Color: stringPtr(adaptiveColorToString(t.MarkdownListItem())),
Color: stringPtr(AdaptiveColorToString(t.MarkdownListItem())),
},
Enumeration: ansi.StylePrimitive{
BlockPrefix: ". ",
Color: stringPtr(adaptiveColorToString(t.MarkdownListEnumeration())),
Color: stringPtr(AdaptiveColorToString(t.MarkdownListEnumeration())),
},
Task: ansi.StyleTask{
StylePrimitive: ansi.StylePrimitive{},
Ticked: "[] ",
Unticked: "[ ] ",
Ticked: "[✓] ",
Unticked: "[ ] ",
},
Link: ansi.StylePrimitive{
Color: stringPtr(adaptiveColorToString(t.MarkdownLink())),
Color: stringPtr(AdaptiveColorToString(t.MarkdownLink())),
Underline: boolPtr(true),
},
LinkText: ansi.StylePrimitive{
Color: stringPtr(adaptiveColorToString(t.MarkdownLinkText())),
Color: stringPtr(AdaptiveColorToString(t.MarkdownLinkText())),
Bold: boolPtr(true),
},
Image: ansi.StylePrimitive{
Color: stringPtr(adaptiveColorToString(t.MarkdownImage())),
Color: stringPtr(AdaptiveColorToString(t.MarkdownImage())),
Underline: boolPtr(true),
Format: "🖼 {{.text}}",
},
ImageText: ansi.StylePrimitive{
Color: stringPtr(adaptiveColorToString(t.MarkdownImageText())),
Color: stringPtr(AdaptiveColorToString(t.MarkdownImageText())),
Format: "{{.text}}",
},
Code: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{
Color: stringPtr(adaptiveColorToString(t.MarkdownCode())),
Prefix: "",
Suffix: "",
BackgroundColor: background,
Color: stringPtr(AdaptiveColorToString(t.MarkdownCode())),
Prefix: "",
Suffix: "",
},
},
CodeBlock: ansi.StyleCodeBlock{
StyleBlock: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{
Prefix: " ",
Color: stringPtr(adaptiveColorToString(t.MarkdownCodeBlock())),
BackgroundColor: background,
Prefix: " ",
Color: stringPtr(AdaptiveColorToString(t.MarkdownCodeBlock())),
},
},
Chroma: &ansi.Chroma{
Background: ansi.StylePrimitive{
BackgroundColor: background,
},
Text: ansi.StylePrimitive{
Color: stringPtr(adaptiveColorToString(t.MarkdownText())),
BackgroundColor: background,
Color: stringPtr(AdaptiveColorToString(t.MarkdownText())),
},
Error: ansi.StylePrimitive{
Color: stringPtr(adaptiveColorToString(t.Error())),
BackgroundColor: background,
Color: stringPtr(AdaptiveColorToString(t.Error())),
},
Comment: ansi.StylePrimitive{
Color: stringPtr(adaptiveColorToString(t.SyntaxComment())),
BackgroundColor: background,
Color: stringPtr(AdaptiveColorToString(t.SyntaxComment())),
},
CommentPreproc: ansi.StylePrimitive{
Color: stringPtr(adaptiveColorToString(t.SyntaxKeyword())),
BackgroundColor: background,
Color: stringPtr(AdaptiveColorToString(t.SyntaxKeyword())),
},
Keyword: ansi.StylePrimitive{
Color: stringPtr(adaptiveColorToString(t.SyntaxKeyword())),
BackgroundColor: background,
Color: stringPtr(AdaptiveColorToString(t.SyntaxKeyword())),
},
KeywordReserved: ansi.StylePrimitive{
Color: stringPtr(adaptiveColorToString(t.SyntaxKeyword())),
BackgroundColor: background,
Color: stringPtr(AdaptiveColorToString(t.SyntaxKeyword())),
},
KeywordNamespace: ansi.StylePrimitive{
Color: stringPtr(adaptiveColorToString(t.SyntaxKeyword())),
BackgroundColor: background,
Color: stringPtr(AdaptiveColorToString(t.SyntaxKeyword())),
},
KeywordType: ansi.StylePrimitive{
Color: stringPtr(adaptiveColorToString(t.SyntaxType())),
BackgroundColor: background,
Color: stringPtr(AdaptiveColorToString(t.SyntaxType())),
},
Operator: ansi.StylePrimitive{
Color: stringPtr(adaptiveColorToString(t.SyntaxOperator())),
BackgroundColor: background,
Color: stringPtr(AdaptiveColorToString(t.SyntaxOperator())),
},
Punctuation: ansi.StylePrimitive{
Color: stringPtr(adaptiveColorToString(t.SyntaxPunctuation())),
BackgroundColor: background,
Color: stringPtr(AdaptiveColorToString(t.SyntaxPunctuation())),
},
Name: ansi.StylePrimitive{
Color: stringPtr(adaptiveColorToString(t.SyntaxVariable())),
BackgroundColor: background,
Color: stringPtr(AdaptiveColorToString(t.SyntaxVariable())),
},
NameBuiltin: ansi.StylePrimitive{
Color: stringPtr(adaptiveColorToString(t.SyntaxVariable())),
BackgroundColor: background,
Color: stringPtr(AdaptiveColorToString(t.SyntaxVariable())),
},
NameTag: ansi.StylePrimitive{
Color: stringPtr(adaptiveColorToString(t.SyntaxKeyword())),
BackgroundColor: background,
Color: stringPtr(AdaptiveColorToString(t.SyntaxKeyword())),
},
NameAttribute: ansi.StylePrimitive{
Color: stringPtr(adaptiveColorToString(t.SyntaxFunction())),
BackgroundColor: background,
Color: stringPtr(AdaptiveColorToString(t.SyntaxFunction())),
},
NameClass: ansi.StylePrimitive{
Color: stringPtr(adaptiveColorToString(t.SyntaxType())),
BackgroundColor: background,
Color: stringPtr(AdaptiveColorToString(t.SyntaxType())),
},
NameConstant: ansi.StylePrimitive{
Color: stringPtr(adaptiveColorToString(t.SyntaxVariable())),
BackgroundColor: background,
Color: stringPtr(AdaptiveColorToString(t.SyntaxVariable())),
},
NameDecorator: ansi.StylePrimitive{
Color: stringPtr(adaptiveColorToString(t.SyntaxFunction())),
BackgroundColor: background,
Color: stringPtr(AdaptiveColorToString(t.SyntaxFunction())),
},
NameFunction: ansi.StylePrimitive{
Color: stringPtr(adaptiveColorToString(t.SyntaxFunction())),
BackgroundColor: background,
Color: stringPtr(AdaptiveColorToString(t.SyntaxFunction())),
},
LiteralNumber: ansi.StylePrimitive{
Color: stringPtr(adaptiveColorToString(t.SyntaxNumber())),
BackgroundColor: background,
Color: stringPtr(AdaptiveColorToString(t.SyntaxNumber())),
},
LiteralString: ansi.StylePrimitive{
Color: stringPtr(adaptiveColorToString(t.SyntaxString())),
BackgroundColor: background,
Color: stringPtr(AdaptiveColorToString(t.SyntaxString())),
},
LiteralStringEscape: ansi.StylePrimitive{
Color: stringPtr(adaptiveColorToString(t.SyntaxKeyword())),
BackgroundColor: background,
Color: stringPtr(AdaptiveColorToString(t.SyntaxKeyword())),
},
GenericDeleted: ansi.StylePrimitive{
Color: stringPtr(adaptiveColorToString(t.DiffRemoved())),
BackgroundColor: background,
Color: stringPtr(AdaptiveColorToString(t.DiffRemoved())),
},
GenericEmph: ansi.StylePrimitive{
Color: stringPtr(adaptiveColorToString(t.MarkdownEmph())),
Italic: boolPtr(true),
BackgroundColor: background,
Color: stringPtr(AdaptiveColorToString(t.MarkdownEmph())),
Italic: boolPtr(true),
},
GenericInserted: ansi.StylePrimitive{
Color: stringPtr(adaptiveColorToString(t.DiffAdded())),
BackgroundColor: background,
Color: stringPtr(AdaptiveColorToString(t.DiffAdded())),
},
GenericStrong: ansi.StylePrimitive{
Color: stringPtr(adaptiveColorToString(t.MarkdownStrong())),
Bold: boolPtr(true),
BackgroundColor: background,
Color: stringPtr(AdaptiveColorToString(t.MarkdownStrong())),
Bold: boolPtr(true),
},
GenericSubheading: ansi.StylePrimitive{
Color: stringPtr(adaptiveColorToString(t.MarkdownHeading())),
BackgroundColor: background,
Color: stringPtr(AdaptiveColorToString(t.MarkdownHeading())),
},
},
},
@@ -259,24 +293,26 @@ func generateMarkdownStyleConfig() ansi.StyleConfig {
},
DefinitionDescription: ansi.StylePrimitive{
BlockPrefix: "\n ",
Color: stringPtr(adaptiveColorToString(t.MarkdownLinkText())),
Color: stringPtr(AdaptiveColorToString(t.MarkdownLinkText())),
},
Text: ansi.StylePrimitive{
Color: stringPtr(adaptiveColorToString(t.MarkdownText())),
Color: stringPtr(AdaptiveColorToString(t.MarkdownText())),
},
Paragraph: ansi.StyleBlock{
StylePrimitive: ansi.StylePrimitive{
Color: stringPtr(adaptiveColorToString(t.MarkdownText())),
Color: stringPtr(AdaptiveColorToString(t.MarkdownText())),
},
},
}
}
// adaptiveColorToString converts a lipgloss.AdaptiveColor to the appropriate
// AdaptiveColorToString converts a compat.AdaptiveColor to the appropriate
// hex color string based on the current terminal background
func adaptiveColorToString(color lipgloss.AdaptiveColor) string {
if lipgloss.HasDarkBackground() {
return color.Dark
func AdaptiveColorToString(color compat.AdaptiveColor) string {
if Terminal.BackgroundIsDark {
c1, _ := colorful.MakeColor(color.Dark)
return c1.Hex()
}
return color.Light
c1, _ := colorful.MakeColor(color.Light)
return c1.Hex()
}

View File

@@ -1,16 +1,15 @@
package styles
import (
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/lipgloss/v2"
"github.com/charmbracelet/lipgloss/v2/compat"
"github.com/sst/opencode/internal/theme"
)
// 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())
return lipgloss.NewStyle().Foreground(t.Text())
}
func Panel() lipgloss.Style {
@@ -29,7 +28,7 @@ func Regular() lipgloss.Style {
func Muted() lipgloss.Style {
t := theme.CurrentTheme()
return lipgloss.NewStyle().Background(t.Background()).Foreground(t.TextMuted())
return lipgloss.NewStyle().Foreground(t.TextMuted())
}
// Bold returns a bold style
@@ -83,76 +82,76 @@ func DimBorder() lipgloss.Style {
}
// PrimaryColor returns the primary color from the current theme
func PrimaryColor() lipgloss.AdaptiveColor {
func PrimaryColor() compat.AdaptiveColor {
return theme.CurrentTheme().Primary()
}
// SecondaryColor returns the secondary color from the current theme
func SecondaryColor() lipgloss.AdaptiveColor {
func SecondaryColor() compat.AdaptiveColor {
return theme.CurrentTheme().Secondary()
}
// AccentColor returns the accent color from the current theme
func AccentColor() lipgloss.AdaptiveColor {
func AccentColor() compat.AdaptiveColor {
return theme.CurrentTheme().Accent()
}
// ErrorColor returns the error color from the current theme
func ErrorColor() lipgloss.AdaptiveColor {
func ErrorColor() compat.AdaptiveColor {
return theme.CurrentTheme().Error()
}
// WarningColor returns the warning color from the current theme
func WarningColor() lipgloss.AdaptiveColor {
func WarningColor() compat.AdaptiveColor {
return theme.CurrentTheme().Warning()
}
// SuccessColor returns the success color from the current theme
func SuccessColor() lipgloss.AdaptiveColor {
func SuccessColor() compat.AdaptiveColor {
return theme.CurrentTheme().Success()
}
// InfoColor returns the info color from the current theme
func InfoColor() lipgloss.AdaptiveColor {
func InfoColor() compat.AdaptiveColor {
return theme.CurrentTheme().Info()
}
// TextColor returns the text color from the current theme
func TextColor() lipgloss.AdaptiveColor {
func TextColor() compat.AdaptiveColor {
return theme.CurrentTheme().Text()
}
// TextMutedColor returns the muted text color from the current theme
func TextMutedColor() lipgloss.AdaptiveColor {
func TextMutedColor() compat.AdaptiveColor {
return theme.CurrentTheme().TextMuted()
}
// BackgroundColor returns the background color from the current theme
func BackgroundColor() lipgloss.AdaptiveColor {
func BackgroundColor() compat.AdaptiveColor {
return theme.CurrentTheme().Background()
}
// BackgroundSubtleColor returns the subtle background color from the current theme
func BackgroundSubtleColor() lipgloss.AdaptiveColor {
func BackgroundSubtleColor() compat.AdaptiveColor {
return theme.CurrentTheme().BackgroundSubtle()
}
// BackgroundElementColor returns the darker background color from the current theme
func BackgroundElementColor() lipgloss.AdaptiveColor {
func BackgroundElementColor() compat.AdaptiveColor {
return theme.CurrentTheme().BackgroundElement()
}
// BorderColor returns the border color from the current theme
func BorderColor() lipgloss.AdaptiveColor {
func BorderColor() compat.AdaptiveColor {
return theme.CurrentTheme().Border()
}
// BorderActiveColor returns the active border color from the current theme
func BorderActiveColor() lipgloss.AdaptiveColor {
func BorderActiveColor() compat.AdaptiveColor {
return theme.CurrentTheme().BorderActive()
}
// BorderSubtleColor returns the subtle border color from the current theme
func BorderSubtleColor() lipgloss.AdaptiveColor {
func BorderSubtleColor() compat.AdaptiveColor {
return theme.CurrentTheme().BorderSubtle()
}

View File

@@ -1,276 +0,0 @@
package theme
import (
"github.com/charmbracelet/lipgloss"
)
// AyuDarkTheme implements the Theme interface with Ayu Dark colors.
type AyuDarkTheme struct {
BaseTheme
}
// AyuLightTheme implements the Theme interface with Ayu Light colors.
type AyuLightTheme struct {
BaseTheme
}
// AyuMirageTheme implements the Theme interface with Ayu Mirage colors.
type AyuMirageTheme struct {
BaseTheme
}
// NewAyuDarkTheme creates a new instance of the Ayu Dark theme.
func NewAyuDarkTheme() *AyuDarkTheme {
// Ayu Dark color palette
darkBackground := "#0f1419"
darkCurrentLine := "#191f26"
darkSelection := "#253340"
darkForeground := "#b3b1ad"
darkComment := "#5c6773"
darkBlue := "#53bdfa"
darkCyan := "#90e1c6"
darkGreen := "#91b362"
darkOrange := "#f9af4f"
darkPurple := "#fae994"
darkRed := "#ea6c73"
darkBorder := "#253340"
// Light mode approximation for terminal compatibility
lightBackground := "#fafafa"
lightCurrentLine := "#f0f0f0"
lightSelection := "#d1d1d1"
lightForeground := "#5c6773"
lightComment := "#828c99"
lightBlue := "#3199e1"
lightCyan := "#46ba94"
lightGreen := "#7c9f32"
lightOrange := "#f29718"
lightPurple := "#9e75c7"
lightRed := "#f07171"
lightBorder := "#d1d1d1"
theme := &AyuDarkTheme{}
// Base colors
theme.PrimaryColor = lipgloss.AdaptiveColor{
Dark: darkBlue,
Light: lightBlue,
}
theme.SecondaryColor = lipgloss.AdaptiveColor{
Dark: darkCyan,
Light: lightCyan,
}
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,
}
// Background colors
theme.BackgroundColor = lipgloss.AdaptiveColor{
Dark: darkBackground,
Light: lightBackground,
}
theme.BackgroundSubtleColor = lipgloss.AdaptiveColor{
Dark: darkCurrentLine,
Light: lightCurrentLine,
}
theme.BackgroundElementColor = lipgloss.AdaptiveColor{
Dark: "#0b0e14", // Darker than background
Light: "#ffffff", // Lighter than background
}
// Border colors
theme.BorderColor = lipgloss.AdaptiveColor{
Dark: darkBorder,
Light: lightBorder,
}
theme.BorderActiveColor = lipgloss.AdaptiveColor{
Dark: darkBlue,
Light: lightBlue,
}
theme.BorderSubtleColor = 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: "#91b362",
Light: "#a5d6a7",
}
theme.DiffHighlightRemovedColor = lipgloss.AdaptiveColor{
Dark: "#ea6c73",
Light: "#ef9a9a",
}
theme.DiffAddedBgColor = lipgloss.AdaptiveColor{
Dark: "#1f2c1f",
Light: "#e8f5e9",
}
theme.DiffRemovedBgColor = lipgloss.AdaptiveColor{
Dark: "#2c1f1f",
Light: "#ffebee",
}
theme.DiffContextBgColor = lipgloss.AdaptiveColor{
Dark: darkBackground,
Light: lightBackground,
}
theme.DiffLineNumberColor = lipgloss.AdaptiveColor{
Dark: darkComment,
Light: lightComment,
}
theme.DiffAddedLineNumberBgColor = lipgloss.AdaptiveColor{
Dark: "#1a261a",
Light: "#c8e6c9",
}
theme.DiffRemovedLineNumberBgColor = lipgloss.AdaptiveColor{
Dark: "#261a1a",
Light: "#ffcdd2",
}
// Markdown colors
theme.MarkdownTextColor = lipgloss.AdaptiveColor{
Dark: darkForeground,
Light: lightForeground,
}
theme.MarkdownHeadingColor = lipgloss.AdaptiveColor{
Dark: darkBlue,
Light: lightBlue,
}
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: darkComment,
Light: lightComment,
}
theme.MarkdownEmphColor = lipgloss.AdaptiveColor{
Dark: darkPurple,
Light: lightPurple,
}
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: darkForeground,
Light: lightForeground,
}
theme.SyntaxStringColor = lipgloss.AdaptiveColor{
Dark: darkGreen,
Light: lightGreen,
}
theme.SyntaxNumberColor = lipgloss.AdaptiveColor{
Dark: darkPurple,
Light: lightPurple,
}
theme.SyntaxTypeColor = lipgloss.AdaptiveColor{
Dark: darkCyan,
Light: lightCyan,
}
theme.SyntaxOperatorColor = lipgloss.AdaptiveColor{
Dark: darkOrange,
Light: lightOrange,
}
theme.SyntaxPunctuationColor = lipgloss.AdaptiveColor{
Dark: darkForeground,
Light: lightForeground,
}
return theme
}
func init() {
// Register all three Ayu theme variants with the theme manager
RegisterTheme("ayu", NewAyuDarkTheme())
}

View File

@@ -1,244 +0,0 @@
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,
}
// Background colors
theme.BackgroundColor = lipgloss.AdaptiveColor{
Dark: "#212121", // From existing styles
Light: "#EEEEEE", // Light equivalent
}
theme.BackgroundSubtleColor = lipgloss.AdaptiveColor{
Dark: "#2c2c2c", // From existing styles
Light: "#E0E0E0", // Light equivalent
}
theme.BackgroundElementColor = lipgloss.AdaptiveColor{
Dark: "#181818", // From existing styles
Light: "#F5F5F5", // Light equivalent
}
// Border colors
theme.BorderColor = lipgloss.AdaptiveColor{
Dark: "#4b4c5c", // From existing styles
Light: "#BDBDBD", // Light equivalent
}
theme.BorderActiveColor = lipgloss.AdaptiveColor{
Dark: mocha.Blue().Hex,
Light: latte.Blue().Hex,
}
theme.BorderSubtleColor = 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())
}

View File

@@ -1,270 +0,0 @@
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,
}
// Background colors
theme.BackgroundElementColor = lipgloss.AdaptiveColor{
Dark: darkBackground,
Light: lightBackground,
}
theme.BackgroundSubtleColor = lipgloss.AdaptiveColor{
Dark: darkCurrentLine,
Light: lightCurrentLine,
}
theme.BackgroundColor = lipgloss.AdaptiveColor{
Dark: "#21222c", // Slightly darker than background
Light: "#ffffff", // Slightly lighter than background
}
// Border colors
theme.BorderColor = lipgloss.AdaptiveColor{
Dark: darkBorder,
Light: lightBorder,
}
theme.BorderActiveColor = lipgloss.AdaptiveColor{
Dark: darkPurple,
Light: lightPurple,
}
theme.BorderSubtleColor = 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: darkCurrentLine,
Light: lightCurrentLine,
}
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())
}

View File

@@ -1,278 +0,0 @@
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,
}
// Background colors
theme.BackgroundColor = lipgloss.AdaptiveColor{
Dark: flexokiBlack,
Light: flexokiPaper,
}
theme.BackgroundSubtleColor = lipgloss.AdaptiveColor{
Dark: flexokiBase950,
Light: flexokiBase50,
}
theme.BackgroundElementColor = lipgloss.AdaptiveColor{
Dark: flexokiBase900,
Light: flexokiBase100,
}
// Border colors
theme.BorderColor = lipgloss.AdaptiveColor{
Dark: flexokiBase900,
Light: flexokiBase100,
}
theme.BorderActiveColor = lipgloss.AdaptiveColor{
Dark: flexokiBlue400,
Light: flexokiBlue600,
}
theme.BorderSubtleColor = 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())
}

View File

@@ -1,298 +0,0 @@
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,
}
// Background colors
theme.BackgroundColor = lipgloss.AdaptiveColor{
Dark: gruvboxDarkBg0,
Light: gruvboxLightBg0,
}
theme.BackgroundSubtleColor = lipgloss.AdaptiveColor{
Dark: gruvboxDarkBg1,
Light: gruvboxLightBg1,
}
theme.BackgroundElementColor = lipgloss.AdaptiveColor{
Dark: gruvboxDarkBg0Soft,
Light: gruvboxLightBg0Soft,
}
// Border colors
theme.BorderColor = lipgloss.AdaptiveColor{
Dark: gruvboxDarkBg2,
Light: gruvboxLightBg2,
}
theme.BorderActiveColor = lipgloss.AdaptiveColor{
Dark: gruvboxDarkBlueBright,
Light: gruvboxLightBlueBright,
}
theme.BorderSubtleColor = 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())
}

View File

@@ -8,6 +8,7 @@ import (
"sync"
"github.com/alecthomas/chroma/v2/styles"
// "github.com/alecthomas/chroma/v2/styles"
)
// Manager handles theme registration, selection, and retrieval.
@@ -48,21 +49,6 @@ func SetTheme(name string) error {
defer globalManager.mu.Unlock()
delete(styles.Registry, "charm")
// Handle custom theme
// if name == "custom" {
// cfg := config.Get()
// if cfg == nil || cfg.TUI.CustomTheme == nil || len(cfg.TUI.CustomTheme) == 0 {
// return fmt.Errorf("custom theme selected but no custom theme colors defined in config")
// }
//
// customTheme, err := LoadCustomTheme(cfg.TUI.CustomTheme)
// if err != nil {
// return fmt.Errorf("failed to load custom theme: %w", err)
// }
//
// // Register the custom theme
// globalManager.themes["custom"] = customTheme
if _, exists := globalManager.themes[name]; !exists {
return fmt.Errorf("theme '%s' not found", name)
}

View File

@@ -1,269 +0,0 @@
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,
}
// Background colors
theme.BackgroundColor = lipgloss.AdaptiveColor{
Dark: darkBackground,
Light: lightBackground,
}
theme.BackgroundSubtleColor = lipgloss.AdaptiveColor{
Dark: darkCurrentLine,
Light: lightCurrentLine,
}
theme.BackgroundElementColor = lipgloss.AdaptiveColor{
Dark: "#221f22", // Slightly darker than background
Light: "#ffffff", // Slightly lighter than background
}
// Border colors
theme.BorderColor = lipgloss.AdaptiveColor{
Dark: darkBorder,
Light: lightBorder,
}
theme.BorderActiveColor = lipgloss.AdaptiveColor{
Dark: darkCyan,
Light: lightCyan,
}
theme.BorderSubtleColor = 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())
}

View File

@@ -1,270 +0,0 @@
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,
}
// Background colors
theme.BackgroundColor = lipgloss.AdaptiveColor{
Dark: darkBackground,
Light: lightBackground,
}
theme.BackgroundSubtleColor = lipgloss.AdaptiveColor{
Dark: darkCurrentLine,
Light: lightCurrentLine,
}
theme.BackgroundElementColor = lipgloss.AdaptiveColor{
Dark: "#21252b", // Slightly darker than background
Light: "#ffffff", // Slightly lighter than background
}
// Border colors
theme.BorderColor = lipgloss.AdaptiveColor{
Dark: darkBorder,
Light: lightBorder,
}
theme.BorderActiveColor = lipgloss.AdaptiveColor{
Dark: darkBlue,
Light: lightBlue,
}
theme.BorderSubtleColor = 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())
}

View File

@@ -1,7 +1,8 @@
package theme
import (
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/lipgloss/v2"
"github.com/charmbracelet/lipgloss/v2/compat"
)
// OpenCodeTheme implements the Theme interface with OpenCode brand colors.
@@ -72,219 +73,219 @@ func NewOpenCodeTheme() *OpenCodeTheme {
theme := &OpenCodeTheme{}
// Base colors
theme.PrimaryColor = lipgloss.AdaptiveColor{
Dark: darkPrimary,
Light: lightPrimary,
theme.PrimaryColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkPrimary),
Light: lipgloss.Color(lightPrimary),
}
theme.SecondaryColor = lipgloss.AdaptiveColor{
Dark: darkSecondary,
Light: lightSecondary,
theme.SecondaryColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkSecondary),
Light: lipgloss.Color(lightSecondary),
}
theme.AccentColor = lipgloss.AdaptiveColor{
Dark: darkAccent,
Light: lightAccent,
theme.AccentColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkAccent),
Light: lipgloss.Color(lightAccent),
}
// Status colors
theme.ErrorColor = lipgloss.AdaptiveColor{
Dark: darkRed,
Light: lightRed,
theme.ErrorColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkRed),
Light: lipgloss.Color(lightRed),
}
theme.WarningColor = lipgloss.AdaptiveColor{
Dark: darkOrange,
Light: lightOrange,
theme.WarningColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkOrange),
Light: lipgloss.Color(lightOrange),
}
theme.SuccessColor = lipgloss.AdaptiveColor{
Dark: darkGreen,
Light: lightGreen,
theme.SuccessColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkGreen),
Light: lipgloss.Color(lightGreen),
}
theme.InfoColor = lipgloss.AdaptiveColor{
Dark: darkCyan,
Light: lightCyan,
theme.InfoColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkCyan),
Light: lipgloss.Color(lightCyan),
}
// Text colors
theme.TextColor = lipgloss.AdaptiveColor{
Dark: darkStep12,
Light: lightStep12,
theme.TextColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep12),
Light: lipgloss.Color(lightStep12),
}
theme.TextMutedColor = lipgloss.AdaptiveColor{
Dark: darkStep11,
Light: lightStep11,
theme.TextMutedColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep11),
Light: lipgloss.Color(lightStep11),
}
// Background colors
theme.BackgroundColor = lipgloss.AdaptiveColor{
Dark: darkStep1,
Light: lightStep1,
theme.BackgroundColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep1),
Light: lipgloss.Color(lightStep1),
}
theme.BackgroundSubtleColor = lipgloss.AdaptiveColor{
Dark: darkStep2,
Light: lightStep2,
theme.BackgroundSubtleColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep2),
Light: lipgloss.Color(lightStep2),
}
theme.BackgroundElementColor = lipgloss.AdaptiveColor{
Dark: darkStep3,
Light: lightStep3,
theme.BackgroundElementColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep3),
Light: lipgloss.Color(lightStep3),
}
// Border colors
theme.BorderColor = lipgloss.AdaptiveColor{
Dark: darkStep7,
Light: lightStep7,
theme.BorderColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep7),
Light: lipgloss.Color(lightStep7),
}
theme.BorderActiveColor = lipgloss.AdaptiveColor{
Dark: darkStep8,
Light: lightStep8,
theme.BorderActiveColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep8),
Light: lipgloss.Color(lightStep8),
}
theme.BorderSubtleColor = lipgloss.AdaptiveColor{
Dark: darkStep6,
Light: lightStep6,
theme.BorderSubtleColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep6),
Light: lipgloss.Color(lightStep6),
}
// Diff view colors
theme.DiffAddedColor = lipgloss.AdaptiveColor{
Dark: "#478247",
Light: "#2E7D32",
theme.DiffAddedColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#478247"),
Light: lipgloss.Color("#2E7D32"),
}
theme.DiffRemovedColor = lipgloss.AdaptiveColor{
Dark: "#7C4444",
Light: "#C62828",
theme.DiffRemovedColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#7C4444"),
Light: lipgloss.Color("#C62828"),
}
theme.DiffContextColor = lipgloss.AdaptiveColor{
Dark: "#a0a0a0",
Light: "#757575",
theme.DiffContextColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#a0a0a0"),
Light: lipgloss.Color("#757575"),
}
theme.DiffHunkHeaderColor = lipgloss.AdaptiveColor{
Dark: "#a0a0a0",
Light: "#757575",
theme.DiffHunkHeaderColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#a0a0a0"),
Light: lipgloss.Color("#757575"),
}
theme.DiffHighlightAddedColor = lipgloss.AdaptiveColor{
Dark: "#DAFADA",
Light: "#A5D6A7",
theme.DiffHighlightAddedColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#DAFADA"),
Light: lipgloss.Color("#A5D6A7"),
}
theme.DiffHighlightRemovedColor = lipgloss.AdaptiveColor{
Dark: "#FADADD",
Light: "#EF9A9A",
theme.DiffHighlightRemovedColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#FADADD"),
Light: lipgloss.Color("#EF9A9A"),
}
theme.DiffAddedBgColor = lipgloss.AdaptiveColor{
Dark: "#303A30",
Light: "#E8F5E9",
theme.DiffAddedBgColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#303A30"),
Light: lipgloss.Color("#E8F5E9"),
}
theme.DiffRemovedBgColor = lipgloss.AdaptiveColor{
Dark: "#3A3030",
Light: "#FFEBEE",
theme.DiffRemovedBgColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#3A3030"),
Light: lipgloss.Color("#FFEBEE"),
}
theme.DiffContextBgColor = lipgloss.AdaptiveColor{
Dark: darkStep2,
Light: lightStep2,
theme.DiffContextBgColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep2),
Light: lipgloss.Color(lightStep2),
}
theme.DiffLineNumberColor = lipgloss.AdaptiveColor{
Dark: darkStep3,
Light: lightStep3,
theme.DiffLineNumberColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep3),
Light: lipgloss.Color(lightStep3),
}
theme.DiffAddedLineNumberBgColor = lipgloss.AdaptiveColor{
Dark: "#293229",
Light: "#C8E6C9",
theme.DiffAddedLineNumberBgColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#293229"),
Light: lipgloss.Color("#C8E6C9"),
}
theme.DiffRemovedLineNumberBgColor = lipgloss.AdaptiveColor{
Dark: "#332929",
Light: "#FFCDD2",
theme.DiffRemovedLineNumberBgColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#332929"),
Light: lipgloss.Color("#FFCDD2"),
}
// Markdown colors
theme.MarkdownTextColor = lipgloss.AdaptiveColor{
Dark: darkStep12,
Light: lightStep12,
theme.MarkdownTextColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep12),
Light: lipgloss.Color(lightStep12),
}
theme.MarkdownHeadingColor = lipgloss.AdaptiveColor{
Dark: darkSecondary,
Light: lightSecondary,
theme.MarkdownHeadingColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkSecondary),
Light: lipgloss.Color(lightSecondary),
}
theme.MarkdownLinkColor = lipgloss.AdaptiveColor{
Dark: darkPrimary,
Light: lightPrimary,
theme.MarkdownLinkColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkPrimary),
Light: lipgloss.Color(lightPrimary),
}
theme.MarkdownLinkTextColor = lipgloss.AdaptiveColor{
Dark: darkCyan,
Light: lightCyan,
theme.MarkdownLinkTextColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkCyan),
Light: lipgloss.Color(lightCyan),
}
theme.MarkdownCodeColor = lipgloss.AdaptiveColor{
Dark: darkGreen,
Light: lightGreen,
theme.MarkdownCodeColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkGreen),
Light: lipgloss.Color(lightGreen),
}
theme.MarkdownBlockQuoteColor = lipgloss.AdaptiveColor{
Dark: darkYellow,
Light: lightYellow,
theme.MarkdownBlockQuoteColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkYellow),
Light: lipgloss.Color(lightYellow),
}
theme.MarkdownEmphColor = lipgloss.AdaptiveColor{
Dark: darkYellow,
Light: lightYellow,
theme.MarkdownEmphColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkYellow),
Light: lipgloss.Color(lightYellow),
}
theme.MarkdownStrongColor = lipgloss.AdaptiveColor{
Dark: darkAccent,
Light: lightAccent,
theme.MarkdownStrongColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkAccent),
Light: lipgloss.Color(lightAccent),
}
theme.MarkdownHorizontalRuleColor = lipgloss.AdaptiveColor{
Dark: darkStep11,
Light: lightStep11,
theme.MarkdownHorizontalRuleColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep11),
Light: lipgloss.Color(lightStep11),
}
theme.MarkdownListItemColor = lipgloss.AdaptiveColor{
Dark: darkPrimary,
Light: lightPrimary,
theme.MarkdownListItemColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkPrimary),
Light: lipgloss.Color(lightPrimary),
}
theme.MarkdownListEnumerationColor = lipgloss.AdaptiveColor{
Dark: darkCyan,
Light: lightCyan,
theme.MarkdownListEnumerationColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkCyan),
Light: lipgloss.Color(lightCyan),
}
theme.MarkdownImageColor = lipgloss.AdaptiveColor{
Dark: darkPrimary,
Light: lightPrimary,
theme.MarkdownImageColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkPrimary),
Light: lipgloss.Color(lightPrimary),
}
theme.MarkdownImageTextColor = lipgloss.AdaptiveColor{
Dark: darkCyan,
Light: lightCyan,
theme.MarkdownImageTextColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkCyan),
Light: lipgloss.Color(lightCyan),
}
theme.MarkdownCodeBlockColor = lipgloss.AdaptiveColor{
Dark: darkStep12,
Light: lightStep12,
theme.MarkdownCodeBlockColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep12),
Light: lipgloss.Color(lightStep12),
}
// Syntax highlighting colors
theme.SyntaxCommentColor = lipgloss.AdaptiveColor{
Dark: darkStep11,
Light: lightStep11,
theme.SyntaxCommentColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep11),
Light: lipgloss.Color(lightStep11),
}
theme.SyntaxKeywordColor = lipgloss.AdaptiveColor{
Dark: darkSecondary,
Light: lightSecondary,
theme.SyntaxKeywordColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkPrimary),
Light: lipgloss.Color(lightPrimary),
}
theme.SyntaxFunctionColor = lipgloss.AdaptiveColor{
Dark: darkPrimary,
Light: lightPrimary,
theme.SyntaxFunctionColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkPrimary),
Light: lipgloss.Color(lightPrimary),
}
theme.SyntaxVariableColor = lipgloss.AdaptiveColor{
Dark: darkRed,
Light: lightRed,
theme.SyntaxVariableColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkRed),
Light: lipgloss.Color(lightRed),
}
theme.SyntaxStringColor = lipgloss.AdaptiveColor{
Dark: darkGreen,
Light: lightGreen,
theme.SyntaxStringColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkGreen),
Light: lipgloss.Color(lightGreen),
}
theme.SyntaxNumberColor = lipgloss.AdaptiveColor{
Dark: darkAccent,
Light: lightAccent,
theme.SyntaxNumberColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkAccent),
Light: lipgloss.Color(lightAccent),
}
theme.SyntaxTypeColor = lipgloss.AdaptiveColor{
Dark: darkYellow,
Light: lightYellow,
theme.SyntaxTypeColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkYellow),
Light: lipgloss.Color(lightYellow),
}
theme.SyntaxOperatorColor = lipgloss.AdaptiveColor{
Dark: darkCyan,
Light: lightCyan,
theme.SyntaxOperatorColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkCyan),
Light: lipgloss.Color(lightCyan),
}
theme.SyntaxPunctuationColor = lipgloss.AdaptiveColor{
Dark: darkStep12,
Light: lightStep12,
theme.SyntaxPunctuationColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep12),
Light: lipgloss.Color(lightStep12),
}
return theme

View File

@@ -4,231 +4,232 @@ import (
"fmt"
"regexp"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/lipgloss/v2"
"github.com/charmbracelet/lipgloss/v2/compat"
)
// Theme defines the interface for all UI themes in the application.
// All colors must be defined as lipgloss.AdaptiveColor to support
// All colors must be defined as compat.AdaptiveColor to support
// both light and dark terminal backgrounds.
type Theme interface {
// Background colors
Background() lipgloss.AdaptiveColor // Radix 1
BackgroundSubtle() lipgloss.AdaptiveColor // Radix 2
BackgroundElement() lipgloss.AdaptiveColor // Radix 3
Background() compat.AdaptiveColor // Radix 1
BackgroundSubtle() compat.AdaptiveColor // Radix 2
BackgroundElement() compat.AdaptiveColor // Radix 3
// Border colors
BorderSubtle() lipgloss.AdaptiveColor // Radix 6
Border() lipgloss.AdaptiveColor // Radix 7
BorderActive() lipgloss.AdaptiveColor // Radix 8
BorderSubtle() compat.AdaptiveColor // Radix 6
Border() compat.AdaptiveColor // Radix 7
BorderActive() compat.AdaptiveColor // Radix 8
// Brand colors
Primary() lipgloss.AdaptiveColor // Radix 9
Secondary() lipgloss.AdaptiveColor
Accent() lipgloss.AdaptiveColor
Primary() compat.AdaptiveColor // Radix 9
Secondary() compat.AdaptiveColor
Accent() compat.AdaptiveColor
// Text colors
TextMuted() lipgloss.AdaptiveColor // Radix 11
Text() lipgloss.AdaptiveColor // Radix 12
TextMuted() compat.AdaptiveColor // Radix 11
Text() compat.AdaptiveColor // Radix 12
// Status colors
Error() lipgloss.AdaptiveColor
Warning() lipgloss.AdaptiveColor
Success() lipgloss.AdaptiveColor
Info() lipgloss.AdaptiveColor
Error() compat.AdaptiveColor
Warning() compat.AdaptiveColor
Success() compat.AdaptiveColor
Info() compat.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
DiffAdded() compat.AdaptiveColor
DiffRemoved() compat.AdaptiveColor
DiffContext() compat.AdaptiveColor
DiffHunkHeader() compat.AdaptiveColor
DiffHighlightAdded() compat.AdaptiveColor
DiffHighlightRemoved() compat.AdaptiveColor
DiffAddedBg() compat.AdaptiveColor
DiffRemovedBg() compat.AdaptiveColor
DiffContextBg() compat.AdaptiveColor
DiffLineNumber() compat.AdaptiveColor
DiffAddedLineNumberBg() compat.AdaptiveColor
DiffRemovedLineNumberBg() compat.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
MarkdownText() compat.AdaptiveColor
MarkdownHeading() compat.AdaptiveColor
MarkdownLink() compat.AdaptiveColor
MarkdownLinkText() compat.AdaptiveColor
MarkdownCode() compat.AdaptiveColor
MarkdownBlockQuote() compat.AdaptiveColor
MarkdownEmph() compat.AdaptiveColor
MarkdownStrong() compat.AdaptiveColor
MarkdownHorizontalRule() compat.AdaptiveColor
MarkdownListItem() compat.AdaptiveColor
MarkdownListEnumeration() compat.AdaptiveColor
MarkdownImage() compat.AdaptiveColor
MarkdownImageText() compat.AdaptiveColor
MarkdownCodeBlock() compat.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
SyntaxComment() compat.AdaptiveColor
SyntaxKeyword() compat.AdaptiveColor
SyntaxFunction() compat.AdaptiveColor
SyntaxVariable() compat.AdaptiveColor
SyntaxString() compat.AdaptiveColor
SyntaxNumber() compat.AdaptiveColor
SyntaxType() compat.AdaptiveColor
SyntaxOperator() compat.AdaptiveColor
SyntaxPunctuation() compat.AdaptiveColor
}
// BaseTheme provides a default implementation of the Theme interface
// that can be embedded in concrete theme implementations.
type BaseTheme struct {
// Background colors
BackgroundColor lipgloss.AdaptiveColor
BackgroundSubtleColor lipgloss.AdaptiveColor
BackgroundElementColor lipgloss.AdaptiveColor
BackgroundColor compat.AdaptiveColor
BackgroundSubtleColor compat.AdaptiveColor
BackgroundElementColor compat.AdaptiveColor
// Border colors
BorderSubtleColor lipgloss.AdaptiveColor
BorderColor lipgloss.AdaptiveColor
BorderActiveColor lipgloss.AdaptiveColor
BorderSubtleColor compat.AdaptiveColor
BorderColor compat.AdaptiveColor
BorderActiveColor compat.AdaptiveColor
// Brand colors
PrimaryColor lipgloss.AdaptiveColor
SecondaryColor lipgloss.AdaptiveColor
AccentColor lipgloss.AdaptiveColor
PrimaryColor compat.AdaptiveColor
SecondaryColor compat.AdaptiveColor
AccentColor compat.AdaptiveColor
// Text colors
TextMutedColor lipgloss.AdaptiveColor
TextColor lipgloss.AdaptiveColor
TextMutedColor compat.AdaptiveColor
TextColor compat.AdaptiveColor
// Status colors
ErrorColor lipgloss.AdaptiveColor
WarningColor lipgloss.AdaptiveColor
SuccessColor lipgloss.AdaptiveColor
InfoColor lipgloss.AdaptiveColor
ErrorColor compat.AdaptiveColor
WarningColor compat.AdaptiveColor
SuccessColor compat.AdaptiveColor
InfoColor compat.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
DiffAddedColor compat.AdaptiveColor
DiffRemovedColor compat.AdaptiveColor
DiffContextColor compat.AdaptiveColor
DiffHunkHeaderColor compat.AdaptiveColor
DiffHighlightAddedColor compat.AdaptiveColor
DiffHighlightRemovedColor compat.AdaptiveColor
DiffAddedBgColor compat.AdaptiveColor
DiffRemovedBgColor compat.AdaptiveColor
DiffContextBgColor compat.AdaptiveColor
DiffLineNumberColor compat.AdaptiveColor
DiffAddedLineNumberBgColor compat.AdaptiveColor
DiffRemovedLineNumberBgColor compat.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
MarkdownTextColor compat.AdaptiveColor
MarkdownHeadingColor compat.AdaptiveColor
MarkdownLinkColor compat.AdaptiveColor
MarkdownLinkTextColor compat.AdaptiveColor
MarkdownCodeColor compat.AdaptiveColor
MarkdownBlockQuoteColor compat.AdaptiveColor
MarkdownEmphColor compat.AdaptiveColor
MarkdownStrongColor compat.AdaptiveColor
MarkdownHorizontalRuleColor compat.AdaptiveColor
MarkdownListItemColor compat.AdaptiveColor
MarkdownListEnumerationColor compat.AdaptiveColor
MarkdownImageColor compat.AdaptiveColor
MarkdownImageTextColor compat.AdaptiveColor
MarkdownCodeBlockColor compat.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
SyntaxCommentColor compat.AdaptiveColor
SyntaxKeywordColor compat.AdaptiveColor
SyntaxFunctionColor compat.AdaptiveColor
SyntaxVariableColor compat.AdaptiveColor
SyntaxStringColor compat.AdaptiveColor
SyntaxNumberColor compat.AdaptiveColor
SyntaxTypeColor compat.AdaptiveColor
SyntaxOperatorColor compat.AdaptiveColor
SyntaxPunctuationColor compat.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) Primary() compat.AdaptiveColor { return t.PrimaryColor }
func (t *BaseTheme) Secondary() compat.AdaptiveColor { return t.SecondaryColor }
func (t *BaseTheme) Accent() compat.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) Error() compat.AdaptiveColor { return t.ErrorColor }
func (t *BaseTheme) Warning() compat.AdaptiveColor { return t.WarningColor }
func (t *BaseTheme) Success() compat.AdaptiveColor { return t.SuccessColor }
func (t *BaseTheme) Info() compat.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) Text() compat.AdaptiveColor { return t.TextColor }
func (t *BaseTheme) TextMuted() compat.AdaptiveColor { return t.TextMutedColor }
func (t *BaseTheme) Background() lipgloss.AdaptiveColor { return t.BackgroundColor }
func (t *BaseTheme) BackgroundSubtle() lipgloss.AdaptiveColor { return t.BackgroundSubtleColor }
func (t *BaseTheme) BackgroundElement() lipgloss.AdaptiveColor { return t.BackgroundElementColor }
func (t *BaseTheme) Background() compat.AdaptiveColor { return t.BackgroundColor }
func (t *BaseTheme) BackgroundSubtle() compat.AdaptiveColor { return t.BackgroundSubtleColor }
func (t *BaseTheme) BackgroundElement() compat.AdaptiveColor { return t.BackgroundElementColor }
func (t *BaseTheme) Border() lipgloss.AdaptiveColor { return t.BorderColor }
func (t *BaseTheme) BorderActive() lipgloss.AdaptiveColor { return t.BorderActiveColor }
func (t *BaseTheme) BorderSubtle() lipgloss.AdaptiveColor { return t.BorderSubtleColor }
func (t *BaseTheme) Border() compat.AdaptiveColor { return t.BorderColor }
func (t *BaseTheme) BorderActive() compat.AdaptiveColor { return t.BorderActiveColor }
func (t *BaseTheme) BorderSubtle() compat.AdaptiveColor { return t.BorderSubtleColor }
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 {
func (t *BaseTheme) DiffAdded() compat.AdaptiveColor { return t.DiffAddedColor }
func (t *BaseTheme) DiffRemoved() compat.AdaptiveColor { return t.DiffRemovedColor }
func (t *BaseTheme) DiffContext() compat.AdaptiveColor { return t.DiffContextColor }
func (t *BaseTheme) DiffHunkHeader() compat.AdaptiveColor { return t.DiffHunkHeaderColor }
func (t *BaseTheme) DiffHighlightAdded() compat.AdaptiveColor { return t.DiffHighlightAddedColor }
func (t *BaseTheme) DiffHighlightRemoved() compat.AdaptiveColor { return t.DiffHighlightRemovedColor }
func (t *BaseTheme) DiffAddedBg() compat.AdaptiveColor { return t.DiffAddedBgColor }
func (t *BaseTheme) DiffRemovedBg() compat.AdaptiveColor { return t.DiffRemovedBgColor }
func (t *BaseTheme) DiffContextBg() compat.AdaptiveColor { return t.DiffContextBgColor }
func (t *BaseTheme) DiffLineNumber() compat.AdaptiveColor { return t.DiffLineNumberColor }
func (t *BaseTheme) DiffAddedLineNumberBg() compat.AdaptiveColor {
return t.DiffAddedLineNumberBgColor
}
func (t *BaseTheme) DiffRemovedLineNumberBg() lipgloss.AdaptiveColor {
func (t *BaseTheme) DiffRemovedLineNumberBg() compat.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 {
func (t *BaseTheme) MarkdownText() compat.AdaptiveColor { return t.MarkdownTextColor }
func (t *BaseTheme) MarkdownHeading() compat.AdaptiveColor { return t.MarkdownHeadingColor }
func (t *BaseTheme) MarkdownLink() compat.AdaptiveColor { return t.MarkdownLinkColor }
func (t *BaseTheme) MarkdownLinkText() compat.AdaptiveColor { return t.MarkdownLinkTextColor }
func (t *BaseTheme) MarkdownCode() compat.AdaptiveColor { return t.MarkdownCodeColor }
func (t *BaseTheme) MarkdownBlockQuote() compat.AdaptiveColor { return t.MarkdownBlockQuoteColor }
func (t *BaseTheme) MarkdownEmph() compat.AdaptiveColor { return t.MarkdownEmphColor }
func (t *BaseTheme) MarkdownStrong() compat.AdaptiveColor { return t.MarkdownStrongColor }
func (t *BaseTheme) MarkdownHorizontalRule() compat.AdaptiveColor {
return t.MarkdownHorizontalRuleColor
}
func (t *BaseTheme) MarkdownListItem() lipgloss.AdaptiveColor { return t.MarkdownListItemColor }
func (t *BaseTheme) MarkdownListEnumeration() lipgloss.AdaptiveColor {
func (t *BaseTheme) MarkdownListItem() compat.AdaptiveColor { return t.MarkdownListItemColor }
func (t *BaseTheme) MarkdownListEnumeration() compat.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) MarkdownImage() compat.AdaptiveColor { return t.MarkdownImageColor }
func (t *BaseTheme) MarkdownImageText() compat.AdaptiveColor { return t.MarkdownImageTextColor }
func (t *BaseTheme) MarkdownCodeBlock() compat.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 }
func (t *BaseTheme) SyntaxComment() compat.AdaptiveColor { return t.SyntaxCommentColor }
func (t *BaseTheme) SyntaxKeyword() compat.AdaptiveColor { return t.SyntaxKeywordColor }
func (t *BaseTheme) SyntaxFunction() compat.AdaptiveColor { return t.SyntaxFunctionColor }
func (t *BaseTheme) SyntaxVariable() compat.AdaptiveColor { return t.SyntaxVariableColor }
func (t *BaseTheme) SyntaxString() compat.AdaptiveColor { return t.SyntaxStringColor }
func (t *BaseTheme) SyntaxNumber() compat.AdaptiveColor { return t.SyntaxNumberColor }
func (t *BaseTheme) SyntaxType() compat.AdaptiveColor { return t.SyntaxTypeColor }
func (t *BaseTheme) SyntaxOperator() compat.AdaptiveColor { return t.SyntaxOperatorColor }
func (t *BaseTheme) SyntaxPunctuation() compat.AdaptiveColor { return t.SyntaxPunctuationColor }
// ParseAdaptiveColor parses a color value from the config file into a lipgloss.AdaptiveColor.
// ParseAdaptiveColor parses a color value from the config file into a compat.AdaptiveColor.
// It accepts either a string (hex color) or a map with "dark" and "light" keys.
func ParseAdaptiveColor(value any) (lipgloss.AdaptiveColor, error) {
func ParseAdaptiveColor(value any) (compat.AdaptiveColor, error) {
// Regular expression to validate hex color format
hexColorRegex := regexp.MustCompile(`^#[0-9a-fA-F]{6}$`)
// Case 1: String value (same color for both dark and light modes)
if hexColor, ok := value.(string); ok {
if !hexColorRegex.MatchString(hexColor) {
return lipgloss.AdaptiveColor{}, fmt.Errorf("invalid hex color format: %s", hexColor)
return compat.AdaptiveColor{}, fmt.Errorf("invalid hex color format: %s", hexColor)
}
return lipgloss.AdaptiveColor{
Dark: hexColor,
Light: hexColor,
return compat.AdaptiveColor{
Dark: lipgloss.Color(hexColor),
Light: lipgloss.Color(hexColor),
}, nil
}
@@ -236,11 +237,11 @@ func ParseAdaptiveColor(value any) (lipgloss.AdaptiveColor, error) {
if numericVal, ok := value.(float64); ok {
intVal := int(numericVal)
if intVal < 0 || intVal > 255 {
return lipgloss.AdaptiveColor{}, fmt.Errorf("invalid int color value (must be between 0 and 255): %d", intVal)
return compat.AdaptiveColor{}, fmt.Errorf("invalid int color value (must be between 0 and 255): %d", intVal)
}
return lipgloss.AdaptiveColor{
Dark: fmt.Sprintf("%d", intVal),
Light: fmt.Sprintf("%d", intVal),
return compat.AdaptiveColor{
Dark: lipgloss.Color(fmt.Sprintf("%d", intVal)),
Light: lipgloss.Color(fmt.Sprintf("%d", intVal)),
}, nil
}
@@ -250,7 +251,7 @@ func ParseAdaptiveColor(value any) (lipgloss.AdaptiveColor, error) {
lightVal, lightOk := colorMap["light"]
if !darkOk || !lightOk {
return lipgloss.AdaptiveColor{}, fmt.Errorf("color map must contain both 'dark' and 'light' keys")
return compat.AdaptiveColor{}, fmt.Errorf("color map must contain both 'dark' and 'light' keys")
}
darkHex, darkIsString := darkVal.(string)
@@ -261,27 +262,27 @@ func ParseAdaptiveColor(value any) (lipgloss.AdaptiveColor, error) {
lightVal, lightIsNumber := lightVal.(float64)
if !darkIsNumber || !lightIsNumber {
return lipgloss.AdaptiveColor{}, fmt.Errorf("color map values must be strings or ints")
return compat.AdaptiveColor{}, fmt.Errorf("color map values must be strings or ints")
}
darkInt := int(darkVal)
lightInt := int(lightVal)
return lipgloss.AdaptiveColor{
Dark: fmt.Sprintf("%d", darkInt),
Light: fmt.Sprintf("%d", lightInt),
return compat.AdaptiveColor{
Dark: lipgloss.Color(fmt.Sprintf("%d", darkInt)),
Light: lipgloss.Color(fmt.Sprintf("%d", lightInt)),
}, nil
}
if !hexColorRegex.MatchString(darkHex) || !hexColorRegex.MatchString(lightHex) {
return lipgloss.AdaptiveColor{}, fmt.Errorf("invalid hex color format")
return compat.AdaptiveColor{}, fmt.Errorf("invalid hex color format")
}
return lipgloss.AdaptiveColor{
Dark: darkHex,
Light: lightHex,
return compat.AdaptiveColor{
Dark: lipgloss.Color(darkHex),
Light: lipgloss.Color(lightHex),
}, nil
}
return lipgloss.AdaptiveColor{}, fmt.Errorf("color must be either a hex string or an object with dark/light keys")
return compat.AdaptiveColor{}, fmt.Errorf("color must be either a hex string or an object with dark/light keys")
}

View File

@@ -1,7 +1,8 @@
package theme
import (
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/lipgloss/v2"
"github.com/charmbracelet/lipgloss/v2/compat"
)
// TokyoNightTheme implements the Theme interface with Tokyo Night colors.
@@ -70,219 +71,219 @@ func NewTokyoNightTheme() *TokyoNightTheme {
theme := &TokyoNightTheme{}
// Base colors
theme.PrimaryColor = lipgloss.AdaptiveColor{
Dark: darkBlue,
Light: lightBlue,
theme.PrimaryColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkBlue),
Light: lipgloss.Color(lightBlue),
}
theme.SecondaryColor = lipgloss.AdaptiveColor{
Dark: darkPurple,
Light: lightPurple,
theme.SecondaryColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkPurple),
Light: lipgloss.Color(lightPurple),
}
theme.AccentColor = lipgloss.AdaptiveColor{
Dark: darkOrange,
Light: lightOrange,
theme.AccentColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkOrange),
Light: lipgloss.Color(lightOrange),
}
// Status colors
theme.ErrorColor = lipgloss.AdaptiveColor{
Dark: darkRed,
Light: lightRed,
theme.ErrorColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkRed),
Light: lipgloss.Color(lightRed),
}
theme.WarningColor = lipgloss.AdaptiveColor{
Dark: darkOrange,
Light: lightOrange,
theme.WarningColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkOrange),
Light: lipgloss.Color(lightOrange),
}
theme.SuccessColor = lipgloss.AdaptiveColor{
Dark: darkGreen,
Light: lightGreen,
theme.SuccessColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkGreen),
Light: lipgloss.Color(lightGreen),
}
theme.InfoColor = lipgloss.AdaptiveColor{
Dark: darkBlue,
Light: lightBlue,
theme.InfoColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkBlue),
Light: lipgloss.Color(lightBlue),
}
// Text colors
theme.TextColor = lipgloss.AdaptiveColor{
Dark: darkStep12,
Light: lightStep12,
theme.TextColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep12),
Light: lipgloss.Color(lightStep12),
}
theme.TextMutedColor = lipgloss.AdaptiveColor{
Dark: darkStep11,
Light: lightStep11,
theme.TextMutedColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep11),
Light: lipgloss.Color(lightStep11),
}
// Background colors
theme.BackgroundColor = lipgloss.AdaptiveColor{
Dark: darkStep1,
Light: lightStep1,
theme.BackgroundColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep1),
Light: lipgloss.Color(lightStep1),
}
theme.BackgroundSubtleColor = lipgloss.AdaptiveColor{
Dark: darkStep2,
Light: lightStep2,
theme.BackgroundSubtleColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep2),
Light: lipgloss.Color(lightStep2),
}
theme.BackgroundElementColor = lipgloss.AdaptiveColor{
Dark: darkStep3,
Light: lightStep3,
theme.BackgroundElementColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep3),
Light: lipgloss.Color(lightStep3),
}
// Border colors
theme.BorderColor = lipgloss.AdaptiveColor{
Dark: darkStep7,
Light: lightStep7,
theme.BorderColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep7),
Light: lipgloss.Color(lightStep7),
}
theme.BorderActiveColor = lipgloss.AdaptiveColor{
Dark: darkStep8,
Light: lightStep8,
theme.BorderActiveColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep8),
Light: lipgloss.Color(lightStep8),
}
theme.BorderSubtleColor = lipgloss.AdaptiveColor{
Dark: darkStep6,
Light: lightStep6,
theme.BorderSubtleColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep6),
Light: lipgloss.Color(lightStep6),
}
// Diff view colors
theme.DiffAddedColor = lipgloss.AdaptiveColor{
Dark: "#4fd6be", // teal from palette
Light: "#1e725c",
theme.DiffAddedColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#4fd6be"), // teal from palette
Light: lipgloss.Color("#1e725c"),
}
theme.DiffRemovedColor = lipgloss.AdaptiveColor{
Dark: "#c53b53", // red1 from palette
Light: "#c53b53",
theme.DiffRemovedColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#c53b53"), // red1 from palette
Light: lipgloss.Color("#c53b53"),
}
theme.DiffContextColor = lipgloss.AdaptiveColor{
Dark: "#828bb8", // fg_dark from palette
Light: "#7086b5",
theme.DiffContextColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#828bb8"), // fg_dark from palette
Light: lipgloss.Color("#7086b5"),
}
theme.DiffHunkHeaderColor = lipgloss.AdaptiveColor{
Dark: "#828bb8", // fg_dark from palette
Light: "#7086b5",
theme.DiffHunkHeaderColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#828bb8"), // fg_dark from palette
Light: lipgloss.Color("#7086b5"),
}
theme.DiffHighlightAddedColor = lipgloss.AdaptiveColor{
Dark: "#b8db87", // git.add from palette
Light: "#4db380",
theme.DiffHighlightAddedColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#b8db87"), // git.add from palette
Light: lipgloss.Color("#4db380"),
}
theme.DiffHighlightRemovedColor = lipgloss.AdaptiveColor{
Dark: "#e26a75", // git.delete from palette
Light: "#f52a65",
theme.DiffHighlightRemovedColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#e26a75"), // git.delete from palette
Light: lipgloss.Color("#f52a65"),
}
theme.DiffAddedBgColor = lipgloss.AdaptiveColor{
Dark: "#20303b",
Light: "#d5e5d5",
theme.DiffAddedBgColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#20303b"),
Light: lipgloss.Color("#d5e5d5"),
}
theme.DiffRemovedBgColor = lipgloss.AdaptiveColor{
Dark: "#37222c",
Light: "#f7d8db",
theme.DiffRemovedBgColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#37222c"),
Light: lipgloss.Color("#f7d8db"),
}
theme.DiffContextBgColor = lipgloss.AdaptiveColor{
Dark: darkStep2,
Light: lightStep2,
theme.DiffContextBgColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep2),
Light: lipgloss.Color(lightStep2),
}
theme.DiffLineNumberColor = lipgloss.AdaptiveColor{
Dark: darkStep3, // dark3 from palette
Light: lightStep3,
theme.DiffLineNumberColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep3), // dark3 from palette
Light: lipgloss.Color(lightStep3),
}
theme.DiffAddedLineNumberBgColor = lipgloss.AdaptiveColor{
Dark: "#1b2b34",
Light: "#c5d5c5",
theme.DiffAddedLineNumberBgColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#1b2b34"),
Light: lipgloss.Color("#c5d5c5"),
}
theme.DiffRemovedLineNumberBgColor = lipgloss.AdaptiveColor{
Dark: "#2d1f26",
Light: "#e7c8cb",
theme.DiffRemovedLineNumberBgColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#2d1f26"),
Light: lipgloss.Color("#e7c8cb"),
}
// Markdown colors
theme.MarkdownTextColor = lipgloss.AdaptiveColor{
Dark: darkStep12,
Light: lightStep12,
theme.MarkdownTextColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep12),
Light: lipgloss.Color(lightStep12),
}
theme.MarkdownHeadingColor = lipgloss.AdaptiveColor{
Dark: darkPurple,
Light: lightPurple,
theme.MarkdownHeadingColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkPurple),
Light: lipgloss.Color(lightPurple),
}
theme.MarkdownLinkColor = lipgloss.AdaptiveColor{
Dark: darkBlue,
Light: lightBlue,
theme.MarkdownLinkColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkBlue),
Light: lipgloss.Color(lightBlue),
}
theme.MarkdownLinkTextColor = lipgloss.AdaptiveColor{
Dark: darkCyan,
Light: lightCyan,
theme.MarkdownLinkTextColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkCyan),
Light: lipgloss.Color(lightCyan),
}
theme.MarkdownCodeColor = lipgloss.AdaptiveColor{
Dark: darkGreen,
Light: lightGreen,
theme.MarkdownCodeColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkGreen),
Light: lipgloss.Color(lightGreen),
}
theme.MarkdownBlockQuoteColor = lipgloss.AdaptiveColor{
Dark: darkYellow,
Light: lightYellow,
theme.MarkdownBlockQuoteColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkYellow),
Light: lipgloss.Color(lightYellow),
}
theme.MarkdownEmphColor = lipgloss.AdaptiveColor{
Dark: darkYellow,
Light: lightYellow,
theme.MarkdownEmphColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkYellow),
Light: lipgloss.Color(lightYellow),
}
theme.MarkdownStrongColor = lipgloss.AdaptiveColor{
Dark: darkOrange,
Light: lightOrange,
theme.MarkdownStrongColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkOrange),
Light: lipgloss.Color(lightOrange),
}
theme.MarkdownHorizontalRuleColor = lipgloss.AdaptiveColor{
Dark: darkStep11,
Light: lightStep11,
theme.MarkdownHorizontalRuleColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep11),
Light: lipgloss.Color(lightStep11),
}
theme.MarkdownListItemColor = lipgloss.AdaptiveColor{
Dark: darkBlue,
Light: lightBlue,
theme.MarkdownListItemColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkBlue),
Light: lipgloss.Color(lightBlue),
}
theme.MarkdownListEnumerationColor = lipgloss.AdaptiveColor{
Dark: darkCyan,
Light: lightCyan,
theme.MarkdownListEnumerationColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkCyan),
Light: lipgloss.Color(lightCyan),
}
theme.MarkdownImageColor = lipgloss.AdaptiveColor{
Dark: darkBlue,
Light: lightBlue,
theme.MarkdownImageColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkBlue),
Light: lipgloss.Color(lightBlue),
}
theme.MarkdownImageTextColor = lipgloss.AdaptiveColor{
Dark: darkCyan,
Light: lightCyan,
theme.MarkdownImageTextColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkCyan),
Light: lipgloss.Color(lightCyan),
}
theme.MarkdownCodeBlockColor = lipgloss.AdaptiveColor{
Dark: darkStep12,
Light: lightStep12,
theme.MarkdownCodeBlockColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep12),
Light: lipgloss.Color(lightStep12),
}
// Syntax highlighting colors
theme.SyntaxCommentColor = lipgloss.AdaptiveColor{
Dark: darkStep11,
Light: lightStep11,
theme.SyntaxCommentColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep11),
Light: lipgloss.Color(lightStep11),
}
theme.SyntaxKeywordColor = lipgloss.AdaptiveColor{
Dark: darkPurple,
Light: lightPurple,
theme.SyntaxKeywordColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkPurple),
Light: lipgloss.Color(lightPurple),
}
theme.SyntaxFunctionColor = lipgloss.AdaptiveColor{
Dark: darkBlue,
Light: lightBlue,
theme.SyntaxFunctionColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkBlue),
Light: lipgloss.Color(lightBlue),
}
theme.SyntaxVariableColor = lipgloss.AdaptiveColor{
Dark: darkRed,
Light: lightRed,
theme.SyntaxVariableColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkRed),
Light: lipgloss.Color(lightRed),
}
theme.SyntaxStringColor = lipgloss.AdaptiveColor{
Dark: darkGreen,
Light: lightGreen,
theme.SyntaxStringColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkGreen),
Light: lipgloss.Color(lightGreen),
}
theme.SyntaxNumberColor = lipgloss.AdaptiveColor{
Dark: darkOrange,
Light: lightOrange,
theme.SyntaxNumberColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkOrange),
Light: lipgloss.Color(lightOrange),
}
theme.SyntaxTypeColor = lipgloss.AdaptiveColor{
Dark: darkYellow,
Light: lightYellow,
theme.SyntaxTypeColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkYellow),
Light: lipgloss.Color(lightYellow),
}
theme.SyntaxOperatorColor = lipgloss.AdaptiveColor{
Dark: darkCyan,
Light: lightCyan,
theme.SyntaxOperatorColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkCyan),
Light: lipgloss.Color(lightCyan),
}
theme.SyntaxPunctuationColor = lipgloss.AdaptiveColor{
Dark: darkStep12,
Light: lightStep12,
theme.SyntaxPunctuationColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep12),
Light: lipgloss.Color(lightStep12),
}
return theme

View File

@@ -1,272 +0,0 @@
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,
}
// Background colors
theme.BackgroundColor = lipgloss.AdaptiveColor{
Dark: darkBackground,
Light: lightBackground,
}
theme.BackgroundSubtleColor = lipgloss.AdaptiveColor{
Dark: darkCurrentLine,
Light: lightCurrentLine,
}
theme.BackgroundElementColor = lipgloss.AdaptiveColor{
Dark: "#070d14", // Slightly darker than background
Light: "#ffffff", // Slightly lighter than background
}
// Border colors
theme.BorderColor = lipgloss.AdaptiveColor{
Dark: darkBorder,
Light: lightBorder,
}
theme.BorderActiveColor = lipgloss.AdaptiveColor{
Dark: darkCyan,
Light: lightCyan,
}
theme.BorderSubtleColor = 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())
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
package util
import (
tea "github.com/charmbracelet/bubbletea"
tea "github.com/charmbracelet/bubbletea/v2"
)
func CmdHandler(msg tea.Msg) tea.Cmd {

View File

@@ -439,6 +439,45 @@
"parameters": [],
"description": "List all providers"
}
},
"/file_search": {
"post": {
"responses": {
"200": {
"description": "Search for files",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
}
},
"operationId": "postFile_search",
"parameters": [],
"description": "Search for files",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"query": {
"type": "string"
}
},
"required": [
"query"
]
}
}
}
}
}
}
},
"components": {
@@ -618,7 +657,6 @@
"role": {
"type": "string",
"enum": [
"system",
"user",
"assistant"
]
@@ -668,17 +706,65 @@
},
"tool": {
"type": "object",
"additionalProperties": {}
"additionalProperties": {
"type": "object",
"properties": {
"title": {
"type": "string"
},
"time": {
"type": "object",
"properties": {
"start": {
"type": "number"
},
"end": {
"type": "number"
}
},
"required": [
"start",
"end"
]
}
},
"required": [
"title",
"time"
],
"additionalProperties": {}
}
},
"assistant": {
"type": "object",
"properties": {
"system": {
"type": "array",
"items": {
"type": "string"
}
},
"modelID": {
"type": "string"
},
"providerID": {
"type": "string"
},
"path": {
"type": "object",
"properties": {
"cwd": {
"type": "string"
},
"root": {
"type": "string"
}
},
"required": [
"cwd",
"root"
]
},
"cost": {
"type": "number"
},
@@ -706,8 +792,10 @@
}
},
"required": [
"system",
"modelID",
"providerID",
"path",
"cost",
"tokens"
]
@@ -715,7 +803,6 @@
},
"required": [
"time",
"error",
"sessionID",
"tool"
]
@@ -1036,10 +1123,18 @@
"properties": {
"part": {
"$ref": "#/components/schemas/Message.Part"
},
"sessionID": {
"type": "string"
},
"messageID": {
"type": "string"
}
},
"required": [
"part"
"part",
"sessionID",
"messageID"
]
}
},
@@ -1079,6 +1174,10 @@
"type": "string",
"pattern": "^ses"
},
"parentID": {
"type": "string",
"pattern": "^ses"
},
"share": {
"type": "object",
"properties": {
@@ -1146,10 +1245,7 @@
}
}
}
},
"required": [
"error"
]
}
}
},
"required": [
@@ -1220,31 +1316,35 @@
"Provider.Info": {
"type": "object",
"properties": {
"id": {
"name": {
"type": "string"
},
"name": {
"env": {
"type": "array",
"items": {
"type": "string"
}
},
"id": {
"type": "string"
},
"models": {
"type": "object",
"additionalProperties": {
"$ref": "#/components/schemas/Provider.Model"
"$ref": "#/components/schemas/Model.Info"
}
}
},
"required": [
"id",
"name",
"env",
"id",
"models"
]
},
"Provider.Model": {
"Model.Info": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
@@ -1254,16 +1354,19 @@
"reasoning": {
"type": "boolean"
},
"temperature": {
"type": "boolean"
},
"cost": {
"type": "object",
"properties": {
"input": {
"type": "number"
},
"inputCached": {
"output": {
"type": "number"
},
"output": {
"inputCached": {
"type": "number"
},
"outputCached": {
@@ -1272,8 +1375,8 @@
},
"required": [
"input",
"inputCached",
"output",
"inputCached",
"outputCached"
]
},
@@ -1291,13 +1394,19 @@
"context",
"output"
]
},
"id": {
"type": "string"
}
},
"required": [
"id",
"name",
"attachment",
"reasoning",
"temperature",
"cost",
"limit"
"limit",
"id"
]
}
}

View File

@@ -20,7 +20,6 @@ import (
// Defines values for MessageInfoRole.
const (
Assistant MessageInfoRole = "assistant"
System MessageInfoRole = "system"
User MessageInfoRole = "user"
)
@@ -61,7 +60,9 @@ type EventLspClientDiagnostics struct {
// EventMessagePartUpdated defines model for Event.message.part.updated.
type EventMessagePartUpdated struct {
Properties struct {
Part MessagePart `json:"part"`
MessageID string `json:"messageID"`
Part MessagePart `json:"part"`
SessionID string `json:"sessionID"`
} `json:"properties"`
Type string `json:"type"`
}
@@ -83,7 +84,7 @@ type EventPermissionUpdated struct {
// EventSessionError defines model for Event.session.error.
type EventSessionError struct {
Properties struct {
Error EventSessionError_Properties_Error `json:"error"`
Error *EventSessionError_Properties_Error `json:"error,omitempty"`
} `json:"properties"`
Type string `json:"type"`
}
@@ -115,23 +116,28 @@ type MessageInfo struct {
Id string `json:"id"`
Metadata struct {
Assistant *struct {
Cost float32 `json:"cost"`
ModelID string `json:"modelID"`
ProviderID string `json:"providerID"`
Summary *bool `json:"summary,omitempty"`
Cost float32 `json:"cost"`
ModelID string `json:"modelID"`
Path struct {
Cwd string `json:"cwd"`
Root string `json:"root"`
} `json:"path"`
ProviderID string `json:"providerID"`
Summary *bool `json:"summary,omitempty"`
System []string `json:"system"`
Tokens struct {
Input float32 `json:"input"`
Output float32 `json:"output"`
Reasoning float32 `json:"reasoning"`
} `json:"tokens"`
} `json:"assistant,omitempty"`
Error MessageInfo_Metadata_Error `json:"error"`
SessionID string `json:"sessionID"`
Error *MessageInfo_Metadata_Error `json:"error,omitempty"`
SessionID string `json:"sessionID"`
Time struct {
Completed *float32 `json:"completed,omitempty"`
Created float32 `json:"created"`
} `json:"time"`
Tool map[string]interface{} `json:"tool"`
Tool map[string]MessageInfo_Metadata_Tool_AdditionalProperties `json:"tool"`
} `json:"metadata"`
Parts []MessagePart `json:"parts"`
Role MessageInfoRole `json:"role"`
@@ -142,6 +148,16 @@ type MessageInfo_Metadata_Error struct {
union json.RawMessage
}
// MessageInfo_Metadata_Tool_AdditionalProperties defines model for MessageInfo.Metadata.Tool.AdditionalProperties.
type MessageInfo_Metadata_Tool_AdditionalProperties struct {
Time struct {
End float32 `json:"end"`
Start float32 `json:"start"`
} `json:"time"`
Title string `json:"title"`
AdditionalProperties map[string]interface{} `json:"-"`
}
// MessageInfoRole defines model for MessageInfo.Role.
type MessageInfoRole string
@@ -224,15 +240,8 @@ type MessageToolInvocationToolResult struct {
ToolName string `json:"toolName"`
}
// ProviderInfo defines model for Provider.Info.
type ProviderInfo struct {
Id string `json:"id"`
Models map[string]ProviderModel `json:"models"`
Name string `json:"name"`
}
// ProviderModel defines model for Provider.Model.
type ProviderModel struct {
// ModelInfo defines model for Model.Info.
type ModelInfo struct {
Attachment bool `json:"attachment"`
Cost struct {
Input float32 `json:"input"`
@@ -245,8 +254,17 @@ type ProviderModel struct {
Context float32 `json:"context"`
Output float32 `json:"output"`
} `json:"limit"`
Name *string `json:"name,omitempty"`
Reasoning *bool `json:"reasoning,omitempty"`
Name string `json:"name"`
Reasoning bool `json:"reasoning"`
Temperature bool `json:"temperature"`
}
// ProviderInfo defines model for Provider.Info.
type ProviderInfo struct {
Env []string `json:"env"`
Id string `json:"id"`
Models map[string]ModelInfo `json:"models"`
Name string `json:"name"`
}
// ProviderAuthError defines model for ProviderAuthError.
@@ -279,8 +297,9 @@ type PermissionInfo struct {
// SessionInfo defines model for session.info.
type SessionInfo struct {
Id string `json:"id"`
Share *struct {
Id string `json:"id"`
ParentID *string `json:"parentID,omitempty"`
Share *struct {
Secret string `json:"secret"`
Url string `json:"url"`
} `json:"share,omitempty"`
@@ -291,6 +310,11 @@ type SessionInfo struct {
Title string `json:"title"`
}
// PostFileSearchJSONBody defines parameters for PostFileSearch.
type PostFileSearchJSONBody struct {
Query string `json:"query"`
}
// PostSessionAbortJSONBody defines parameters for PostSessionAbort.
type PostSessionAbortJSONBody struct {
SessionID string `json:"sessionID"`
@@ -328,6 +352,9 @@ type PostSessionSummarizeJSONBody struct {
SessionID string `json:"sessionID"`
}
// PostFileSearchJSONRequestBody defines body for PostFileSearch for application/json ContentType.
type PostFileSearchJSONRequestBody PostFileSearchJSONBody
// PostSessionAbortJSONRequestBody defines body for PostSessionAbort for application/json ContentType.
type PostSessionAbortJSONRequestBody PostSessionAbortJSONBody
@@ -346,6 +373,85 @@ type PostSessionShareJSONRequestBody PostSessionShareJSONBody
// PostSessionSummarizeJSONRequestBody defines body for PostSessionSummarize for application/json ContentType.
type PostSessionSummarizeJSONRequestBody PostSessionSummarizeJSONBody
// Getter for additional properties for MessageInfo_Metadata_Tool_AdditionalProperties. Returns the specified
// element and whether it was found
func (a MessageInfo_Metadata_Tool_AdditionalProperties) Get(fieldName string) (value interface{}, found bool) {
if a.AdditionalProperties != nil {
value, found = a.AdditionalProperties[fieldName]
}
return
}
// Setter for additional properties for MessageInfo_Metadata_Tool_AdditionalProperties
func (a *MessageInfo_Metadata_Tool_AdditionalProperties) Set(fieldName string, value interface{}) {
if a.AdditionalProperties == nil {
a.AdditionalProperties = make(map[string]interface{})
}
a.AdditionalProperties[fieldName] = value
}
// Override default JSON handling for MessageInfo_Metadata_Tool_AdditionalProperties to handle AdditionalProperties
func (a *MessageInfo_Metadata_Tool_AdditionalProperties) UnmarshalJSON(b []byte) error {
object := make(map[string]json.RawMessage)
err := json.Unmarshal(b, &object)
if err != nil {
return err
}
if raw, found := object["time"]; found {
err = json.Unmarshal(raw, &a.Time)
if err != nil {
return fmt.Errorf("error reading 'time': %w", err)
}
delete(object, "time")
}
if raw, found := object["title"]; found {
err = json.Unmarshal(raw, &a.Title)
if err != nil {
return fmt.Errorf("error reading 'title': %w", err)
}
delete(object, "title")
}
if len(object) != 0 {
a.AdditionalProperties = make(map[string]interface{})
for fieldName, fieldBuf := range object {
var fieldVal interface{}
err := json.Unmarshal(fieldBuf, &fieldVal)
if err != nil {
return fmt.Errorf("error unmarshaling field %s: %w", fieldName, err)
}
a.AdditionalProperties[fieldName] = fieldVal
}
}
return nil
}
// Override default JSON handling for MessageInfo_Metadata_Tool_AdditionalProperties to handle AdditionalProperties
func (a MessageInfo_Metadata_Tool_AdditionalProperties) MarshalJSON() ([]byte, error) {
var err error
object := make(map[string]json.RawMessage)
object["time"], err = json.Marshal(a.Time)
if err != nil {
return nil, fmt.Errorf("error marshaling 'time': %w", err)
}
object["title"], err = json.Marshal(a.Title)
if err != nil {
return nil, fmt.Errorf("error marshaling 'title': %w", err)
}
for fieldName, field := range a.AdditionalProperties {
object[fieldName], err = json.Marshal(field)
if err != nil {
return nil, fmt.Errorf("error marshaling '%s': %w", fieldName, err)
}
}
return json.Marshal(object)
}
// AsEventStorageWrite returns the union data inside the Event as a EventStorageWrite
func (t Event) AsEventStorageWrite() (EventStorageWrite, error) {
var body EventStorageWrite
@@ -1173,6 +1279,11 @@ type ClientInterface interface {
// GetEvent request
GetEvent(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error)
// PostFileSearchWithBody request with any body
PostFileSearchWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error)
PostFileSearch(ctx context.Context, body PostFileSearchJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error)
// PostPathGet request
PostPathGet(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error)
@@ -1252,6 +1363,30 @@ func (c *Client) GetEvent(ctx context.Context, reqEditors ...RequestEditorFn) (*
return c.Client.Do(req)
}
func (c *Client) PostFileSearchWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) {
req, err := NewPostFileSearchRequestWithBody(c.Server, contentType, body)
if err != nil {
return nil, err
}
req = req.WithContext(ctx)
if err := c.applyEditors(ctx, req, reqEditors); err != nil {
return nil, err
}
return c.Client.Do(req)
}
func (c *Client) PostFileSearch(ctx context.Context, body PostFileSearchJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) {
req, err := NewPostFileSearchRequest(c.Server, body)
if err != nil {
return nil, err
}
req = req.WithContext(ctx)
if err := c.applyEditors(ctx, req, reqEditors); err != nil {
return nil, err
}
return c.Client.Do(req)
}
func (c *Client) PostPathGet(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) {
req, err := NewPostPathGetRequest(c.Server)
if err != nil {
@@ -1525,6 +1660,46 @@ func NewGetEventRequest(server string) (*http.Request, error) {
return req, nil
}
// NewPostFileSearchRequest calls the generic PostFileSearch builder with application/json body
func NewPostFileSearchRequest(server string, body PostFileSearchJSONRequestBody) (*http.Request, error) {
var bodyReader io.Reader
buf, err := json.Marshal(body)
if err != nil {
return nil, err
}
bodyReader = bytes.NewReader(buf)
return NewPostFileSearchRequestWithBody(server, "application/json", bodyReader)
}
// NewPostFileSearchRequestWithBody generates requests for PostFileSearch with any type of body
func NewPostFileSearchRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) {
var err error
serverURL, err := url.Parse(server)
if err != nil {
return nil, err
}
operationPath := fmt.Sprintf("/file_search")
if operationPath[0] == '/' {
operationPath = "." + operationPath
}
queryURL, err := serverURL.Parse(operationPath)
if err != nil {
return nil, err
}
req, err := http.NewRequest("POST", queryURL.String(), body)
if err != nil {
return nil, err
}
req.Header.Add("Content-Type", contentType)
return req, nil
}
// NewPostPathGetRequest generates requests for PostPathGet
func NewPostPathGetRequest(server string) (*http.Request, error) {
var err error
@@ -1925,6 +2100,11 @@ type ClientWithResponsesInterface interface {
// GetEventWithResponse request
GetEventWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetEventResponse, error)
// PostFileSearchWithBodyWithResponse request with any body
PostFileSearchWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostFileSearchResponse, error)
PostFileSearchWithResponse(ctx context.Context, body PostFileSearchJSONRequestBody, reqEditors ...RequestEditorFn) (*PostFileSearchResponse, error)
// PostPathGetWithResponse request
PostPathGetWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*PostPathGetResponse, error)
@@ -2034,6 +2214,28 @@ func (r GetEventResponse) StatusCode() int {
return 0
}
type PostFileSearchResponse struct {
Body []byte
HTTPResponse *http.Response
JSON200 *[]string
}
// Status returns HTTPResponse.Status
func (r PostFileSearchResponse) Status() string {
if r.HTTPResponse != nil {
return r.HTTPResponse.Status
}
return http.StatusText(0)
}
// StatusCode returns HTTPResponse.StatusCode
func (r PostFileSearchResponse) StatusCode() int {
if r.HTTPResponse != nil {
return r.HTTPResponse.StatusCode
}
return 0
}
type PostPathGetResponse struct {
Body []byte
HTTPResponse *http.Response
@@ -2290,6 +2492,23 @@ func (c *ClientWithResponses) GetEventWithResponse(ctx context.Context, reqEdito
return ParseGetEventResponse(rsp)
}
// PostFileSearchWithBodyWithResponse request with arbitrary body returning *PostFileSearchResponse
func (c *ClientWithResponses) PostFileSearchWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostFileSearchResponse, error) {
rsp, err := c.PostFileSearchWithBody(ctx, contentType, body, reqEditors...)
if err != nil {
return nil, err
}
return ParsePostFileSearchResponse(rsp)
}
func (c *ClientWithResponses) PostFileSearchWithResponse(ctx context.Context, body PostFileSearchJSONRequestBody, reqEditors ...RequestEditorFn) (*PostFileSearchResponse, error) {
rsp, err := c.PostFileSearch(ctx, body, reqEditors...)
if err != nil {
return nil, err
}
return ParsePostFileSearchResponse(rsp)
}
// PostPathGetWithResponse request returning *PostPathGetResponse
func (c *ClientWithResponses) PostPathGetWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*PostPathGetResponse, error) {
rsp, err := c.PostPathGet(ctx, reqEditors...)
@@ -2506,6 +2725,32 @@ func ParseGetEventResponse(rsp *http.Response) (*GetEventResponse, error) {
return response, nil
}
// ParsePostFileSearchResponse parses an HTTP response from a PostFileSearchWithResponse call
func ParsePostFileSearchResponse(rsp *http.Response) (*PostFileSearchResponse, error) {
bodyBytes, err := io.ReadAll(rsp.Body)
defer func() { _ = rsp.Body.Close() }()
if err != nil {
return nil, err
}
response := &PostFileSearchResponse{
Body: bodyBytes,
HTTPResponse: rsp,
}
switch {
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200:
var dest []string
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
return nil, err
}
response.JSON200 = &dest
}
return response, nil
}
// ParsePostPathGetResponse parses an HTTP response from a PostPathGetWithResponse call
func ParsePostPathGetResponse(rsp *http.Response) (*PostPathGetResponse, error) {
bodyBytes, err := io.ReadAll(rsp.Body)

View File

@@ -1,3 +1,3 @@
<svg width="29" height="29" viewBox="0 0 29 29" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.184 28.512C12.144 28.512 10.26 28.152 8.532 27.432C6.804 26.736 5.304 25.752 4.032 24.48C2.76 23.184 1.764 21.672 1.044 19.944C0.348 18.216 0 16.332 0 14.292C0 12.228 0.36 10.332 1.08 8.604C1.8 6.852 2.796 5.34 4.068 4.068C5.364 2.772 6.876 1.776 8.604 1.08C10.332 0.36 12.192 0 14.184 0C16.224 0 18.108 0.36 19.836 1.08C21.588 1.8 23.112 2.808 24.408 4.104C25.704 5.4 26.712 6.924 27.432 8.676C28.152 10.404 28.512 12.276 28.512 14.292C28.512 16.284 28.152 18.144 27.432 19.872C26.736 21.6 25.74 23.112 24.444 24.408C23.172 25.704 21.66 26.712 19.908 27.432C18.18 28.152 16.272 28.512 14.184 28.512ZM13.104 25.74V2.772L11.016 3.168V25.416L13.104 25.74ZM15.3 25.74L17.388 25.416V3.168L15.3 2.772V25.74ZM8.82 24.516V3.996C8.076 4.404 7.392 4.884 6.768 5.436V23.148C7.104 23.364 7.44 23.592 7.776 23.832C8.136 24.048 8.484 24.276 8.82 24.516ZM19.548 24.552L21.636 23.148V5.364L19.548 3.996V24.552ZM23.868 20.736C25.14 18.888 25.776 16.74 25.776 14.292C25.776 11.964 25.14 9.816 23.868 7.848V20.736ZM4.608 20.736V7.884C3.384 9.756 2.772 11.892 2.772 14.292C2.772 15.468 2.916 16.596 3.204 17.676C3.516 18.732 3.984 19.752 4.608 20.736Z" fill="black"/>
<svg width="33" height="42" viewBox="0 0 33 42" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.5 0H32.5V41.5955H0.5V0ZM24.5 8.5H8.5V33H24.5V8.5Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 221 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -1,11 +1,12 @@
<svg width="187" height="32" viewBox="0 0 187 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.184 28.512C12.144 28.512 10.26 28.152 8.532 27.432C6.804 26.736 5.304 25.752 4.032 24.48C2.76 23.184 1.764 21.672 1.044 19.944C0.348 18.216 0 16.332 0 14.292C0 12.228 0.36 10.332 1.08 8.604C1.8 6.852 2.796 5.34 4.068 4.068C5.364 2.772 6.876 1.776 8.604 1.08C10.332 0.36 12.192 0 14.184 0C16.224 0 18.108 0.36 19.836 1.08C21.588 1.8 23.112 2.808 24.408 4.104C25.704 5.4 26.712 6.924 27.432 8.676C28.152 10.404 28.512 12.276 28.512 14.292C28.512 16.284 28.152 18.144 27.432 19.872C26.736 21.6 25.74 23.112 24.444 24.408C23.172 25.704 21.66 26.712 19.908 27.432C18.18 28.152 16.272 28.512 14.184 28.512ZM13.104 25.74V2.772L11.016 3.168V25.416L13.104 25.74ZM15.3 25.74L17.388 25.416V3.168L15.3 2.772V25.74ZM8.82 24.516V3.996C8.076 4.404 7.392 4.884 6.768 5.436V23.148C7.104 23.364 7.44 23.592 7.776 23.832C8.136 24.048 8.484 24.276 8.82 24.516ZM19.548 24.552L21.636 23.148V5.364L19.548 3.996V24.552ZM23.868 20.736C25.14 18.888 25.776 16.74 25.776 14.292C25.776 11.964 25.14 9.816 23.868 7.848V20.736ZM4.608 20.736V7.884C3.384 9.756 2.772 11.892 2.772 14.292C2.772 15.468 2.916 16.596 3.204 17.676C3.516 18.732 3.984 19.752 4.608 20.736Z" fill="white"/>
<path d="M45.628 25.432C42.556 25.432 40.252 24.688 38.716 23.2C37.204 21.688 36.448 19.552 36.448 16.792V9.62799C36.448 6.84399 37.204 4.70799 38.716 3.21999C40.252 1.73199 42.556 0.987986 45.628 0.987986C48.7 0.987986 50.992 1.73199 52.504 3.21999C54.04 4.70799 54.808 6.84399 54.808 9.62799V16.792C54.808 19.552 54.04 21.688 52.504 23.2C50.992 24.688 48.7 25.432 45.628 25.432ZM45.628 22.228C47.452 22.228 48.832 21.76 49.768 20.824C50.704 19.864 51.172 18.484 51.172 16.684V9.73599C51.172 7.91199 50.704 6.53199 49.768 5.59599C48.832 4.65999 47.452 4.19199 45.628 4.19199C43.828 4.19199 42.448 4.65999 41.488 5.59599C40.552 6.53199 40.084 7.91199 40.084 9.73599V16.684C40.084 18.484 40.552 19.864 41.488 20.824C42.448 21.76 43.828 22.228 45.628 22.228Z" fill="white"/>
<path d="M67.0294 25.432C66.1654 25.432 65.2414 25.312 64.2574 25.072C63.2734 24.856 62.4694 24.544 61.8454 24.136L61.7734 21.184C62.4214 21.544 63.1414 21.832 63.9334 22.048C64.7254 22.24 65.4574 22.336 66.1294 22.336C67.4254 22.336 68.3614 21.964 68.9374 21.22C69.5374 20.452 69.8374 19.312 69.8374 17.8V13.912C69.8374 12.592 69.6094 11.632 69.1534 11.032C68.6974 10.408 68.0014 10.096 67.0654 10.096C66.2734 10.096 65.4694 10.336 64.6534 10.816C63.8374 11.272 62.8174 12.064 61.5934 13.192L61.4854 10.168C62.2534 9.44799 62.9854 8.84799 63.6814 8.36799C64.3774 7.88799 65.0854 7.52799 65.8054 7.28799C66.5254 7.04799 67.2934 6.92799 68.1094 6.92799C69.8614 6.92799 71.1814 7.46799 72.0694 8.54799C72.9814 9.62799 73.4374 11.272 73.4374 13.48V18.196C73.4374 20.572 72.8854 22.372 71.7814 23.596C70.7014 24.82 69.1174 25.432 67.0294 25.432ZM59.1094 31.66C58.8454 31.66 58.7134 31.528 58.7134 31.264V11.536C58.7134 10.936 58.6894 10.3 58.6414 9.62799C58.5934 8.95599 58.5334 8.35599 58.4614 7.82799C58.4374 7.51599 58.5694 7.35999 58.8574 7.35999H61.4134C61.6774 7.35999 61.8214 7.47999 61.8454 7.71999C61.8934 7.91199 61.9414 8.16399 61.9894 8.47599C62.0374 8.78799 62.0734 9.09999 62.0974 9.41199C62.1214 9.69999 62.1334 9.91599 62.1334 10.06L62.3134 11.968V31.264C62.3134 31.528 62.1814 31.66 61.9174 31.66H59.1094Z" fill="white"/>
<path d="M84.0237 25.432C81.5757 25.432 79.7157 24.88 78.4437 23.776C77.1957 22.672 76.5717 21.028 76.5717 18.844V14.056C76.5717 11.728 77.1957 9.96399 78.4437 8.76399C79.6917 7.53999 81.5397 6.92799 83.9877 6.92799C86.4117 6.92799 88.2477 7.51599 89.4957 8.69199C90.7437 9.86799 91.3677 11.584 91.3677 13.84V16.648C91.3677 16.912 91.2477 17.044 91.0077 17.044H80.1717V18.196C80.1717 19.66 80.4837 20.728 81.1077 21.4C81.7317 22.072 82.7397 22.408 84.1317 22.408C85.1877 22.408 86.0037 22.24 86.5797 21.904C87.1557 21.568 87.4437 21.052 87.4437 20.356C87.4437 20.092 87.5877 19.96 87.8757 19.96H90.6477C90.8637 19.96 90.9957 20.08 91.0437 20.32C91.1397 21.928 90.5637 23.188 89.3157 24.1C88.0677 24.988 86.3037 25.432 84.0237 25.432ZM80.1717 14.308H87.8037V14.128C87.8037 12.688 87.4917 11.632 86.8677 10.96C86.2437 10.288 85.2957 9.95199 84.0237 9.95199C82.7277 9.95199 81.7557 10.3 81.1077 10.996C80.4837 11.692 80.1717 12.736 80.1717 14.128V14.308Z" fill="white"/>
<path d="M106.647 25C106.383 25 106.251 24.868 106.251 24.604V13.3C106.251 12.196 106.023 11.392 105.567 10.888C105.135 10.36 104.451 10.096 103.515 10.096C102.627 10.096 101.751 10.36 100.887 10.888C100.023 11.392 98.9906 12.244 97.7906 13.444L97.7186 10.528C98.4866 9.71199 99.2306 9.03999 99.9506 8.51199C100.671 7.98399 101.403 7.58799 102.147 7.32399C102.915 7.05999 103.719 6.92799 104.559 6.92799C106.287 6.92799 107.595 7.43199 108.483 8.43999C109.395 9.42399 109.851 10.912 109.851 12.904V24.604C109.851 24.868 109.731 25 109.491 25H106.647ZM95.2346 25C94.9706 25 94.8386 24.868 94.8386 24.604V11.824C94.8386 11.152 94.8026 10.444 94.7306 9.69999C94.6826 8.93199 94.6346 8.31999 94.5866 7.86399C94.5386 7.52799 94.6706 7.35999 94.9826 7.35999H97.5386C97.7786 7.35999 97.9226 7.47999 97.9706 7.71999C98.0186 7.93599 98.0666 8.23599 98.1146 8.61999C98.1626 9.00399 98.2106 9.41199 98.2586 9.84399C98.3066 10.276 98.3306 10.648 98.3306 10.96L98.4386 12.112V24.604C98.4386 24.868 98.3066 25 98.0426 25H95.2346Z" fill="white"/>
<path d="M122.87 25.432C119.894 25.432 117.626 24.7 116.066 23.236C114.506 21.748 113.726 19.6 113.726 16.792V9.66399C113.726 6.83199 114.506 4.68399 116.066 3.21999C117.65 1.73199 119.93 0.987986 122.906 0.987986C124.85 0.987986 126.542 1.31199 127.982 1.95999C129.422 2.60799 130.49 3.54399 131.186 4.76799C131.906 5.96799 132.146 7.40799 131.906 9.08799C131.882 9.23199 131.846 9.35199 131.798 9.44799C131.75 9.54399 131.654 9.59199 131.51 9.59199H128.63C128.366 9.59199 128.246 9.45999 128.27 9.19599C128.342 7.61199 127.922 6.37599 127.01 5.48799C126.098 4.59999 124.742 4.15599 122.942 4.15599C121.142 4.15599 119.762 4.62399 118.802 5.55999C117.842 6.49599 117.362 7.86399 117.362 9.66399V16.756C117.362 18.556 117.842 19.924 118.802 20.86C119.762 21.796 121.142 22.264 122.942 22.264C124.766 22.264 126.146 21.82 127.082 20.932C128.042 20.02 128.438 18.784 128.27 17.224C128.246 16.96 128.366 16.828 128.63 16.828H131.474C131.714 16.828 131.858 16.996 131.906 17.332C132.05 18.964 131.762 20.392 131.042 21.616C130.346 22.816 129.29 23.752 127.874 24.424C126.458 25.096 124.79 25.432 122.87 25.432Z" fill="white"/>
<path d="M142.558 25.432C140.086 25.432 138.19 24.832 136.87 23.632C135.55 22.432 134.89 20.62 134.89 18.196V14.164C134.89 11.74 135.538 9.92799 136.834 8.72799C138.154 7.52799 140.062 6.92799 142.558 6.92799C145.03 6.92799 146.914 7.52799 148.21 8.72799C149.53 9.92799 150.19 11.74 150.19 14.164V18.196C150.19 20.62 149.542 22.432 148.246 23.632C146.95 24.832 145.054 25.432 142.558 25.432ZM142.558 22.3C143.974 22.3 144.994 21.94 145.618 21.22C146.266 20.5 146.59 19.384 146.59 17.872V14.488C146.59 13 146.266 11.896 145.618 11.176C144.994 10.432 143.974 10.06 142.558 10.06C141.118 10.06 140.074 10.432 139.426 11.176C138.802 11.896 138.49 13 138.49 14.488V17.872C138.49 19.384 138.802 20.5 139.426 21.22C140.074 21.94 141.118 22.3 142.558 22.3Z" fill="white"/>
<path d="M158.473 25.432C156.793 25.432 155.509 24.892 154.621 23.812C153.733 22.708 153.289 21.064 153.289 18.88V14.2C153.289 11.848 153.853 10.048 154.981 8.79999C156.133 7.55199 157.729 6.92799 159.769 6.92799C160.657 6.92799 161.605 7.04799 162.613 7.28799C163.645 7.52799 164.449 7.83999 165.025 8.22399L165.169 11.212C164.449 10.828 163.657 10.54 162.793 10.348C161.953 10.132 161.209 10.024 160.561 10.024C159.337 10.024 158.413 10.408 157.789 11.176C157.189 11.92 156.889 13.048 156.889 14.56V18.772C156.889 19.948 157.117 20.836 157.573 21.436C158.029 22.012 158.713 22.3 159.625 22.3C160.201 22.3 160.765 22.192 161.317 21.976C161.869 21.736 162.469 21.376 163.117 20.896C163.765 20.392 164.497 19.72 165.313 18.88L165.349 21.832C164.581 22.6 163.837 23.26 163.117 23.812C162.397 24.34 161.665 24.736 160.921 25C160.177 25.288 159.361 25.432 158.473 25.432ZM165.601 25C165.313 25 165.157 24.868 165.133 24.604C165.085 24.244 165.025 23.8 164.953 23.272C164.881 22.72 164.857 22.252 164.881 21.868L164.629 20.104V0.807986C164.629 0.543987 164.761 0.411987 165.025 0.411987H167.833C168.097 0.411987 168.229 0.543987 168.229 0.807986V20.824C168.229 21.184 168.241 21.604 168.265 22.084C168.289 22.54 168.313 22.984 168.337 23.416C168.385 23.848 168.421 24.208 168.445 24.496C168.517 24.832 168.397 25 168.085 25H165.601Z" fill="white"/>
<path d="M179.171 25.432C176.723 25.432 174.863 24.88 173.591 23.776C172.343 22.672 171.719 21.028 171.719 18.844V14.056C171.719 11.728 172.343 9.96399 173.591 8.76399C174.839 7.53999 176.687 6.92799 179.135 6.92799C181.559 6.92799 183.395 7.51599 184.643 8.69199C185.891 9.86799 186.515 11.584 186.515 13.84V16.648C186.515 16.912 186.395 17.044 186.155 17.044H175.319V18.196C175.319 19.66 175.631 20.728 176.255 21.4C176.879 22.072 177.887 22.408 179.279 22.408C180.335 22.408 181.151 22.24 181.727 21.904C182.303 21.568 182.591 21.052 182.591 20.356C182.591 20.092 182.735 19.96 183.023 19.96H185.795C186.011 19.96 186.143 20.08 186.191 20.32C186.287 21.928 185.711 23.188 184.463 24.1C183.215 24.988 181.451 25.432 179.171 25.432ZM175.319 14.308H182.951V14.128C182.951 12.688 182.639 11.632 182.015 10.96C181.391 10.288 180.443 9.95199 179.171 9.95199C177.875 9.95199 176.903 10.3 176.255 10.996C175.631 11.692 175.319 12.736 175.319 14.128V14.308Z" fill="white"/>
<svg width="289" height="50" viewBox="0 0 289 50" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M264.5 0H288.5V8.5H272.5V16.5H288.5V25H272.5V33H288.5V41.5H264.5V0Z" fill="white"/>
<path d="M248.5 0H224.5V41.5H248.5V33H232.5V8.5H248.5V0Z" fill="white"/>
<path d="M256.5 8.5H248.5V33H256.5V8.5Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M184.5 0H216.5V41.5H184.5V0ZM208.5 8.5H192.5V33H208.5V8.5Z" fill="white"/>
<path d="M144.5 8.5H136.5V41.5H144.5V8.5Z" fill="white"/>
<path d="M136.5 0H112.5V41.5H120.5V8.5H136.5V0Z" fill="white"/>
<path d="M80.5 0H104.5V8.5H88.5V16.5H104.5V25H88.5V33H104.5V41.5H80.5V0Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M40.5 0H72.5V41.5H48.5V49.5H40.5V0ZM64.5 8.5H48.5V33H64.5V8.5Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.5 0H32.5V41.5955H0.5V0ZM24.5 8.5H8.5V33H24.5V8.5Z" fill="white"/>
<path d="M152.5 0H176.5V8.5H160.5V33H176.5V41.5H152.5V0Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 981 B

View File

@@ -1,11 +1,12 @@
<svg width="187" height="32" viewBox="0 0 187 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.184 28.512C12.144 28.512 10.26 28.152 8.532 27.432C6.804 26.736 5.304 25.752 4.032 24.48C2.76 23.184 1.764 21.672 1.044 19.944C0.348 18.216 0 16.332 0 14.292C0 12.228 0.36 10.332 1.08 8.604C1.8 6.852 2.796 5.34 4.068 4.068C5.364 2.772 6.876 1.776 8.604 1.08C10.332 0.36 12.192 0 14.184 0C16.224 0 18.108 0.36 19.836 1.08C21.588 1.8 23.112 2.808 24.408 4.104C25.704 5.4 26.712 6.924 27.432 8.676C28.152 10.404 28.512 12.276 28.512 14.292C28.512 16.284 28.152 18.144 27.432 19.872C26.736 21.6 25.74 23.112 24.444 24.408C23.172 25.704 21.66 26.712 19.908 27.432C18.18 28.152 16.272 28.512 14.184 28.512ZM13.104 25.74V2.772L11.016 3.168V25.416L13.104 25.74ZM15.3 25.74L17.388 25.416V3.168L15.3 2.772V25.74ZM8.82 24.516V3.996C8.076 4.404 7.392 4.884 6.768 5.436V23.148C7.104 23.364 7.44 23.592 7.776 23.832C8.136 24.048 8.484 24.276 8.82 24.516ZM19.548 24.552L21.636 23.148V5.364L19.548 3.996V24.552ZM23.868 20.736C25.14 18.888 25.776 16.74 25.776 14.292C25.776 11.964 25.14 9.816 23.868 7.848V20.736ZM4.608 20.736V7.884C3.384 9.756 2.772 11.892 2.772 14.292C2.772 15.468 2.916 16.596 3.204 17.676C3.516 18.732 3.984 19.752 4.608 20.736Z" fill="black"/>
<path d="M45.628 25.432C42.556 25.432 40.252 24.688 38.716 23.2C37.204 21.688 36.448 19.552 36.448 16.792V9.62799C36.448 6.84399 37.204 4.70799 38.716 3.21999C40.252 1.73199 42.556 0.987986 45.628 0.987986C48.7 0.987986 50.992 1.73199 52.504 3.21999C54.04 4.70799 54.808 6.84399 54.808 9.62799V16.792C54.808 19.552 54.04 21.688 52.504 23.2C50.992 24.688 48.7 25.432 45.628 25.432ZM45.628 22.228C47.452 22.228 48.832 21.76 49.768 20.824C50.704 19.864 51.172 18.484 51.172 16.684V9.73599C51.172 7.91199 50.704 6.53199 49.768 5.59599C48.832 4.65999 47.452 4.19199 45.628 4.19199C43.828 4.19199 42.448 4.65999 41.488 5.59599C40.552 6.53199 40.084 7.91199 40.084 9.73599V16.684C40.084 18.484 40.552 19.864 41.488 20.824C42.448 21.76 43.828 22.228 45.628 22.228Z" fill="black"/>
<path d="M67.0294 25.432C66.1654 25.432 65.2414 25.312 64.2574 25.072C63.2734 24.856 62.4694 24.544 61.8454 24.136L61.7734 21.184C62.4214 21.544 63.1414 21.832 63.9334 22.048C64.7254 22.24 65.4574 22.336 66.1294 22.336C67.4254 22.336 68.3614 21.964 68.9374 21.22C69.5374 20.452 69.8374 19.312 69.8374 17.8V13.912C69.8374 12.592 69.6094 11.632 69.1534 11.032C68.6974 10.408 68.0014 10.096 67.0654 10.096C66.2734 10.096 65.4694 10.336 64.6534 10.816C63.8374 11.272 62.8174 12.064 61.5934 13.192L61.4854 10.168C62.2534 9.44799 62.9854 8.84799 63.6814 8.36799C64.3774 7.88799 65.0854 7.52799 65.8054 7.28799C66.5254 7.04799 67.2934 6.92799 68.1094 6.92799C69.8614 6.92799 71.1814 7.46799 72.0694 8.54799C72.9814 9.62799 73.4374 11.272 73.4374 13.48V18.196C73.4374 20.572 72.8854 22.372 71.7814 23.596C70.7014 24.82 69.1174 25.432 67.0294 25.432ZM59.1094 31.66C58.8454 31.66 58.7134 31.528 58.7134 31.264V11.536C58.7134 10.936 58.6894 10.3 58.6414 9.62799C58.5934 8.95599 58.5334 8.35599 58.4614 7.82799C58.4374 7.51599 58.5694 7.35999 58.8574 7.35999H61.4134C61.6774 7.35999 61.8214 7.47999 61.8454 7.71999C61.8934 7.91199 61.9414 8.16399 61.9894 8.47599C62.0374 8.78799 62.0734 9.09999 62.0974 9.41199C62.1214 9.69999 62.1334 9.91599 62.1334 10.06L62.3134 11.968V31.264C62.3134 31.528 62.1814 31.66 61.9174 31.66H59.1094Z" fill="black"/>
<path d="M84.0237 25.432C81.5757 25.432 79.7157 24.88 78.4437 23.776C77.1957 22.672 76.5717 21.028 76.5717 18.844V14.056C76.5717 11.728 77.1957 9.96399 78.4437 8.76399C79.6917 7.53999 81.5397 6.92799 83.9877 6.92799C86.4117 6.92799 88.2477 7.51599 89.4957 8.69199C90.7437 9.86799 91.3677 11.584 91.3677 13.84V16.648C91.3677 16.912 91.2477 17.044 91.0077 17.044H80.1717V18.196C80.1717 19.66 80.4837 20.728 81.1077 21.4C81.7317 22.072 82.7397 22.408 84.1317 22.408C85.1877 22.408 86.0037 22.24 86.5797 21.904C87.1557 21.568 87.4437 21.052 87.4437 20.356C87.4437 20.092 87.5877 19.96 87.8757 19.96H90.6477C90.8637 19.96 90.9957 20.08 91.0437 20.32C91.1397 21.928 90.5637 23.188 89.3157 24.1C88.0677 24.988 86.3037 25.432 84.0237 25.432ZM80.1717 14.308H87.8037V14.128C87.8037 12.688 87.4917 11.632 86.8677 10.96C86.2437 10.288 85.2957 9.95199 84.0237 9.95199C82.7277 9.95199 81.7557 10.3 81.1077 10.996C80.4837 11.692 80.1717 12.736 80.1717 14.128V14.308Z" fill="black"/>
<path d="M106.647 25C106.383 25 106.251 24.868 106.251 24.604V13.3C106.251 12.196 106.023 11.392 105.567 10.888C105.135 10.36 104.451 10.096 103.515 10.096C102.627 10.096 101.751 10.36 100.887 10.888C100.023 11.392 98.9906 12.244 97.7906 13.444L97.7186 10.528C98.4866 9.71199 99.2306 9.03999 99.9506 8.51199C100.671 7.98399 101.403 7.58799 102.147 7.32399C102.915 7.05999 103.719 6.92799 104.559 6.92799C106.287 6.92799 107.595 7.43199 108.483 8.43999C109.395 9.42399 109.851 10.912 109.851 12.904V24.604C109.851 24.868 109.731 25 109.491 25H106.647ZM95.2346 25C94.9706 25 94.8386 24.868 94.8386 24.604V11.824C94.8386 11.152 94.8026 10.444 94.7306 9.69999C94.6826 8.93199 94.6346 8.31999 94.5866 7.86399C94.5386 7.52799 94.6706 7.35999 94.9826 7.35999H97.5386C97.7786 7.35999 97.9226 7.47999 97.9706 7.71999C98.0186 7.93599 98.0666 8.23599 98.1146 8.61999C98.1626 9.00399 98.2106 9.41199 98.2586 9.84399C98.3066 10.276 98.3306 10.648 98.3306 10.96L98.4386 12.112V24.604C98.4386 24.868 98.3066 25 98.0426 25H95.2346Z" fill="black"/>
<path d="M122.87 25.432C119.894 25.432 117.626 24.7 116.066 23.236C114.506 21.748 113.726 19.6 113.726 16.792V9.66399C113.726 6.83199 114.506 4.68399 116.066 3.21999C117.65 1.73199 119.93 0.987986 122.906 0.987986C124.85 0.987986 126.542 1.31199 127.982 1.95999C129.422 2.60799 130.49 3.54399 131.186 4.76799C131.906 5.96799 132.146 7.40799 131.906 9.08799C131.882 9.23199 131.846 9.35199 131.798 9.44799C131.75 9.54399 131.654 9.59199 131.51 9.59199H128.63C128.366 9.59199 128.246 9.45999 128.27 9.19599C128.342 7.61199 127.922 6.37599 127.01 5.48799C126.098 4.59999 124.742 4.15599 122.942 4.15599C121.142 4.15599 119.762 4.62399 118.802 5.55999C117.842 6.49599 117.362 7.86399 117.362 9.66399V16.756C117.362 18.556 117.842 19.924 118.802 20.86C119.762 21.796 121.142 22.264 122.942 22.264C124.766 22.264 126.146 21.82 127.082 20.932C128.042 20.02 128.438 18.784 128.27 17.224C128.246 16.96 128.366 16.828 128.63 16.828H131.474C131.714 16.828 131.858 16.996 131.906 17.332C132.05 18.964 131.762 20.392 131.042 21.616C130.346 22.816 129.29 23.752 127.874 24.424C126.458 25.096 124.79 25.432 122.87 25.432Z" fill="black"/>
<path d="M142.558 25.432C140.086 25.432 138.19 24.832 136.87 23.632C135.55 22.432 134.89 20.62 134.89 18.196V14.164C134.89 11.74 135.538 9.92799 136.834 8.72799C138.154 7.52799 140.062 6.92799 142.558 6.92799C145.03 6.92799 146.914 7.52799 148.21 8.72799C149.53 9.92799 150.19 11.74 150.19 14.164V18.196C150.19 20.62 149.542 22.432 148.246 23.632C146.95 24.832 145.054 25.432 142.558 25.432ZM142.558 22.3C143.974 22.3 144.994 21.94 145.618 21.22C146.266 20.5 146.59 19.384 146.59 17.872V14.488C146.59 13 146.266 11.896 145.618 11.176C144.994 10.432 143.974 10.06 142.558 10.06C141.118 10.06 140.074 10.432 139.426 11.176C138.802 11.896 138.49 13 138.49 14.488V17.872C138.49 19.384 138.802 20.5 139.426 21.22C140.074 21.94 141.118 22.3 142.558 22.3Z" fill="black"/>
<path d="M158.473 25.432C156.793 25.432 155.509 24.892 154.621 23.812C153.733 22.708 153.289 21.064 153.289 18.88V14.2C153.289 11.848 153.853 10.048 154.981 8.79999C156.133 7.55199 157.729 6.92799 159.769 6.92799C160.657 6.92799 161.605 7.04799 162.613 7.28799C163.645 7.52799 164.449 7.83999 165.025 8.22399L165.169 11.212C164.449 10.828 163.657 10.54 162.793 10.348C161.953 10.132 161.209 10.024 160.561 10.024C159.337 10.024 158.413 10.408 157.789 11.176C157.189 11.92 156.889 13.048 156.889 14.56V18.772C156.889 19.948 157.117 20.836 157.573 21.436C158.029 22.012 158.713 22.3 159.625 22.3C160.201 22.3 160.765 22.192 161.317 21.976C161.869 21.736 162.469 21.376 163.117 20.896C163.765 20.392 164.497 19.72 165.313 18.88L165.349 21.832C164.581 22.6 163.837 23.26 163.117 23.812C162.397 24.34 161.665 24.736 160.921 25C160.177 25.288 159.361 25.432 158.473 25.432ZM165.601 25C165.313 25 165.157 24.868 165.133 24.604C165.085 24.244 165.025 23.8 164.953 23.272C164.881 22.72 164.857 22.252 164.881 21.868L164.629 20.104V0.807986C164.629 0.543987 164.761 0.411987 165.025 0.411987H167.833C168.097 0.411987 168.229 0.543987 168.229 0.807986V20.824C168.229 21.184 168.241 21.604 168.265 22.084C168.289 22.54 168.313 22.984 168.337 23.416C168.385 23.848 168.421 24.208 168.445 24.496C168.517 24.832 168.397 25 168.085 25H165.601Z" fill="black"/>
<path d="M179.171 25.432C176.723 25.432 174.863 24.88 173.591 23.776C172.343 22.672 171.719 21.028 171.719 18.844V14.056C171.719 11.728 172.343 9.96399 173.591 8.76399C174.839 7.53999 176.687 6.92799 179.135 6.92799C181.559 6.92799 183.395 7.51599 184.643 8.69199C185.891 9.86799 186.515 11.584 186.515 13.84V16.648C186.515 16.912 186.395 17.044 186.155 17.044H175.319V18.196C175.319 19.66 175.631 20.728 176.255 21.4C176.879 22.072 177.887 22.408 179.279 22.408C180.335 22.408 181.151 22.24 181.727 21.904C182.303 21.568 182.591 21.052 182.591 20.356C182.591 20.092 182.735 19.96 183.023 19.96H185.795C186.011 19.96 186.143 20.08 186.191 20.32C186.287 21.928 185.711 23.188 184.463 24.1C183.215 24.988 181.451 25.432 179.171 25.432ZM175.319 14.308H182.951V14.128C182.951 12.688 182.639 11.632 182.015 10.96C181.391 10.288 180.443 9.95199 179.171 9.95199C177.875 9.95199 176.903 10.3 176.255 10.996C175.631 11.692 175.319 12.736 175.319 14.128V14.308Z" fill="black"/>
<svg width="289" height="50" viewBox="0 0 289 50" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M264.5 0H288.5V8.5H272.5V16.5H288.5V25H272.5V33H288.5V41.5H264.5V0Z" fill="black"/>
<path d="M248.5 0H224.5V41.5H248.5V33H232.5V8.5H248.5V0Z" fill="black"/>
<path d="M256.5 8.5H248.5V33H256.5V8.5Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M184.5 0H216.5V41.5H184.5V0ZM208.5 8.5H192.5V33H208.5V8.5Z" fill="black"/>
<path d="M144.5 8.5H136.5V41.5H144.5V8.5Z" fill="black"/>
<path d="M136.5 0H112.5V41.5H120.5V8.5H136.5V0Z" fill="black"/>
<path d="M80.5 0H104.5V8.5H88.5V16.5H104.5V25H88.5V33H104.5V41.5H80.5V0Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M40.5 0H72.5V41.5H48.5V49.5H40.5V0ZM64.5 8.5H48.5V33H64.5V8.5Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.5 0H32.5V41.5955H0.5V0ZM24.5 8.5H8.5V33H24.5V8.5Z" fill="black"/>
<path d="M152.5 0H176.5V8.5H160.5V33H176.5V41.5H152.5V0Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 981 B

View File

@@ -167,6 +167,8 @@ export function getDiagnostics(
): string[] {
const result: string[] = []
if (diagnosticsByFile === undefined) return result
for (const diags of Object.values(diagnosticsByFile)) {
for (const d of diags) {
// Only keep diagnostics explicitly marked as Error (severity === 1)
@@ -1395,6 +1397,7 @@ export default function Share(props: {
part().toolInvocation.toolCallId
],
)
const hasError = metadata()?.error
const args = part().toolInvocation.args
const filePath = args.filePath
const diagnostics = createMemo(() =>
@@ -1429,13 +1432,27 @@ export default function Share(props: {
<span data-element-label>Edit</span>
<b>{filePath}</b>
</span>
<div data-part-tool-edit>
<DiffView
class={styles["diff-code-block"]}
diff={metadata()?.diff}
lang={getFileType(filePath)}
/>
</div>
<Switch>
<Match when={hasError}>
<div data-part-tool-result>
<TextPart
expand
data-size="sm"
data-color="dimmed"
text={metadata()?.message}
/>
</div>
</Match>
<Match when={metadata()?.diff}>
<div data-part-tool-edit>
<DiffView
class={styles["diff-code-block"]}
diff={metadata()?.diff}
lang={getFileType(filePath)}
/>
</div>
</Match>
</Switch>
<Show when={diagnostics().length > 0}>
<TextPart
data-size="sm"

View File

@@ -12,8 +12,11 @@
margin-bottom: 1rem;
}
ul,
ol {
list-style-position: inside;
padding-left: 0.75rem;
}
ul {
padding-left: 1.5rem;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 75 KiB