mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-02 16:26:14 -04:00
Compare commits
79 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 00c4d4f9f8 | |||
| 6e6fe6e013 | |||
| d05b60291e | |||
| 5162361372 | |||
| d271b9f75b | |||
| 333569bed3 | |||
| 09b89fdb23 | |||
| 0e8c3359d1 | |||
| 37e0a7050f | |||
| 774dcb6980 | |||
| 28bc49ad17 | |||
| dc1947838c | |||
| 3ea2daaa4c | |||
| 137e964131 | |||
| 8efbe497fd | |||
| 119d2d966c | |||
| 194415e785 | |||
| 1684042fb6 | |||
| 59f0004d34 | |||
| da35a64fa1 | |||
| 460338ca53 | |||
| 53c18a64b4 | |||
| b8144c5654 | |||
| 9081e17fcc | |||
| ef3fd5900f | |||
| 453d690c11 | |||
| c45be6a645 | |||
| 7b9b177088 | |||
| 3cee5b0470 | |||
| 9246d1c901 | |||
| cc12abc83e | |||
| 4f7e4a9436 | |||
| eee396f903 | |||
| 0d2f8e175a | |||
| 4df40e0d9b | |||
| b72e17a8b7 | |||
| 61160dc220 | |||
| 98734ff28c | |||
| 9991352663 | |||
| 91c4da5dbd | |||
| 2fd0e7dd6b | |||
| d50b7ad481 | |||
| df95c49401 | |||
| 8b73c52f00 | |||
| 5603098d17 | |||
| f436a50125 | |||
| e19e977591 | |||
| addbe295b1 | |||
| 9a573dedc6 | |||
| 9ea0d71e8d | |||
| b1a3599017 | |||
| 7b0329f67f | |||
| 311b9c74dd | |||
| f7e8dd2ff8 | |||
| 40b1dd7ef2 | |||
| 261e76e0a3 | |||
| a300bfaccb | |||
| 41dba0db08 | |||
| 6674c6083a | |||
| f6afa2c6bb | |||
| b2fb0508ea | |||
| 93f4252bb1 | |||
| 46ab9c16dd | |||
| d869df4fee | |||
| b99d4650ec | |||
| 261bb7f110 | |||
| 0515fbb260 | |||
| 88211d8c5b | |||
| a812f95b9d | |||
| 3728a12bee | |||
| af07e51213 | |||
| 3113788c92 | |||
| efb5fe6d4e | |||
| 54dd6c644d | |||
| 39ad8f2667 | |||
| c4a2c84e53 | |||
| 44fe012812 | |||
| f5e7f079ea | |||
| 15a8936806 |
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 OpenCode
|
||||
Copyright (c) 2025 opencode
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -7,19 +7,17 @@
|
||||
</picture>
|
||||
</a>
|
||||
</p>
|
||||
<p align="center">AI coding agent, built for the terminal.</p>
|
||||
<p align="center">
|
||||
<a href="https://opencode.ai/docs"><img alt="View docs" src="https://img.shields.io/badge/view-docs-blue?style=flat-square" /></a>
|
||||
<a href="https://www.npmjs.com/package/opencode-ai"><img alt="npm" src="https://img.shields.io/npm/v/opencode-ai?style=flat-square" /></a>
|
||||
<a href="https://github.com/sst/opencode/actions/workflows/publish.yml"><img alt="Build status" src="https://img.shields.io/github/actions/workflow/status/sst/opencode/publish.yml?style=flat-square&branch=dev" /></a>
|
||||
</p>
|
||||
|
||||
[](https://opencode.ai)
|
||||
|
||||
---
|
||||
|
||||
AI coding agent, built for the terminal.
|
||||
|
||||
**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.
|
||||
|
||||
[](https://opencode.ai)
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
@@ -34,167 +32,38 @@ paru -S opencode-bin # Arch Linux
|
||||
|
||||
> **Note:** Remove versions older than 0.1.x before installing
|
||||
|
||||
### Providers
|
||||
### Documentation
|
||||
|
||||
The recommended approach is to sign up for Claude Pro or Max, run `opencode auth login`, and select Anthropic. It's the most cost-effective way to use opencode.
|
||||
|
||||
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
|
||||
$ opencode auth login
|
||||
|
||||
┌ Add credential
|
||||
│
|
||||
◆ Select provider
|
||||
│ ● Anthropic (recommended)
|
||||
│ ○ OpenAI
|
||||
│ ○ Google
|
||||
│ ○ Amazon Bedrock
|
||||
│ ○ Azure
|
||||
│ ○ DeepSeek
|
||||
│ ○ Groq
|
||||
│ ...
|
||||
└
|
||||
```
|
||||
|
||||
The Models.dev dataset is also used to detect common environment variables like `OPENAI_API_KEY` to autoload that provider.
|
||||
|
||||
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.
|
||||
|
||||
### Config
|
||||
|
||||
Config is optional and can be placed in the root of your repo or globally in `~/.config/opencode/config.json`. It can be checked in and shared with your team.
|
||||
|
||||
```json title="opencode.json"
|
||||
{
|
||||
"$schema": "http://opencode.ai/config.json"
|
||||
"theme": "opencode",
|
||||
"model": "anthropic/claude-sonnet-4-20250514" // format is provider/model
|
||||
"autoshare": false,
|
||||
"autoupdate": true,
|
||||
}
|
||||
```
|
||||
|
||||
#### Keybinds
|
||||
|
||||
You can configure custom keybinds, the values listed below are the defaults.
|
||||
|
||||
```json title="opencode.json"
|
||||
{
|
||||
"$schema": "http://opencode.ai/config.json",
|
||||
"keybinds": {
|
||||
"leader": "ctrl+x",
|
||||
"help": "<leader>h",
|
||||
"editor_open": "<leader>e",
|
||||
"session_new": "<leader>n",
|
||||
"session_list": "<leader>l",
|
||||
"session_share": "<leader>s",
|
||||
"session_interrupt": "esc",
|
||||
"session_compact": "<leader>c",
|
||||
"tool_details": "<leader>d",
|
||||
"model_list": "<leader>m",
|
||||
"theme_list": "<leader>t",
|
||||
"project_init": "<leader>i",
|
||||
"input_clear": "ctrl+c",
|
||||
"input_paste": "ctrl+v",
|
||||
"input_submit": "enter",
|
||||
"input_newline": "shift+enter,ctrl+j",
|
||||
"history_previous": "up",
|
||||
"history_next": "down",
|
||||
"messages_page_up": "pgup",
|
||||
"messages_page_down": "pgdown",
|
||||
"messages_half_page_up": "ctrl+alt+u",
|
||||
"messages_half_page_down": "ctrl+alt+d",
|
||||
"messages_previous": "ctrl+alt+k",
|
||||
"messages_next": "ctrl+alt+j",
|
||||
"messages_first": "ctrl+g",
|
||||
"messages_last": "ctrl+alt+g",
|
||||
"app_exit": "ctrl+c,<leader>q"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### MCP
|
||||
|
||||
```json title="opencode.json"
|
||||
{
|
||||
"$schema": "http://opencode.ai/config.json",
|
||||
"mcp": {
|
||||
"localmcp": {
|
||||
"type": "local",
|
||||
"command": ["bun", "x", "my-mcp-command"],
|
||||
"environment": {
|
||||
"MY_ENV_VAR": "my_env_var_value"
|
||||
}
|
||||
},
|
||||
"remotemcp": {
|
||||
"type": "remote",
|
||||
"url": "https://my-mcp-server.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Providers
|
||||
|
||||
You can use opencode with any provider listed at [here](https://ai-sdk.dev/providers/ai-sdk-providers). Be sure to specify the npm package to use to load the provider. Remember most popular providers are preloaded from [models.dev](https://models.dev)
|
||||
|
||||
```json title="opencode.json"
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"provider": {
|
||||
"ollama": {
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"options": {
|
||||
"baseURL": "http://localhost:11434/v1"
|
||||
},
|
||||
"models": {
|
||||
"llama2": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
For more info on how to configure opencode [**head over to our docs**](https://opencode.ai/docs).
|
||||
|
||||
### Contributing
|
||||
|
||||
For any new features we'd appreciate it if you could open an issue first to discuss what you'd like to implement. We're pretty responsive there and it'll save you from working on something that we don't end up using. No need to do this for simpler fixes.
|
||||
|
||||
To run opencode locally you need.
|
||||
|
||||
- Bun
|
||||
- Golang 1.24.x
|
||||
|
||||
To run.
|
||||
And run.
|
||||
|
||||
```bash
|
||||
$ bun install
|
||||
$ bun run packages/opencode/src/index.ts
|
||||
```
|
||||
|
||||
### FAQ
|
||||
#### Development Notes
|
||||
|
||||
#### How do I use this with OpenRouter?
|
||||
**API Client Generation**: After making changes to the TypeScript API endpoints in `packages/opencode/src/server/server.ts`, you need to regenerate the Go client and OpenAPI specification:
|
||||
|
||||
OpenRouter is not in the Models.dev database yet, but you can configure it manually.
|
||||
|
||||
```json title="opencode.json"
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"provider": {
|
||||
"openrouter": {
|
||||
"npm": "@openrouter/ai-sdk-provider",
|
||||
"name": "OpenRouter",
|
||||
"options": {},
|
||||
"models": {
|
||||
"anthropic/claude-3.5-sonnet": {
|
||||
"name": "Claude 3.5 Sonnet"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```bash
|
||||
$ cd packages/tui
|
||||
$ go generate ./pkg/client/
|
||||
```
|
||||
|
||||
And then to configure an api key you can do `opencode auth login` and select "Other -> 'openrouter'"
|
||||
This updates the generated Go client code that the TUI uses to communicate with the backend server.
|
||||
|
||||
### FAQ
|
||||
|
||||
#### How is this different than Claude Code?
|
||||
|
||||
|
||||
@@ -43,8 +43,10 @@
|
||||
"yargs": "18.0.0",
|
||||
"zod": "catalog:",
|
||||
"zod-openapi": "4.2.4",
|
||||
"zod-validation-error": "3.5.2",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ai-sdk/amazon-bedrock": "2.2.10",
|
||||
"@ai-sdk/anthropic": "1.2.12",
|
||||
"@tsconfig/bun": "1.0.7",
|
||||
"@types/bun": "latest",
|
||||
@@ -69,6 +71,7 @@
|
||||
"astro": "5.7.13",
|
||||
"diff": "8.0.2",
|
||||
"js-base64": "3.7.7",
|
||||
"lang-map": "0.4.0",
|
||||
"luxon": "3.6.1",
|
||||
"marked": "15.0.12",
|
||||
"rehype-autolink-headings": "7.1.0",
|
||||
@@ -101,6 +104,8 @@
|
||||
"zod": "3.24.2",
|
||||
},
|
||||
"packages": {
|
||||
"@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@2.2.10", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.0.0" } }, "sha512-icLGO7Q0NinnHIPgT+y1QjHVwH4HwV+brWbvM+FfCG2Afpa89PyKa3Ret91kGjZpBgM/xnj1B7K5eM+rRlsXQA=="],
|
||||
|
||||
"@ai-sdk/anthropic": ["@ai-sdk/anthropic@1.2.12", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8" }, "peerDependencies": { "zod": "^3.0.0" } }, "sha512-YSzjlko7JvuiyQFmI9RN1tNZdEiZxc+6xld/0tq/VkJaHpEzGAb1yiNxxvmYVcjvfu/PcvCxAAYXmTYQQ63IHQ=="],
|
||||
|
||||
"@ai-sdk/provider": ["@ai-sdk/provider@1.1.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg=="],
|
||||
@@ -137,6 +142,12 @@
|
||||
|
||||
"@astrojs/underscore-redirects": ["@astrojs/underscore-redirects@0.6.1", "", {}, "sha512-4bMLrs2KW+8/vHEE5Ffv2HbxCbbgXO+2N6MpoCsMXUlUoi7pgEEx8kbkzMXJ2dZtWF3gvwm9lvgjnFeanC2LGg=="],
|
||||
|
||||
"@aws-crypto/crc32": ["@aws-crypto/crc32@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg=="],
|
||||
|
||||
"@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="],
|
||||
|
||||
"@aws-sdk/types": ["@aws-sdk/types@3.821.0", "", { "dependencies": { "@smithy/types": "^4.3.1", "tslib": "^2.6.2" } }, "sha512-Znroqdai1a90TlxGaJ+FK1lwC0fHpo97Xjsp5UKGR5JODYm7f9+/fF17ebO1KdoBr/Rm0UIFiF5VmI8ts9F1eA=="],
|
||||
|
||||
"@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
||||
|
||||
"@babel/compat-data": ["@babel/compat-data@7.27.3", "", {}, "sha512-V42wFfx1ymFte+ecf6iXghnnP8kWTO+ZLXIyZq+1LAXHHvTZdVxicn4yiVYdYMGaCO3tmqub11AorKkv+iodqw=="],
|
||||
@@ -417,6 +428,18 @@
|
||||
|
||||
"@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],
|
||||
|
||||
"@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.0.4", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.3.1", "@smithy/util-hex-encoding": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-7XoWfZqWb/QoR/rAU4VSi0mWnO2vu9/ltS6JZ5ZSZv0eovLVfDfu0/AX4ub33RsJTOth3TiFWSHS5YdztvFnig=="],
|
||||
|
||||
"@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw=="],
|
||||
|
||||
"@smithy/types": ["@smithy/types@4.3.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-UqKOQBL2x6+HWl3P+3QqFD4ncKq0I8Nuz9QItGv5WuKuMHuuwlhvqcZCoXGfc+P1QmfJE7VieykoYYmrOoFJxA=="],
|
||||
|
||||
"@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.0.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug=="],
|
||||
|
||||
"@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Yk5mLhHtfIgW2W2WQZWSg5kuMZCVbvhFmC7rV4IO2QqnZdbEFPmQnCcGMAX2z/8Qj3B9hYYNjZOhWym+RwhePw=="],
|
||||
|
||||
"@smithy/util-utf8": ["@smithy/util-utf8@4.0.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow=="],
|
||||
|
||||
"@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="],
|
||||
|
||||
"@swc/helpers": ["@swc/helpers@0.5.17", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A=="],
|
||||
@@ -989,6 +1012,10 @@
|
||||
|
||||
"klona": ["klona@2.0.6", "", {}, "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA=="],
|
||||
|
||||
"lang-map": ["lang-map@0.4.0", "", { "dependencies": { "language-map": "^1.1.0" } }, "sha512-oiSqZIEUnWdFeDNsp4HId4tAxdFbx5iMBOwA3666Fn2L8Khj8NiD9xRvMsGmKXopPVkaDFtSv3CJOmXFUB0Hcg=="],
|
||||
|
||||
"language-map": ["language-map@1.5.0", "", {}, "sha512-n7gFZpe+DwEAX9cXVTw43i3wiudWDDtSn28RmdnS/HCPr284dQI/SztsamWanRr75oSlKSaGbV2nmWCTzGCoVg=="],
|
||||
|
||||
"leven": ["leven@2.1.0", "", {}, "sha512-nvVPLpIHUxCUoRLrFqTgSxXJ614d8AgQoWl7zPe/2VadE8+1dpU3LBhowRuBAcuwruWtOdD8oYC9jDNJjXDPyA=="],
|
||||
|
||||
"longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="],
|
||||
@@ -1655,12 +1682,24 @@
|
||||
|
||||
"zod-to-ts": ["zod-to-ts@1.2.0", "", { "peerDependencies": { "typescript": "^4.9.4 || ^5.0.2", "zod": "^3" } }, "sha512-x30XE43V+InwGpvTySRNz9kB7qFU8DlyEy7BsSTCHPH1R0QasMmHWZDCzYm6bVXtj/9NNJAZF3jW8rzFvH5OFA=="],
|
||||
|
||||
"zod-validation-error": ["zod-validation-error@3.5.2", "", { "peerDependencies": { "zod": "^3.25.0" } }, "sha512-mdi7YOLtram5dzJ5aDtm1AG9+mxRma1iaMrZdYIpFO7epdKBUwLHIxTF8CPDeCQ828zAXYtizrKlEJAtzgfgrw=="],
|
||||
|
||||
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
|
||||
|
||||
"@ai-sdk/amazon-bedrock/aws4fetch": ["aws4fetch@1.0.20", "", {}, "sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g=="],
|
||||
|
||||
"@ampproject/remapping/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
|
||||
|
||||
"@astrojs/mdx/@astrojs/markdown-remark": ["@astrojs/markdown-remark@6.3.2", "", { "dependencies": { "@astrojs/internal-helpers": "0.6.1", "@astrojs/prism": "3.3.0", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "hast-util-to-text": "^4.0.2", "import-meta-resolve": "^4.1.0", "js-yaml": "^4.1.0", "mdast-util-definitions": "^6.0.0", "rehype-raw": "^7.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remark-smartypants": "^3.0.2", "shiki": "^3.2.1", "smol-toml": "^1.3.1", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.1", "vfile": "^6.0.3" } }, "sha512-bO35JbWpVvyKRl7cmSJD822e8YA8ThR/YbUsciWNA7yTcqpIAL2hJDToWP5KcZBWxGT6IOdOkHSXARSNZc4l/Q=="],
|
||||
|
||||
"@aws-crypto/crc32/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="],
|
||||
|
||||
"@aws-crypto/util/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"@aws-sdk/types/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
|
||||
@@ -1681,6 +1720,18 @@
|
||||
|
||||
"@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
||||
|
||||
"@smithy/eventstream-codec/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"@smithy/is-array-buffer/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"@smithy/types/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"@smithy/util-buffer-from/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"@smithy/util-hex-encoding/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"@smithy/util-utf8/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"@swc/helpers/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"ansi-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
@@ -1751,6 +1802,8 @@
|
||||
|
||||
"@astrojs/mdx/@astrojs/markdown-remark/@astrojs/prism": ["@astrojs/prism@3.3.0", "", { "dependencies": { "prismjs": "^1.30.0" } }, "sha512-q8VwfU/fDZNoDOf+r7jUnMC2//H2l0TuQ6FkGJL8vD8nw/q5KiL3DS1KKBI3QhI9UQhpJ5dc7AtqfbXWuOgLCQ=="],
|
||||
|
||||
"@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="],
|
||||
|
||||
"@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
||||
|
||||
"ansi-align/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
@@ -1817,6 +1870,8 @@
|
||||
|
||||
"wrangler/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.4", "", { "os": "win32", "cpu": "x64" }, "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ=="],
|
||||
|
||||
"@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="],
|
||||
|
||||
"ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"args/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="],
|
||||
|
||||
+3
-2
@@ -23,8 +23,9 @@ export const api = new sst.cloudflare.Worker("Api", {
|
||||
},
|
||||
])
|
||||
args.migrations = {
|
||||
oldTag: "v1",
|
||||
newTag: "v1",
|
||||
// Note: when releasing the next tag, make sure all stages use tag v2
|
||||
oldTag: $app.stage === "production" ? "" : "v1",
|
||||
newTag: $app.stage === "production" ? "" : "v1",
|
||||
//newSqliteClasses: ["SyncServer"],
|
||||
}
|
||||
},
|
||||
|
||||
+1
-3
@@ -1,5 +1,3 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"keybinds": {},
|
||||
"mcp": {}
|
||||
"$schema": "https://opencode.ai/config.json"
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
- **Typecheck**: `bun run typecheck` (npm run typecheck)
|
||||
- **Test**: `bun test` (runs all tests)
|
||||
- **Single test**: `bun test test/tool/tool.test.ts` (specific test file)
|
||||
- **API Client Generation**: `cd packages/tui && go generate ./pkg/client/` (after changes to server endpoints)
|
||||
|
||||
## Code Style
|
||||
|
||||
@@ -37,3 +38,4 @@
|
||||
- **Validation**: All inputs validated with Zod schemas
|
||||
- **Logging**: Use `Log.create({ service: "name" })` pattern
|
||||
- **Storage**: Use `Storage` namespace for persistence
|
||||
- **API Client**: Go TUI communicates with TypeScript server via generated client. When adding/modifying server endpoints in `packages/opencode/src/server/server.ts`, run `cd packages/tui && go generate ./pkg/client/` to update the Go client code and OpenAPI spec.
|
||||
|
||||
@@ -12,13 +12,14 @@
|
||||
"./*": "./src/*.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ai-sdk/amazon-bedrock": "2.2.10",
|
||||
"@ai-sdk/anthropic": "1.2.12",
|
||||
"@tsconfig/bun": "1.0.7",
|
||||
"@types/bun": "latest",
|
||||
"@types/turndown": "5.0.5",
|
||||
"@types/yargs": "17.0.33",
|
||||
"typescript": "catalog:",
|
||||
"zod-to-json-schema": "3.24.5",
|
||||
"@ai-sdk/anthropic": "1.2.12"
|
||||
"zod-to-json-schema": "3.24.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@clack/prompts": "0.11.0",
|
||||
@@ -42,6 +43,7 @@
|
||||
"xdg-basedir": "5.1.0",
|
||||
"yargs": "18.0.0",
|
||||
"zod": "catalog:",
|
||||
"zod-openapi": "4.2.4"
|
||||
"zod-openapi": "4.2.4",
|
||||
"zod-validation-error": "3.5.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import "zod-openapi/extend"
|
||||
import { Log } from "../util/log"
|
||||
import { Context } from "../util/context"
|
||||
import { Filesystem } from "../util/filesystem"
|
||||
|
||||
@@ -8,7 +8,7 @@ export namespace AuthAnthropic {
|
||||
const pkce = await generatePKCE()
|
||||
const url = new URL("https://claude.ai/oauth/authorize", import.meta.url)
|
||||
url.searchParams.set("code", "true")
|
||||
url.searchParams.set("client_id", "9d1c250a-e61b-44d9-88ed-5944d1962f5e")
|
||||
url.searchParams.set("client_id", CLIENT_ID)
|
||||
url.searchParams.set("response_type", "code")
|
||||
url.searchParams.set(
|
||||
"redirect_uri",
|
||||
@@ -38,7 +38,7 @@ export namespace AuthAnthropic {
|
||||
code: splits[0],
|
||||
state: splits[1],
|
||||
grant_type: "authorization_code",
|
||||
client_id: "9d1c250a-e61b-44d9-88ed-5944d1962f5e",
|
||||
client_id: CLIENT_ID,
|
||||
redirect_uri: "https://console.anthropic.com/oauth/code/callback",
|
||||
code_verifier: verifier,
|
||||
}),
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
import { z } from "zod"
|
||||
import { Auth } from "./index"
|
||||
import { NamedError } from "../util/error"
|
||||
|
||||
export namespace AuthGithubCopilot {
|
||||
const CLIENT_ID = "Iv1.b507a08c87ecfe98"
|
||||
const DEVICE_CODE_URL = "https://github.com/login/device/code"
|
||||
const ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token"
|
||||
const COPILOT_API_KEY_URL = "https://api.github.com/copilot_internal/v2/token"
|
||||
|
||||
interface DeviceCodeResponse {
|
||||
device_code: string
|
||||
user_code: string
|
||||
verification_uri: string
|
||||
expires_in: number
|
||||
interval: number
|
||||
}
|
||||
|
||||
interface AccessTokenResponse {
|
||||
access_token?: string
|
||||
error?: string
|
||||
error_description?: string
|
||||
}
|
||||
|
||||
interface CopilotTokenResponse {
|
||||
token: string
|
||||
expires_at: number
|
||||
refresh_in: number
|
||||
endpoints: {
|
||||
api: string
|
||||
}
|
||||
}
|
||||
|
||||
export async function authorize() {
|
||||
const deviceResponse = await fetch(DEVICE_CODE_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "GithubCopilot/1.155.0",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
client_id: CLIENT_ID,
|
||||
scope: "read:user",
|
||||
}),
|
||||
})
|
||||
const deviceData: DeviceCodeResponse = await deviceResponse.json()
|
||||
return {
|
||||
device: deviceData.device_code,
|
||||
user: deviceData.user_code,
|
||||
verification: deviceData.verification_uri,
|
||||
interval: deviceData.interval || 5,
|
||||
expiry: deviceData.expires_in,
|
||||
}
|
||||
}
|
||||
|
||||
export async function poll(device_code: string) {
|
||||
const response = await fetch(ACCESS_TOKEN_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "GithubCopilot/1.155.0",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
client_id: CLIENT_ID,
|
||||
device_code,
|
||||
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) return "failed"
|
||||
|
||||
const data: AccessTokenResponse = await response.json()
|
||||
|
||||
if (data.access_token) {
|
||||
// Store the GitHub OAuth token
|
||||
await Auth.set("github-copilot", {
|
||||
type: "oauth",
|
||||
refresh: data.access_token,
|
||||
access: "",
|
||||
expires: 0,
|
||||
})
|
||||
return "complete"
|
||||
}
|
||||
|
||||
if (data.error === "authorization_pending") return "pending"
|
||||
|
||||
if (data.error) return "failed"
|
||||
|
||||
return "pending"
|
||||
}
|
||||
|
||||
export async function access() {
|
||||
const info = await Auth.get("github-copilot")
|
||||
if (!info || info.type !== "oauth") return
|
||||
if (info.access && info.expires > Date.now()) return info.access
|
||||
|
||||
// Get new Copilot API token
|
||||
const response = await fetch(COPILOT_API_KEY_URL, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
Authorization: `Bearer ${info.refresh}`,
|
||||
"User-Agent": "GithubCopilot/1.155.0",
|
||||
"Editor-Version": "vscode/1.85.1",
|
||||
"Editor-Plugin-Version": "copilot/1.155.0",
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) return
|
||||
|
||||
const tokenData: CopilotTokenResponse = await response.json()
|
||||
|
||||
// Store the Copilot API token
|
||||
await Auth.set("github-copilot", {
|
||||
type: "oauth",
|
||||
refresh: info.refresh,
|
||||
access: tokenData.token,
|
||||
expires: tokenData.expires_at * 1000,
|
||||
})
|
||||
|
||||
return tokenData.token
|
||||
}
|
||||
|
||||
export const DeviceCodeError = NamedError.create(
|
||||
"DeviceCodeError",
|
||||
z.object({}),
|
||||
)
|
||||
|
||||
export const TokenExchangeError = NamedError.create(
|
||||
"TokenExchangeError",
|
||||
z.object({
|
||||
message: z.string(),
|
||||
}),
|
||||
)
|
||||
|
||||
export const AuthenticationError = NamedError.create(
|
||||
"AuthenticationError",
|
||||
z.object({
|
||||
message: z.string(),
|
||||
}),
|
||||
)
|
||||
|
||||
export const CopilotTokenError = NamedError.create(
|
||||
"CopilotTokenError",
|
||||
z.object({
|
||||
message: z.string(),
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { AuthAnthropic } from "../../auth/anthropic"
|
||||
import { AuthGithubCopilot } from "../../auth/github-copilot"
|
||||
import { Auth } from "../../auth"
|
||||
import { cmd } from "./cmd"
|
||||
import * as prompts from "@clack/prompts"
|
||||
@@ -9,7 +10,7 @@ import { map, pipe, sortBy, values } from "remeda"
|
||||
|
||||
export const AuthCommand = cmd({
|
||||
command: "auth",
|
||||
describe: "manage credentials",
|
||||
describe: "Manage credentials",
|
||||
builder: (yargs) =>
|
||||
yargs
|
||||
.command(AuthLoginCommand)
|
||||
@@ -47,8 +48,9 @@ export const AuthLoginCommand = cmd({
|
||||
const providers = await ModelsDev.get()
|
||||
const priority: Record<string, number> = {
|
||||
anthropic: 0,
|
||||
openai: 1,
|
||||
google: 2,
|
||||
"github-copilot": 1,
|
||||
openai: 2,
|
||||
google: 3,
|
||||
}
|
||||
let provider = await prompts.select({
|
||||
message: "Select provider",
|
||||
@@ -146,6 +148,37 @@ export const AuthLoginCommand = cmd({
|
||||
}
|
||||
}
|
||||
|
||||
if (provider === "github-copilot") {
|
||||
await new Promise((resolve) => setTimeout(resolve, 10))
|
||||
const deviceInfo = await AuthGithubCopilot.authorize()
|
||||
|
||||
prompts.note(
|
||||
`Please visit: ${deviceInfo.verification}\nEnter code: ${deviceInfo.user}`,
|
||||
)
|
||||
|
||||
const spinner = prompts.spinner()
|
||||
spinner.start("Waiting for authorization...")
|
||||
|
||||
while (true) {
|
||||
await new Promise((resolve) =>
|
||||
setTimeout(resolve, deviceInfo.interval * 1000),
|
||||
)
|
||||
const status = await AuthGithubCopilot.poll(deviceInfo.device)
|
||||
if (status === "pending") continue
|
||||
if (status === "complete") {
|
||||
spinner.stop("Login successful")
|
||||
break
|
||||
}
|
||||
if (status === "failed") {
|
||||
spinner.stop("Failed to authorize", 1)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
prompts.outro("Done")
|
||||
return
|
||||
}
|
||||
|
||||
const key = await prompts.password({
|
||||
message: "Enter your API key",
|
||||
validate: (x) => (x.length > 0 ? undefined : "Required"),
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import { AuthAnthropic } from "../../auth/anthropic"
|
||||
import { UI } from "../ui"
|
||||
|
||||
// Example: https://claude.ai/oauth/authorize?code=true&client_id=9d1c250a-e61b-44d9-88ed-5944d1962f5e&response_type=code&redirect_uri=https%3A%2F%2Fconsole.anthropic.com%2Foauth%2Fcode%2Fcallback&scope=org%3Acreate_api_key+user%3Aprofile+user%3Ainference&code_challenge=MdFtFgFap23AWDSN0oa3-eaKjQRFE4CaEhXx8M9fHZg&code_challenge_method=S256&state=rKLtaDzm88GSwekyEqdi0wXX-YqIr13tSzYymSzpvfs
|
||||
|
||||
export const LoginAnthropicCommand = {
|
||||
command: "anthropic",
|
||||
describe: "Login to Anthropic",
|
||||
handler: async () => {
|
||||
const { url, verifier } = await AuthAnthropic.authorize()
|
||||
|
||||
UI.println("Login to Anthropic")
|
||||
UI.println("Open the following URL in your browser:")
|
||||
UI.println(url)
|
||||
UI.println("")
|
||||
|
||||
const code = await UI.input("Paste the authorization code here: ")
|
||||
await AuthAnthropic.exchange(code, verifier)
|
||||
},
|
||||
}
|
||||
@@ -29,7 +29,7 @@ export const RunCommand = cmd({
|
||||
builder: (yargs: Argv) => {
|
||||
return yargs
|
||||
.positional("message", {
|
||||
describe: "Message to send",
|
||||
describe: "message to send",
|
||||
type: "string",
|
||||
array: true,
|
||||
default: [],
|
||||
@@ -46,7 +46,7 @@ export const RunCommand = cmd({
|
||||
})
|
||||
.option("share", {
|
||||
type: "boolean",
|
||||
describe: "Share the session",
|
||||
describe: "share the session",
|
||||
})
|
||||
.option("model", {
|
||||
type: "string",
|
||||
@@ -90,8 +90,8 @@ export const RunCommand = cmd({
|
||||
await Session.share(session.id)
|
||||
UI.println(
|
||||
UI.Style.TEXT_INFO_BOLD +
|
||||
"~ https://opencode.ai/s/" +
|
||||
session.id.slice(-8),
|
||||
"~ https://opencode.ai/s/" +
|
||||
session.id.slice(-8),
|
||||
)
|
||||
}
|
||||
UI.empty()
|
||||
@@ -109,8 +109,8 @@ export const RunCommand = cmd({
|
||||
UI.println(
|
||||
color + `|`,
|
||||
UI.Style.TEXT_NORMAL +
|
||||
UI.Style.TEXT_DIM +
|
||||
` ${type.padEnd(7, " ")}`,
|
||||
UI.Style.TEXT_DIM +
|
||||
` ${type.padEnd(7, " ")}`,
|
||||
"",
|
||||
UI.Style.TEXT_NORMAL + title,
|
||||
)
|
||||
|
||||
@@ -5,19 +5,27 @@ import { Installation } from "../../installation"
|
||||
|
||||
export const UpgradeCommand = {
|
||||
command: "upgrade [target]",
|
||||
describe: "Upgrade opencode to the latest version or a specific version",
|
||||
describe: "upgrade opencode to the latest version or a specific version",
|
||||
builder: (yargs: Argv) => {
|
||||
return yargs.positional("target", {
|
||||
describe: "Specific version to upgrade to (e.g., '0.1.48' or 'v0.1.48')",
|
||||
type: "string",
|
||||
})
|
||||
return yargs
|
||||
.positional("target", {
|
||||
describe: "specific version to upgrade to (e.g., '0.1.48' or 'v0.1.48')",
|
||||
type: "string",
|
||||
})
|
||||
.option("method", {
|
||||
alias: "m",
|
||||
describe: "installation method to use (curl, npm, pnpm, bun, brew)",
|
||||
type: "string",
|
||||
choices: ["curl", "npm", "pnpm", "bun", "brew"],
|
||||
})
|
||||
},
|
||||
handler: async (args: { target?: string }) => {
|
||||
handler: async (args: { target?: string; method?: string }) => {
|
||||
UI.empty()
|
||||
UI.println(UI.logo(" "))
|
||||
UI.empty()
|
||||
prompts.intro("Upgrade")
|
||||
const method = await Installation.method()
|
||||
const detectedMethod = await Installation.method()
|
||||
const method = (args.method as Installation.Method) ?? detectedMethod
|
||||
if (method === "unknown") {
|
||||
prompts.log.error(
|
||||
`opencode is installed to ${process.execPath} and seems to be managed by a package manager`,
|
||||
@@ -25,6 +33,7 @@ export const UpgradeCommand = {
|
||||
prompts.outro("Done")
|
||||
return
|
||||
}
|
||||
prompts.log.info("Using method: " + method)
|
||||
const target = args.target ?? (await Installation.latest())
|
||||
prompts.log.info(`From ${Installation.VERSION} → ${target}`)
|
||||
const spinner = prompts.spinner()
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Config } from "../config/config"
|
||||
|
||||
export function FormatError(input: unknown) {
|
||||
if (Config.JsonError.isInstance(input))
|
||||
return `Config file at ${input.data.path} is not valid JSON`
|
||||
if (Config.InvalidError.isInstance(input))
|
||||
return [
|
||||
`Config file at ${input.data.path} is invalid`,
|
||||
...(input.data.issues?.map(
|
||||
(issue) => "↳ " + issue.message + " " + issue.path.join("."),
|
||||
) ?? []),
|
||||
].join("\n")
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import { mergeDeep } from "remeda"
|
||||
import { Global } from "../global"
|
||||
import fs from "fs/promises"
|
||||
import { lazy } from "../util/lazy"
|
||||
import { NamedError } from "../util/error"
|
||||
|
||||
export namespace Config {
|
||||
const log = Log.create({ service: "config" })
|
||||
@@ -15,27 +16,9 @@ export namespace Config {
|
||||
export const state = App.state("config", async (app) => {
|
||||
let result = await global()
|
||||
for (const file of ["opencode.jsonc", "opencode.json"]) {
|
||||
const [resolved] = await Filesystem.findUp(
|
||||
file,
|
||||
app.path.cwd,
|
||||
app.path.root,
|
||||
)
|
||||
if (!resolved) continue
|
||||
try {
|
||||
result = mergeDeep(
|
||||
result,
|
||||
await import(resolved).then((mod) => Info.parse(mod.default)),
|
||||
)
|
||||
log.info("found", { path: resolved })
|
||||
break
|
||||
} catch (e) {
|
||||
if (e instanceof z.ZodError) {
|
||||
for (const issue of e.issues) {
|
||||
log.info(issue.message)
|
||||
}
|
||||
throw e
|
||||
}
|
||||
continue
|
||||
const found = await Filesystem.findUp(file, app.path.cwd, app.path.root)
|
||||
for (const resolved of found.toReversed()) {
|
||||
result = mergeDeep(result, await load(resolved))
|
||||
}
|
||||
}
|
||||
log.info("loaded", result)
|
||||
@@ -45,9 +28,16 @@ export namespace Config {
|
||||
export const McpLocal = z
|
||||
.object({
|
||||
type: z.literal("local").describe("Type of MCP server connection"),
|
||||
command: z.string().array().describe("Command and arguments to run the MCP server"),
|
||||
environment: z.record(z.string(), z.string()).optional().describe("Environment variables to set when running the MCP server"),
|
||||
command: z
|
||||
.string()
|
||||
.array()
|
||||
.describe("Command and arguments to run the MCP server"),
|
||||
environment: z
|
||||
.record(z.string(), z.string())
|
||||
.optional()
|
||||
.describe("Environment variables to set when running the MCP server"),
|
||||
})
|
||||
.strict()
|
||||
.openapi({
|
||||
ref: "Config.McpLocal",
|
||||
})
|
||||
@@ -57,6 +47,7 @@ export namespace Config {
|
||||
type: z.literal("remote").describe("Type of MCP server connection"),
|
||||
url: z.string().describe("URL of the remote MCP server"),
|
||||
})
|
||||
.strict()
|
||||
.openapi({
|
||||
ref: "Config.McpRemote",
|
||||
})
|
||||
@@ -66,41 +57,84 @@ export namespace Config {
|
||||
|
||||
export const Keybinds = z
|
||||
.object({
|
||||
leader: z.string().optional().describe("Leader key for keybind combinations"),
|
||||
leader: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Leader key for keybind combinations"),
|
||||
help: z.string().optional().describe("Show help dialog"),
|
||||
editor_open: z.string().optional().describe("Open external editor"),
|
||||
session_new: z.string().optional().describe("Create a new session"),
|
||||
session_list: z.string().optional().describe("List all sessions"),
|
||||
session_share: z.string().optional().describe("Share current session"),
|
||||
session_interrupt: z.string().optional().describe("Interrupt current session"),
|
||||
session_compact: z.string().optional().describe("Toggle compact mode for session"),
|
||||
session_interrupt: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Interrupt current session"),
|
||||
session_compact: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Toggle compact mode for session"),
|
||||
tool_details: z.string().optional().describe("Show tool details"),
|
||||
model_list: z.string().optional().describe("List available models"),
|
||||
theme_list: z.string().optional().describe("List available themes"),
|
||||
project_init: z.string().optional().describe("Initialize project configuration"),
|
||||
project_init: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Initialize project configuration"),
|
||||
input_clear: z.string().optional().describe("Clear input field"),
|
||||
input_paste: z.string().optional().describe("Paste from clipboard"),
|
||||
input_submit: z.string().optional().describe("Submit input"),
|
||||
input_newline: z.string().optional().describe("Insert newline in input"),
|
||||
history_previous: z.string().optional().describe("Navigate to previous history item"),
|
||||
history_next: z.string().optional().describe("Navigate to next history item"),
|
||||
messages_page_up: z.string().optional().describe("Scroll messages up by one page"),
|
||||
messages_page_down: z.string().optional().describe("Scroll messages down by one page"),
|
||||
messages_half_page_up: z.string().optional().describe("Scroll messages up by half page"),
|
||||
messages_half_page_down: z.string().optional().describe("Scroll messages down by half page"),
|
||||
messages_previous: z.string().optional().describe("Navigate to previous message"),
|
||||
history_previous: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Navigate to previous history item"),
|
||||
history_next: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Navigate to next history item"),
|
||||
messages_page_up: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Scroll messages up by one page"),
|
||||
messages_page_down: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Scroll messages down by one page"),
|
||||
messages_half_page_up: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Scroll messages up by half page"),
|
||||
messages_half_page_down: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Scroll messages down by half page"),
|
||||
messages_previous: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Navigate to previous message"),
|
||||
messages_next: z.string().optional().describe("Navigate to next message"),
|
||||
messages_first: z.string().optional().describe("Navigate to first message"),
|
||||
messages_first: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Navigate to first message"),
|
||||
messages_last: z.string().optional().describe("Navigate to last message"),
|
||||
app_exit: z.string().optional().describe("Exit the application"),
|
||||
})
|
||||
.strict()
|
||||
.openapi({
|
||||
ref: "Config.Keybinds",
|
||||
})
|
||||
export const Info = z
|
||||
.object({
|
||||
$schema: z.string().optional().describe("JSON schema reference for configuration validation"),
|
||||
theme: z.string().optional().describe("Theme name to use for the interface"),
|
||||
$schema: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("JSON schema reference for configuration validation"),
|
||||
theme: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Theme name to use for the interface"),
|
||||
keybinds: Keybinds.optional().describe("Custom keybind configurations"),
|
||||
autoshare: z
|
||||
.boolean()
|
||||
@@ -129,8 +163,12 @@ export namespace Config {
|
||||
)
|
||||
.optional()
|
||||
.describe("Custom provider configurations and model overrides"),
|
||||
mcp: z.record(z.string(), Mcp).optional().describe("MCP (Model Context Protocol) server configurations"),
|
||||
mcp: z
|
||||
.record(z.string(), Mcp)
|
||||
.optional()
|
||||
.describe("MCP (Model Context Protocol) server configurations"),
|
||||
})
|
||||
.strict()
|
||||
.openapi({
|
||||
ref: "Config.Info",
|
||||
})
|
||||
@@ -138,10 +176,7 @@ export namespace Config {
|
||||
export type Info = z.output<typeof Info>
|
||||
|
||||
export const global = lazy(async () => {
|
||||
let result = await Bun.file(path.join(Global.Path.config, "config.json"))
|
||||
.json()
|
||||
.then((mod) => Info.parse(mod))
|
||||
.catch(() => ({}) as Info)
|
||||
let result = await load(path.join(Global.Path.config, "config.json"))
|
||||
|
||||
await import(path.join(Global.Path.config, "config"), {
|
||||
with: {
|
||||
@@ -160,9 +195,38 @@ export namespace Config {
|
||||
await fs.unlink(path.join(Global.Path.config, "config"))
|
||||
})
|
||||
.catch(() => {})
|
||||
return Info.parse(result)
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
async function load(path: string) {
|
||||
const data = await Bun.file(path)
|
||||
.json()
|
||||
.catch((err) => {
|
||||
if (err.code === "ENOENT") return {}
|
||||
throw new JsonError({ path }, { cause: err })
|
||||
})
|
||||
|
||||
const parsed = Info.safeParse(data)
|
||||
if (parsed.success) return parsed.data
|
||||
throw new InvalidError({ path, issues: parsed.error.issues })
|
||||
}
|
||||
|
||||
export const JsonError = NamedError.create(
|
||||
"ConfigJsonError",
|
||||
z.object({
|
||||
path: z.string(),
|
||||
}),
|
||||
)
|
||||
|
||||
export const InvalidError = NamedError.create(
|
||||
"ConfigInvalidError",
|
||||
z.object({
|
||||
path: z.string(),
|
||||
issues: z.custom<z.ZodIssue[]>().optional(),
|
||||
}),
|
||||
)
|
||||
|
||||
export function get() {
|
||||
return state()
|
||||
}
|
||||
|
||||
@@ -41,6 +41,17 @@ export namespace Identifier {
|
||||
return given
|
||||
}
|
||||
|
||||
function randomBase62(length: number): string {
|
||||
const chars =
|
||||
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
||||
let result = ""
|
||||
const bytes = randomBytes(length)
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += chars[bytes[i] % 62]
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function generateNewID(
|
||||
prefix: keyof typeof prefixes,
|
||||
descending: boolean,
|
||||
@@ -62,14 +73,11 @@ export namespace Identifier {
|
||||
timeBytes[i] = Number((now >> BigInt(40 - 8 * i)) & BigInt(0xff))
|
||||
}
|
||||
|
||||
const randLength = (LENGTH - 12) / 2
|
||||
const random = randomBytes(randLength)
|
||||
|
||||
return (
|
||||
prefixes[prefix] +
|
||||
"_" +
|
||||
timeBytes.toString("hex") +
|
||||
random.toString("hex")
|
||||
randomBase62(LENGTH - 12)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,8 @@ import { UI } from "./cli/ui"
|
||||
import { Installation } from "./installation"
|
||||
import { Bus } from "./bus"
|
||||
import { Config } from "./config/config"
|
||||
import { NamedError } from "./util/error"
|
||||
import { FormatError } from "./cli/error"
|
||||
|
||||
const cli = yargs(hideBin(process.argv))
|
||||
.scriptName("opencode")
|
||||
@@ -36,7 +38,7 @@ const cli = yargs(hideBin(process.argv))
|
||||
.usage("\n" + UI.logo())
|
||||
.command({
|
||||
command: "$0 [project]",
|
||||
describe: "Start opencode TUI",
|
||||
describe: "start opencode TUI",
|
||||
builder: (yargs) =>
|
||||
yargs.positional("project", {
|
||||
type: "string",
|
||||
@@ -133,7 +135,25 @@ const cli = yargs(hideBin(process.argv))
|
||||
try {
|
||||
await cli.parse()
|
||||
} catch (e) {
|
||||
Log.Default.error(e, {
|
||||
stack: e instanceof Error ? e.stack : undefined,
|
||||
})
|
||||
const data: Record<string, any> = {}
|
||||
if (e instanceof NamedError) {
|
||||
const obj = e.toObject()
|
||||
Object.assign(data, {
|
||||
...obj.data,
|
||||
})
|
||||
}
|
||||
if (e instanceof Error) {
|
||||
Object.assign(data, {
|
||||
name: e.name,
|
||||
message: e.message,
|
||||
cause: e.cause?.toString(),
|
||||
})
|
||||
}
|
||||
Log.Default.error("fatal", data)
|
||||
const formatted = FormatError(e)
|
||||
if (formatted) UI.error(formatted)
|
||||
if (!formatted)
|
||||
UI.error(
|
||||
"Unexpected error, check log file at " + Log.file() + " for more details",
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,12 +3,15 @@ import { $ } from "bun"
|
||||
import { z } from "zod"
|
||||
import { NamedError } from "../util/error"
|
||||
import { Bus } from "../bus"
|
||||
import { Log } from "../util/log"
|
||||
|
||||
declare global {
|
||||
const OPENCODE_VERSION: string
|
||||
}
|
||||
|
||||
export namespace Installation {
|
||||
const log = Log.create({ service: "installation" })
|
||||
|
||||
export type Method = Awaited<ReturnType<typeof method>>
|
||||
|
||||
export const Event = {
|
||||
@@ -66,6 +69,10 @@ export namespace Installation {
|
||||
name: "bun" as const,
|
||||
command: () => $`bun pm ls -g`.throws(false).text(),
|
||||
},
|
||||
{
|
||||
name: "brew" as const,
|
||||
command: () => $`brew list --formula opencode-ai`.throws(false).text(),
|
||||
},
|
||||
]
|
||||
|
||||
checks.sort((a, b) => {
|
||||
@@ -97,18 +104,29 @@ export namespace Installation {
|
||||
const cmd = (() => {
|
||||
switch (method) {
|
||||
case "curl":
|
||||
return $`curl -fsSL https://opencode.ai/install | bash`
|
||||
return $`curl -fsSL https://opencode.ai/install | bash`.env({
|
||||
...process.env,
|
||||
VERSION: target,
|
||||
})
|
||||
case "npm":
|
||||
return $`npm install -g opencode-ai@${target}`
|
||||
case "pnpm":
|
||||
return $`pnpm install -g opencode-ai@${target}`
|
||||
case "bun":
|
||||
return $`bun install -g opencode-ai@${target}`
|
||||
case "brew":
|
||||
return $`brew install sst/tap/opencode`
|
||||
default:
|
||||
throw new Error(`Unknown method: ${method}`)
|
||||
}
|
||||
})()
|
||||
const result = await cmd.quiet().throws(false)
|
||||
log.info("upgraded", {
|
||||
method,
|
||||
target,
|
||||
stdout: result.stdout.toString(),
|
||||
stderr: result.stderr.toString(),
|
||||
})
|
||||
if (result.exitCode !== 0)
|
||||
throw new UpgradeFailedError({
|
||||
stderr: result.stderr.toString("utf8"),
|
||||
|
||||
@@ -70,6 +70,9 @@ export namespace LSPServer {
|
||||
const proc = Bun.spawn({
|
||||
cmd: ["go", "install", "golang.org/x/tools/gopls@latest"],
|
||||
env: { ...process.env, GOBIN: Global.Path.bin },
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
stdin: "pipe",
|
||||
})
|
||||
const exit = await proc.exited
|
||||
if (exit !== 0) {
|
||||
|
||||
@@ -64,6 +64,7 @@ export namespace Permission {
|
||||
title: Info["title"]
|
||||
metadata: Info["metadata"]
|
||||
}) {
|
||||
return
|
||||
const { pending, approved } = state()
|
||||
log.info("asking", {
|
||||
sessionID: input.sessionID,
|
||||
|
||||
@@ -13,6 +13,7 @@ export namespace ModelsDev {
|
||||
attachment: z.boolean(),
|
||||
reasoning: z.boolean(),
|
||||
temperature: z.boolean(),
|
||||
tool_call: z.boolean(),
|
||||
cost: z.object({
|
||||
input: z.number(),
|
||||
output: z.number(),
|
||||
@@ -24,6 +25,7 @@ export namespace ModelsDev {
|
||||
output: z.number(),
|
||||
}),
|
||||
id: z.string(),
|
||||
options: z.record(z.any()),
|
||||
})
|
||||
.openapi({
|
||||
ref: "Model.Info",
|
||||
|
||||
@@ -19,6 +19,7 @@ import type { Tool } from "../tool/tool"
|
||||
import { WriteTool } from "../tool/write"
|
||||
import { TodoReadTool, TodoWriteTool } from "../tool/todo"
|
||||
import { AuthAnthropic } from "../auth/anthropic"
|
||||
import { AuthGithubCopilot } from "../auth/github-copilot"
|
||||
import { ModelsDev } from "./models"
|
||||
import { NamedError } from "../util/error"
|
||||
import { Auth } from "../auth"
|
||||
@@ -27,9 +28,13 @@ import { TaskTool } from "../tool/task"
|
||||
export namespace Provider {
|
||||
const log = Log.create({ service: "provider" })
|
||||
|
||||
type CustomLoader = (
|
||||
provider: ModelsDev.Provider,
|
||||
) => Promise<Record<string, any> | false>
|
||||
type CustomLoader = (provider: ModelsDev.Provider) => Promise<
|
||||
| {
|
||||
getModel?: (sdk: any, modelID: string) => Promise<any>
|
||||
options: Record<string, any>
|
||||
}
|
||||
| false
|
||||
>
|
||||
|
||||
type Source = "env" | "config" | "custom" | "api"
|
||||
|
||||
@@ -44,30 +49,87 @@ export namespace Provider {
|
||||
}
|
||||
}
|
||||
return {
|
||||
apiKey: "",
|
||||
async fetch(input: any, init: any) {
|
||||
const access = await AuthAnthropic.access()
|
||||
const headers = {
|
||||
...init.headers,
|
||||
authorization: `Bearer ${access}`,
|
||||
"anthropic-beta": "oauth-2025-04-20",
|
||||
}
|
||||
delete headers["x-api-key"]
|
||||
return fetch(input, {
|
||||
...init,
|
||||
headers,
|
||||
})
|
||||
options: {
|
||||
apiKey: "",
|
||||
async fetch(input: any, init: any) {
|
||||
const access = await AuthAnthropic.access()
|
||||
const headers = {
|
||||
...init.headers,
|
||||
authorization: `Bearer ${access}`,
|
||||
"anthropic-beta": "oauth-2025-04-20",
|
||||
}
|
||||
delete headers["x-api-key"]
|
||||
return fetch(input, {
|
||||
...init,
|
||||
headers,
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
"github-copilot": async (provider) => {
|
||||
const info = await AuthGithubCopilot.access()
|
||||
if (!info) return false
|
||||
|
||||
if (provider && provider.models) {
|
||||
for (const model of Object.values(provider.models)) {
|
||||
model.cost = {
|
||||
input: 0,
|
||||
output: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
options: {
|
||||
apiKey: "",
|
||||
async fetch(input: any, init: any) {
|
||||
const token = await AuthGithubCopilot.access()
|
||||
if (!token) throw new Error("GitHub Copilot authentication expired")
|
||||
const headers = {
|
||||
...init.headers,
|
||||
Authorization: `Bearer ${token}`,
|
||||
"User-Agent": "GithubCopilot/1.155.0",
|
||||
"Editor-Version": "vscode/1.85.1",
|
||||
"Editor-Plugin-Version": "copilot/1.155.0",
|
||||
}
|
||||
delete headers["x-api-key"]
|
||||
return fetch(input, {
|
||||
...init,
|
||||
headers,
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
openai: async () => {
|
||||
return {
|
||||
async getModel(sdk: any, modelID: string) {
|
||||
return sdk.responses(modelID)
|
||||
},
|
||||
options: {},
|
||||
}
|
||||
},
|
||||
"amazon-bedrock": async () => {
|
||||
if (!process.env["AWS_PROFILE"]) return false
|
||||
|
||||
const region = process.env["AWS_REGION"] ?? "us-east-1"
|
||||
|
||||
const { fromNodeProviderChain } = await import(
|
||||
await BunProc.install("@aws-sdk/credential-providers")
|
||||
)
|
||||
return {
|
||||
region: process.env["AWS_REGION"] ?? "us-east-1",
|
||||
credentialProvider: fromNodeProviderChain(),
|
||||
options: {
|
||||
region,
|
||||
credentialProvider: fromNodeProviderChain(),
|
||||
},
|
||||
async getModel(sdk: any, modelID: string) {
|
||||
if (modelID.includes("claude")) {
|
||||
const prefix = region.split("-")[0]
|
||||
modelID = `${prefix}.${modelID}`
|
||||
}
|
||||
return sdk.languageModel(modelID)
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -80,6 +142,7 @@ export namespace Provider {
|
||||
[providerID: string]: {
|
||||
source: Source
|
||||
info: ModelsDev.Provider
|
||||
getModel?: (sdk: any, modelID: string) => Promise<any>
|
||||
options: Record<string, any>
|
||||
}
|
||||
} = {}
|
||||
@@ -95,6 +158,7 @@ export namespace Provider {
|
||||
id: string,
|
||||
options: Record<string, any>,
|
||||
source: Source,
|
||||
getModel?: (sdk: any, modelID: string) => Promise<any>,
|
||||
) {
|
||||
const provider = providers[id]
|
||||
if (!provider) {
|
||||
@@ -110,6 +174,7 @@ export namespace Provider {
|
||||
}
|
||||
provider.options = mergeDeep(provider.options, options)
|
||||
provider.source = source
|
||||
provider.getModel = getModel ?? provider.getModel
|
||||
}
|
||||
|
||||
const configProviders = Object.entries(config.provider ?? {})
|
||||
@@ -132,13 +197,19 @@ export namespace Provider {
|
||||
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,
|
||||
},
|
||||
tool_call: model.tool_call ?? existing?.tool_call ?? true,
|
||||
cost: {
|
||||
...existing?.cost,
|
||||
...model.cost,
|
||||
input: 0,
|
||||
output: 0,
|
||||
cache_read: 0,
|
||||
cache_write: 0,
|
||||
},
|
||||
options: {
|
||||
...existing?.options,
|
||||
...model.options,
|
||||
},
|
||||
limit: model.limit ??
|
||||
existing?.limit ?? {
|
||||
context: 0,
|
||||
@@ -173,7 +244,9 @@ export namespace Provider {
|
||||
for (const [providerID, fn] of Object.entries(CUSTOM_LOADERS)) {
|
||||
if (disabled.has(providerID)) continue
|
||||
const result = await fn(database[providerID])
|
||||
if (result) mergeProvider(providerID, result, "custom")
|
||||
if (result) {
|
||||
mergeProvider(providerID, result.options, "custom", result.getModel)
|
||||
}
|
||||
}
|
||||
|
||||
// load config
|
||||
@@ -181,7 +254,11 @@ export namespace Provider {
|
||||
mergeProvider(providerID, provider.options ?? {}, "config")
|
||||
}
|
||||
|
||||
for (const providerID of Object.keys(providers)) {
|
||||
for (const [providerID, provider] of Object.entries(providers)) {
|
||||
if (Object.keys(provider.info.models).length === 0) {
|
||||
delete providers[providerID]
|
||||
continue
|
||||
}
|
||||
log.info("found", { providerID })
|
||||
}
|
||||
|
||||
@@ -232,9 +309,9 @@ export namespace Provider {
|
||||
const sdk = await getSDK(provider.info)
|
||||
|
||||
try {
|
||||
const language =
|
||||
// @ts-expect-error
|
||||
"responses" in sdk ? sdk.responses(modelID) : sdk.languageModel(modelID)
|
||||
const language = provider.getModel
|
||||
? await provider.getModel(sdk, modelID)
|
||||
: sdk.languageModel(modelID)
|
||||
log.info("found", { providerID, modelID })
|
||||
s.models.set(key, {
|
||||
info,
|
||||
@@ -322,6 +399,10 @@ export namespace Provider {
|
||||
...t,
|
||||
parameters: optionalToNullable(t.parameters),
|
||||
})),
|
||||
azure: TOOLS.map((t) => ({
|
||||
...t,
|
||||
parameters: optionalToNullable(t.parameters),
|
||||
})),
|
||||
google: TOOLS,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,24 +1,25 @@
|
||||
import type { CoreMessage } from "ai"
|
||||
import type { LanguageModelV1Prompt } from "ai"
|
||||
import { unique } from "remeda"
|
||||
|
||||
export namespace ProviderTransform {
|
||||
export function message(
|
||||
msg: CoreMessage,
|
||||
index: number,
|
||||
msgs: LanguageModelV1Prompt,
|
||||
providerID: string,
|
||||
modelID: string,
|
||||
) {
|
||||
if (
|
||||
(providerID === "anthropic" || modelID.includes("anthropic")) &&
|
||||
index < 4
|
||||
) {
|
||||
msg.providerOptions = {
|
||||
...msg.providerOptions,
|
||||
anthropic: {
|
||||
cacheControl: { type: "ephemeral" },
|
||||
},
|
||||
if (providerID === "anthropic" || modelID.includes("anthropic")) {
|
||||
const system = msgs.filter((msg) => msg.role === "system").slice(0, 2)
|
||||
const final = msgs.filter((msg) => msg.role !== "system").slice(-2)
|
||||
|
||||
for (const msg of unique([...system, ...final])) {
|
||||
msg.providerMetadata = {
|
||||
...msg.providerMetadata,
|
||||
anthropic: {
|
||||
cacheControl: { type: "ephemeral" },
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return msg
|
||||
return msgs
|
||||
}
|
||||
}
|
||||
|
||||
@@ -290,6 +290,34 @@ export namespace Server {
|
||||
return c.json(session)
|
||||
},
|
||||
)
|
||||
.post(
|
||||
"/session_unshare",
|
||||
describeRoute({
|
||||
description: "Unshare the session",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successfully unshared session",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(Session.Info),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
zValidator(
|
||||
"json",
|
||||
z.object({
|
||||
sessionID: z.string(),
|
||||
}),
|
||||
),
|
||||
async (c) => {
|
||||
const body = c.req.valid("json")
|
||||
await Session.unshare(body.sessionID)
|
||||
const session = await Session.get(body.sessionID)
|
||||
return c.json(session)
|
||||
},
|
||||
)
|
||||
.post(
|
||||
"/session_messages",
|
||||
describeRoute({
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import { App } from "../app/app"
|
||||
import { Filesystem } from "../util/filesystem"
|
||||
|
||||
export namespace SessionContext {
|
||||
const FILES = [
|
||||
"AGENTS.md",
|
||||
"CLAUDE.md",
|
||||
"CONTEXT.md", // deprecated
|
||||
]
|
||||
export async function find() {
|
||||
const { cwd, root } = App.info().path
|
||||
const found = []
|
||||
for (const item of FILES) {
|
||||
const matches = await Filesystem.findUp(item, cwd, root)
|
||||
found.push(...matches.map((x) => Bun.file(x).text()))
|
||||
}
|
||||
return Promise.all(found).then((parts) => parts.join("\n\n"))
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
type CoreMessage,
|
||||
type UIMessage,
|
||||
type ProviderMetadata,
|
||||
wrapLanguageModel,
|
||||
} from "ai"
|
||||
import { z, ZodSchema } from "zod"
|
||||
import { Decimal } from "decimal.js"
|
||||
@@ -159,6 +160,14 @@ export namespace Session {
|
||||
return share
|
||||
}
|
||||
|
||||
export async function unshare(id: string) {
|
||||
await Storage.remove("session/share/" + id)
|
||||
await update(id, (draft) => {
|
||||
draft.share = undefined
|
||||
})
|
||||
await Share.remove(id)
|
||||
}
|
||||
|
||||
export async function update(id: string, editor: (session: Info) => void) {
|
||||
const { sessions } = state()
|
||||
const session = await get(id)
|
||||
@@ -239,7 +248,7 @@ export namespace Session {
|
||||
if (
|
||||
model.info.limit.context &&
|
||||
tokens >
|
||||
(model.info.limit.context - (model.info.limit.output ?? 0)) * 0.9
|
||||
(model.info.limit.context - (model.info.limit.output ?? 0)) * 0.9
|
||||
) {
|
||||
await summarize({
|
||||
sessionID: input.sessionID,
|
||||
@@ -262,6 +271,7 @@ export namespace Session {
|
||||
if (msgs.length === 0 && !session.parentID) {
|
||||
generateText({
|
||||
maxTokens: input.providerID === "google" ? 1024 : 20,
|
||||
providerOptions: model.info.options,
|
||||
messages: [
|
||||
...SystemPrompt.title(input.providerID).map(
|
||||
(x): CoreMessage => ({
|
||||
@@ -276,9 +286,7 @@ export namespace Session {
|
||||
parts: toParts(input.parts),
|
||||
},
|
||||
]),
|
||||
].map((msg, i) =>
|
||||
ProviderTransform.message(msg, i, input.providerID, input.modelID),
|
||||
),
|
||||
],
|
||||
model: model.language,
|
||||
})
|
||||
.then((result) => {
|
||||
@@ -287,7 +295,7 @@ export namespace Session {
|
||||
draft.title = result.text
|
||||
})
|
||||
})
|
||||
.catch(() => {})
|
||||
.catch(() => { })
|
||||
}
|
||||
const msg: Message.Info = {
|
||||
role: "user",
|
||||
@@ -507,6 +515,7 @@ export namespace Session {
|
||||
toolCallStreaming: true,
|
||||
abortSignal: abort.signal,
|
||||
maxSteps: 1000,
|
||||
providerOptions: model.info.options,
|
||||
messages: [
|
||||
...system.map(
|
||||
(x): CoreMessage => ({
|
||||
@@ -517,14 +526,26 @@ export namespace Session {
|
||||
...convertToCoreMessages(
|
||||
msgs.map(toUIMessage).filter((x) => x.parts.length > 0),
|
||||
),
|
||||
].map((msg, i) =>
|
||||
ProviderTransform.message(msg, i, input.providerID, input.modelID),
|
||||
),
|
||||
],
|
||||
temperature: model.info.temperature ? 0 : undefined,
|
||||
tools: {
|
||||
...tools,
|
||||
},
|
||||
model: model.language,
|
||||
tools: model.info.tool_call === false ? undefined : tools,
|
||||
model: wrapLanguageModel({
|
||||
model: model.language,
|
||||
middleware: [
|
||||
{
|
||||
async transformParams(args) {
|
||||
if (args.type === "stream") {
|
||||
args.params.prompt = ProviderTransform.message(
|
||||
args.params.prompt,
|
||||
input.providerID,
|
||||
input.modelID,
|
||||
)
|
||||
}
|
||||
return args.params
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
})
|
||||
try {
|
||||
for await (const value of result.fullStream) {
|
||||
@@ -551,7 +572,7 @@ export namespace Session {
|
||||
case "tool-call": {
|
||||
const [match] = next.parts.flatMap((p) =>
|
||||
p.type === "tool-invocation" &&
|
||||
p.toolInvocation.toolCallId === value.toolCallId
|
||||
p.toolInvocation.toolCallId === value.toolCallId
|
||||
? [p]
|
||||
: [],
|
||||
)
|
||||
@@ -715,7 +736,9 @@ export namespace Session {
|
||||
},
|
||||
}
|
||||
await updateMessage(next)
|
||||
const result = await generateText({
|
||||
|
||||
let text: Message.TextPart | undefined
|
||||
const result = streamText({
|
||||
abortSignal: abort.signal,
|
||||
model: model.language,
|
||||
messages: [
|
||||
@@ -736,16 +759,46 @@ export namespace Session {
|
||||
],
|
||||
},
|
||||
],
|
||||
onStepFinish: async (step) => {
|
||||
const assistant = next.metadata!.assistant!
|
||||
const usage = getUsage(model.info, step.usage, step.providerMetadata)
|
||||
assistant.cost += usage.cost
|
||||
assistant.tokens = usage.tokens
|
||||
await updateMessage(next)
|
||||
if (text) {
|
||||
Bus.publish(Message.Event.PartUpdated, {
|
||||
part: text,
|
||||
messageID: next.id,
|
||||
sessionID: next.metadata.sessionID,
|
||||
})
|
||||
}
|
||||
text = undefined
|
||||
},
|
||||
async onFinish(input) {
|
||||
const assistant = next.metadata!.assistant!
|
||||
const usage = getUsage(model.info, input.usage, input.providerMetadata)
|
||||
assistant.cost = usage.cost
|
||||
assistant.tokens = usage.tokens
|
||||
next.metadata!.time.completed = Date.now()
|
||||
await updateMessage(next)
|
||||
},
|
||||
})
|
||||
next.parts.push({
|
||||
type: "text",
|
||||
text: result.text,
|
||||
})
|
||||
const assistant = next.metadata!.assistant!
|
||||
const usage = getUsage(model.info, result.usage, result.providerMetadata)
|
||||
assistant.cost = usage.cost
|
||||
assistant.tokens = usage.tokens
|
||||
await updateMessage(next)
|
||||
|
||||
for await (const value of result.fullStream) {
|
||||
switch (value.type) {
|
||||
case "text-delta":
|
||||
if (!text) {
|
||||
text = {
|
||||
type: "text",
|
||||
text: value.textDelta,
|
||||
}
|
||||
next.parts.push(text)
|
||||
} else text.text += value.textDelta
|
||||
|
||||
await updateMessage(next)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function lock(sessionID: string) {
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
You will generate a short title based on the first message a user begins a conversation with
|
||||
- ensure it is not more than 50 characters long
|
||||
- the title should be a summary of the user's message
|
||||
- it should be one line long
|
||||
- do not use quotes or colons
|
||||
- the entire text you return will be used as the title
|
||||
- never return anything that is more than one sentence (one line) long
|
||||
Generate a short title based on the first message a user begins a conversation with. CRITICAL: Your response must be EXACTLY one line with NO line breaks, newlines, or multiple sentences.
|
||||
|
||||
Requirements:
|
||||
- Maximum 50 characters
|
||||
- Single line only - NO newlines or line breaks
|
||||
- Summary of the user's message
|
||||
- No quotes, colons, or special formatting
|
||||
- Do not include explanatory text like "summary:" or similar
|
||||
- Your entire response becomes the title
|
||||
|
||||
IMPORTANT: Return only the title text on a single line. Do not add any explanations, formatting, or additional text.
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { App } from "../app/app"
|
||||
import { Ripgrep } from "../external/ripgrep"
|
||||
import { Global } from "../global"
|
||||
import { Filesystem } from "../util/filesystem"
|
||||
import path from "path"
|
||||
import os from "os"
|
||||
|
||||
import PROMPT_ANTHROPIC from "./prompt/anthropic.txt"
|
||||
import PROMPT_ANTHROPIC_SPOOF from "./prompt/anthropic_spoof.txt"
|
||||
@@ -25,7 +28,7 @@ export namespace SystemPrompt {
|
||||
export async function environment() {
|
||||
const app = App.info()
|
||||
|
||||
const tree = async () => {
|
||||
;async () => {
|
||||
const files = await Ripgrep.files({
|
||||
cwd: app.path.cwd,
|
||||
})
|
||||
@@ -82,9 +85,9 @@ export namespace SystemPrompt {
|
||||
` Platform: ${process.platform}`,
|
||||
` Today's date: ${new Date().toDateString()}`,
|
||||
`</env>`,
|
||||
`<project>`,
|
||||
` ${app.git ? await tree() : ""}`,
|
||||
`</project>`,
|
||||
// `<project>`,
|
||||
// ` ${app.git ? await tree() : ""}`,
|
||||
// `</project>`,
|
||||
].join("\n"),
|
||||
]
|
||||
}
|
||||
@@ -101,7 +104,17 @@ export namespace SystemPrompt {
|
||||
const matches = await Filesystem.findUp(item, cwd, root)
|
||||
found.push(...matches.map((x) => Bun.file(x).text()))
|
||||
}
|
||||
return Promise.all(found)
|
||||
found.push(
|
||||
Bun.file(path.join(Global.Path.config, "AGENTS.md"))
|
||||
.text()
|
||||
.catch(() => ""),
|
||||
)
|
||||
found.push(
|
||||
Bun.file(path.join(os.homedir(), ".claude", "CLAUDE.md"))
|
||||
.text()
|
||||
.catch(() => ""),
|
||||
)
|
||||
return Promise.all(found).then((result) => result.filter(Boolean))
|
||||
}
|
||||
|
||||
export function summarize(providerID: string) {
|
||||
|
||||
@@ -70,4 +70,11 @@ export namespace Share {
|
||||
.then((x) => x.json())
|
||||
.then((x) => x as { url: string; secret: string })
|
||||
}
|
||||
|
||||
export async function remove(id: string) {
|
||||
return fetch(`${URL}/share_delete`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ id }),
|
||||
}).then((x) => x.json())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,11 @@ export namespace Storage {
|
||||
}
|
||||
})
|
||||
|
||||
export async function remove(key: string) {
|
||||
const target = path.join(state().dir, key + ".json")
|
||||
await fs.unlink(target).catch(() => {})
|
||||
}
|
||||
|
||||
export async function readJSON<T>(key: string) {
|
||||
return Bun.file(path.join(state().dir, key + ".json")).json() as Promise<T>
|
||||
}
|
||||
|
||||
@@ -63,10 +63,18 @@ export const BashTool = Tool.define({
|
||||
metadata: {
|
||||
stderr,
|
||||
stdout,
|
||||
exit: process.exitCode,
|
||||
description: params.description,
|
||||
title: params.command,
|
||||
},
|
||||
output: stdout.replaceAll(/\x1b\[[0-9;]*m/g, ""),
|
||||
output: [
|
||||
`<stdout>`,
|
||||
stdout ?? "",
|
||||
`</stdout>`,
|
||||
`<stderr>`,
|
||||
stderr ?? "",
|
||||
`</stderr>`,
|
||||
].join("\n"),
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -22,7 +22,7 @@ export const EditTool = Tool.define({
|
||||
replaceAll: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe("Replace all occurences of old_string (default false)"),
|
||||
.describe("Replace all occurrences of old_string (default false)"),
|
||||
}),
|
||||
async execute(params, ctx) {
|
||||
if (!params.filePath) {
|
||||
|
||||
@@ -10,7 +10,7 @@ To make multiple file edits, provide the following:
|
||||
2. edits: An array of edit operations to perform, where each edit contains:
|
||||
- old_string: The text to replace (must match the file contents exactly, including all whitespace and indentation)
|
||||
- new_string: The edited text to replace the old_string
|
||||
- replace_all: Replace all occurences of old_string. This parameter is optional and defaults to false.
|
||||
- replace_all: Replace all occurrences of old_string. This parameter is optional and defaults to false.
|
||||
|
||||
IMPORTANT:
|
||||
- All edits are applied in sequence, in the order they are provided
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { z, type ZodSchema } from "zod"
|
||||
import { Log } from "./log"
|
||||
// import { Log } from "./log"
|
||||
|
||||
const log = Log.create()
|
||||
// const log = Log.create()
|
||||
|
||||
export abstract class NamedError extends Error {
|
||||
abstract schema(): ZodSchema
|
||||
@@ -30,10 +30,6 @@ export abstract class NamedError extends Error {
|
||||
) {
|
||||
super(name, options)
|
||||
this.name = name
|
||||
log.error(name, {
|
||||
...this.data,
|
||||
cause: options?.cause?.toString(),
|
||||
})
|
||||
}
|
||||
|
||||
static isInstance(input: any): input is InstanceType<typeof result> {
|
||||
|
||||
@@ -68,13 +68,13 @@ export namespace Log {
|
||||
}
|
||||
const result = {
|
||||
info(message?: any, extra?: Record<string, any>) {
|
||||
process.stderr.write(build(message, extra))
|
||||
process.stderr.write("INFO " + build(message, extra))
|
||||
},
|
||||
error(message?: any, extra?: Record<string, any>) {
|
||||
process.stderr.write(build(message, extra))
|
||||
process.stderr.write("ERROR " + build(message, extra))
|
||||
},
|
||||
warn(message?: any, extra?: Record<string, any>) {
|
||||
process.stderr.write(build(message, extra))
|
||||
process.stderr.write("WARN " + build(message, extra))
|
||||
},
|
||||
tag(key: string, value: string) {
|
||||
if (tags) tags[key] = value
|
||||
|
||||
@@ -14,7 +14,7 @@ describe("tool.glob", () => {
|
||||
await App.provide({ cwd: process.cwd() }, async () => {
|
||||
let result = await GlobTool.execute(
|
||||
{
|
||||
pattern: "./node_modules/**/*",
|
||||
pattern: "../../node_modules/**/*",
|
||||
path: undefined,
|
||||
},
|
||||
ctx,
|
||||
@@ -33,7 +33,7 @@ describe("tool.glob", () => {
|
||||
)
|
||||
expect(result.metadata).toMatchObject({
|
||||
truncated: false,
|
||||
count: 2,
|
||||
count: 3,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -26,7 +26,11 @@ func main() {
|
||||
|
||||
appInfoStr := os.Getenv("OPENCODE_APP_INFO")
|
||||
var appInfo client.AppInfo
|
||||
json.Unmarshal([]byte(appInfoStr), &appInfo)
|
||||
err := json.Unmarshal([]byte(appInfoStr), &appInfo)
|
||||
if err != nil {
|
||||
slog.Error("Failed to unmarshal app info", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
logfile := filepath.Join(appInfo.Path.Data, "log", "tui.log")
|
||||
if _, err := os.Stat(filepath.Dir(logfile)); os.IsNotExist(err) {
|
||||
|
||||
@@ -23,7 +23,7 @@ type App struct {
|
||||
Info client.AppInfo
|
||||
Version string
|
||||
StatePath string
|
||||
Configg *client.ConfigInfo
|
||||
Config *client.ConfigInfo
|
||||
Client *client.ClientWithResponses
|
||||
State *config.State
|
||||
Provider *client.ProviderInfo
|
||||
@@ -44,7 +44,7 @@ type SendMsg struct {
|
||||
Text string
|
||||
Attachments []Attachment
|
||||
}
|
||||
type CompletionDialogTriggerdMsg struct {
|
||||
type CompletionDialogTriggeredMsg struct {
|
||||
InitialValue string
|
||||
}
|
||||
|
||||
@@ -60,6 +60,9 @@ func New(
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if configResponse.StatusCode() != 200 || configResponse.JSON200 == nil {
|
||||
return nil, fmt.Errorf("failed to get config: %d", configResponse.StatusCode())
|
||||
}
|
||||
configInfo := configResponse.JSON200
|
||||
if configInfo.Keybinds == nil {
|
||||
leader := "ctrl+x"
|
||||
@@ -85,6 +88,15 @@ func New(
|
||||
appState.Model = strings.Join(splits[1:], "/")
|
||||
}
|
||||
|
||||
// Load themes from all directories
|
||||
if err := theme.LoadThemesFromDirectories(
|
||||
appInfo.Path.Config,
|
||||
appInfo.Path.Root,
|
||||
appInfo.Path.Cwd,
|
||||
); err != nil {
|
||||
slog.Warn("Failed to load themes from directories", "error", err)
|
||||
}
|
||||
|
||||
if appState.Theme != "" {
|
||||
theme.SetTheme(appState.Theme)
|
||||
}
|
||||
@@ -95,7 +107,7 @@ func New(
|
||||
Info: appInfo,
|
||||
Version: version,
|
||||
StatePath: appStatePath,
|
||||
Configg: configInfo,
|
||||
Config: configInfo,
|
||||
State: appState,
|
||||
Client: httpClient,
|
||||
Session: &client.SessionInfo{},
|
||||
@@ -114,6 +126,10 @@ func (a *App) InitializeProvider() tea.Cmd {
|
||||
// TODO: notify user
|
||||
return nil
|
||||
}
|
||||
if providersResponse != nil && providersResponse.StatusCode() != 200 {
|
||||
slog.Error("failed to retrieve providers", "status", providersResponse.StatusCode(), "message", string(providersResponse.Body))
|
||||
return nil
|
||||
}
|
||||
providers := []client.ProviderInfo{}
|
||||
var defaultProvider *client.ProviderInfo
|
||||
var defaultModel *client.ModelInfo
|
||||
@@ -236,17 +252,19 @@ func (a *App) InitializeProject(ctx context.Context) tea.Cmd {
|
||||
}
|
||||
|
||||
func (a *App) CompactSession(ctx context.Context) tea.Cmd {
|
||||
response, err := a.Client.PostSessionSummarizeWithResponse(ctx, client.PostSessionSummarizeJSONRequestBody{
|
||||
SessionID: a.Session.Id,
|
||||
ProviderID: a.Provider.Id,
|
||||
ModelID: a.Model.Id,
|
||||
})
|
||||
if err != nil {
|
||||
slog.Error("Failed to compact session", "error", err)
|
||||
}
|
||||
if response != nil && response.StatusCode() != 200 {
|
||||
slog.Error("Failed to compact session", "error", response.StatusCode)
|
||||
}
|
||||
go func() {
|
||||
response, err := a.Client.PostSessionSummarizeWithResponse(ctx, client.PostSessionSummarizeJSONRequestBody{
|
||||
SessionID: a.Session.Id,
|
||||
ProviderID: a.Provider.Id,
|
||||
ModelID: a.Model.Id,
|
||||
})
|
||||
if err != nil {
|
||||
slog.Error("Failed to compact session", "error", err)
|
||||
}
|
||||
if response != nil && response.StatusCode() != 200 {
|
||||
slog.Error("Failed to compact session", "error", response.StatusCode)
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -186,7 +186,7 @@ func LoadFromConfig(config *client.ConfigInfo) CommandRegistry {
|
||||
},
|
||||
{
|
||||
Name: ProjectInitCommand,
|
||||
Description: "create or update AGENTS.md",
|
||||
Description: "create/update AGENTS.md",
|
||||
Keybindings: parseBindings("<leader>i"),
|
||||
Trigger: "init",
|
||||
},
|
||||
|
||||
@@ -35,8 +35,7 @@ func (c *CommandCompletionProvider) GetEmptyMessage() string {
|
||||
return "no matching commands"
|
||||
}
|
||||
|
||||
func getCommandCompletionItem(cmd commands.Command, space int) dialog.CompletionItemI {
|
||||
t := theme.CurrentTheme()
|
||||
func getCommandCompletionItem(cmd commands.Command, space int, t theme.Theme) dialog.CompletionItemI {
|
||||
spacer := strings.Repeat(" ", space)
|
||||
title := " /" + cmd.Trigger + lipgloss.NewStyle().Foreground(t.TextMuted()).Render(spacer+cmd.Description)
|
||||
value := string(cmd.Name)
|
||||
@@ -47,6 +46,9 @@ func getCommandCompletionItem(cmd commands.Command, space int) dialog.Completion
|
||||
}
|
||||
|
||||
func (c *CommandCompletionProvider) GetChildEntries(query string) ([]dialog.CompletionItemI, error) {
|
||||
t := theme.CurrentTheme()
|
||||
commands := c.app.Commands
|
||||
|
||||
space := 1
|
||||
for _, cmd := range c.app.Commands {
|
||||
if lipgloss.Width(cmd.Trigger) > space {
|
||||
@@ -55,15 +57,16 @@ func (c *CommandCompletionProvider) GetChildEntries(query string) ([]dialog.Comp
|
||||
}
|
||||
space += 2
|
||||
|
||||
sorted := commands.Sorted()
|
||||
if query == "" {
|
||||
// If no query, return all commands
|
||||
items := []dialog.CompletionItemI{}
|
||||
for _, cmd := range c.app.Commands {
|
||||
for _, cmd := range sorted {
|
||||
if cmd.Trigger == "" {
|
||||
continue
|
||||
}
|
||||
space := space - lipgloss.Width(cmd.Trigger)
|
||||
items = append(items, getCommandCompletionItem(cmd, space))
|
||||
items = append(items, getCommandCompletionItem(cmd, space, t))
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
@@ -72,13 +75,13 @@ func (c *CommandCompletionProvider) GetChildEntries(query string) ([]dialog.Comp
|
||||
var commandNames []string
|
||||
commandMap := make(map[string]dialog.CompletionItemI)
|
||||
|
||||
for _, cmd := range c.app.Commands {
|
||||
for _, cmd := range sorted {
|
||||
if cmd.Trigger == "" {
|
||||
continue
|
||||
}
|
||||
space := space - lipgloss.Width(cmd.Trigger)
|
||||
commandNames = append(commandNames, cmd.Trigger)
|
||||
commandMap[cmd.Trigger] = getCommandCompletionItem(cmd, space)
|
||||
commandMap[cmd.Trigger] = getCommandCompletionItem(cmd, space, t)
|
||||
}
|
||||
|
||||
// Find fuzzy matches
|
||||
|
||||
@@ -54,6 +54,9 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
var cmd tea.Cmd
|
||||
|
||||
switch msg := msg.(type) {
|
||||
case spinner.TickMsg:
|
||||
m.spinner, cmd = m.spinner.Update(msg)
|
||||
return m, cmd
|
||||
case tea.KeyPressMsg:
|
||||
// Maximize editor responsiveness for printable characters
|
||||
if msg.Text != "" {
|
||||
@@ -214,8 +217,7 @@ func (m *editorComponent) Paste() (tea.Model, tea.Cmd) {
|
||||
}
|
||||
|
||||
func (m *editorComponent) Newline() (tea.Model, tea.Cmd) {
|
||||
value := m.textarea.Value()
|
||||
m.textarea.SetValue(value + "\n")
|
||||
m.textarea.Newline()
|
||||
return m, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,8 @@ import (
|
||||
func toMarkdown(content string, width int, backgroundColor compat.AdaptiveColor) string {
|
||||
r := styles.GetMarkdownRenderer(width, backgroundColor)
|
||||
content = strings.ReplaceAll(content, app.RootPath+"/", "")
|
||||
content = strings.ReplaceAll(content, "<", "\\<")
|
||||
content = strings.ReplaceAll(content, ">", "\\>")
|
||||
rendered, _ := r.Render(content)
|
||||
lines := strings.Split(rendered, "\n")
|
||||
|
||||
@@ -44,7 +46,6 @@ func toMarkdown(content string, width int, backgroundColor compat.AdaptiveColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
content = strings.Join(lines, "\n")
|
||||
return strings.TrimSuffix(content, "\n")
|
||||
}
|
||||
@@ -137,7 +138,7 @@ func renderContentBlock(content string, options ...renderingOption) string {
|
||||
PaddingBottom(renderer.paddingBottom).
|
||||
PaddingLeft(renderer.paddingLeft).
|
||||
PaddingRight(renderer.paddingRight).
|
||||
Background(t.BackgroundSubtle()).
|
||||
Background(t.BackgroundPanel()).
|
||||
Foreground(t.TextMuted()).
|
||||
BorderStyle(lipgloss.ThickBorder())
|
||||
|
||||
@@ -146,7 +147,7 @@ func renderContentBlock(content string, options ...renderingOption) string {
|
||||
align = *renderer.align
|
||||
}
|
||||
|
||||
borderColor := t.BackgroundSubtle()
|
||||
borderColor := t.BackgroundPanel()
|
||||
if renderer.borderColor != nil {
|
||||
borderColor = *renderer.borderColor
|
||||
}
|
||||
@@ -159,7 +160,7 @@ func renderContentBlock(content string, options ...renderingOption) string {
|
||||
AlignHorizontal(align).
|
||||
BorderLeftForeground(borderColor).
|
||||
BorderLeftBackground(t.Background()).
|
||||
BorderRightForeground(t.BackgroundSubtle()).
|
||||
BorderRightForeground(t.BackgroundPanel()).
|
||||
BorderRightBackground(t.Background())
|
||||
case lipgloss.Right:
|
||||
style = style.
|
||||
@@ -168,7 +169,7 @@ func renderContentBlock(content string, options ...renderingOption) string {
|
||||
AlignHorizontal(align).
|
||||
BorderRightForeground(borderColor).
|
||||
BorderRightBackground(t.Background()).
|
||||
BorderLeftForeground(t.BackgroundSubtle()).
|
||||
BorderLeftForeground(t.BackgroundPanel()).
|
||||
BorderLeftBackground(t.Background())
|
||||
}
|
||||
|
||||
@@ -229,7 +230,7 @@ func renderText(message client.MessageInfo, text string, author string) string {
|
||||
if message.Role == client.Assistant {
|
||||
markdownWidth = width - padding - 4
|
||||
}
|
||||
content := toMarkdown(text, markdownWidth, t.BackgroundSubtle())
|
||||
content := toMarkdown(text, markdownWidth, t.BackgroundPanel())
|
||||
content = strings.Join([]string{content, info}, "\n")
|
||||
|
||||
switch message.Role {
|
||||
@@ -274,7 +275,7 @@ func renderToolInvocation(
|
||||
t := theme.CurrentTheme()
|
||||
style := styles.Muted().
|
||||
Width(outerWidth).
|
||||
Background(t.BackgroundSubtle()).
|
||||
Background(t.BackgroundPanel()).
|
||||
PaddingTop(paddingTop).
|
||||
PaddingBottom(paddingBottom).
|
||||
PaddingLeft(2).
|
||||
@@ -282,7 +283,7 @@ func renderToolInvocation(
|
||||
BorderLeft(true).
|
||||
BorderRight(true).
|
||||
BorderBackground(t.Background()).
|
||||
BorderForeground(t.BackgroundSubtle()).
|
||||
BorderForeground(t.BackgroundPanel()).
|
||||
BorderStyle(lipgloss.ThickBorder())
|
||||
|
||||
if toolCall.State == "partial-call" {
|
||||
@@ -290,7 +291,7 @@ func renderToolInvocation(
|
||||
if !showDetails {
|
||||
title = "∟ " + title
|
||||
padding := calculatePadding()
|
||||
style := lipgloss.NewStyle().Width(outerWidth - padding - 4).Background(t.BackgroundSubtle())
|
||||
style := lipgloss.NewStyle().Width(outerWidth - padding - 4).Background(t.BackgroundPanel())
|
||||
return renderContentBlock(style.Render(title),
|
||||
WithAlign(lipgloss.Left),
|
||||
WithBorderColor(t.Accent()),
|
||||
@@ -332,7 +333,7 @@ func renderToolInvocation(
|
||||
if m, ok := metadata.Get("message"); ok {
|
||||
style = style.BorderLeftForeground(t.Error())
|
||||
error = styles.BaseStyle().
|
||||
Background(t.BackgroundSubtle()).
|
||||
Background(t.BackgroundPanel()).
|
||||
Foreground(t.Error()).
|
||||
Render(m.(string))
|
||||
error = renderContentBlock(
|
||||
@@ -344,22 +345,11 @@ func renderToolInvocation(
|
||||
}
|
||||
}
|
||||
|
||||
elapsed := ""
|
||||
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 "read":
|
||||
toolArgs = renderArgs(&toolArgsMap, "filePath")
|
||||
title = fmt.Sprintf("READ %s %s", toolArgs, elapsed)
|
||||
title = fmt.Sprintf("READ %s", toolArgs)
|
||||
if preview, ok := metadata.Get("preview"); ok && toolArgsMap["filePath"] != nil {
|
||||
filename := toolArgsMap["filePath"].(string)
|
||||
body = preview.(string)
|
||||
@@ -367,7 +357,7 @@ func renderToolInvocation(
|
||||
}
|
||||
case "edit":
|
||||
if filename, ok := toolArgsMap["filePath"].(string); ok {
|
||||
title = fmt.Sprintf("EDIT %s %s", relative(filename), elapsed)
|
||||
title = fmt.Sprintf("EDIT %s", relative(filename))
|
||||
if d, ok := metadata.Get("diff"); ok {
|
||||
patch := d.(string)
|
||||
var formattedDiff string
|
||||
@@ -385,7 +375,7 @@ func renderToolInvocation(
|
||||
formattedDiff = lipgloss.NewStyle().
|
||||
BorderStyle(lipgloss.ThickBorder()).
|
||||
BorderBackground(t.Background()).
|
||||
BorderForeground(t.BackgroundSubtle()).
|
||||
BorderForeground(t.BackgroundPanel()).
|
||||
BorderLeft(true).
|
||||
BorderRight(true).
|
||||
Render(formattedDiff)
|
||||
@@ -408,35 +398,37 @@ func renderToolInvocation(
|
||||
}
|
||||
case "write":
|
||||
if filename, ok := toolArgsMap["filePath"].(string); ok {
|
||||
title = fmt.Sprintf("WRITE %s %s", relative(filename), elapsed)
|
||||
title = fmt.Sprintf("WRITE %s", relative(filename))
|
||||
if content, ok := toolArgsMap["content"].(string); ok {
|
||||
body = renderFile(filename, content)
|
||||
}
|
||||
}
|
||||
case "bash":
|
||||
if description, ok := toolArgsMap["description"].(string); ok {
|
||||
title = fmt.Sprintf("SHELL %s %s", description, elapsed)
|
||||
title = fmt.Sprintf("SHELL %s", description)
|
||||
}
|
||||
if stdout, ok := metadata.Get("stdout"); ok {
|
||||
command := toolArgsMap["command"].(string)
|
||||
stdout := stdout.(string)
|
||||
body = fmt.Sprintf("```console\n> %s\n%s```", command, stdout)
|
||||
body = toMarkdown(body, innerWidth, t.BackgroundSubtle())
|
||||
body = toMarkdown(body, innerWidth, t.BackgroundPanel())
|
||||
body = renderContentBlock(body, WithFullWidth(), WithMarginBottom(1))
|
||||
}
|
||||
case "webfetch":
|
||||
toolArgs = renderArgs(&toolArgsMap, "url")
|
||||
title = fmt.Sprintf("FETCH %s %s", toolArgs, elapsed)
|
||||
title = fmt.Sprintf("FETCH %s", toolArgs)
|
||||
if format, ok := toolArgsMap["format"].(string); ok {
|
||||
body = *result
|
||||
body = truncateHeight(body, 10)
|
||||
if format == "html" || format == "markdown" {
|
||||
body = toMarkdown(body, innerWidth, t.BackgroundSubtle())
|
||||
if result != nil {
|
||||
body = *result
|
||||
body = truncateHeight(body, 10)
|
||||
if format == "html" || format == "markdown" {
|
||||
body = toMarkdown(body, innerWidth, t.BackgroundPanel())
|
||||
}
|
||||
body = renderContentBlock(body, WithFullWidth(), WithMarginBottom(1))
|
||||
}
|
||||
body = renderContentBlock(body, WithFullWidth(), WithMarginBottom(1))
|
||||
}
|
||||
case "todowrite":
|
||||
title = fmt.Sprintf("PLAN %s", elapsed)
|
||||
title = fmt.Sprintf("PLAN")
|
||||
|
||||
if to, ok := metadata.Get("todos"); ok && finished {
|
||||
todos := to.([]any)
|
||||
@@ -452,12 +444,12 @@ func renderToolInvocation(
|
||||
body += fmt.Sprintf("- [ ] %s\n", content)
|
||||
}
|
||||
}
|
||||
body = toMarkdown(body, innerWidth, t.BackgroundSubtle())
|
||||
body = toMarkdown(body, innerWidth, t.BackgroundPanel())
|
||||
body = renderContentBlock(body, WithFullWidth(), WithMarginBottom(1))
|
||||
}
|
||||
case "task":
|
||||
if description, ok := toolArgsMap["description"].(string); ok {
|
||||
title = fmt.Sprintf("TASK %s %s", description, elapsed)
|
||||
title = fmt.Sprintf("TASK %s", description)
|
||||
if summary, ok := metadata.Get("summary"); ok {
|
||||
toolcalls := summary.([]any)
|
||||
// toolcalls :=
|
||||
@@ -494,7 +486,7 @@ func renderToolInvocation(
|
||||
|
||||
default:
|
||||
toolName := renderToolName(toolCall.ToolName)
|
||||
title = fmt.Sprintf("%s %s %s", toolName, toolArgs, elapsed)
|
||||
title = fmt.Sprintf("%s %s", toolName, toolArgs)
|
||||
if result == nil {
|
||||
empty := ""
|
||||
result = &empty
|
||||
@@ -512,7 +504,7 @@ func renderToolInvocation(
|
||||
if !showDetails {
|
||||
title = "∟ " + title
|
||||
padding := calculatePadding()
|
||||
style := lipgloss.NewStyle().Width(outerWidth - padding - 4).Background(t.BackgroundSubtle())
|
||||
style := lipgloss.NewStyle().Width(outerWidth - padding - 4).Background(t.BackgroundPanel())
|
||||
paddingBottom := 0
|
||||
if isLast {
|
||||
paddingBottom = 1
|
||||
@@ -601,7 +593,7 @@ func renderFile(filename string, content string, options ...fileRenderingOption)
|
||||
content = truncateHeight(content, renderer.height)
|
||||
}
|
||||
content = fmt.Sprintf("```%s\n%s\n```", extension(renderer.filename), content)
|
||||
content = toMarkdown(content, width, t.BackgroundSubtle())
|
||||
content = toMarkdown(content, width, t.BackgroundPanel())
|
||||
|
||||
return renderContentBlock(content, WithFullWidth(), WithMarginBottom(1))
|
||||
}
|
||||
@@ -615,7 +607,7 @@ func renderToolAction(name string) string {
|
||||
case "edit":
|
||||
return "Preparing edit..."
|
||||
case "webfetch":
|
||||
return "Writing fetch..."
|
||||
return "Fetching from the web..."
|
||||
case "glob":
|
||||
return "Finding files..."
|
||||
case "grep":
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
tea "github.com/charmbracelet/bubbletea/v2"
|
||||
"github.com/charmbracelet/lipgloss/v2"
|
||||
"github.com/sst/opencode/internal/app"
|
||||
"github.com/sst/opencode/internal/components/commands"
|
||||
"github.com/sst/opencode/internal/components/dialog"
|
||||
"github.com/sst/opencode/internal/layout"
|
||||
"github.com/sst/opencode/internal/styles"
|
||||
@@ -37,6 +38,7 @@ type messagesComponent struct {
|
||||
viewport viewport.Model
|
||||
spinner spinner.Model
|
||||
attachments viewport.Model
|
||||
commands commands.CommandsComponent
|
||||
cache *MessageCache
|
||||
rendering bool
|
||||
showToolDetails bool
|
||||
@@ -46,7 +48,7 @@ type renderFinishedMsg struct{}
|
||||
type ToggleToolDetailsMsg struct{}
|
||||
|
||||
func (m *messagesComponent) Init() tea.Cmd {
|
||||
return tea.Batch(m.viewport.Init(), m.spinner.Tick)
|
||||
return tea.Batch(m.viewport.Init(), m.spinner.Tick, m.commands.Init())
|
||||
}
|
||||
|
||||
func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
@@ -58,17 +60,14 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m, nil
|
||||
case dialog.ThemeSelectedMsg:
|
||||
m.cache.Clear()
|
||||
m.renderView()
|
||||
return m, nil
|
||||
return m, m.Reload()
|
||||
case ToggleToolDetailsMsg:
|
||||
m.showToolDetails = !m.showToolDetails
|
||||
m.renderView()
|
||||
return m, nil
|
||||
return m, m.Reload()
|
||||
case app.SessionSelectedMsg:
|
||||
m.cache.Clear()
|
||||
cmd := m.Reload()
|
||||
m.viewport.GotoBottom()
|
||||
return m, cmd
|
||||
m.tail = true
|
||||
return m, m.Reload()
|
||||
case app.SessionClearedMsg:
|
||||
m.cache.Clear()
|
||||
cmd := m.Reload()
|
||||
@@ -93,6 +92,11 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
spinner, cmd := m.spinner.Update(msg)
|
||||
m.spinner = spinner
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
updated, cmd := m.commands.Update(msg)
|
||||
m.commands = updated.(commands.CommandsComponent)
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
return m, tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
@@ -281,7 +285,13 @@ func (m *messagesComponent) View() string {
|
||||
return m.home()
|
||||
}
|
||||
if m.rendering {
|
||||
return m.viewport.View()
|
||||
return lipgloss.Place(
|
||||
m.width,
|
||||
m.height,
|
||||
lipgloss.Center,
|
||||
lipgloss.Center,
|
||||
"Loading session...",
|
||||
)
|
||||
}
|
||||
t := theme.CurrentTheme()
|
||||
return lipgloss.JoinVertical(
|
||||
@@ -319,50 +329,43 @@ func (m *messagesComponent) home() string {
|
||||
// cwd := app.Info.Path.Cwd
|
||||
// config := app.Info.Path.Config
|
||||
|
||||
commands := [][]string{
|
||||
{"/help", "show help"},
|
||||
{"/sessions", "list sessions"},
|
||||
{"/new", "start a new session"},
|
||||
{"/model", "switch model"},
|
||||
{"/theme", "switch theme"},
|
||||
{"/exit", "exit the app"},
|
||||
}
|
||||
versionStyle := lipgloss.NewStyle().
|
||||
Background(t.Background()).
|
||||
Foreground(t.TextMuted()).
|
||||
Width(lipgloss.Width(logo)).
|
||||
Align(lipgloss.Right)
|
||||
version := versionStyle.Render(m.app.Version)
|
||||
|
||||
commandLines := []string{}
|
||||
for _, command := range commands {
|
||||
commandLines = append(commandLines, (base(command[0]+" ") + muted(command[1])))
|
||||
}
|
||||
|
||||
logoAndVersion := lipgloss.JoinVertical(
|
||||
lipgloss.Right,
|
||||
logo,
|
||||
muted(m.app.Version),
|
||||
logoAndVersion := strings.Join([]string{logo, version}, "\n")
|
||||
logoAndVersion = lipgloss.PlaceHorizontal(
|
||||
m.width,
|
||||
lipgloss.Center,
|
||||
logoAndVersion,
|
||||
lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
|
||||
)
|
||||
m.commands.SetBackgroundColor(t.Background())
|
||||
commands := lipgloss.PlaceHorizontal(
|
||||
m.width,
|
||||
lipgloss.Center,
|
||||
m.commands.View(),
|
||||
lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
|
||||
)
|
||||
|
||||
lines := []string{}
|
||||
lines = append(lines, "")
|
||||
lines = append(lines, "")
|
||||
lines = append(lines, logoAndVersion)
|
||||
lines = append(lines, "")
|
||||
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 {
|
||||
lines = append(lines, base("Loading session..."))
|
||||
} else {
|
||||
lines = append(lines, "")
|
||||
}
|
||||
lines = append(lines, commands)
|
||||
|
||||
return lipgloss.Place(
|
||||
m.width,
|
||||
m.height,
|
||||
lipgloss.Center,
|
||||
lipgloss.Center,
|
||||
baseStyle.Width(lipgloss.Width(logoAndVersion)).Render(
|
||||
strings.Join(lines, "\n"),
|
||||
),
|
||||
baseStyle.Render(strings.Join(lines, "\n")),
|
||||
lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
|
||||
)
|
||||
}
|
||||
@@ -381,6 +384,7 @@ func (m *messagesComponent) SetSize(width, height int) tea.Cmd {
|
||||
m.viewport.SetHeight(height - lipgloss.Height(m.header()))
|
||||
m.attachments.SetWidth(width + 40)
|
||||
m.attachments.SetHeight(3)
|
||||
m.commands.SetSize(width, height)
|
||||
m.renderView()
|
||||
return nil
|
||||
}
|
||||
@@ -444,11 +448,19 @@ func NewMessagesComponent(app *app.App) MessagesComponent {
|
||||
attachments := viewport.New()
|
||||
vp.KeyMap = viewport.KeyMap{}
|
||||
|
||||
t := theme.CurrentTheme()
|
||||
commandsView := commands.New(
|
||||
app,
|
||||
commands.WithBackground(t.Background()),
|
||||
commands.WithLimit(6),
|
||||
)
|
||||
|
||||
return &messagesComponent{
|
||||
app: app,
|
||||
viewport: vp,
|
||||
spinner: s,
|
||||
attachments: attachments,
|
||||
commands: commandsView,
|
||||
showToolDetails: true,
|
||||
cache: NewMessageCache(),
|
||||
tail: true,
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea/v2"
|
||||
"github.com/charmbracelet/lipgloss/v2"
|
||||
"github.com/charmbracelet/lipgloss/v2/compat"
|
||||
"github.com/sst/opencode/internal/app"
|
||||
"github.com/sst/opencode/internal/commands"
|
||||
"github.com/sst/opencode/internal/layout"
|
||||
"github.com/sst/opencode/internal/styles"
|
||||
"github.com/sst/opencode/internal/theme"
|
||||
)
|
||||
|
||||
type CommandsComponent interface {
|
||||
tea.Model
|
||||
tea.ViewModel
|
||||
layout.Sizeable
|
||||
SetBackgroundColor(color compat.AdaptiveColor)
|
||||
}
|
||||
|
||||
type commandsComponent struct {
|
||||
app *app.App
|
||||
width, height int
|
||||
showKeybinds bool
|
||||
showAll bool
|
||||
background *compat.AdaptiveColor
|
||||
limit *int
|
||||
}
|
||||
|
||||
func (c *commandsComponent) SetSize(width, height int) tea.Cmd {
|
||||
c.width = width
|
||||
c.height = height
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *commandsComponent) GetSize() (int, int) {
|
||||
return c.width, c.height
|
||||
}
|
||||
|
||||
func (c *commandsComponent) SetBackgroundColor(color compat.AdaptiveColor) {
|
||||
c.background = &color
|
||||
}
|
||||
|
||||
func (c *commandsComponent) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *commandsComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.WindowSizeMsg:
|
||||
c.width = msg.Width
|
||||
c.height = msg.Height
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (c *commandsComponent) View() string {
|
||||
t := theme.CurrentTheme()
|
||||
|
||||
triggerStyle := lipgloss.NewStyle().
|
||||
Foreground(t.Primary()).
|
||||
Bold(true)
|
||||
|
||||
descriptionStyle := lipgloss.NewStyle().
|
||||
Foreground(t.Text())
|
||||
|
||||
keybindStyle := lipgloss.NewStyle().
|
||||
Foreground(t.TextMuted())
|
||||
|
||||
if c.background != nil {
|
||||
triggerStyle = triggerStyle.Background(*c.background)
|
||||
descriptionStyle = descriptionStyle.Background(*c.background)
|
||||
keybindStyle = keybindStyle.Background(*c.background)
|
||||
}
|
||||
|
||||
var commandsToShow []commands.Command
|
||||
var triggeredCommands []commands.Command
|
||||
var untriggeredCommands []commands.Command
|
||||
|
||||
for _, cmd := range c.app.Commands.Sorted() {
|
||||
if c.showAll || cmd.Trigger != "" {
|
||||
if cmd.Trigger != "" {
|
||||
triggeredCommands = append(triggeredCommands, cmd)
|
||||
} else if c.showAll {
|
||||
untriggeredCommands = append(untriggeredCommands, cmd)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Combine triggered commands first, then untriggered
|
||||
commandsToShow = append(commandsToShow, triggeredCommands...)
|
||||
commandsToShow = append(commandsToShow, untriggeredCommands...)
|
||||
|
||||
if c.limit != nil && len(commandsToShow) > *c.limit {
|
||||
commandsToShow = commandsToShow[:*c.limit]
|
||||
}
|
||||
|
||||
if len(commandsToShow) == 0 {
|
||||
if c.showAll {
|
||||
return styles.Muted().Render("No commands available")
|
||||
}
|
||||
return styles.Muted().Render("No commands with triggers available")
|
||||
}
|
||||
|
||||
// Calculate column widths
|
||||
maxTriggerWidth := 0
|
||||
maxDescriptionWidth := 0
|
||||
maxKeybindWidth := 0
|
||||
|
||||
// Prepare command data
|
||||
type commandRow struct {
|
||||
trigger string
|
||||
description string
|
||||
keybinds string
|
||||
}
|
||||
|
||||
rows := make([]commandRow, 0, len(commandsToShow))
|
||||
|
||||
for _, cmd := range commandsToShow {
|
||||
trigger := ""
|
||||
if cmd.Trigger != "" {
|
||||
trigger = "/" + cmd.Trigger
|
||||
} else {
|
||||
trigger = string(cmd.Name)
|
||||
}
|
||||
description := cmd.Description
|
||||
|
||||
// Format keybindings
|
||||
var keybindStrs []string
|
||||
if c.showKeybinds {
|
||||
for _, kb := range cmd.Keybindings {
|
||||
if kb.RequiresLeader {
|
||||
keybindStrs = append(keybindStrs, *c.app.Config.Keybinds.Leader+" "+kb.Key)
|
||||
} else {
|
||||
keybindStrs = append(keybindStrs, kb.Key)
|
||||
}
|
||||
}
|
||||
}
|
||||
keybinds := strings.Join(keybindStrs, ", ")
|
||||
|
||||
rows = append(rows, commandRow{
|
||||
trigger: trigger,
|
||||
description: description,
|
||||
keybinds: keybinds,
|
||||
})
|
||||
|
||||
// Update max widths
|
||||
if len(trigger) > maxTriggerWidth {
|
||||
maxTriggerWidth = len(trigger)
|
||||
}
|
||||
if len(description) > maxDescriptionWidth {
|
||||
maxDescriptionWidth = len(description)
|
||||
}
|
||||
if len(keybinds) > maxKeybindWidth {
|
||||
maxKeybindWidth = len(keybinds)
|
||||
}
|
||||
}
|
||||
|
||||
// Add padding between columns
|
||||
columnPadding := 3
|
||||
|
||||
// Build the output
|
||||
var output strings.Builder
|
||||
|
||||
maxWidth := 0
|
||||
for _, row := range rows {
|
||||
// Pad each column to align properly
|
||||
trigger := fmt.Sprintf("%-*s", maxTriggerWidth, row.trigger)
|
||||
description := fmt.Sprintf("%-*s", maxDescriptionWidth, row.description)
|
||||
|
||||
// Apply styles and combine
|
||||
line := triggerStyle.Render(trigger) +
|
||||
triggerStyle.Render(strings.Repeat(" ", columnPadding)) +
|
||||
descriptionStyle.Render(description)
|
||||
|
||||
if c.showKeybinds && row.keybinds != "" {
|
||||
line += keybindStyle.Render(strings.Repeat(" ", columnPadding)) +
|
||||
keybindStyle.Render(row.keybinds)
|
||||
}
|
||||
|
||||
output.WriteString(line + "\n")
|
||||
maxWidth = max(maxWidth, lipgloss.Width(line))
|
||||
}
|
||||
|
||||
// Remove trailing newline
|
||||
result := strings.TrimSuffix(output.String(), "\n")
|
||||
if c.background != nil {
|
||||
result = lipgloss.NewStyle().Background(c.background).Width(maxWidth).Render(result)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
type Option func(*commandsComponent)
|
||||
|
||||
func WithKeybinds(show bool) Option {
|
||||
return func(c *commandsComponent) {
|
||||
c.showKeybinds = show
|
||||
}
|
||||
}
|
||||
|
||||
func WithBackground(background compat.AdaptiveColor) Option {
|
||||
return func(c *commandsComponent) {
|
||||
c.background = &background
|
||||
}
|
||||
}
|
||||
|
||||
func WithLimit(limit int) Option {
|
||||
return func(c *commandsComponent) {
|
||||
c.limit = &limit
|
||||
}
|
||||
}
|
||||
|
||||
func WithShowAll(showAll bool) Option {
|
||||
return func(c *commandsComponent) {
|
||||
c.showAll = showAll
|
||||
}
|
||||
}
|
||||
|
||||
func New(app *app.App, opts ...Option) CommandsComponent {
|
||||
c := &commandsComponent{
|
||||
app: app,
|
||||
background: nil,
|
||||
showKeybinds: true,
|
||||
showAll: false,
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(c)
|
||||
}
|
||||
return c
|
||||
}
|
||||
@@ -116,7 +116,7 @@ func (c *completionDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case []CompletionItemI:
|
||||
c.list.SetItems(msg)
|
||||
case app.CompletionDialogTriggerdMsg:
|
||||
case app.CompletionDialogTriggeredMsg:
|
||||
c.pseudoSearchTextArea.SetValue(msg.InitialValue)
|
||||
case tea.KeyMsg:
|
||||
if c.pseudoSearchTextArea.Focused() {
|
||||
@@ -173,7 +173,6 @@ func (c *completionDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
cmds = append(cmds, cmd)
|
||||
cmds = append(cmds, c.pseudoSearchTextArea.Focus())
|
||||
// c.pseudoSearchTextArea.SetValue(msg.String())
|
||||
return c, tea.Batch(cmds...)
|
||||
}
|
||||
case tea.WindowSizeMsg:
|
||||
@@ -218,12 +217,12 @@ func (c *completionDialogComponent) SetProvider(provider CompletionProvider) {
|
||||
if c.completionProvider.GetId() != provider.GetId() {
|
||||
c.completionProvider = provider
|
||||
c.list.SetEmptyMessage(" " + provider.GetEmptyMessage())
|
||||
c.list.SetItems([]CompletionItemI{})
|
||||
}
|
||||
}
|
||||
|
||||
func (c *completionDialogComponent) complete(item CompletionItemI) tea.Cmd {
|
||||
value := c.pseudoSearchTextArea.Value()
|
||||
|
||||
if value == "" {
|
||||
return nil
|
||||
}
|
||||
@@ -242,10 +241,8 @@ func (c *completionDialogComponent) complete(item CompletionItemI) tea.Cmd {
|
||||
}
|
||||
|
||||
func (c *completionDialogComponent) close() tea.Cmd {
|
||||
c.list.SetItems([]CompletionItemI{})
|
||||
c.pseudoSearchTextArea.Reset()
|
||||
c.pseudoSearchTextArea.Blur()
|
||||
|
||||
return util.CmdHandler(CompletionDialogCloseMsg{})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,71 +1,62 @@
|
||||
package dialog
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/bubbles/v2/viewport"
|
||||
tea "github.com/charmbracelet/bubbletea/v2"
|
||||
"github.com/charmbracelet/lipgloss/v2"
|
||||
"github.com/sst/opencode/internal/commands"
|
||||
"github.com/sst/opencode/internal/app"
|
||||
commandsComponent "github.com/sst/opencode/internal/components/commands"
|
||||
"github.com/sst/opencode/internal/components/modal"
|
||||
"github.com/sst/opencode/internal/layout"
|
||||
"github.com/sst/opencode/internal/theme"
|
||||
)
|
||||
|
||||
type helpDialog struct {
|
||||
width int
|
||||
height int
|
||||
modal *modal.Modal
|
||||
commands []commands.Command
|
||||
width int
|
||||
height int
|
||||
modal *modal.Modal
|
||||
app *app.App
|
||||
commandsComponent commandsComponent.CommandsComponent
|
||||
viewport viewport.Model
|
||||
}
|
||||
|
||||
func (h *helpDialog) Init() tea.Cmd {
|
||||
return nil
|
||||
return tea.Batch(
|
||||
h.commandsComponent.Init(),
|
||||
h.viewport.Init(),
|
||||
)
|
||||
}
|
||||
|
||||
func (h *helpDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
var cmds []tea.Cmd
|
||||
|
||||
switch msg := msg.(type) {
|
||||
case tea.WindowSizeMsg:
|
||||
h.width = msg.Width
|
||||
h.height = msg.Height
|
||||
// Set viewport size with some padding for the modal
|
||||
h.viewport = viewport.New(viewport.WithWidth(msg.Width-4), viewport.WithHeight(msg.Height-6))
|
||||
h.commandsComponent.SetSize(msg.Width-4, msg.Height-6)
|
||||
}
|
||||
return h, nil
|
||||
|
||||
// Update commands component first to get the latest content
|
||||
_, cmdCmd := h.commandsComponent.Update(msg)
|
||||
cmds = append(cmds, cmdCmd)
|
||||
|
||||
// Update viewport content
|
||||
h.viewport.SetContent(h.commandsComponent.View())
|
||||
|
||||
// Update viewport
|
||||
var vpCmd tea.Cmd
|
||||
h.viewport, vpCmd = h.viewport.Update(msg)
|
||||
cmds = append(cmds, vpCmd)
|
||||
|
||||
return h, tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
func (h *helpDialog) View() string {
|
||||
t := theme.CurrentTheme()
|
||||
keyStyle := lipgloss.NewStyle().
|
||||
Background(t.BackgroundElement()).
|
||||
Foreground(t.Text()).
|
||||
Bold(true)
|
||||
descStyle := lipgloss.NewStyle().
|
||||
Background(t.BackgroundElement()).
|
||||
Foreground(t.TextMuted())
|
||||
contentStyle := lipgloss.NewStyle().
|
||||
PaddingLeft(1).Background(t.BackgroundElement())
|
||||
|
||||
lines := []string{}
|
||||
for _, b := range h.commands {
|
||||
// Only interested in slash commands
|
||||
if b.Trigger == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
content := keyStyle.Render("/" + b.Trigger)
|
||||
content += descStyle.Render(" " + b.Description)
|
||||
// for i, key := range b.Keybindings {
|
||||
// if i == 0 {
|
||||
// keyString := " (" + key.Key + ")"
|
||||
// space := max(h.width-lipgloss.Width(content)-lipgloss.Width(keyString), 0)
|
||||
// spacer := strings.Repeat(" ", space)
|
||||
// content += descStyle.Render(spacer)
|
||||
// content += descStyle.Render(keyString)
|
||||
// }
|
||||
// }
|
||||
|
||||
lines = append(lines, contentStyle.Render(content))
|
||||
}
|
||||
|
||||
return strings.Join(lines, "\n")
|
||||
h.commandsComponent.SetBackgroundColor(t.BackgroundElement())
|
||||
return h.viewport.View()
|
||||
}
|
||||
|
||||
func (h *helpDialog) Render(background string) string {
|
||||
@@ -80,9 +71,16 @@ type HelpDialog interface {
|
||||
layout.Modal
|
||||
}
|
||||
|
||||
func NewHelpDialog(commands []commands.Command) HelpDialog {
|
||||
func NewHelpDialog(app *app.App) HelpDialog {
|
||||
vp := viewport.New(viewport.WithHeight(12))
|
||||
return &helpDialog{
|
||||
commands: commands,
|
||||
app: app,
|
||||
commandsComponent: commandsComponent.New(app,
|
||||
commandsComponent.WithBackground(theme.CurrentTheme().BackgroundElement()),
|
||||
commandsComponent.WithShowAll(true),
|
||||
commandsComponent.WithKeybinds(true),
|
||||
),
|
||||
modal: modal.New(modal.WithTitle("Help")),
|
||||
viewport: vp,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
tea "github.com/charmbracelet/bubbletea/v2"
|
||||
"github.com/charmbracelet/lipgloss/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/styles"
|
||||
@@ -33,20 +34,15 @@ type modelDialog struct {
|
||||
app *app.App
|
||||
availableProviders []client.ProviderInfo
|
||||
provider client.ProviderInfo
|
||||
|
||||
selectedIdx int
|
||||
width int
|
||||
height int
|
||||
scrollOffset int
|
||||
hScrollOffset int
|
||||
hScrollPossible bool
|
||||
|
||||
modal *modal.Modal
|
||||
width int
|
||||
height int
|
||||
hScrollOffset int
|
||||
hScrollPossible bool
|
||||
modal *modal.Modal
|
||||
modelList list.List[list.StringItem]
|
||||
}
|
||||
|
||||
type modelKeyMap struct {
|
||||
Up key.Binding
|
||||
Down key.Binding
|
||||
Left key.Binding
|
||||
Right key.Binding
|
||||
Enter key.Binding
|
||||
@@ -54,14 +50,6 @@ type modelKeyMap struct {
|
||||
}
|
||||
|
||||
var modelKeys = modelKeyMap{
|
||||
Up: key.NewBinding(
|
||||
key.WithKeys("up", "k"),
|
||||
key.WithHelp("↑", "previous model"),
|
||||
),
|
||||
Down: key.NewBinding(
|
||||
key.WithKeys("down", "j"),
|
||||
key.WithHelp("↓", "next model"),
|
||||
),
|
||||
Left: key.NewBinding(
|
||||
key.WithKeys("left", "h"),
|
||||
key.WithHelp("←", "scroll left"),
|
||||
@@ -81,15 +69,7 @@ var modelKeys = modelKeyMap{
|
||||
}
|
||||
|
||||
func (m *modelDialog) Init() tea.Cmd {
|
||||
// cfg := config.Get()
|
||||
// modelInfo := GetSelectedModel(cfg)
|
||||
// m.availableProviders = getEnabledProviders(cfg)
|
||||
// m.hScrollPossible = len(m.availableProviders) > 1
|
||||
|
||||
// m.provider = modelInfo.Provider
|
||||
// m.hScrollOffset = findProviderIndex(m.availableProviders, m.provider)
|
||||
|
||||
// m.setupModelsForProvider(m.provider)
|
||||
m.setupModelsForProvider(m.provider.Id)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -97,26 +77,32 @@ 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):
|
||||
m.moveSelectionUp()
|
||||
case key.Matches(msg, modelKeys.Down):
|
||||
m.moveSelectionDown()
|
||||
case key.Matches(msg, modelKeys.Left):
|
||||
if m.hScrollPossible {
|
||||
m.switchProvider(-1)
|
||||
}
|
||||
return m, nil
|
||||
case key.Matches(msg, modelKeys.Right):
|
||||
if m.hScrollPossible {
|
||||
m.switchProvider(1)
|
||||
}
|
||||
return m, nil
|
||||
case key.Matches(msg, modelKeys.Enter):
|
||||
selectedItem, _ := m.modelList.GetSelectedItem()
|
||||
models := m.models()
|
||||
var selectedModel client.ModelInfo
|
||||
for _, model := range models {
|
||||
if model.Name == string(selectedItem) {
|
||||
selectedModel = model
|
||||
break
|
||||
}
|
||||
}
|
||||
return m, tea.Sequence(
|
||||
util.CmdHandler(modal.CloseModalMsg{}),
|
||||
util.CmdHandler(
|
||||
app.ModelSelectedMsg{
|
||||
Provider: m.provider,
|
||||
Model: models[m.selectedIdx],
|
||||
Model: selectedModel,
|
||||
}),
|
||||
)
|
||||
case key.Matches(msg, modelKeys.Escape):
|
||||
@@ -127,7 +113,10 @@ func (m *modelDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.height = msg.Height
|
||||
}
|
||||
|
||||
return m, nil
|
||||
// Update the list component
|
||||
updatedList, cmd := m.modelList.Update(msg)
|
||||
m.modelList = updatedList.(list.List[list.StringItem])
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
func (m *modelDialog) models() []client.ModelInfo {
|
||||
@@ -137,40 +126,9 @@ func (m *modelDialog) models() []client.ModelInfo {
|
||||
return models
|
||||
}
|
||||
|
||||
// moveSelectionUp moves the selection up or wraps to bottom
|
||||
func (m *modelDialog) moveSelectionUp() {
|
||||
if m.selectedIdx > 0 {
|
||||
m.selectedIdx--
|
||||
} else {
|
||||
m.selectedIdx = len(m.provider.Models) - 1
|
||||
m.scrollOffset = max(0, len(m.provider.Models)-numVisibleModels)
|
||||
}
|
||||
|
||||
// Keep selection visible
|
||||
if m.selectedIdx < m.scrollOffset {
|
||||
m.scrollOffset = m.selectedIdx
|
||||
}
|
||||
}
|
||||
|
||||
// moveSelectionDown moves the selection down or wraps to top
|
||||
func (m *modelDialog) moveSelectionDown() {
|
||||
if m.selectedIdx < len(m.provider.Models)-1 {
|
||||
m.selectedIdx++
|
||||
} else {
|
||||
m.selectedIdx = 0
|
||||
m.scrollOffset = 0
|
||||
}
|
||||
|
||||
// Keep selection visible
|
||||
if m.selectedIdx >= m.scrollOffset+numVisibleModels {
|
||||
m.scrollOffset = m.selectedIdx - (numVisibleModels - 1)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *modelDialog) switchProvider(offset int) {
|
||||
newOffset := m.hScrollOffset + offset
|
||||
|
||||
// Ensure we stay within bounds
|
||||
if newOffset < 0 {
|
||||
newOffset = len(m.availableProviders) - 1
|
||||
}
|
||||
@@ -185,105 +143,46 @@ func (m *modelDialog) switchProvider(offset int) {
|
||||
}
|
||||
|
||||
func (m *modelDialog) View() string {
|
||||
t := theme.CurrentTheme()
|
||||
baseStyle := lipgloss.NewStyle().
|
||||
Background(t.BackgroundElement()).
|
||||
Foreground(t.Text())
|
||||
|
||||
// Render visible models
|
||||
endIdx := min(m.scrollOffset+numVisibleModels, len(m.provider.Models))
|
||||
modelItems := make([]string, 0, endIdx-m.scrollOffset)
|
||||
|
||||
models := m.models()
|
||||
for i := m.scrollOffset; i < endIdx; i++ {
|
||||
itemStyle := baseStyle.Width(maxDialogWidth)
|
||||
if i == m.selectedIdx {
|
||||
itemStyle = itemStyle.
|
||||
Background(t.Primary()).
|
||||
Foreground(t.BackgroundElement()).
|
||||
Bold(true)
|
||||
}
|
||||
modelItems = append(modelItems, itemStyle.Render(models[i].Name))
|
||||
}
|
||||
|
||||
listView := m.modelList.View()
|
||||
scrollIndicator := m.getScrollIndicators(maxDialogWidth)
|
||||
|
||||
content := lipgloss.JoinVertical(
|
||||
lipgloss.Left,
|
||||
baseStyle.
|
||||
Width(maxDialogWidth).
|
||||
Render(lipgloss.JoinVertical(lipgloss.Left, modelItems...)),
|
||||
scrollIndicator,
|
||||
)
|
||||
|
||||
return content
|
||||
return strings.Join([]string{listView, scrollIndicator}, "\n")
|
||||
}
|
||||
|
||||
func (m *modelDialog) getScrollIndicators(maxWidth int) string {
|
||||
var indicator string
|
||||
|
||||
if len(m.provider.Models) > numVisibleModels {
|
||||
if m.scrollOffset > 0 {
|
||||
indicator += "↑ "
|
||||
}
|
||||
if m.scrollOffset+numVisibleModels < len(m.provider.Models) {
|
||||
indicator += "↓ "
|
||||
}
|
||||
}
|
||||
|
||||
if m.hScrollPossible {
|
||||
indicator = "← " + indicator + "→"
|
||||
indicator = "← → (switch provider) "
|
||||
}
|
||||
|
||||
if indicator == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
t := theme.CurrentTheme()
|
||||
baseStyle := styles.BaseStyle()
|
||||
|
||||
return baseStyle.
|
||||
Foreground(t.Primary()).
|
||||
return styles.BaseStyle().
|
||||
Foreground(t.TextMuted()).
|
||||
Width(maxWidth).
|
||||
Align(lipgloss.Right).
|
||||
Bold(true).
|
||||
Render(indicator)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// if p == provider {
|
||||
// return i
|
||||
// }
|
||||
// }
|
||||
// return -1
|
||||
// }
|
||||
func (m *modelDialog) setupModelsForProvider(providerId string) {
|
||||
models := m.models()
|
||||
modelNames := make([]string, len(models))
|
||||
for i, model := range models {
|
||||
modelNames[i] = model.Name
|
||||
}
|
||||
|
||||
func (m *modelDialog) setupModelsForProvider(_ string) {
|
||||
m.selectedIdx = 0
|
||||
m.scrollOffset = 0
|
||||
m.modelList = list.NewStringList(modelNames, numVisibleModels, "No models available", true)
|
||||
m.modelList.SetMaxWidth(maxDialogWidth)
|
||||
|
||||
// cfg := config.Get()
|
||||
// agentCfg := cfg.Agents[config.AgentPrimary]
|
||||
// selectedModelId := agentCfg.Model
|
||||
|
||||
// m.provider = provider
|
||||
// m.models = getModelsForProvider(provider)
|
||||
|
||||
// Try to select the current model if it belongs to this provider
|
||||
// if provider == models.SupportedModels[selectedModelId].Provider {
|
||||
// for i, model := range m.models {
|
||||
// if model.ID == selectedModelId {
|
||||
// m.selectedIdx = i
|
||||
// // Adjust scroll position to keep selected model visible
|
||||
// if m.selectedIdx >= numVisibleModels {
|
||||
// m.scrollOffset = m.selectedIdx - (numVisibleModels - 1)
|
||||
// }
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
if m.app.Provider != nil && m.app.Model != nil && m.app.Provider.Id == providerId {
|
||||
for i, model := range models {
|
||||
if model.Id == m.app.Model.Id {
|
||||
m.modelList.SetSelectedIndex(i)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *modelDialog) Render(background string) string {
|
||||
@@ -297,11 +196,30 @@ func (s *modelDialog) Close() tea.Cmd {
|
||||
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(modal.WithTitle(fmt.Sprintf("Select %s Model", availableProviders[0].Name))),
|
||||
currentProvider := availableProviders[0]
|
||||
hScrollOffset := 0
|
||||
if app.Provider != nil {
|
||||
for i, provider := range availableProviders {
|
||||
if provider.Id == app.Provider.Id {
|
||||
currentProvider = provider
|
||||
hScrollOffset = i
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dialog := &modelDialog{
|
||||
app: app,
|
||||
availableProviders: availableProviders,
|
||||
hScrollOffset: hScrollOffset,
|
||||
hScrollPossible: len(availableProviders) > 1,
|
||||
provider: currentProvider,
|
||||
modal: modal.New(
|
||||
modal.WithTitle(fmt.Sprintf("Select %s Model", currentProvider.Name)),
|
||||
modal.WithMaxWidth(maxDialogWidth+4),
|
||||
),
|
||||
}
|
||||
|
||||
dialog.setupModelsForProvider(currentProvider.Id)
|
||||
return dialog
|
||||
}
|
||||
|
||||
@@ -8,8 +8,6 @@ import (
|
||||
"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/styles"
|
||||
"github.com/sst/opencode/internal/theme"
|
||||
"github.com/sst/opencode/internal/util"
|
||||
"github.com/sst/opencode/pkg/client"
|
||||
)
|
||||
@@ -19,33 +17,12 @@ type SessionDialog interface {
|
||||
layout.Modal
|
||||
}
|
||||
|
||||
type sessionItem client.SessionInfo
|
||||
|
||||
func (s sessionItem) Render(selected bool, width int) string {
|
||||
t := theme.CurrentTheme()
|
||||
baseStyle := styles.BaseStyle().
|
||||
Width(width - 4).
|
||||
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.Title)
|
||||
}
|
||||
|
||||
type sessionDialog struct {
|
||||
width int
|
||||
height int
|
||||
modal *modal.Modal
|
||||
selectedSessionID string
|
||||
list list.List[sessionItem]
|
||||
width int
|
||||
height int
|
||||
modal *modal.Modal
|
||||
sessions []client.SessionInfo
|
||||
list list.List[list.StringItem]
|
||||
}
|
||||
|
||||
func (s *sessionDialog) Init() tea.Cmd {
|
||||
@@ -61,11 +38,11 @@ func (s *sessionDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
case tea.KeyPressMsg:
|
||||
switch msg.String() {
|
||||
case "enter":
|
||||
if item, idx := s.list.GetSelectedItem(); idx >= 0 {
|
||||
s.selectedSessionID = item.Id
|
||||
if _, idx := s.list.GetSelectedItem(); idx >= 0 && idx < len(s.sessions) {
|
||||
selectedSession := s.sessions[idx]
|
||||
return s, tea.Sequence(
|
||||
util.CmdHandler(modal.CloseModalMsg{}),
|
||||
util.CmdHandler(app.SessionSelectedMsg(&item)),
|
||||
util.CmdHandler(app.SessionSelectedMsg(&selectedSession)),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -73,7 +50,7 @@ func (s *sessionDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
|
||||
var cmd tea.Cmd
|
||||
listModel, cmd := s.list.Update(msg)
|
||||
s.list = listModel.(list.List[sessionItem])
|
||||
s.list = listModel.(list.List[list.StringItem])
|
||||
return s, cmd
|
||||
}
|
||||
|
||||
@@ -89,23 +66,30 @@ func (s *sessionDialog) Close() tea.Cmd {
|
||||
func NewSessionDialog(app *app.App) SessionDialog {
|
||||
sessions, _ := app.ListSessions(context.Background())
|
||||
|
||||
var sessionItems []sessionItem
|
||||
var filteredSessions []client.SessionInfo
|
||||
var sessionTitles []string
|
||||
for _, sess := range sessions {
|
||||
if sess.ParentID != nil {
|
||||
continue
|
||||
}
|
||||
sessionItems = append(sessionItems, sessionItem(sess))
|
||||
filteredSessions = append(filteredSessions, sess)
|
||||
sessionTitles = append(sessionTitles, sess.Title)
|
||||
}
|
||||
|
||||
list := list.NewListComponent(
|
||||
sessionItems,
|
||||
list := list.NewStringList(
|
||||
sessionTitles,
|
||||
10, // maxVisibleSessions
|
||||
"No sessions available",
|
||||
true, // useAlphaNumericKeys
|
||||
)
|
||||
list.SetMaxWidth(layout.Current.Container.Width - 12)
|
||||
|
||||
return &sessionDialog{
|
||||
list: list,
|
||||
modal: modal.New(modal.WithTitle("Switch Session"), modal.WithMaxWidth(80)),
|
||||
sessions: filteredSessions,
|
||||
list: list,
|
||||
modal: modal.New(
|
||||
modal.WithTitle("Switch Session"),
|
||||
modal.WithMaxWidth(layout.Current.Container.Width-8),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
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/styles"
|
||||
"github.com/sst/opencode/internal/theme"
|
||||
"github.com/sst/opencode/internal/util"
|
||||
)
|
||||
@@ -20,35 +19,14 @@ type ThemeDialog interface {
|
||||
layout.Modal
|
||||
}
|
||||
|
||||
type themeItem struct {
|
||||
name string
|
||||
}
|
||||
|
||||
func (t themeItem) Render(selected bool, width int) string {
|
||||
th := theme.CurrentTheme()
|
||||
baseStyle := styles.BaseStyle().
|
||||
Width(width - 2).
|
||||
Background(th.BackgroundElement())
|
||||
|
||||
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]
|
||||
modal *modal.Modal
|
||||
list list.List[list.StringItem]
|
||||
originalTheme string
|
||||
themeApplied bool
|
||||
}
|
||||
|
||||
func (t *themeDialog) Init() tea.Cmd {
|
||||
@@ -64,26 +42,31 @@ func (t *themeDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "enter":
|
||||
if item, idx := t.list.GetSelectedItem(); idx >= 0 {
|
||||
previousTheme := theme.CurrentThemeName()
|
||||
selectedTheme := item.name
|
||||
if previousTheme == selectedTheme {
|
||||
return t, util.CmdHandler(modal.CloseModalMsg{})
|
||||
}
|
||||
selectedTheme := string(item)
|
||||
if err := theme.SetTheme(selectedTheme); err != nil {
|
||||
// status.Error(err.Error())
|
||||
return t, nil
|
||||
}
|
||||
t.themeApplied = true
|
||||
return t, tea.Sequence(
|
||||
util.CmdHandler(modal.CloseModalMsg{}),
|
||||
util.CmdHandler(ThemeSelectedMsg{ThemeName: selectedTheme}),
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
_, prevIdx := t.list.GetSelectedItem()
|
||||
|
||||
var cmd tea.Cmd
|
||||
listModel, cmd := t.list.Update(msg)
|
||||
t.list = listModel.(list.List[themeItem])
|
||||
t.list = listModel.(list.List[list.StringItem])
|
||||
|
||||
if item, newIdx := t.list.GetSelectedItem(); newIdx >= 0 && newIdx != prevIdx {
|
||||
theme.SetTheme(string(item))
|
||||
return t, util.CmdHandler(ThemeSelectedMsg{ThemeName: string(item)})
|
||||
}
|
||||
return t, cmd
|
||||
}
|
||||
|
||||
@@ -92,6 +75,10 @@ func (t *themeDialog) Render(background string) string {
|
||||
}
|
||||
|
||||
func (t *themeDialog) Close() tea.Cmd {
|
||||
if !t.themeApplied {
|
||||
theme.SetTheme(t.originalTheme)
|
||||
return util.CmdHandler(ThemeSelectedMsg{ThemeName: t.originalTheme})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -100,17 +87,15 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
list := list.NewListComponent(
|
||||
themeItems,
|
||||
list := list.NewStringList(
|
||||
themes,
|
||||
10, // maxVisibleThemes
|
||||
"No themes available",
|
||||
true,
|
||||
@@ -118,9 +103,14 @@ func NewThemeDialog() ThemeDialog {
|
||||
|
||||
// Set the initial selection to the current theme
|
||||
list.SetSelectedIndex(selectedIdx)
|
||||
|
||||
// Set the max width for the list to match the modal width
|
||||
list.SetMaxWidth(36) // 40 (modal max width) - 4 (modal padding)
|
||||
|
||||
return &themeDialog{
|
||||
list: list,
|
||||
modal: modal.New(modal.WithTitle("Select Theme"), modal.WithMaxWidth(40)),
|
||||
list: list,
|
||||
modal: modal.New(modal.WithTitle("Select Theme"), modal.WithMaxWidth(40)),
|
||||
originalTheme: currentTheme,
|
||||
themeApplied: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -441,10 +441,10 @@ func SyntaxHighlight(w io.Writer, source, fileName, formatter string, bg color.C
|
||||
<entry type="TextWhitespace" style="%s"/>
|
||||
</style>
|
||||
`,
|
||||
getColor(t.BackgroundSubtle()), // Background
|
||||
getColor(t.Text()), // Text
|
||||
getColor(t.Text()), // Other
|
||||
getColor(t.Error()), // Error
|
||||
getColor(t.BackgroundPanel()), // Background
|
||||
getColor(t.Text()), // Text
|
||||
getColor(t.Text()), // Other
|
||||
getColor(t.Error()), // Error
|
||||
|
||||
getColor(t.SyntaxKeyword()), // Keyword
|
||||
getColor(t.SyntaxKeyword()), // KeywordConstant
|
||||
@@ -614,7 +614,7 @@ func applyHighlighting(content string, segments []Segment, segmentType LineType,
|
||||
|
||||
// Get the appropriate color based on terminal background
|
||||
bgColor := lipgloss.Color(getColor(highlightBg))
|
||||
fgColor := lipgloss.Color(getColor(theme.CurrentTheme().BackgroundSubtle()))
|
||||
fgColor := lipgloss.Color(getColor(theme.CurrentTheme().BackgroundPanel()))
|
||||
|
||||
for i := 0; i < len(content); {
|
||||
// Check if we're at an ANSI sequence
|
||||
|
||||
@@ -5,6 +5,10 @@ import (
|
||||
|
||||
"github.com/charmbracelet/bubbles/v2/key"
|
||||
tea "github.com/charmbracelet/bubbletea/v2"
|
||||
"github.com/charmbracelet/lipgloss/v2"
|
||||
"github.com/muesli/reflow/truncate"
|
||||
"github.com/sst/opencode/internal/styles"
|
||||
"github.com/sst/opencode/internal/theme"
|
||||
)
|
||||
|
||||
type ListItem interface {
|
||||
@@ -123,6 +127,9 @@ func (c *listComponent[T]) SetSelectedIndex(idx int) {
|
||||
func (c *listComponent[T]) View() string {
|
||||
items := c.items
|
||||
maxWidth := c.maxWidth
|
||||
if maxWidth == 0 {
|
||||
maxWidth = 80 // Default width if not set
|
||||
}
|
||||
maxVisibleItems := min(c.maxVisibleItems, len(items))
|
||||
startIdx := 0
|
||||
|
||||
@@ -161,3 +168,36 @@ func NewListComponent[T ListItem](items []T, maxVisibleItems int, fallbackMsg st
|
||||
selectedIdx: 0,
|
||||
}
|
||||
}
|
||||
|
||||
// StringItem is a simple implementation of ListItem for string values
|
||||
type StringItem string
|
||||
|
||||
func (s StringItem) Render(selected bool, width int) string {
|
||||
t := theme.CurrentTheme()
|
||||
baseStyle := styles.BaseStyle()
|
||||
|
||||
truncatedStr := truncate.StringWithTail(string(s), uint(width-1), "...")
|
||||
|
||||
var itemStyle lipgloss.Style
|
||||
if selected {
|
||||
itemStyle = baseStyle.
|
||||
Background(t.Primary()).
|
||||
Foreground(t.Background()).
|
||||
Width(width).
|
||||
PaddingLeft(1)
|
||||
} else {
|
||||
itemStyle = baseStyle.
|
||||
PaddingLeft(1)
|
||||
}
|
||||
|
||||
return itemStyle.Render(truncatedStr)
|
||||
}
|
||||
|
||||
// NewStringList creates a new list component with string items
|
||||
func NewStringList(items []string, maxVisibleItems int, fallbackMsg string, useAlphaNumericKeys bool) List[StringItem] {
|
||||
stringItems := make([]StringItem, len(items))
|
||||
for i, item := range items {
|
||||
stringItems[i] = StringItem(item)
|
||||
}
|
||||
return NewListComponent(stringItems, maxVisibleItems, fallbackMsg, useAlphaNumericKeys)
|
||||
}
|
||||
|
||||
@@ -103,18 +103,18 @@ func (m *Modal) Render(contentView string, background string) string {
|
||||
Bold(true).
|
||||
Padding(0, 1)
|
||||
|
||||
escStyle := baseStyle.Foreground(t.TextMuted()).Bold(false)
|
||||
escStyle := baseStyle.Foreground(t.TextMuted())
|
||||
escText := escStyle.Render("esc")
|
||||
|
||||
// Calculate position for esc text
|
||||
titleWidth := lipgloss.Width(m.title)
|
||||
escWidth := lipgloss.Width(escText)
|
||||
spacesNeeded := max(0, innerWidth-titleWidth-escWidth-3)
|
||||
spacesNeeded := max(0, innerWidth-titleWidth-escWidth-2)
|
||||
spacer := strings.Repeat(" ", spacesNeeded)
|
||||
titleLine := m.title + spacer + escText
|
||||
titleLine = titleStyle.Render(titleLine)
|
||||
|
||||
finalContent = strings.Join([]string{titleLine, contentView}, "\n") + "\n"
|
||||
finalContent = strings.Join([]string{titleLine, "", contentView}, "\n")
|
||||
} else {
|
||||
finalContent = contentView
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ func formatTokensAndCost(tokens float32, contextWindow float32, cost float32) st
|
||||
formattedCost := fmt.Sprintf("$%.2f", cost)
|
||||
percentage := (float64(tokens) / float64(contextWindow)) * 100
|
||||
|
||||
return fmt.Sprintf("Tokens: %s (%d%%), Cost: %s", formattedTokens, int(percentage), formattedCost)
|
||||
return fmt.Sprintf("Context: %s (%d%%), Cost: %s", formattedTokens, int(percentage), formattedCost)
|
||||
}
|
||||
|
||||
func (m statusComponent) View() string {
|
||||
@@ -88,7 +88,7 @@ func (m statusComponent) View() string {
|
||||
|
||||
cwd := styles.Padded().
|
||||
Foreground(t.TextMuted()).
|
||||
Background(t.BackgroundSubtle()).
|
||||
Background(t.BackgroundPanel()).
|
||||
Render(m.app.Info.Path.Cwd)
|
||||
|
||||
sessionInfo := ""
|
||||
@@ -123,7 +123,7 @@ func (m statusComponent) View() string {
|
||||
0,
|
||||
m.width-lipgloss.Width(logo)-lipgloss.Width(cwd)-lipgloss.Width(sessionInfo),
|
||||
)
|
||||
spacer := lipgloss.NewStyle().Background(t.BackgroundSubtle()).Width(space).Render("")
|
||||
spacer := lipgloss.NewStyle().Background(t.BackgroundPanel()).Width(space).Render("")
|
||||
|
||||
status := logo + cwd + spacer + sessionInfo
|
||||
|
||||
|
||||
@@ -553,6 +553,14 @@ func (m Model) Line() int {
|
||||
return m.row
|
||||
}
|
||||
|
||||
func (m *Model) Newline() {
|
||||
if m.MaxHeight > 0 && len(m.value) >= m.MaxHeight {
|
||||
return
|
||||
}
|
||||
m.col = clamp(m.col, 0, len(m.value[m.row]))
|
||||
m.splitLine(m.row, m.col)
|
||||
}
|
||||
|
||||
// CursorDown moves the cursor down by one line.
|
||||
// Returns whether or not the cursor blink should be reset.
|
||||
func (m *Model) CursorDown() {
|
||||
@@ -1113,11 +1121,7 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
|
||||
}
|
||||
m.deleteWordRight()
|
||||
case key.Matches(msg, m.KeyMap.InsertNewline):
|
||||
if m.MaxHeight > 0 && len(m.value) >= m.MaxHeight {
|
||||
return m, nil
|
||||
}
|
||||
m.col = clamp(m.col, 0, len(m.value[m.row]))
|
||||
m.splitLine(m.row, m.col)
|
||||
m.Newline()
|
||||
case key.Matches(msg, m.KeyMap.LineEnd):
|
||||
m.CursorEnd()
|
||||
case key.Matches(msg, m.KeyMap.LineStart):
|
||||
@@ -1161,8 +1165,6 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
|
||||
}
|
||||
|
||||
var cmd tea.Cmd
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
newRow, newCol := m.cursorLineNumber(), m.col
|
||||
m.virtualCursor, cmd = m.virtualCursor.Update(msg)
|
||||
if (newRow != oldRow || newCol != oldCol) && m.virtualCursor.Mode() == cursor.CursorBlink {
|
||||
@@ -1171,7 +1173,6 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
|
||||
}
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
m.SetHeight(m.ContentHeight())
|
||||
return m, tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ package image
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"image"
|
||||
"github.com/atotto/clipboard"
|
||||
"image"
|
||||
)
|
||||
|
||||
func GetImageFromClipboard() ([]byte, string, error) {
|
||||
@@ -28,8 +28,6 @@ func GetImageFromClipboard() ([]byte, string, error) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
func binaryToImage(data []byte) ([]byte, error) {
|
||||
reader := bytes.NewReader(data)
|
||||
img, _, err := image.Decode(reader)
|
||||
@@ -40,7 +38,6 @@ func binaryToImage(data []byte) ([]byte, error) {
|
||||
return ImageToBytes(img)
|
||||
}
|
||||
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
|
||||
@@ -109,11 +109,11 @@ func PlaceOverlay(
|
||||
// Get the foreground line
|
||||
fgLine := fgLines[i-y]
|
||||
fgLineWidth := ansi.PrintableRuneWidth(fgLine)
|
||||
|
||||
|
||||
// Extract the styles at the border positions
|
||||
leftStyle := getStyleAtPosition(bgLine, pos)
|
||||
rightStyle := getStyleAtPosition(bgLine, pos + 1 + fgLineWidth)
|
||||
|
||||
rightStyle := getStyleAtPosition(bgLine, pos+1+fgLineWidth)
|
||||
|
||||
// Left border - combine background from original with border foreground
|
||||
leftSeq := combineStyles(leftStyle, options.borderColor)
|
||||
if leftSeq != "" {
|
||||
@@ -172,17 +172,17 @@ type ansiStyle struct {
|
||||
// parseANSISequence parses an ANSI escape sequence into its components
|
||||
func parseANSISequence(seq string) ansiStyle {
|
||||
style := ansiStyle{}
|
||||
|
||||
|
||||
// Extract the parameters from the sequence (e.g., \x1b[38;5;123;48;5;456m -> "38;5;123;48;5;456")
|
||||
if !strings.HasPrefix(seq, "\x1b[") || !strings.HasSuffix(seq, "m") {
|
||||
return style
|
||||
}
|
||||
|
||||
|
||||
params := seq[2 : len(seq)-1]
|
||||
if params == "" {
|
||||
return style
|
||||
}
|
||||
|
||||
|
||||
parts := strings.Split(params, ";")
|
||||
i := 0
|
||||
for i < len(parts) {
|
||||
@@ -222,7 +222,7 @@ func parseANSISequence(seq string) ansiStyle {
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
|
||||
return style
|
||||
}
|
||||
|
||||
@@ -231,32 +231,32 @@ func combineStyles(bgStyle ansiStyle, fgColor *compat.AdaptiveColor) string {
|
||||
if fgColor == nil && bgStyle.bgColor == "" && len(bgStyle.attrs) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
|
||||
var parts []string
|
||||
|
||||
|
||||
// Add attributes
|
||||
parts = append(parts, bgStyle.attrs...)
|
||||
|
||||
|
||||
// Add background color from the original style
|
||||
if bgStyle.bgColor != "" {
|
||||
parts = append(parts, bgStyle.bgColor)
|
||||
}
|
||||
|
||||
|
||||
// Add foreground color if specified
|
||||
if fgColor != nil {
|
||||
// Use the light color (could be improved to detect terminal background)
|
||||
color := (*fgColor).Light
|
||||
|
||||
|
||||
// Use RGBA to get color components
|
||||
r, g, b, _ := color.RGBA()
|
||||
// RGBA returns 16-bit values, we need 8-bit
|
||||
parts = append(parts, fmt.Sprintf("38;2;%d;%d;%d", r>>8, g>>8, b>>8))
|
||||
}
|
||||
|
||||
|
||||
if len(parts) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
|
||||
return fmt.Sprintf("\x1b[%sm", strings.Join(parts, ";"))
|
||||
}
|
||||
|
||||
@@ -264,10 +264,10 @@ func combineStyles(bgStyle ansiStyle, fgColor *compat.AdaptiveColor) string {
|
||||
func getStyleAtPosition(s string, targetPos int) ansiStyle {
|
||||
// ANSI escape sequence regex
|
||||
ansiRegex := regexp.MustCompile(`\x1b\[[0-9;]*m`)
|
||||
|
||||
|
||||
visualPos := 0
|
||||
currentStyle := ansiStyle{}
|
||||
|
||||
|
||||
i := 0
|
||||
for i < len(s) && visualPos <= targetPos {
|
||||
// Check if we're at an ANSI escape sequence
|
||||
@@ -275,7 +275,7 @@ func getStyleAtPosition(s string, targetPos int) ansiStyle {
|
||||
// Found an ANSI sequence at current position
|
||||
seq := s[i : i+match[1]]
|
||||
parsedStyle := parseANSISequence(seq)
|
||||
|
||||
|
||||
// Update current style (merge with existing)
|
||||
if parsedStyle.fgColor != "" {
|
||||
currentStyle.fgColor = parsedStyle.fgColor
|
||||
@@ -286,7 +286,7 @@ func getStyleAtPosition(s string, targetPos int) ansiStyle {
|
||||
if len(parsedStyle.attrs) > 0 {
|
||||
currentStyle.attrs = parsedStyle.attrs
|
||||
}
|
||||
|
||||
|
||||
i += match[1]
|
||||
} else if i < len(s) {
|
||||
// Regular character
|
||||
@@ -298,7 +298,7 @@ func getStyleAtPosition(s string, targetPos int) ansiStyle {
|
||||
visualPos++
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return currentStyle
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ func BaseStyle() lipgloss.Style {
|
||||
func Panel() lipgloss.Style {
|
||||
t := theme.CurrentTheme()
|
||||
return lipgloss.NewStyle().
|
||||
Background(t.BackgroundSubtle()).
|
||||
Background(t.BackgroundPanel()).
|
||||
Border(lipgloss.NormalBorder(), true, false, true, false).
|
||||
BorderForeground(t.BorderSubtle()).
|
||||
Foreground(t.Text())
|
||||
@@ -131,9 +131,9 @@ func BackgroundColor() compat.AdaptiveColor {
|
||||
return theme.CurrentTheme().Background()
|
||||
}
|
||||
|
||||
// BackgroundSubtleColor returns the subtle background color from the current theme
|
||||
func BackgroundSubtleColor() compat.AdaptiveColor {
|
||||
return theme.CurrentTheme().BackgroundSubtle()
|
||||
// BackgroundPanelColor returns the subtle background color from the current theme
|
||||
func BackgroundPanelColor() compat.AdaptiveColor {
|
||||
return theme.CurrentTheme().BackgroundPanel()
|
||||
}
|
||||
|
||||
// BackgroundElementColor returns the darker background color from the current theme
|
||||
|
||||
@@ -1,276 +0,0 @@
|
||||
package theme
|
||||
|
||||
import (
|
||||
"github.com/charmbracelet/lipgloss/v2"
|
||||
"github.com/charmbracelet/lipgloss/v2/compat"
|
||||
)
|
||||
|
||||
// AyuTheme implements the Theme interface with Ayu Dark colors.
|
||||
// It provides a modern dark theme inspired by the Ayu color scheme.
|
||||
type AyuTheme struct {
|
||||
BaseTheme
|
||||
}
|
||||
|
||||
// NewAyuTheme creates a new instance of the Ayu Dark theme.
|
||||
func NewAyuTheme() *AyuTheme {
|
||||
// Ayu Dark color palette
|
||||
// Base background colors
|
||||
darkBg := "#0B0E14" // App background
|
||||
darkBgAlt := "#0D1017" // Editor background
|
||||
darkLine := "#11151C" // UI line separators
|
||||
darkPanel := "#0F131A" // UI panel background
|
||||
|
||||
// Text colors
|
||||
darkFg := "#BFBDB6" // Primary text
|
||||
darkFgMuted := "#565B66" // Muted text
|
||||
darkGutter := "#6C7380" // Gutter text
|
||||
|
||||
// Syntax highlighting colors
|
||||
darkTag := "#39BAE6" // Tags and attributes
|
||||
darkFunc := "#FFB454" // Functions
|
||||
darkEntity := "#59C2FF" // Entities and variables
|
||||
darkString := "#AAD94C" // Strings
|
||||
darkRegexp := "#95E6CB" // Regular expressions
|
||||
darkMarkup := "#F07178" // Markup elements
|
||||
darkKeyword := "#FF8F40" // Keywords
|
||||
darkSpecial := "#E6B673" // Special characters
|
||||
darkComment := "#ACB6BF" // Comments
|
||||
darkConstant := "#D2A6FF" // Constants
|
||||
darkOperator := "#F29668" // Operators
|
||||
|
||||
// Version control colors
|
||||
darkAdded := "#7FD962" // Added lines
|
||||
darkRemoved := "#F26D78" // Removed lines
|
||||
|
||||
// Accent colors
|
||||
darkAccent := "#E6B450" // Primary accent
|
||||
darkError := "#D95757" // Error color
|
||||
|
||||
// Active state colors
|
||||
darkIndentActive := "#6C7380" // Active indent guides
|
||||
|
||||
theme := &AyuTheme{}
|
||||
|
||||
// Base colors
|
||||
theme.PrimaryColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkEntity),
|
||||
Light: lipgloss.Color(darkEntity),
|
||||
}
|
||||
theme.SecondaryColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkConstant),
|
||||
Light: lipgloss.Color(darkConstant),
|
||||
}
|
||||
theme.AccentColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkAccent),
|
||||
Light: lipgloss.Color(darkAccent),
|
||||
}
|
||||
|
||||
// Status colors
|
||||
theme.ErrorColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkError),
|
||||
Light: lipgloss.Color(darkError),
|
||||
}
|
||||
theme.WarningColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkSpecial),
|
||||
Light: lipgloss.Color(darkSpecial),
|
||||
}
|
||||
theme.SuccessColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkAdded),
|
||||
Light: lipgloss.Color(darkAdded),
|
||||
}
|
||||
theme.InfoColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkTag),
|
||||
Light: lipgloss.Color(darkTag),
|
||||
}
|
||||
|
||||
// Text colors
|
||||
theme.TextColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkFg),
|
||||
Light: lipgloss.Color(darkFg),
|
||||
}
|
||||
theme.TextMutedColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkFgMuted),
|
||||
Light: lipgloss.Color(darkFgMuted),
|
||||
}
|
||||
|
||||
// Background colors
|
||||
theme.BackgroundColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkBg),
|
||||
Light: lipgloss.Color(darkBg),
|
||||
}
|
||||
theme.BackgroundSubtleColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkBgAlt),
|
||||
Light: lipgloss.Color(darkBgAlt),
|
||||
}
|
||||
theme.BackgroundElementColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkPanel),
|
||||
Light: lipgloss.Color(darkPanel),
|
||||
}
|
||||
|
||||
// Border colors
|
||||
theme.BorderColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkGutter),
|
||||
Light: lipgloss.Color(darkGutter),
|
||||
}
|
||||
theme.BorderActiveColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkIndentActive),
|
||||
Light: lipgloss.Color(darkIndentActive),
|
||||
}
|
||||
theme.BorderSubtleColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkLine),
|
||||
Light: lipgloss.Color(darkLine),
|
||||
}
|
||||
|
||||
// Diff view colors
|
||||
theme.DiffAddedColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkAdded),
|
||||
Light: lipgloss.Color(darkAdded),
|
||||
}
|
||||
theme.DiffRemovedColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkRemoved),
|
||||
Light: lipgloss.Color(darkRemoved),
|
||||
}
|
||||
theme.DiffContextColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkFgMuted),
|
||||
Light: lipgloss.Color(darkFgMuted),
|
||||
}
|
||||
theme.DiffHunkHeaderColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkGutter),
|
||||
Light: lipgloss.Color(darkGutter),
|
||||
}
|
||||
theme.DiffHighlightAddedColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkAdded),
|
||||
Light: lipgloss.Color(darkAdded),
|
||||
}
|
||||
theme.DiffHighlightRemovedColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkRemoved),
|
||||
Light: lipgloss.Color(darkRemoved),
|
||||
}
|
||||
theme.DiffAddedBgColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color("#1a2b1a"),
|
||||
Light: lipgloss.Color("#1a2b1a"),
|
||||
}
|
||||
theme.DiffRemovedBgColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color("#2b1a1a"),
|
||||
Light: lipgloss.Color("#2b1a1a"),
|
||||
}
|
||||
theme.DiffContextBgColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkBgAlt),
|
||||
Light: lipgloss.Color(darkBgAlt),
|
||||
}
|
||||
theme.DiffLineNumberColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkGutter),
|
||||
Light: lipgloss.Color(darkGutter),
|
||||
}
|
||||
theme.DiffAddedLineNumberBgColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color("#152b15"),
|
||||
Light: lipgloss.Color("#152b15"),
|
||||
}
|
||||
theme.DiffRemovedLineNumberBgColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color("#2b1515"),
|
||||
Light: lipgloss.Color("#2b1515"),
|
||||
}
|
||||
|
||||
// Markdown colors
|
||||
theme.MarkdownTextColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkFg),
|
||||
Light: lipgloss.Color(darkFg),
|
||||
}
|
||||
theme.MarkdownHeadingColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkFunc),
|
||||
Light: lipgloss.Color(darkFunc),
|
||||
}
|
||||
theme.MarkdownLinkColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkTag),
|
||||
Light: lipgloss.Color(darkTag),
|
||||
}
|
||||
theme.MarkdownLinkTextColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkEntity),
|
||||
Light: lipgloss.Color(darkEntity),
|
||||
}
|
||||
theme.MarkdownCodeColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkString),
|
||||
Light: lipgloss.Color(darkString),
|
||||
}
|
||||
theme.MarkdownBlockQuoteColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkSpecial),
|
||||
Light: lipgloss.Color(darkSpecial),
|
||||
}
|
||||
theme.MarkdownEmphColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkKeyword),
|
||||
Light: lipgloss.Color(darkKeyword),
|
||||
}
|
||||
theme.MarkdownStrongColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkMarkup),
|
||||
Light: lipgloss.Color(darkMarkup),
|
||||
}
|
||||
theme.MarkdownHorizontalRuleColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkGutter),
|
||||
Light: lipgloss.Color(darkGutter),
|
||||
}
|
||||
theme.MarkdownListItemColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkOperator),
|
||||
Light: lipgloss.Color(darkOperator),
|
||||
}
|
||||
theme.MarkdownListEnumerationColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkConstant),
|
||||
Light: lipgloss.Color(darkConstant),
|
||||
}
|
||||
theme.MarkdownImageColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkRegexp),
|
||||
Light: lipgloss.Color(darkRegexp),
|
||||
}
|
||||
theme.MarkdownImageTextColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkEntity),
|
||||
Light: lipgloss.Color(darkEntity),
|
||||
}
|
||||
theme.MarkdownCodeBlockColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkString),
|
||||
Light: lipgloss.Color(darkString),
|
||||
}
|
||||
|
||||
// Syntax highlighting colors
|
||||
theme.SyntaxCommentColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkComment),
|
||||
Light: lipgloss.Color(darkComment),
|
||||
}
|
||||
theme.SyntaxKeywordColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkKeyword),
|
||||
Light: lipgloss.Color(darkKeyword),
|
||||
}
|
||||
theme.SyntaxFunctionColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkFunc),
|
||||
Light: lipgloss.Color(darkFunc),
|
||||
}
|
||||
theme.SyntaxVariableColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkEntity),
|
||||
Light: lipgloss.Color(darkEntity),
|
||||
}
|
||||
theme.SyntaxStringColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkString),
|
||||
Light: lipgloss.Color(darkString),
|
||||
}
|
||||
theme.SyntaxNumberColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkConstant),
|
||||
Light: lipgloss.Color(darkConstant),
|
||||
}
|
||||
theme.SyntaxTypeColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkSpecial),
|
||||
Light: lipgloss.Color(darkSpecial),
|
||||
}
|
||||
theme.SyntaxOperatorColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkOperator),
|
||||
Light: lipgloss.Color(darkOperator),
|
||||
}
|
||||
theme.SyntaxPunctuationColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkFg),
|
||||
Light: lipgloss.Color(darkFg),
|
||||
}
|
||||
|
||||
return theme
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Register the Ayu theme with the theme manager
|
||||
RegisterTheme("ayu", NewAyuTheme())
|
||||
}
|
||||
@@ -1,298 +0,0 @@
|
||||
package theme
|
||||
|
||||
import (
|
||||
"github.com/charmbracelet/lipgloss/v2"
|
||||
"github.com/charmbracelet/lipgloss/v2/compat"
|
||||
)
|
||||
|
||||
// EverforestTheme implements the Theme interface with Everforest colors.
|
||||
// It provides both dark and light variants with Medium (default) contrast.
|
||||
type EverforestTheme struct {
|
||||
BaseTheme
|
||||
}
|
||||
|
||||
// NewEverforestTheme creates a new instance of the Everforest Medium theme.
|
||||
func NewEverforestTheme() *EverforestTheme {
|
||||
// Everforest color palette - Medium variant
|
||||
// Official colors from https://github.com/sainnhe/everforest/wiki
|
||||
// Dark mode colors - using Everforest:Dark Medium contrast palette
|
||||
darkStep1 := "#2d353b" // App background
|
||||
darkStep2 := "#333c43" // Subtle background
|
||||
darkStep3 := "#343f44" // UI element background
|
||||
darkStep4 := "#3d484d" // Hovered UI element background
|
||||
darkStep5 := "#475258" // Active/Selected UI element background
|
||||
darkStep6 := "#7a8478" // Subtle borders and separators
|
||||
darkStep7 := "#859289" // UI element border and focus rings
|
||||
darkStep8 := "#9da9a0" // Hovered UI element border
|
||||
darkStep9 := "#a7c080" // Solid backgrounds
|
||||
darkStep10 := "#83c092" // Hovered solid backgrounds
|
||||
darkStep11 := "#7a8478" // Low-contrast text
|
||||
darkStep12 := "#d3c6aa" // High-contrast text
|
||||
|
||||
// Dark mode accent colors
|
||||
darkPrimary := darkStep9 // Primary uses step 9 (green)
|
||||
darkSecondary := "#7fbbb3" // Secondary (blue)
|
||||
darkAccent := "#d699b6" // Accent (purple)
|
||||
darkRed := "#e67e80" // Error (red)
|
||||
darkOrange := "#e69875" // Warning (orange)
|
||||
darkGreen := "#a7c080" // Success (green)
|
||||
darkCyan := "#83c092" // Info (aqua)
|
||||
darkYellow := "#dbbc7f" // Emphasized text
|
||||
|
||||
// Light mode colors for the Everforest:Light Medium contrast palette
|
||||
lightStep1 := "#fdf6e3" // App background
|
||||
lightStep2 := "#efebd4" // Subtle background
|
||||
lightStep3 := "#f4f0d9" // UI element background
|
||||
lightStep4 := "#efebd4" // Hovered UI element background
|
||||
lightStep5 := "#e6e2cc" // Active/Selected UI element background
|
||||
lightStep6 := "#a6b0a0" // Subtle borders and separators
|
||||
lightStep7 := "#939f91" // UI element border and focus rings
|
||||
lightStep8 := "#829181" // Hovered UI element border
|
||||
lightStep9 := "#8da101" // Solid backgrounds
|
||||
lightStep10 := "#35a77c" // Hovered solid backgrounds
|
||||
lightStep11 := "#a6b0a0" // Low-contrast text
|
||||
lightStep12 := "#5c6a72" // High-contrast text
|
||||
|
||||
// Light mode accent colors
|
||||
lightPrimary := lightStep9 // Primary uses step 9 (green)
|
||||
lightSecondary := "#3a94c5" // Secondary blue
|
||||
lightAccent := "#df69ba" // Accent purple
|
||||
lightRed := "#f85552" // Error red
|
||||
lightOrange := "#f57d26" // Warning orange
|
||||
lightGreen := "#8da101" // Success green
|
||||
lightCyan := "#35a77c" // Info aqua
|
||||
lightYellow := "#dfa000" // Emphasized text
|
||||
|
||||
// Unused variables. These could be used for hover states
|
||||
_ = darkStep4
|
||||
_ = darkStep5
|
||||
_ = darkStep10
|
||||
_ = lightStep4
|
||||
_ = lightStep5
|
||||
_ = lightStep10
|
||||
|
||||
theme := &EverforestTheme{}
|
||||
|
||||
// Base colors
|
||||
theme.PrimaryColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkPrimary),
|
||||
Light: lipgloss.Color(lightPrimary),
|
||||
}
|
||||
theme.SecondaryColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkSecondary),
|
||||
Light: lipgloss.Color(lightSecondary),
|
||||
}
|
||||
theme.AccentColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkAccent),
|
||||
Light: lipgloss.Color(lightAccent),
|
||||
}
|
||||
|
||||
// Status colors
|
||||
theme.ErrorColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkRed),
|
||||
Light: lipgloss.Color(lightRed),
|
||||
}
|
||||
theme.WarningColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkOrange),
|
||||
Light: lipgloss.Color(lightOrange),
|
||||
}
|
||||
theme.SuccessColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkGreen),
|
||||
Light: lipgloss.Color(lightGreen),
|
||||
}
|
||||
theme.InfoColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkCyan),
|
||||
Light: lipgloss.Color(lightCyan),
|
||||
}
|
||||
|
||||
// Text colors
|
||||
theme.TextColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkStep12),
|
||||
Light: lipgloss.Color(lightStep12),
|
||||
}
|
||||
theme.TextMutedColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkStep11),
|
||||
Light: lipgloss.Color(lightStep11),
|
||||
}
|
||||
|
||||
// Background colors
|
||||
theme.BackgroundColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkStep1),
|
||||
Light: lipgloss.Color(lightStep1),
|
||||
}
|
||||
theme.BackgroundSubtleColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkStep2),
|
||||
Light: lipgloss.Color(lightStep2),
|
||||
}
|
||||
theme.BackgroundElementColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkStep3),
|
||||
Light: lipgloss.Color(lightStep3),
|
||||
}
|
||||
|
||||
// Border colors
|
||||
theme.BorderColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkStep7),
|
||||
Light: lipgloss.Color(lightStep7),
|
||||
}
|
||||
theme.BorderActiveColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkStep8),
|
||||
Light: lipgloss.Color(lightStep8),
|
||||
}
|
||||
theme.BorderSubtleColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkStep6),
|
||||
Light: lipgloss.Color(lightStep6),
|
||||
}
|
||||
|
||||
// Diff view colors
|
||||
theme.DiffAddedColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color("#A7C080"),
|
||||
Light: lipgloss.Color("#8DA101"),
|
||||
}
|
||||
theme.DiffRemovedColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color("#E67E80"),
|
||||
Light: lipgloss.Color("#F85552"),
|
||||
}
|
||||
theme.DiffContextColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color("#7A8478"),
|
||||
Light: lipgloss.Color("#A6B0A0"),
|
||||
}
|
||||
theme.DiffHunkHeaderColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color("#859289"),
|
||||
Light: lipgloss.Color("#939F91"),
|
||||
}
|
||||
theme.DiffHighlightAddedColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color("#A7C080"),
|
||||
Light: lipgloss.Color("#8DA101"),
|
||||
}
|
||||
theme.DiffHighlightRemovedColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color("#E67E80"),
|
||||
Light: lipgloss.Color("#F85552"),
|
||||
}
|
||||
theme.DiffAddedBgColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color("#425047"),
|
||||
Light: lipgloss.Color("#F0F1D2"),
|
||||
}
|
||||
theme.DiffRemovedBgColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color("#543A48"),
|
||||
Light: lipgloss.Color("#FBE3DA"),
|
||||
}
|
||||
theme.DiffContextBgColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkStep2),
|
||||
Light: lipgloss.Color(lightStep2),
|
||||
}
|
||||
theme.DiffLineNumberColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkStep3),
|
||||
Light: lipgloss.Color(lightStep3),
|
||||
}
|
||||
theme.DiffAddedLineNumberBgColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color("#3A4A3F"),
|
||||
Light: lipgloss.Color("#E8F2D1"),
|
||||
}
|
||||
theme.DiffRemovedLineNumberBgColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color("#4A3A40"),
|
||||
Light: lipgloss.Color("#FBDAD2"),
|
||||
}
|
||||
|
||||
// Markdown colors
|
||||
theme.MarkdownTextColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkStep12),
|
||||
Light: lipgloss.Color(lightStep12),
|
||||
}
|
||||
theme.MarkdownHeadingColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkSecondary),
|
||||
Light: lipgloss.Color(lightSecondary),
|
||||
}
|
||||
theme.MarkdownLinkColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkPrimary),
|
||||
Light: lipgloss.Color(lightPrimary),
|
||||
}
|
||||
theme.MarkdownLinkTextColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkCyan),
|
||||
Light: lipgloss.Color(lightCyan),
|
||||
}
|
||||
theme.MarkdownCodeColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkGreen),
|
||||
Light: lipgloss.Color(lightGreen),
|
||||
}
|
||||
theme.MarkdownBlockQuoteColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkYellow),
|
||||
Light: lipgloss.Color(lightYellow),
|
||||
}
|
||||
theme.MarkdownEmphColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkYellow),
|
||||
Light: lipgloss.Color(lightYellow),
|
||||
}
|
||||
theme.MarkdownStrongColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkAccent),
|
||||
Light: lipgloss.Color(lightAccent),
|
||||
}
|
||||
theme.MarkdownHorizontalRuleColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkStep11),
|
||||
Light: lipgloss.Color(lightStep11),
|
||||
}
|
||||
theme.MarkdownListItemColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkPrimary),
|
||||
Light: lipgloss.Color(lightPrimary),
|
||||
}
|
||||
theme.MarkdownListEnumerationColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkCyan),
|
||||
Light: lipgloss.Color(lightCyan),
|
||||
}
|
||||
theme.MarkdownImageColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkPrimary),
|
||||
Light: lipgloss.Color(lightPrimary),
|
||||
}
|
||||
theme.MarkdownImageTextColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkCyan),
|
||||
Light: lipgloss.Color(lightCyan),
|
||||
}
|
||||
theme.MarkdownCodeBlockColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkStep12),
|
||||
Light: lipgloss.Color(lightStep12),
|
||||
}
|
||||
|
||||
// Syntax highlighting colors
|
||||
theme.SyntaxCommentColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkStep11),
|
||||
Light: lipgloss.Color(lightStep11),
|
||||
}
|
||||
theme.SyntaxKeywordColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkPrimary),
|
||||
Light: lipgloss.Color(lightPrimary),
|
||||
}
|
||||
theme.SyntaxFunctionColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkSecondary),
|
||||
Light: lipgloss.Color(lightSecondary),
|
||||
}
|
||||
theme.SyntaxVariableColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkRed),
|
||||
Light: lipgloss.Color(lightRed),
|
||||
}
|
||||
theme.SyntaxStringColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkGreen),
|
||||
Light: lipgloss.Color(lightGreen),
|
||||
}
|
||||
theme.SyntaxNumberColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkAccent),
|
||||
Light: lipgloss.Color(lightAccent),
|
||||
}
|
||||
theme.SyntaxTypeColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkYellow),
|
||||
Light: lipgloss.Color(lightYellow),
|
||||
}
|
||||
theme.SyntaxOperatorColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkCyan),
|
||||
Light: lipgloss.Color(lightCyan),
|
||||
}
|
||||
theme.SyntaxPunctuationColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkStep12),
|
||||
Light: lipgloss.Color(lightStep12),
|
||||
}
|
||||
|
||||
return theme
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Register the Everforest theme with the theme manager
|
||||
RegisterTheme("everforest", NewEverforestTheme())
|
||||
}
|
||||
@@ -0,0 +1,394 @@
|
||||
package theme
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"image/color"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/lipgloss/v2"
|
||||
"github.com/charmbracelet/lipgloss/v2/compat"
|
||||
)
|
||||
|
||||
//go:embed themes/*.json
|
||||
var themesFS embed.FS
|
||||
|
||||
type JSONTheme struct {
|
||||
Defs map[string]any `json:"defs,omitempty"`
|
||||
Theme map[string]any `json:"theme"`
|
||||
}
|
||||
|
||||
type LoadedTheme struct {
|
||||
BaseTheme
|
||||
name string
|
||||
}
|
||||
|
||||
type colorRef struct {
|
||||
value any
|
||||
resolved bool
|
||||
}
|
||||
|
||||
func LoadThemesFromJSON() error {
|
||||
entries, err := themesFS.ReadDir("themes")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read themes directory: %w", err)
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if !strings.HasSuffix(entry.Name(), ".json") {
|
||||
continue
|
||||
}
|
||||
themeName := strings.TrimSuffix(entry.Name(), ".json")
|
||||
data, err := themesFS.ReadFile(filepath.Join("themes", entry.Name()))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read theme file %s: %w", entry.Name(), err)
|
||||
}
|
||||
theme, err := parseJSONTheme(themeName, data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse theme %s: %w", themeName, err)
|
||||
}
|
||||
RegisterTheme(themeName, theme)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadThemesFromDirectories loads themes from user directories in the correct override order.
|
||||
// The hierarchy is (from lowest to highest priority):
|
||||
// 1. Built-in themes (embedded)
|
||||
// 2. USER_CONFIG/opencode/themes/*.json
|
||||
// 3. PROJECT_ROOT/.opencode/themes/*.json
|
||||
// 4. CWD/.opencode/themes/*.json
|
||||
func LoadThemesFromDirectories(userConfig, projectRoot, cwd string) error {
|
||||
if err := LoadThemesFromJSON(); err != nil {
|
||||
return fmt.Errorf("failed to load built-in themes: %w", err)
|
||||
}
|
||||
|
||||
dirs := []string{
|
||||
filepath.Join(userConfig, "themes"),
|
||||
filepath.Join(projectRoot, ".opencode", "themes"),
|
||||
}
|
||||
if cwd != projectRoot {
|
||||
dirs = append(dirs, filepath.Join(cwd, ".opencode", "themes"))
|
||||
}
|
||||
|
||||
for _, dir := range dirs {
|
||||
if err := loadThemesFromDirectory(dir); err != nil {
|
||||
fmt.Printf("Warning: Failed to load themes from %s: %v\n", dir, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadThemesFromDirectory(dir string) error {
|
||||
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||
return nil // Directory doesn't exist, which is fine
|
||||
}
|
||||
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read directory: %w", err)
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".json") {
|
||||
continue
|
||||
}
|
||||
|
||||
themeName := strings.TrimSuffix(entry.Name(), ".json")
|
||||
filePath := filepath.Join(dir, entry.Name())
|
||||
|
||||
data, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
fmt.Printf("Warning: Failed to read theme file %s: %v\n", filePath, err)
|
||||
continue
|
||||
}
|
||||
|
||||
theme, err := parseJSONTheme(themeName, data)
|
||||
if err != nil {
|
||||
fmt.Printf("Warning: Failed to parse theme %s: %v\n", filePath, err)
|
||||
continue
|
||||
}
|
||||
|
||||
RegisterTheme(themeName, theme)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseJSONTheme(name string, data []byte) (Theme, error) {
|
||||
var jsonTheme JSONTheme
|
||||
if err := json.Unmarshal(data, &jsonTheme); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal JSON: %w", err)
|
||||
}
|
||||
theme := &LoadedTheme{
|
||||
name: name,
|
||||
}
|
||||
colorMap := make(map[string]*colorRef)
|
||||
for key, value := range jsonTheme.Defs {
|
||||
colorMap[key] = &colorRef{value: value, resolved: false}
|
||||
}
|
||||
for key, value := range jsonTheme.Theme {
|
||||
colorMap[key] = &colorRef{value: value, resolved: false}
|
||||
}
|
||||
resolver := &colorResolver{
|
||||
colors: colorMap,
|
||||
visited: make(map[string]bool),
|
||||
}
|
||||
for key, value := range jsonTheme.Theme {
|
||||
resolved, err := resolver.resolveColor(key, value)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to resolve color %s: %w", key, err)
|
||||
}
|
||||
adaptiveColor, err := parseResolvedColor(resolved)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse color %s: %w", key, err)
|
||||
}
|
||||
if err := setThemeColor(theme, key, adaptiveColor); err != nil {
|
||||
return nil, fmt.Errorf("failed to set color %s: %w", key, err)
|
||||
}
|
||||
}
|
||||
|
||||
return theme, nil
|
||||
}
|
||||
|
||||
type colorResolver struct {
|
||||
colors map[string]*colorRef
|
||||
visited map[string]bool
|
||||
}
|
||||
|
||||
func (r *colorResolver) resolveColor(key string, value any) (any, error) {
|
||||
if r.visited[key] {
|
||||
return nil, fmt.Errorf("circular reference detected for color %s", key)
|
||||
}
|
||||
r.visited[key] = true
|
||||
defer func() { r.visited[key] = false }()
|
||||
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
if strings.HasPrefix(v, "#") {
|
||||
return v, nil
|
||||
}
|
||||
return r.resolveReference(v)
|
||||
case float64:
|
||||
return v, nil
|
||||
case map[string]any:
|
||||
resolved := make(map[string]any)
|
||||
|
||||
if dark, ok := v["dark"]; ok {
|
||||
resolvedDark, err := r.resolveColorValue(dark)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to resolve dark variant: %w", err)
|
||||
}
|
||||
resolved["dark"] = resolvedDark
|
||||
}
|
||||
|
||||
if light, ok := v["light"]; ok {
|
||||
resolvedLight, err := r.resolveColorValue(light)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to resolve light variant: %w", err)
|
||||
}
|
||||
resolved["light"] = resolvedLight
|
||||
}
|
||||
|
||||
return resolved, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid color value type: %T", value)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *colorResolver) resolveColorValue(value any) (any, error) {
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
if strings.HasPrefix(v, "#") {
|
||||
return v, nil
|
||||
}
|
||||
return r.resolveReference(v)
|
||||
case float64:
|
||||
return v, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid color value type: %T", value)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *colorResolver) resolveReference(ref string) (any, error) {
|
||||
colorRef, exists := r.colors[ref]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("color reference '%s' not found", ref)
|
||||
}
|
||||
|
||||
if colorRef.resolved {
|
||||
return colorRef.value, nil
|
||||
}
|
||||
|
||||
resolved, err := r.resolveColor(ref, colorRef.value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
colorRef.value = resolved
|
||||
colorRef.resolved = true
|
||||
|
||||
return resolved, nil
|
||||
}
|
||||
|
||||
func parseResolvedColor(value any) (compat.AdaptiveColor, error) {
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
return compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(v),
|
||||
Light: lipgloss.Color(v),
|
||||
}, nil
|
||||
case float64:
|
||||
colorStr := fmt.Sprintf("%d", int(v))
|
||||
return compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(colorStr),
|
||||
Light: lipgloss.Color(colorStr),
|
||||
}, nil
|
||||
case map[string]any:
|
||||
dark, darkOk := v["dark"]
|
||||
light, lightOk := v["light"]
|
||||
|
||||
if !darkOk || !lightOk {
|
||||
return compat.AdaptiveColor{}, fmt.Errorf("color object must have both 'dark' and 'light' keys")
|
||||
}
|
||||
darkColor, err := parseColorValue(dark)
|
||||
if err != nil {
|
||||
return compat.AdaptiveColor{}, fmt.Errorf("failed to parse dark color: %w", err)
|
||||
}
|
||||
lightColor, err := parseColorValue(light)
|
||||
if err != nil {
|
||||
return compat.AdaptiveColor{}, fmt.Errorf("failed to parse light color: %w", err)
|
||||
}
|
||||
return compat.AdaptiveColor{
|
||||
Dark: darkColor,
|
||||
Light: lightColor,
|
||||
}, nil
|
||||
default:
|
||||
return compat.AdaptiveColor{}, fmt.Errorf("invalid resolved color type: %T", value)
|
||||
}
|
||||
}
|
||||
|
||||
func parseColorValue(value any) (color.Color, error) {
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
return lipgloss.Color(v), nil
|
||||
case float64:
|
||||
return lipgloss.Color(fmt.Sprintf("%d", int(v))), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid color value type: %T", value)
|
||||
}
|
||||
}
|
||||
|
||||
func setThemeColor(theme *LoadedTheme, key string, color compat.AdaptiveColor) error {
|
||||
switch key {
|
||||
case "primary":
|
||||
theme.PrimaryColor = color
|
||||
case "secondary":
|
||||
theme.SecondaryColor = color
|
||||
case "accent":
|
||||
theme.AccentColor = color
|
||||
case "error":
|
||||
theme.ErrorColor = color
|
||||
case "warning":
|
||||
theme.WarningColor = color
|
||||
case "success":
|
||||
theme.SuccessColor = color
|
||||
case "info":
|
||||
theme.InfoColor = color
|
||||
case "text":
|
||||
theme.TextColor = color
|
||||
case "textMuted":
|
||||
theme.TextMutedColor = color
|
||||
case "background":
|
||||
theme.BackgroundColor = color
|
||||
case "backgroundPanel":
|
||||
theme.BackgroundPanelColor = color
|
||||
case "backgroundElement":
|
||||
theme.BackgroundElementColor = color
|
||||
case "border":
|
||||
theme.BorderColor = color
|
||||
case "borderActive":
|
||||
theme.BorderActiveColor = color
|
||||
case "borderSubtle":
|
||||
theme.BorderSubtleColor = color
|
||||
case "diffAdded":
|
||||
theme.DiffAddedColor = color
|
||||
case "diffRemoved":
|
||||
theme.DiffRemovedColor = color
|
||||
case "diffContext":
|
||||
theme.DiffContextColor = color
|
||||
case "diffHunkHeader":
|
||||
theme.DiffHunkHeaderColor = color
|
||||
case "diffHighlightAdded":
|
||||
theme.DiffHighlightAddedColor = color
|
||||
case "diffHighlightRemoved":
|
||||
theme.DiffHighlightRemovedColor = color
|
||||
case "diffAddedBg":
|
||||
theme.DiffAddedBgColor = color
|
||||
case "diffRemovedBg":
|
||||
theme.DiffRemovedBgColor = color
|
||||
case "diffContextBg":
|
||||
theme.DiffContextBgColor = color
|
||||
case "diffLineNumber":
|
||||
theme.DiffLineNumberColor = color
|
||||
case "diffAddedLineNumberBg":
|
||||
theme.DiffAddedLineNumberBgColor = color
|
||||
case "diffRemovedLineNumberBg":
|
||||
theme.DiffRemovedLineNumberBgColor = color
|
||||
case "markdownText":
|
||||
theme.MarkdownTextColor = color
|
||||
case "markdownHeading":
|
||||
theme.MarkdownHeadingColor = color
|
||||
case "markdownLink":
|
||||
theme.MarkdownLinkColor = color
|
||||
case "markdownLinkText":
|
||||
theme.MarkdownLinkTextColor = color
|
||||
case "markdownCode":
|
||||
theme.MarkdownCodeColor = color
|
||||
case "markdownBlockQuote":
|
||||
theme.MarkdownBlockQuoteColor = color
|
||||
case "markdownEmph":
|
||||
theme.MarkdownEmphColor = color
|
||||
case "markdownStrong":
|
||||
theme.MarkdownStrongColor = color
|
||||
case "markdownHorizontalRule":
|
||||
theme.MarkdownHorizontalRuleColor = color
|
||||
case "markdownListItem":
|
||||
theme.MarkdownListItemColor = color
|
||||
case "markdownListEnumeration":
|
||||
theme.MarkdownListEnumerationColor = color
|
||||
case "markdownImage":
|
||||
theme.MarkdownImageColor = color
|
||||
case "markdownImageText":
|
||||
theme.MarkdownImageTextColor = color
|
||||
case "markdownCodeBlock":
|
||||
theme.MarkdownCodeBlockColor = color
|
||||
case "syntaxComment":
|
||||
theme.SyntaxCommentColor = color
|
||||
case "syntaxKeyword":
|
||||
theme.SyntaxKeywordColor = color
|
||||
case "syntaxFunction":
|
||||
theme.SyntaxFunctionColor = color
|
||||
case "syntaxVariable":
|
||||
theme.SyntaxVariableColor = color
|
||||
case "syntaxString":
|
||||
theme.SyntaxStringColor = color
|
||||
case "syntaxNumber":
|
||||
theme.SyntaxNumberColor = color
|
||||
case "syntaxType":
|
||||
theme.SyntaxTypeColor = color
|
||||
case "syntaxOperator":
|
||||
theme.SyntaxOperatorColor = color
|
||||
case "syntaxPunctuation":
|
||||
theme.SyntaxPunctuationColor = color
|
||||
default:
|
||||
// Ignore unknown keys for forward compatibility
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
package theme
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLoadThemesFromJSON(t *testing.T) {
|
||||
// Test loading themes
|
||||
err := LoadThemesFromJSON()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to load themes: %v", err)
|
||||
}
|
||||
|
||||
// Check that themes were loaded
|
||||
themes := AvailableThemes()
|
||||
if len(themes) == 0 {
|
||||
t.Fatal("No themes were loaded")
|
||||
}
|
||||
|
||||
// Check for expected themes
|
||||
expectedThemes := []string{"tokyonight", "opencode", "everforest", "ayu", "example"}
|
||||
for _, expected := range expectedThemes {
|
||||
found := slices.Contains(themes, expected)
|
||||
if !found {
|
||||
t.Errorf("Expected theme %s not found", expected)
|
||||
}
|
||||
}
|
||||
|
||||
// Test getting a specific theme
|
||||
tokyonight := GetTheme("tokyonight")
|
||||
if tokyonight == nil {
|
||||
t.Fatal("Failed to get tokyonight theme")
|
||||
}
|
||||
|
||||
// Test theme colors
|
||||
primary := tokyonight.Primary()
|
||||
if primary.Dark == nil || primary.Light == nil {
|
||||
t.Error("Primary color not properly set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestColorReferenceResolution(t *testing.T) {
|
||||
// Test the example theme which uses references
|
||||
example := GetTheme("example")
|
||||
if example == nil {
|
||||
t.Fatal("Failed to get example theme")
|
||||
}
|
||||
|
||||
// Check that brandBlue reference was resolved
|
||||
primary := example.Primary()
|
||||
if primary.Dark == nil || primary.Light == nil {
|
||||
t.Error("Primary color (brandBlue reference) not resolved")
|
||||
}
|
||||
|
||||
// Check that nested reference (borderActive -> primary -> brandBlue) works
|
||||
borderActive := example.BorderActive()
|
||||
if borderActive.Dark == nil || borderActive.Light == nil {
|
||||
t.Error("BorderActive color (nested reference) not resolved")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadThemesFromDirectories(t *testing.T) {
|
||||
// Create temporary directories for testing
|
||||
tempDir := t.TempDir()
|
||||
|
||||
userConfig := filepath.Join(tempDir, "config")
|
||||
projectRoot := filepath.Join(tempDir, "project")
|
||||
cwd := filepath.Join(tempDir, "cwd")
|
||||
|
||||
// Create theme directories
|
||||
os.MkdirAll(filepath.Join(userConfig, "opencode", "themes"), 0755)
|
||||
os.MkdirAll(filepath.Join(projectRoot, ".opencode", "themes"), 0755)
|
||||
os.MkdirAll(filepath.Join(cwd, ".opencode", "themes"), 0755)
|
||||
|
||||
// Create test themes with same name to test override behavior
|
||||
testTheme1 := `{
|
||||
"theme": {
|
||||
"primary": "#111111",
|
||||
"secondary": "#222222",
|
||||
"accent": "#333333",
|
||||
"text": "#ffffff",
|
||||
"textMuted": "#cccccc",
|
||||
"background": "#000000"
|
||||
}
|
||||
}`
|
||||
|
||||
testTheme2 := `{
|
||||
"theme": {
|
||||
"primary": "#444444",
|
||||
"secondary": "#555555",
|
||||
"accent": "#666666",
|
||||
"text": "#ffffff",
|
||||
"textMuted": "#cccccc",
|
||||
"background": "#000000"
|
||||
}
|
||||
}`
|
||||
|
||||
testTheme3 := `{
|
||||
"theme": {
|
||||
"primary": "#777777",
|
||||
"secondary": "#888888",
|
||||
"accent": "#999999",
|
||||
"text": "#ffffff",
|
||||
"textMuted": "#cccccc",
|
||||
"background": "#000000"
|
||||
}
|
||||
}`
|
||||
|
||||
// Write themes to different directories
|
||||
os.WriteFile(filepath.Join(userConfig, "opencode", "themes", "override-test.json"), []byte(testTheme1), 0644)
|
||||
os.WriteFile(filepath.Join(projectRoot, ".opencode", "themes", "override-test.json"), []byte(testTheme2), 0644)
|
||||
os.WriteFile(filepath.Join(cwd, ".opencode", "themes", "override-test.json"), []byte(testTheme3), 0644)
|
||||
|
||||
// Load themes
|
||||
err := LoadThemesFromDirectories(userConfig, projectRoot, cwd)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to load themes from directories: %v", err)
|
||||
}
|
||||
|
||||
// Check that the theme from CWD (highest priority) won
|
||||
overrideTheme := GetTheme("override-test")
|
||||
if overrideTheme == nil {
|
||||
t.Fatal("Failed to get override-test theme")
|
||||
}
|
||||
|
||||
// The primary color should be from testTheme3 (#777777)
|
||||
primary := overrideTheme.Primary()
|
||||
// We can't directly check the color value, but we can verify it was loaded
|
||||
if primary.Dark == nil || primary.Light == nil {
|
||||
t.Error("Override theme not properly loaded")
|
||||
}
|
||||
}
|
||||
@@ -2,13 +2,11 @@ package theme
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/alecthomas/chroma/v2/styles"
|
||||
// "github.com/alecthomas/chroma/v2/styles"
|
||||
)
|
||||
|
||||
// Manager handles theme registration, selection, and retrieval.
|
||||
@@ -25,9 +23,6 @@ var globalManager = &Manager{
|
||||
currentName: "",
|
||||
}
|
||||
|
||||
// Default theme instance for custom theme defaulting
|
||||
var defaultThemeColors = NewOpenCodeTheme()
|
||||
|
||||
// RegisterTheme adds a new theme to the registry.
|
||||
// If this is the first theme registered, it becomes the default.
|
||||
func RegisterTheme(name string, theme Theme) {
|
||||
@@ -89,6 +84,7 @@ func AvailableThemes() []string {
|
||||
names = append(names, name)
|
||||
}
|
||||
slices.SortFunc(names, func(a, b string) int {
|
||||
// list system theme first
|
||||
if a == "opencode" {
|
||||
return -1
|
||||
} else if b == "opencode" {
|
||||
@@ -107,130 +103,3 @@ func GetTheme(name string) Theme {
|
||||
|
||||
return globalManager.themes[name]
|
||||
}
|
||||
|
||||
// LoadCustomTheme creates a new theme instance based on the custom theme colors
|
||||
// defined in the configuration. It uses the default OpenCode theme as a base
|
||||
// and overrides colors that are specified in the customTheme map.
|
||||
func LoadCustomTheme(customTheme map[string]any) (Theme, error) {
|
||||
// Create a new theme based on the default OpenCode theme
|
||||
theme := NewOpenCodeTheme()
|
||||
|
||||
// Process each color in the custom theme map
|
||||
for key, value := range customTheme {
|
||||
adaptiveColor, err := ParseAdaptiveColor(value)
|
||||
if err != nil {
|
||||
slog.Warn("Invalid color definition in custom theme", "key", key, "error", err)
|
||||
continue // Skip this color but continue processing others
|
||||
}
|
||||
|
||||
// Set the color in the theme based on the key
|
||||
switch strings.ToLower(key) {
|
||||
case "primary":
|
||||
theme.PrimaryColor = adaptiveColor
|
||||
case "secondary":
|
||||
theme.SecondaryColor = adaptiveColor
|
||||
case "accent":
|
||||
theme.AccentColor = adaptiveColor
|
||||
case "error":
|
||||
theme.ErrorColor = adaptiveColor
|
||||
case "warning":
|
||||
theme.WarningColor = adaptiveColor
|
||||
case "success":
|
||||
theme.SuccessColor = adaptiveColor
|
||||
case "info":
|
||||
theme.InfoColor = adaptiveColor
|
||||
case "text":
|
||||
theme.TextColor = adaptiveColor
|
||||
case "textmuted":
|
||||
theme.TextMutedColor = adaptiveColor
|
||||
case "background":
|
||||
theme.BackgroundColor = adaptiveColor
|
||||
case "backgroundsubtle":
|
||||
theme.BackgroundSubtleColor = adaptiveColor
|
||||
case "backgroundelement":
|
||||
theme.BackgroundElementColor = adaptiveColor
|
||||
case "border":
|
||||
theme.BorderColor = adaptiveColor
|
||||
case "borderactive":
|
||||
theme.BorderActiveColor = adaptiveColor
|
||||
case "bordersubtle":
|
||||
theme.BorderSubtleColor = adaptiveColor
|
||||
case "diffadded":
|
||||
theme.DiffAddedColor = adaptiveColor
|
||||
case "diffremoved":
|
||||
theme.DiffRemovedColor = adaptiveColor
|
||||
case "diffcontext":
|
||||
theme.DiffContextColor = adaptiveColor
|
||||
case "diffhunkheader":
|
||||
theme.DiffHunkHeaderColor = adaptiveColor
|
||||
case "diffhighlightadded":
|
||||
theme.DiffHighlightAddedColor = adaptiveColor
|
||||
case "diffhighlightremoved":
|
||||
theme.DiffHighlightRemovedColor = adaptiveColor
|
||||
case "diffaddedbg":
|
||||
theme.DiffAddedBgColor = adaptiveColor
|
||||
case "diffremovedbg":
|
||||
theme.DiffRemovedBgColor = adaptiveColor
|
||||
case "diffcontextbg":
|
||||
theme.DiffContextBgColor = adaptiveColor
|
||||
case "difflinenumber":
|
||||
theme.DiffLineNumberColor = adaptiveColor
|
||||
case "diffaddedlinenumberbg":
|
||||
theme.DiffAddedLineNumberBgColor = adaptiveColor
|
||||
case "diffremovedlinenumberbg":
|
||||
theme.DiffRemovedLineNumberBgColor = adaptiveColor
|
||||
case "syntaxcomment":
|
||||
theme.SyntaxCommentColor = adaptiveColor
|
||||
case "syntaxkeyword":
|
||||
theme.SyntaxKeywordColor = adaptiveColor
|
||||
case "syntaxfunction":
|
||||
theme.SyntaxFunctionColor = adaptiveColor
|
||||
case "syntaxvariable":
|
||||
theme.SyntaxVariableColor = adaptiveColor
|
||||
case "syntaxstring":
|
||||
theme.SyntaxStringColor = adaptiveColor
|
||||
case "syntaxnumber":
|
||||
theme.SyntaxNumberColor = adaptiveColor
|
||||
case "syntaxtype":
|
||||
theme.SyntaxTypeColor = adaptiveColor
|
||||
case "syntaxoperator":
|
||||
theme.SyntaxOperatorColor = adaptiveColor
|
||||
case "syntaxpunctuation":
|
||||
theme.SyntaxPunctuationColor = adaptiveColor
|
||||
case "markdowntext":
|
||||
theme.MarkdownTextColor = adaptiveColor
|
||||
case "markdownheading":
|
||||
theme.MarkdownHeadingColor = adaptiveColor
|
||||
case "markdownlink":
|
||||
theme.MarkdownLinkColor = adaptiveColor
|
||||
case "markdownlinktext":
|
||||
theme.MarkdownLinkTextColor = adaptiveColor
|
||||
case "markdowncode":
|
||||
theme.MarkdownCodeColor = adaptiveColor
|
||||
case "markdownblockquote":
|
||||
theme.MarkdownBlockQuoteColor = adaptiveColor
|
||||
case "markdownemph":
|
||||
theme.MarkdownEmphColor = adaptiveColor
|
||||
case "markdownstrong":
|
||||
theme.MarkdownStrongColor = adaptiveColor
|
||||
case "markdownhorizontalrule":
|
||||
theme.MarkdownHorizontalRuleColor = adaptiveColor
|
||||
case "markdownlistitem":
|
||||
theme.MarkdownListItemColor = adaptiveColor
|
||||
case "markdownlistitemenum":
|
||||
theme.MarkdownListEnumerationColor = adaptiveColor
|
||||
case "markdownimage":
|
||||
theme.MarkdownImageColor = adaptiveColor
|
||||
case "markdownimagetext":
|
||||
theme.MarkdownImageTextColor = adaptiveColor
|
||||
case "markdowncodeblock":
|
||||
theme.MarkdownCodeBlockColor = adaptiveColor
|
||||
case "markdownlistenumeration":
|
||||
theme.MarkdownListEnumerationColor = adaptiveColor
|
||||
default:
|
||||
slog.Warn("Unknown color key in custom theme", "key", key)
|
||||
}
|
||||
}
|
||||
|
||||
return theme, nil
|
||||
}
|
||||
|
||||
@@ -1,297 +0,0 @@
|
||||
package theme
|
||||
|
||||
import (
|
||||
"github.com/charmbracelet/lipgloss/v2"
|
||||
"github.com/charmbracelet/lipgloss/v2/compat"
|
||||
)
|
||||
|
||||
// OpenCodeTheme implements the Theme interface with OpenCode brand colors.
|
||||
// It provides both dark and light variants.
|
||||
type OpenCodeTheme struct {
|
||||
BaseTheme
|
||||
}
|
||||
|
||||
// NewOpenCodeTheme creates a new instance of the OpenCode theme.
|
||||
func NewOpenCodeTheme() *OpenCodeTheme {
|
||||
// OpenCode color palette with Radix-inspired scale progression
|
||||
// Dark mode colors - using a neutral gray scale as base
|
||||
darkStep1 := "#0a0a0a" // App background
|
||||
darkStep2 := "#141414" // Subtle background
|
||||
darkStep3 := "#1e1e1e" // UI element background
|
||||
darkStep4 := "#282828" // Hovered UI element background
|
||||
darkStep5 := "#323232" // Active/Selected UI element background
|
||||
darkStep6 := "#3c3c3c" // Subtle borders and separators
|
||||
darkStep7 := "#484848" // UI element border and focus rings
|
||||
darkStep8 := "#606060" // Hovered UI element border
|
||||
darkStep9 := "#fab283" // Solid backgrounds (primary orange/gold)
|
||||
darkStep10 := "#ffc09f" // Hovered solid backgrounds
|
||||
darkStep11 := "#808080" // Low-contrast text (more muted)
|
||||
darkStep12 := "#eeeeee" // High-contrast text
|
||||
|
||||
// Dark mode accent colors
|
||||
darkPrimary := darkStep9 // Primary uses step 9 (solid background)
|
||||
darkSecondary := "#5c9cf5" // Secondary blue
|
||||
darkAccent := "#9d7cd8" // Accent purple
|
||||
darkRed := "#e06c75" // Error red
|
||||
darkOrange := "#f5a742" // Warning orange
|
||||
darkGreen := "#7fd88f" // Success green
|
||||
darkCyan := "#56b6c2" // Info cyan
|
||||
darkYellow := "#e5c07b" // Emphasized text
|
||||
|
||||
// Light mode colors - using a neutral gray scale as base
|
||||
lightStep1 := "#ffffff" // App background
|
||||
lightStep2 := "#fafafa" // Subtle background
|
||||
lightStep3 := "#f5f5f5" // UI element background
|
||||
lightStep4 := "#ebebeb" // Hovered UI element background
|
||||
lightStep5 := "#e1e1e1" // Active/Selected UI element background
|
||||
lightStep6 := "#d4d4d4" // Subtle borders and separators
|
||||
lightStep7 := "#b8b8b8" // UI element border and focus rings
|
||||
lightStep8 := "#a0a0a0" // Hovered UI element border
|
||||
lightStep9 := "#3b7dd8" // Solid backgrounds (primary blue)
|
||||
lightStep10 := "#2968c3" // Hovered solid backgrounds
|
||||
lightStep11 := "#8a8a8a" // Low-contrast text (more muted)
|
||||
lightStep12 := "#1a1a1a" // High-contrast text
|
||||
|
||||
// Light mode accent colors
|
||||
lightPrimary := lightStep9 // Primary uses step 9 (solid background)
|
||||
lightSecondary := "#7b5bb6" // Secondary purple
|
||||
lightAccent := "#d68c27" // Accent orange/gold
|
||||
lightRed := "#d1383d" // Error red
|
||||
lightOrange := "#d68c27" // Warning orange
|
||||
lightGreen := "#3d9a57" // Success green
|
||||
lightCyan := "#318795" // Info cyan
|
||||
lightYellow := "#b0851f" // Emphasized text
|
||||
|
||||
// Unused variables to avoid compiler errors (these could be used for hover states)
|
||||
_ = darkStep4
|
||||
_ = darkStep5
|
||||
_ = darkStep10
|
||||
_ = lightStep4
|
||||
_ = lightStep5
|
||||
_ = lightStep10
|
||||
|
||||
theme := &OpenCodeTheme{}
|
||||
|
||||
// Base colors
|
||||
theme.PrimaryColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkPrimary),
|
||||
Light: lipgloss.Color(lightPrimary),
|
||||
}
|
||||
theme.SecondaryColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkSecondary),
|
||||
Light: lipgloss.Color(lightSecondary),
|
||||
}
|
||||
theme.AccentColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkAccent),
|
||||
Light: lipgloss.Color(lightAccent),
|
||||
}
|
||||
|
||||
// Status colors
|
||||
theme.ErrorColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkRed),
|
||||
Light: lipgloss.Color(lightRed),
|
||||
}
|
||||
theme.WarningColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkOrange),
|
||||
Light: lipgloss.Color(lightOrange),
|
||||
}
|
||||
theme.SuccessColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkGreen),
|
||||
Light: lipgloss.Color(lightGreen),
|
||||
}
|
||||
theme.InfoColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkCyan),
|
||||
Light: lipgloss.Color(lightCyan),
|
||||
}
|
||||
|
||||
// Text colors
|
||||
theme.TextColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkStep12),
|
||||
Light: lipgloss.Color(lightStep12),
|
||||
}
|
||||
theme.TextMutedColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkStep11),
|
||||
Light: lipgloss.Color(lightStep11),
|
||||
}
|
||||
|
||||
// Background colors
|
||||
theme.BackgroundColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkStep1),
|
||||
Light: lipgloss.Color(lightStep1),
|
||||
}
|
||||
theme.BackgroundSubtleColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkStep2),
|
||||
Light: lipgloss.Color(lightStep2),
|
||||
}
|
||||
theme.BackgroundElementColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkStep3),
|
||||
Light: lipgloss.Color(lightStep3),
|
||||
}
|
||||
|
||||
// Border colors
|
||||
theme.BorderColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkStep7),
|
||||
Light: lipgloss.Color(lightStep7),
|
||||
}
|
||||
theme.BorderActiveColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkStep8),
|
||||
Light: lipgloss.Color(lightStep8),
|
||||
}
|
||||
theme.BorderSubtleColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkStep6),
|
||||
Light: lipgloss.Color(lightStep6),
|
||||
}
|
||||
|
||||
// Diff view colors
|
||||
theme.DiffAddedColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color("#478247"),
|
||||
Light: lipgloss.Color("#2E7D32"),
|
||||
}
|
||||
theme.DiffRemovedColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color("#7C4444"),
|
||||
Light: lipgloss.Color("#C62828"),
|
||||
}
|
||||
theme.DiffContextColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color("#a0a0a0"),
|
||||
Light: lipgloss.Color("#757575"),
|
||||
}
|
||||
theme.DiffHunkHeaderColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color("#a0a0a0"),
|
||||
Light: lipgloss.Color("#757575"),
|
||||
}
|
||||
theme.DiffHighlightAddedColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color("#DAFADA"),
|
||||
Light: lipgloss.Color("#A5D6A7"),
|
||||
}
|
||||
theme.DiffHighlightRemovedColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color("#FADADD"),
|
||||
Light: lipgloss.Color("#EF9A9A"),
|
||||
}
|
||||
theme.DiffAddedBgColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color("#303A30"),
|
||||
Light: lipgloss.Color("#E8F5E9"),
|
||||
}
|
||||
theme.DiffRemovedBgColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color("#3A3030"),
|
||||
Light: lipgloss.Color("#FFEBEE"),
|
||||
}
|
||||
theme.DiffContextBgColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkStep2),
|
||||
Light: lipgloss.Color(lightStep2),
|
||||
}
|
||||
theme.DiffLineNumberColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkStep3),
|
||||
Light: lipgloss.Color(lightStep3),
|
||||
}
|
||||
theme.DiffAddedLineNumberBgColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color("#293229"),
|
||||
Light: lipgloss.Color("#C8E6C9"),
|
||||
}
|
||||
theme.DiffRemovedLineNumberBgColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color("#332929"),
|
||||
Light: lipgloss.Color("#FFCDD2"),
|
||||
}
|
||||
|
||||
// Markdown colors
|
||||
theme.MarkdownTextColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkStep12),
|
||||
Light: lipgloss.Color(lightStep12),
|
||||
}
|
||||
theme.MarkdownHeadingColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkSecondary),
|
||||
Light: lipgloss.Color(lightSecondary),
|
||||
}
|
||||
theme.MarkdownLinkColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkPrimary),
|
||||
Light: lipgloss.Color(lightPrimary),
|
||||
}
|
||||
theme.MarkdownLinkTextColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkCyan),
|
||||
Light: lipgloss.Color(lightCyan),
|
||||
}
|
||||
theme.MarkdownCodeColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkGreen),
|
||||
Light: lipgloss.Color(lightGreen),
|
||||
}
|
||||
theme.MarkdownBlockQuoteColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkYellow),
|
||||
Light: lipgloss.Color(lightYellow),
|
||||
}
|
||||
theme.MarkdownEmphColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkYellow),
|
||||
Light: lipgloss.Color(lightYellow),
|
||||
}
|
||||
theme.MarkdownStrongColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkAccent),
|
||||
Light: lipgloss.Color(lightAccent),
|
||||
}
|
||||
theme.MarkdownHorizontalRuleColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkStep11),
|
||||
Light: lipgloss.Color(lightStep11),
|
||||
}
|
||||
theme.MarkdownListItemColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkPrimary),
|
||||
Light: lipgloss.Color(lightPrimary),
|
||||
}
|
||||
theme.MarkdownListEnumerationColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkCyan),
|
||||
Light: lipgloss.Color(lightCyan),
|
||||
}
|
||||
theme.MarkdownImageColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkPrimary),
|
||||
Light: lipgloss.Color(lightPrimary),
|
||||
}
|
||||
theme.MarkdownImageTextColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkCyan),
|
||||
Light: lipgloss.Color(lightCyan),
|
||||
}
|
||||
theme.MarkdownCodeBlockColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkStep12),
|
||||
Light: lipgloss.Color(lightStep12),
|
||||
}
|
||||
|
||||
// Syntax highlighting colors
|
||||
theme.SyntaxCommentColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkStep11),
|
||||
Light: lipgloss.Color(lightStep11),
|
||||
}
|
||||
theme.SyntaxKeywordColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkPrimary),
|
||||
Light: lipgloss.Color(lightPrimary),
|
||||
}
|
||||
theme.SyntaxFunctionColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkPrimary),
|
||||
Light: lipgloss.Color(lightPrimary),
|
||||
}
|
||||
theme.SyntaxVariableColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkRed),
|
||||
Light: lipgloss.Color(lightRed),
|
||||
}
|
||||
theme.SyntaxStringColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkGreen),
|
||||
Light: lipgloss.Color(lightGreen),
|
||||
}
|
||||
theme.SyntaxNumberColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkAccent),
|
||||
Light: lipgloss.Color(lightAccent),
|
||||
}
|
||||
theme.SyntaxTypeColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkYellow),
|
||||
Light: lipgloss.Color(lightYellow),
|
||||
}
|
||||
theme.SyntaxOperatorColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkCyan),
|
||||
Light: lipgloss.Color(lightCyan),
|
||||
}
|
||||
theme.SyntaxPunctuationColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkStep12),
|
||||
Light: lipgloss.Color(lightStep12),
|
||||
}
|
||||
|
||||
return theme
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Register the OpenCode theme with the theme manager
|
||||
RegisterTheme("opencode", NewOpenCodeTheme())
|
||||
}
|
||||
@@ -1,10 +1,6 @@
|
||||
package theme
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"github.com/charmbracelet/lipgloss/v2"
|
||||
"github.com/charmbracelet/lipgloss/v2/compat"
|
||||
)
|
||||
|
||||
@@ -14,7 +10,7 @@ import (
|
||||
type Theme interface {
|
||||
// Background colors
|
||||
Background() compat.AdaptiveColor // Radix 1
|
||||
BackgroundSubtle() compat.AdaptiveColor // Radix 2
|
||||
BackgroundPanel() compat.AdaptiveColor // Radix 2
|
||||
BackgroundElement() compat.AdaptiveColor // Radix 3
|
||||
|
||||
// Border colors
|
||||
@@ -84,7 +80,7 @@ type Theme interface {
|
||||
type BaseTheme struct {
|
||||
// Background colors
|
||||
BackgroundColor compat.AdaptiveColor
|
||||
BackgroundSubtleColor compat.AdaptiveColor
|
||||
BackgroundPanelColor compat.AdaptiveColor
|
||||
BackgroundElementColor compat.AdaptiveColor
|
||||
|
||||
// Border colors
|
||||
@@ -163,7 +159,7 @@ func (t *BaseTheme) Text() compat.AdaptiveColor { return t.TextColor }
|
||||
func (t *BaseTheme) TextMuted() compat.AdaptiveColor { return t.TextMutedColor }
|
||||
|
||||
func (t *BaseTheme) Background() compat.AdaptiveColor { return t.BackgroundColor }
|
||||
func (t *BaseTheme) BackgroundSubtle() compat.AdaptiveColor { return t.BackgroundSubtleColor }
|
||||
func (t *BaseTheme) BackgroundPanel() compat.AdaptiveColor { return t.BackgroundPanelColor }
|
||||
func (t *BaseTheme) BackgroundElement() compat.AdaptiveColor { return t.BackgroundElementColor }
|
||||
|
||||
func (t *BaseTheme) Border() compat.AdaptiveColor { return t.BorderColor }
|
||||
@@ -215,74 +211,3 @@ func (t *BaseTheme) SyntaxNumber() compat.AdaptiveColor { return t.SyntaxNu
|
||||
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 compat.AdaptiveColor.
|
||||
// It accepts either a string (hex color) or a map with "dark" and "light" keys.
|
||||
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 compat.AdaptiveColor{}, fmt.Errorf("invalid hex color format: %s", hexColor)
|
||||
}
|
||||
return compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(hexColor),
|
||||
Light: lipgloss.Color(hexColor),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Case 2: Int value between 0 and 255
|
||||
if numericVal, ok := value.(float64); ok {
|
||||
intVal := int(numericVal)
|
||||
if intVal < 0 || intVal > 255 {
|
||||
return compat.AdaptiveColor{}, fmt.Errorf("invalid int color value (must be between 0 and 255): %d", intVal)
|
||||
}
|
||||
return compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(fmt.Sprintf("%d", intVal)),
|
||||
Light: lipgloss.Color(fmt.Sprintf("%d", intVal)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Case 3: Map with dark and light keys
|
||||
if colorMap, ok := value.(map[string]any); ok {
|
||||
darkVal, darkOk := colorMap["dark"]
|
||||
lightVal, lightOk := colorMap["light"]
|
||||
|
||||
if !darkOk || !lightOk {
|
||||
return compat.AdaptiveColor{}, fmt.Errorf("color map must contain both 'dark' and 'light' keys")
|
||||
}
|
||||
|
||||
darkHex, darkIsString := darkVal.(string)
|
||||
lightHex, lightIsString := lightVal.(string)
|
||||
|
||||
if !darkIsString || !lightIsString {
|
||||
darkVal, darkIsNumber := darkVal.(float64)
|
||||
lightVal, lightIsNumber := lightVal.(float64)
|
||||
|
||||
if !darkIsNumber || !lightIsNumber {
|
||||
return compat.AdaptiveColor{}, fmt.Errorf("color map values must be strings or ints")
|
||||
}
|
||||
|
||||
darkInt := int(darkVal)
|
||||
lightInt := int(lightVal)
|
||||
|
||||
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 compat.AdaptiveColor{}, fmt.Errorf("invalid hex color format")
|
||||
}
|
||||
|
||||
return compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkHex),
|
||||
Light: lipgloss.Color(lightHex),
|
||||
}, nil
|
||||
}
|
||||
|
||||
return compat.AdaptiveColor{}, fmt.Errorf("color must be either a hex string or an object with dark/light keys")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/theme.json",
|
||||
"defs": {
|
||||
"darkBg": "#0B0E14",
|
||||
"darkBgAlt": "#0D1017",
|
||||
"darkLine": "#11151C",
|
||||
"darkPanel": "#0F131A",
|
||||
"darkFg": "#BFBDB6",
|
||||
"darkFgMuted": "#565B66",
|
||||
"darkGutter": "#6C7380",
|
||||
"darkTag": "#39BAE6",
|
||||
"darkFunc": "#FFB454",
|
||||
"darkEntity": "#59C2FF",
|
||||
"darkString": "#AAD94C",
|
||||
"darkRegexp": "#95E6CB",
|
||||
"darkMarkup": "#F07178",
|
||||
"darkKeyword": "#FF8F40",
|
||||
"darkSpecial": "#E6B673",
|
||||
"darkComment": "#ACB6BF",
|
||||
"darkConstant": "#D2A6FF",
|
||||
"darkOperator": "#F29668",
|
||||
"darkAdded": "#7FD962",
|
||||
"darkRemoved": "#F26D78",
|
||||
"darkAccent": "#E6B450",
|
||||
"darkError": "#D95757",
|
||||
"darkIndentActive": "#6C7380"
|
||||
},
|
||||
"theme": {
|
||||
"primary": "darkEntity",
|
||||
"secondary": "darkConstant",
|
||||
"accent": "darkAccent",
|
||||
"error": "darkError",
|
||||
"warning": "darkSpecial",
|
||||
"success": "darkAdded",
|
||||
"info": "darkTag",
|
||||
"text": "darkFg",
|
||||
"textMuted": "darkFgMuted",
|
||||
"background": "darkBg",
|
||||
"backgroundPanel": "darkPanel",
|
||||
"backgroundElement": "darkBgAlt",
|
||||
"border": "darkGutter",
|
||||
"borderActive": "darkIndentActive",
|
||||
"borderSubtle": "darkLine",
|
||||
"diffAdded": "darkAdded",
|
||||
"diffRemoved": "darkRemoved",
|
||||
"diffContext": "darkComment",
|
||||
"diffHunkHeader": "darkComment",
|
||||
"diffHighlightAdded": "darkString",
|
||||
"diffHighlightRemoved": "darkMarkup",
|
||||
"diffAddedBg": "#20303b",
|
||||
"diffRemovedBg": "#37222c",
|
||||
"diffContextBg": "darkPanel",
|
||||
"diffLineNumber": "darkGutter",
|
||||
"diffAddedLineNumberBg": "#1b2b34",
|
||||
"diffRemovedLineNumberBg": "#2d1f26",
|
||||
"markdownText": "darkFg",
|
||||
"markdownHeading": "darkConstant",
|
||||
"markdownLink": "darkEntity",
|
||||
"markdownLinkText": "darkTag",
|
||||
"markdownCode": "darkString",
|
||||
"markdownBlockQuote": "darkSpecial",
|
||||
"markdownEmph": "darkSpecial",
|
||||
"markdownStrong": "darkFunc",
|
||||
"markdownHorizontalRule": "darkFgMuted",
|
||||
"markdownListItem": "darkEntity",
|
||||
"markdownListEnumeration": "darkTag",
|
||||
"markdownImage": "darkEntity",
|
||||
"markdownImageText": "darkTag",
|
||||
"markdownCodeBlock": "darkFg",
|
||||
"syntaxComment": "darkComment",
|
||||
"syntaxKeyword": "darkKeyword",
|
||||
"syntaxFunction": "darkFunc",
|
||||
"syntaxVariable": "darkEntity",
|
||||
"syntaxString": "darkString",
|
||||
"syntaxNumber": "darkConstant",
|
||||
"syntaxType": "darkSpecial",
|
||||
"syntaxOperator": "darkOperator",
|
||||
"syntaxPunctuation": "darkFg"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/theme.json",
|
||||
"defs": {
|
||||
"lightRosewater": "#dc8a78",
|
||||
"lightFlamingo": "#dd7878",
|
||||
"lightPink": "#ea76cb",
|
||||
"lightMauve": "#8839ef",
|
||||
"lightRed": "#d20f39",
|
||||
"lightMaroon": "#e64553",
|
||||
"lightPeach": "#fe640b",
|
||||
"lightYellow": "#df8e1d",
|
||||
"lightGreen": "#40a02b",
|
||||
"lightTeal": "#179299",
|
||||
"lightSky": "#04a5e5",
|
||||
"lightSapphire": "#209fb5",
|
||||
"lightBlue": "#1e66f5",
|
||||
"lightLavender": "#7287fd",
|
||||
"lightText": "#4c4f69",
|
||||
"lightSubtext1": "#5c5f77",
|
||||
"lightSubtext0": "#6c6f85",
|
||||
"lightOverlay2": "#7c7f93",
|
||||
"lightOverlay1": "#8c8fa1",
|
||||
"lightOverlay0": "#9ca0b0",
|
||||
"lightSurface2": "#acb0be",
|
||||
"lightSurface1": "#bcc0cc",
|
||||
"lightSurface0": "#ccd0da",
|
||||
"lightBase": "#eff1f5",
|
||||
"lightMantle": "#e6e9ef",
|
||||
"lightCrust": "#dce0e8",
|
||||
"darkRosewater": "#f5e0dc",
|
||||
"darkFlamingo": "#f2cdcd",
|
||||
"darkPink": "#f5c2e7",
|
||||
"darkMauve": "#cba6f7",
|
||||
"darkRed": "#f38ba8",
|
||||
"darkMaroon": "#eba0ac",
|
||||
"darkPeach": "#fab387",
|
||||
"darkYellow": "#f9e2af",
|
||||
"darkGreen": "#a6e3a1",
|
||||
"darkTeal": "#94e2d5",
|
||||
"darkSky": "#89dceb",
|
||||
"darkSapphire": "#74c7ec",
|
||||
"darkBlue": "#89b4fa",
|
||||
"darkLavender": "#b4befe",
|
||||
"darkText": "#cdd6f4",
|
||||
"darkSubtext1": "#bac2de",
|
||||
"darkSubtext0": "#a6adc8",
|
||||
"darkOverlay2": "#9399b2",
|
||||
"darkOverlay1": "#7f849c",
|
||||
"darkOverlay0": "#6c7086",
|
||||
"darkSurface2": "#585b70",
|
||||
"darkSurface1": "#45475a",
|
||||
"darkSurface0": "#313244",
|
||||
"darkBase": "#1e1e2e",
|
||||
"darkMantle": "#181825",
|
||||
"darkCrust": "#11111b"
|
||||
},
|
||||
"theme": {
|
||||
"primary": { "dark": "darkBlue", "light": "lightBlue" },
|
||||
"secondary": { "dark": "darkMauve", "light": "lightMauve" },
|
||||
"accent": { "dark": "darkPink", "light": "lightPink" },
|
||||
"error": { "dark": "darkRed", "light": "lightRed" },
|
||||
"warning": { "dark": "darkYellow", "light": "lightYellow" },
|
||||
"success": { "dark": "darkGreen", "light": "lightGreen" },
|
||||
"info": { "dark": "darkTeal", "light": "lightTeal" },
|
||||
"text": { "dark": "darkText", "light": "lightText" },
|
||||
"textMuted": { "dark": "darkSubtext1", "light": "lightSubtext1" },
|
||||
"background": { "dark": "darkBase", "light": "lightBase" },
|
||||
"backgroundPanel": { "dark": "darkMantle", "light": "lightMantle" },
|
||||
"backgroundElement": { "dark": "darkCrust", "light": "lightCrust" },
|
||||
"border": { "dark": "darkSurface0", "light": "lightSurface0" },
|
||||
"borderActive": { "dark": "darkSurface1", "light": "lightSurface1" },
|
||||
"borderSubtle": { "dark": "darkSurface2", "light": "lightSurface2" },
|
||||
"diffAdded": { "dark": "darkGreen", "light": "lightGreen" },
|
||||
"diffRemoved": { "dark": "darkRed", "light": "lightRed" },
|
||||
"diffContext": { "dark": "darkOverlay2", "light": "lightOverlay2" },
|
||||
"diffHunkHeader": { "dark": "darkPeach", "light": "lightPeach" },
|
||||
"diffHighlightAdded": { "dark": "darkGreen", "light": "lightGreen" },
|
||||
"diffHighlightRemoved": { "dark": "darkRed", "light": "lightRed" },
|
||||
"diffAddedBg": { "dark": "#24312b", "light": "#d6f0d9" },
|
||||
"diffRemovedBg": { "dark": "#3c2a32", "light": "#f6dfe2" },
|
||||
"diffContextBg": { "dark": "darkMantle", "light": "lightMantle" },
|
||||
"diffLineNumber": { "dark": "darkSurface1", "light": "lightSurface1" },
|
||||
"diffAddedLineNumberBg": { "dark": "#1e2a25", "light": "#c9e3cb" },
|
||||
"diffRemovedLineNumberBg": { "dark": "#32232a", "light": "#e9d3d6" },
|
||||
"markdownText": { "dark": "darkText", "light": "lightText" },
|
||||
"markdownHeading": { "dark": "darkMauve", "light": "lightMauve" },
|
||||
"markdownLink": { "dark": "darkBlue", "light": "lightBlue" },
|
||||
"markdownLinkText": { "dark": "darkSky", "light": "lightSky" },
|
||||
"markdownCode": { "dark": "darkGreen", "light": "lightGreen" },
|
||||
"markdownBlockQuote": { "dark": "darkYellow", "light": "lightYellow" },
|
||||
"markdownEmph": { "dark": "darkYellow", "light": "lightYellow" },
|
||||
"markdownStrong": { "dark": "darkPeach", "light": "lightPeach" },
|
||||
"markdownHorizontalRule": {
|
||||
"dark": "darkSubtext0",
|
||||
"light": "lightSubtext0"
|
||||
},
|
||||
"markdownListItem": { "dark": "darkBlue", "light": "lightBlue" },
|
||||
"markdownListEnumeration": { "dark": "darkSky", "light": "lightSky" },
|
||||
"markdownImage": { "dark": "darkBlue", "light": "lightBlue" },
|
||||
"markdownImageText": { "dark": "darkSky", "light": "lightSky" },
|
||||
"markdownCodeBlock": { "dark": "darkText", "light": "lightText" },
|
||||
"syntaxComment": { "dark": "darkOverlay2", "light": "lightOverlay2" },
|
||||
"syntaxKeyword": { "dark": "darkMauve", "light": "lightMauve" },
|
||||
"syntaxFunction": { "dark": "darkBlue", "light": "lightBlue" },
|
||||
"syntaxVariable": { "dark": "darkRed", "light": "lightRed" },
|
||||
"syntaxString": { "dark": "darkGreen", "light": "lightGreen" },
|
||||
"syntaxNumber": { "dark": "darkPeach", "light": "lightPeach" },
|
||||
"syntaxType": { "dark": "darkYellow", "light": "lightYellow" },
|
||||
"syntaxOperator": { "dark": "darkSky", "light": "lightSky" },
|
||||
"syntaxPunctuation": { "dark": "darkText", "light": "lightText" }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/theme.json",
|
||||
"defs": {
|
||||
"darkStep1": "#2d353b",
|
||||
"darkStep2": "#333c43",
|
||||
"darkStep3": "#343f44",
|
||||
"darkStep4": "#3d484d",
|
||||
"darkStep5": "#475258",
|
||||
"darkStep6": "#7a8478",
|
||||
"darkStep7": "#859289",
|
||||
"darkStep8": "#9da9a0",
|
||||
"darkStep9": "#a7c080",
|
||||
"darkStep10": "#83c092",
|
||||
"darkStep11": "#7a8478",
|
||||
"darkStep12": "#d3c6aa",
|
||||
"darkRed": "#e67e80",
|
||||
"darkOrange": "#e69875",
|
||||
"darkGreen": "#a7c080",
|
||||
"darkCyan": "#83c092",
|
||||
"darkYellow": "#dbbc7f",
|
||||
"lightStep1": "#fdf6e3",
|
||||
"lightStep2": "#efebd4",
|
||||
"lightStep3": "#f4f0d9",
|
||||
"lightStep4": "#efebd4",
|
||||
"lightStep5": "#e6e2cc",
|
||||
"lightStep6": "#a6b0a0",
|
||||
"lightStep7": "#939f91",
|
||||
"lightStep8": "#829181",
|
||||
"lightStep9": "#8da101",
|
||||
"lightStep10": "#35a77c",
|
||||
"lightStep11": "#a6b0a0",
|
||||
"lightStep12": "#5c6a72",
|
||||
"lightRed": "#f85552",
|
||||
"lightOrange": "#f57d26",
|
||||
"lightGreen": "#8da101",
|
||||
"lightCyan": "#35a77c",
|
||||
"lightYellow": "#dfa000"
|
||||
},
|
||||
"theme": {
|
||||
"primary": {
|
||||
"dark": "darkStep9",
|
||||
"light": "lightStep9"
|
||||
},
|
||||
"secondary": {
|
||||
"dark": "#7fbbb3",
|
||||
"light": "#3a94c5"
|
||||
},
|
||||
"accent": {
|
||||
"dark": "#d699b6",
|
||||
"light": "#df69ba"
|
||||
},
|
||||
"error": {
|
||||
"dark": "darkRed",
|
||||
"light": "lightRed"
|
||||
},
|
||||
"warning": {
|
||||
"dark": "darkOrange",
|
||||
"light": "lightOrange"
|
||||
},
|
||||
"success": {
|
||||
"dark": "darkGreen",
|
||||
"light": "lightGreen"
|
||||
},
|
||||
"info": {
|
||||
"dark": "darkCyan",
|
||||
"light": "lightCyan"
|
||||
},
|
||||
"text": {
|
||||
"dark": "darkStep12",
|
||||
"light": "lightStep12"
|
||||
},
|
||||
"textMuted": {
|
||||
"dark": "darkStep11",
|
||||
"light": "lightStep11"
|
||||
},
|
||||
"background": {
|
||||
"dark": "darkStep1",
|
||||
"light": "lightStep1"
|
||||
},
|
||||
"backgroundPanel": {
|
||||
"dark": "darkStep2",
|
||||
"light": "lightStep2"
|
||||
},
|
||||
"backgroundElement": {
|
||||
"dark": "darkStep3",
|
||||
"light": "lightStep3"
|
||||
},
|
||||
"border": {
|
||||
"dark": "darkStep7",
|
||||
"light": "lightStep7"
|
||||
},
|
||||
"borderActive": {
|
||||
"dark": "darkStep8",
|
||||
"light": "lightStep8"
|
||||
},
|
||||
"borderSubtle": {
|
||||
"dark": "darkStep6",
|
||||
"light": "lightStep6"
|
||||
},
|
||||
"diffAdded": {
|
||||
"dark": "#4fd6be",
|
||||
"light": "#1e725c"
|
||||
},
|
||||
"diffRemoved": {
|
||||
"dark": "#c53b53",
|
||||
"light": "#c53b53"
|
||||
},
|
||||
"diffContext": {
|
||||
"dark": "#828bb8",
|
||||
"light": "#7086b5"
|
||||
},
|
||||
"diffHunkHeader": {
|
||||
"dark": "#828bb8",
|
||||
"light": "#7086b5"
|
||||
},
|
||||
"diffHighlightAdded": {
|
||||
"dark": "#b8db87",
|
||||
"light": "#4db380"
|
||||
},
|
||||
"diffHighlightRemoved": {
|
||||
"dark": "#e26a75",
|
||||
"light": "#f52a65"
|
||||
},
|
||||
"diffAddedBg": {
|
||||
"dark": "#20303b",
|
||||
"light": "#d5e5d5"
|
||||
},
|
||||
"diffRemovedBg": {
|
||||
"dark": "#37222c",
|
||||
"light": "#f7d8db"
|
||||
},
|
||||
"diffContextBg": {
|
||||
"dark": "darkStep2",
|
||||
"light": "lightStep2"
|
||||
},
|
||||
"diffLineNumber": {
|
||||
"dark": "darkStep3",
|
||||
"light": "lightStep3"
|
||||
},
|
||||
"diffAddedLineNumberBg": {
|
||||
"dark": "#1b2b34",
|
||||
"light": "#c5d5c5"
|
||||
},
|
||||
"diffRemovedLineNumberBg": {
|
||||
"dark": "#2d1f26",
|
||||
"light": "#e7c8cb"
|
||||
},
|
||||
"markdownText": {
|
||||
"dark": "darkStep12",
|
||||
"light": "lightStep12"
|
||||
},
|
||||
"markdownHeading": {
|
||||
"dark": "#d699b6",
|
||||
"light": "#df69ba"
|
||||
},
|
||||
"markdownLink": {
|
||||
"dark": "darkStep9",
|
||||
"light": "lightStep9"
|
||||
},
|
||||
"markdownLinkText": {
|
||||
"dark": "darkCyan",
|
||||
"light": "lightCyan"
|
||||
},
|
||||
"markdownCode": {
|
||||
"dark": "darkGreen",
|
||||
"light": "lightGreen"
|
||||
},
|
||||
"markdownBlockQuote": {
|
||||
"dark": "darkYellow",
|
||||
"light": "lightYellow"
|
||||
},
|
||||
"markdownEmph": {
|
||||
"dark": "darkYellow",
|
||||
"light": "lightYellow"
|
||||
},
|
||||
"markdownStrong": {
|
||||
"dark": "darkOrange",
|
||||
"light": "lightOrange"
|
||||
},
|
||||
"markdownHorizontalRule": {
|
||||
"dark": "darkStep11",
|
||||
"light": "lightStep11"
|
||||
},
|
||||
"markdownListItem": {
|
||||
"dark": "darkStep9",
|
||||
"light": "lightStep9"
|
||||
},
|
||||
"markdownListEnumeration": {
|
||||
"dark": "darkCyan",
|
||||
"light": "lightCyan"
|
||||
},
|
||||
"markdownImage": {
|
||||
"dark": "darkStep9",
|
||||
"light": "lightStep9"
|
||||
},
|
||||
"markdownImageText": {
|
||||
"dark": "darkCyan",
|
||||
"light": "lightCyan"
|
||||
},
|
||||
"markdownCodeBlock": {
|
||||
"dark": "darkStep12",
|
||||
"light": "lightStep12"
|
||||
},
|
||||
"syntaxComment": {
|
||||
"dark": "darkStep11",
|
||||
"light": "lightStep11"
|
||||
},
|
||||
"syntaxKeyword": {
|
||||
"dark": "#d699b6",
|
||||
"light": "#df69ba"
|
||||
},
|
||||
"syntaxFunction": {
|
||||
"dark": "darkStep9",
|
||||
"light": "lightStep9"
|
||||
},
|
||||
"syntaxVariable": {
|
||||
"dark": "darkRed",
|
||||
"light": "lightRed"
|
||||
},
|
||||
"syntaxString": {
|
||||
"dark": "darkGreen",
|
||||
"light": "lightGreen"
|
||||
},
|
||||
"syntaxNumber": {
|
||||
"dark": "darkOrange",
|
||||
"light": "lightOrange"
|
||||
},
|
||||
"syntaxType": {
|
||||
"dark": "darkYellow",
|
||||
"light": "lightYellow"
|
||||
},
|
||||
"syntaxOperator": {
|
||||
"dark": "darkCyan",
|
||||
"light": "lightCyan"
|
||||
},
|
||||
"syntaxPunctuation": {
|
||||
"dark": "darkStep12",
|
||||
"light": "lightStep12"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/theme.json",
|
||||
"defs": {
|
||||
"darkBg0": "#282828",
|
||||
"darkBg1": "#3c3836",
|
||||
"darkBg2": "#504945",
|
||||
"darkBg3": "#665c54",
|
||||
"darkFg0": "#fbf1c7",
|
||||
"darkFg1": "#ebdbb2",
|
||||
"darkGray": "#928374",
|
||||
"darkRed": "#cc241d",
|
||||
"darkGreen": "#98971a",
|
||||
"darkYellow": "#d79921",
|
||||
"darkBlue": "#458588",
|
||||
"darkPurple": "#b16286",
|
||||
"darkAqua": "#689d6a",
|
||||
"darkOrange": "#d65d0e",
|
||||
"darkRedBright": "#fb4934",
|
||||
"darkGreenBright": "#b8bb26",
|
||||
"darkYellowBright": "#fabd2f",
|
||||
"darkBlueBright": "#83a598",
|
||||
"darkPurpleBright": "#d3869b",
|
||||
"darkAquaBright": "#8ec07c",
|
||||
"darkOrangeBright": "#fe8019",
|
||||
"lightBg0": "#fbf1c7",
|
||||
"lightBg1": "#ebdbb2",
|
||||
"lightBg2": "#d5c4a1",
|
||||
"lightBg3": "#bdae93",
|
||||
"lightFg0": "#282828",
|
||||
"lightFg1": "#3c3836",
|
||||
"lightGray": "#7c6f64",
|
||||
"lightRed": "#9d0006",
|
||||
"lightGreen": "#79740e",
|
||||
"lightYellow": "#b57614",
|
||||
"lightBlue": "#076678",
|
||||
"lightPurple": "#8f3f71",
|
||||
"lightAqua": "#427b58",
|
||||
"lightOrange": "#af3a03"
|
||||
},
|
||||
"theme": {
|
||||
"primary": { "dark": "darkBlueBright", "light": "lightBlue" },
|
||||
"secondary": { "dark": "darkPurpleBright", "light": "lightPurple" },
|
||||
"accent": { "dark": "darkAquaBright", "light": "lightAqua" },
|
||||
"error": { "dark": "darkRedBright", "light": "lightRed" },
|
||||
"warning": { "dark": "darkOrangeBright", "light": "lightOrange" },
|
||||
"success": { "dark": "darkGreenBright", "light": "lightGreen" },
|
||||
"info": { "dark": "darkYellowBright", "light": "lightYellow" },
|
||||
"text": { "dark": "darkFg1", "light": "lightFg1" },
|
||||
"textMuted": { "dark": "darkGray", "light": "lightGray" },
|
||||
"background": { "dark": "darkBg0", "light": "lightBg0" },
|
||||
"backgroundPanel": { "dark": "darkBg1", "light": "lightBg1" },
|
||||
"backgroundElement": { "dark": "darkBg2", "light": "lightBg2" },
|
||||
"border": { "dark": "darkBg3", "light": "lightBg3" },
|
||||
"borderActive": { "dark": "darkFg1", "light": "lightFg1" },
|
||||
"borderSubtle": { "dark": "darkBg2", "light": "lightBg2" },
|
||||
"diffAdded": { "dark": "darkGreen", "light": "lightGreen" },
|
||||
"diffRemoved": { "dark": "darkRed", "light": "lightRed" },
|
||||
"diffContext": { "dark": "darkGray", "light": "lightGray" },
|
||||
"diffHunkHeader": { "dark": "darkAqua", "light": "lightAqua" },
|
||||
"diffHighlightAdded": { "dark": "darkGreenBright", "light": "lightGreen" },
|
||||
"diffHighlightRemoved": { "dark": "darkRedBright", "light": "lightRed" },
|
||||
"diffAddedBg": { "dark": "#32302f", "light": "#e2e0b5" },
|
||||
"diffRemovedBg": { "dark": "#322929", "light": "#e9d8d5" },
|
||||
"diffContextBg": { "dark": "darkBg1", "light": "lightBg1" },
|
||||
"diffLineNumber": { "dark": "darkBg3", "light": "lightBg3" },
|
||||
"diffAddedLineNumberBg": { "dark": "#2a2827", "light": "#d4d2a9" },
|
||||
"diffRemovedLineNumberBg": { "dark": "#2a2222", "light": "#d8cbc8" },
|
||||
"markdownText": { "dark": "darkFg1", "light": "lightFg1" },
|
||||
"markdownHeading": { "dark": "darkBlueBright", "light": "lightBlue" },
|
||||
"markdownLink": { "dark": "darkAquaBright", "light": "lightAqua" },
|
||||
"markdownLinkText": { "dark": "darkGreenBright", "light": "lightGreen" },
|
||||
"markdownCode": { "dark": "darkYellowBright", "light": "lightYellow" },
|
||||
"markdownBlockQuote": { "dark": "darkGray", "light": "lightGray" },
|
||||
"markdownEmph": { "dark": "darkPurpleBright", "light": "lightPurple" },
|
||||
"markdownStrong": { "dark": "darkOrangeBright", "light": "lightOrange" },
|
||||
"markdownHorizontalRule": { "dark": "darkGray", "light": "lightGray" },
|
||||
"markdownListItem": { "dark": "darkBlueBright", "light": "lightBlue" },
|
||||
"markdownListEnumeration": {
|
||||
"dark": "darkAquaBright",
|
||||
"light": "lightAqua"
|
||||
},
|
||||
"markdownImage": { "dark": "darkAquaBright", "light": "lightAqua" },
|
||||
"markdownImageText": { "dark": "darkGreenBright", "light": "lightGreen" },
|
||||
"markdownCodeBlock": { "dark": "darkFg1", "light": "lightFg1" },
|
||||
"syntaxComment": { "dark": "darkGray", "light": "lightGray" },
|
||||
"syntaxKeyword": { "dark": "darkRedBright", "light": "lightRed" },
|
||||
"syntaxFunction": { "dark": "darkGreenBright", "light": "lightGreen" },
|
||||
"syntaxVariable": { "dark": "darkBlueBright", "light": "lightBlue" },
|
||||
"syntaxString": { "dark": "darkYellowBright", "light": "lightYellow" },
|
||||
"syntaxNumber": { "dark": "darkPurpleBright", "light": "lightPurple" },
|
||||
"syntaxType": { "dark": "darkAquaBright", "light": "lightAqua" },
|
||||
"syntaxOperator": { "dark": "darkOrangeBright", "light": "lightOrange" },
|
||||
"syntaxPunctuation": { "dark": "darkFg1", "light": "lightFg1" }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/theme.json",
|
||||
"defs": {
|
||||
"sumiInk0": "#1F1F28",
|
||||
"sumiInk1": "#2A2A37",
|
||||
"sumiInk2": "#363646",
|
||||
"sumiInk3": "#54546D",
|
||||
"fujiWhite": "#DCD7BA",
|
||||
"oldWhite": "#C8C093",
|
||||
"fujiGray": "#727169",
|
||||
"oniViolet": "#957FB8",
|
||||
"crystalBlue": "#7E9CD8",
|
||||
"carpYellow": "#C38D9D",
|
||||
"sakuraPink": "#D27E99",
|
||||
"waveAqua": "#76946A",
|
||||
"roninYellow": "#D7A657",
|
||||
"dragonRed": "#E82424",
|
||||
"lotusGreen": "#98BB6C",
|
||||
"waveBlue": "#2D4F67",
|
||||
"lightBg": "#F2E9DE",
|
||||
"lightPaper": "#EAE4D7",
|
||||
"lightText": "#54433A",
|
||||
"lightGray": "#9E9389"
|
||||
},
|
||||
"theme": {
|
||||
"primary": { "dark": "crystalBlue", "light": "waveBlue" },
|
||||
"secondary": { "dark": "oniViolet", "light": "oniViolet" },
|
||||
"accent": { "dark": "sakuraPink", "light": "sakuraPink" },
|
||||
"error": { "dark": "dragonRed", "light": "dragonRed" },
|
||||
"warning": { "dark": "roninYellow", "light": "roninYellow" },
|
||||
"success": { "dark": "lotusGreen", "light": "lotusGreen" },
|
||||
"info": { "dark": "waveAqua", "light": "waveAqua" },
|
||||
"text": { "dark": "fujiWhite", "light": "lightText" },
|
||||
"textMuted": { "dark": "fujiGray", "light": "lightGray" },
|
||||
"background": { "dark": "sumiInk0", "light": "lightBg" },
|
||||
"backgroundPanel": { "dark": "sumiInk1", "light": "lightPaper" },
|
||||
"backgroundElement": { "dark": "sumiInk2", "light": "#E3DCD2" },
|
||||
"border": { "dark": "sumiInk3", "light": "#D4CBBF" },
|
||||
"borderActive": { "dark": "carpYellow", "light": "carpYellow" },
|
||||
"borderSubtle": { "dark": "sumiInk2", "light": "#DCD4C9" },
|
||||
"diffAdded": { "dark": "lotusGreen", "light": "lotusGreen" },
|
||||
"diffRemoved": { "dark": "dragonRed", "light": "dragonRed" },
|
||||
"diffContext": { "dark": "fujiGray", "light": "lightGray" },
|
||||
"diffHunkHeader": { "dark": "waveBlue", "light": "waveBlue" },
|
||||
"diffHighlightAdded": { "dark": "#A9D977", "light": "#89AF5B" },
|
||||
"diffHighlightRemoved": { "dark": "#F24A4A", "light": "#D61F1F" },
|
||||
"diffAddedBg": { "dark": "#252E25", "light": "#EAF3E4" },
|
||||
"diffRemovedBg": { "dark": "#362020", "light": "#FBE6E6" },
|
||||
"diffContextBg": { "dark": "sumiInk1", "light": "lightPaper" },
|
||||
"diffLineNumber": { "dark": "sumiInk3", "light": "#C7BEB4" },
|
||||
"diffAddedLineNumberBg": { "dark": "#202820", "light": "#DDE8D6" },
|
||||
"diffRemovedLineNumberBg": { "dark": "#2D1C1C", "light": "#F2DADA" },
|
||||
"markdownText": { "dark": "fujiWhite", "light": "lightText" },
|
||||
"markdownHeading": { "dark": "oniViolet", "light": "oniViolet" },
|
||||
"markdownLink": { "dark": "crystalBlue", "light": "waveBlue" },
|
||||
"markdownLinkText": { "dark": "waveAqua", "light": "waveAqua" },
|
||||
"markdownCode": { "dark": "lotusGreen", "light": "lotusGreen" },
|
||||
"markdownBlockQuote": { "dark": "fujiGray", "light": "lightGray" },
|
||||
"markdownEmph": { "dark": "carpYellow", "light": "carpYellow" },
|
||||
"markdownStrong": { "dark": "roninYellow", "light": "roninYellow" },
|
||||
"markdownHorizontalRule": { "dark": "fujiGray", "light": "lightGray" },
|
||||
"markdownListItem": { "dark": "crystalBlue", "light": "waveBlue" },
|
||||
"markdownListEnumeration": { "dark": "waveAqua", "light": "waveAqua" },
|
||||
"markdownImage": { "dark": "crystalBlue", "light": "waveBlue" },
|
||||
"markdownImageText": { "dark": "waveAqua", "light": "waveAqua" },
|
||||
"markdownCodeBlock": { "dark": "fujiWhite", "light": "lightText" },
|
||||
"syntaxComment": { "dark": "fujiGray", "light": "lightGray" },
|
||||
"syntaxKeyword": { "dark": "oniViolet", "light": "oniViolet" },
|
||||
"syntaxFunction": { "dark": "crystalBlue", "light": "waveBlue" },
|
||||
"syntaxVariable": { "dark": "fujiWhite", "light": "lightText" },
|
||||
"syntaxString": { "dark": "lotusGreen", "light": "lotusGreen" },
|
||||
"syntaxNumber": { "dark": "roninYellow", "light": "roninYellow" },
|
||||
"syntaxType": { "dark": "carpYellow", "light": "carpYellow" },
|
||||
"syntaxOperator": { "dark": "sakuraPink", "light": "sakuraPink" },
|
||||
"syntaxPunctuation": { "dark": "fujiWhite", "light": "lightText" }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/theme.json",
|
||||
"defs": {
|
||||
"matrixInk0": "#0a0e0a",
|
||||
"matrixInk1": "#0e130d",
|
||||
"matrixInk2": "#141c12",
|
||||
"matrixInk3": "#1e2a1b",
|
||||
"rainGreen": "#2eff6a",
|
||||
"rainGreenDim": "#1cc24b",
|
||||
"rainGreenHi": "#62ff94",
|
||||
"rainCyan": "#00efff",
|
||||
"rainTeal": "#24f6d9",
|
||||
"rainPurple": "#c770ff",
|
||||
"rainOrange": "#ffa83d",
|
||||
"alertRed": "#ff4b4b",
|
||||
"alertYellow": "#e6ff57",
|
||||
"alertBlue": "#30b3ff",
|
||||
"rainGray": "#8ca391",
|
||||
"lightBg": "#eef3ea",
|
||||
"lightPaper": "#e4ebe1",
|
||||
"lightInk1": "#dae1d7",
|
||||
"lightText": "#203022",
|
||||
"lightGray": "#748476"
|
||||
},
|
||||
"theme": {
|
||||
"primary": { "dark": "rainGreen", "light": "rainGreenDim" },
|
||||
"secondary": { "dark": "rainCyan", "light": "rainTeal" },
|
||||
"accent": { "dark": "rainPurple", "light": "rainPurple" },
|
||||
"error": { "dark": "alertRed", "light": "alertRed" },
|
||||
"warning": { "dark": "alertYellow", "light": "alertYellow" },
|
||||
"success": { "dark": "rainGreenHi", "light": "rainGreenDim" },
|
||||
"info": { "dark": "alertBlue", "light": "alertBlue" },
|
||||
"text": { "dark": "rainGreenHi", "light": "lightText" },
|
||||
"textMuted": { "dark": "rainGray", "light": "lightGray" },
|
||||
"background": { "dark": "matrixInk0", "light": "lightBg" },
|
||||
"backgroundPanel": { "dark": "matrixInk1", "light": "lightPaper" },
|
||||
"backgroundElement": { "dark": "matrixInk2", "light": "lightInk1" },
|
||||
"border": { "dark": "matrixInk3", "light": "lightGray" },
|
||||
"borderActive": { "dark": "rainGreen", "light": "rainGreenDim" },
|
||||
"borderSubtle": { "dark": "matrixInk2", "light": "lightInk1" },
|
||||
"diffAdded": { "dark": "rainGreenDim", "light": "rainGreenDim" },
|
||||
"diffRemoved": { "dark": "alertRed", "light": "alertRed" },
|
||||
"diffContext": { "dark": "rainGray", "light": "lightGray" },
|
||||
"diffHunkHeader": { "dark": "alertBlue", "light": "alertBlue" },
|
||||
"diffHighlightAdded": { "dark": "#77ffaf", "light": "#5dac7e" },
|
||||
"diffHighlightRemoved": { "dark": "#ff7171", "light": "#d53a3a" },
|
||||
"diffAddedBg": { "dark": "#132616", "light": "#e0efde" },
|
||||
"diffRemovedBg": { "dark": "#261212", "light": "#f9e5e5" },
|
||||
"diffContextBg": { "dark": "matrixInk1", "light": "lightPaper" },
|
||||
"diffLineNumber": { "dark": "matrixInk3", "light": "lightGray" },
|
||||
"diffAddedLineNumberBg": { "dark": "#0f1b11", "light": "#d6e7d2" },
|
||||
"diffRemovedLineNumberBg": { "dark": "#1b1414", "light": "#f2d2d2" },
|
||||
"markdownText": { "dark": "rainGreenHi", "light": "lightText" },
|
||||
"markdownHeading": { "dark": "rainCyan", "light": "rainTeal" },
|
||||
"markdownLink": { "dark": "alertBlue", "light": "alertBlue" },
|
||||
"markdownLinkText": { "dark": "rainTeal", "light": "rainTeal" },
|
||||
"markdownCode": { "dark": "rainGreenDim", "light": "rainGreenDim" },
|
||||
"markdownBlockQuote": { "dark": "rainGray", "light": "lightGray" },
|
||||
"markdownEmph": { "dark": "rainOrange", "light": "rainOrange" },
|
||||
"markdownStrong": { "dark": "alertYellow", "light": "alertYellow" },
|
||||
"markdownHorizontalRule": { "dark": "rainGray", "light": "lightGray" },
|
||||
"markdownListItem": { "dark": "alertBlue", "light": "alertBlue" },
|
||||
"markdownListEnumeration": { "dark": "rainTeal", "light": "rainTeal" },
|
||||
"markdownImage": { "dark": "alertBlue", "light": "alertBlue" },
|
||||
"markdownImageText": { "dark": "rainTeal", "light": "rainTeal" },
|
||||
"markdownCodeBlock": { "dark": "rainGreenHi", "light": "lightText" },
|
||||
"syntaxComment": { "dark": "rainGray", "light": "lightGray" },
|
||||
"syntaxKeyword": { "dark": "rainPurple", "light": "rainPurple" },
|
||||
"syntaxFunction": { "dark": "alertBlue", "light": "alertBlue" },
|
||||
"syntaxVariable": { "dark": "rainGreenHi", "light": "lightText" },
|
||||
"syntaxString": { "dark": "rainGreenDim", "light": "rainGreenDim" },
|
||||
"syntaxNumber": { "dark": "rainOrange", "light": "rainOrange" },
|
||||
"syntaxType": { "dark": "alertYellow", "light": "alertYellow" },
|
||||
"syntaxOperator": { "dark": "rainTeal", "light": "rainTeal" },
|
||||
"syntaxPunctuation": { "dark": "rainGreenHi", "light": "lightText" }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/theme.json",
|
||||
"defs": {
|
||||
"nord0": "#2E3440",
|
||||
"nord1": "#3B4252",
|
||||
"nord2": "#434C5E",
|
||||
"nord3": "#4C566A",
|
||||
"nord4": "#D8DEE9",
|
||||
"nord5": "#E5E9F0",
|
||||
"nord6": "#ECEFF4",
|
||||
"nord7": "#8FBCBB",
|
||||
"nord8": "#88C0D0",
|
||||
"nord9": "#81A1C1",
|
||||
"nord10": "#5E81AC",
|
||||
"nord11": "#BF616A",
|
||||
"nord12": "#D08770",
|
||||
"nord13": "#EBCB8B",
|
||||
"nord14": "#A3BE8C",
|
||||
"nord15": "#B48EAD"
|
||||
},
|
||||
"theme": {
|
||||
"primary": {
|
||||
"dark": "nord8",
|
||||
"light": "nord10"
|
||||
},
|
||||
"secondary": {
|
||||
"dark": "nord9",
|
||||
"light": "nord9"
|
||||
},
|
||||
"accent": {
|
||||
"dark": "nord7",
|
||||
"light": "nord7"
|
||||
},
|
||||
"error": {
|
||||
"dark": "nord11",
|
||||
"light": "nord11"
|
||||
},
|
||||
"warning": {
|
||||
"dark": "nord12",
|
||||
"light": "nord12"
|
||||
},
|
||||
"success": {
|
||||
"dark": "nord14",
|
||||
"light": "nord14"
|
||||
},
|
||||
"info": {
|
||||
"dark": "nord8",
|
||||
"light": "nord10"
|
||||
},
|
||||
"text": {
|
||||
"dark": "nord6",
|
||||
"light": "nord0"
|
||||
},
|
||||
"textMuted": {
|
||||
"dark": "#8B95A7",
|
||||
"light": "nord1"
|
||||
},
|
||||
"background": {
|
||||
"dark": "nord0",
|
||||
"light": "nord6"
|
||||
},
|
||||
"backgroundPanel": {
|
||||
"dark": "nord1",
|
||||
"light": "nord5"
|
||||
},
|
||||
"backgroundElement": {
|
||||
"dark": "nord2",
|
||||
"light": "nord4"
|
||||
},
|
||||
"border": {
|
||||
"dark": "nord2",
|
||||
"light": "nord3"
|
||||
},
|
||||
"borderActive": {
|
||||
"dark": "nord3",
|
||||
"light": "nord2"
|
||||
},
|
||||
"borderSubtle": {
|
||||
"dark": "nord2",
|
||||
"light": "nord3"
|
||||
},
|
||||
"diffAdded": {
|
||||
"dark": "nord14",
|
||||
"light": "nord14"
|
||||
},
|
||||
"diffRemoved": {
|
||||
"dark": "nord11",
|
||||
"light": "nord11"
|
||||
},
|
||||
"diffContext": {
|
||||
"dark": "#8B95A7",
|
||||
"light": "nord3"
|
||||
},
|
||||
"diffHunkHeader": {
|
||||
"dark": "#8B95A7",
|
||||
"light": "nord3"
|
||||
},
|
||||
"diffHighlightAdded": {
|
||||
"dark": "nord14",
|
||||
"light": "nord14"
|
||||
},
|
||||
"diffHighlightRemoved": {
|
||||
"dark": "nord11",
|
||||
"light": "nord11"
|
||||
},
|
||||
"diffAddedBg": {
|
||||
"dark": "#3B4252",
|
||||
"light": "#E5E9F0"
|
||||
},
|
||||
"diffRemovedBg": {
|
||||
"dark": "#3B4252",
|
||||
"light": "#E5E9F0"
|
||||
},
|
||||
"diffContextBg": {
|
||||
"dark": "nord1",
|
||||
"light": "nord5"
|
||||
},
|
||||
"diffLineNumber": {
|
||||
"dark": "nord2",
|
||||
"light": "nord4"
|
||||
},
|
||||
"diffAddedLineNumberBg": {
|
||||
"dark": "#3B4252",
|
||||
"light": "#E5E9F0"
|
||||
},
|
||||
"diffRemovedLineNumberBg": {
|
||||
"dark": "#3B4252",
|
||||
"light": "#E5E9F0"
|
||||
},
|
||||
"markdownText": {
|
||||
"dark": "nord4",
|
||||
"light": "nord0"
|
||||
},
|
||||
"markdownHeading": {
|
||||
"dark": "nord8",
|
||||
"light": "nord10"
|
||||
},
|
||||
"markdownLink": {
|
||||
"dark": "nord9",
|
||||
"light": "nord9"
|
||||
},
|
||||
"markdownLinkText": {
|
||||
"dark": "nord7",
|
||||
"light": "nord7"
|
||||
},
|
||||
"markdownCode": {
|
||||
"dark": "nord14",
|
||||
"light": "nord14"
|
||||
},
|
||||
"markdownBlockQuote": {
|
||||
"dark": "#8B95A7",
|
||||
"light": "nord3"
|
||||
},
|
||||
"markdownEmph": {
|
||||
"dark": "nord12",
|
||||
"light": "nord12"
|
||||
},
|
||||
"markdownStrong": {
|
||||
"dark": "nord13",
|
||||
"light": "nord13"
|
||||
},
|
||||
"markdownHorizontalRule": {
|
||||
"dark": "#8B95A7",
|
||||
"light": "nord3"
|
||||
},
|
||||
"markdownListItem": {
|
||||
"dark": "nord8",
|
||||
"light": "nord10"
|
||||
},
|
||||
"markdownListEnumeration": {
|
||||
"dark": "nord7",
|
||||
"light": "nord7"
|
||||
},
|
||||
"markdownImage": {
|
||||
"dark": "nord9",
|
||||
"light": "nord9"
|
||||
},
|
||||
"markdownImageText": {
|
||||
"dark": "nord7",
|
||||
"light": "nord7"
|
||||
},
|
||||
"markdownCodeBlock": {
|
||||
"dark": "nord4",
|
||||
"light": "nord0"
|
||||
},
|
||||
"syntaxComment": {
|
||||
"dark": "#8B95A7",
|
||||
"light": "nord3"
|
||||
},
|
||||
"syntaxKeyword": {
|
||||
"dark": "nord9",
|
||||
"light": "nord9"
|
||||
},
|
||||
"syntaxFunction": {
|
||||
"dark": "nord8",
|
||||
"light": "nord8"
|
||||
},
|
||||
"syntaxVariable": {
|
||||
"dark": "nord7",
|
||||
"light": "nord7"
|
||||
},
|
||||
"syntaxString": {
|
||||
"dark": "nord14",
|
||||
"light": "nord14"
|
||||
},
|
||||
"syntaxNumber": {
|
||||
"dark": "nord15",
|
||||
"light": "nord15"
|
||||
},
|
||||
"syntaxType": {
|
||||
"dark": "nord7",
|
||||
"light": "nord7"
|
||||
},
|
||||
"syntaxOperator": {
|
||||
"dark": "nord9",
|
||||
"light": "nord9"
|
||||
},
|
||||
"syntaxPunctuation": {
|
||||
"dark": "nord4",
|
||||
"light": "nord0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/theme.json",
|
||||
"defs": {
|
||||
"darkBg": "#282c34",
|
||||
"darkBgAlt": "#21252b",
|
||||
"darkBgPanel": "#353b45",
|
||||
"darkFg": "#abb2bf",
|
||||
"darkFgMuted": "#5c6370",
|
||||
"darkPurple": "#c678dd",
|
||||
"darkBlue": "#61afef",
|
||||
"darkRed": "#e06c75",
|
||||
"darkGreen": "#98c379",
|
||||
"darkYellow": "#e5c07b",
|
||||
"darkOrange": "#d19a66",
|
||||
"darkCyan": "#56b6c2",
|
||||
"lightBg": "#fafafa",
|
||||
"lightBgAlt": "#f0f0f1",
|
||||
"lightBgPanel": "#eaeaeb",
|
||||
"lightFg": "#383a42",
|
||||
"lightFgMuted": "#a0a1a7",
|
||||
"lightPurple": "#a626a4",
|
||||
"lightBlue": "#4078f2",
|
||||
"lightRed": "#e45649",
|
||||
"lightGreen": "#50a14f",
|
||||
"lightYellow": "#c18401",
|
||||
"lightOrange": "#986801",
|
||||
"lightCyan": "#0184bc"
|
||||
},
|
||||
"theme": {
|
||||
"primary": { "dark": "darkBlue", "light": "lightBlue" },
|
||||
"secondary": { "dark": "darkPurple", "light": "lightPurple" },
|
||||
"accent": { "dark": "darkCyan", "light": "lightCyan" },
|
||||
"error": { "dark": "darkRed", "light": "lightRed" },
|
||||
"warning": { "dark": "darkYellow", "light": "lightYellow" },
|
||||
"success": { "dark": "darkGreen", "light": "lightGreen" },
|
||||
"info": { "dark": "darkOrange", "light": "lightOrange" },
|
||||
"text": { "dark": "darkFg", "light": "lightFg" },
|
||||
"textMuted": { "dark": "darkFgMuted", "light": "lightFgMuted" },
|
||||
"background": { "dark": "darkBg", "light": "lightBg" },
|
||||
"backgroundPanel": { "dark": "darkBgAlt", "light": "lightBgAlt" },
|
||||
"backgroundElement": { "dark": "darkBgPanel", "light": "lightBgPanel" },
|
||||
"border": { "dark": "#393f4a", "light": "#d1d1d2" },
|
||||
"borderActive": { "dark": "darkBlue", "light": "lightBlue" },
|
||||
"borderSubtle": { "dark": "#2c313a", "light": "#e0e0e1" },
|
||||
"diffAdded": { "dark": "darkGreen", "light": "lightGreen" },
|
||||
"diffRemoved": { "dark": "darkRed", "light": "lightRed" },
|
||||
"diffContext": { "dark": "darkFgMuted", "light": "lightFgMuted" },
|
||||
"diffHunkHeader": { "dark": "darkCyan", "light": "lightCyan" },
|
||||
"diffHighlightAdded": { "dark": "#aad482", "light": "#489447" },
|
||||
"diffHighlightRemoved": { "dark": "#e8828b", "light": "#d65145" },
|
||||
"diffAddedBg": { "dark": "#2c382b", "light": "#eafbe9" },
|
||||
"diffRemovedBg": { "dark": "#3a2d2f", "light": "#fce9e8" },
|
||||
"diffContextBg": { "dark": "darkBgAlt", "light": "lightBgAlt" },
|
||||
"diffLineNumber": { "dark": "#495162", "light": "#c9c9ca" },
|
||||
"diffAddedLineNumberBg": { "dark": "#283427", "light": "#e1f3df" },
|
||||
"diffRemovedLineNumberBg": { "dark": "#36292b", "light": "#f5e2e1" },
|
||||
"markdownText": { "dark": "darkFg", "light": "lightFg" },
|
||||
"markdownHeading": { "dark": "darkPurple", "light": "lightPurple" },
|
||||
"markdownLink": { "dark": "darkBlue", "light": "lightBlue" },
|
||||
"markdownLinkText": { "dark": "darkCyan", "light": "lightCyan" },
|
||||
"markdownCode": { "dark": "darkGreen", "light": "lightGreen" },
|
||||
"markdownBlockQuote": { "dark": "darkFgMuted", "light": "lightFgMuted" },
|
||||
"markdownEmph": { "dark": "darkYellow", "light": "lightYellow" },
|
||||
"markdownStrong": { "dark": "darkOrange", "light": "lightOrange" },
|
||||
"markdownHorizontalRule": {
|
||||
"dark": "darkFgMuted",
|
||||
"light": "lightFgMuted"
|
||||
},
|
||||
"markdownListItem": { "dark": "darkBlue", "light": "lightBlue" },
|
||||
"markdownListEnumeration": { "dark": "darkCyan", "light": "lightCyan" },
|
||||
"markdownImage": { "dark": "darkBlue", "light": "lightBlue" },
|
||||
"markdownImageText": { "dark": "darkCyan", "light": "lightCyan" },
|
||||
"markdownCodeBlock": { "dark": "darkFg", "light": "lightFg" },
|
||||
"syntaxComment": { "dark": "darkFgMuted", "light": "lightFgMuted" },
|
||||
"syntaxKeyword": { "dark": "darkPurple", "light": "lightPurple" },
|
||||
"syntaxFunction": { "dark": "darkBlue", "light": "lightBlue" },
|
||||
"syntaxVariable": { "dark": "darkRed", "light": "lightRed" },
|
||||
"syntaxString": { "dark": "darkGreen", "light": "lightGreen" },
|
||||
"syntaxNumber": { "dark": "darkOrange", "light": "lightOrange" },
|
||||
"syntaxType": { "dark": "darkYellow", "light": "lightYellow" },
|
||||
"syntaxOperator": { "dark": "darkCyan", "light": "lightCyan" },
|
||||
"syntaxPunctuation": { "dark": "darkFg", "light": "lightFg" }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/theme.json",
|
||||
"defs": {
|
||||
"darkStep1": "#0a0a0a",
|
||||
"darkStep2": "#141414",
|
||||
"darkStep3": "#1e1e1e",
|
||||
"darkStep4": "#282828",
|
||||
"darkStep5": "#323232",
|
||||
"darkStep6": "#3c3c3c",
|
||||
"darkStep7": "#484848",
|
||||
"darkStep8": "#606060",
|
||||
"darkStep9": "#fab283",
|
||||
"darkStep10": "#ffc09f",
|
||||
"darkStep11": "#808080",
|
||||
"darkStep12": "#eeeeee",
|
||||
"darkSecondary": "#5c9cf5",
|
||||
"darkAccent": "#9d7cd8",
|
||||
"darkRed": "#e06c75",
|
||||
"darkOrange": "#f5a742",
|
||||
"darkGreen": "#7fd88f",
|
||||
"darkCyan": "#56b6c2",
|
||||
"darkYellow": "#e5c07b",
|
||||
"lightStep1": "#ffffff",
|
||||
"lightStep2": "#fafafa",
|
||||
"lightStep3": "#f5f5f5",
|
||||
"lightStep4": "#ebebeb",
|
||||
"lightStep5": "#e1e1e1",
|
||||
"lightStep6": "#d4d4d4",
|
||||
"lightStep7": "#b8b8b8",
|
||||
"lightStep8": "#a0a0a0",
|
||||
"lightStep9": "#3b7dd8",
|
||||
"lightStep10": "#2968c3",
|
||||
"lightStep11": "#8a8a8a",
|
||||
"lightStep12": "#1a1a1a",
|
||||
"lightSecondary": "#7b5bb6",
|
||||
"lightAccent": "#d68c27",
|
||||
"lightRed": "#d1383d",
|
||||
"lightOrange": "#d68c27",
|
||||
"lightGreen": "#3d9a57",
|
||||
"lightCyan": "#318795",
|
||||
"lightYellow": "#b0851f"
|
||||
},
|
||||
"theme": {
|
||||
"primary": {
|
||||
"dark": "darkStep9",
|
||||
"light": "lightStep9"
|
||||
},
|
||||
"secondary": {
|
||||
"dark": "darkSecondary",
|
||||
"light": "lightSecondary"
|
||||
},
|
||||
"accent": {
|
||||
"dark": "darkAccent",
|
||||
"light": "lightAccent"
|
||||
},
|
||||
"error": {
|
||||
"dark": "darkRed",
|
||||
"light": "lightRed"
|
||||
},
|
||||
"warning": {
|
||||
"dark": "darkOrange",
|
||||
"light": "lightOrange"
|
||||
},
|
||||
"success": {
|
||||
"dark": "darkGreen",
|
||||
"light": "lightGreen"
|
||||
},
|
||||
"info": {
|
||||
"dark": "darkCyan",
|
||||
"light": "lightCyan"
|
||||
},
|
||||
"text": {
|
||||
"dark": "darkStep12",
|
||||
"light": "lightStep12"
|
||||
},
|
||||
"textMuted": {
|
||||
"dark": "darkStep11",
|
||||
"light": "lightStep11"
|
||||
},
|
||||
"background": {
|
||||
"dark": "darkStep1",
|
||||
"light": "lightStep1"
|
||||
},
|
||||
"backgroundPanel": {
|
||||
"dark": "darkStep2",
|
||||
"light": "lightStep2"
|
||||
},
|
||||
"backgroundElement": {
|
||||
"dark": "darkStep3",
|
||||
"light": "lightStep3"
|
||||
},
|
||||
"border": {
|
||||
"dark": "darkStep7",
|
||||
"light": "lightStep7"
|
||||
},
|
||||
"borderActive": {
|
||||
"dark": "darkStep8",
|
||||
"light": "lightStep8"
|
||||
},
|
||||
"borderSubtle": {
|
||||
"dark": "darkStep6",
|
||||
"light": "lightStep6"
|
||||
},
|
||||
"diffAdded": {
|
||||
"dark": "#4fd6be",
|
||||
"light": "#1e725c"
|
||||
},
|
||||
"diffRemoved": {
|
||||
"dark": "#c53b53",
|
||||
"light": "#c53b53"
|
||||
},
|
||||
"diffContext": {
|
||||
"dark": "#828bb8",
|
||||
"light": "#7086b5"
|
||||
},
|
||||
"diffHunkHeader": {
|
||||
"dark": "#828bb8",
|
||||
"light": "#7086b5"
|
||||
},
|
||||
"diffHighlightAdded": {
|
||||
"dark": "#b8db87",
|
||||
"light": "#4db380"
|
||||
},
|
||||
"diffHighlightRemoved": {
|
||||
"dark": "#e26a75",
|
||||
"light": "#f52a65"
|
||||
},
|
||||
"diffAddedBg": {
|
||||
"dark": "#20303b",
|
||||
"light": "#d5e5d5"
|
||||
},
|
||||
"diffRemovedBg": {
|
||||
"dark": "#37222c",
|
||||
"light": "#f7d8db"
|
||||
},
|
||||
"diffContextBg": {
|
||||
"dark": "darkStep2",
|
||||
"light": "lightStep2"
|
||||
},
|
||||
"diffLineNumber": {
|
||||
"dark": "darkStep3",
|
||||
"light": "lightStep3"
|
||||
},
|
||||
"diffAddedLineNumberBg": {
|
||||
"dark": "#1b2b34",
|
||||
"light": "#c5d5c5"
|
||||
},
|
||||
"diffRemovedLineNumberBg": {
|
||||
"dark": "#2d1f26",
|
||||
"light": "#e7c8cb"
|
||||
},
|
||||
"markdownText": {
|
||||
"dark": "darkStep12",
|
||||
"light": "lightStep12"
|
||||
},
|
||||
"markdownHeading": {
|
||||
"dark": "darkAccent",
|
||||
"light": "lightAccent"
|
||||
},
|
||||
"markdownLink": {
|
||||
"dark": "darkStep9",
|
||||
"light": "lightStep9"
|
||||
},
|
||||
"markdownLinkText": {
|
||||
"dark": "darkCyan",
|
||||
"light": "lightCyan"
|
||||
},
|
||||
"markdownCode": {
|
||||
"dark": "darkGreen",
|
||||
"light": "lightGreen"
|
||||
},
|
||||
"markdownBlockQuote": {
|
||||
"dark": "darkYellow",
|
||||
"light": "lightYellow"
|
||||
},
|
||||
"markdownEmph": {
|
||||
"dark": "darkYellow",
|
||||
"light": "lightYellow"
|
||||
},
|
||||
"markdownStrong": {
|
||||
"dark": "darkOrange",
|
||||
"light": "lightOrange"
|
||||
},
|
||||
"markdownHorizontalRule": {
|
||||
"dark": "darkStep11",
|
||||
"light": "lightStep11"
|
||||
},
|
||||
"markdownListItem": {
|
||||
"dark": "darkStep9",
|
||||
"light": "lightStep9"
|
||||
},
|
||||
"markdownListEnumeration": {
|
||||
"dark": "darkCyan",
|
||||
"light": "lightCyan"
|
||||
},
|
||||
"markdownImage": {
|
||||
"dark": "darkStep9",
|
||||
"light": "lightStep9"
|
||||
},
|
||||
"markdownImageText": {
|
||||
"dark": "darkCyan",
|
||||
"light": "lightCyan"
|
||||
},
|
||||
"markdownCodeBlock": {
|
||||
"dark": "darkStep12",
|
||||
"light": "lightStep12"
|
||||
},
|
||||
"syntaxComment": {
|
||||
"dark": "darkStep11",
|
||||
"light": "lightStep11"
|
||||
},
|
||||
"syntaxKeyword": {
|
||||
"dark": "darkAccent",
|
||||
"light": "lightAccent"
|
||||
},
|
||||
"syntaxFunction": {
|
||||
"dark": "darkStep9",
|
||||
"light": "lightStep9"
|
||||
},
|
||||
"syntaxVariable": {
|
||||
"dark": "darkRed",
|
||||
"light": "lightRed"
|
||||
},
|
||||
"syntaxString": {
|
||||
"dark": "darkGreen",
|
||||
"light": "lightGreen"
|
||||
},
|
||||
"syntaxNumber": {
|
||||
"dark": "darkOrange",
|
||||
"light": "lightOrange"
|
||||
},
|
||||
"syntaxType": {
|
||||
"dark": "darkYellow",
|
||||
"light": "lightYellow"
|
||||
},
|
||||
"syntaxOperator": {
|
||||
"dark": "darkCyan",
|
||||
"light": "lightCyan"
|
||||
},
|
||||
"syntaxPunctuation": {
|
||||
"dark": "darkStep12",
|
||||
"light": "lightStep12"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/theme.json",
|
||||
"defs": {
|
||||
"darkStep1": "#1a1b26",
|
||||
"darkStep2": "#1e2030",
|
||||
"darkStep3": "#222436",
|
||||
"darkStep4": "#292e42",
|
||||
"darkStep5": "#3b4261",
|
||||
"darkStep6": "#545c7e",
|
||||
"darkStep7": "#737aa2",
|
||||
"darkStep8": "#9099b2",
|
||||
"darkStep9": "#82aaff",
|
||||
"darkStep10": "#89b4fa",
|
||||
"darkStep11": "#828bb8",
|
||||
"darkStep12": "#c8d3f5",
|
||||
"darkRed": "#ff757f",
|
||||
"darkOrange": "#ff966c",
|
||||
"darkYellow": "#ffc777",
|
||||
"darkGreen": "#c3e88d",
|
||||
"darkCyan": "#86e1fc",
|
||||
"darkPurple": "#c099ff",
|
||||
"lightStep1": "#e1e2e7",
|
||||
"lightStep2": "#d5d6db",
|
||||
"lightStep3": "#c8c9ce",
|
||||
"lightStep4": "#b9bac1",
|
||||
"lightStep5": "#a8aecb",
|
||||
"lightStep6": "#9699a8",
|
||||
"lightStep7": "#737a8c",
|
||||
"lightStep8": "#5a607d",
|
||||
"lightStep9": "#2e7de9",
|
||||
"lightStep10": "#1a6ce7",
|
||||
"lightStep11": "#8990a3",
|
||||
"lightStep12": "#3760bf",
|
||||
"lightRed": "#f52a65",
|
||||
"lightOrange": "#b15c00",
|
||||
"lightYellow": "#8c6c3e",
|
||||
"lightGreen": "#587539",
|
||||
"lightCyan": "#007197",
|
||||
"lightPurple": "#9854f1"
|
||||
},
|
||||
"theme": {
|
||||
"primary": {
|
||||
"dark": "darkStep9",
|
||||
"light": "lightStep9"
|
||||
},
|
||||
"secondary": {
|
||||
"dark": "darkPurple",
|
||||
"light": "lightPurple"
|
||||
},
|
||||
"accent": {
|
||||
"dark": "darkOrange",
|
||||
"light": "lightOrange"
|
||||
},
|
||||
"error": {
|
||||
"dark": "darkRed",
|
||||
"light": "lightRed"
|
||||
},
|
||||
"warning": {
|
||||
"dark": "darkOrange",
|
||||
"light": "lightOrange"
|
||||
},
|
||||
"success": {
|
||||
"dark": "darkGreen",
|
||||
"light": "lightGreen"
|
||||
},
|
||||
"info": {
|
||||
"dark": "darkStep9",
|
||||
"light": "lightStep9"
|
||||
},
|
||||
"text": {
|
||||
"dark": "darkStep12",
|
||||
"light": "lightStep12"
|
||||
},
|
||||
"textMuted": {
|
||||
"dark": "darkStep11",
|
||||
"light": "lightStep11"
|
||||
},
|
||||
"background": {
|
||||
"dark": "darkStep1",
|
||||
"light": "lightStep1"
|
||||
},
|
||||
"backgroundPanel": {
|
||||
"dark": "darkStep2",
|
||||
"light": "lightStep2"
|
||||
},
|
||||
"backgroundElement": {
|
||||
"dark": "darkStep3",
|
||||
"light": "lightStep3"
|
||||
},
|
||||
"border": {
|
||||
"dark": "darkStep7",
|
||||
"light": "lightStep7"
|
||||
},
|
||||
"borderActive": {
|
||||
"dark": "darkStep8",
|
||||
"light": "lightStep8"
|
||||
},
|
||||
"borderSubtle": {
|
||||
"dark": "darkStep6",
|
||||
"light": "lightStep6"
|
||||
},
|
||||
"diffAdded": {
|
||||
"dark": "#4fd6be",
|
||||
"light": "#1e725c"
|
||||
},
|
||||
"diffRemoved": {
|
||||
"dark": "#c53b53",
|
||||
"light": "#c53b53"
|
||||
},
|
||||
"diffContext": {
|
||||
"dark": "#828bb8",
|
||||
"light": "#7086b5"
|
||||
},
|
||||
"diffHunkHeader": {
|
||||
"dark": "#828bb8",
|
||||
"light": "#7086b5"
|
||||
},
|
||||
"diffHighlightAdded": {
|
||||
"dark": "#b8db87",
|
||||
"light": "#4db380"
|
||||
},
|
||||
"diffHighlightRemoved": {
|
||||
"dark": "#e26a75",
|
||||
"light": "#f52a65"
|
||||
},
|
||||
"diffAddedBg": {
|
||||
"dark": "#20303b",
|
||||
"light": "#d5e5d5"
|
||||
},
|
||||
"diffRemovedBg": {
|
||||
"dark": "#37222c",
|
||||
"light": "#f7d8db"
|
||||
},
|
||||
"diffContextBg": {
|
||||
"dark": "darkStep2",
|
||||
"light": "lightStep2"
|
||||
},
|
||||
"diffLineNumber": {
|
||||
"dark": "darkStep3",
|
||||
"light": "lightStep3"
|
||||
},
|
||||
"diffAddedLineNumberBg": {
|
||||
"dark": "#1b2b34",
|
||||
"light": "#c5d5c5"
|
||||
},
|
||||
"diffRemovedLineNumberBg": {
|
||||
"dark": "#2d1f26",
|
||||
"light": "#e7c8cb"
|
||||
},
|
||||
"markdownText": {
|
||||
"dark": "darkStep12",
|
||||
"light": "lightStep12"
|
||||
},
|
||||
"markdownHeading": {
|
||||
"dark": "darkPurple",
|
||||
"light": "lightPurple"
|
||||
},
|
||||
"markdownLink": {
|
||||
"dark": "darkStep9",
|
||||
"light": "lightStep9"
|
||||
},
|
||||
"markdownLinkText": {
|
||||
"dark": "darkCyan",
|
||||
"light": "lightCyan"
|
||||
},
|
||||
"markdownCode": {
|
||||
"dark": "darkGreen",
|
||||
"light": "lightGreen"
|
||||
},
|
||||
"markdownBlockQuote": {
|
||||
"dark": "darkYellow",
|
||||
"light": "lightYellow"
|
||||
},
|
||||
"markdownEmph": {
|
||||
"dark": "darkYellow",
|
||||
"light": "lightYellow"
|
||||
},
|
||||
"markdownStrong": {
|
||||
"dark": "darkOrange",
|
||||
"light": "lightOrange"
|
||||
},
|
||||
"markdownHorizontalRule": {
|
||||
"dark": "darkStep11",
|
||||
"light": "lightStep11"
|
||||
},
|
||||
"markdownListItem": {
|
||||
"dark": "darkStep9",
|
||||
"light": "lightStep9"
|
||||
},
|
||||
"markdownListEnumeration": {
|
||||
"dark": "darkCyan",
|
||||
"light": "lightCyan"
|
||||
},
|
||||
"markdownImage": {
|
||||
"dark": "darkStep9",
|
||||
"light": "lightStep9"
|
||||
},
|
||||
"markdownImageText": {
|
||||
"dark": "darkCyan",
|
||||
"light": "lightCyan"
|
||||
},
|
||||
"markdownCodeBlock": {
|
||||
"dark": "darkStep12",
|
||||
"light": "lightStep12"
|
||||
},
|
||||
"syntaxComment": {
|
||||
"dark": "darkStep11",
|
||||
"light": "lightStep11"
|
||||
},
|
||||
"syntaxKeyword": {
|
||||
"dark": "darkPurple",
|
||||
"light": "lightPurple"
|
||||
},
|
||||
"syntaxFunction": {
|
||||
"dark": "darkStep9",
|
||||
"light": "lightStep9"
|
||||
},
|
||||
"syntaxVariable": {
|
||||
"dark": "darkRed",
|
||||
"light": "lightRed"
|
||||
},
|
||||
"syntaxString": {
|
||||
"dark": "darkGreen",
|
||||
"light": "lightGreen"
|
||||
},
|
||||
"syntaxNumber": {
|
||||
"dark": "darkOrange",
|
||||
"light": "lightOrange"
|
||||
},
|
||||
"syntaxType": {
|
||||
"dark": "darkYellow",
|
||||
"light": "lightYellow"
|
||||
},
|
||||
"syntaxOperator": {
|
||||
"dark": "darkCyan",
|
||||
"light": "lightCyan"
|
||||
},
|
||||
"syntaxPunctuation": {
|
||||
"dark": "darkStep12",
|
||||
"light": "lightStep12"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,295 +0,0 @@
|
||||
package theme
|
||||
|
||||
import (
|
||||
"github.com/charmbracelet/lipgloss/v2"
|
||||
"github.com/charmbracelet/lipgloss/v2/compat"
|
||||
)
|
||||
|
||||
// TokyoNightTheme implements the Theme interface with Tokyo Night colors.
|
||||
// It provides both dark and light variants.
|
||||
type TokyoNightTheme struct {
|
||||
BaseTheme
|
||||
}
|
||||
|
||||
// NewTokyoNightTheme creates a new instance of the Tokyo Night theme.
|
||||
func NewTokyoNightTheme() *TokyoNightTheme {
|
||||
// Tokyo Night color palette with Radix-inspired scale progression
|
||||
// Dark mode colors - Tokyo Night Moon variant
|
||||
darkStep1 := "#1a1b26" // App background (bg)
|
||||
darkStep2 := "#1e2030" // Subtle background (bg_dark)
|
||||
darkStep3 := "#222436" // UI element background (bg_highlight)
|
||||
darkStep4 := "#292e42" // Hovered UI element background
|
||||
darkStep5 := "#3b4261" // Active/Selected UI element background (bg_visual)
|
||||
darkStep6 := "#545c7e" // Subtle borders and separators (dark3)
|
||||
darkStep7 := "#737aa2" // UI element border and focus rings (dark5)
|
||||
darkStep8 := "#9099b2" // Hovered UI element border
|
||||
darkStep9 := "#82aaff" // Solid backgrounds (blue)
|
||||
darkStep10 := "#89b4fa" // Hovered solid backgrounds
|
||||
darkStep11 := "#828bb8" // Low-contrast text (using fg_dark for better contrast)
|
||||
darkStep12 := "#c8d3f5" // High-contrast text (fg)
|
||||
|
||||
// Dark mode accent colors
|
||||
darkRed := "#ff757f"
|
||||
darkOrange := "#ff966c"
|
||||
darkYellow := "#ffc777"
|
||||
darkGreen := "#c3e88d"
|
||||
darkCyan := "#86e1fc"
|
||||
darkBlue := darkStep9 // Using step 9 for primary
|
||||
darkPurple := "#c099ff"
|
||||
|
||||
// Light mode colors - Tokyo Night Day variant
|
||||
lightStep1 := "#e1e2e7" // App background
|
||||
lightStep2 := "#d5d6db" // Subtle background
|
||||
lightStep3 := "#c8c9ce" // UI element background
|
||||
lightStep4 := "#b9bac1" // Hovered UI element background
|
||||
lightStep5 := "#a8aecb" // Active/Selected UI element background
|
||||
lightStep6 := "#9699a8" // Subtle borders and separators
|
||||
lightStep7 := "#737a8c" // UI element border and focus rings
|
||||
lightStep8 := "#5a607d" // Hovered UI element border
|
||||
lightStep9 := "#2e7de9" // Solid backgrounds (blue)
|
||||
lightStep10 := "#1a6ce7" // Hovered solid backgrounds
|
||||
lightStep11 := "#8990a3" // Low-contrast text (more muted)
|
||||
lightStep12 := "#3760bf" // High-contrast text
|
||||
|
||||
// Light mode accent colors
|
||||
lightRed := "#f52a65"
|
||||
lightOrange := "#b15c00"
|
||||
lightYellow := "#8c6c3e"
|
||||
lightGreen := "#587539"
|
||||
lightCyan := "#007197"
|
||||
lightBlue := lightStep9 // Using step 9 for primary
|
||||
lightPurple := "#9854f1"
|
||||
|
||||
// Unused variables to avoid compiler errors (these could be used for hover states)
|
||||
_ = darkStep4
|
||||
_ = darkStep5
|
||||
_ = darkStep10
|
||||
_ = lightStep4
|
||||
_ = lightStep5
|
||||
_ = lightStep10
|
||||
|
||||
theme := &TokyoNightTheme{}
|
||||
|
||||
// Base colors
|
||||
theme.PrimaryColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkBlue),
|
||||
Light: lipgloss.Color(lightBlue),
|
||||
}
|
||||
theme.SecondaryColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkPurple),
|
||||
Light: lipgloss.Color(lightPurple),
|
||||
}
|
||||
theme.AccentColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkOrange),
|
||||
Light: lipgloss.Color(lightOrange),
|
||||
}
|
||||
|
||||
// Status colors
|
||||
theme.ErrorColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkRed),
|
||||
Light: lipgloss.Color(lightRed),
|
||||
}
|
||||
theme.WarningColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkOrange),
|
||||
Light: lipgloss.Color(lightOrange),
|
||||
}
|
||||
theme.SuccessColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkGreen),
|
||||
Light: lipgloss.Color(lightGreen),
|
||||
}
|
||||
theme.InfoColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkBlue),
|
||||
Light: lipgloss.Color(lightBlue),
|
||||
}
|
||||
|
||||
// Text colors
|
||||
theme.TextColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkStep12),
|
||||
Light: lipgloss.Color(lightStep12),
|
||||
}
|
||||
theme.TextMutedColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkStep11),
|
||||
Light: lipgloss.Color(lightStep11),
|
||||
}
|
||||
|
||||
// Background colors
|
||||
theme.BackgroundColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkStep1),
|
||||
Light: lipgloss.Color(lightStep1),
|
||||
}
|
||||
theme.BackgroundSubtleColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkStep2),
|
||||
Light: lipgloss.Color(lightStep2),
|
||||
}
|
||||
theme.BackgroundElementColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkStep3),
|
||||
Light: lipgloss.Color(lightStep3),
|
||||
}
|
||||
|
||||
// Border colors
|
||||
theme.BorderColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkStep7),
|
||||
Light: lipgloss.Color(lightStep7),
|
||||
}
|
||||
theme.BorderActiveColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkStep8),
|
||||
Light: lipgloss.Color(lightStep8),
|
||||
}
|
||||
theme.BorderSubtleColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkStep6),
|
||||
Light: lipgloss.Color(lightStep6),
|
||||
}
|
||||
|
||||
// Diff view colors
|
||||
theme.DiffAddedColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color("#4fd6be"), // teal from palette
|
||||
Light: lipgloss.Color("#1e725c"),
|
||||
}
|
||||
theme.DiffRemovedColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color("#c53b53"), // red1 from palette
|
||||
Light: lipgloss.Color("#c53b53"),
|
||||
}
|
||||
theme.DiffContextColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color("#828bb8"), // fg_dark from palette
|
||||
Light: lipgloss.Color("#7086b5"),
|
||||
}
|
||||
theme.DiffHunkHeaderColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color("#828bb8"), // fg_dark from palette
|
||||
Light: lipgloss.Color("#7086b5"),
|
||||
}
|
||||
theme.DiffHighlightAddedColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color("#b8db87"), // git.add from palette
|
||||
Light: lipgloss.Color("#4db380"),
|
||||
}
|
||||
theme.DiffHighlightRemovedColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color("#e26a75"), // git.delete from palette
|
||||
Light: lipgloss.Color("#f52a65"),
|
||||
}
|
||||
theme.DiffAddedBgColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color("#20303b"),
|
||||
Light: lipgloss.Color("#d5e5d5"),
|
||||
}
|
||||
theme.DiffRemovedBgColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color("#37222c"),
|
||||
Light: lipgloss.Color("#f7d8db"),
|
||||
}
|
||||
theme.DiffContextBgColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkStep2),
|
||||
Light: lipgloss.Color(lightStep2),
|
||||
}
|
||||
theme.DiffLineNumberColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkStep3), // dark3 from palette
|
||||
Light: lipgloss.Color(lightStep3),
|
||||
}
|
||||
theme.DiffAddedLineNumberBgColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color("#1b2b34"),
|
||||
Light: lipgloss.Color("#c5d5c5"),
|
||||
}
|
||||
theme.DiffRemovedLineNumberBgColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color("#2d1f26"),
|
||||
Light: lipgloss.Color("#e7c8cb"),
|
||||
}
|
||||
|
||||
// Markdown colors
|
||||
theme.MarkdownTextColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkStep12),
|
||||
Light: lipgloss.Color(lightStep12),
|
||||
}
|
||||
theme.MarkdownHeadingColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkPurple),
|
||||
Light: lipgloss.Color(lightPurple),
|
||||
}
|
||||
theme.MarkdownLinkColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkBlue),
|
||||
Light: lipgloss.Color(lightBlue),
|
||||
}
|
||||
theme.MarkdownLinkTextColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkCyan),
|
||||
Light: lipgloss.Color(lightCyan),
|
||||
}
|
||||
theme.MarkdownCodeColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkGreen),
|
||||
Light: lipgloss.Color(lightGreen),
|
||||
}
|
||||
theme.MarkdownBlockQuoteColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkYellow),
|
||||
Light: lipgloss.Color(lightYellow),
|
||||
}
|
||||
theme.MarkdownEmphColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkYellow),
|
||||
Light: lipgloss.Color(lightYellow),
|
||||
}
|
||||
theme.MarkdownStrongColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkOrange),
|
||||
Light: lipgloss.Color(lightOrange),
|
||||
}
|
||||
theme.MarkdownHorizontalRuleColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkStep11),
|
||||
Light: lipgloss.Color(lightStep11),
|
||||
}
|
||||
theme.MarkdownListItemColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkBlue),
|
||||
Light: lipgloss.Color(lightBlue),
|
||||
}
|
||||
theme.MarkdownListEnumerationColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkCyan),
|
||||
Light: lipgloss.Color(lightCyan),
|
||||
}
|
||||
theme.MarkdownImageColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkBlue),
|
||||
Light: lipgloss.Color(lightBlue),
|
||||
}
|
||||
theme.MarkdownImageTextColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkCyan),
|
||||
Light: lipgloss.Color(lightCyan),
|
||||
}
|
||||
theme.MarkdownCodeBlockColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkStep12),
|
||||
Light: lipgloss.Color(lightStep12),
|
||||
}
|
||||
|
||||
// Syntax highlighting colors
|
||||
theme.SyntaxCommentColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkStep11),
|
||||
Light: lipgloss.Color(lightStep11),
|
||||
}
|
||||
theme.SyntaxKeywordColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkPurple),
|
||||
Light: lipgloss.Color(lightPurple),
|
||||
}
|
||||
theme.SyntaxFunctionColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkBlue),
|
||||
Light: lipgloss.Color(lightBlue),
|
||||
}
|
||||
theme.SyntaxVariableColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkRed),
|
||||
Light: lipgloss.Color(lightRed),
|
||||
}
|
||||
theme.SyntaxStringColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkGreen),
|
||||
Light: lipgloss.Color(lightGreen),
|
||||
}
|
||||
theme.SyntaxNumberColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkOrange),
|
||||
Light: lipgloss.Color(lightOrange),
|
||||
}
|
||||
theme.SyntaxTypeColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkYellow),
|
||||
Light: lipgloss.Color(lightYellow),
|
||||
}
|
||||
theme.SyntaxOperatorColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkCyan),
|
||||
Light: lipgloss.Color(lightCyan),
|
||||
}
|
||||
theme.SyntaxPunctuationColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(darkStep12),
|
||||
Light: lipgloss.Color(lightStep12),
|
||||
}
|
||||
|
||||
return theme
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Register the Tokyo Night theme with the theme manager
|
||||
RegisterTheme("tokyonight", NewTokyoNightTheme())
|
||||
}
|
||||
@@ -44,7 +44,11 @@ type appModel struct {
|
||||
|
||||
func (a appModel) Init() tea.Cmd {
|
||||
var cmds []tea.Cmd
|
||||
cmds = append(cmds, tea.RequestBackgroundColor)
|
||||
// https://github.com/charmbracelet/bubbletea/issues/1440
|
||||
// https://github.com/sst/opencode/issues/127
|
||||
if !util.IsWsl() {
|
||||
cmds = append(cmds, tea.RequestBackgroundColor)
|
||||
}
|
||||
cmds = append(cmds, a.app.InitializeProvider())
|
||||
cmds = append(cmds, a.editor.Init())
|
||||
cmds = append(cmds, a.messages.Init())
|
||||
@@ -66,13 +70,16 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyPressMsg:
|
||||
keyString := msg.String()
|
||||
|
||||
// 1. Handle active modal
|
||||
if a.modal != nil {
|
||||
switch msg.String() {
|
||||
switch keyString {
|
||||
// Escape always closes current modal
|
||||
case "esc", "ctrl+c":
|
||||
cmd := a.modal.Close()
|
||||
a.modal = nil
|
||||
return a, nil
|
||||
return a, cmd
|
||||
}
|
||||
|
||||
// Pass all other key presses to the modal
|
||||
@@ -84,7 +91,6 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
// 2. Check for commands that require leader
|
||||
if a.isLeaderSequence {
|
||||
matches := a.app.Commands.Matches(msg, a.isLeaderSequence)
|
||||
// Reset leader state
|
||||
a.isLeaderSequence = false
|
||||
if len(matches) > 0 {
|
||||
return a, util.CmdHandler(commands.ExecuteCommandsMsg(matches))
|
||||
@@ -92,48 +98,49 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
|
||||
// 3. Handle completions trigger
|
||||
switch msg.String() {
|
||||
case "/":
|
||||
if !a.showCompletionDialog {
|
||||
a.showCompletionDialog = true
|
||||
if keyString == "/" && !a.showCompletionDialog {
|
||||
a.showCompletionDialog = true
|
||||
|
||||
initialValue := "/"
|
||||
currentInput := a.editor.Value()
|
||||
// if the input doesn't end with a space,
|
||||
// then we want to include the last word
|
||||
if !strings.HasSuffix(currentInput, " ") {
|
||||
words := strings.Split(a.editor.Value(), " ")
|
||||
if len(words) > 0 {
|
||||
lastWord := words[len(words)-1]
|
||||
lastWord = strings.TrimSpace(lastWord)
|
||||
initialValue = lastWord + "/"
|
||||
}
|
||||
initialValue := "/"
|
||||
currentInput := a.editor.Value()
|
||||
|
||||
// if the input doesn't end with a space,
|
||||
// then we want to include the last word
|
||||
// (ie, `packages/`)
|
||||
if !strings.HasSuffix(currentInput, " ") {
|
||||
words := strings.Split(a.editor.Value(), " ")
|
||||
if len(words) > 0 {
|
||||
lastWord := words[len(words)-1]
|
||||
lastWord = strings.TrimSpace(lastWord)
|
||||
initialValue = lastWord + "/"
|
||||
}
|
||||
updated, cmd := a.completions.Update(
|
||||
app.CompletionDialogTriggerdMsg{
|
||||
InitialValue: initialValue,
|
||||
},
|
||||
)
|
||||
a.completions = updated.(dialog.CompletionDialog)
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
updated, cmd = a.completions.Update(msg)
|
||||
a.completions = updated.(dialog.CompletionDialog)
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
updated, cmd = a.editor.Update(msg)
|
||||
a.editor = updated.(chat.EditorComponent)
|
||||
cmds = append(cmds, cmd)
|
||||
return a, tea.Sequence(cmds...)
|
||||
}
|
||||
|
||||
updated, cmd := a.completions.Update(
|
||||
app.CompletionDialogTriggeredMsg{
|
||||
InitialValue: initialValue,
|
||||
},
|
||||
)
|
||||
a.completions = updated.(dialog.CompletionDialog)
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
updated, cmd = a.editor.Update(msg)
|
||||
a.editor = updated.(chat.EditorComponent)
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
updated, cmd = a.updateCompletions(msg)
|
||||
a.completions = updated.(dialog.CompletionDialog)
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
return a, tea.Sequence(cmds...)
|
||||
}
|
||||
|
||||
if a.showCompletionDialog {
|
||||
switch msg.String() {
|
||||
switch keyString {
|
||||
case "tab", "enter", "esc", "ctrl+c":
|
||||
context, contextCmd := a.completions.Update(msg)
|
||||
a.completions = context.(dialog.CompletionDialog)
|
||||
cmds = append(cmds, contextCmd)
|
||||
updated, cmd := a.updateCompletions(msg)
|
||||
a.completions = updated.(dialog.CompletionDialog)
|
||||
cmds = append(cmds, cmd)
|
||||
return a, tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
@@ -141,13 +148,10 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
a.editor = updated.(chat.EditorComponent)
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
currentInput := a.editor.Value()
|
||||
provider := a.completionManager.GetProvider(currentInput)
|
||||
a.completions.SetProvider(provider)
|
||||
updated, cmd = a.updateCompletions(msg)
|
||||
a.completions = updated.(dialog.CompletionDialog)
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
context, contextCmd := a.completions.Update(msg)
|
||||
a.completions = context.(dialog.CompletionDialog)
|
||||
cmds = append(cmds, contextCmd)
|
||||
return a, tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
@@ -173,10 +177,8 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return a, util.CmdHandler(commands.ExecuteCommandsMsg(matches))
|
||||
}
|
||||
|
||||
// 7. Fallback to editor. This shouldn't happen?
|
||||
// All printable characters were already sent, and
|
||||
// any other keypress that didn't match a command
|
||||
// is likely a noop.
|
||||
// 7. Fallback to editor. This is for other characters
|
||||
// like backspace, tab, etc.
|
||||
updatedEditor, cmd := a.editor.Update(msg)
|
||||
a.editor = updatedEditor.(chat.EditorComponent)
|
||||
return a, cmd
|
||||
@@ -187,14 +189,19 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
updated, cmd := a.messages.Update(msg)
|
||||
a.messages = updated.(chat.MessagesComponent)
|
||||
cmds = append(cmds, cmd)
|
||||
return a, tea.Batch(cmds...)
|
||||
case tea.BackgroundColorMsg:
|
||||
styles.Terminal = &styles.TerminalInfo{
|
||||
BackgroundIsDark: msg.IsDark(),
|
||||
}
|
||||
slog.Debug("Background color", "isDark", msg.IsDark())
|
||||
case modal.CloseModalMsg:
|
||||
var cmd tea.Cmd
|
||||
if a.modal != nil {
|
||||
cmd = a.modal.Close()
|
||||
}
|
||||
a.modal = nil
|
||||
return a, nil
|
||||
return a, cmd
|
||||
case commands.ExecuteCommandMsg:
|
||||
updated, cmd := a.executeCommand(commands.Command(msg))
|
||||
return updated, cmd
|
||||
@@ -211,11 +218,10 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
cmds = append(cmds, cmd)
|
||||
case dialog.CompletionDialogCloseMsg:
|
||||
a.showCompletionDialog = false
|
||||
a.completions.SetProvider(a.completionManager.DefaultProvider())
|
||||
case client.EventInstallationUpdated:
|
||||
return a, toast.NewSuccessToast(
|
||||
"New version installed",
|
||||
toast.WithTitle("opencode updated to "+msg.Properties.Version+", restart to apply."),
|
||||
"opencode updated to "+msg.Properties.Version+", restart to apply.",
|
||||
toast.WithTitle("New version installed"),
|
||||
)
|
||||
case client.EventSessionUpdated:
|
||||
if msg.Properties.Info.Id == a.app.Session.Id {
|
||||
@@ -341,7 +347,7 @@ func (a appModel) View() string {
|
||||
layoutView,
|
||||
a.status.View(),
|
||||
}
|
||||
appView := lipgloss.JoinVertical(lipgloss.Top, components...)
|
||||
appView := strings.Join(components, "\n")
|
||||
|
||||
if a.modal != nil {
|
||||
appView = a.modal.Render(appView)
|
||||
@@ -358,7 +364,7 @@ func (a appModel) executeCommand(command commands.Command) (tea.Model, tea.Cmd)
|
||||
}
|
||||
switch command.Name {
|
||||
case commands.AppHelpCommand:
|
||||
helpDialog := dialog.NewHelpDialog(a.app.Commands.Sorted())
|
||||
helpDialog := dialog.NewHelpDialog(a.app)
|
||||
a.modal = helpDialog
|
||||
case commands.EditorOpenCommand:
|
||||
if a.app.IsBusy() {
|
||||
@@ -507,30 +513,18 @@ func (a appModel) executeCommand(command commands.Command) (tea.Model, tea.Cmd)
|
||||
a.messages = updated.(chat.MessagesComponent)
|
||||
cmds = append(cmds, cmd)
|
||||
case commands.MessagesPageUpCommand:
|
||||
if a.showCompletionDialog {
|
||||
return a, nil
|
||||
}
|
||||
updated, cmd := a.messages.PageUp()
|
||||
a.messages = updated.(chat.MessagesComponent)
|
||||
cmds = append(cmds, cmd)
|
||||
case commands.MessagesPageDownCommand:
|
||||
if a.showCompletionDialog {
|
||||
return a, nil
|
||||
}
|
||||
updated, cmd := a.messages.PageDown()
|
||||
a.messages = updated.(chat.MessagesComponent)
|
||||
cmds = append(cmds, cmd)
|
||||
case commands.MessagesHalfPageUpCommand:
|
||||
if a.showCompletionDialog {
|
||||
return a, nil
|
||||
}
|
||||
updated, cmd := a.messages.HalfPageUp()
|
||||
a.messages = updated.(chat.MessagesComponent)
|
||||
cmds = append(cmds, cmd)
|
||||
case commands.MessagesHalfPageDownCommand:
|
||||
if a.showCompletionDialog {
|
||||
return a, nil
|
||||
}
|
||||
updated, cmd := a.messages.HalfPageDown()
|
||||
a.messages = updated.(chat.MessagesComponent)
|
||||
cmds = append(cmds, cmd)
|
||||
@@ -540,6 +534,15 @@ func (a appModel) executeCommand(command commands.Command) (tea.Model, tea.Cmd)
|
||||
return a, tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
func (a appModel) updateCompletions(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
currentInput := a.editor.Value()
|
||||
if currentInput != "" {
|
||||
provider := a.completionManager.GetProvider(currentInput)
|
||||
a.completions.SetProvider(provider)
|
||||
}
|
||||
return a.completions.Update(msg)
|
||||
}
|
||||
|
||||
func NewModel(app *app.App) tea.Model {
|
||||
completionManager := completions.NewCompletionManager(app)
|
||||
initialProvider := completionManager.DefaultProvider()
|
||||
@@ -556,8 +559,8 @@ func NewModel(app *app.App) tea.Model {
|
||||
messagesContainer := layout.NewContainer(messages)
|
||||
|
||||
var leaderBinding *key.Binding
|
||||
if (*app.Configg.Keybinds).Leader != nil {
|
||||
binding := key.NewBinding(key.WithKeys(*app.Configg.Keybinds.Leader))
|
||||
if (*app.Config.Keybinds).Leader != nil {
|
||||
binding := key.NewBinding(key.WithKeys(*app.Config.Keybinds.Leader))
|
||||
leaderBinding = &binding
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea/v2"
|
||||
)
|
||||
|
||||
@@ -17,3 +20,18 @@ func Clamp(v, low, high int) int {
|
||||
}
|
||||
return min(high, max(low, v))
|
||||
}
|
||||
|
||||
func IsWsl() bool {
|
||||
// Check for WSL environment variables
|
||||
if os.Getenv("WSL_DISTRO_NAME") != "" {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check /proc/version for WSL signature
|
||||
if data, err := os.ReadFile("/proc/version"); err == nil {
|
||||
version := strings.ToLower(string(data))
|
||||
return strings.Contains(version, "microsoft") || strings.Contains(version, "wsl")
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -603,7 +603,7 @@ func (a *MessageInfo_Metadata_Tool_AdditionalProperties) UnmarshalJSON(b []byte)
|
||||
var fieldVal interface{}
|
||||
err := json.Unmarshal(fieldBuf, &fieldVal)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error unmarshaling field %s: %w", fieldName, err)
|
||||
return fmt.Errorf("error unmarshalling field %s: %w", fieldName, err)
|
||||
}
|
||||
a.AdditionalProperties[fieldName] = fieldVal
|
||||
}
|
||||
|
||||
@@ -4,11 +4,15 @@ import starlight from "@astrojs/starlight"
|
||||
import solidJs from "@astrojs/solid-js"
|
||||
import cloudflare from "@astrojs/cloudflare"
|
||||
import theme from "toolbeam-docs-theme"
|
||||
import config from "./config.mjs"
|
||||
import { rehypeHeadingIds } from "@astrojs/markdown-remark"
|
||||
import rehypeAutolinkHeadings from "rehype-autolink-headings"
|
||||
|
||||
const discord = "https://discord.gg/sst"
|
||||
const github = "https://github.com/sst/opencode"
|
||||
const headerLinks = [
|
||||
{ name: "Docs", url: "/docs/" },
|
||||
{ name: "GitHub", url: github },
|
||||
]
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
@@ -28,11 +32,10 @@ export default defineConfig({
|
||||
integrations: [
|
||||
solidJs(),
|
||||
starlight({
|
||||
title: "OpenCode",
|
||||
title: "opencode",
|
||||
expressiveCode: { themes: ["github-light", "github-dark"] },
|
||||
social: [
|
||||
{ icon: "discord", label: "Discord", href: discord },
|
||||
{ icon: "github", label: "GitHub", href: github },
|
||||
{ icon: "github", label: "GitHub", href: config.github },
|
||||
],
|
||||
editLink: {
|
||||
baseUrl: `${github}/edit/master/www/`,
|
||||
@@ -52,8 +55,7 @@ export default defineConfig({
|
||||
"docs/config",
|
||||
"docs/models",
|
||||
"docs/themes",
|
||||
"docs/shortcuts",
|
||||
"docs/lsp-servers",
|
||||
"docs/keybinds",
|
||||
"docs/mcp-servers",
|
||||
],
|
||||
components: {
|
||||
@@ -62,11 +64,7 @@ export default defineConfig({
|
||||
},
|
||||
plugins: [
|
||||
theme({
|
||||
// Optionally, add your own header links
|
||||
headerLinks: [
|
||||
{ name: "Home", url: "/" },
|
||||
{ name: "Docs", url: "/docs/" },
|
||||
],
|
||||
headerLinks: config.headerLinks,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
export default {
|
||||
github: "https://github.com/sst/opencode",
|
||||
headerLinks: [
|
||||
{ name: "Home", url: "/" },
|
||||
{ name: "Docs", url: "/docs/" },
|
||||
],
|
||||
}
|
||||
@@ -21,6 +21,7 @@
|
||||
"astro": "5.7.13",
|
||||
"diff": "8.0.2",
|
||||
"js-base64": "3.7.7",
|
||||
"lang-map": "0.4.0",
|
||||
"luxon": "3.6.1",
|
||||
"marked": "15.0.12",
|
||||
"rehype-autolink-headings": "7.1.0",
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"$schema": {
|
||||
"type": "string",
|
||||
"description": "JSON schema reference for configuration validation"
|
||||
},
|
||||
"defs": {
|
||||
"type": "object",
|
||||
"description": "Color definitions that can be referenced in the theme",
|
||||
"patternProperties": {
|
||||
"^[a-zA-Z][a-zA-Z0-9_]*$": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"pattern": "^#[0-9a-fA-F]{6}$",
|
||||
"description": "Hex color value"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 255,
|
||||
"description": "ANSI color code (0-255)"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"theme": {
|
||||
"type": "object",
|
||||
"description": "Theme color definitions",
|
||||
"properties": {
|
||||
"primary": { "$ref": "#/definitions/colorValue" },
|
||||
"secondary": { "$ref": "#/definitions/colorValue" },
|
||||
"accent": { "$ref": "#/definitions/colorValue" },
|
||||
"error": { "$ref": "#/definitions/colorValue" },
|
||||
"warning": { "$ref": "#/definitions/colorValue" },
|
||||
"success": { "$ref": "#/definitions/colorValue" },
|
||||
"info": { "$ref": "#/definitions/colorValue" },
|
||||
"text": { "$ref": "#/definitions/colorValue" },
|
||||
"textMuted": { "$ref": "#/definitions/colorValue" },
|
||||
"background": { "$ref": "#/definitions/colorValue" },
|
||||
"backgroundPanel": { "$ref": "#/definitions/colorValue" },
|
||||
"backgroundElement": { "$ref": "#/definitions/colorValue" },
|
||||
"border": { "$ref": "#/definitions/colorValue" },
|
||||
"borderActive": { "$ref": "#/definitions/colorValue" },
|
||||
"borderSubtle": { "$ref": "#/definitions/colorValue" },
|
||||
"diffAdded": { "$ref": "#/definitions/colorValue" },
|
||||
"diffRemoved": { "$ref": "#/definitions/colorValue" },
|
||||
"diffContext": { "$ref": "#/definitions/colorValue" },
|
||||
"diffHunkHeader": { "$ref": "#/definitions/colorValue" },
|
||||
"diffHighlightAdded": { "$ref": "#/definitions/colorValue" },
|
||||
"diffHighlightRemoved": { "$ref": "#/definitions/colorValue" },
|
||||
"diffAddedBg": { "$ref": "#/definitions/colorValue" },
|
||||
"diffRemovedBg": { "$ref": "#/definitions/colorValue" },
|
||||
"diffContextBg": { "$ref": "#/definitions/colorValue" },
|
||||
"diffLineNumber": { "$ref": "#/definitions/colorValue" },
|
||||
"diffAddedLineNumberBg": { "$ref": "#/definitions/colorValue" },
|
||||
"diffRemovedLineNumberBg": { "$ref": "#/definitions/colorValue" },
|
||||
"markdownText": { "$ref": "#/definitions/colorValue" },
|
||||
"markdownHeading": { "$ref": "#/definitions/colorValue" },
|
||||
"markdownLink": { "$ref": "#/definitions/colorValue" },
|
||||
"markdownLinkText": { "$ref": "#/definitions/colorValue" },
|
||||
"markdownCode": { "$ref": "#/definitions/colorValue" },
|
||||
"markdownBlockQuote": { "$ref": "#/definitions/colorValue" },
|
||||
"markdownEmph": { "$ref": "#/definitions/colorValue" },
|
||||
"markdownStrong": { "$ref": "#/definitions/colorValue" },
|
||||
"markdownHorizontalRule": { "$ref": "#/definitions/colorValue" },
|
||||
"markdownListItem": { "$ref": "#/definitions/colorValue" },
|
||||
"markdownListEnumeration": { "$ref": "#/definitions/colorValue" },
|
||||
"markdownImage": { "$ref": "#/definitions/colorValue" },
|
||||
"markdownImageText": { "$ref": "#/definitions/colorValue" },
|
||||
"markdownCodeBlock": { "$ref": "#/definitions/colorValue" },
|
||||
"syntaxComment": { "$ref": "#/definitions/colorValue" },
|
||||
"syntaxKeyword": { "$ref": "#/definitions/colorValue" },
|
||||
"syntaxFunction": { "$ref": "#/definitions/colorValue" },
|
||||
"syntaxVariable": { "$ref": "#/definitions/colorValue" },
|
||||
"syntaxString": { "$ref": "#/definitions/colorValue" },
|
||||
"syntaxNumber": { "$ref": "#/definitions/colorValue" },
|
||||
"syntaxType": { "$ref": "#/definitions/colorValue" },
|
||||
"syntaxOperator": { "$ref": "#/definitions/colorValue" },
|
||||
"syntaxPunctuation": { "$ref": "#/definitions/colorValue" }
|
||||
},
|
||||
"required": [
|
||||
"primary",
|
||||
"secondary",
|
||||
"accent",
|
||||
"text",
|
||||
"textMuted",
|
||||
"background"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["theme"],
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"colorValue": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"pattern": "^#[0-9a-fA-F]{6}$",
|
||||
"description": "Hex color value (same for dark and light)"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 255,
|
||||
"description": "ANSI color code (0-255, same for dark and light)"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"pattern": "^[a-zA-Z][a-zA-Z0-9_]*$",
|
||||
"description": "Reference to another color in the theme or defs"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"dark": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"pattern": "^#[0-9a-fA-F]{6}$",
|
||||
"description": "Hex color value for dark mode"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 255,
|
||||
"description": "ANSI color code for dark mode"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"pattern": "^[a-zA-Z][a-zA-Z0-9_]*$",
|
||||
"description": "Reference to another color for dark mode"
|
||||
}
|
||||
]
|
||||
},
|
||||
"light": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"pattern": "^#[0-9a-fA-F]{6}$",
|
||||
"description": "Hex color value for light mode"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
"maximum": 255,
|
||||
"description": "ANSI color code for light mode"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"pattern": "^[a-zA-Z][a-zA-Z0-9_]*$",
|
||||
"description": "Reference to another color for light mode"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": ["dark", "light"],
|
||||
"additionalProperties": false,
|
||||
"description": "Separate colors for dark and light modes"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 439 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 438 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 432 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 442 KiB |
@@ -1,29 +1,50 @@
|
||||
---
|
||||
import config from 'virtual:starlight/user-config';
|
||||
import config from '../../config.mjs';
|
||||
import astroConfig from 'virtual:starlight/user-config';
|
||||
import { Icon } from '@astrojs/starlight/components';
|
||||
import { HeaderLinks } from 'toolbeam-docs-theme/components';
|
||||
import Default from 'toolbeam-docs-theme/overrides/Header.astro';
|
||||
import SocialIcons from 'virtual:starlight/components/SocialIcons';
|
||||
import SiteTitle from '@astrojs/starlight/components/SiteTitle.astro';
|
||||
|
||||
const path = Astro.url.pathname;
|
||||
|
||||
const links = config.social || [];
|
||||
const links = astroConfig.social || [];
|
||||
const headerLinks = config.headerLinks;
|
||||
|
||||
---
|
||||
|
||||
{ path.startsWith("/s")
|
||||
? <div class="header sl-flex">
|
||||
<div class="title-wrapper sl-flex">
|
||||
<SiteTitle {...Astro.props} />
|
||||
</div>
|
||||
<div class="middle-group sl-flex">
|
||||
<HeaderLinks {...Astro.props} />
|
||||
</div>
|
||||
</div>
|
||||
? <div class="header sl-flex">
|
||||
<div class="title-wrapper sl-flex">
|
||||
<SiteTitle {...Astro.props} />
|
||||
</div>
|
||||
<div class="middle-group sl-flex">
|
||||
{
|
||||
headerLinks?.map(({ name, url }) => (
|
||||
<a class="links" href={url}>{name}</a>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
<div class="sl-hidden md:sl-flex right-group">
|
||||
{
|
||||
links.length > 0 && (
|
||||
<div class="sl-flex social-icons">
|
||||
{links.map(({ href, icon }) => (
|
||||
<a {href} rel="me" target="_blank">
|
||||
<Icon name={icon} size="1rem" />
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
: <Default {...Astro.props}><slot /></Default>
|
||||
}
|
||||
|
||||
<style>
|
||||
.header {
|
||||
gap: var(--sl-nav-gap);
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
@@ -53,4 +74,55 @@ const links = config.social || [];
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.right-group,
|
||||
.social-icons {
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
|
||||
a {
|
||||
line-height: 1;
|
||||
|
||||
svg {
|
||||
color: var(--sl-color-text-dimmed);
|
||||
}
|
||||
}
|
||||
a.links {
|
||||
text-transform: uppercase;
|
||||
font-size: var(--sl-text-sm);
|
||||
color: var(--sl-color-text-secondary);
|
||||
line-height: normal;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 50rem) {
|
||||
:global(:root[data-has-sidebar]) {
|
||||
--__sidebar-pad: calc(2 * var(--sl-nav-pad-x));
|
||||
}
|
||||
:global(:root:not([data-has-toc])) {
|
||||
--__toc-width: 0rem;
|
||||
}
|
||||
.header {
|
||||
--__sidebar-width: max(0rem, var(--sl-content-inline-start, 0rem) - var(--sl-nav-pad-x));
|
||||
--__main-column-fr: calc(
|
||||
(
|
||||
100% + var(--__sidebar-pad, 0rem) - var(--__toc-width, var(--sl-sidebar-width)) -
|
||||
(2 * var(--__toc-width, var(--sl-nav-pad-x))) - var(--sl-content-inline-start, 0rem) -
|
||||
var(--sl-content-width)
|
||||
) / 2
|
||||
);
|
||||
display: grid;
|
||||
grid-template-columns:
|
||||
/* 1 (site title): runs up until the main content column’s left edge or the width of the title, whichever is the largest */
|
||||
minmax(
|
||||
calc(var(--__sidebar-width) + max(0rem, var(--__main-column-fr) - var(--sl-nav-gap))),
|
||||
auto
|
||||
)
|
||||
/* 2 (search box): all free space that is available. */
|
||||
1fr
|
||||
/* 3 (right items): use the space that these need. */
|
||||
auto;
|
||||
align-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { Props } from '@astrojs/starlight/props';
|
||||
|
||||
import CopyIcon from "../assets/lander/copy.svg";
|
||||
import CheckIcon from "../assets/lander/check.svg";
|
||||
import Screenshot from "../assets/themes/tokyonight.png";
|
||||
|
||||
const { data } = Astro.locals.starlightRoute.entry;
|
||||
const { title = data.title, tagline, image, actions = [] } = data.hero || {};
|
||||
@@ -19,7 +20,7 @@ const imageAttrs = {
|
||||
const github = config.social.filter(s => s.icon === 'github')[0];
|
||||
|
||||
const command = "npm i -g";
|
||||
const pkg = "opencode";
|
||||
const pkg = "opencode-ai";
|
||||
|
||||
let darkImage: ImageMetadata | undefined;
|
||||
let lightImage: ImageMetadata | undefined;
|
||||
@@ -68,15 +69,22 @@ if (image) {
|
||||
|
||||
<section class="content">
|
||||
<ul>
|
||||
<li><b>Native TUI</b>: A native terminal UI for a smoother, snappier experience.</li>
|
||||
<li><b>LSP enabled</b>: Loads the right LSPs for your codebase. Helps the LLM make fewer mistakes.</li>
|
||||
<li><b>Multi-session</b>: Start multiple conversations in a project to have agents working in parallel.</li>
|
||||
<li><b>Use any model</b>: Supports all the models from OpenAI, Anthropic, Google, OpenRouter, and more.</li>
|
||||
<li><b>Change tracking</b>: View the file changes from the current conversation in the sidebar.</li>
|
||||
<li><b>Edit with Vim</b>: Use Vim as an external editor to compose longer messages.</li>
|
||||
<li><b>Native TUI</b>: A responsive, native, themeable terminal UI.</li>
|
||||
<li><b>LSP enabled</b>: Automatically loads the right LSPs for the LLM.</li>
|
||||
<li><b>Multi-session</b>: Start multiple agents in parallel on the same project.</li>
|
||||
<li><b>Shareable links</b>: Share a link to any sessions for reference or to debug.</li>
|
||||
<li><b>Claude Pro</b>: Log in with Anthropic to use your Claude Pro or Max account.</li>
|
||||
<li><b>Use any model</b>: Supports 75+ LLM providers through <a href="https://models.dev">Models.dev</a>, including local models.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="images">
|
||||
<div>
|
||||
<p>opencode TUI with the tokyonight theme</p>
|
||||
<Image width={600} src={Screenshot} alt="opencode TUI with the tokyonight theme" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="footer">
|
||||
<div class="col1">
|
||||
<span>Version: Beta</span>
|
||||
@@ -94,7 +102,7 @@ if (image) {
|
||||
--heading-font-size: var(--sl-text-3xl);
|
||||
|
||||
margin: 1rem;
|
||||
border: 2px solid var(--sl-color-white);
|
||||
border: 2px solid var(--sl-color-border);
|
||||
}
|
||||
@media (max-width: 30rem) {
|
||||
.hero {
|
||||
@@ -126,26 +134,41 @@ section.cta {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
border-top: 2px solid var(--sl-color-white);
|
||||
border-top: 2px solid var(--sl-color-border);
|
||||
|
||||
@media (max-width: 40rem) {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
& > div {
|
||||
flex: 1;
|
||||
line-height: 1.4;
|
||||
padding: calc(var(--padding) / 2) 0.5rem;
|
||||
|
||||
@media (max-width: 40rem) {
|
||||
padding-bottom: calc(var(--padding) / 2 + 4px);
|
||||
}
|
||||
|
||||
a {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
& > div.col2 {
|
||||
@media (max-width: 50rem) {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
}
|
||||
& > div:not(.col2) {
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
@media (max-width: 30rem) {
|
||||
& > div {
|
||||
padding-bottom: calc(var(--padding) / 2 + 4px);
|
||||
}
|
||||
}
|
||||
|
||||
& > div + div {
|
||||
border-left: 2px solid var(--sl-color-white);
|
||||
border-left: 2px solid var(--sl-color-border);
|
||||
@media (max-width: 40rem) {
|
||||
border-left: none;
|
||||
border-top: 2px solid var(--sl-color-border);
|
||||
}
|
||||
}
|
||||
|
||||
.command {
|
||||
@@ -195,7 +218,7 @@ section.cta {
|
||||
}
|
||||
|
||||
section.content {
|
||||
border-top: 2px solid var(--sl-color-white);
|
||||
border-top: 2px solid var(--sl-color-border);
|
||||
padding: var(--padding);
|
||||
|
||||
ul {
|
||||
@@ -211,8 +234,63 @@ section.content {
|
||||
}
|
||||
}
|
||||
|
||||
section.images {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: stretch;
|
||||
justify-content: space-between;
|
||||
border-top: 2px solid var(--sl-color-border);
|
||||
|
||||
& > div {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: calc(var(--padding) / 4);
|
||||
padding: calc(var(--padding) / 2);
|
||||
border-width: 0;
|
||||
border-style: solid;
|
||||
border-color: var(--sl-color-border);
|
||||
|
||||
& > div, p {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
letter-spacing: -0.03125rem;
|
||||
text-transform: uppercase;
|
||||
color: var(--sl-color-text-dimmed);
|
||||
|
||||
@media (max-width: 30rem) {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
align-self: center;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
& > div + div {
|
||||
border-width: 0 0 0 2px;
|
||||
}
|
||||
|
||||
@media (max-width: 30rem) {
|
||||
& {
|
||||
flex-direction: column;
|
||||
}
|
||||
& > div + div {
|
||||
border-width: 2px 0 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
section.approach {
|
||||
border-top: 2px solid var(--sl-color-white);
|
||||
border-top: 2px solid var(--sl-color-border);
|
||||
padding: var(--padding);
|
||||
|
||||
p + p {
|
||||
@@ -221,7 +299,7 @@ section.approach {
|
||||
}
|
||||
|
||||
section.footer {
|
||||
border-top: 2px solid var(--sl-color-white);
|
||||
border-top: 2px solid var(--sl-color-border);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
@@ -233,7 +311,7 @@ section.footer {
|
||||
}
|
||||
|
||||
& > div + div {
|
||||
border-left: 2px solid var(--sl-color-white);
|
||||
border-left: 2px solid var(--sl-color-border);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
createEffect,
|
||||
createSignal,
|
||||
} from "solid-js"
|
||||
import map from "lang-map"
|
||||
import { DateTime } from "luxon"
|
||||
import { createStore, reconcile } from "solid-js/store"
|
||||
import type { Diagnostic } from "vscode-languageserver-types"
|
||||
@@ -85,7 +86,7 @@ function scrollToAnchor(id: string) {
|
||||
}
|
||||
|
||||
function stripWorkingDirectory(filePath: string, workingDir?: string) {
|
||||
if (workingDir === undefined) return filePath
|
||||
if (filePath === undefined || workingDir === undefined) return filePath
|
||||
|
||||
const prefix = workingDir.endsWith("/") ? workingDir : workingDir + "/"
|
||||
|
||||
@@ -100,8 +101,24 @@ function stripWorkingDirectory(filePath: string, workingDir?: string) {
|
||||
return filePath
|
||||
}
|
||||
|
||||
function getFileType(path: string) {
|
||||
return path.split(".").pop()
|
||||
function getShikiLang(filename: string) {
|
||||
const ext = filename
|
||||
.split('.')
|
||||
.pop()
|
||||
?.toLowerCase() ?? ''
|
||||
|
||||
// map.languages(ext) returns an array of matching Linguist language names (e.g. ['TypeScript'])
|
||||
const langs = map.languages(ext)
|
||||
const type = langs?.[0]?.toLowerCase()
|
||||
|
||||
// Overrride any specific language mappings
|
||||
const overrides: Record<string, string> = {
|
||||
"conf": "shellscript"
|
||||
}
|
||||
|
||||
return type
|
||||
? overrides[type] ?? type
|
||||
: 'plaintext'
|
||||
}
|
||||
|
||||
function formatDuration(ms: number): string {
|
||||
@@ -427,7 +444,7 @@ function MarkdownPart(props: MarkdownPartProps) {
|
||||
{...rest}
|
||||
>
|
||||
<MarkdownView
|
||||
data-elment-markdown
|
||||
data-element-markdown
|
||||
markdown={local.text}
|
||||
ref={(el) => (divEl = el)}
|
||||
/>
|
||||
@@ -709,7 +726,7 @@ export default function Share(props: {
|
||||
for (let i = 0; i < messages().length; i++) {
|
||||
const msg = messages()[i]
|
||||
|
||||
// TODO: Cleaup
|
||||
// TODO: Cleanup
|
||||
// const system = result.messages.length === 0 && msg.role === "system"
|
||||
const assistant = msg.metadata?.assistant
|
||||
|
||||
@@ -802,7 +819,7 @@ export default function Share(props: {
|
||||
<IconOpencode width={16} height={16} />
|
||||
</div>
|
||||
<Show when={store.info?.version} fallback="v0.0.1">
|
||||
<span>v${store.info?.version}</span>
|
||||
<span>v{store.info?.version}</span>
|
||||
</Show>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -980,7 +997,7 @@ export default function Share(props: {
|
||||
const prompts = assistant().system || []
|
||||
return prompts.filter(
|
||||
(p: string) =>
|
||||
!p.startsWith("You are Claude Code"),
|
||||
!p.startsWith("You are Claude"),
|
||||
)
|
||||
})
|
||||
return (
|
||||
@@ -1379,7 +1396,7 @@ export default function Share(props: {
|
||||
<Show when={showResults()}>
|
||||
<div data-part-tool-code>
|
||||
<CodeBlock
|
||||
lang={getFileType(filePath())}
|
||||
lang={getShikiLang(filePath())}
|
||||
code={preview()}
|
||||
/>
|
||||
</div>
|
||||
@@ -1483,8 +1500,8 @@ export default function Share(props: {
|
||||
<Show when={showResults()}>
|
||||
<div data-part-tool-code>
|
||||
<CodeBlock
|
||||
lang={getFileType(filePath())}
|
||||
code={content()}
|
||||
lang={getShikiLang(filePath())}
|
||||
code={args.content}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
@@ -1557,7 +1574,7 @@ export default function Share(props: {
|
||||
<DiffView
|
||||
class={styles["diff-code-block"]}
|
||||
diff={diff()}
|
||||
lang={getFileType(filePath())}
|
||||
lang={getShikiLang(filePath())}
|
||||
/>
|
||||
</div>
|
||||
</Match>
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
h1 {
|
||||
font-size: 2.75rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.15;
|
||||
line-height: 1.2;
|
||||
letter-spacing: -0.05em;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
@@ -254,7 +254,7 @@
|
||||
line-height: 18px;
|
||||
font-size: 0.875rem;
|
||||
color: var(--sl-color-text-secondary);
|
||||
max-wdith: var(--sm-tool-width);
|
||||
max-width: var(--sm-tool-width);
|
||||
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
@@ -360,7 +360,7 @@
|
||||
[data-part-type="tool-fetch"] {
|
||||
[data-part-tool-result] {
|
||||
[data-part-tool-code] {
|
||||
width: var(--md-tool-width);
|
||||
max-width: var(--md-tool-width);
|
||||
border: 1px solid var(--sl-color-divider);
|
||||
background-color: var(--sl-color-bg-surface);
|
||||
border-radius: 0.25rem;
|
||||
@@ -659,12 +659,12 @@
|
||||
}
|
||||
|
||||
&[data-expanded="true"] {
|
||||
[data-elment-markdown] {
|
||||
[data-element-markdown] {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
&[data-expanded="false"] {
|
||||
[data-elment-markdown] {
|
||||
[data-element-markdown] {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 3;
|
||||
|
||||
@@ -2,88 +2,119 @@
|
||||
title: CLI
|
||||
---
|
||||
|
||||
Once installed you can run the OpenCode CLI.
|
||||
Running the opencode CLI starts it for the current directory.
|
||||
|
||||
```bash
|
||||
opencode
|
||||
```
|
||||
|
||||
Or pass in flags. For example, to start with debug logging:
|
||||
Or you can start it for a specific working directory.
|
||||
|
||||
```bash
|
||||
opencode -d
|
||||
opencode /path/to/project
|
||||
```
|
||||
|
||||
Or start with a specific working directory.
|
||||
---
|
||||
|
||||
## Commands
|
||||
|
||||
The opencode CLI also has the following commands.
|
||||
|
||||
### run
|
||||
|
||||
Run opencode in non-interactive mode by passing a prompt directly.
|
||||
|
||||
```bash
|
||||
opencode -c /path/to/project
|
||||
opencode run [message..]
|
||||
```
|
||||
|
||||
This is useful for scripting, automation, or when you want a quick answer without launching the full TUI. For example.
|
||||
|
||||
```bash "opencode run"
|
||||
opencode run Explain the use of context in Go
|
||||
```
|
||||
|
||||
#### Flags
|
||||
|
||||
| Flag | Short | Description |
|
||||
| ----------------- | ----- | --------------------- |
|
||||
| `--continue` | `-c` | Continue the last session |
|
||||
| `--session` | `-s` | Session ID to continue |
|
||||
| `--share` | | Share the session |
|
||||
| `--model` | `-m` | Mode to use in the form of provider/model |
|
||||
|
||||
---
|
||||
|
||||
### auth
|
||||
|
||||
Command to manage credentials and login for providers.
|
||||
|
||||
```bash
|
||||
opencode auth [command]
|
||||
```
|
||||
|
||||
#### login
|
||||
|
||||
Logs you into a provider and saves them in the credentials file in `~/.local/share/opencode/auth.json`.
|
||||
|
||||
```bash
|
||||
opencode auth login
|
||||
```
|
||||
|
||||
When opencode starts up it will loads the providers from the credentials file. And if there are any keys defined in your environments or a `.env` file in your project.
|
||||
|
||||
#### list
|
||||
|
||||
Lists all the authenticated providers as stored in the credentials file.
|
||||
|
||||
```bash
|
||||
opencode auth list
|
||||
```
|
||||
|
||||
Or the short version.
|
||||
|
||||
```bash
|
||||
opencode auth ls
|
||||
```
|
||||
|
||||
#### logout
|
||||
|
||||
Logs you out of a provider by clearing it from the credentials file.
|
||||
|
||||
```bash
|
||||
opencode auth logout
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### upgrade
|
||||
|
||||
Updates opencode to the latest version or a specific version.
|
||||
|
||||
```bash
|
||||
opencode upgrade [target]
|
||||
```
|
||||
|
||||
To upgrade to the latest version.
|
||||
|
||||
```bash
|
||||
opencode upgrade
|
||||
```
|
||||
|
||||
To upgrade to a specific version.
|
||||
|
||||
```bash
|
||||
opencode upgrade v0.1.48
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Flags
|
||||
|
||||
The OpenCode CLI takes the following flags.
|
||||
The opencode CLI takes the following flags.
|
||||
|
||||
| Flag | Short | Description |
|
||||
| ----------------- | ----- | -------------------------------------------------------- |
|
||||
| `--help` | `-h` | Display help |
|
||||
| `--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` or `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 |
|
||||
|
||||
## Non-interactive
|
||||
|
||||
By default, OpenCode runs in interactive mode.
|
||||
|
||||
But you can also run OpenCode in non-interactive mode by passing a prompt directly as a command-line argument. This is useful for scripting, automation, or when you want a quick answer without launching the full TUI.
|
||||
|
||||
For example, to run a single prompt use the `-p` flag.
|
||||
|
||||
```bash "-p"
|
||||
opencode -p "Explain the use of context in Go"
|
||||
```
|
||||
|
||||
If you want to run without showing the spinner, use `-q`.
|
||||
|
||||
```bash "-q"
|
||||
opencode -p "Explain the use of context in Go" -q
|
||||
```
|
||||
|
||||
In this mode, OpenCode will process your prompt, print the result to standard output, and then exit. All **permissions are auto-approved** for the session.
|
||||
|
||||
#### Tool restrictions
|
||||
|
||||
You can control which tools the AI assistant has access to in non-interactive mode.
|
||||
|
||||
- `--allowedTools`
|
||||
|
||||
A comma-separated list of tools that the agent is allowed to use. Only these tools will be available.
|
||||
|
||||
```bash "--allowedTools"
|
||||
opencode -p "Explain the use of context in Go" --allowedTools=view,ls,glob
|
||||
```
|
||||
|
||||
- `--excludedTools`
|
||||
|
||||
Comma-separated list of tools that the agent is not allowed to use. All other tools will be available.
|
||||
|
||||
```bash "--excludedTools"
|
||||
opencode -p "Explain the use of context in Go" --excludedTools=bash,edit
|
||||
```
|
||||
|
||||
These flags are mutually exclusive. So you can either use `--allowedTools` or `--excludedTools`, but not both.
|
||||
|
||||
#### Output formats
|
||||
|
||||
In non-interactive mode, you can also set the CLI to return as JSON using `-f`.
|
||||
|
||||
```bash "-f json"
|
||||
opencode -p "Explain the use of context in Go" -f json
|
||||
```
|
||||
|
||||
By default, this is set to `text`, to return plain text.
|
||||
| Flag | Short | Description |
|
||||
| ----------------- | ----- | --------------------- |
|
||||
| `--help` | `-h` | Display help |
|
||||
| `--version` | | Print version number |
|
||||
| `--print-logs` | | Print logs to stderr |
|
||||
|
||||
@@ -2,86 +2,90 @@
|
||||
title: Config
|
||||
---
|
||||
|
||||
You can configure OpenCode using the OpenCode config. It can be places in:
|
||||
You can configure opencode using a JSON config file that can be placed in:
|
||||
|
||||
- `$HOME/.opencode.json`
|
||||
- `$XDG_CONFIG_HOME/opencode/.opencode.json`
|
||||
- Globally under `~/.config/opencode/config.json`.
|
||||
- Your project root under `opencode.json`. This is safe to be checked into Git and uses the same schema as the global one.
|
||||
|
||||
Or in the current directory, `./.opencode.json`.
|
||||
|
||||
## OpenCode config
|
||||
|
||||
The config file has the following structure.
|
||||
|
||||
```json title=".opencode.json"
|
||||
```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"
|
||||
}
|
||||
},
|
||||
"debug": false,
|
||||
"debugLSP": false
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"theme": "opencode",
|
||||
"model": "anthropic/claude-sonnet-4-20250514",
|
||||
"autoshare": false,
|
||||
"autoupdate": true
|
||||
}
|
||||
```
|
||||
|
||||
## Environment variables
|
||||
In most cases, you'll want to use the global config for things like themes, providers, or keybinds. Having a config per project is useful if you are using different providers for your company.
|
||||
|
||||
For the providers, you can also specify the keys using environment variables.
|
||||
When opencode starts up, it looks for a config file in the current directory or traverse up to the nearest Git directory.
|
||||
|
||||
| Environment Variable | Models |
|
||||
| -------------------------- | ------------------------------------------ |
|
||||
| `ANTHROPIC_API_KEY` | Claude |
|
||||
| `OPENAI_API_KEY` | OpenAI |
|
||||
| `GEMINI_API_KEY` | Google Gemini |
|
||||
| `GROQ_API_KEY` | Groq |
|
||||
| `AWS_ACCESS_KEY_ID` | Amazon Bedrock |
|
||||
| `AWS_SECRET_ACCESS_KEY` | Amazon Bedrock |
|
||||
| `AWS_REGION` | Amazon Bedrock |
|
||||
| `AZURE_OPENAI_ENDPOINT` | Azure OpenAI |
|
||||
| `AZURE_OPENAI_API_KEY` | Azure OpenAI, optional when using Entra ID |
|
||||
| `AZURE_OPENAI_API_VERSION` | Azure OpenAI |
|
||||
---
|
||||
|
||||
## Schema
|
||||
|
||||
The config file has a schema that's defined in [**`opencode.ai/config.json`**](https://opencode.ai/config.json).
|
||||
|
||||
Your editor should be able to validate and autocomplete based on the schema.
|
||||
|
||||
---
|
||||
|
||||
### Models
|
||||
|
||||
You can configure the providers and models you want to use in your opencode config through the `provider` and `model` options.
|
||||
|
||||
```json title="opencode.json"
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"provider": { },
|
||||
"model": ""
|
||||
}
|
||||
```
|
||||
|
||||
[Learn more here](/docs/models).
|
||||
|
||||
---
|
||||
|
||||
### Themes
|
||||
|
||||
You can configure the theme you want to use in your opencode config through the `theme` option.
|
||||
|
||||
```json title="opencode.json"
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"theme": ""
|
||||
}
|
||||
```
|
||||
|
||||
[Learn more here](/docs/themes).
|
||||
|
||||
---
|
||||
|
||||
### Keybinds
|
||||
|
||||
You can customize your keybinds through the `keybinds` option.
|
||||
|
||||
```json title="opencode.json"
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"keybinds": { }
|
||||
}
|
||||
```
|
||||
|
||||
[Learn more here](/docs/keybinds).
|
||||
|
||||
---
|
||||
|
||||
### MCP servers
|
||||
|
||||
You can configure MCP servers you want to use through the `mcp` option.
|
||||
|
||||
```json title="opencode.json"
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"mcp": { }
|
||||
}
|
||||
```
|
||||
|
||||
[Learn more here](/docs/mcp-servers).
|
||||
|
||||
@@ -2,57 +2,91 @@
|
||||
title: Intro
|
||||
---
|
||||
|
||||
OpenCode is an AI coding agent built natively for the terminal. It features:
|
||||
import { Tabs, TabItem } from '@astrojs/starlight/components';
|
||||
|
||||
- Native TUI for a smoother, snappier experience
|
||||
- Uses LSPs to help the LLM make fewer mistakes
|
||||
- Opening multiple conversations with the same project
|
||||
- Use of any model through the AI SDK
|
||||
- Tracks and visualizes all the file changes
|
||||
- Editing longer messages with Vim
|
||||
[**opencode**](/) is an AI coding agent built for the terminal. It features:
|
||||
|
||||
## Installation
|
||||
- A responsive, native, themeable terminal UI.
|
||||
- Automatically loads the right LSPs, so the LLMs make fewer mistakes.
|
||||
- Have multiple agents working in parallel on the same project.
|
||||
- Create shareable links to any session for reference or to debug.
|
||||
- Log in with Anthropic to use your Claude Pro or Claude Max account.
|
||||
- Supports 75+ LLM providers through [Models.dev](https://models.dev), including local models.
|
||||
|
||||
```bash
|
||||
npm i -g opencode
|
||||
```
|
||||

|
||||
|
||||
If you don't have NPM installed, you can also install the OpenCode binary through the following.
|
||||
---
|
||||
|
||||
#### Using the install script
|
||||
## Install
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="npm">
|
||||
```bash
|
||||
npm install -g opencode-ai
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem label="Bun">
|
||||
```bash
|
||||
bun install -g opencode-ai
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem label="pnpm">
|
||||
```bash
|
||||
pnpm install -g opencode-ai
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem label="Yarn">
|
||||
```bash
|
||||
yarn global add opencode-ai
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
You can also install the opencode binary through the following.
|
||||
|
||||
##### Using the install script
|
||||
|
||||
```bash
|
||||
curl -fsSL https://opencode.ai/install | bash
|
||||
```
|
||||
|
||||
Or install a specific version.
|
||||
|
||||
```bash
|
||||
curl -fsSL https://opencode.ai/install | VERSION=0.1.0 bash
|
||||
```
|
||||
|
||||
#### Using Homebrew on macOS and Linux
|
||||
##### Using Homebrew on macOS
|
||||
|
||||
```bash
|
||||
brew install sst/tap/opencode
|
||||
```
|
||||
|
||||
#### Using AUR in Arch Linux
|
||||
|
||||
With yay.
|
||||
|
||||
```bash
|
||||
yay -S opencode-bin
|
||||
```
|
||||
|
||||
Or with paru.
|
||||
##### Using Paru on Arch Linux
|
||||
|
||||
```bash
|
||||
paru -S opencode-bin
|
||||
```
|
||||
|
||||
#### Using Go
|
||||
---
|
||||
|
||||
## Providers
|
||||
|
||||
We recommend signing up for Claude Pro or Max, running `opencode auth login` and selecting Anthropic. It's the most cost-effective way to use opencode.
|
||||
|
||||
```bash
|
||||
go install github.com/sst/opencode@latest
|
||||
$ opencode auth login
|
||||
|
||||
┌ Add credential
|
||||
│
|
||||
◆ Select provider
|
||||
│ ● Anthropic (recommended)
|
||||
│ ○ OpenAI
|
||||
│ ○ Google
|
||||
│ ○ Amazon Bedrock
|
||||
│ ○ Azure
|
||||
│ ○ DeepSeek
|
||||
│ ○ Groq
|
||||
│ ...
|
||||
└
|
||||
```
|
||||
|
||||
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`.
|
||||
|
||||
The Models.dev dataset is also used to detect common environment variables like `OPENAI_API_KEY` to autoload that provider.
|
||||
|
||||
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). You can also [add them to your config](/docs/config) for yourself.
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
---
|
||||
title: Keybinds
|
||||
---
|
||||
|
||||
opencode has a list of keybinds that you can customize through the opencode config.
|
||||
|
||||
```json title="opencode.json"
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"keybinds": {
|
||||
"leader": "ctrl+x",
|
||||
"help": "<leader>h",
|
||||
"editor_open": "<leader>e",
|
||||
"session_new": "<leader>n",
|
||||
"session_list": "<leader>l",
|
||||
"session_share": "<leader>s",
|
||||
"session_interrupt": "esc",
|
||||
"session_compact": "<leader>c",
|
||||
"tool_details": "<leader>d",
|
||||
"model_list": "<leader>m",
|
||||
"theme_list": "<leader>t",
|
||||
"project_init": "<leader>i",
|
||||
"input_clear": "ctrl+c",
|
||||
"input_paste": "ctrl+v",
|
||||
"input_submit": "enter",
|
||||
"input_newline": "shift+enter,ctrl+j",
|
||||
"history_previous": "up",
|
||||
"history_next": "down",
|
||||
"messages_page_up": "pgup",
|
||||
"messages_page_down": "pgdown",
|
||||
"messages_half_page_up": "ctrl+alt+u",
|
||||
"messages_half_page_down": "ctrl+alt+d",
|
||||
"messages_previous": "ctrl+alt+k",
|
||||
"messages_next": "ctrl+alt+j",
|
||||
"messages_first": "ctrl+g",
|
||||
"messages_last": "ctrl+alt+g",
|
||||
"app_exit": "ctrl+c,<leader>q"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Leader key
|
||||
|
||||
opencode uses a `leader` key for most keybinds. This avoids conflicts in your terminal.
|
||||
|
||||
By default, `ctrl+x` is the leader key and most actions require you to first press the leader key and then the shortcut. For example, to start a new session you first press `ctrl+x` and then press `n`.
|
||||
|
||||
You don't need to use a leader key for your keybinds but we recommend doing so.
|
||||
@@ -2,7 +2,7 @@
|
||||
title: LSP servers
|
||||
---
|
||||
|
||||
OpenCode integrates with _Language Server Protocol_, or LSP to improve how the LLM interacts with your codebase.
|
||||
opencode integrates with _Language Server Protocol_, or LSP to improve how the LLM interacts with your codebase.
|
||||
|
||||
LSP servers for different languages give the LLM:
|
||||
|
||||
@@ -11,13 +11,13 @@ LSP servers for different languages give the LLM:
|
||||
|
||||
## Auto-detection
|
||||
|
||||
By default, OpenCode will **automatically detect** the languages used in your project and add the right LSP servers.
|
||||
By default, opencode will **automatically detect** the languages used in your project and add the right LSP servers.
|
||||
|
||||
## Manual configuration
|
||||
|
||||
You can also manually configure LSP servers by adding them under the `lsp` section in your OpenCode config.
|
||||
You can also manually configure LSP servers by adding them under the `lsp` section in your opencode config.
|
||||
|
||||
```json title=".opencode.json"
|
||||
```json title="opencode.json"
|
||||
{
|
||||
"lsp": {
|
||||
"go": {
|
||||
|
||||
@@ -2,27 +2,33 @@
|
||||
title: MCP servers
|
||||
---
|
||||
|
||||
You can add external tools to OpenCode using the _Model Context Protocol_, or MCP. OpenCode supports both:
|
||||
You can add external tools to opencode using the _Model Context Protocol_, or MCP. opencode supports both:
|
||||
|
||||
- Local servers that use standard input/output, `stdio`
|
||||
- Remote servers that use server-sent events `sse`
|
||||
- Local servers
|
||||
- And remote servers
|
||||
|
||||
## Add MCP servers
|
||||
Once added, MCP tools are automatically available to the LLM alongside built-in tools.
|
||||
|
||||
You can define MCP servers in your OpenCode config under the `mcpServers` section:
|
||||
---
|
||||
|
||||
## Configure
|
||||
|
||||
You can define MCP servers in your opencode config under `mcp`.
|
||||
|
||||
### Local
|
||||
|
||||
To add a local or `stdio` MCP server.
|
||||
Add a local MCP servers under `mcp.localmcp`.
|
||||
|
||||
```json title=".opencode.json" {4}
|
||||
```json title="opencode.json"
|
||||
{
|
||||
"mcpServers": {
|
||||
"local-example": {
|
||||
"type": "stdio",
|
||||
"command": "path/to/mcp-server",
|
||||
"env": [],
|
||||
"args": []
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"mcp": {
|
||||
"localmcp": {
|
||||
"type": "local",
|
||||
"command": ["bun", "x", "my-mcp-command"],
|
||||
"environment": {
|
||||
"MY_ENV_VAR": "my_env_var_value"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,22 +36,16 @@ To add a local or `stdio` MCP server.
|
||||
|
||||
### Remote
|
||||
|
||||
To add a remote or `sse` MCP server.
|
||||
Add a remote MCP servers under `mcp.remotemcp`.
|
||||
|
||||
```json title=".opencode.json" {4}
|
||||
```json title="opencode.json"
|
||||
{
|
||||
"mcpServers": {
|
||||
"remote-example": {
|
||||
"type": "sse",
|
||||
"url": "https://example.com/mcp",
|
||||
"headers": {
|
||||
"Authorization": "Bearer token"
|
||||
}
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"mcp": {
|
||||
"remotemcp": {
|
||||
"type": "remote",
|
||||
"url": "https://my-mcp-server.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Once added, MCP tools are automatically available to the LLM alongside built-in tools. They follow the same permission model; requiring user approval before execution.
|
||||
|
||||
@@ -2,33 +2,88 @@
|
||||
title: Models
|
||||
---
|
||||
|
||||
OpenCode uses the [AI SDK](https://ai-sdk.dev/) to have the support for **all the AI models**.
|
||||
opencode uses the [AI SDK](https://ai-sdk.dev/) and [Models.dev](https://models.dev) to support for **75+ LLM providers** and it supports running local models.
|
||||
|
||||
Start by setting the [keys for the providers](/docs/config) you want to use in your OpenCode config.
|
||||
---
|
||||
|
||||
## Model select
|
||||
## Providers
|
||||
|
||||
You can now select the model you want from the menu by hitting `Ctrl+O`.
|
||||
You can configure providers in your opencode config under the `provider` section.
|
||||
|
||||
## Multiple models
|
||||
### Defaults
|
||||
|
||||
You can also use specific models for specific tasks. For example, you can use a smaller model to generate the title of the conversation or to run a sub task.
|
||||
Most popular providers are preloaded by default. If you've added the credentials for a provider through `opencode auth login`, they'll be available when you start opencode.
|
||||
|
||||
```json title=".opencode.json"
|
||||
### Custom
|
||||
|
||||
You can add custom providers by specifying the npm package for the provider and the models you want to use.
|
||||
|
||||
```json title="opencode.json" {5,9-11}
|
||||
{
|
||||
"agents": {
|
||||
"primary": {
|
||||
"model": "gpt-4",
|
||||
"maxTokens": 5000
|
||||
},
|
||||
"task": {
|
||||
"model": "gpt-3.5-turbo",
|
||||
"maxTokens": 5000
|
||||
},
|
||||
"title": {
|
||||
"model": "gpt-3.5-turbo",
|
||||
"maxTokens": 80
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"provider": {
|
||||
"openrouter": {
|
||||
"npm": "@openrouter/ai-sdk-provider",
|
||||
"name": "OpenRouter",
|
||||
"options": {},
|
||||
"models": {
|
||||
"anthropic/claude-3.5-sonnet": {
|
||||
"name": "Claude 3.5 Sonnet"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Local
|
||||
|
||||
To configure a local model, specify the npm package to use and the `baseURL`.
|
||||
|
||||
```json title="opencode.json" {5,7}
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"provider": {
|
||||
"ollama": {
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"options": {
|
||||
"baseURL": "http://localhost:11434/v1"
|
||||
},
|
||||
"models": {
|
||||
"llama2": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Select a model
|
||||
|
||||
If you have multiple models, you can select the model you want by typing in:
|
||||
|
||||
```bash frame="none"
|
||||
/models
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Loading models
|
||||
|
||||
When opencode starts up, it checks for the following:
|
||||
|
||||
1. The model list in the opencode config.
|
||||
|
||||
```json title="opencode.json"
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"model": "anthropic/claude-sonnet-4-20250514"
|
||||
}
|
||||
```
|
||||
|
||||
The format here is `provider/model`.
|
||||
|
||||
2. The last used model.
|
||||
|
||||
3. The first model using an internal priority.
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
---
|
||||
title: Keyboard shortcuts
|
||||
sidebar:
|
||||
label: Shortcuts
|
||||
---
|
||||
|
||||
Below are a list of keyboard shortcuts that OpenCode supports.
|
||||
|
||||
## Global
|
||||
|
||||
| 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 pane
|
||||
|
||||
| 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 view
|
||||
|
||||
| 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
|
||||
|
||||
| Shortcut | Action |
|
||||
| ---------- | ---------------- |
|
||||
| `↑` or `k` | Previous session |
|
||||
| `↓` or `j` | Next session |
|
||||
| `Enter` | Select session |
|
||||
| `Esc` | Close dialog |
|
||||
|
||||
## Model dialog
|
||||
|
||||
| Shortcut | Action |
|
||||
| ---------- | ----------------- |
|
||||
| `↑` or `k` | Move up |
|
||||
| `↓` or `j` | Move down |
|
||||
| `←` or `h` | Previous provider |
|
||||
| `→` or `l` | Next provider |
|
||||
| `Esc` | Close dialog |
|
||||
|
||||
## Permission dialog
|
||||
|
||||
| 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 |
|
||||
@@ -2,74 +2,294 @@
|
||||
title: Themes
|
||||
---
|
||||
|
||||
OpenCode supports most common terminal themes and you can create your own custom theme.
|
||||
opencode supports a flexible JSON-based theme system that allows users to create and customize themes easily.
|
||||
|
||||
## Built-in themes
|
||||
## Theme Loading Hierarchy
|
||||
|
||||
The following predefined themes are available:
|
||||
Themes are loaded from multiple directories in the following order (later directories override earlier ones):
|
||||
|
||||
- `opencode`
|
||||
- `catppuccin`
|
||||
- `dracula`
|
||||
- `flexoki`
|
||||
- `gruvbox`
|
||||
- `monokai`
|
||||
- `onedark`
|
||||
- `tokyonight`
|
||||
- `tron`
|
||||
- `custom`
|
||||
1. **Built-in themes** - Embedded in the binary
|
||||
2. **User config directory** - `~/.config/opencode/themes/*.json` (or `$XDG_CONFIG_HOME/opencode/themes/*.json`)
|
||||
3. **Project root directory** - `<project-root>/.opencode/themes/*.json`
|
||||
4. **Current working directory** - `./.opencode/themes/*.json`
|
||||
|
||||
Where `opencode` is the default theme and `custom` let's you define your own theme.
|
||||
If multiple directories contain a theme with the same name, the theme from the directory with higher priority will be used.
|
||||
|
||||
## Setting a theme
|
||||
## Creating a Custom Theme
|
||||
|
||||
You can set your theme in your OpenCode config.
|
||||
To create a custom theme, create a JSON file in one of the theme directories:
|
||||
|
||||
```json title=".opencode.json"
|
||||
{
|
||||
"tui": {
|
||||
"theme": "monokai"
|
||||
}
|
||||
}
|
||||
```bash no-frame
|
||||
# For user-wide themes
|
||||
mkdir -p ~/.config/opencode/themes
|
||||
vim ~/.config/opencode/themes/my-theme.json
|
||||
|
||||
# For project-specific themes
|
||||
mkdir -p .opencode/themes
|
||||
vim .opencode/themes/my-theme.json
|
||||
```
|
||||
|
||||
## Create a theme
|
||||
## Theme JSON Format
|
||||
|
||||
You can create your own custom theme by setting the `theme: custom` and providing color definitions through the `customTheme`.
|
||||
Themes use a flexible JSON format with support for:
|
||||
|
||||
```json title=".opencode.json"
|
||||
- **Hex colors**: `"#ffffff"`
|
||||
- **ANSI colors**: `3` (0-255)
|
||||
- **Color references**: `"primary"` or custom definitions
|
||||
- **Dark/light variants**: `{"dark": "#000", "light": "#fff"}`
|
||||
|
||||
### Example Theme
|
||||
|
||||
```json no-frame
|
||||
{
|
||||
"tui": {
|
||||
"theme": "custom",
|
||||
"customTheme": {
|
||||
"primary": "#ffcc00",
|
||||
"secondary": "#00ccff",
|
||||
"accent": { "dark": "#aa00ff", "light": "#ddccff" },
|
||||
"error": "#ff0000"
|
||||
"$schema": "https://opencode.ai/theme.json",
|
||||
"defs": {
|
||||
"nord0": "#2E3440",
|
||||
"nord1": "#3B4252",
|
||||
"nord2": "#434C5E",
|
||||
"nord3": "#4C566A",
|
||||
"nord4": "#D8DEE9",
|
||||
"nord5": "#E5E9F0",
|
||||
"nord6": "#ECEFF4",
|
||||
"nord7": "#8FBCBB",
|
||||
"nord8": "#88C0D0",
|
||||
"nord9": "#81A1C1",
|
||||
"nord10": "#5E81AC",
|
||||
"nord11": "#BF616A",
|
||||
"nord12": "#D08770",
|
||||
"nord13": "#EBCB8B",
|
||||
"nord14": "#A3BE8C",
|
||||
"nord15": "#B48EAD"
|
||||
},
|
||||
"theme": {
|
||||
"primary": {
|
||||
"dark": "nord8",
|
||||
"light": "nord10"
|
||||
},
|
||||
"secondary": {
|
||||
"dark": "nord9",
|
||||
"light": "nord9"
|
||||
},
|
||||
"accent": {
|
||||
"dark": "nord7",
|
||||
"light": "nord7"
|
||||
},
|
||||
"error": {
|
||||
"dark": "nord11",
|
||||
"light": "nord11"
|
||||
},
|
||||
"warning": {
|
||||
"dark": "nord12",
|
||||
"light": "nord12"
|
||||
},
|
||||
"success": {
|
||||
"dark": "nord14",
|
||||
"light": "nord14"
|
||||
},
|
||||
"info": {
|
||||
"dark": "nord8",
|
||||
"light": "nord10"
|
||||
},
|
||||
"text": {
|
||||
"dark": "nord4",
|
||||
"light": "nord0"
|
||||
},
|
||||
"textMuted": {
|
||||
"dark": "nord3",
|
||||
"light": "nord1"
|
||||
},
|
||||
"background": {
|
||||
"dark": "nord0",
|
||||
"light": "nord6"
|
||||
},
|
||||
"backgroundPanel": {
|
||||
"dark": "nord1",
|
||||
"light": "nord5"
|
||||
},
|
||||
"backgroundElement": {
|
||||
"dark": "nord1",
|
||||
"light": "nord4"
|
||||
},
|
||||
"border": {
|
||||
"dark": "nord2",
|
||||
"light": "nord3"
|
||||
},
|
||||
"borderActive": {
|
||||
"dark": "nord3",
|
||||
"light": "nord2"
|
||||
},
|
||||
"borderSubtle": {
|
||||
"dark": "nord2",
|
||||
"light": "nord3"
|
||||
},
|
||||
"diffAdded": {
|
||||
"dark": "nord14",
|
||||
"light": "nord14"
|
||||
},
|
||||
"diffRemoved": {
|
||||
"dark": "nord11",
|
||||
"light": "nord11"
|
||||
},
|
||||
"diffContext": {
|
||||
"dark": "nord3",
|
||||
"light": "nord3"
|
||||
},
|
||||
"diffHunkHeader": {
|
||||
"dark": "nord3",
|
||||
"light": "nord3"
|
||||
},
|
||||
"diffHighlightAdded": {
|
||||
"dark": "nord14",
|
||||
"light": "nord14"
|
||||
},
|
||||
"diffHighlightRemoved": {
|
||||
"dark": "nord11",
|
||||
"light": "nord11"
|
||||
},
|
||||
"diffAddedBg": {
|
||||
"dark": "#3B4252",
|
||||
"light": "#E5E9F0"
|
||||
},
|
||||
"diffRemovedBg": {
|
||||
"dark": "#3B4252",
|
||||
"light": "#E5E9F0"
|
||||
},
|
||||
"diffContextBg": {
|
||||
"dark": "nord1",
|
||||
"light": "nord5"
|
||||
},
|
||||
"diffLineNumber": {
|
||||
"dark": "nord2",
|
||||
"light": "nord4"
|
||||
},
|
||||
"diffAddedLineNumberBg": {
|
||||
"dark": "#3B4252",
|
||||
"light": "#E5E9F0"
|
||||
},
|
||||
"diffRemovedLineNumberBg": {
|
||||
"dark": "#3B4252",
|
||||
"light": "#E5E9F0"
|
||||
},
|
||||
"markdownText": {
|
||||
"dark": "nord4",
|
||||
"light": "nord0"
|
||||
},
|
||||
"markdownHeading": {
|
||||
"dark": "nord8",
|
||||
"light": "nord10"
|
||||
},
|
||||
"markdownLink": {
|
||||
"dark": "nord9",
|
||||
"light": "nord9"
|
||||
},
|
||||
"markdownLinkText": {
|
||||
"dark": "nord7",
|
||||
"light": "nord7"
|
||||
},
|
||||
"markdownCode": {
|
||||
"dark": "nord14",
|
||||
"light": "nord14"
|
||||
},
|
||||
"markdownBlockQuote": {
|
||||
"dark": "nord3",
|
||||
"light": "nord3"
|
||||
},
|
||||
"markdownEmph": {
|
||||
"dark": "nord12",
|
||||
"light": "nord12"
|
||||
},
|
||||
"markdownStrong": {
|
||||
"dark": "nord13",
|
||||
"light": "nord13"
|
||||
},
|
||||
"markdownHorizontalRule": {
|
||||
"dark": "nord3",
|
||||
"light": "nord3"
|
||||
},
|
||||
"markdownListItem": {
|
||||
"dark": "nord8",
|
||||
"light": "nord10"
|
||||
},
|
||||
"markdownListEnumeration": {
|
||||
"dark": "nord7",
|
||||
"light": "nord7"
|
||||
},
|
||||
"markdownImage": {
|
||||
"dark": "nord9",
|
||||
"light": "nord9"
|
||||
},
|
||||
"markdownImageText": {
|
||||
"dark": "nord7",
|
||||
"light": "nord7"
|
||||
},
|
||||
"markdownCodeBlock": {
|
||||
"dark": "nord4",
|
||||
"light": "nord0"
|
||||
},
|
||||
"syntaxComment": {
|
||||
"dark": "nord3",
|
||||
"light": "nord3"
|
||||
},
|
||||
"syntaxKeyword": {
|
||||
"dark": "nord9",
|
||||
"light": "nord9"
|
||||
},
|
||||
"syntaxFunction": {
|
||||
"dark": "nord8",
|
||||
"light": "nord8"
|
||||
},
|
||||
"syntaxVariable": {
|
||||
"dark": "nord7",
|
||||
"light": "nord7"
|
||||
},
|
||||
"syntaxString": {
|
||||
"dark": "nord14",
|
||||
"light": "nord14"
|
||||
},
|
||||
"syntaxNumber": {
|
||||
"dark": "nord15",
|
||||
"light": "nord15"
|
||||
},
|
||||
"syntaxType": {
|
||||
"dark": "nord7",
|
||||
"light": "nord7"
|
||||
},
|
||||
"syntaxOperator": {
|
||||
"dark": "nord9",
|
||||
"light": "nord9"
|
||||
},
|
||||
"syntaxPunctuation": {
|
||||
"dark": "nord4",
|
||||
"light": "nord0"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Color keys
|
||||
### Color Definitions
|
||||
|
||||
You can define any of the following color keys in your `customTheme`.
|
||||
The `defs` section (optional) allows you to define reusable colors that can be referenced in the theme.
|
||||
|
||||
| Type | Color keys |
|
||||
| ----------------- | ------------------------------------------------------- |
|
||||
| Base colors | `primary`, `secondary`, `accent` |
|
||||
| Status colors | `error`, `warning`, `success`, `info` |
|
||||
| Text colors | `text`, `textMuted` |
|
||||
| Background colors | `background`, `backgroundSubtle`, `backgroundElement` |
|
||||
| Border colors | `border`, `borderActive`, `borderSubtle` |
|
||||
| Diff view colors | `diffAdded`, `diffRemoved`, `diffContext`, etc. |
|
||||
## Built-in Themes
|
||||
|
||||
You don't need to define all the color keys. Any undefined colors will fall back to the default `opencode` theme colors.
|
||||
opencode comes with several built-in themes:
|
||||
- `opencode` - Default opencode theme
|
||||
- `tokyonight` - Tokyonight theme
|
||||
- `everforest` - Everforest theme
|
||||
- `ayu` - Ayu dark theme
|
||||
- `catppuccin` - Catppuccin theme
|
||||
- `gruvbox` - Gruvbox theme
|
||||
- `kanagawa` - Kanagawa theme
|
||||
- `nord` - Nord theme
|
||||
- and more (see ./packages/tui/internal/theme/themes)
|
||||
|
||||
#### Color definitions
|
||||
## Using a Theme
|
||||
|
||||
Color keys can take:
|
||||
To use a theme, set it in your opencode configuration or select it from the theme dialog in the TUI.
|
||||
|
||||
1. **Hex string**: A single hex color string, like `"#aabbcc"`, that'll be used for both light and dark terminal backgrounds.
|
||||
|
||||
2. **Light and dark colors**: An object with `dark` and `light` hex colors that'll be set based on the terminal's background.
|
||||
```json title="opencode.json" {3}
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"theme": "tokyonight"
|
||||
}
|
||||
```
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user