Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
4e4cff49c0 | ||
|
|
5540503bee | ||
|
|
193718034b | ||
|
|
72108c0296 | ||
|
|
ec1c9f8cd1 | ||
|
|
a85b0a370e | ||
|
|
e7784d2864 | ||
|
|
97c4815444 | ||
|
|
7d1a1663c8 | ||
|
|
24c0ce6e53 | ||
|
|
4cdc86612c | ||
|
|
f1f3f8d12c | ||
|
|
e78d3b54bf | ||
|
|
f8a7cd372d | ||
|
|
f48eac638d | ||
|
|
e1f12f93eb | ||
|
|
7ca8334a8b | ||
|
|
f1a2b2eba4 | ||
|
|
4b132656df | ||
|
|
26bab00dab | ||
|
|
568c04753e | ||
|
|
4a06e164d2 | ||
|
|
c57b52c300 | ||
|
|
0b8f48f17f | ||
|
|
3862184ccb | ||
|
|
8619c50976 | ||
|
|
bb6b56b72a | ||
|
|
1252b65166 | ||
|
|
6840276dad |
@@ -4,6 +4,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- production
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
@@ -20,6 +21,6 @@ jobs:
|
||||
|
||||
- run: bun install
|
||||
|
||||
- run: bun sst deploy --stage=dev
|
||||
- run: bun sst deploy --stage=${{ github.ref_name }}
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
|
||||
@@ -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,127 +32,9 @@ 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`. 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",
|
||||
"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
|
||||
|
||||
@@ -163,38 +43,25 @@ 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?
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"name": "opencode",
|
||||
"devDependencies": {
|
||||
"prettier": "3.5.3",
|
||||
"sst": "3.17.4",
|
||||
"sst": "3.17.6",
|
||||
},
|
||||
},
|
||||
"packages/function": {
|
||||
@@ -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=="],
|
||||
@@ -1425,23 +1452,23 @@
|
||||
|
||||
"split2": ["split2@3.2.2", "", { "dependencies": { "readable-stream": "^3.0.0" } }, "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg=="],
|
||||
|
||||
"sst": ["sst@3.17.4", "", { "dependencies": { "aws-sdk": "2.1692.0", "aws4fetch": "1.0.18", "jose": "5.2.3", "opencontrol": "0.0.6", "openid-client": "5.6.4" }, "optionalDependencies": { "sst-darwin-arm64": "3.17.4", "sst-darwin-x64": "3.17.4", "sst-linux-arm64": "3.17.4", "sst-linux-x64": "3.17.4", "sst-linux-x86": "3.17.4", "sst-win32-arm64": "3.17.4", "sst-win32-x64": "3.17.4", "sst-win32-x86": "3.17.4" }, "bin": { "sst": "bin/sst.mjs" } }, "sha512-WpAws1ASJIilKC9/DGBhZ5wk2I4gtlzHXKpuwPC25bHWjqllv1jZiehIYhhN0PpV2pV8xCvqzyN8Gdm3J4EWQg=="],
|
||||
"sst": ["sst@3.17.6", "", { "dependencies": { "aws-sdk": "2.1692.0", "aws4fetch": "1.0.18", "jose": "5.2.3", "opencontrol": "0.0.6", "openid-client": "5.6.4" }, "optionalDependencies": { "sst-darwin-arm64": "3.17.6", "sst-darwin-x64": "3.17.6", "sst-linux-arm64": "3.17.6", "sst-linux-x64": "3.17.6", "sst-linux-x86": "3.17.6", "sst-win32-arm64": "3.17.6", "sst-win32-x64": "3.17.6", "sst-win32-x86": "3.17.6" }, "bin": { "sst": "bin/sst.mjs" } }, "sha512-p+AcqwfYQUdkxeRjCikQoTMviPCBiGoU7M0vcV6GDVmVis8hzhVw4EFfHTafZC+aWfy1Ke2UQi66vZlEVWuEqA=="],
|
||||
|
||||
"sst-darwin-arm64": ["sst-darwin-arm64@3.17.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-IJansQWlPdiaQNsJw3FQ+Q/ZXN1hzrq2Q31xG4l2HhA1doj1C3y+6s57vu4cTRDFo2OwBlC4+zlQBJHsOYGhrA=="],
|
||||
"sst-darwin-arm64": ["sst-darwin-arm64@3.17.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-6tb7KlcPR7PTi3ofQv8dX/n6Jf7pNP9VfrnYL4HBWnWrcYaZeJ5MWobILfIJ/y2jHgoqmg9e5C3266Eds0JQyw=="],
|
||||
|
||||
"sst-darwin-x64": ["sst-darwin-x64@3.17.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-mHd26/AtaQ79ajqzsutRhgEjkCxX+bXgW4KJIN0AGT3110fo2OL0x2UXmfX+sxSWOFHvJQsjFjFm4CLtQSxyBg=="],
|
||||
"sst-darwin-x64": ["sst-darwin-x64@3.17.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-lFakq6/EgTuBSjbl8Kry4pfgAPEIyn6o7ZkyRz3hz5331wUaX88yfjs3tL9JQ8Ey6jBUYxwhP/Q1n7fzIG046g=="],
|
||||
|
||||
"sst-linux-arm64": ["sst-linux-arm64@3.17.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-unaNWOY3oEI/jUUG47/2Gbreaoi/D/rLsTPeKyYEWhWEBWCojns7LfMQs1bgW0qjBGmazB2IJD4NVYhYqYQxqQ=="],
|
||||
"sst-linux-arm64": ["sst-linux-arm64@3.17.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-SdTxXMbTEdiwOqp37w31kXv97vHqSx3oK9h/76lKg7V9k5JxPJ6JMefPLhoKWwK0Zh6AndY2zo2oRoEv4SIaDw=="],
|
||||
|
||||
"sst-linux-x64": ["sst-linux-x64@3.17.4", "", { "os": "linux", "cpu": "x64" }, "sha512-zoErI6dVoRxWcmoVVrzNJWKEqfUF/MyQInEkGROGY2YsFFzOM5RD5Dsdm9q6oDGwx+NxFAhQWc8/8C+OmoW1nA=="],
|
||||
"sst-linux-x64": ["sst-linux-x64@3.17.6", "", { "os": "linux", "cpu": "x64" }, "sha512-qneh7uWDiTUYx8X1Y3h2YVw3SJ0ybBBlRrVybIvCM09JqQ8+qq/XjKXGzA/3/EF0Jr7Ug8cARSn9CwxhdQGN7Q=="],
|
||||
|
||||
"sst-linux-x86": ["sst-linux-x86@3.17.4", "", { "os": "linux", "cpu": "none" }, "sha512-7ZHS2rxzxVAxMFW3u5+GMRGGACaBMuLht8JYxqruD8mFVqk9UaPQgrFKIHGKWHLBJLVnF2AdwmlHOcEKP+UJWA=="],
|
||||
"sst-linux-x86": ["sst-linux-x86@3.17.6", "", { "os": "linux", "cpu": "none" }, "sha512-pU3D5OeqnmfxGqN31DxuwWnc1OayxhkErnITHhZ39D0MTiwbIgCapH26FuLW8B08/uxJWG8djUlOboCRhSBvWA=="],
|
||||
|
||||
"sst-win32-arm64": ["sst-win32-arm64@3.17.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-q4cedr6WD3NqeQkDvmAsIgMgPIjziIWy81wA3ZmnY6UT0jFgFus23ppLIi6F4BFJfOygvAP2PeGrRR3o8giclw=="],
|
||||
"sst-win32-arm64": ["sst-win32-arm64@3.17.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-Rr3RTYWAsH9sM9CbM/sAZCk7dB1OsSAljjJuuHMvdSAYW3RDpXEza0PBJGxnBID2eOrpswEchzMPL2d8LtL7oA=="],
|
||||
|
||||
"sst-win32-x64": ["sst-win32-x64@3.17.4", "", { "os": "win32", "cpu": "x64" }, "sha512-sSQL041YCusZ8/0ynYGe9DCmPYVZOFsemXKUA9tX4IGSDqXae1FN0Sj7HQ17JyY24UUirY1zR7LFk+7KrP6wiA=="],
|
||||
"sst-win32-x64": ["sst-win32-x64@3.17.6", "", { "os": "win32", "cpu": "x64" }, "sha512-yZ3roxwI0Wve9PFzdrrF1kfzCmIMFCCoa8qKeXY7LxCJ4QQIqHbCOccLK1Wv/MIU/mcZHWXTQVCLHw77uaa0GQ=="],
|
||||
|
||||
"sst-win32-x86": ["sst-win32-x86@3.17.4", "", { "os": "win32", "cpu": "none" }, "sha512-WhjsD2dkA2fbQ03CgwIJb+2p0osll2PTXlr7HC3L+H8wG2DgLFPjoE+6N8n6r2dVMVaDzuNwy/7J8hRB29blaw=="],
|
||||
"sst-win32-x86": ["sst-win32-x86@3.17.6", "", { "os": "win32", "cpu": "none" }, "sha512-zV7TJWPJN9PmIXr15iXFSs0tbGsa52oBR3+xiKrUj2qj9XsZe7HBFwskRnHyiFq0durZY9kk9ZtoVlpuUuzr1g=="],
|
||||
|
||||
"stacktracey": ["stacktracey@2.1.8", "", { "dependencies": { "as-table": "^1.0.36", "get-source": "^2.0.12" } }, "sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw=="],
|
||||
|
||||
@@ -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",
|
||||
"mcp": {},
|
||||
"provider": {}
|
||||
"$schema": "https://opencode.ai/config.json"
|
||||
}
|
||||
|
||||
+3
-2
@@ -5,7 +5,8 @@
|
||||
"type": "module",
|
||||
"packageManager": "bun@1.2.14",
|
||||
"scripts": {
|
||||
"typecheck": "bun run --filter='*' typecheck"
|
||||
"typecheck": "bun run --filter='*' typecheck",
|
||||
"postinstall": "./scripts/hooks"
|
||||
},
|
||||
"workspaces": {
|
||||
"packages": [
|
||||
@@ -20,7 +21,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "3.5.3",
|
||||
"sst": "3.17.4"
|
||||
"sst": "3.17.6"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -120,7 +120,7 @@ export default {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
secret,
|
||||
url: "https://dev.opencode.ai/s/" + short,
|
||||
url: "https://opencode.ai/s/" + short,
|
||||
}),
|
||||
{
|
||||
headers: { "Content-Type": "application/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.
|
||||
|
||||
@@ -2,97 +2,127 @@
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"$schema": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "JSON schema reference for configuration validation"
|
||||
},
|
||||
"theme": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "Theme name to use for the interface"
|
||||
},
|
||||
"keybinds": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"leader": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "Leader key for keybind combinations"
|
||||
},
|
||||
"help": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "Show help dialog"
|
||||
},
|
||||
"editor_open": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "Open external editor"
|
||||
},
|
||||
"session_new": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "Create a new session"
|
||||
},
|
||||
"session_list": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "List all sessions"
|
||||
},
|
||||
"session_share": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "Share current session"
|
||||
},
|
||||
"session_interrupt": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "Interrupt current session"
|
||||
},
|
||||
"session_compact": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "Toggle compact mode for session"
|
||||
},
|
||||
"tool_details": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "Show tool details"
|
||||
},
|
||||
"model_list": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "List available models"
|
||||
},
|
||||
"theme_list": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "List available themes"
|
||||
},
|
||||
"project_init": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "Initialize project configuration"
|
||||
},
|
||||
"input_clear": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "Clear input field"
|
||||
},
|
||||
"input_paste": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "Paste from clipboard"
|
||||
},
|
||||
"input_submit": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "Submit input"
|
||||
},
|
||||
"input_newline": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "Insert newline in input"
|
||||
},
|
||||
"history_previous": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "Navigate to previous history item"
|
||||
},
|
||||
"history_next": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "Navigate to next history item"
|
||||
},
|
||||
"messages_page_up": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "Scroll messages up by one page"
|
||||
},
|
||||
"messages_page_down": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "Scroll messages down by one page"
|
||||
},
|
||||
"messages_half_page_up": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "Scroll messages up by half page"
|
||||
},
|
||||
"messages_half_page_down": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "Scroll messages down by half page"
|
||||
},
|
||||
"messages_previous": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "Navigate to previous message"
|
||||
},
|
||||
"messages_next": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "Navigate to next message"
|
||||
},
|
||||
"messages_first": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "Navigate to first message"
|
||||
},
|
||||
"messages_last": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "Navigate to last message"
|
||||
},
|
||||
"app_exit": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "Exit the application"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
"additionalProperties": false,
|
||||
"description": "Custom keybind configurations"
|
||||
},
|
||||
"autoshare": {
|
||||
"type": "boolean",
|
||||
@@ -207,7 +237,8 @@
|
||||
"models"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"description": "Custom provider configurations and model overrides"
|
||||
},
|
||||
"mcp": {
|
||||
"type": "object",
|
||||
@@ -218,19 +249,22 @@
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"const": "local"
|
||||
"const": "local",
|
||||
"description": "Type of MCP server connection"
|
||||
},
|
||||
"command": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"description": "Command and arguments to run the MCP server"
|
||||
},
|
||||
"environment": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"description": "Environment variables to set when running the MCP server"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -244,10 +278,12 @@
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"const": "remote"
|
||||
"const": "remote",
|
||||
"description": "Type of MCP server connection"
|
||||
},
|
||||
"url": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "URL of the remote MCP server"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -257,7 +293,8 @@
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"description": "MCP (Model Context Protocol) server configurations"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,9 +80,9 @@ function main() {
|
||||
|
||||
// Create symlink to the actual binary
|
||||
fs.symlinkSync(binaryPath, binScript)
|
||||
console.log(`OpenCode binary symlinked: ${binScript} -> ${binaryPath}`)
|
||||
console.log(`opencode binary symlinked: ${binScript} -> ${binaryPath}`)
|
||||
} catch (error) {
|
||||
console.error("Failed to create OpenCode binary symlink:", error.message)
|
||||
console.error("Failed to create opencode binary symlink:", error.message)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
|
||||
@@ -9,14 +9,14 @@ import { map, pipe, sortBy, values } from "remeda"
|
||||
|
||||
export const AuthCommand = cmd({
|
||||
command: "auth",
|
||||
describe: "manage credentials",
|
||||
describe: "Manage credentials",
|
||||
builder: (yargs) =>
|
||||
yargs
|
||||
.command(AuthLoginCommand)
|
||||
.command(AuthLogoutCommand)
|
||||
.command(AuthListCommand)
|
||||
.demandCommand(),
|
||||
async handler() {},
|
||||
async handler() { },
|
||||
})
|
||||
|
||||
export const AuthListCommand = cmd({
|
||||
|
||||
@@ -11,35 +11,47 @@ import { Flag } from "../../flag/flag"
|
||||
import { Config } from "../../config/config"
|
||||
|
||||
const TOOL: Record<string, [string, string]> = {
|
||||
opencode_todowrite: ["Todo", UI.Style.TEXT_WARNING_BOLD],
|
||||
opencode_todoread: ["Todo", UI.Style.TEXT_WARNING_BOLD],
|
||||
opencode_bash: ["Bash", UI.Style.TEXT_DANGER_BOLD],
|
||||
opencode_edit: ["Edit", UI.Style.TEXT_SUCCESS_BOLD],
|
||||
opencode_glob: ["Glob", UI.Style.TEXT_INFO_BOLD],
|
||||
opencode_grep: ["Grep", UI.Style.TEXT_INFO_BOLD],
|
||||
opencode_list: ["List", UI.Style.TEXT_INFO_BOLD],
|
||||
opencode_read: ["Read", UI.Style.TEXT_HIGHLIGHT_BOLD],
|
||||
opencode_write: ["Write", UI.Style.TEXT_SUCCESS_BOLD],
|
||||
todowrite: ["Todo", UI.Style.TEXT_WARNING_BOLD],
|
||||
todoread: ["Todo", UI.Style.TEXT_WARNING_BOLD],
|
||||
bash: ["Bash", UI.Style.TEXT_DANGER_BOLD],
|
||||
edit: ["Edit", UI.Style.TEXT_SUCCESS_BOLD],
|
||||
glob: ["Glob", UI.Style.TEXT_INFO_BOLD],
|
||||
grep: ["Grep", UI.Style.TEXT_INFO_BOLD],
|
||||
list: ["List", UI.Style.TEXT_INFO_BOLD],
|
||||
read: ["Read", UI.Style.TEXT_HIGHLIGHT_BOLD],
|
||||
write: ["Write", UI.Style.TEXT_SUCCESS_BOLD],
|
||||
websearch: ["Search", UI.Style.TEXT_DIM_BOLD],
|
||||
}
|
||||
|
||||
export const RunCommand = cmd({
|
||||
command: "run [message..]",
|
||||
describe: "Run OpenCode with a message",
|
||||
describe: "Run opencode with a message",
|
||||
builder: (yargs: Argv) => {
|
||||
return yargs
|
||||
.positional("message", {
|
||||
describe: "Message to send",
|
||||
describe: "message to send",
|
||||
type: "string",
|
||||
array: true,
|
||||
default: [],
|
||||
})
|
||||
.option("continue", {
|
||||
alias: ["c"],
|
||||
describe: "Continue the last session",
|
||||
type: "boolean",
|
||||
})
|
||||
.option("session", {
|
||||
alias: ["s"],
|
||||
describe: "Session ID to continue",
|
||||
type: "string",
|
||||
})
|
||||
.option("share", {
|
||||
type: "boolean",
|
||||
describe: "Share the session",
|
||||
describe: "share the session",
|
||||
})
|
||||
.option("model", {
|
||||
type: "string",
|
||||
alias: ["m"],
|
||||
describe: "Model to use in the format of provider/model",
|
||||
})
|
||||
},
|
||||
handler: async (args) => {
|
||||
@@ -50,9 +62,22 @@ export const RunCommand = cmd({
|
||||
},
|
||||
async () => {
|
||||
await Share.init()
|
||||
const session = args.session
|
||||
? await Session.get(args.session)
|
||||
: await Session.create()
|
||||
const session = await (async () => {
|
||||
if (args.continue) {
|
||||
const first = await Session.list().next()
|
||||
if (first.done) return
|
||||
return first.value
|
||||
}
|
||||
|
||||
if (args.session) return Session.get(args.session)
|
||||
|
||||
return Session.create()
|
||||
})()
|
||||
|
||||
if (!session) {
|
||||
UI.error("Session not found")
|
||||
return
|
||||
}
|
||||
|
||||
UI.empty()
|
||||
UI.println(UI.logo())
|
||||
@@ -65,13 +90,15 @@ export const RunCommand = cmd({
|
||||
await Session.share(session.id)
|
||||
UI.println(
|
||||
UI.Style.TEXT_INFO_BOLD +
|
||||
"~ https://dev.opencode.ai/s/" +
|
||||
session.id.slice(-8),
|
||||
"~ https://opencode.ai/s/" +
|
||||
session.id.slice(-8),
|
||||
)
|
||||
}
|
||||
UI.empty()
|
||||
|
||||
const { providerID, modelID } = await Provider.defaultModel()
|
||||
const { providerID, modelID } = args.model
|
||||
? Provider.parseModel(args.model)
|
||||
: await Provider.defaultModel()
|
||||
UI.println(
|
||||
UI.Style.TEXT_NORMAL_BOLD + "@ ",
|
||||
UI.Style.TEXT_NORMAL + `${providerID}/${modelID}`,
|
||||
@@ -82,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")
|
||||
}
|
||||
@@ -71,4 +71,8 @@ export namespace UI {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export function error(message: string) {
|
||||
println(Style.TEXT_DANGER_BOLD + "Error: " + Style.TEXT_NORMAL + message)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
@@ -44,19 +27,27 @@ export namespace Config {
|
||||
|
||||
export const McpLocal = z
|
||||
.object({
|
||||
type: z.literal("local"),
|
||||
command: z.string().array(),
|
||||
environment: z.record(z.string(), z.string()).optional(),
|
||||
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"),
|
||||
})
|
||||
.strict()
|
||||
.openapi({
|
||||
ref: "Config.McpLocal",
|
||||
})
|
||||
|
||||
export const McpRemote = z
|
||||
.object({
|
||||
type: z.literal("remote"),
|
||||
url: z.string(),
|
||||
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,42 +57,85 @@ export namespace Config {
|
||||
|
||||
export const Keybinds = z
|
||||
.object({
|
||||
leader: z.string().optional(),
|
||||
help: z.string().optional(),
|
||||
editor_open: z.string().optional(),
|
||||
session_new: z.string().optional(),
|
||||
session_list: z.string().optional(),
|
||||
session_share: z.string().optional(),
|
||||
session_interrupt: z.string().optional(),
|
||||
session_compact: z.string().optional(),
|
||||
tool_details: z.string().optional(),
|
||||
model_list: z.string().optional(),
|
||||
theme_list: z.string().optional(),
|
||||
project_init: z.string().optional(),
|
||||
input_clear: z.string().optional(),
|
||||
input_paste: z.string().optional(),
|
||||
input_submit: z.string().optional(),
|
||||
input_newline: z.string().optional(),
|
||||
history_previous: z.string().optional(),
|
||||
history_next: z.string().optional(),
|
||||
messages_page_up: z.string().optional(),
|
||||
messages_page_down: z.string().optional(),
|
||||
messages_half_page_up: z.string().optional(),
|
||||
messages_half_page_down: z.string().optional(),
|
||||
messages_previous: z.string().optional(),
|
||||
messages_next: z.string().optional(),
|
||||
messages_first: z.string().optional(),
|
||||
messages_last: z.string().optional(),
|
||||
app_exit: z.string().optional(),
|
||||
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"),
|
||||
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"),
|
||||
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"),
|
||||
messages_next: z.string().optional().describe("Navigate to next 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(),
|
||||
theme: z.string().optional(),
|
||||
keybinds: Keybinds.optional(),
|
||||
$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()
|
||||
.optional()
|
||||
@@ -127,9 +161,14 @@ export namespace Config {
|
||||
options: z.record(z.any()).optional(),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
mcp: z.record(z.string(), Mcp).optional(),
|
||||
.optional()
|
||||
.describe("Custom provider configurations and model overrides"),
|
||||
mcp: z
|
||||
.record(z.string(), Mcp)
|
||||
.optional()
|
||||
.describe("MCP (Model Context Protocol) server configurations"),
|
||||
})
|
||||
.strict()
|
||||
.openapi({
|
||||
ref: "Config.Info",
|
||||
})
|
||||
@@ -137,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: {
|
||||
@@ -150,6 +186,7 @@ export namespace Config {
|
||||
.then(async (mod) => {
|
||||
const { provider, model, ...rest } = mod.default
|
||||
if (provider && model) result.model = `${provider}/${model}`
|
||||
result["$schema"] = "https://opencode.ai/config.json"
|
||||
result = mergeDeep(result, rest)
|
||||
await Bun.write(
|
||||
path.join(Global.Path.config, "config.json"),
|
||||
@@ -158,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 = {
|
||||
@@ -41,6 +44,10 @@ export namespace Installation {
|
||||
return VERSION.startsWith("0.0.0")
|
||||
}
|
||||
|
||||
export function isDev() {
|
||||
return VERSION === "dev"
|
||||
}
|
||||
|
||||
export async function method() {
|
||||
if (process.execPath.includes(path.join(".opencode", "bin"))) return "curl"
|
||||
const exec = process.execPath.toLowerCase()
|
||||
@@ -62,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) => {
|
||||
@@ -93,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",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import z from "zod"
|
||||
import path from "path"
|
||||
import { App } from "../app/app"
|
||||
import { Config } from "../config/config"
|
||||
import { mergeDeep, sortBy } from "remeda"
|
||||
@@ -24,14 +23,17 @@ import { ModelsDev } from "./models"
|
||||
import { NamedError } from "../util/error"
|
||||
import { Auth } from "../auth"
|
||||
import { TaskTool } from "../tool/task"
|
||||
import { Global } from "../global"
|
||||
|
||||
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"
|
||||
|
||||
@@ -46,30 +48,52 @@ 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,
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
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)
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -82,6 +106,7 @@ export namespace Provider {
|
||||
[providerID: string]: {
|
||||
source: Source
|
||||
info: ModelsDev.Provider
|
||||
getModel?: (sdk: any, modelID: string) => Promise<any>
|
||||
options: Record<string, any>
|
||||
}
|
||||
} = {}
|
||||
@@ -97,6 +122,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) {
|
||||
@@ -112,35 +138,10 @@ export namespace Provider {
|
||||
}
|
||||
provider.options = mergeDeep(provider.options, options)
|
||||
provider.source = source
|
||||
provider.getModel = getModel ?? provider.getModel
|
||||
}
|
||||
|
||||
const configProviders = Object.entries(config.provider ?? {})
|
||||
for await (const providerPath of new Bun.Glob("*/provider.toml").scan({
|
||||
cwd: Global.Path.providers,
|
||||
})) {
|
||||
const [providerID] = providerPath.split("/")
|
||||
const toml = await import(
|
||||
path.join(Global.Path.providers, providerPath),
|
||||
{
|
||||
with: {
|
||||
type: "toml",
|
||||
},
|
||||
}
|
||||
).then((mod) => mod.default)
|
||||
toml.models = {}
|
||||
const modelsPath = path.join(Global.Path.providers, providerID, "models")
|
||||
for await (const modelPath of new Bun.Glob("**/*.toml").scan({
|
||||
cwd: modelsPath,
|
||||
})) {
|
||||
const modelID = modelPath.slice(0, -5)
|
||||
toml.models[modelID] = await import(path.join(modelsPath, modelPath), {
|
||||
with: {
|
||||
type: "toml",
|
||||
},
|
||||
})
|
||||
}
|
||||
configProviders.unshift([providerID, toml])
|
||||
}
|
||||
|
||||
for (const [providerID, provider] of configProviders) {
|
||||
const existing = database[providerID]
|
||||
@@ -160,13 +161,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,
|
||||
@@ -201,7 +208,8 @@ 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
|
||||
@@ -209,7 +217,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 })
|
||||
}
|
||||
|
||||
@@ -260,9 +272,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,
|
||||
@@ -300,9 +312,14 @@ export namespace Provider {
|
||||
|
||||
export async function defaultModel() {
|
||||
const cfg = await Config.get()
|
||||
if (cfg.model) return parseModel(cfg.model)
|
||||
const provider = await list()
|
||||
.then((val) => Object.values(val))
|
||||
.then((x) => x.find((p) => !cfg.provider || cfg.provider === p.info.id))
|
||||
.then((x) =>
|
||||
x.find(
|
||||
(p) => !cfg.provider || Object.keys(cfg.provider).includes(p.info.id),
|
||||
),
|
||||
)
|
||||
if (!provider) throw new Error("no providers found")
|
||||
const [model] = sort(Object.values(provider.info.models))
|
||||
if (!model) throw new Error("no models found")
|
||||
@@ -312,6 +329,14 @@ export namespace Provider {
|
||||
}
|
||||
}
|
||||
|
||||
export function parseModel(model: string) {
|
||||
const [providerID, ...rest] = model.split("/")
|
||||
return {
|
||||
providerID: providerID,
|
||||
modelID: rest.join("/"),
|
||||
}
|
||||
}
|
||||
|
||||
const TOOLS = [
|
||||
BashTool,
|
||||
EditTool,
|
||||
@@ -332,11 +357,15 @@ export namespace Provider {
|
||||
]
|
||||
|
||||
const TOOL_MAPPING: Record<string, Tool.Info[]> = {
|
||||
anthropic: TOOLS.filter((t) => t.id !== "opencode.patch"),
|
||||
anthropic: TOOLS.filter((t) => t.id !== "patch"),
|
||||
openai: TOOLS.map((t) => ({
|
||||
...t,
|
||||
parameters: optionalToNullable(t.parameters),
|
||||
})),
|
||||
azure: TOOLS.map((t) => ({
|
||||
...t,
|
||||
parameters: optionalToNullable(t.parameters),
|
||||
})),
|
||||
google: TOOLS,
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { CoreMessage, LanguageModelV1Prompt } from "ai"
|
||||
import { unique } from "remeda"
|
||||
|
||||
export namespace ProviderTransform {
|
||||
export function message(
|
||||
msgs: LanguageModelV1Prompt,
|
||||
providerID: string,
|
||||
modelID: string,
|
||||
) {
|
||||
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 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"
|
||||
@@ -32,6 +33,7 @@ import { Flag } from "../flag/flag"
|
||||
import type { ModelsDev } from "../provider/models"
|
||||
import { Installation } from "../installation"
|
||||
import { Config } from "../config/config"
|
||||
import { ProviderTransform } from "../provider/transform"
|
||||
|
||||
export namespace Session {
|
||||
const log = Log.create({ service: "session" })
|
||||
@@ -151,12 +153,21 @@ export namespace Session {
|
||||
}
|
||||
})
|
||||
await Storage.writeJSON<ShareInfo>("session/share/" + id, share)
|
||||
await Share.sync("session/info/" + id, session)
|
||||
for (const msg of await messages(id)) {
|
||||
await Share.sync("session/message/" + id + "/" + msg.id, msg)
|
||||
}
|
||||
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)
|
||||
@@ -260,20 +271,12 @@ 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 => ({
|
||||
role: "system",
|
||||
content: x,
|
||||
providerOptions: {
|
||||
...(input.providerID === "anthropic"
|
||||
? {
|
||||
anthropic: {
|
||||
cacheControl: { type: "ephemeral" },
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
}),
|
||||
),
|
||||
...convertToCoreMessages([
|
||||
@@ -512,20 +515,12 @@ export namespace Session {
|
||||
toolCallStreaming: true,
|
||||
abortSignal: abort.signal,
|
||||
maxSteps: 1000,
|
||||
providerOptions: model.info.options,
|
||||
messages: [
|
||||
...system.map(
|
||||
(x, index): CoreMessage => ({
|
||||
(x): CoreMessage => ({
|
||||
role: "system",
|
||||
content: x,
|
||||
providerOptions: {
|
||||
...(input.providerID === "anthropic" && index < 4
|
||||
? {
|
||||
anthropic: {
|
||||
cacheControl: { type: "ephemeral" },
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
}),
|
||||
),
|
||||
...convertToCoreMessages(
|
||||
@@ -533,10 +528,24 @@ export namespace Session {
|
||||
),
|
||||
],
|
||||
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) {
|
||||
@@ -596,7 +605,7 @@ export namespace Session {
|
||||
break
|
||||
|
||||
case "tool-call-delta":
|
||||
break
|
||||
continue
|
||||
|
||||
// for some reason ai sdk claims to not send this part but it does
|
||||
// @ts-expect-error
|
||||
@@ -631,6 +640,7 @@ export namespace Session {
|
||||
l.info("unhandled", {
|
||||
type: value.type,
|
||||
})
|
||||
continue
|
||||
}
|
||||
await updateMessage(next)
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { App } from "../app/app"
|
||||
import { Bus } from "../bus"
|
||||
import { Installation } from "../installation"
|
||||
import { Session } from "../session"
|
||||
import { Storage } from "../storage/storage"
|
||||
import { Log } from "../util/log"
|
||||
@@ -56,7 +57,10 @@ export namespace Share {
|
||||
}
|
||||
|
||||
export const URL =
|
||||
process.env["OPENCODE_API"] ?? "https://api.dev.opencode.ai"
|
||||
process.env["OPENCODE_API"] ??
|
||||
(Installation.isSnapshot() || Installation.isDev()
|
||||
? "https://api.dev.opencode.ai"
|
||||
: "https://api.opencode.ai")
|
||||
|
||||
export async function create(sessionID: string) {
|
||||
return fetch(`${URL}/share_create`, {
|
||||
@@ -66,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>
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ const DEFAULT_TIMEOUT = 1 * 60 * 1000
|
||||
const MAX_TIMEOUT = 10 * 60 * 1000
|
||||
|
||||
export const BashTool = Tool.define({
|
||||
id: "opencode.bash",
|
||||
id: "bash",
|
||||
description: DESCRIPTION,
|
||||
parameters: z.object({
|
||||
command: z.string().describe("The command to execute"),
|
||||
|
||||
@@ -9,7 +9,7 @@ import DESCRIPTION from "./edit.txt"
|
||||
import { App } from "../app/app"
|
||||
|
||||
export const EditTool = Tool.define({
|
||||
id: "opencode.edit",
|
||||
id: "edit",
|
||||
description: DESCRIPTION,
|
||||
parameters: z.object({
|
||||
filePath: z.string().describe("The absolute path to the file to modify"),
|
||||
@@ -35,7 +35,7 @@ export const EditTool = Tool.define({
|
||||
: path.join(app.path.cwd, params.filePath)
|
||||
|
||||
await Permission.ask({
|
||||
id: "opencode.edit",
|
||||
id: "edit",
|
||||
sessionID: ctx.sessionID,
|
||||
title: "Edit this file: " + filepath,
|
||||
metadata: {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { App } from "../app/app"
|
||||
import DESCRIPTION from "./glob.txt"
|
||||
|
||||
export const GlobTool = Tool.define({
|
||||
id: "opencode.glob",
|
||||
id: "glob",
|
||||
description: DESCRIPTION,
|
||||
parameters: z.object({
|
||||
pattern: z.string().describe("The glob pattern to match files against"),
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Ripgrep } from "../external/ripgrep"
|
||||
import DESCRIPTION from "./grep.txt"
|
||||
|
||||
export const GrepTool = Tool.define({
|
||||
id: "opencode.grep",
|
||||
id: "grep",
|
||||
description: DESCRIPTION,
|
||||
parameters: z.object({
|
||||
pattern: z
|
||||
|
||||
@@ -21,7 +21,7 @@ export const IGNORE_PATTERNS = [
|
||||
const LIMIT = 100
|
||||
|
||||
export const ListTool = Tool.define({
|
||||
id: "opencode.list",
|
||||
id: "list",
|
||||
description: DESCRIPTION,
|
||||
parameters: z.object({
|
||||
path: z
|
||||
|
||||
@@ -6,7 +6,7 @@ import { App } from "../app/app"
|
||||
import DESCRIPTION from "./lsp-diagnostics.txt"
|
||||
|
||||
export const LspDiagnosticTool = Tool.define({
|
||||
id: "opencode.lsp_diagnostics",
|
||||
id: "lsp_diagnostics",
|
||||
description: DESCRIPTION,
|
||||
parameters: z.object({
|
||||
path: z.string().describe("The path to the file to get diagnostics."),
|
||||
|
||||
@@ -6,7 +6,7 @@ import { App } from "../app/app"
|
||||
import DESCRIPTION from "./lsp-hover.txt"
|
||||
|
||||
export const LspHoverTool = Tool.define({
|
||||
id: "opencode.lsp_hover",
|
||||
id: "lsp_hover",
|
||||
description: DESCRIPTION,
|
||||
parameters: z.object({
|
||||
file: z.string().describe("The path to the file to get diagnostics."),
|
||||
|
||||
@@ -6,7 +6,7 @@ import path from "path"
|
||||
import { App } from "../app/app"
|
||||
|
||||
export const MultiEditTool = Tool.define({
|
||||
id: "opencode.multiedit",
|
||||
id: "multiedit",
|
||||
description: DESCRIPTION,
|
||||
parameters: z.object({
|
||||
filePath: z.string().describe("The absolute path to the file to modify"),
|
||||
|
||||
@@ -232,7 +232,7 @@ async function applyCommit(
|
||||
}
|
||||
|
||||
export const PatchTool = Tool.define({
|
||||
id: "opencode.patch",
|
||||
id: "patch",
|
||||
description: DESCRIPTION,
|
||||
parameters: PatchParams,
|
||||
execute: async (params, ctx) => {
|
||||
|
||||
@@ -12,7 +12,7 @@ const DEFAULT_READ_LIMIT = 2000
|
||||
const MAX_LINE_LENGTH = 2000
|
||||
|
||||
export const ReadTool = Tool.define({
|
||||
id: "opencode.read",
|
||||
id: "read",
|
||||
description: DESCRIPTION,
|
||||
parameters: z.object({
|
||||
filePath: z.string().describe("The path to the file to read"),
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Bus } from "../bus"
|
||||
import { Message } from "../session/message"
|
||||
|
||||
export const TaskTool = Tool.define({
|
||||
id: "opencode.task",
|
||||
id: "task",
|
||||
description: DESCRIPTION,
|
||||
parameters: z.object({
|
||||
description: z
|
||||
@@ -34,13 +34,16 @@ export const TaskTool = Tool.define({
|
||||
}
|
||||
|
||||
const unsub = Bus.subscribe(Message.Event.Updated, async (evt) => {
|
||||
if (evt.properties.info.metadata.sessionID !== ctx.sessionID) return
|
||||
if (evt.properties.info.metadata.sessionID !== session.id) return
|
||||
ctx.metadata({
|
||||
title: params.description,
|
||||
summary: summary(evt.properties.info),
|
||||
})
|
||||
})
|
||||
|
||||
ctx.abort.addEventListener("abort", () => {
|
||||
Session.abort(session.id)
|
||||
})
|
||||
const result = await Session.chat({
|
||||
sessionID: session.id,
|
||||
modelID: metadata.modelID,
|
||||
|
||||
@@ -23,7 +23,7 @@ const state = App.state("todo-tool", () => {
|
||||
})
|
||||
|
||||
export const TodoWriteTool = Tool.define({
|
||||
id: "opencode.todowrite",
|
||||
id: "todowrite",
|
||||
description: DESCRIPTION_WRITE,
|
||||
parameters: z.object({
|
||||
todos: z.array(TodoInfo).describe("The updated todo list"),
|
||||
@@ -42,7 +42,7 @@ export const TodoWriteTool = Tool.define({
|
||||
})
|
||||
|
||||
export const TodoReadTool = Tool.define({
|
||||
id: "opencode.todoread",
|
||||
id: "todoread",
|
||||
description: "Use this tool to read your todo list",
|
||||
parameters: z.object({}),
|
||||
async execute(_params, opts) {
|
||||
|
||||
@@ -8,7 +8,7 @@ const DEFAULT_TIMEOUT = 30 * 1000 // 30 seconds
|
||||
const MAX_TIMEOUT = 120 * 1000 // 2 minutes
|
||||
|
||||
export const WebFetchTool = Tool.define({
|
||||
id: "opencode.webfetch",
|
||||
id: "webfetch",
|
||||
description: DESCRIPTION,
|
||||
parameters: z.object({
|
||||
url: z.string().describe("The URL to fetch content from"),
|
||||
@@ -76,7 +76,7 @@ export const WebFetchTool = Tool.define({
|
||||
switch (params.format) {
|
||||
case "text":
|
||||
if (contentType.includes("text/html")) {
|
||||
const text = extractTextFromHTML(content)
|
||||
const text = await extractTextFromHTML(content)
|
||||
return {
|
||||
output: text,
|
||||
metadata: {
|
||||
@@ -127,10 +127,45 @@ export const WebFetchTool = Tool.define({
|
||||
},
|
||||
})
|
||||
|
||||
function extractTextFromHTML(html: string): string {
|
||||
const doc = new DOMParser().parseFromString(html, "text/html")
|
||||
const text = doc.body.textContent || doc.body.innerText || ""
|
||||
return text.replace(/\s+/g, " ").trim()
|
||||
async function extractTextFromHTML(html: string) {
|
||||
let text = ""
|
||||
let skipContent = false
|
||||
|
||||
const rewriter = new HTMLRewriter()
|
||||
.on("script, style, noscript, iframe, object, embed", {
|
||||
element() {
|
||||
skipContent = true
|
||||
},
|
||||
text() {
|
||||
// Skip text content inside these elements
|
||||
},
|
||||
})
|
||||
.on("*", {
|
||||
element(element) {
|
||||
// Reset skip flag when entering other elements
|
||||
if (
|
||||
![
|
||||
"script",
|
||||
"style",
|
||||
"noscript",
|
||||
"iframe",
|
||||
"object",
|
||||
"embed",
|
||||
].includes(element.tagName)
|
||||
) {
|
||||
skipContent = false
|
||||
}
|
||||
},
|
||||
text(input) {
|
||||
if (!skipContent) {
|
||||
text += input.text
|
||||
}
|
||||
},
|
||||
})
|
||||
.transform(new Response(html))
|
||||
|
||||
await rewriter.text()
|
||||
return text.trim()
|
||||
}
|
||||
|
||||
function convertHTMLToMarkdown(html: string): string {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
- Allows OpenCode to search the web and use the results to inform responses
|
||||
- Allows opencode to search the web and use the results to inform responses
|
||||
- Provides up-to-date information for current events and recent data
|
||||
- Returns search result information formatted as search result blocks
|
||||
- Use this tool for accessing information beyond Claude's knowledge cutoff
|
||||
|
||||
@@ -8,7 +8,7 @@ import DESCRIPTION from "./write.txt"
|
||||
import { App } from "../app/app"
|
||||
|
||||
export const WriteTool = Tool.define({
|
||||
id: "opencode.write",
|
||||
id: "write",
|
||||
description: DESCRIPTION,
|
||||
parameters: z.object({
|
||||
filePath: z
|
||||
@@ -29,7 +29,7 @@ export const WriteTool = Tool.define({
|
||||
if (exists) await FileTimes.assert(ctx.sessionID, filepath)
|
||||
|
||||
await Permission.ask({
|
||||
id: "opencode.write",
|
||||
id: "write",
|
||||
sessionID: ctx.sessionID,
|
||||
title: exists
|
||||
? "Overwrite this file: " + filepath
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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{},
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
@@ -208,18 +208,18 @@ func LoadFromConfig(config *client.ConfigInfo) CommandRegistry {
|
||||
{
|
||||
Name: InputNewlineCommand,
|
||||
Description: "insert newline",
|
||||
Keybindings: parseBindings("shift+enter"),
|
||||
},
|
||||
{
|
||||
Name: HistoryPreviousCommand,
|
||||
Description: "previous prompt",
|
||||
Keybindings: parseBindings("up"),
|
||||
},
|
||||
{
|
||||
Name: HistoryNextCommand,
|
||||
Description: "next prompt",
|
||||
Keybindings: parseBindings("down"),
|
||||
Keybindings: parseBindings("shift+enter", "ctrl+j"),
|
||||
},
|
||||
// {
|
||||
// Name: HistoryPreviousCommand,
|
||||
// Description: "previous prompt",
|
||||
// Keybindings: parseBindings("up"),
|
||||
// },
|
||||
// {
|
||||
// Name: HistoryNextCommand,
|
||||
// Description: "next prompt",
|
||||
// Keybindings: parseBindings("down"),
|
||||
// },
|
||||
{
|
||||
Name: MessagesPageUpCommand,
|
||||
Description: "page up",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -6,12 +6,12 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/bubbles/v2/spinner"
|
||||
"github.com/charmbracelet/bubbles/v2/textarea"
|
||||
tea "github.com/charmbracelet/bubbletea/v2"
|
||||
"github.com/charmbracelet/lipgloss/v2"
|
||||
"github.com/sst/opencode/internal/app"
|
||||
"github.com/sst/opencode/internal/commands"
|
||||
"github.com/sst/opencode/internal/components/dialog"
|
||||
"github.com/sst/opencode/internal/components/textarea"
|
||||
"github.com/sst/opencode/internal/image"
|
||||
"github.com/sst/opencode/internal/layout"
|
||||
"github.com/sst/opencode/internal/styles"
|
||||
@@ -23,6 +23,8 @@ type EditorComponent interface {
|
||||
tea.Model
|
||||
tea.ViewModel
|
||||
layout.Sizeable
|
||||
Content() string
|
||||
Lines() int
|
||||
Value() string
|
||||
Submit() (tea.Model, tea.Cmd)
|
||||
Clear() (tea.Model, tea.Cmd)
|
||||
@@ -50,22 +52,18 @@ func (m *editorComponent) Init() tea.Cmd {
|
||||
func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
var cmds []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 != "" {
|
||||
m.textarea, cmd = m.textarea.Update(msg)
|
||||
return m, cmd
|
||||
cmds = append(cmds, cmd)
|
||||
return m, tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
// // TODO: ?
|
||||
// if key.Matches(msg, messageKeys.PageUp) ||
|
||||
// key.Matches(msg, messageKeys.PageDown) ||
|
||||
// key.Matches(msg, messageKeys.HalfPageUp) ||
|
||||
// key.Matches(msg, messageKeys.HalfPageDown) {
|
||||
// return m, nil
|
||||
// }
|
||||
|
||||
case dialog.ThemeSelectedMsg:
|
||||
m.textarea = createTextArea(&m.textarea)
|
||||
m.spinner = createSpinner()
|
||||
@@ -73,10 +71,11 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
case dialog.CompletionSelectedMsg:
|
||||
if msg.IsCommand {
|
||||
commandName := strings.TrimPrefix(msg.CompletionValue, "/")
|
||||
m.textarea.Reset()
|
||||
return m, util.CmdHandler(
|
||||
commands.ExecuteCommandMsg(m.app.Commands[commands.CommandName(commandName)]),
|
||||
)
|
||||
updated, cmd := m.Clear()
|
||||
m = updated.(*editorComponent)
|
||||
cmds = append(cmds, cmd)
|
||||
cmds = append(cmds, util.CmdHandler(commands.ExecuteCommandMsg(m.app.Commands[commands.CommandName(commandName)])))
|
||||
return m, tea.Batch(cmds...)
|
||||
} else {
|
||||
existingValue := m.textarea.Value()
|
||||
modifiedValue := strings.Replace(existingValue, msg.SearchString, msg.CompletionValue, 1)
|
||||
@@ -94,7 +93,7 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m, tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
func (m *editorComponent) View() string {
|
||||
func (m *editorComponent) Content() string {
|
||||
t := theme.CurrentTheme()
|
||||
base := styles.BaseStyle().Background(t.Background()).Render
|
||||
muted := styles.Muted().Background(t.Background()).Render
|
||||
@@ -114,9 +113,6 @@ func (m *editorComponent) View() string {
|
||||
PaddingTop(1).
|
||||
PaddingBottom(1).
|
||||
Background(t.BackgroundElement()).
|
||||
Border(lipgloss.ThickBorder(), false, true).
|
||||
BorderForeground(t.BackgroundElement()).
|
||||
BorderBackground(t.Background()).
|
||||
Render(textarea)
|
||||
|
||||
hint := base("enter") + muted(" send ")
|
||||
@@ -139,6 +135,13 @@ func (m *editorComponent) View() string {
|
||||
return content
|
||||
}
|
||||
|
||||
func (m *editorComponent) View() string {
|
||||
if m.Lines() > 1 {
|
||||
return ""
|
||||
}
|
||||
return m.Content()
|
||||
}
|
||||
|
||||
func (m *editorComponent) GetSize() (width, height int) {
|
||||
return m.width, m.height
|
||||
}
|
||||
@@ -146,18 +149,21 @@ func (m *editorComponent) GetSize() (width, height int) {
|
||||
func (m *editorComponent) SetSize(width, height int) tea.Cmd {
|
||||
m.width = width
|
||||
m.height = height
|
||||
m.textarea.SetWidth(width - 5) // account for the prompt and padding right
|
||||
m.textarea.SetHeight(height - 4) // account for info underneath
|
||||
m.textarea.SetWidth(width - 5) // account for the prompt and padding right
|
||||
// m.textarea.SetHeight(height - 4)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *editorComponent) Lines() int {
|
||||
return m.textarea.LineCount()
|
||||
}
|
||||
|
||||
func (m *editorComponent) Value() string {
|
||||
return m.textarea.Value()
|
||||
}
|
||||
|
||||
func (m *editorComponent) Submit() (tea.Model, tea.Cmd) {
|
||||
value := strings.TrimSpace(m.Value())
|
||||
m.textarea.Reset()
|
||||
if value == "" {
|
||||
return m, nil
|
||||
}
|
||||
@@ -167,6 +173,11 @@ func (m *editorComponent) Submit() (tea.Model, tea.Cmd) {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
var cmds []tea.Cmd
|
||||
updated, cmd := m.Clear()
|
||||
m = updated.(*editorComponent)
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
attachments := m.attachments
|
||||
|
||||
// Save to history if not empty and not a duplicate of the last entry
|
||||
@@ -180,12 +191,8 @@ func (m *editorComponent) Submit() (tea.Model, tea.Cmd) {
|
||||
|
||||
m.attachments = nil
|
||||
|
||||
return m, tea.Batch(
|
||||
util.CmdHandler(app.SendMsg{
|
||||
Text: value,
|
||||
Attachments: attachments,
|
||||
}),
|
||||
)
|
||||
cmds = append(cmds, util.CmdHandler(app.SendMsg{Text: value, Attachments: attachments}))
|
||||
return m, tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
func (m *editorComponent) Clear() (tea.Model, tea.Cmd) {
|
||||
@@ -210,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
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package chat
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
@@ -136,7 +137,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())
|
||||
|
||||
@@ -145,7 +146,7 @@ func renderContentBlock(content string, options ...renderingOption) string {
|
||||
align = *renderer.align
|
||||
}
|
||||
|
||||
borderColor := t.BackgroundSubtle()
|
||||
borderColor := t.BackgroundPanel()
|
||||
if renderer.borderColor != nil {
|
||||
borderColor = *renderer.borderColor
|
||||
}
|
||||
@@ -158,7 +159,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.
|
||||
@@ -167,7 +168,7 @@ func renderContentBlock(content string, options ...renderingOption) string {
|
||||
AlignHorizontal(align).
|
||||
BorderRightForeground(borderColor).
|
||||
BorderRightBackground(t.Background()).
|
||||
BorderLeftForeground(t.BackgroundSubtle()).
|
||||
BorderLeftForeground(t.BackgroundPanel()).
|
||||
BorderLeftBackground(t.Background())
|
||||
}
|
||||
|
||||
@@ -228,7 +229,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 {
|
||||
@@ -252,8 +253,9 @@ func renderToolInvocation(
|
||||
metadata client.MessageInfo_Metadata_Tool_AdditionalProperties,
|
||||
showDetails bool,
|
||||
isLast bool,
|
||||
contentOnly bool,
|
||||
) string {
|
||||
ignoredTools := []string{"opencode_todoread"}
|
||||
ignoredTools := []string{"todoread"}
|
||||
if slices.Contains(ignoredTools, toolCall.ToolName) {
|
||||
return ""
|
||||
}
|
||||
@@ -272,7 +274,7 @@ func renderToolInvocation(
|
||||
t := theme.CurrentTheme()
|
||||
style := styles.Muted().
|
||||
Width(outerWidth).
|
||||
Background(t.BackgroundSubtle()).
|
||||
Background(t.BackgroundPanel()).
|
||||
PaddingTop(paddingTop).
|
||||
PaddingBottom(paddingBottom).
|
||||
PaddingLeft(2).
|
||||
@@ -280,7 +282,7 @@ func renderToolInvocation(
|
||||
BorderLeft(true).
|
||||
BorderRight(true).
|
||||
BorderBackground(t.Background()).
|
||||
BorderForeground(t.BackgroundSubtle()).
|
||||
BorderForeground(t.BackgroundPanel()).
|
||||
BorderStyle(lipgloss.ThickBorder())
|
||||
|
||||
if toolCall.State == "partial-call" {
|
||||
@@ -288,7 +290,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()),
|
||||
@@ -305,14 +307,19 @@ func renderToolInvocation(
|
||||
toolArgsMap := make(map[string]any)
|
||||
if toolCall.Args != nil {
|
||||
value := *toolCall.Args
|
||||
m, ok := value.(map[string]any)
|
||||
if ok {
|
||||
if m, ok := value.(map[string]any); ok {
|
||||
toolArgsMap = m
|
||||
firstKey := ""
|
||||
|
||||
keys := make([]string, 0, len(toolArgsMap))
|
||||
for key := range toolArgsMap {
|
||||
firstKey = key
|
||||
break
|
||||
keys = append(keys, key)
|
||||
}
|
||||
slices.Sort(keys)
|
||||
firstKey := ""
|
||||
if len(keys) > 0 {
|
||||
firstKey = keys[0]
|
||||
}
|
||||
|
||||
toolArgs = renderArgs(&toolArgsMap, firstKey)
|
||||
}
|
||||
}
|
||||
@@ -325,7 +332,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(
|
||||
@@ -337,30 +344,19 @@ 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 "opencode_read":
|
||||
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)
|
||||
body = renderFile(filename, body, WithTruncate(6))
|
||||
}
|
||||
case "opencode_edit":
|
||||
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
|
||||
@@ -378,7 +374,7 @@ func renderToolInvocation(
|
||||
formattedDiff = lipgloss.NewStyle().
|
||||
BorderStyle(lipgloss.ThickBorder()).
|
||||
BorderBackground(t.Background()).
|
||||
BorderForeground(t.BackgroundSubtle()).
|
||||
BorderForeground(t.BackgroundPanel()).
|
||||
BorderLeft(true).
|
||||
BorderRight(true).
|
||||
Render(formattedDiff)
|
||||
@@ -399,37 +395,39 @@ func renderToolInvocation(
|
||||
)
|
||||
}
|
||||
}
|
||||
case "opencode_write":
|
||||
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 "opencode_bash":
|
||||
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 "opencode_webfetch":
|
||||
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 "opencode_todowrite":
|
||||
title = fmt.Sprintf("PLAN %s", elapsed)
|
||||
case "todowrite":
|
||||
title = fmt.Sprintf("PLAN")
|
||||
|
||||
if to, ok := metadata.Get("todos"); ok && finished {
|
||||
todos := to.([]any)
|
||||
@@ -445,21 +443,67 @@ 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", description)
|
||||
if summary, ok := metadata.Get("summary"); ok {
|
||||
toolcalls := summary.([]any)
|
||||
// toolcalls :=
|
||||
|
||||
steps := []string{}
|
||||
for _, toolcall := range toolcalls {
|
||||
call := toolcall.(map[string]any)
|
||||
if toolInvocation, ok := call["toolInvocation"].(map[string]any); ok {
|
||||
data, _ := json.Marshal(toolInvocation)
|
||||
var toolCall client.MessageToolInvocationToolCall
|
||||
_ = json.Unmarshal(data, &toolCall)
|
||||
|
||||
if metadata, ok := call["metadata"].(map[string]any); ok {
|
||||
data, _ = json.Marshal(metadata)
|
||||
var toolMetadata client.MessageInfo_Metadata_Tool_AdditionalProperties
|
||||
_ = json.Unmarshal(data, &toolMetadata)
|
||||
|
||||
step := renderToolInvocation(
|
||||
toolCall,
|
||||
nil,
|
||||
toolMetadata,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
)
|
||||
steps = append(steps, step)
|
||||
}
|
||||
}
|
||||
}
|
||||
body = strings.Join(steps, "\n")
|
||||
body = renderContentBlock(body, WithFullWidth(), WithMarginBottom(1))
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
body = *result
|
||||
body = truncateHeight(body, 10)
|
||||
body = renderContentBlock(body, WithFullWidth(), WithMarginBottom(1))
|
||||
}
|
||||
|
||||
if contentOnly {
|
||||
title = "∟ " + title
|
||||
return title
|
||||
}
|
||||
|
||||
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
|
||||
@@ -496,13 +540,11 @@ func renderToolInvocation(
|
||||
|
||||
func renderToolName(name string) string {
|
||||
switch name {
|
||||
// case agent.AgentToolName:
|
||||
// return "Task"
|
||||
case "opencode_ls":
|
||||
case "list":
|
||||
return "LIST"
|
||||
case "opencode_webfetch":
|
||||
case "webfetch":
|
||||
return "FETCH"
|
||||
case "opencode_todowrite":
|
||||
case "todowrite":
|
||||
return "PLAN"
|
||||
default:
|
||||
normalizedName := name
|
||||
@@ -550,36 +592,36 @@ 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))
|
||||
}
|
||||
|
||||
func renderToolAction(name string) string {
|
||||
switch name {
|
||||
// case agent.AgentToolName:
|
||||
// return "Preparing prompt..."
|
||||
case "opencode_bash":
|
||||
case "task":
|
||||
return "Searching..."
|
||||
case "bash":
|
||||
return "Building command..."
|
||||
case "opencode_edit":
|
||||
case "edit":
|
||||
return "Preparing edit..."
|
||||
case "opencode_fetch":
|
||||
return "Writing fetch..."
|
||||
case "opencode_glob":
|
||||
case "webfetch":
|
||||
return "Fetching from the web..."
|
||||
case "glob":
|
||||
return "Finding files..."
|
||||
case "opencode_grep":
|
||||
case "grep":
|
||||
return "Searching content..."
|
||||
case "opencode_ls":
|
||||
case "list":
|
||||
return "Listing directory..."
|
||||
case "opencode_read":
|
||||
case "read":
|
||||
return "Reading file..."
|
||||
case "opencode_write":
|
||||
case "write":
|
||||
return "Preparing write..."
|
||||
case "opencode_todowrite", "opencode_todoread":
|
||||
case "todowrite", "todoread":
|
||||
return "Planning..."
|
||||
case "opencode_patch":
|
||||
case "patch":
|
||||
return "Preparing patch..."
|
||||
case "opencode_batch":
|
||||
case "batch":
|
||||
return "Running batch operations..."
|
||||
}
|
||||
return "Working..."
|
||||
@@ -589,9 +631,17 @@ func renderArgs(args *map[string]any, titleKey string) string {
|
||||
if args == nil || len(*args) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
keys := make([]string, 0, len(*args))
|
||||
for key := range *args {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
slices.Sort(keys)
|
||||
|
||||
title := ""
|
||||
parts := []string{}
|
||||
for key, value := range *args {
|
||||
for _, key := range keys {
|
||||
value := (*args)[key]
|
||||
if value == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
@@ -28,6 +29,7 @@ type MessagesComponent interface {
|
||||
Last() (tea.Model, tea.Cmd)
|
||||
// Previous() (tea.Model, tea.Cmd)
|
||||
// Next() (tea.Model, tea.Cmd)
|
||||
ToolDetailsVisible() bool
|
||||
}
|
||||
|
||||
type messagesComponent struct {
|
||||
@@ -36,6 +38,7 @@ type messagesComponent struct {
|
||||
viewport viewport.Model
|
||||
spinner spinner.Model
|
||||
attachments viewport.Model
|
||||
commands commands.CommandsComponent
|
||||
cache *MessageCache
|
||||
rendering bool
|
||||
showToolDetails bool
|
||||
@@ -45,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) {
|
||||
@@ -57,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()
|
||||
@@ -92,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...)
|
||||
}
|
||||
|
||||
@@ -190,6 +195,7 @@ func (m *messagesComponent) renderView() {
|
||||
metadata,
|
||||
m.showToolDetails,
|
||||
isLastToolInvocation,
|
||||
false,
|
||||
)
|
||||
m.cache.Set(key, content)
|
||||
}
|
||||
@@ -201,6 +207,7 @@ func (m *messagesComponent) renderView() {
|
||||
metadata,
|
||||
m.showToolDetails,
|
||||
isLastToolInvocation,
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -278,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(
|
||||
@@ -316,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"},
|
||||
{"/quit", "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())),
|
||||
)
|
||||
}
|
||||
@@ -378,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
|
||||
}
|
||||
@@ -426,6 +433,10 @@ func (m *messagesComponent) Last() (tea.Model, tea.Cmd) {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *messagesComponent) ToolDetailsVisible() bool {
|
||||
return m.showToolDetails
|
||||
}
|
||||
|
||||
func NewMessagesComponent(app *app.App) MessagesComponent {
|
||||
customSpinner := spinner.Spinner{
|
||||
Frames: []string{" ", "┃", "┃"},
|
||||
@@ -437,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,201 @@
|
||||
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
|
||||
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 commandsWithTriggers []commands.Command
|
||||
for _, cmd := range c.app.Commands.Sorted() {
|
||||
if cmd.Trigger != "" {
|
||||
commandsWithTriggers = append(commandsWithTriggers, cmd)
|
||||
}
|
||||
}
|
||||
if c.limit != nil && len(commandsWithTriggers) > *c.limit {
|
||||
commandsWithTriggers = commandsWithTriggers[:*c.limit]
|
||||
}
|
||||
|
||||
if len(commandsWithTriggers) == 0 {
|
||||
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(commandsWithTriggers))
|
||||
|
||||
for _, cmd := range commandsWithTriggers {
|
||||
trigger := "/" + cmd.Trigger
|
||||
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
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
// Remove trailing newline
|
||||
result := strings.TrimSuffix(output.String(), "\n")
|
||||
|
||||
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 New(app *app.App, opts ...Option) CommandsComponent {
|
||||
c := &commandsComponent{
|
||||
app: app,
|
||||
background: nil,
|
||||
showKeybinds: true,
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(c)
|
||||
}
|
||||
return c
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"github.com/charmbracelet/bubbles/v2/key"
|
||||
"github.com/charmbracelet/bubbles/v2/textarea"
|
||||
tea "github.com/charmbracelet/bubbletea/v2"
|
||||
"github.com/charmbracelet/lipgloss/v2"
|
||||
"github.com/sst/opencode/internal/app"
|
||||
"github.com/sst/opencode/internal/components/list"
|
||||
"github.com/sst/opencode/internal/styles"
|
||||
@@ -101,10 +100,10 @@ type completionDialogKeyMap struct {
|
||||
|
||||
var completionDialogKeys = completionDialogKeyMap{
|
||||
Complete: key.NewBinding(
|
||||
key.WithKeys("tab", "enter"),
|
||||
key.WithKeys("tab", "enter", "right"),
|
||||
),
|
||||
Cancel: key.NewBinding(
|
||||
key.WithKeys(" ", "esc", "backspace"),
|
||||
key.WithKeys(" ", "esc", "backspace", "ctrl+c"),
|
||||
),
|
||||
}
|
||||
|
||||
@@ -174,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:
|
||||
@@ -203,13 +201,6 @@ func (c *completionDialogComponent) View() string {
|
||||
|
||||
return baseStyle.Padding(0, 0).
|
||||
Background(t.BackgroundElement()).
|
||||
Border(lipgloss.ThickBorder()).
|
||||
BorderTop(false).
|
||||
BorderBottom(false).
|
||||
BorderRight(true).
|
||||
BorderLeft(true).
|
||||
BorderBackground(t.Background()).
|
||||
BorderForeground(t.BackgroundSubtle()).
|
||||
Width(c.width).
|
||||
Render(c.list.View())
|
||||
}
|
||||
@@ -226,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
|
||||
}
|
||||
@@ -250,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{})
|
||||
}
|
||||
|
||||
|
||||
@@ -47,8 +47,10 @@ type themeDialog struct {
|
||||
width int
|
||||
height int
|
||||
|
||||
modal *modal.Modal
|
||||
list list.List[themeItem]
|
||||
modal *modal.Modal
|
||||
list list.List[themeItem]
|
||||
originalTheme string
|
||||
themeApplied bool
|
||||
}
|
||||
|
||||
func (t *themeDialog) Init() tea.Cmd {
|
||||
@@ -64,26 +66,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{})
|
||||
}
|
||||
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])
|
||||
|
||||
if item, newIdx := t.list.GetSelectedItem(); newIdx >= 0 && newIdx != prevIdx {
|
||||
theme.SetTheme(item.name)
|
||||
return t, util.CmdHandler(ThemeSelectedMsg{ThemeName: item.name})
|
||||
}
|
||||
return t, cmd
|
||||
}
|
||||
|
||||
@@ -92,6 +99,9 @@ func (t *themeDialog) Render(background string) string {
|
||||
}
|
||||
|
||||
func (t *themeDialog) Close() tea.Cmd {
|
||||
if !t.themeApplied {
|
||||
theme.SetTheme(t.originalTheme)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -120,7 +130,9 @@ func NewThemeDialog() ThemeDialog {
|
||||
list.SetSelectedIndex(selectedIdx)
|
||||
|
||||
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
|
||||
|
||||
@@ -103,7 +103,6 @@ func (m *Modal) Render(contentView string, background string) string {
|
||||
Bold(true).
|
||||
Padding(0, 1)
|
||||
|
||||
// titleView := titleStyle.Render(m.title)
|
||||
escStyle := baseStyle.Foreground(t.TextMuted()).Bold(false)
|
||||
escText := escStyle.Render("esc")
|
||||
|
||||
@@ -115,7 +114,7 @@ func (m *Modal) Render(contentView string, background string) string {
|
||||
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
|
||||
}
|
||||
@@ -124,14 +123,7 @@ func (m *Modal) Render(contentView string, background string) string {
|
||||
PaddingTop(1).
|
||||
PaddingBottom(1).
|
||||
PaddingLeft(2).
|
||||
PaddingRight(2).
|
||||
BorderStyle(lipgloss.ThickBorder()).
|
||||
BorderLeft(true).
|
||||
BorderRight(true).
|
||||
BorderLeftForeground(t.BackgroundSubtle()).
|
||||
BorderLeftBackground(t.Background()).
|
||||
BorderRightForeground(t.BackgroundSubtle()).
|
||||
BorderRightBackground(t.Background())
|
||||
PaddingRight(2)
|
||||
|
||||
modalView := modalStyle.
|
||||
Width(outerWidth).
|
||||
@@ -151,5 +143,7 @@ func (m *Modal) Render(contentView string, background string) string {
|
||||
row,
|
||||
modalView,
|
||||
background,
|
||||
layout.WithOverlayBorder(),
|
||||
layout.WithOverlayBorderColor(t.Primary()),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
// Package memoization implement a simple memoization cache. It's designed to
|
||||
// improve performance in textarea.
|
||||
package textarea
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Hasher is an interface that requires a Hash method. The Hash method is
|
||||
// expected to return a string representation of the hash of the object.
|
||||
type Hasher interface {
|
||||
Hash() string
|
||||
}
|
||||
|
||||
// entry is a struct that holds a key-value pair. It is used as an element
|
||||
// in the evictionList of the MemoCache.
|
||||
type entry[T any] struct {
|
||||
key string
|
||||
value T
|
||||
}
|
||||
|
||||
// MemoCache is a struct that represents a cache with a set capacity. It
|
||||
// uses an LRU (Least Recently Used) eviction policy. It is safe for
|
||||
// concurrent use.
|
||||
type MemoCache[H Hasher, T any] struct {
|
||||
capacity int
|
||||
mutex sync.Mutex
|
||||
cache map[string]*list.Element // The cache holding the results
|
||||
evictionList *list.List // A list to keep track of the order for LRU
|
||||
hashableItems map[string]T // This map keeps track of the original hashable items (optional)
|
||||
}
|
||||
|
||||
// NewMemoCache is a function that creates a new MemoCache with a given
|
||||
// capacity. It returns a pointer to the created MemoCache.
|
||||
func NewMemoCache[H Hasher, T any](capacity int) *MemoCache[H, T] {
|
||||
return &MemoCache[H, T]{
|
||||
capacity: capacity,
|
||||
cache: make(map[string]*list.Element),
|
||||
evictionList: list.New(),
|
||||
hashableItems: make(map[string]T),
|
||||
}
|
||||
}
|
||||
|
||||
// Capacity is a method that returns the capacity of the MemoCache.
|
||||
func (m *MemoCache[H, T]) Capacity() int {
|
||||
return m.capacity
|
||||
}
|
||||
|
||||
// Size is a method that returns the current size of the MemoCache. It is
|
||||
// the number of items currently stored in the cache.
|
||||
func (m *MemoCache[H, T]) Size() int {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
return m.evictionList.Len()
|
||||
}
|
||||
|
||||
// Get is a method that returns the value associated with the given
|
||||
// hashable item in the MemoCache. If there is no corresponding value, the
|
||||
// method returns nil.
|
||||
func (m *MemoCache[H, T]) Get(h H) (T, bool) {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
hashedKey := h.Hash()
|
||||
if element, found := m.cache[hashedKey]; found {
|
||||
m.evictionList.MoveToFront(element)
|
||||
return element.Value.(*entry[T]).value, true
|
||||
}
|
||||
var result T
|
||||
return result, false
|
||||
}
|
||||
|
||||
// Set is a method that sets the value for the given hashable item in the
|
||||
// MemoCache. If the cache is at capacity, it evicts the least recently
|
||||
// used item before adding the new item.
|
||||
func (m *MemoCache[H, T]) Set(h H, value T) {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
|
||||
hashedKey := h.Hash()
|
||||
if element, found := m.cache[hashedKey]; found {
|
||||
m.evictionList.MoveToFront(element)
|
||||
element.Value.(*entry[T]).value = value
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the cache is at capacity
|
||||
if m.evictionList.Len() >= m.capacity {
|
||||
// Evict the least recently used item from the cache
|
||||
toEvict := m.evictionList.Back()
|
||||
if toEvict != nil {
|
||||
evictedEntry := m.evictionList.Remove(toEvict).(*entry[T])
|
||||
delete(m.cache, evictedEntry.key)
|
||||
delete(m.hashableItems, evictedEntry.key) // if you're keeping track of original items
|
||||
}
|
||||
}
|
||||
|
||||
// Add the value to the cache and the evictionList
|
||||
newEntry := &entry[T]{
|
||||
key: hashedKey,
|
||||
value: value,
|
||||
}
|
||||
element := m.evictionList.PushFront(newEntry)
|
||||
m.cache[hashedKey] = element
|
||||
m.hashableItems[hashedKey] = value // if you're keeping track of original items
|
||||
}
|
||||
|
||||
// HString is a type that implements the Hasher interface for strings.
|
||||
type HString string
|
||||
|
||||
// Hash is a method that returns the hash of the string.
|
||||
func (h HString) Hash() string {
|
||||
return fmt.Sprintf("%x", sha256.Sum256([]byte(h)))
|
||||
}
|
||||
|
||||
// HInt is a type that implements the Hasher interface for integers.
|
||||
type HInt int
|
||||
|
||||
// Hash is a method that returns the hash of the integer.
|
||||
func (h HInt) Hash() string {
|
||||
return fmt.Sprintf("%x", sha256.Sum256([]byte(fmt.Sprintf("%d", h))))
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
// Package runeutil provides utility functions for tidying up incoming runes
|
||||
// from Key messages.
|
||||
package textarea
|
||||
|
||||
import (
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// Sanitizer is a helper for bubble widgets that want to process
|
||||
// Runes from input key messages.
|
||||
type Sanitizer interface {
|
||||
// Sanitize removes control characters from runes in a KeyRunes
|
||||
// message, and optionally replaces newline/carriage return/tabs by a
|
||||
// specified character.
|
||||
//
|
||||
// The rune array is modified in-place if possible. In that case, the
|
||||
// returned slice is the original slice shortened after the control
|
||||
// characters have been removed/translated.
|
||||
Sanitize(runes []rune) []rune
|
||||
}
|
||||
|
||||
// NewSanitizer constructs a rune sanitizer.
|
||||
func NewSanitizer(opts ...Option) Sanitizer {
|
||||
s := sanitizer{
|
||||
replaceNewLine: []rune("\n"),
|
||||
replaceTab: []rune(" "),
|
||||
}
|
||||
for _, o := range opts {
|
||||
s = o(s)
|
||||
}
|
||||
return &s
|
||||
}
|
||||
|
||||
// Option is the type of option that can be passed to Sanitize().
|
||||
type Option func(sanitizer) sanitizer
|
||||
|
||||
// ReplaceTabs replaces tabs by the specified string.
|
||||
func ReplaceTabs(tabRepl string) Option {
|
||||
return func(s sanitizer) sanitizer {
|
||||
s.replaceTab = []rune(tabRepl)
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
// ReplaceNewlines replaces newline characters by the specified string.
|
||||
func ReplaceNewlines(nlRepl string) Option {
|
||||
return func(s sanitizer) sanitizer {
|
||||
s.replaceNewLine = []rune(nlRepl)
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
func (s *sanitizer) Sanitize(runes []rune) []rune {
|
||||
// dstrunes are where we are storing the result.
|
||||
dstrunes := runes[:0:len(runes)]
|
||||
// copied indicates whether dstrunes is an alias of runes
|
||||
// or a copy. We need a copy when dst moves past src.
|
||||
// We use this as an optimization to avoid allocating
|
||||
// a new rune slice in the common case where the output
|
||||
// is smaller or equal to the input.
|
||||
copied := false
|
||||
|
||||
for src := 0; src < len(runes); src++ {
|
||||
r := runes[src]
|
||||
switch {
|
||||
case r == utf8.RuneError:
|
||||
// skip
|
||||
|
||||
case r == '\r' || r == '\n':
|
||||
if len(dstrunes)+len(s.replaceNewLine) > src && !copied {
|
||||
dst := len(dstrunes)
|
||||
dstrunes = make([]rune, dst, len(runes)+len(s.replaceNewLine))
|
||||
copy(dstrunes, runes[:dst])
|
||||
copied = true
|
||||
}
|
||||
dstrunes = append(dstrunes, s.replaceNewLine...)
|
||||
|
||||
case r == '\t':
|
||||
if len(dstrunes)+len(s.replaceTab) > src && !copied {
|
||||
dst := len(dstrunes)
|
||||
dstrunes = make([]rune, dst, len(runes)+len(s.replaceTab))
|
||||
copy(dstrunes, runes[:dst])
|
||||
copied = true
|
||||
}
|
||||
dstrunes = append(dstrunes, s.replaceTab...)
|
||||
|
||||
case unicode.IsControl(r):
|
||||
// Other control characters: skip.
|
||||
|
||||
default:
|
||||
// Keep the character.
|
||||
dstrunes = append(dstrunes, runes[src])
|
||||
}
|
||||
}
|
||||
return dstrunes
|
||||
}
|
||||
|
||||
type sanitizer struct {
|
||||
replaceNewLine []rune
|
||||
replaceTab []rune
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,267 @@
|
||||
package toast
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea/v2"
|
||||
"github.com/charmbracelet/lipgloss/v2"
|
||||
"github.com/charmbracelet/lipgloss/v2/compat"
|
||||
"github.com/sst/opencode/internal/layout"
|
||||
"github.com/sst/opencode/internal/styles"
|
||||
"github.com/sst/opencode/internal/theme"
|
||||
)
|
||||
|
||||
// ShowToastMsg is a message to display a toast notification
|
||||
type ShowToastMsg struct {
|
||||
Message string
|
||||
Title *string
|
||||
Color compat.AdaptiveColor
|
||||
Duration time.Duration
|
||||
}
|
||||
|
||||
// DismissToastMsg is a message to dismiss a specific toast
|
||||
type DismissToastMsg struct {
|
||||
ID string
|
||||
}
|
||||
|
||||
// Toast represents a single toast notification
|
||||
type Toast struct {
|
||||
ID string
|
||||
Message string
|
||||
Title *string
|
||||
Color compat.AdaptiveColor
|
||||
CreatedAt time.Time
|
||||
Duration time.Duration
|
||||
}
|
||||
|
||||
// ToastManager manages multiple toast notifications
|
||||
type ToastManager struct {
|
||||
toasts []Toast
|
||||
}
|
||||
|
||||
// NewToastManager creates a new toast manager
|
||||
func NewToastManager() *ToastManager {
|
||||
return &ToastManager{
|
||||
toasts: []Toast{},
|
||||
}
|
||||
}
|
||||
|
||||
// Init initializes the toast manager
|
||||
func (tm *ToastManager) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update handles messages for the toast manager
|
||||
func (tm *ToastManager) Update(msg tea.Msg) (*ToastManager, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case ShowToastMsg:
|
||||
toast := Toast{
|
||||
ID: fmt.Sprintf("toast-%d", time.Now().UnixNano()),
|
||||
Title: msg.Title,
|
||||
Message: msg.Message,
|
||||
Color: msg.Color,
|
||||
CreatedAt: time.Now(),
|
||||
Duration: msg.Duration,
|
||||
}
|
||||
|
||||
tm.toasts = append(tm.toasts, toast)
|
||||
|
||||
// Return command to dismiss after duration
|
||||
return tm, tea.Tick(toast.Duration, func(t time.Time) tea.Msg {
|
||||
return DismissToastMsg{ID: toast.ID}
|
||||
})
|
||||
|
||||
case DismissToastMsg:
|
||||
var newToasts []Toast
|
||||
for _, t := range tm.toasts {
|
||||
if t.ID != msg.ID {
|
||||
newToasts = append(newToasts, t)
|
||||
}
|
||||
}
|
||||
tm.toasts = newToasts
|
||||
}
|
||||
|
||||
return tm, nil
|
||||
}
|
||||
|
||||
// renderSingleToast renders a single toast notification
|
||||
func (tm *ToastManager) renderSingleToast(toast Toast) string {
|
||||
t := theme.CurrentTheme()
|
||||
|
||||
baseStyle := styles.BaseStyle().
|
||||
Background(t.BackgroundElement()).
|
||||
Foreground(t.Text()).
|
||||
Padding(1, 2)
|
||||
|
||||
maxWidth := max(40, layout.Current.Viewport.Width/3)
|
||||
contentMaxWidth := max(maxWidth-6, 20)
|
||||
|
||||
// Build content with wrapping
|
||||
var content strings.Builder
|
||||
if toast.Title != nil {
|
||||
titleStyle := lipgloss.NewStyle().
|
||||
Foreground(toast.Color).
|
||||
Bold(true)
|
||||
content.WriteString(titleStyle.Render(*toast.Title))
|
||||
content.WriteString("\n")
|
||||
}
|
||||
|
||||
// Wrap message text
|
||||
messageStyle := lipgloss.NewStyle()
|
||||
contentWidth := lipgloss.Width(toast.Message)
|
||||
if contentWidth > contentMaxWidth {
|
||||
messageStyle = messageStyle.Width(contentMaxWidth)
|
||||
}
|
||||
content.WriteString(messageStyle.Render(toast.Message))
|
||||
|
||||
// Render toast with max width
|
||||
return baseStyle.MaxWidth(maxWidth).Render(content.String())
|
||||
}
|
||||
|
||||
// View renders all active toasts
|
||||
func (tm *ToastManager) View() string {
|
||||
if len(tm.toasts) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
var toastViews []string
|
||||
for _, toast := range tm.toasts {
|
||||
toastView := tm.renderSingleToast(toast)
|
||||
toastViews = append(toastViews, toastView+"\n")
|
||||
}
|
||||
|
||||
return strings.Join(toastViews, "\n")
|
||||
}
|
||||
|
||||
// RenderOverlay renders the toasts as an overlay on the given background
|
||||
func (tm *ToastManager) RenderOverlay(background string) string {
|
||||
if len(tm.toasts) == 0 {
|
||||
return background
|
||||
}
|
||||
|
||||
bgWidth := lipgloss.Width(background)
|
||||
bgHeight := lipgloss.Height(background)
|
||||
result := background
|
||||
|
||||
// Start from top with 2 character padding
|
||||
currentY := 2
|
||||
|
||||
// Render each toast individually
|
||||
for _, toast := range tm.toasts {
|
||||
// Render individual toast
|
||||
toastView := tm.renderSingleToast(toast)
|
||||
toastWidth := lipgloss.Width(toastView)
|
||||
toastHeight := lipgloss.Height(toastView)
|
||||
|
||||
// Position at top-right with 2 character padding from right edge
|
||||
x := max(bgWidth-toastWidth-4, 0)
|
||||
|
||||
// Check if toast fits vertically
|
||||
if currentY+toastHeight > bgHeight-2 {
|
||||
// No more room for toasts
|
||||
break
|
||||
}
|
||||
|
||||
// Place this toast
|
||||
result = layout.PlaceOverlay(
|
||||
x,
|
||||
currentY,
|
||||
toastView,
|
||||
result,
|
||||
layout.WithOverlayBorder(),
|
||||
layout.WithOverlayBorderColor(toast.Color),
|
||||
)
|
||||
|
||||
// Move down for next toast (add 1 for spacing between toasts)
|
||||
currentY += toastHeight + 1
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
type ToastOptions struct {
|
||||
Title string
|
||||
Duration time.Duration
|
||||
}
|
||||
|
||||
type toastOptions struct {
|
||||
title *string
|
||||
duration *time.Duration
|
||||
color *compat.AdaptiveColor
|
||||
}
|
||||
|
||||
type ToastOption func(*toastOptions)
|
||||
|
||||
func WithTitle(title string) ToastOption {
|
||||
return func(t *toastOptions) {
|
||||
t.title = &title
|
||||
}
|
||||
}
|
||||
func WithDuration(duration time.Duration) ToastOption {
|
||||
return func(t *toastOptions) {
|
||||
t.duration = &duration
|
||||
}
|
||||
}
|
||||
|
||||
func WithColor(color compat.AdaptiveColor) ToastOption {
|
||||
return func(t *toastOptions) {
|
||||
t.color = &color
|
||||
}
|
||||
}
|
||||
|
||||
func NewToast(message string, options ...ToastOption) tea.Cmd {
|
||||
t := theme.CurrentTheme()
|
||||
duration := 5 * time.Second
|
||||
color := t.Primary()
|
||||
|
||||
opts := toastOptions{
|
||||
duration: &duration,
|
||||
color: &color,
|
||||
}
|
||||
for _, option := range options {
|
||||
option(&opts)
|
||||
}
|
||||
|
||||
return func() tea.Msg {
|
||||
return ShowToastMsg{
|
||||
Message: message,
|
||||
Title: opts.title,
|
||||
Duration: *opts.duration,
|
||||
Color: *opts.color,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func NewInfoToast(message string, options ...ToastOption) tea.Cmd {
|
||||
options = append(options, WithColor(theme.CurrentTheme().Info()))
|
||||
return NewToast(
|
||||
message,
|
||||
options...,
|
||||
)
|
||||
}
|
||||
|
||||
func NewSuccessToast(message string, options ...ToastOption) tea.Cmd {
|
||||
options = append(options, WithColor(theme.CurrentTheme().Success()))
|
||||
return NewToast(
|
||||
message,
|
||||
options...,
|
||||
)
|
||||
}
|
||||
|
||||
func NewWarningToast(message string, options ...ToastOption) tea.Cmd {
|
||||
options = append(options, WithColor(theme.CurrentTheme().Warning()))
|
||||
return NewToast(
|
||||
message,
|
||||
options...,
|
||||
)
|
||||
}
|
||||
|
||||
func NewErrorToast(message string, options ...ToastOption) tea.Cmd {
|
||||
options = append(options, WithColor(theme.CurrentTheme().Error()))
|
||||
return NewToast(
|
||||
message,
|
||||
options...,
|
||||
)
|
||||
}
|
||||
@@ -1,9 +1,13 @@
|
||||
package layout
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/charmbracelet/lipgloss/v2"
|
||||
"github.com/charmbracelet/lipgloss/v2/compat"
|
||||
chAnsi "github.com/charmbracelet/x/ansi"
|
||||
"github.com/muesli/ansi"
|
||||
"github.com/muesli/reflow/truncate"
|
||||
@@ -23,29 +27,58 @@ func getLines(s string) (lines []string, widest int) {
|
||||
return lines, widest
|
||||
}
|
||||
|
||||
// overlayOptions holds configuration for overlay rendering
|
||||
type overlayOptions struct {
|
||||
whitespace *whitespace
|
||||
border bool
|
||||
borderColor *compat.AdaptiveColor
|
||||
}
|
||||
|
||||
// OverlayOption sets options for overlay rendering
|
||||
type OverlayOption func(*overlayOptions)
|
||||
|
||||
// PlaceOverlay places fg on top of bg.
|
||||
func PlaceOverlay(
|
||||
x, y int,
|
||||
fg, bg string,
|
||||
opts ...WhitespaceOption,
|
||||
opts ...OverlayOption,
|
||||
) string {
|
||||
fgLines, fgWidth := getLines(fg)
|
||||
bgLines, bgWidth := getLines(bg)
|
||||
bgHeight := len(bgLines)
|
||||
fgHeight := len(fgLines)
|
||||
|
||||
if fgWidth >= bgWidth && fgHeight >= bgHeight {
|
||||
// FIXME: return fg or bg?
|
||||
return fg
|
||||
// Parse options
|
||||
options := &overlayOptions{
|
||||
whitespace: &whitespace{},
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(options)
|
||||
}
|
||||
|
||||
// TODO: allow placement outside of the bg box?
|
||||
x = util.Clamp(x, 0, bgWidth-fgWidth)
|
||||
y = util.Clamp(y, 0, bgHeight-fgHeight)
|
||||
// Adjust for borders if enabled
|
||||
if options.border {
|
||||
// Add space for left and right borders
|
||||
adjustedFgWidth := fgWidth + 2
|
||||
// Adjust placement to account for borders
|
||||
x = util.Clamp(x, 0, bgWidth-adjustedFgWidth)
|
||||
y = util.Clamp(y, 0, bgHeight-fgHeight)
|
||||
|
||||
ws := &whitespace{}
|
||||
for _, opt := range opts {
|
||||
opt(ws)
|
||||
// Pad all foreground lines to the same width for consistent borders
|
||||
for i := range fgLines {
|
||||
lineWidth := ansi.PrintableRuneWidth(fgLines[i])
|
||||
if lineWidth < fgWidth {
|
||||
fgLines[i] += strings.Repeat(" ", fgWidth-lineWidth)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if fgWidth >= bgWidth && fgHeight >= bgHeight {
|
||||
// FIXME: return fg or bg?
|
||||
return fg
|
||||
}
|
||||
// TODO: allow placement outside of the bg box?
|
||||
x = util.Clamp(x, 0, bgWidth-fgWidth)
|
||||
y = util.Clamp(y, 0, bgHeight-fgHeight)
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
@@ -59,25 +92,62 @@ func PlaceOverlay(
|
||||
}
|
||||
|
||||
pos := 0
|
||||
|
||||
// Handle left side of the line up to the overlay
|
||||
if x > 0 {
|
||||
left := truncate.String(bgLine, uint(x))
|
||||
pos = ansi.PrintableRuneWidth(left)
|
||||
b.WriteString(left)
|
||||
if pos < x {
|
||||
b.WriteString(ws.render(x - pos))
|
||||
b.WriteString(options.whitespace.render(x - pos))
|
||||
pos = x
|
||||
}
|
||||
}
|
||||
|
||||
fgLine := fgLines[i-y]
|
||||
b.WriteString(fgLine)
|
||||
pos += ansi.PrintableRuneWidth(fgLine)
|
||||
// Render the overlay content with optional borders
|
||||
if options.border {
|
||||
// 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)
|
||||
|
||||
// Left border - combine background from original with border foreground
|
||||
leftSeq := combineStyles(leftStyle, options.borderColor)
|
||||
if leftSeq != "" {
|
||||
b.WriteString(leftSeq)
|
||||
}
|
||||
b.WriteString("┃")
|
||||
b.WriteString("\x1b[0m") // Reset all styles
|
||||
pos++
|
||||
|
||||
// Content
|
||||
b.WriteString(fgLine)
|
||||
pos += fgLineWidth
|
||||
|
||||
// Right border - combine background from original with border foreground
|
||||
rightSeq := combineStyles(rightStyle, options.borderColor)
|
||||
if rightSeq != "" {
|
||||
b.WriteString(rightSeq)
|
||||
}
|
||||
b.WriteString("┃")
|
||||
b.WriteString("\x1b[0m") // Reset all styles
|
||||
pos++
|
||||
} else {
|
||||
// No border, just render the content
|
||||
fgLine := fgLines[i-y]
|
||||
b.WriteString(fgLine)
|
||||
pos += ansi.PrintableRuneWidth(fgLine)
|
||||
}
|
||||
|
||||
// Handle right side of the line after the overlay
|
||||
right := cutLeft(bgLine, pos)
|
||||
bgWidth := ansi.PrintableRuneWidth(bgLine)
|
||||
rightWidth := ansi.PrintableRuneWidth(right)
|
||||
if rightWidth <= bgWidth-pos {
|
||||
b.WriteString(ws.render(bgWidth - rightWidth - pos))
|
||||
b.WriteString(options.whitespace.render(bgWidth - rightWidth - pos))
|
||||
}
|
||||
|
||||
b.WriteString(right)
|
||||
@@ -92,6 +162,146 @@ func cutLeft(s string, cutWidth int) string {
|
||||
return chAnsi.Cut(s, cutWidth, lipgloss.Width(s))
|
||||
}
|
||||
|
||||
// ansiStyle represents parsed ANSI style attributes
|
||||
type ansiStyle struct {
|
||||
fgColor string
|
||||
bgColor string
|
||||
attrs []string
|
||||
}
|
||||
|
||||
// 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) {
|
||||
switch parts[i] {
|
||||
case "0": // Reset
|
||||
style = ansiStyle{}
|
||||
case "1", "2", "3", "4", "5", "6", "7", "8", "9": // Various attributes
|
||||
style.attrs = append(style.attrs, parts[i])
|
||||
case "38": // Foreground color
|
||||
if i+1 < len(parts) && parts[i+1] == "5" && i+2 < len(parts) {
|
||||
// 256 color mode
|
||||
style.fgColor = strings.Join(parts[i:i+3], ";")
|
||||
i += 2
|
||||
} else if i+1 < len(parts) && parts[i+1] == "2" && i+4 < len(parts) {
|
||||
// RGB color mode
|
||||
style.fgColor = strings.Join(parts[i:i+5], ";")
|
||||
i += 4
|
||||
}
|
||||
case "48": // Background color
|
||||
if i+1 < len(parts) && parts[i+1] == "5" && i+2 < len(parts) {
|
||||
// 256 color mode
|
||||
style.bgColor = strings.Join(parts[i:i+3], ";")
|
||||
i += 2
|
||||
} else if i+1 < len(parts) && parts[i+1] == "2" && i+4 < len(parts) {
|
||||
// RGB color mode
|
||||
style.bgColor = strings.Join(parts[i:i+5], ";")
|
||||
i += 4
|
||||
}
|
||||
case "30", "31", "32", "33", "34", "35", "36", "37": // Standard foreground colors
|
||||
style.fgColor = parts[i]
|
||||
case "40", "41", "42", "43", "44", "45", "46", "47": // Standard background colors
|
||||
style.bgColor = parts[i]
|
||||
case "90", "91", "92", "93", "94", "95", "96", "97": // Bright foreground colors
|
||||
style.fgColor = parts[i]
|
||||
case "100", "101", "102", "103", "104", "105", "106", "107": // Bright background colors
|
||||
style.bgColor = parts[i]
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
return style
|
||||
}
|
||||
|
||||
// combineStyles creates an ANSI sequence that combines background from one style with foreground from another
|
||||
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, ";"))
|
||||
}
|
||||
|
||||
// getStyleAtPosition extracts the active ANSI style at a given visual position
|
||||
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
|
||||
if match := ansiRegex.FindStringIndex(s[i:]); match != nil && match[0] == 0 {
|
||||
// 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
|
||||
}
|
||||
if parsedStyle.bgColor != "" {
|
||||
currentStyle.bgColor = parsedStyle.bgColor
|
||||
}
|
||||
if len(parsedStyle.attrs) > 0 {
|
||||
currentStyle.attrs = parsedStyle.attrs
|
||||
}
|
||||
|
||||
i += match[1]
|
||||
} else if i < len(s) {
|
||||
// Regular character
|
||||
if visualPos == targetPos {
|
||||
return currentStyle
|
||||
}
|
||||
_, size := utf8.DecodeRuneInString(s[i:])
|
||||
i += size
|
||||
visualPos++
|
||||
}
|
||||
}
|
||||
|
||||
return currentStyle
|
||||
}
|
||||
|
||||
type whitespace struct {
|
||||
style termenv.Style
|
||||
chars string
|
||||
@@ -129,3 +339,26 @@ func (w whitespace) render(width int) string {
|
||||
|
||||
// WhitespaceOption sets a styling rule for rendering whitespace.
|
||||
type WhitespaceOption func(*whitespace)
|
||||
|
||||
// WithWhitespace sets whitespace options for the overlay
|
||||
func WithWhitespace(opts ...WhitespaceOption) OverlayOption {
|
||||
return func(o *overlayOptions) {
|
||||
for _, opt := range opts {
|
||||
opt(o.whitespace)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithOverlayBorder enables border rendering for the overlay
|
||||
func WithOverlayBorder() OverlayOption {
|
||||
return func(o *overlayOptions) {
|
||||
o.border = true
|
||||
}
|
||||
}
|
||||
|
||||
// WithOverlayBorderColor sets the border color for the overlay
|
||||
func WithOverlayBorderColor(color compat.AdaptiveColor) OverlayOption {
|
||||
return func(o *overlayOptions) {
|
||||
o.borderColor = &color
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,6 @@ var Terminal *TerminalInfo
|
||||
|
||||
func init() {
|
||||
Terminal = &TerminalInfo{
|
||||
BackgroundIsDark: false,
|
||||
BackgroundIsDark: true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,136 @@
|
||||
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,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": "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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/sst/opencode/internal/components/dialog"
|
||||
"github.com/sst/opencode/internal/components/modal"
|
||||
"github.com/sst/opencode/internal/components/status"
|
||||
"github.com/sst/opencode/internal/components/toast"
|
||||
"github.com/sst/opencode/internal/layout"
|
||||
"github.com/sst/opencode/internal/styles"
|
||||
"github.com/sst/opencode/internal/util"
|
||||
@@ -38,16 +39,22 @@ type appModel struct {
|
||||
showCompletionDialog bool
|
||||
leaderBinding *key.Binding
|
||||
isLeaderSequence bool
|
||||
toastManager *toast.ToastManager
|
||||
}
|
||||
|
||||
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())
|
||||
cmds = append(cmds, a.status.Init())
|
||||
cmds = append(cmds, a.completions.Init())
|
||||
cmds = append(cmds, a.toastManager.Init())
|
||||
|
||||
// Check if we should show the init dialog
|
||||
cmds = append(cmds, func() tea.Msg {
|
||||
@@ -63,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
|
||||
@@ -81,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))
|
||||
@@ -89,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.CompletionDialogTriggerdMsg{
|
||||
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() {
|
||||
case "tab", "enter", "esc":
|
||||
context, contextCmd := a.completions.Update(msg)
|
||||
a.completions = context.(dialog.CompletionDialog)
|
||||
cmds = append(cmds, contextCmd)
|
||||
switch keyString {
|
||||
case "tab", "enter", "esc", "ctrl+c":
|
||||
updated, cmd := a.updateCompletions(msg)
|
||||
a.completions = updated.(dialog.CompletionDialog)
|
||||
cmds = append(cmds, cmd)
|
||||
return a, tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
@@ -138,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...)
|
||||
}
|
||||
|
||||
@@ -170,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
|
||||
// likek backspace, tab, etc.
|
||||
updatedEditor, cmd := a.editor.Update(msg)
|
||||
a.editor = updatedEditor.(chat.EditorComponent)
|
||||
return a, cmd
|
||||
@@ -190,8 +195,12 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
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
|
||||
@@ -208,7 +217,11 @@ 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(
|
||||
"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 {
|
||||
a.app.Session = &msg.Properties.Info
|
||||
@@ -226,6 +239,12 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
a.app.Messages = append(a.app.Messages, msg.Properties.Info)
|
||||
}
|
||||
}
|
||||
case client.EventSessionError:
|
||||
unknownError, err := msg.Properties.Error.AsUnknownError()
|
||||
if err == nil {
|
||||
slog.Error("Server error", "name", unknownError.Name, "message", unknownError.Data.Message)
|
||||
return a, toast.NewErrorToast(unknownError.Data.Message, toast.WithTitle(unknownError.Name))
|
||||
}
|
||||
case tea.WindowSizeMsg:
|
||||
msg.Height -= 2 // Make space for the status bar
|
||||
a.width, a.height = msg.Width, msg.Height
|
||||
@@ -240,12 +259,13 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
a.layout.SetSize(a.width, a.height)
|
||||
case app.SessionSelectedMsg:
|
||||
var err error
|
||||
a.app.Session = msg
|
||||
a.app.Messages, err = a.app.ListMessages(context.Background(), msg.Id)
|
||||
messages, err := a.app.ListMessages(context.Background(), msg.Id)
|
||||
if err != nil {
|
||||
slog.Error("Failed to list messages", "error", err)
|
||||
return a, toast.NewErrorToast("Failed to open session")
|
||||
}
|
||||
a.app.Session = msg
|
||||
a.app.Messages = messages
|
||||
case app.ModelSelectedMsg:
|
||||
a.app.Provider = &msg.Provider
|
||||
a.app.Model = &msg.Model
|
||||
@@ -255,6 +275,14 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
case dialog.ThemeSelectedMsg:
|
||||
a.app.State.Theme = msg.ThemeName
|
||||
a.app.SaveState()
|
||||
case toast.ShowToastMsg:
|
||||
tm, cmd := a.toastManager.Update(msg)
|
||||
a.toastManager = tm
|
||||
cmds = append(cmds, cmd)
|
||||
case toast.DismissToastMsg:
|
||||
tm, cmd := a.toastManager.Update(msg)
|
||||
a.toastManager = tm
|
||||
cmds = append(cmds, cmd)
|
||||
}
|
||||
|
||||
// update status bar
|
||||
@@ -290,14 +318,22 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
|
||||
func (a appModel) View() string {
|
||||
layoutView := a.layout.View()
|
||||
editorWidth, _ := a.editorContainer.GetSize()
|
||||
editorX, editorY := a.editorContainer.GetPosition()
|
||||
|
||||
if a.editor.Lines() > 1 {
|
||||
editorY = editorY - a.editor.Lines() + 1
|
||||
layoutView = layout.PlaceOverlay(
|
||||
editorX,
|
||||
editorY,
|
||||
a.editor.Content(),
|
||||
layoutView,
|
||||
)
|
||||
}
|
||||
|
||||
if a.showCompletionDialog {
|
||||
editorWidth, _ := a.editorContainer.GetSize()
|
||||
editorX, editorY := a.editorContainer.GetPosition()
|
||||
|
||||
a.completions.SetWidth(editorWidth)
|
||||
overlay := a.completions.View()
|
||||
|
||||
layoutView = layout.PlaceOverlay(
|
||||
editorX,
|
||||
editorY-lipgloss.Height(overlay)+2,
|
||||
@@ -310,11 +346,14 @@ 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)
|
||||
}
|
||||
|
||||
appView = a.toastManager.RenderOverlay(appView)
|
||||
|
||||
return appView
|
||||
}
|
||||
|
||||
@@ -333,8 +372,7 @@ func (a appModel) executeCommand(command commands.Command) (tea.Model, tea.Cmd)
|
||||
}
|
||||
editor := os.Getenv("EDITOR")
|
||||
if editor == "" {
|
||||
// TODO: let the user know there's no EDITOR set
|
||||
return a, nil
|
||||
return a, toast.NewErrorToast("No EDITOR set, can't open editor")
|
||||
}
|
||||
|
||||
value := a.editor.Value()
|
||||
@@ -346,7 +384,7 @@ func (a appModel) executeCommand(command commands.Command) (tea.Model, tea.Cmd)
|
||||
tmpfile.WriteString(value)
|
||||
if err != nil {
|
||||
slog.Error("Failed to create temp file", "error", err)
|
||||
return a, nil
|
||||
return a, toast.NewErrorToast("Something went wrong, couldn't open editor")
|
||||
}
|
||||
tmpfile.Close()
|
||||
c := exec.Command(editor, tmpfile.Name()) //nolint:gosec
|
||||
@@ -390,12 +428,21 @@ func (a appModel) executeCommand(command commands.Command) (tea.Model, tea.Cmd)
|
||||
if a.app.Session.Id == "" {
|
||||
return a, nil
|
||||
}
|
||||
a.app.Client.PostSessionShareWithResponse(
|
||||
response, err := a.app.Client.PostSessionShareWithResponse(
|
||||
context.Background(),
|
||||
client.PostSessionShareJSONRequestBody{
|
||||
SessionID: a.app.Session.Id,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
slog.Error("Failed to share session", "error", err)
|
||||
return a, toast.NewErrorToast("Failed to share session")
|
||||
}
|
||||
if response.JSON200 != nil && response.JSON200.Share != nil {
|
||||
shareUrl := response.JSON200.Share.Url
|
||||
cmds = append(cmds, tea.SetClipboard(shareUrl))
|
||||
cmds = append(cmds, toast.NewSuccessToast("Share URL copied to clipboard!"))
|
||||
}
|
||||
case commands.SessionInterruptCommand:
|
||||
if a.app.Session.Id == "" {
|
||||
return a, nil
|
||||
@@ -409,7 +456,12 @@ func (a appModel) executeCommand(command commands.Command) (tea.Model, tea.Cmd)
|
||||
// TODO: block until compaction is complete
|
||||
a.app.CompactSession(context.Background())
|
||||
case commands.ToolDetailsCommand:
|
||||
message := "Tool details are now visible"
|
||||
if a.messages.ToolDetailsVisible() {
|
||||
message = "Tool details are now hidden"
|
||||
}
|
||||
cmds = append(cmds, util.CmdHandler(chat.ToggleToolDetailsMsg{}))
|
||||
cmds = append(cmds, toast.NewInfoToast(message))
|
||||
case commands.ModelListCommand:
|
||||
modelDialog := dialog.NewModelDialog(a.app)
|
||||
a.modal = modelDialog
|
||||
@@ -460,30 +512,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)
|
||||
@@ -493,6 +533,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()
|
||||
@@ -509,8 +558,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
|
||||
}
|
||||
|
||||
@@ -525,12 +574,13 @@ func NewModel(app *app.App) tea.Model {
|
||||
isLeaderSequence: false,
|
||||
showCompletionDialog: false,
|
||||
editorContainer: editorContainer,
|
||||
toastManager: toast.NewToastManager(),
|
||||
layout: layout.NewFlexLayout(
|
||||
[]tea.ViewModel{messagesContainer, editorContainer},
|
||||
layout.WithDirection(layout.FlexDirectionVertical),
|
||||
layout.WithSizes(
|
||||
layout.FlexChildSizeGrow,
|
||||
layout.FlexChildSizeFixed(6),
|
||||
layout.FlexChildSizeFixed(5),
|
||||
),
|
||||
),
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -525,6 +525,9 @@
|
||||
{
|
||||
"$ref": "#/components/schemas/Event.storage.write"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/Event.installation.updated"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/Event.lsp.client.diagnostics"
|
||||
},
|
||||
@@ -537,9 +540,6 @@
|
||||
{
|
||||
"$ref": "#/components/schemas/Event.message.part.updated"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/Event.installation.updated"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/Event.session.updated"
|
||||
},
|
||||
@@ -551,11 +551,11 @@
|
||||
"propertyName": "type",
|
||||
"mapping": {
|
||||
"storage.write": "#/components/schemas/Event.storage.write",
|
||||
"installation.updated": "#/components/schemas/Event.installation.updated",
|
||||
"lsp.client.diagnostics": "#/components/schemas/Event.lsp.client.diagnostics",
|
||||
"permission.updated": "#/components/schemas/Event.permission.updated",
|
||||
"message.updated": "#/components/schemas/Event.message.updated",
|
||||
"message.part.updated": "#/components/schemas/Event.message.part.updated",
|
||||
"installation.updated": "#/components/schemas/Event.installation.updated",
|
||||
"session.updated": "#/components/schemas/Event.session.updated",
|
||||
"session.error": "#/components/schemas/Event.session.error"
|
||||
}
|
||||
@@ -586,6 +586,30 @@
|
||||
"properties"
|
||||
]
|
||||
},
|
||||
"Event.installation.updated": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"const": "installation.updated"
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"version": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"version"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type",
|
||||
"properties"
|
||||
]
|
||||
},
|
||||
"Event.lsp.client.diagnostics": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -1201,30 +1225,6 @@
|
||||
"properties"
|
||||
]
|
||||
},
|
||||
"Event.installation.updated": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"const": "installation.updated"
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"version": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"version"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type",
|
||||
"properties"
|
||||
]
|
||||
},
|
||||
"Event.session.updated": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -1391,13 +1391,16 @@
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"$schema": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "JSON schema reference for configuration validation"
|
||||
},
|
||||
"theme": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "Theme name to use for the interface"
|
||||
},
|
||||
"keybinds": {
|
||||
"$ref": "#/components/schemas/Config.Keybinds"
|
||||
"$ref": "#/components/schemas/Config.Keybinds",
|
||||
"description": "Custom keybind configurations"
|
||||
},
|
||||
"autoshare": {
|
||||
"type": "boolean",
|
||||
@@ -1508,7 +1511,8 @@
|
||||
"required": [
|
||||
"models"
|
||||
]
|
||||
}
|
||||
},
|
||||
"description": "Custom provider configurations and model overrides"
|
||||
},
|
||||
"mcp": {
|
||||
"type": "object",
|
||||
@@ -1528,7 +1532,8 @@
|
||||
"remote": "#/components/schemas/Config.McpRemote"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "MCP (Model Context Protocol) server configurations"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1536,85 +1541,112 @@
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"leader": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "Leader key for keybind combinations"
|
||||
},
|
||||
"help": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "Show help dialog"
|
||||
},
|
||||
"editor_open": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "Open external editor"
|
||||
},
|
||||
"session_new": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "Create a new session"
|
||||
},
|
||||
"session_list": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "List all sessions"
|
||||
},
|
||||
"session_share": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "Share current session"
|
||||
},
|
||||
"session_interrupt": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "Interrupt current session"
|
||||
},
|
||||
"session_compact": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "Toggle compact mode for session"
|
||||
},
|
||||
"tool_details": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "Show tool details"
|
||||
},
|
||||
"model_list": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "List available models"
|
||||
},
|
||||
"theme_list": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "List available themes"
|
||||
},
|
||||
"project_init": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "Initialize project configuration"
|
||||
},
|
||||
"input_clear": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "Clear input field"
|
||||
},
|
||||
"input_paste": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "Paste from clipboard"
|
||||
},
|
||||
"input_submit": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "Submit input"
|
||||
},
|
||||
"input_newline": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "Insert newline in input"
|
||||
},
|
||||
"history_previous": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "Navigate to previous history item"
|
||||
},
|
||||
"history_next": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "Navigate to next history item"
|
||||
},
|
||||
"messages_page_up": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "Scroll messages up by one page"
|
||||
},
|
||||
"messages_page_down": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "Scroll messages down by one page"
|
||||
},
|
||||
"messages_half_page_up": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "Scroll messages up by half page"
|
||||
},
|
||||
"messages_half_page_down": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "Scroll messages down by half page"
|
||||
},
|
||||
"messages_previous": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "Navigate to previous message"
|
||||
},
|
||||
"messages_next": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "Navigate to next message"
|
||||
},
|
||||
"messages_first": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "Navigate to first message"
|
||||
},
|
||||
"messages_last": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "Navigate to last message"
|
||||
},
|
||||
"app_exit": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "Exit the application"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1723,19 +1755,22 @@
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"const": "local"
|
||||
"const": "local",
|
||||
"description": "Type of MCP server connection"
|
||||
},
|
||||
"command": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"description": "Command and arguments to run the MCP server"
|
||||
},
|
||||
"environment": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"description": "Environment variables to set when running the MCP server"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -1748,10 +1783,12 @@
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"const": "remote"
|
||||
"const": "remote",
|
||||
"description": "Type of MCP server connection"
|
||||
},
|
||||
"url": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"description": "URL of the remote MCP server"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
||||
@@ -41,6 +41,7 @@ type AppInfo struct {
|
||||
|
||||
// ConfigInfo defines model for Config.Info.
|
||||
type ConfigInfo struct {
|
||||
// Schema JSON schema reference for configuration validation
|
||||
Schema *string `json:"$schema,omitempty"`
|
||||
|
||||
// Autoshare Share newly created sessions automatically
|
||||
@@ -50,12 +51,16 @@ type ConfigInfo struct {
|
||||
Autoupdate *bool `json:"autoupdate,omitempty"`
|
||||
|
||||
// DisabledProviders Disable providers that are loaded automatically
|
||||
DisabledProviders *[]string `json:"disabled_providers,omitempty"`
|
||||
Keybinds *ConfigKeybinds `json:"keybinds,omitempty"`
|
||||
Mcp *map[string]ConfigInfo_Mcp_AdditionalProperties `json:"mcp,omitempty"`
|
||||
DisabledProviders *[]string `json:"disabled_providers,omitempty"`
|
||||
Keybinds *ConfigKeybinds `json:"keybinds,omitempty"`
|
||||
|
||||
// Mcp MCP (Model Context Protocol) server configurations
|
||||
Mcp *map[string]ConfigInfo_Mcp_AdditionalProperties `json:"mcp,omitempty"`
|
||||
|
||||
// Model Model to use in the format of provider/model, eg anthropic/claude-2
|
||||
Model *string `json:"model,omitempty"`
|
||||
Model *string `json:"model,omitempty"`
|
||||
|
||||
// Provider Custom provider configurations and model overrides
|
||||
Provider *map[string]struct {
|
||||
Api *string `json:"api,omitempty"`
|
||||
Env *[]string `json:"env,omitempty"`
|
||||
@@ -81,6 +86,8 @@ type ConfigInfo struct {
|
||||
Npm *string `json:"npm,omitempty"`
|
||||
Options *map[string]interface{} `json:"options,omitempty"`
|
||||
} `json:"provider,omitempty"`
|
||||
|
||||
// Theme Theme name to use for the interface
|
||||
Theme *string `json:"theme,omitempty"`
|
||||
}
|
||||
|
||||
@@ -91,46 +98,107 @@ type ConfigInfo_Mcp_AdditionalProperties struct {
|
||||
|
||||
// ConfigKeybinds defines model for Config.Keybinds.
|
||||
type ConfigKeybinds struct {
|
||||
AppExit *string `json:"app_exit,omitempty"`
|
||||
EditorOpen *string `json:"editor_open,omitempty"`
|
||||
Help *string `json:"help,omitempty"`
|
||||
HistoryNext *string `json:"history_next,omitempty"`
|
||||
HistoryPrevious *string `json:"history_previous,omitempty"`
|
||||
InputClear *string `json:"input_clear,omitempty"`
|
||||
InputNewline *string `json:"input_newline,omitempty"`
|
||||
InputPaste *string `json:"input_paste,omitempty"`
|
||||
InputSubmit *string `json:"input_submit,omitempty"`
|
||||
Leader *string `json:"leader,omitempty"`
|
||||
MessagesFirst *string `json:"messages_first,omitempty"`
|
||||
// AppExit Exit the application
|
||||
AppExit *string `json:"app_exit,omitempty"`
|
||||
|
||||
// EditorOpen Open external editor
|
||||
EditorOpen *string `json:"editor_open,omitempty"`
|
||||
|
||||
// Help Show help dialog
|
||||
Help *string `json:"help,omitempty"`
|
||||
|
||||
// HistoryNext Navigate to next history item
|
||||
HistoryNext *string `json:"history_next,omitempty"`
|
||||
|
||||
// HistoryPrevious Navigate to previous history item
|
||||
HistoryPrevious *string `json:"history_previous,omitempty"`
|
||||
|
||||
// InputClear Clear input field
|
||||
InputClear *string `json:"input_clear,omitempty"`
|
||||
|
||||
// InputNewline Insert newline in input
|
||||
InputNewline *string `json:"input_newline,omitempty"`
|
||||
|
||||
// InputPaste Paste from clipboard
|
||||
InputPaste *string `json:"input_paste,omitempty"`
|
||||
|
||||
// InputSubmit Submit input
|
||||
InputSubmit *string `json:"input_submit,omitempty"`
|
||||
|
||||
// Leader Leader key for keybind combinations
|
||||
Leader *string `json:"leader,omitempty"`
|
||||
|
||||
// MessagesFirst Navigate to first message
|
||||
MessagesFirst *string `json:"messages_first,omitempty"`
|
||||
|
||||
// MessagesHalfPageDown Scroll messages down by half page
|
||||
MessagesHalfPageDown *string `json:"messages_half_page_down,omitempty"`
|
||||
MessagesHalfPageUp *string `json:"messages_half_page_up,omitempty"`
|
||||
MessagesLast *string `json:"messages_last,omitempty"`
|
||||
MessagesNext *string `json:"messages_next,omitempty"`
|
||||
MessagesPageDown *string `json:"messages_page_down,omitempty"`
|
||||
MessagesPageUp *string `json:"messages_page_up,omitempty"`
|
||||
MessagesPrevious *string `json:"messages_previous,omitempty"`
|
||||
ModelList *string `json:"model_list,omitempty"`
|
||||
ProjectInit *string `json:"project_init,omitempty"`
|
||||
SessionCompact *string `json:"session_compact,omitempty"`
|
||||
SessionInterrupt *string `json:"session_interrupt,omitempty"`
|
||||
SessionList *string `json:"session_list,omitempty"`
|
||||
SessionNew *string `json:"session_new,omitempty"`
|
||||
SessionShare *string `json:"session_share,omitempty"`
|
||||
ThemeList *string `json:"theme_list,omitempty"`
|
||||
ToolDetails *string `json:"tool_details,omitempty"`
|
||||
|
||||
// MessagesHalfPageUp Scroll messages up by half page
|
||||
MessagesHalfPageUp *string `json:"messages_half_page_up,omitempty"`
|
||||
|
||||
// MessagesLast Navigate to last message
|
||||
MessagesLast *string `json:"messages_last,omitempty"`
|
||||
|
||||
// MessagesNext Navigate to next message
|
||||
MessagesNext *string `json:"messages_next,omitempty"`
|
||||
|
||||
// MessagesPageDown Scroll messages down by one page
|
||||
MessagesPageDown *string `json:"messages_page_down,omitempty"`
|
||||
|
||||
// MessagesPageUp Scroll messages up by one page
|
||||
MessagesPageUp *string `json:"messages_page_up,omitempty"`
|
||||
|
||||
// MessagesPrevious Navigate to previous message
|
||||
MessagesPrevious *string `json:"messages_previous,omitempty"`
|
||||
|
||||
// ModelList List available models
|
||||
ModelList *string `json:"model_list,omitempty"`
|
||||
|
||||
// ProjectInit Initialize project configuration
|
||||
ProjectInit *string `json:"project_init,omitempty"`
|
||||
|
||||
// SessionCompact Toggle compact mode for session
|
||||
SessionCompact *string `json:"session_compact,omitempty"`
|
||||
|
||||
// SessionInterrupt Interrupt current session
|
||||
SessionInterrupt *string `json:"session_interrupt,omitempty"`
|
||||
|
||||
// SessionList List all sessions
|
||||
SessionList *string `json:"session_list,omitempty"`
|
||||
|
||||
// SessionNew Create a new session
|
||||
SessionNew *string `json:"session_new,omitempty"`
|
||||
|
||||
// SessionShare Share current session
|
||||
SessionShare *string `json:"session_share,omitempty"`
|
||||
|
||||
// ThemeList List available themes
|
||||
ThemeList *string `json:"theme_list,omitempty"`
|
||||
|
||||
// ToolDetails Show tool details
|
||||
ToolDetails *string `json:"tool_details,omitempty"`
|
||||
}
|
||||
|
||||
// ConfigMcpLocal defines model for Config.McpLocal.
|
||||
type ConfigMcpLocal struct {
|
||||
Command []string `json:"command"`
|
||||
// Command Command and arguments to run the MCP server
|
||||
Command []string `json:"command"`
|
||||
|
||||
// Environment Environment variables to set when running the MCP server
|
||||
Environment *map[string]string `json:"environment,omitempty"`
|
||||
Type string `json:"type"`
|
||||
|
||||
// Type Type of MCP server connection
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
// ConfigMcpRemote defines model for Config.McpRemote.
|
||||
type ConfigMcpRemote struct {
|
||||
// Type Type of MCP server connection
|
||||
Type string `json:"type"`
|
||||
Url string `json:"url"`
|
||||
|
||||
// Url URL of the remote MCP server
|
||||
Url string `json:"url"`
|
||||
}
|
||||
|
||||
// Error defines model for Error.
|
||||
@@ -684,6 +752,34 @@ func (t *Event) MergeEventStorageWrite(v EventStorageWrite) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// AsEventInstallationUpdated returns the union data inside the Event as a EventInstallationUpdated
|
||||
func (t Event) AsEventInstallationUpdated() (EventInstallationUpdated, error) {
|
||||
var body EventInstallationUpdated
|
||||
err := json.Unmarshal(t.union, &body)
|
||||
return body, err
|
||||
}
|
||||
|
||||
// FromEventInstallationUpdated overwrites any union data inside the Event as the provided EventInstallationUpdated
|
||||
func (t *Event) FromEventInstallationUpdated(v EventInstallationUpdated) error {
|
||||
v.Type = "installation.updated"
|
||||
b, err := json.Marshal(v)
|
||||
t.union = b
|
||||
return err
|
||||
}
|
||||
|
||||
// MergeEventInstallationUpdated performs a merge with any union data inside the Event, using the provided EventInstallationUpdated
|
||||
func (t *Event) MergeEventInstallationUpdated(v EventInstallationUpdated) error {
|
||||
v.Type = "installation.updated"
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
merged, err := runtime.JSONMerge(t.union, b)
|
||||
t.union = merged
|
||||
return err
|
||||
}
|
||||
|
||||
// AsEventLspClientDiagnostics returns the union data inside the Event as a EventLspClientDiagnostics
|
||||
func (t Event) AsEventLspClientDiagnostics() (EventLspClientDiagnostics, error) {
|
||||
var body EventLspClientDiagnostics
|
||||
@@ -796,34 +892,6 @@ func (t *Event) MergeEventMessagePartUpdated(v EventMessagePartUpdated) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// AsEventInstallationUpdated returns the union data inside the Event as a EventInstallationUpdated
|
||||
func (t Event) AsEventInstallationUpdated() (EventInstallationUpdated, error) {
|
||||
var body EventInstallationUpdated
|
||||
err := json.Unmarshal(t.union, &body)
|
||||
return body, err
|
||||
}
|
||||
|
||||
// FromEventInstallationUpdated overwrites any union data inside the Event as the provided EventInstallationUpdated
|
||||
func (t *Event) FromEventInstallationUpdated(v EventInstallationUpdated) error {
|
||||
v.Type = "installation.updated"
|
||||
b, err := json.Marshal(v)
|
||||
t.union = b
|
||||
return err
|
||||
}
|
||||
|
||||
// MergeEventInstallationUpdated performs a merge with any union data inside the Event, using the provided EventInstallationUpdated
|
||||
func (t *Event) MergeEventInstallationUpdated(v EventInstallationUpdated) error {
|
||||
v.Type = "installation.updated"
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
merged, err := runtime.JSONMerge(t.union, b)
|
||||
t.union = merged
|
||||
return err
|
||||
}
|
||||
|
||||
// AsEventSessionUpdated returns the union data inside the Event as a EventSessionUpdated
|
||||
func (t Event) AsEventSessionUpdated() (EventSessionUpdated, error) {
|
||||
var body EventSessionUpdated
|
||||
|
||||
@@ -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>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user