Compare commits

..

71 Commits

Author SHA1 Message Date
Dax Raad
eb70b1e5c8 docs: windows instructions 2025-06-24 18:54:59 -04:00
Dax Raad
00a3d818b6 ci: windows 2025-06-24 18:46:43 -04:00
Dax Raad
2384c7e734 ci: windows 2025-06-24 18:40:36 -04:00
Dax Raad
1bad3d9894 ci: windows 2025-06-24 18:27:57 -04:00
Dax Raad
4f715e66dc ci: windows 2025-06-24 18:13:15 -04:00
Dax
ec001ca02f windows fixes (#374)
Co-authored-by: Matthew Glazar <strager.nds@gmail.com>
2025-06-24 18:05:04 -04:00
Jay
a2d3b9f0c8 docs: Share page diff view improvements (#373) 2025-06-24 17:11:43 -04:00
Dax Raad
9cfb6ff964 ignore: revert 2025-06-24 14:59:27 -04:00
Dax Raad
6ed661c140 ci: upgrade bun 2025-06-24 14:42:25 -04:00
Dax Raad
9dc00edfc9 potential fix for failing to install provider package on first run 2025-06-24 14:33:35 -04:00
Jay V
e063bf888e docs: share code blocks in markdown 2025-06-24 13:53:59 -04:00
Adam
6f18475428 feat: delete sessions (#362)
Co-authored-by: adamdottv <2363879+adamdottv@users.noreply.github.com>
2025-06-24 11:07:41 -05:00
Dax Raad
3664b09812 remove debug code writing to /tmp/message.json 2025-06-24 11:16:17 -04:00
Dax Raad
7050cc0ac3 ignore: fix type errors 2025-06-24 11:09:36 -04:00
Dax Raad
4d3d63294d externalize github copilot code 2025-06-24 10:42:19 -04:00
Tom
6bc61cbc2d feat(tui): add debounce logic to escape key interrupt (#169)
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: adamdottv <2363879+adamdottv@users.noreply.github.com>
2025-06-24 06:31:02 -05:00
Dax Raad
01d351bebe add HOMEBREW_NO_AUTO_UPDATE to brew upgrades 2025-06-23 20:36:08 -04:00
Dax Raad
dbba4a97aa force use npm registry 2025-06-23 20:23:37 -04:00
GitMurf
0dc586faef fix: typescript error (any) from models (#347) 2025-06-23 18:44:57 -04:00
Dax Raad
f19c6b05f2 glob tool should respect .gitignore 2025-06-23 17:37:32 -04:00
Dax Raad
bc34f08333 bundle models.dev at build time and ignore refresh errors 2025-06-23 14:50:19 -04:00
Dax Raad
b7ee16aabd ignore: remove opencode.json 2025-06-23 14:32:57 -04:00
Lucas Grzegorczyk
ed1b0d97bf Fix project folder name starting with "-" in data (#323). Note old session data will still be in the old format in ~/.local/share/opencode/projects - you can remove the leading dash to recover the, 2025-06-23 14:31:51 -04:00
adamdottv
8d3b2fb821 feat(tui): optimistically render user messages 2025-06-23 12:30:20 -05:00
Jay V
fa991920bc fix help copy 2025-06-23 13:00:24 -04:00
adamdottv
5e79e3d7a5 fix(tui): less incorrect escapingn of < and > 2025-06-23 11:32:32 -05:00
adamdottv
966015c9ae fix: overlay border color issues 2025-06-23 11:21:49 -05:00
adamdottv
61f057337a fix: markdown wrapping issue 2025-06-23 11:20:44 -05:00
adamdottv
0b261054a2 chore: unused import 2025-06-23 10:21:57 -05:00
adamdottv
e2e481cbb5 docs: disabled_providers 2025-06-23 10:21:25 -05:00
GitMurf
5140e83012 feat(copilot): edit headers for better rate limit avoidance (#321) 2025-06-23 10:44:19 -04:00
Dax Raad
100d6212be more graceful mcp failures 2025-06-22 21:10:05 -04:00
Dax Raad
f0e19a6542 aws autoload include more env vars 2025-06-22 20:16:10 -04:00
Dax Raad
00c4d4f9f8 fix double entry of github copilot in auth login 2025-06-22 19:13:25 -04:00
Martin Palma
6e6fe6e013 Add Github Copilot OAuth authentication flow (#305) 2025-06-22 19:11:37 -04:00
Dax Raad
d05b60291e docs: contributing 2025-06-22 17:55:10 -04:00
adamdottv
5162361372 fix(tui): color contrast fixes for nord 2025-06-22 15:17:18 -05:00
adamdottv
d271b9f75b fix(tui): help dialog visuals 2025-06-22 14:28:16 -05:00
Márk Magyar
333569bed3 ignore: fix typos and formatting (#294) 2025-06-22 14:26:46 -04:00
Tom
09b89fdb23 fix: resolve test failures by adding missing zod-openapi import (#301)
Co-authored-by: opencode <noreply@opencode.ai>
2025-06-22 14:25:02 -04:00
Tom
0e8c3359d1 combine stdout and stderr in bash tool output (#300)
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: Dax Raad <d@ironbay.co>
2025-06-22 14:24:35 -04:00
Adam
37e0a7050f fix(tui): mouse wheel escape codes leaking into input 2025-06-22 10:26:44 -05:00
adamdottv
774dcb6980 fix(tui): cleanup help dialog 2025-06-22 06:44:23 -05:00
phantomreactor
28bc49ad17 fix: invisible html tags and compact long delay (#304) 2025-06-22 06:29:04 -05:00
adamdottv
dc1947838c fix(tui): cleanup modal visuals 2025-06-22 06:09:23 -05:00
adamdottv
3ea2daaa4c fix(tui): theme dialog visuals 2025-06-22 05:34:22 -05:00
Márk Magyar
137e964131 fix: session title generation (#293) 2025-06-21 14:32:11 -05:00
tyrellshawn
8efbe497fd Created a Theme inspired by the matrix (#285) 2025-06-21 07:29:49 -05:00
Thomas Meire
119d2d966c Add error handling on the calls to the server to debug issue #132 (#137) 2025-06-21 07:24:39 -05:00
Dax Raad
194415e785 footer clarifies it's showing context usage, not input token usage 2025-06-20 22:52:51 -04:00
Dax Raad
1684042fb6 huge optimization for token usage with anthropic 2025-06-20 22:43:04 -04:00
Dax Raad
59f0004d34 Add --method option to upgrade command for manual installation method selection
🤖 Generated with [opencode](https://opencode.ai)

Co-Authored-By: opencode <noreply@opencode.ai>
2025-06-20 20:48:23 -04:00
Dax Raad
da35a64fa1 handle brew upgrades better 2025-06-20 20:27:23 -04:00
Dax Raad
460338ca53 make IDs more random 2025-06-20 17:39:59 -04:00
Saatvik Arya
53c18a64b4 docs: add API client generation instructions to README and AGENTS.md (#273) 2025-06-20 17:27:58 -04:00
Saatvik Arya
b8144c5654 fix: return false for missing AWS_PROFILE in amazon-bedrock provider (#277) 2025-06-20 17:27:27 -04:00
adamdottv
9081e17fcc fix(tui): visual tweaks to themes 2025-06-20 15:49:51 -05:00
adamdottv
ef3fd5900f docs: cleanup casing 2025-06-20 15:35:25 -05:00
adamdottv
453d690c11 docs: new themes docs 2025-06-20 15:31:38 -05:00
adamdottv
c45be6a645 feat(tui): one dark theme 2025-06-20 15:14:23 -05:00
adamdottv
7b9b177088 feat(tui): kanagawa theme 2025-06-20 15:14:23 -05:00
adamdottv
3cee5b0470 feat(tui): gruvbox theme 2025-06-20 15:14:23 -05:00
adamdottv
9246d1c901 feat(tui): catppuccin theme 2025-06-20 15:14:22 -05:00
adamdottv
cc12abc83e feat(tui): nord theme 2025-06-20 15:14:22 -05:00
adamdottv
4f7e4a9436 feat(tui): custom themes 2025-06-20 15:14:22 -05:00
Márk Magyar
eee396f903 feat(tui): theme switcher with preview (#264) 2025-06-20 15:14:05 -05:00
Jay V
0d2f8e175a docs: share bugs 2025-06-20 15:50:12 -04:00
Jay V
4df40e0d9b docs: share page bugs 2025-06-20 15:50:12 -04:00
Dax Raad
b72e17a8b7 fix issue with conversations hanging 2025-06-20 15:49:49 -04:00
Dax Raad
61160dc220 docs: readme 2025-06-20 15:22:41 -04:00
Dax Raad
98734ff28c Consolidate session context handling and add global config support
Refactored context file discovery by removing separate SessionContext module and integrating functionality into SystemPrompt.context(). Added support for finding AGENTS.md and CLAUDE.md files in global config directories.

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

Co-Authored-By: opencode <noreply@opencode.ai>
2025-06-20 15:14:12 -04:00
91 changed files with 4638 additions and 2279 deletions

View File

@@ -17,7 +17,7 @@ jobs:
- uses: oven-sh/setup-bun@v1
with:
bun-version: latest
bun-version: 1.2.17
- run: bun install

View File

@@ -32,7 +32,7 @@ jobs:
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.2.16
bun-version: 1.2.17
- name: Install makepkg
run: |

View File

@@ -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

View File

@@ -38,6 +38,8 @@ For more info on how to configure opencode [**head over to our docs**](https://o
### Contributing
For any new features we'd appreciate it if you could open an issue first to discuss what you'd like to implement. We're pretty responsive there and it'll save you from working on something that we don't end up using. No need to do this for simpler fixes.
To run opencode locally you need.
- Bun
@@ -50,31 +52,18 @@ $ 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?

View File

@@ -71,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",
@@ -457,7 +458,7 @@
"@types/babel__traverse": ["@types/babel__traverse@7.20.7", "", { "dependencies": { "@babel/types": "^7.20.7" } }, "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng=="],
"@types/bun": ["@types/bun@1.2.16", "", { "dependencies": { "bun-types": "1.2.16" } }, "sha512-1aCZJ/6nSiViw339RsaNhkNoEloLaPzZhxMOYEa7OzRzO41IGg5n/7I43/ZIAW/c+Q6cT12Vf7fOZOoVIzb5BQ=="],
"@types/bun": ["@types/bun@1.2.17", "", { "dependencies": { "bun-types": "1.2.17" } }, "sha512-l/BYs/JYt+cXA/0+wUhulYJB6a6p//GTPiJ7nV+QHa8iiId4HZmnu/3J/SowP5g0rTiERY2kfGKXEK5Ehltx4Q=="],
"@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
@@ -595,7 +596,7 @@
"buffer": ["buffer@4.9.2", "", { "dependencies": { "base64-js": "^1.0.2", "ieee754": "^1.1.4", "isarray": "^1.0.0" } }, "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg=="],
"bun-types": ["bun-types@1.2.16", "", { "dependencies": { "@types/node": "*" } }, "sha512-ciXLrHV4PXax9vHvUrkvun9VPVGOVwbbbBF/Ev1cXz12lyEZMoJpIJABOfPcN9gDJRaiKF9MVbSygLg4NXu3/A=="],
"bun-types": ["bun-types@1.2.17", "", { "dependencies": { "@types/node": "*" } }, "sha512-ElC7ItwT3SCQwYZDYoAH+q6KT4Fxjl8DtZ6qDulUFBmXA8YB4xo+l54J9ZJN+k2pphfn9vk7kfubeSd5QfTVJQ=="],
"bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
@@ -1011,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=="],

View File

@@ -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.

View File

@@ -0,0 +1,56 @@
@echo off
setlocal enabledelayedexpansion
if defined OPENCODE_BIN_PATH (
set "resolved=%OPENCODE_BIN_PATH%"
goto :execute
)
rem Get the directory of this script
set "script_dir=%~dp0"
set "script_dir=%script_dir:~0,-1%"
rem Detect platform and architecture
set "platform=win32"
rem Detect architecture
if "%PROCESSOR_ARCHITECTURE%"=="AMD64" (
set "arch=x64"
) else if "%PROCESSOR_ARCHITECTURE%"=="ARM64" (
set "arch=arm64"
) else if "%PROCESSOR_ARCHITECTURE%"=="x86" (
set "arch=x86"
) else (
set "arch=x64"
)
set "name=opencode-!platform!-!arch!"
set "binary=opencode.exe"
rem Search for the binary starting from script location
set "resolved="
set "current_dir=%script_dir%"
:search_loop
set "candidate=%current_dir%\node_modules\%name%\bin\%binary%"
if exist "%candidate%" (
set "resolved=%candidate%"
goto :execute
)
rem Move up one directory
for %%i in ("%current_dir%") do set "parent_dir=%%~dpi"
set "parent_dir=%parent_dir:~0,-1%"
rem Check if we've reached the root
if "%current_dir%"=="%parent_dir%" goto :not_found
set "current_dir=%parent_dir%"
goto :search_loop
:not_found
echo It seems that your package manager failed to install the right version of the OpenCode CLI for your platform. You can try manually installing the "%name%" package >&2
exit /b 1
:execute
rem Execute the binary with all arguments
"%resolved%" %*

View File

@@ -8,6 +8,9 @@
"typecheck": "tsc --noEmit",
"dev": "bun run ./src/index.ts"
},
"bin": {
"opencode": "./bin/opencode"
},
"exports": {
"./*": "./src/*.ts"
},

View File

@@ -29,7 +29,7 @@ const targets = [
["linux", "x64"],
["darwin", "x64"],
["darwin", "arm64"],
// ["windows", "x64"],
["windows", "x64"],
]
await $`rm -rf dist`

View File

@@ -1,3 +1,4 @@
import "zod-openapi/extend"
import { Log } from "../util/log"
import { Context } from "../util/context"
import { Filesystem } from "../util/filesystem"
@@ -45,7 +46,7 @@ export namespace App {
const data = path.join(
Global.Path.data,
"project",
git ? git.split(path.sep).join("-") : "global",
git ? directory(git) : "global",
)
const stateFile = Bun.file(path.join(data, APP_JSON))
const state = (await stateFile.json().catch(() => ({}))) as {
@@ -132,4 +133,13 @@ export namespace App {
}),
)
}
function directory(input: string): string {
return input
.split(path.sep)
.filter(Boolean)
.join("-")
.replace(/[^A-Za-z0-9_]/g, "-")
}
}

View File

@@ -0,0 +1,20 @@
import { Global } from "../global"
import { lazy } from "../util/lazy"
import path from "path"
export const AuthCopilot = lazy(async () => {
const file = Bun.file(path.join(Global.Path.state, "plugin", "copilot.ts"))
const response = fetch(
"https://raw.githubusercontent.com/sst/opencode-github-copilot/refs/heads/main/auth.ts",
)
.then((x) => Bun.write(file, x))
.catch(() => {})
if (!file.exists()) {
const worked = await response
if (!worked) return
}
const result = await import(file.name!).catch(() => {})
if (!result) return
return result.AuthCopilot
})

View File

@@ -0,0 +1,150 @@
import { z } from "zod"
import { Auth } from "./index"
import { NamedError } from "../util/error"
export namespace AuthGithubCopilot {
const CLIENT_ID = "Iv1.b507a08c87ecfe98"
const DEVICE_CODE_URL = "https://github.com/login/device/code"
const ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token"
const COPILOT_API_KEY_URL = "https://api.github.com/copilot_internal/v2/token"
interface DeviceCodeResponse {
device_code: string
user_code: string
verification_uri: string
expires_in: number
interval: number
}
interface AccessTokenResponse {
access_token?: string
error?: string
error_description?: string
}
interface CopilotTokenResponse {
token: string
expires_at: number
refresh_in: number
endpoints: {
api: string
}
}
export async function authorize() {
const deviceResponse = await fetch(DEVICE_CODE_URL, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
"User-Agent": "GitHubCopilotChat/0.26.7",
},
body: JSON.stringify({
client_id: CLIENT_ID,
scope: "read:user",
}),
})
const deviceData: DeviceCodeResponse = await deviceResponse.json()
return {
device: deviceData.device_code,
user: deviceData.user_code,
verification: deviceData.verification_uri,
interval: deviceData.interval || 5,
expiry: deviceData.expires_in,
}
}
export async function poll(device_code: string) {
const response = await fetch(ACCESS_TOKEN_URL, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
"User-Agent": "GitHubCopilotChat/0.26.7",
},
body: JSON.stringify({
client_id: CLIENT_ID,
device_code,
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
}),
})
if (!response.ok) return "failed"
const data: AccessTokenResponse = await response.json()
if (data.access_token) {
// Store the GitHub OAuth token
await Auth.set("github-copilot", {
type: "oauth",
refresh: data.access_token,
access: "",
expires: 0,
})
return "complete"
}
if (data.error === "authorization_pending") return "pending"
if (data.error) return "failed"
return "pending"
}
export async function access() {
const info = await Auth.get("github-copilot")
if (!info || info.type !== "oauth") return
if (info.access && info.expires > Date.now()) return info.access
// Get new Copilot API token
const response = await fetch(COPILOT_API_KEY_URL, {
headers: {
Accept: "application/json",
Authorization: `Bearer ${info.refresh}`,
"User-Agent": "GitHubCopilotChat/0.26.7",
"Editor-Version": "vscode/1.99.3",
"Editor-Plugin-Version": "copilot-chat/0.26.7",
},
})
if (!response.ok) return
const tokenData: CopilotTokenResponse = await response.json()
// Store the Copilot API token
await Auth.set("github-copilot", {
type: "oauth",
refresh: info.refresh,
access: tokenData.token,
expires: tokenData.expires_at * 1000,
})
return tokenData.token
}
export const DeviceCodeError = NamedError.create(
"DeviceCodeError",
z.object({}),
)
export const TokenExchangeError = NamedError.create(
"TokenExchangeError",
z.object({
message: z.string(),
}),
)
export const AuthenticationError = NamedError.create(
"AuthenticationError",
z.object({
message: z.string(),
}),
)
export const CopilotTokenError = NamedError.create(
"CopilotTokenError",
z.object({
message: z.string(),
}),
)
}

View File

@@ -13,7 +13,7 @@ export namespace BunProc {
) {
log.info("running", {
cmd: [which(), ...cmd],
options,
...options,
})
const result = Bun.spawn([which(), ...cmd], {
...options,
@@ -26,6 +26,15 @@ export namespace BunProc {
},
})
const code = await result.exited
// @ts-ignore
const stdout = await result.stdout.text()
// @ts-ignore
const stderr = await result.stderr.text()
log.info("done", {
code,
stdout,
stderr,
})
if (code !== 0) {
throw new Error(`Command failed with exit code ${result.exitCode}`)
}
@@ -53,7 +62,7 @@ export namespace BunProc {
if (parsed.dependencies[pkg] === version) return mod
parsed.dependencies[pkg] = version
await Bun.write(pkgjson, JSON.stringify(parsed, null, 2))
await BunProc.run(["install"], {
await BunProc.run(["install", "--registry=https://registry.npmjs.org"], {
cwd: Global.Path.cache,
}).catch((e) => {
new InstallFailedError(

View File

@@ -1,4 +1,5 @@
import { AuthAnthropic } from "../../auth/anthropic"
import { AuthCopilot } from "../../auth/copilot"
import { Auth } from "../../auth"
import { cmd } from "./cmd"
import * as prompts from "@clack/prompts"
@@ -9,14 +10,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({
@@ -40,15 +41,16 @@ export const AuthListCommand = cmd({
export const AuthLoginCommand = cmd({
command: "login",
describe: "login to a provider",
describe: "log in to a provider",
async handler() {
UI.empty()
prompts.intro("Add credential")
const providers = await ModelsDev.get()
const priority: Record<string, number> = {
anthropic: 0,
openai: 1,
google: 2,
"github-copilot": 1,
openai: 2,
google: 3,
}
let provider = await prompts.select({
message: "Select provider",
@@ -146,6 +148,44 @@ export const AuthLoginCommand = cmd({
}
}
const copilot = await AuthCopilot()
if (provider === "github-copilot" && copilot) {
await new Promise((resolve) => setTimeout(resolve, 10))
const deviceInfo = await copilot.authorize()
prompts.note(
`Please visit: ${deviceInfo.verification}\nEnter code: ${deviceInfo.user}`,
)
const spinner = prompts.spinner()
spinner.start("Waiting for authorization...")
while (true) {
await new Promise((resolve) =>
setTimeout(resolve, deviceInfo.interval * 1000),
)
const response = await copilot.poll(deviceInfo.device)
if (response.status === "pending") continue
if (response.status === "success") {
await Auth.set("github-copilot", {
type: "oauth",
refresh: response.refresh,
access: response.access,
expires: response.expires,
})
spinner.stop("Login successful")
break
}
if (response.status === "failed") {
spinner.stop("Failed to authorize", 1)
break
}
}
prompts.outro("Done")
return
}
const key = await prompts.password({
message: "Enter your API key",
validate: (x) => (x.length > 0 ? undefined : "Required"),
@@ -162,7 +202,7 @@ export const AuthLoginCommand = cmd({
export const AuthLogoutCommand = cmd({
command: "logout",
describe: "logout from a configured provider",
describe: "log out from a configured provider",
async handler() {
UI.empty()
const credentials = await Auth.all().then((x) => Object.entries(x))

View File

@@ -1,20 +0,0 @@
import { AuthAnthropic } from "../../auth/anthropic"
import { UI } from "../ui"
// Example: https://claude.ai/oauth/authorize?code=true&client_id=9d1c250a-e61b-44d9-88ed-5944d1962f5e&response_type=code&redirect_uri=https%3A%2F%2Fconsole.anthropic.com%2Foauth%2Fcode%2Fcallback&scope=org%3Acreate_api_key+user%3Aprofile+user%3Ainference&code_challenge=MdFtFgFap23AWDSN0oa3-eaKjQRFE4CaEhXx8M9fHZg&code_challenge_method=S256&state=rKLtaDzm88GSwekyEqdi0wXX-YqIr13tSzYymSzpvfs
export const LoginAnthropicCommand = {
command: "anthropic",
describe: "Login to Anthropic",
handler: async () => {
const { url, verifier } = await AuthAnthropic.authorize()
UI.println("Login to Anthropic")
UI.println("Open the following URL in your browser:")
UI.println(url)
UI.println("")
const code = await UI.input("Paste the authorization code here: ")
await AuthAnthropic.exchange(code, verifier)
},
}

View File

@@ -25,7 +25,7 @@ const TOOL: Record<string, [string, string]> = {
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", {
@@ -36,12 +36,12 @@ export const RunCommand = cmd({
})
.option("continue", {
alias: ["c"],
describe: "Continue the last session",
describe: "continue the last session",
type: "boolean",
})
.option("session", {
alias: ["s"],
describe: "Session ID to continue",
describe: "session id to continue",
type: "string",
})
.option("share", {
@@ -51,7 +51,7 @@ export const RunCommand = cmd({
.option("model", {
type: "string",
alias: ["m"],
describe: "Model to use in the format of provider/model",
describe: "model to use in the format of provider/model",
})
},
handler: async (args) => {

View File

@@ -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 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: "version to upgrade to, for ex '0.1.48' or 'v0.1.48'",
type: "string",
})
.option("method", {
alias: "m",
describe: "installation method to use",
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()

View File

@@ -1,6 +1,9 @@
import { Config } from "../config/config"
import { MCP } from "../mcp"
export function FormatError(input: unknown) {
if (MCP.Failed.isInstance(input))
return `MCP server "${input.data.name}" failed. Note, opencode does not support MCP authentication yet.`
if (Config.JsonError.isInstance(input))
return `Config file at ${input.data.path} is not valid JSON`
if (Config.InvalidError.isInstance(input))

View File

@@ -1,4 +1,5 @@
import { z } from "zod"
import { EOL } from "os"
import { NamedError } from "../util/error"
export namespace UI {
@@ -29,7 +30,7 @@ export namespace UI {
export function println(...message: string[]) {
print(...message)
Bun.stderr.write("\n")
Bun.stderr.write(EOL)
}
export function print(...message: string[]) {
@@ -52,7 +53,7 @@ export namespace UI {
result.push(row[0])
result.push("\x1b[0m")
result.push(row[1])
result.push("\n")
result.push(EOL)
}
return result.join("").trimEnd()
}

View File

@@ -116,14 +116,17 @@ export namespace Ripgrep {
export async function files(input: {
cwd: string
query?: string
glob?: string
limit?: number
}) {
const commands = [`${await filepath()} --files --hidden --glob='!.git/*'`]
const commands = [
`${await filepath()} --files --hidden --glob='!.git/*' ${input.glob ? `--glob='${input.glob}'` : ``}`,
]
if (input.query)
commands.push(`${await Fzf.filepath()} --filter=${input.query}`)
if (input.limit) commands.push(`head -n ${input.limit}`)
const joined = commands.join(" | ")
const result = await $`${{ raw: joined }}`.cwd(input.cwd).text()
const result = await $`${{ raw: joined }}`.cwd(input.cwd).nothrow().text()
return result.split("\n").filter(Boolean)
}
}

View File

@@ -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)
)
}
}

View File

@@ -4,6 +4,7 @@ import { Server } from "./server/server"
import fs from "fs/promises"
import path from "path"
import { Share } from "./share/share"
import url from "node:url"
import { Global } from "./global"
import yargs from "yargs"
import { hideBin } from "yargs/helpers"
@@ -21,11 +22,16 @@ import { Config } from "./config/config"
import { NamedError } from "./util/error"
import { FormatError } from "./cli/error"
const cancel = new AbortController()
const cli = yargs(hideBin(process.argv))
.scriptName("opencode")
.version(Installation.VERSION)
.help("help", "show help")
.alias("help", "h")
.version("version", "show version number", Installation.VERSION)
.alias("version", "v")
.option("print-logs", {
describe: "Print logs to stderr",
describe: "print logs to stderr",
type: "boolean",
})
.middleware(async () => {
@@ -38,7 +44,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",
@@ -58,10 +64,14 @@ const cli = yargs(hideBin(process.argv))
const server = Server.listen()
let cmd = ["go", "run", "./main.go"]
let cwd = new URL("../../tui/cmd/opencode", import.meta.url).pathname
let cwd = url.fileURLToPath(new URL("../../tui/cmd/opencode", import.meta.url))
if (Bun.embeddedFiles.length > 0) {
const blob = Bun.embeddedFiles[0] as File
const binary = path.join(Global.Path.cache, "tui", blob.name)
let binaryName = blob.name
if (process.platform === "win32" && !binaryName.endsWith(".exe")) {
binaryName += ".exe"
}
const binary = path.join(Global.Path.cache, "tui", binaryName)
const file = Bun.file(binary)
if (!(await file.exists())) {
await Bun.write(file, blob, { mode: 0o755 })
@@ -72,6 +82,7 @@ const cli = yargs(hideBin(process.argv))
}
const proc = Bun.spawn({
cmd: [...cmd, ...process.argv.slice(2)],
signal: cancel.signal,
cwd,
stdout: "inherit",
stderr: "inherit",
@@ -91,7 +102,8 @@ const cli = yargs(hideBin(process.argv))
if (Installation.isSnapshot()) return
const config = await Config.global()
if (config.autoupdate === false) return
const latest = await Installation.latest()
const latest = await Installation.latest().catch(() => {})
if (!latest) return
if (Installation.VERSION === latest) return
const method = await Installation.method()
if (method === "unknown") return
@@ -157,3 +169,5 @@ try {
"Unexpected error, check log file at " + Log.file() + " for more details",
)
}
cancel.abort()

View File

@@ -3,12 +3,15 @@ import { $ } from "bun"
import { z } from "zod"
import { NamedError } from "../util/error"
import { Bus } from "../bus"
import { Log } from "../util/log"
declare global {
const OPENCODE_VERSION: string
}
export namespace Installation {
const log = Log.create({ service: "installation" })
export type Method = Awaited<ReturnType<typeof method>>
export const Event = {
@@ -66,6 +69,10 @@ export namespace Installation {
name: "bun" as const,
command: () => $`bun pm ls -g`.throws(false).text(),
},
{
name: "brew" as const,
command: () => $`brew list --formula opencode-ai`.throws(false).text(),
},
]
checks.sort((a, b) => {
@@ -97,18 +104,31 @@ 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`.env({
HOMEBREW_NO_AUTO_UPDATE: "1",
})
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"),

View File

@@ -2,8 +2,22 @@ import { experimental_createMCPClient, type Tool } from "ai"
import { Experimental_StdioMCPTransport } from "ai/mcp-stdio"
import { App } from "../app/app"
import { Config } from "../config/config"
import { Log } from "../util/log"
import { NamedError } from "../util/error"
import { z } from "zod"
import { Session } from "../session"
import { Bus } from "../bus"
export namespace MCP {
const log = Log.create({ service: "mcp" })
export const Failed = NamedError.create(
"MCPFailed",
z.object({
name: z.string(),
}),
)
const state = App.state(
"mcp",
async () => {
@@ -12,27 +26,56 @@ export namespace MCP {
[name: string]: Awaited<ReturnType<typeof experimental_createMCPClient>>
} = {}
for (const [key, mcp] of Object.entries(cfg.mcp ?? {})) {
log.info("found", { key, type: mcp.type })
if (mcp.type === "remote") {
clients[key] = await experimental_createMCPClient({
const client = await experimental_createMCPClient({
name: key,
transport: {
type: "sse",
url: mcp.url,
},
})
}).catch(() => {})
if (!client) {
Bus.publish(Session.Event.Error, {
error: {
name: "UnknownError",
data: {
message: `MCP server ${key} failed to start`,
},
},
})
continue
}
clients[key] = client
}
if (mcp.type === "local") {
const [cmd, ...args] = mcp.command
clients[key] = await experimental_createMCPClient({
const client = await experimental_createMCPClient({
name: key,
transport: new Experimental_StdioMCPTransport({
stderr: "ignore",
command: cmd,
args,
env: mcp.environment,
env: {
...process.env,
...(cmd === "opencode" ? { BUN_BE_BUN: "1" } : {}),
...mcp.environment,
},
}),
})
}).catch(() => {})
if (!client) {
Bus.publish(Session.Event.Error, {
error: {
name: "UnknownError",
data: {
message: `MCP server ${key} failed to start`,
},
},
})
continue
}
clients[key] = client
}
}

View File

@@ -0,0 +1,4 @@
export async function data() {
const json = await fetch("https://models.dev/api.json").then((x) => x.text())
return json
}

View File

@@ -2,6 +2,7 @@ import { Global } from "../global"
import { Log } from "../util/log"
import path from "path"
import { z } from "zod"
import { data } from "./models-macro" with { type: "macro" }
export namespace ModelsDev {
const log = Log.create({ service: "models.dev" })
@@ -54,16 +55,15 @@ export namespace ModelsDev {
refresh()
return result as Record<string, Provider>
}
await refresh()
return get()
refresh()
const json = await data()
return JSON.parse(json) as Record<string, Provider>
}
async function refresh() {
const file = Bun.file(filepath)
log.info("refreshing")
const result = await fetch("https://models.dev/api.json")
if (!result.ok)
throw new Error(`Failed to fetch models.dev: ${result.statusText}`)
await Bun.write(file, result)
const result = await fetch("https://models.dev/api.json").catch(() => {})
if (result && result.ok) await Bun.write(file, result)
}
}

View File

@@ -19,6 +19,7 @@ import type { Tool } from "../tool/tool"
import { WriteTool } from "../tool/write"
import { TodoReadTool, TodoWriteTool } from "../tool/todo"
import { AuthAnthropic } from "../auth/anthropic"
import { AuthCopilot } from "../auth/copilot"
import { ModelsDev } from "./models"
import { NamedError } from "../util/error"
import { Auth } from "../auth"
@@ -66,6 +67,52 @@ export namespace Provider {
},
}
},
"github-copilot": async (provider) => {
const copilot = await AuthCopilot()
if (!copilot) return false
let info = await Auth.get("github-copilot")
if (!info || info.type !== "oauth") return false
if (provider && provider.models) {
for (const model of Object.values(provider.models)) {
model.cost = {
input: 0,
output: 0,
}
}
}
return {
options: {
apiKey: "",
async fetch(input: any, init: any) {
const info = await Auth.get("github-copilot")
if (!info || info.type !== "oauth") return
if (!info.access || info.expires < Date.now()) {
const tokens = await copilot.access(info.refresh)
if (!tokens)
throw new Error("GitHub Copilot authentication expired")
await Auth.set("github-copilot", {
type: "oauth",
...tokens,
})
info.access = tokens.access
}
const headers = {
...init.headers,
...copilot.HEADERS,
Authorization: `Bearer ${info.access}`,
"Openai-Intent": "conversation-edits",
}
delete headers["x-api-key"]
return fetch(input, {
...init,
headers,
})
},
},
}
},
openai: async () => {
return {
async getModel(sdk: any, modelID: string) {
@@ -75,7 +122,8 @@ export namespace Provider {
}
},
"amazon-bedrock": async () => {
if (!process.env["AWS_PROFILE"]) false
if (!process.env["AWS_PROFILE"] && !process.env["AWS_ACCESS_KEY_ID"])
return false
const region = process.env["AWS_REGION"] ?? "us-east-1"
@@ -208,8 +256,9 @@ export namespace Provider {
for (const [providerID, fn] of Object.entries(CUSTOM_LOADERS)) {
if (disabled.has(providerID)) continue
const result = await fn(database[providerID])
if (result)
if (result) {
mergeProvider(providerID, result.options, "custom", result.getModel)
}
}
// load config

View File

@@ -1,24 +1,25 @@
import type { CoreMessage } from "ai"
import type { LanguageModelV1Prompt } from "ai"
import { unique } from "remeda"
export namespace ProviderTransform {
export function message(
msg: CoreMessage,
index: number,
msgs: LanguageModelV1Prompt,
providerID: string,
modelID: string,
) {
if (
(providerID === "anthropic" || modelID.includes("anthropic")) &&
index < 4
) {
msg.providerOptions = {
...msg.providerOptions,
anthropic: {
cacheControl: { type: "ephemeral" },
},
if (providerID === "anthropic" || modelID.includes("anthropic")) {
const system = msgs.filter((msg) => msg.role === "system").slice(0, 2)
const final = msgs.filter((msg) => msg.role !== "system").slice(-2)
for (const msg of unique([...system, ...final])) {
msg.providerMetadata = {
...msg.providerMetadata,
anthropic: {
cacheControl: { type: "ephemeral" },
},
}
}
}
return msg
return msgs
}
}

View File

@@ -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({
@@ -362,6 +390,33 @@ export namespace Server {
return c.json(Session.abort(body.sessionID))
},
)
.post(
"/session_delete",
describeRoute({
description: "Delete a session and all its data",
responses: {
200: {
description: "Successfully deleted session",
content: {
"application/json": {
schema: resolver(z.boolean()),
},
},
},
},
}),
zValidator(
"json",
z.object({
sessionID: z.string(),
}),
),
async (c) => {
const body = c.req.valid("json")
await Session.remove(body.sessionID)
return c.json(true)
},
)
.post(
"/session_summarize",
describeRoute({

View File

@@ -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"))
}
}

View File

@@ -14,6 +14,7 @@ import {
type CoreMessage,
type UIMessage,
type ProviderMetadata,
wrapLanguageModel,
} from "ai"
import { z, ZodSchema } from "zod"
import { Decimal } from "decimal.js"
@@ -71,6 +72,12 @@ export namespace Session {
info: Info,
}),
),
Deleted: Bus.event(
"session.deleted",
z.object({
info: Info,
}),
),
Error: Bus.event(
"session.error",
z.object({
@@ -159,6 +166,14 @@ export namespace Session {
return share
}
export async function unshare(id: string) {
await Storage.remove("session/share/" + id)
await update(id, (draft) => {
draft.share = undefined
})
await Share.remove(id)
}
export async function update(id: string, editor: (session: Info) => void) {
const { sessions } = state()
const session = await get(id)
@@ -197,6 +212,17 @@ export namespace Session {
}
}
export async function children(parentID: string) {
const result = [] as Session.Info[]
for await (const item of Storage.list("session/info")) {
const sessionID = path.basename(item, ".json")
const session = await get(sessionID)
if (session.parentID !== parentID) continue
result.push(session)
}
return result
}
export function abort(sessionID: string) {
const controller = state().pending.get(sessionID)
if (!controller) return false
@@ -205,6 +231,28 @@ export namespace Session {
return true
}
export async function remove(sessionID: string, emitEvent = true) {
try {
abort(sessionID)
const session = await get(sessionID)
for (const child of await children(sessionID)) {
await remove(child.id, false)
}
await unshare(sessionID).catch(() => {})
await Storage.remove(`session/info/${sessionID}`).catch(() => {})
await Storage.removeDir(`session/message/${sessionID}/`).catch(() => {})
state().sessions.delete(sessionID)
state().messages.delete(sessionID)
if (emitEvent) {
Bus.publish(Event.Deleted, {
info: session,
})
}
} catch (e) {
log.error(e)
}
}
async function updateMessage(msg: Message.Info) {
await Storage.writeJSON(
"session/message/" + msg.metadata.sessionID + "/" + msg.id,
@@ -277,9 +325,7 @@ export namespace Session {
parts: toParts(input.parts),
},
]),
].map((msg, i) =>
ProviderTransform.message(msg, i, input.providerID, input.modelID),
),
],
model: model.language,
})
.then((result) => {
@@ -426,24 +472,6 @@ export namespace Session {
}
let text: Message.TextPart | undefined
await Bun.write(
"/tmp/message.json",
JSON.stringify(
[
...system.map(
(x): CoreMessage => ({
role: "system",
content: x,
}),
),
...convertToCoreMessages(
msgs.map(toUIMessage).filter((x) => x.parts.length > 0),
),
],
null,
2,
),
)
const result = streamText({
onStepFinish: async (step) => {
log.info("step finish", { finishReason: step.finishReason })
@@ -519,12 +547,26 @@ export namespace Session {
...convertToCoreMessages(
msgs.map(toUIMessage).filter((x) => x.parts.length > 0),
),
].map((msg, i) =>
ProviderTransform.message(msg, i, input.providerID, input.modelID),
),
],
temperature: model.info.temperature ? 0 : undefined,
tools: model.info.tool_call === false ? undefined : tools,
model: model.language,
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) {
@@ -715,7 +757,9 @@ export namespace Session {
},
}
await updateMessage(next)
const result = await generateText({
let text: Message.TextPart | undefined
const result = streamText({
abortSignal: abort.signal,
model: model.language,
messages: [
@@ -736,16 +780,46 @@ export namespace Session {
],
},
],
onStepFinish: async (step) => {
const assistant = next.metadata!.assistant!
const usage = getUsage(model.info, step.usage, step.providerMetadata)
assistant.cost += usage.cost
assistant.tokens = usage.tokens
await updateMessage(next)
if (text) {
Bus.publish(Message.Event.PartUpdated, {
part: text,
messageID: next.id,
sessionID: next.metadata.sessionID,
})
}
text = undefined
},
async onFinish(input) {
const assistant = next.metadata!.assistant!
const usage = getUsage(model.info, input.usage, input.providerMetadata)
assistant.cost = usage.cost
assistant.tokens = usage.tokens
next.metadata!.time.completed = Date.now()
await updateMessage(next)
},
})
next.parts.push({
type: "text",
text: result.text,
})
const assistant = next.metadata!.assistant!
const usage = getUsage(model.info, result.usage, result.providerMetadata)
assistant.cost = usage.cost
assistant.tokens = usage.tokens
await updateMessage(next)
for await (const value of result.fullStream) {
switch (value.type) {
case "text-delta":
if (!text) {
text = {
type: "text",
text: value.textDelta,
}
next.parts.push(text)
} else text.text += value.textDelta
await updateMessage(next)
break
}
}
}
function lock(sessionID: string) {

View File

@@ -135,53 +135,55 @@ export namespace Message {
id: z.string(),
role: z.enum(["user", "assistant"]),
parts: z.array(Part),
metadata: z.object({
time: z.object({
created: z.number(),
completed: z.number().optional(),
}),
error: z
.discriminatedUnion("name", [
Provider.AuthError.Schema,
NamedError.Unknown.Schema,
])
.optional(),
sessionID: z.string(),
tool: z.record(
z.string(),
z
metadata: z
.object({
time: z.object({
created: z.number(),
completed: z.number().optional(),
}),
error: z
.discriminatedUnion("name", [
Provider.AuthError.Schema,
NamedError.Unknown.Schema,
])
.optional(),
sessionID: z.string(),
tool: z.record(
z.string(),
z
.object({
title: z.string(),
time: z.object({
start: z.number(),
end: z.number(),
}),
})
.catchall(z.any()),
),
assistant: z
.object({
title: z.string(),
time: z.object({
start: z.number(),
end: z.number(),
system: z.string().array(),
modelID: z.string(),
providerID: z.string(),
path: z.object({
cwd: z.string(),
root: z.string(),
}),
cost: z.number(),
summary: z.boolean().optional(),
tokens: z.object({
input: z.number(),
output: z.number(),
reasoning: z.number(),
cache: z.object({
read: z.number(),
write: z.number(),
}),
}),
})
.catchall(z.any()),
),
assistant: z
.object({
system: z.string().array(),
modelID: z.string(),
providerID: z.string(),
path: z.object({
cwd: z.string(),
root: z.string(),
}),
cost: z.number(),
summary: z.boolean().optional(),
tokens: z.object({
input: z.number(),
output: z.number(),
reasoning: z.number(),
cache: z.object({
read: z.number(),
write: z.number(),
}),
}),
})
.optional(),
}),
.optional(),
})
.openapi({ ref: "Message.Metadata" }),
})
.openapi({
ref: "Message.Info",

View File

@@ -1,7 +1,11 @@
You will generate a short title based on the first message a user begins a conversation with
- ensure it is not more than 50 characters long
- the title should be a summary of the user's message
- it should be one line long
- do not use quotes or colons
- the entire text you return will be used as the title
- never return anything that is more than one sentence (one line) long
Generate a short title based on the first message a user begins a conversation with. CRITICAL: Your response must be EXACTLY one line with NO line breaks, newlines, or multiple sentences.
Requirements:
- Maximum 50 characters
- Single line only - NO newlines or line breaks
- Summary of the user's message
- No quotes, colons, or special formatting
- Do not include explanatory text like "summary:" or similar
- Your entire response becomes the title
IMPORTANT: Return only the title text on a single line. Do not add any explanations, formatting, or additional text.

View File

@@ -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"
@@ -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) {

View File

@@ -70,4 +70,11 @@ export namespace Share {
.then((x) => x.json())
.then((x) => x as { url: string; secret: string })
}
export async function remove(id: string) {
return fetch(`${URL}/share_delete`, {
method: "POST",
body: JSON.stringify({ id }),
}).then((x) => x.json())
}
}

View File

@@ -24,6 +24,16 @@ 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 removeDir(key: string) {
const target = path.join(state().dir, key)
await fs.rm(target, { recursive: true, force: true }).catch(() => {})
}
export async function readJSON<T>(key: string) {
return Bun.file(path.join(state().dir, key + ".json")).json() as Promise<T>
}

View File

@@ -63,10 +63,18 @@ export const BashTool = Tool.define({
metadata: {
stderr,
stdout,
exit: process.exitCode,
description: params.description,
title: params.command,
},
output: stdout.replaceAll(/\x1b\[[0-9;]*m/g, ""),
output: [
`<stdout>`,
stdout ?? "",
`</stdout>`,
`<stderr>`,
stderr ?? "",
`</stderr>`,
].join("\n"),
}
},
})

View File

@@ -22,7 +22,7 @@ export const EditTool = Tool.define({
replaceAll: z
.boolean()
.optional()
.describe("Replace all occurences of old_string (default false)"),
.describe("Replace all occurrences of old_string (default false)"),
}),
async execute(params, ctx) {
if (!params.filePath) {

View File

@@ -3,6 +3,7 @@ import path from "path"
import { Tool } from "./tool"
import { App } from "../app/app"
import DESCRIPTION from "./glob.txt"
import { Ripgrep } from "../external/ripgrep"
export const GlobTool = Tool.define({
id: "glob",
@@ -24,10 +25,12 @@ export const GlobTool = Tool.define({
: path.resolve(app.path.cwd, search)
const limit = 100
const glob = new Bun.Glob(params.pattern)
const files = []
let truncated = false
for await (const file of glob.scan({ cwd: search, dot: true })) {
for (const file of await Ripgrep.files({
cwd: search,
glob: params.pattern,
})) {
if (files.length >= limit) {
truncated = true
break

View File

@@ -10,7 +10,7 @@ To make multiple file edits, provide the following:
2. edits: An array of edit operations to perform, where each edit contains:
- old_string: The text to replace (must match the file contents exactly, including all whitespace and indentation)
- new_string: The edited text to replace the old_string
- replace_all: Replace all occurences of old_string. This parameter is optional and defaults to false.
- replace_all: Replace all occurrences of old_string. This parameter is optional and defaults to false.
IMPORTANT:
- All edits are applied in sequence, in the order they are provided

View File

@@ -19,7 +19,7 @@ export namespace Log {
await fs.mkdir(dir, { recursive: true })
cleanup(dir)
if (options.print) return
logpath = path.join(dir, new Date().toISOString().split(".")[0] + ".log")
logpath = path.join(dir, new Date().toISOString().split(".")[0].replace(/:/g, "") + ".log")
const logfile = Bun.file(logpath)
await fs.truncate(logpath).catch(() => {})
const writer = logfile.writer()

View File

@@ -14,7 +14,7 @@ describe("tool.glob", () => {
await App.provide({ cwd: process.cwd() }, async () => {
let result = await GlobTool.execute(
{
pattern: "./node_modules/**/*",
pattern: "../../node_modules/**/*",
path: undefined,
},
ctx,
@@ -33,7 +33,7 @@ describe("tool.glob", () => {
)
expect(result.metadata).toMatchObject({
truncated: false,
count: 2,
count: 3,
})
})
})

View File

@@ -26,7 +26,11 @@ func main() {
appInfoStr := os.Getenv("OPENCODE_APP_INFO")
var appInfo client.AppInfo
json.Unmarshal([]byte(appInfoStr), &appInfo)
err := json.Unmarshal([]byte(appInfoStr), &appInfo)
if err != nil {
slog.Error("Failed to unmarshal app info", "error", err)
os.Exit(1)
}
logfile := filepath.Join(appInfo.Path.Data, "log", "tui.log")
if _, err := os.Stat(filepath.Dir(logfile)); os.IsNotExist(err) {

View File

@@ -6,11 +6,13 @@ import (
"path/filepath"
"sort"
"strings"
"time"
"log/slog"
tea "github.com/charmbracelet/bubbletea/v2"
"github.com/sst/opencode/internal/commands"
"github.com/sst/opencode/internal/components/toast"
"github.com/sst/opencode/internal/config"
"github.com/sst/opencode/internal/theme"
"github.com/sst/opencode/internal/util"
@@ -44,9 +46,12 @@ type SendMsg struct {
Text string
Attachments []Attachment
}
type CompletionDialogTriggerdMsg struct {
type CompletionDialogTriggeredMsg struct {
InitialValue string
}
type OptimisticMessageAddedMsg struct {
Message client.MessageInfo
}
func New(
ctx context.Context,
@@ -60,6 +65,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 +93,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)
}
@@ -114,6 +131,10 @@ func (a *App) InitializeProvider() tea.Cmd {
// TODO: notify user
return nil
}
if providersResponse != nil && providersResponse.StatusCode() != 200 {
slog.Error("failed to retrieve providers", "status", providersResponse.StatusCode(), "message", string(providersResponse.Body))
return nil
}
providers := []client.ProviderInfo{}
var defaultProvider *client.ProviderInfo
var defaultModel *client.ModelInfo
@@ -236,17 +257,19 @@ func (a *App) InitializeProject(ctx context.Context) tea.Cmd {
}
func (a *App) CompactSession(ctx context.Context) tea.Cmd {
response, err := a.Client.PostSessionSummarizeWithResponse(ctx, client.PostSessionSummarizeJSONRequestBody{
SessionID: a.Session.Id,
ProviderID: a.Provider.Id,
ModelID: a.Model.Id,
})
if err != nil {
slog.Error("Failed to compact session", "error", err)
}
if response != nil && response.StatusCode() != 200 {
slog.Error("Failed to compact session", "error", response.StatusCode)
}
go func() {
response, err := a.Client.PostSessionSummarizeWithResponse(ctx, client.PostSessionSummarizeJSONRequestBody{
SessionID: a.Session.Id,
ProviderID: a.Provider.Id,
ModelID: a.Model.Id,
})
if err != nil {
slog.Error("Failed to compact session", "error", err)
}
if response != nil && response.StatusCode() != 200 {
slog.Error("Failed to compact session", "error", response.StatusCode)
}
}()
return nil
}
@@ -279,19 +302,12 @@ func (a *App) SendChatMessage(ctx context.Context, text string, attachments []At
if a.Session.Id == "" {
session, err := a.CreateSession(ctx)
if err != nil {
// status.Error(err.Error())
return nil
return toast.NewErrorToast(err.Error())
}
a.Session = session
cmds = append(cmds, util.CmdHandler(SessionSelectedMsg(session)))
}
// TODO: Handle attachments when API supports them
if len(attachments) > 0 {
// For now, ignore attachments
// return "", fmt.Errorf("attachments not supported yet")
}
part := client.MessagePart{}
part.FromMessagePartText(client.MessagePartText{
Type: "text",
@@ -299,7 +315,26 @@ func (a *App) SendChatMessage(ctx context.Context, text string, attachments []At
})
parts := []client.MessagePart{part}
go func() {
optimisticMessage := client.MessageInfo{
Id: fmt.Sprintf("optimistic-%d", time.Now().UnixNano()),
Role: client.User,
Parts: parts,
Metadata: client.MessageMetadata{
SessionID: a.Session.Id,
Time: struct {
Completed *float32 `json:"completed,omitempty"`
Created float32 `json:"created"`
}{
Created: float32(time.Now().Unix()),
},
Tool: make(map[string]client.MessageMetadata_Tool_AdditionalProperties),
},
}
a.Messages = append(a.Messages, optimisticMessage)
cmds = append(cmds, util.CmdHandler(OptimisticMessageAddedMsg{Message: optimisticMessage}))
cmds = append(cmds, func() tea.Msg {
response, err := a.Client.PostSessionChat(ctx, client.PostSessionChatJSONRequestBody{
SessionID: a.Session.Id,
Parts: parts,
@@ -307,14 +342,17 @@ func (a *App) SendChatMessage(ctx context.Context, text string, attachments []At
ModelID: a.Model.Id,
})
if err != nil {
slog.Error("Failed to send message", "error", err)
// status.Error(err.Error())
errormsg := fmt.Sprintf("failed to send message: %v", err)
slog.Error(errormsg)
return toast.NewErrorToast(errormsg)()
}
if response != nil && response.StatusCode != 200 {
slog.Error("Failed to send message", "error", fmt.Sprintf("failed to send message: %d", response.StatusCode))
// status.Error(fmt.Sprintf("failed to send message: %d", response.StatusCode))
errormsg := fmt.Sprintf("failed to send message: %d", response.StatusCode)
slog.Error(errormsg)
return toast.NewErrorToast(errormsg)()
}
}()
return nil
})
// The actual response will come through SSE
// For now, just return success
@@ -358,6 +396,19 @@ func (a *App) ListSessions(ctx context.Context) ([]client.SessionInfo, error) {
return sessions, nil
}
func (a *App) DeleteSession(ctx context.Context, sessionID string) error {
resp, err := a.Client.PostSessionDeleteWithResponse(ctx, client.PostSessionDeleteJSONRequestBody{
SessionID: sessionID,
})
if err != nil {
return err
}
if resp.StatusCode() != 200 {
return fmt.Errorf("failed to delete session: %d", resp.StatusCode())
}
return nil
}
func (a *App) ListMessages(ctx context.Context, sessionId string) ([]client.MessageInfo, error) {
resp, err := a.Client.PostSessionMessagesWithResponse(ctx, client.PostSessionMessagesJSONRequestBody{SessionID: sessionId})
if err != nil {

View File

@@ -32,17 +32,19 @@ type EditorComponent interface {
Newline() (tea.Model, tea.Cmd)
Previous() (tea.Model, tea.Cmd)
Next() (tea.Model, tea.Cmd)
SetInterruptKeyInDebounce(inDebounce bool)
}
type editorComponent struct {
app *app.App
width, height int
textarea textarea.Model
attachments []app.Attachment
history []string
historyIndex int
currentMessage string
spinner spinner.Model
app *app.App
width, height int
textarea textarea.Model
attachments []app.Attachment
history []string
historyIndex int
currentMessage string
spinner spinner.Model
interruptKeyInDebounce bool
}
func (m *editorComponent) Init() tea.Cmd {
@@ -115,9 +117,14 @@ func (m *editorComponent) Content() string {
Background(t.BackgroundElement()).
Render(textarea)
hint := base("enter") + muted(" send ")
hint := base(m.getSubmitKeyText()) + muted(" send ")
if m.app.IsBusy() {
hint = muted("working") + m.spinner.View() + muted(" ") + base("esc") + muted(" interrupt")
keyText := m.getInterruptKeyText()
if m.interruptKeyInDebounce {
hint = muted("working") + m.spinner.View() + muted(" ") + base(keyText+" again") + muted(" interrupt")
} else {
hint = muted("working") + m.spinner.View() + muted(" ") + base(keyText) + muted(" interrupt")
}
}
model := ""
@@ -263,6 +270,18 @@ func (m *editorComponent) Next() (tea.Model, tea.Cmd) {
return m, nil
}
func (m *editorComponent) SetInterruptKeyInDebounce(inDebounce bool) {
m.interruptKeyInDebounce = inDebounce
}
func (m *editorComponent) getInterruptKeyText() string {
return m.app.Commands[commands.SessionInterruptCommand].Keys()[0]
}
func (m *editorComponent) getSubmitKeyText() string {
return m.app.Commands[commands.InputSubmitCommand].Keys()[0]
}
func createTextArea(existing *textarea.Model) textarea.Model {
t := theme.CurrentTheme()
bgColor := t.BackgroundElement()
@@ -311,11 +330,12 @@ func NewEditorComponent(app *app.App) EditorComponent {
ta := createTextArea(nil)
return &editorComponent{
app: app,
textarea: ta,
history: []string{},
historyIndex: 0,
currentMessage: "",
spinner: s,
app: app,
textarea: ta,
history: []string{},
historyIndex: 0,
currentMessage: "",
spinner: s,
interruptKeyInDebounce: false,
}
}

View File

@@ -44,7 +44,6 @@ func toMarkdown(content string, width int, backgroundColor compat.AdaptiveColor)
}
}
}
content = strings.Join(lines, "\n")
return strings.TrimSuffix(content, "\n")
}
@@ -227,7 +226,11 @@ func renderText(message client.MessageInfo, text string, author string) string {
textWidth := max(lipgloss.Width(text), lipgloss.Width(info))
markdownWidth := min(textWidth, width-padding-4) // -4 for the border and padding
if message.Role == client.Assistant {
markdownWidth = width - padding - 4
markdownWidth = width - padding - 4 - 2
}
if message.Role == client.User {
text = strings.ReplaceAll(text, "<", "\\<")
text = strings.ReplaceAll(text, ">", "\\>")
}
content := toMarkdown(text, markdownWidth, t.BackgroundPanel())
content = strings.Join([]string{content, info}, "\n")
@@ -250,7 +253,7 @@ func renderText(message client.MessageInfo, text string, author string) string {
func renderToolInvocation(
toolCall client.MessageToolInvocationToolCall,
result *string,
metadata client.MessageInfo_Metadata_Tool_AdditionalProperties,
metadata client.MessageMetadata_Tool_AdditionalProperties,
showDetails bool,
isLast bool,
contentOnly bool,
@@ -463,7 +466,7 @@ func renderToolInvocation(
if metadata, ok := call["metadata"].(map[string]any); ok {
data, _ = json.Marshal(metadata)
var toolMetadata client.MessageInfo_Metadata_Tool_AdditionalProperties
var toolMetadata client.MessageMetadata_Tool_AdditionalProperties
_ = json.Unmarshal(data, &toolMetadata)
step := renderToolInvocation(

View File

@@ -58,6 +58,12 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.viewport.GotoBottom()
m.tail = true
return m, nil
case app.OptimisticMessageAddedMsg:
m.renderView()
if m.tail {
m.viewport.GotoBottom()
}
return m, nil
case dialog.ThemeSelectedMsg:
m.cache.Clear()
return m, m.Reload()
@@ -171,7 +177,7 @@ func (m *messagesComponent) renderView() {
isLastToolInvocation := slices.Contains(lastToolIndices, i)
toolInvocationPart := part.(client.MessagePartToolInvocation)
toolCall, _ := toolInvocationPart.ToolInvocation.AsMessageToolInvocationToolCall()
metadata := client.MessageInfo_Metadata_Tool_AdditionalProperties{}
metadata := client.MessageMetadata_Tool_AdditionalProperties{}
if _, ok := message.Metadata.Tool[toolCall.ToolCallId]; ok {
metadata = message.Metadata.Tool[toolCall.ToolCallId]
}

View File

@@ -25,6 +25,7 @@ type commandsComponent struct {
app *app.App
width, height int
showKeybinds bool
showAll bool
background *compat.AdaptiveColor
limit *int
}
@@ -75,17 +76,32 @@ func (c *commandsComponent) View() string {
keybindStyle = keybindStyle.Background(*c.background)
}
var commandsWithTriggers []commands.Command
var commandsToShow []commands.Command
var triggeredCommands []commands.Command
var untriggeredCommands []commands.Command
for _, cmd := range c.app.Commands.Sorted() {
if cmd.Trigger != "" {
commandsWithTriggers = append(commandsWithTriggers, cmd)
if c.showAll || cmd.Trigger != "" {
if cmd.Trigger != "" {
triggeredCommands = append(triggeredCommands, cmd)
} else if c.showAll {
untriggeredCommands = append(untriggeredCommands, cmd)
}
}
}
if c.limit != nil && len(commandsWithTriggers) > *c.limit {
commandsWithTriggers = commandsWithTriggers[:*c.limit]
// Combine triggered commands first, then untriggered
commandsToShow = append(commandsToShow, triggeredCommands...)
commandsToShow = append(commandsToShow, untriggeredCommands...)
if c.limit != nil && len(commandsToShow) > *c.limit {
commandsToShow = commandsToShow[:*c.limit]
}
if len(commandsWithTriggers) == 0 {
if len(commandsToShow) == 0 {
if c.showAll {
return styles.Muted().Render("No commands available")
}
return styles.Muted().Render("No commands with triggers available")
}
@@ -101,10 +117,15 @@ func (c *commandsComponent) View() string {
keybinds string
}
rows := make([]commandRow, 0, len(commandsWithTriggers))
rows := make([]commandRow, 0, len(commandsToShow))
for _, cmd := range commandsWithTriggers {
trigger := "/" + cmd.Trigger
for _, cmd := range commandsToShow {
trigger := ""
if cmd.Trigger != "" {
trigger = "/" + cmd.Trigger
} else {
trigger = string(cmd.Name)
}
description := cmd.Description
// Format keybindings
@@ -144,6 +165,7 @@ func (c *commandsComponent) View() string {
// Build the output
var output strings.Builder
maxWidth := 0
for _, row := range rows {
// Pad each column to align properly
trigger := fmt.Sprintf("%-*s", maxTriggerWidth, row.trigger)
@@ -160,10 +182,14 @@ func (c *commandsComponent) View() string {
}
output.WriteString(line + "\n")
maxWidth = max(maxWidth, lipgloss.Width(line))
}
// Remove trailing newline
result := strings.TrimSuffix(output.String(), "\n")
if c.background != nil {
result = lipgloss.NewStyle().Background(c.background).Width(maxWidth).Render(result)
}
return result
}
@@ -188,11 +214,18 @@ func WithLimit(limit int) Option {
}
}
func WithShowAll(showAll bool) Option {
return func(c *commandsComponent) {
c.showAll = showAll
}
}
func New(app *app.App, opts ...Option) CommandsComponent {
c := &commandsComponent{
app: app,
background: nil,
showKeybinds: true,
showAll: false,
}
for _, opt := range opts {
opt(c)

View File

@@ -116,7 +116,7 @@ func (c *completionDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case []CompletionItemI:
c.list.SetItems(msg)
case app.CompletionDialogTriggerdMsg:
case app.CompletionDialogTriggeredMsg:
c.pseudoSearchTextArea.SetValue(msg.InitialValue)
case tea.KeyMsg:
if c.pseudoSearchTextArea.Focused() {

View File

@@ -1,71 +1,62 @@
package dialog
import (
"strings"
"github.com/charmbracelet/bubbles/v2/viewport"
tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/lipgloss/v2"
"github.com/sst/opencode/internal/commands"
"github.com/sst/opencode/internal/app"
commandsComponent "github.com/sst/opencode/internal/components/commands"
"github.com/sst/opencode/internal/components/modal"
"github.com/sst/opencode/internal/layout"
"github.com/sst/opencode/internal/theme"
)
type helpDialog struct {
width int
height int
modal *modal.Modal
commands []commands.Command
width int
height int
modal *modal.Modal
app *app.App
commandsComponent commandsComponent.CommandsComponent
viewport viewport.Model
}
func (h *helpDialog) Init() tea.Cmd {
return nil
return tea.Batch(
h.commandsComponent.Init(),
h.viewport.Init(),
)
}
func (h *helpDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmds []tea.Cmd
switch msg := msg.(type) {
case tea.WindowSizeMsg:
h.width = msg.Width
h.height = msg.Height
// Set viewport size with some padding for the modal
h.viewport = viewport.New(viewport.WithWidth(msg.Width-4), viewport.WithHeight(msg.Height-6))
h.commandsComponent.SetSize(msg.Width-4, msg.Height-6)
}
return h, nil
// Update commands component first to get the latest content
_, cmdCmd := h.commandsComponent.Update(msg)
cmds = append(cmds, cmdCmd)
// Update viewport content
h.viewport.SetContent(h.commandsComponent.View())
// Update viewport
var vpCmd tea.Cmd
h.viewport, vpCmd = h.viewport.Update(msg)
cmds = append(cmds, vpCmd)
return h, tea.Batch(cmds...)
}
func (h *helpDialog) View() string {
t := theme.CurrentTheme()
keyStyle := lipgloss.NewStyle().
Background(t.BackgroundElement()).
Foreground(t.Text()).
Bold(true)
descStyle := lipgloss.NewStyle().
Background(t.BackgroundElement()).
Foreground(t.TextMuted())
contentStyle := lipgloss.NewStyle().
PaddingLeft(1).Background(t.BackgroundElement())
lines := []string{}
for _, b := range h.commands {
// Only interested in slash commands
if b.Trigger == "" {
continue
}
content := keyStyle.Render("/" + b.Trigger)
content += descStyle.Render(" " + b.Description)
// for i, key := range b.Keybindings {
// if i == 0 {
// keyString := " (" + key.Key + ")"
// space := max(h.width-lipgloss.Width(content)-lipgloss.Width(keyString), 0)
// spacer := strings.Repeat(" ", space)
// content += descStyle.Render(spacer)
// content += descStyle.Render(keyString)
// }
// }
lines = append(lines, contentStyle.Render(content))
}
return strings.Join(lines, "\n")
h.commandsComponent.SetBackgroundColor(t.BackgroundElement())
return h.viewport.View()
}
func (h *helpDialog) Render(background string) string {
@@ -80,9 +71,16 @@ type HelpDialog interface {
layout.Modal
}
func NewHelpDialog(commands []commands.Command) HelpDialog {
func NewHelpDialog(app *app.App) HelpDialog {
vp := viewport.New(viewport.WithHeight(12))
return &helpDialog{
commands: commands,
app: app,
commandsComponent: commandsComponent.New(app,
commandsComponent.WithBackground(theme.CurrentTheme().BackgroundElement()),
commandsComponent.WithShowAll(true),
commandsComponent.WithKeybinds(true),
),
modal: modal.New(modal.WithTitle("Help")),
viewport: vp,
}
}

View File

@@ -11,6 +11,7 @@ import (
tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/lipgloss/v2"
"github.com/sst/opencode/internal/app"
"github.com/sst/opencode/internal/components/list"
"github.com/sst/opencode/internal/components/modal"
"github.com/sst/opencode/internal/layout"
"github.com/sst/opencode/internal/styles"
@@ -33,20 +34,15 @@ type modelDialog struct {
app *app.App
availableProviders []client.ProviderInfo
provider client.ProviderInfo
selectedIdx int
width int
height int
scrollOffset int
hScrollOffset int
hScrollPossible bool
modal *modal.Modal
width int
height int
hScrollOffset int
hScrollPossible bool
modal *modal.Modal
modelList list.List[list.StringItem]
}
type modelKeyMap struct {
Up key.Binding
Down key.Binding
Left key.Binding
Right key.Binding
Enter key.Binding
@@ -54,14 +50,6 @@ type modelKeyMap struct {
}
var modelKeys = modelKeyMap{
Up: key.NewBinding(
key.WithKeys("up", "k"),
key.WithHelp("↑", "previous model"),
),
Down: key.NewBinding(
key.WithKeys("down", "j"),
key.WithHelp("↓", "next model"),
),
Left: key.NewBinding(
key.WithKeys("left", "h"),
key.WithHelp("←", "scroll left"),
@@ -81,15 +69,7 @@ var modelKeys = modelKeyMap{
}
func (m *modelDialog) Init() tea.Cmd {
// cfg := config.Get()
// modelInfo := GetSelectedModel(cfg)
// m.availableProviders = getEnabledProviders(cfg)
// m.hScrollPossible = len(m.availableProviders) > 1
// m.provider = modelInfo.Provider
// m.hScrollOffset = findProviderIndex(m.availableProviders, m.provider)
// m.setupModelsForProvider(m.provider)
m.setupModelsForProvider(m.provider.Id)
return nil
}
@@ -97,26 +77,32 @@ func (m *modelDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch {
case key.Matches(msg, modelKeys.Up):
m.moveSelectionUp()
case key.Matches(msg, modelKeys.Down):
m.moveSelectionDown()
case key.Matches(msg, modelKeys.Left):
if m.hScrollPossible {
m.switchProvider(-1)
}
return m, nil
case key.Matches(msg, modelKeys.Right):
if m.hScrollPossible {
m.switchProvider(1)
}
return m, nil
case key.Matches(msg, modelKeys.Enter):
selectedItem, _ := m.modelList.GetSelectedItem()
models := m.models()
var selectedModel client.ModelInfo
for _, model := range models {
if model.Name == string(selectedItem) {
selectedModel = model
break
}
}
return m, tea.Sequence(
util.CmdHandler(modal.CloseModalMsg{}),
util.CmdHandler(
app.ModelSelectedMsg{
Provider: m.provider,
Model: models[m.selectedIdx],
Model: selectedModel,
}),
)
case key.Matches(msg, modelKeys.Escape):
@@ -127,7 +113,10 @@ func (m *modelDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.height = msg.Height
}
return m, nil
// Update the list component
updatedList, cmd := m.modelList.Update(msg)
m.modelList = updatedList.(list.List[list.StringItem])
return m, cmd
}
func (m *modelDialog) models() []client.ModelInfo {
@@ -137,40 +126,9 @@ func (m *modelDialog) models() []client.ModelInfo {
return models
}
// moveSelectionUp moves the selection up or wraps to bottom
func (m *modelDialog) moveSelectionUp() {
if m.selectedIdx > 0 {
m.selectedIdx--
} else {
m.selectedIdx = len(m.provider.Models) - 1
m.scrollOffset = max(0, len(m.provider.Models)-numVisibleModels)
}
// Keep selection visible
if m.selectedIdx < m.scrollOffset {
m.scrollOffset = m.selectedIdx
}
}
// moveSelectionDown moves the selection down or wraps to top
func (m *modelDialog) moveSelectionDown() {
if m.selectedIdx < len(m.provider.Models)-1 {
m.selectedIdx++
} else {
m.selectedIdx = 0
m.scrollOffset = 0
}
// Keep selection visible
if m.selectedIdx >= m.scrollOffset+numVisibleModels {
m.scrollOffset = m.selectedIdx - (numVisibleModels - 1)
}
}
func (m *modelDialog) switchProvider(offset int) {
newOffset := m.hScrollOffset + offset
// Ensure we stay within bounds
if newOffset < 0 {
newOffset = len(m.availableProviders) - 1
}
@@ -185,105 +143,46 @@ func (m *modelDialog) switchProvider(offset int) {
}
func (m *modelDialog) View() string {
t := theme.CurrentTheme()
baseStyle := lipgloss.NewStyle().
Background(t.BackgroundElement()).
Foreground(t.Text())
// Render visible models
endIdx := min(m.scrollOffset+numVisibleModels, len(m.provider.Models))
modelItems := make([]string, 0, endIdx-m.scrollOffset)
models := m.models()
for i := m.scrollOffset; i < endIdx; i++ {
itemStyle := baseStyle.Width(maxDialogWidth)
if i == m.selectedIdx {
itemStyle = itemStyle.
Background(t.Primary()).
Foreground(t.BackgroundElement()).
Bold(true)
}
modelItems = append(modelItems, itemStyle.Render(models[i].Name))
}
listView := m.modelList.View()
scrollIndicator := m.getScrollIndicators(maxDialogWidth)
content := lipgloss.JoinVertical(
lipgloss.Left,
baseStyle.
Width(maxDialogWidth).
Render(lipgloss.JoinVertical(lipgloss.Left, modelItems...)),
scrollIndicator,
)
return content
return strings.Join([]string{listView, scrollIndicator}, "\n")
}
func (m *modelDialog) getScrollIndicators(maxWidth int) string {
var indicator string
if len(m.provider.Models) > numVisibleModels {
if m.scrollOffset > 0 {
indicator += "↑ "
}
if m.scrollOffset+numVisibleModels < len(m.provider.Models) {
indicator += "↓ "
}
}
if m.hScrollPossible {
indicator = "← " + indicator + "→"
indicator = "← → (switch provider) "
}
if indicator == "" {
return ""
}
t := theme.CurrentTheme()
baseStyle := styles.BaseStyle()
return baseStyle.
Foreground(t.Primary()).
return styles.BaseStyle().
Foreground(t.TextMuted()).
Width(maxWidth).
Align(lipgloss.Right).
Bold(true).
Render(indicator)
}
// findProviderIndex returns the index of the provider in the list, or -1 if not found
// func findProviderIndex(providers []string, provider string) int {
// for i, p := range providers {
// if p == provider {
// return i
// }
// }
// return -1
// }
func (m *modelDialog) setupModelsForProvider(providerId string) {
models := m.models()
modelNames := make([]string, len(models))
for i, model := range models {
modelNames[i] = model.Name
}
func (m *modelDialog) setupModelsForProvider(_ string) {
m.selectedIdx = 0
m.scrollOffset = 0
m.modelList = list.NewStringList(modelNames, numVisibleModels, "No models available", true)
m.modelList.SetMaxWidth(maxDialogWidth)
// cfg := config.Get()
// agentCfg := cfg.Agents[config.AgentPrimary]
// selectedModelId := agentCfg.Model
// m.provider = provider
// m.models = getModelsForProvider(provider)
// Try to select the current model if it belongs to this provider
// if provider == models.SupportedModels[selectedModelId].Provider {
// for i, model := range m.models {
// if model.ID == selectedModelId {
// m.selectedIdx = i
// // Adjust scroll position to keep selected model visible
// if m.selectedIdx >= numVisibleModels {
// m.scrollOffset = m.selectedIdx - (numVisibleModels - 1)
// }
// break
// }
// }
// }
if m.app.Provider != nil && m.app.Model != nil && m.app.Provider.Id == providerId {
for i, model := range models {
if model.Id == m.app.Model.Id {
m.modelList.SetSelectedIndex(i)
break
}
}
}
}
func (m *modelDialog) Render(background string) string {
@@ -297,11 +196,30 @@ func (s *modelDialog) Close() tea.Cmd {
func NewModelDialog(app *app.App) ModelDialog {
availableProviders, _ := app.ListProviders(context.Background())
return &modelDialog{
availableProviders: availableProviders,
hScrollOffset: 0,
hScrollPossible: len(availableProviders) > 1,
provider: availableProviders[0],
modal: modal.New(modal.WithTitle(fmt.Sprintf("Select %s Model", availableProviders[0].Name))),
currentProvider := availableProviders[0]
hScrollOffset := 0
if app.Provider != nil {
for i, provider := range availableProviders {
if provider.Id == app.Provider.Id {
currentProvider = provider
hScrollOffset = i
break
}
}
}
dialog := &modelDialog{
app: app,
availableProviders: availableProviders,
hScrollOffset: hScrollOffset,
hScrollPossible: len(availableProviders) > 1,
provider: currentProvider,
modal: modal.New(
modal.WithTitle(fmt.Sprintf("Select %s Model", currentProvider.Name)),
modal.WithMaxWidth(maxDialogWidth+4),
),
}
dialog.setupModelsForProvider(currentProvider.Id)
return dialog
}

View File

@@ -2,11 +2,17 @@ package dialog
import (
"context"
"strings"
"slices"
tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/lipgloss/v2"
"github.com/muesli/reflow/truncate"
"github.com/sst/opencode/internal/app"
"github.com/sst/opencode/internal/components/list"
"github.com/sst/opencode/internal/components/modal"
"github.com/sst/opencode/internal/components/toast"
"github.com/sst/opencode/internal/layout"
"github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme"
@@ -19,33 +25,65 @@ type SessionDialog interface {
layout.Modal
}
type sessionItem client.SessionInfo
// sessionItem is a custom list item for sessions that can show delete confirmation
type sessionItem struct {
title string
isDeleteConfirming bool
}
func (s sessionItem) Render(selected bool, width int) string {
t := theme.CurrentTheme()
baseStyle := styles.BaseStyle().
Width(width - 4).
Background(t.BackgroundElement())
baseStyle := styles.BaseStyle()
if selected {
baseStyle = baseStyle.
Background(t.Primary()).
Foreground(t.BackgroundElement()).
Bold(true)
var text string
if s.isDeleteConfirming {
text = "Press again to confirm delete"
} else {
baseStyle = baseStyle.
Foreground(t.Text())
text = s.title
}
return baseStyle.Padding(0, 1).Render(s.Title)
truncatedStr := truncate.StringWithTail(text, uint(width-1), "...")
var itemStyle lipgloss.Style
if selected {
if s.isDeleteConfirming {
// Red background for delete confirmation
itemStyle = baseStyle.
Background(t.Error()).
Foreground(t.Background()).
Width(width).
PaddingLeft(1)
} else {
// Normal selection
itemStyle = baseStyle.
Background(t.Primary()).
Foreground(t.Background()).
Width(width).
PaddingLeft(1)
}
} else {
if s.isDeleteConfirming {
// Red text for delete confirmation when not selected
itemStyle = baseStyle.
Foreground(t.Error()).
PaddingLeft(1)
} else {
itemStyle = baseStyle.
PaddingLeft(1)
}
}
return itemStyle.Render(truncatedStr)
}
type sessionDialog struct {
width int
height int
modal *modal.Modal
selectedSessionID string
list list.List[sessionItem]
width int
height int
modal *modal.Modal
sessions []client.SessionInfo
list list.List[sessionItem]
app *app.App
deleteConfirmation int // -1 means no confirmation, >= 0 means confirming deletion of session at this index
}
func (s *sessionDialog) Init() tea.Cmd {
@@ -61,13 +99,45 @@ func (s *sessionDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case tea.KeyPressMsg:
switch msg.String() {
case "enter":
if item, idx := s.list.GetSelectedItem(); idx >= 0 {
s.selectedSessionID = item.Id
if s.deleteConfirmation >= 0 {
s.deleteConfirmation = -1
s.updateListItems()
return s, nil
}
if _, idx := s.list.GetSelectedItem(); idx >= 0 && idx < len(s.sessions) {
selectedSession := s.sessions[idx]
return s, tea.Sequence(
util.CmdHandler(modal.CloseModalMsg{}),
util.CmdHandler(app.SessionSelectedMsg(&item)),
util.CmdHandler(app.SessionSelectedMsg(&selectedSession)),
)
}
case "x", "delete", "backspace":
if _, idx := s.list.GetSelectedItem(); idx >= 0 && idx < len(s.sessions) {
if s.deleteConfirmation == idx {
// Second press - actually delete the session
sessionToDelete := s.sessions[idx]
return s, tea.Sequence(
func() tea.Msg {
s.sessions = slices.Delete(s.sessions, idx, idx+1)
s.deleteConfirmation = -1
s.updateListItems()
return nil
},
s.deleteSession(sessionToDelete.Id),
)
} else {
// First press - enter delete confirmation mode
s.deleteConfirmation = idx
s.updateListItems()
return s, nil
}
}
case "esc":
if s.deleteConfirmation >= 0 {
s.deleteConfirmation = -1
s.updateListItems()
return s, nil
}
}
}
@@ -78,7 +148,42 @@ func (s *sessionDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
func (s *sessionDialog) Render(background string) string {
return s.modal.Render(s.list.View(), background)
listView := s.list.View()
t := theme.CurrentTheme()
helpStyle := styles.BaseStyle().PaddingLeft(1).PaddingTop(1)
helpText := styles.BaseStyle().Foreground(t.Text()).Render("x/del")
helpText = helpText + styles.BaseStyle().Background(t.BackgroundElement()).Foreground(t.TextMuted()).Render(" delete session")
helpText = helpStyle.Render(helpText)
content := strings.Join([]string{listView, helpText}, "\n")
return s.modal.Render(content, background)
}
func (s *sessionDialog) updateListItems() {
_, currentIdx := s.list.GetSelectedItem()
var items []sessionItem
for i, sess := range s.sessions {
item := sessionItem{
title: sess.Title,
isDeleteConfirming: s.deleteConfirmation == i,
}
items = append(items, item)
}
s.list.SetItems(items)
s.list.SetSelectedIndex(currentIdx)
}
func (s *sessionDialog) deleteSession(sessionID string) tea.Cmd {
return func() tea.Msg {
ctx := context.Background()
if err := s.app.DeleteSession(ctx, sessionID); err != nil {
return toast.NewErrorToast("Failed to delete session: " + err.Error())()
}
return nil
}
}
func (s *sessionDialog) Close() tea.Cmd {
@@ -89,23 +194,36 @@ func (s *sessionDialog) Close() tea.Cmd {
func NewSessionDialog(app *app.App) SessionDialog {
sessions, _ := app.ListSessions(context.Background())
var sessionItems []sessionItem
var filteredSessions []client.SessionInfo
var items []sessionItem
for _, sess := range sessions {
if sess.ParentID != nil {
continue
}
sessionItems = append(sessionItems, sessionItem(sess))
filteredSessions = append(filteredSessions, sess)
items = append(items, sessionItem{
title: sess.Title,
isDeleteConfirming: false,
})
}
list := list.NewListComponent(
sessionItems,
// Create a generic list component
listComponent := list.NewListComponent(
items,
10, // maxVisibleSessions
"No sessions available",
true, // useAlphaNumericKeys
)
listComponent.SetMaxWidth(layout.Current.Container.Width - 12)
return &sessionDialog{
list: list,
modal: modal.New(modal.WithTitle("Switch Session"), modal.WithMaxWidth(80)),
sessions: filteredSessions,
list: listComponent,
app: app,
deleteConfirmation: -1,
modal: modal.New(
modal.WithTitle("Switch Session"),
modal.WithMaxWidth(layout.Current.Container.Width-8),
),
}
}

View File

@@ -5,7 +5,6 @@ import (
list "github.com/sst/opencode/internal/components/list"
"github.com/sst/opencode/internal/components/modal"
"github.com/sst/opencode/internal/layout"
"github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme"
"github.com/sst/opencode/internal/util"
)
@@ -20,35 +19,14 @@ type ThemeDialog interface {
layout.Modal
}
type themeItem struct {
name string
}
func (t themeItem) Render(selected bool, width int) string {
th := theme.CurrentTheme()
baseStyle := styles.BaseStyle().
Width(width - 2).
Background(th.BackgroundElement())
if selected {
baseStyle = baseStyle.
Background(th.Primary()).
Foreground(th.BackgroundElement()).
Bold(true)
} else {
baseStyle = baseStyle.
Foreground(th.Text())
}
return baseStyle.Padding(0, 1).Render(t.name)
}
type themeDialog struct {
width int
height int
modal *modal.Modal
list list.List[themeItem]
modal *modal.Modal
list list.List[list.StringItem]
originalTheme string
themeApplied bool
}
func (t *themeDialog) Init() tea.Cmd {
@@ -64,26 +42,31 @@ func (t *themeDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg.String() {
case "enter":
if item, idx := t.list.GetSelectedItem(); idx >= 0 {
previousTheme := theme.CurrentThemeName()
selectedTheme := item.name
if previousTheme == selectedTheme {
return t, util.CmdHandler(modal.CloseModalMsg{})
}
selectedTheme := string(item)
if err := theme.SetTheme(selectedTheme); err != nil {
// status.Error(err.Error())
return t, nil
}
t.themeApplied = true
return t, tea.Sequence(
util.CmdHandler(modal.CloseModalMsg{}),
util.CmdHandler(ThemeSelectedMsg{ThemeName: selectedTheme}),
)
}
}
}
_, prevIdx := t.list.GetSelectedItem()
var cmd tea.Cmd
listModel, cmd := t.list.Update(msg)
t.list = listModel.(list.List[themeItem])
t.list = listModel.(list.List[list.StringItem])
if item, newIdx := t.list.GetSelectedItem(); newIdx >= 0 && newIdx != prevIdx {
theme.SetTheme(string(item))
return t, util.CmdHandler(ThemeSelectedMsg{ThemeName: string(item)})
}
return t, cmd
}
@@ -92,6 +75,10 @@ func (t *themeDialog) Render(background string) string {
}
func (t *themeDialog) Close() tea.Cmd {
if !t.themeApplied {
theme.SetTheme(t.originalTheme)
return util.CmdHandler(ThemeSelectedMsg{ThemeName: t.originalTheme})
}
return nil
}
@@ -100,17 +87,15 @@ func NewThemeDialog() ThemeDialog {
themes := theme.AvailableThemes()
currentTheme := theme.CurrentThemeName()
var themeItems []themeItem
var selectedIdx int
for i, name := range themes {
themeItems = append(themeItems, themeItem{name: name})
if name == currentTheme {
selectedIdx = i
}
}
list := list.NewListComponent(
themeItems,
list := list.NewStringList(
themes,
10, // maxVisibleThemes
"No themes available",
true,
@@ -118,9 +103,14 @@ func NewThemeDialog() ThemeDialog {
// Set the initial selection to the current theme
list.SetSelectedIndex(selectedIdx)
// Set the max width for the list to match the modal width
list.SetMaxWidth(36) // 40 (modal max width) - 4 (modal padding)
return &themeDialog{
list: list,
modal: modal.New(modal.WithTitle("Select Theme"), modal.WithMaxWidth(40)),
list: list,
modal: modal.New(modal.WithTitle("Select Theme"), modal.WithMaxWidth(40)),
originalTheme: currentTheme,
themeApplied: false,
}
}

View File

@@ -5,6 +5,10 @@ import (
"github.com/charmbracelet/bubbles/v2/key"
tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/lipgloss/v2"
"github.com/muesli/reflow/truncate"
"github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme"
)
type ListItem interface {
@@ -123,6 +127,9 @@ func (c *listComponent[T]) SetSelectedIndex(idx int) {
func (c *listComponent[T]) View() string {
items := c.items
maxWidth := c.maxWidth
if maxWidth == 0 {
maxWidth = 80 // Default width if not set
}
maxVisibleItems := min(c.maxVisibleItems, len(items))
startIdx := 0
@@ -161,3 +168,36 @@ func NewListComponent[T ListItem](items []T, maxVisibleItems int, fallbackMsg st
selectedIdx: 0,
}
}
// StringItem is a simple implementation of ListItem for string values
type StringItem string
func (s StringItem) Render(selected bool, width int) string {
t := theme.CurrentTheme()
baseStyle := styles.BaseStyle()
truncatedStr := truncate.StringWithTail(string(s), uint(width-1), "...")
var itemStyle lipgloss.Style
if selected {
itemStyle = baseStyle.
Background(t.Primary()).
Foreground(t.Background()).
Width(width).
PaddingLeft(1)
} else {
itemStyle = baseStyle.
PaddingLeft(1)
}
return itemStyle.Render(truncatedStr)
}
// NewStringList creates a new list component with string items
func NewStringList(items []string, maxVisibleItems int, fallbackMsg string, useAlphaNumericKeys bool) List[StringItem] {
stringItems := make([]StringItem, len(items))
for i, item := range items {
stringItems[i] = StringItem(item)
}
return NewListComponent(stringItems, maxVisibleItems, fallbackMsg, useAlphaNumericKeys)
}

View File

@@ -103,18 +103,18 @@ func (m *Modal) Render(contentView string, background string) string {
Bold(true).
Padding(0, 1)
escStyle := baseStyle.Foreground(t.TextMuted()).Bold(false)
escStyle := baseStyle.Foreground(t.TextMuted())
escText := escStyle.Render("esc")
// Calculate position for esc text
titleWidth := lipgloss.Width(m.title)
escWidth := lipgloss.Width(escText)
spacesNeeded := max(0, innerWidth-titleWidth-escWidth-3)
spacesNeeded := max(0, innerWidth-titleWidth-escWidth-2)
spacer := strings.Repeat(" ", spacesNeeded)
titleLine := m.title + spacer + escText
titleLine = titleStyle.Render(titleLine)
finalContent = strings.Join([]string{titleLine, contentView}, "\n") + "\n"
finalContent = strings.Join([]string{titleLine, "", contentView}, "\n")
} else {
finalContent = contentView
}

View File

@@ -71,7 +71,7 @@ func formatTokensAndCost(tokens float32, contextWindow float32, cost float32) st
formattedCost := fmt.Sprintf("$%.2f", cost)
percentage := (float64(tokens) / float64(contextWindow)) * 100
return fmt.Sprintf("Tokens: %s (%d%%), Cost: %s", formattedTokens, int(percentage), formattedCost)
return fmt.Sprintf("Context: %s (%d%%), Cost: %s", formattedTokens, int(percentage), formattedCost)
}
func (m statusComponent) View() string {

View File

@@ -5,8 +5,8 @@ package image
import (
"bytes"
"fmt"
"image"
"github.com/atotto/clipboard"
"image"
)
func GetImageFromClipboard() ([]byte, string, error) {
@@ -28,8 +28,6 @@ func GetImageFromClipboard() ([]byte, string, error) {
}
func binaryToImage(data []byte) ([]byte, error) {
reader := bytes.NewReader(data)
img, _, err := image.Decode(reader)
@@ -40,7 +38,6 @@ func binaryToImage(data []byte) ([]byte, error) {
return ImageToBytes(img)
}
func min(a, b int) int {
if a < b {
return a

View File

@@ -109,18 +109,26 @@ func PlaceOverlay(
// Get the foreground line
fgLine := fgLines[i-y]
fgLineWidth := ansi.PrintableRuneWidth(fgLine)
// Extract the styles at the border positions
leftStyle := getStyleAtPosition(bgLine, pos)
rightStyle := getStyleAtPosition(bgLine, pos + 1 + fgLineWidth)
// We need to get the style just before the border position to preserve background
leftStyle := ansiStyle{}
if pos > 0 {
leftStyle = getStyleAtPosition(bgLine, pos-1)
} else {
leftStyle = getStyleAtPosition(bgLine, pos)
}
rightStyle := getStyleAtPosition(bgLine, pos+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
if leftSeq != "" {
b.WriteString("\x1b[0m") // Reset all styles only if we applied any
}
pos++
// Content
@@ -133,7 +141,9 @@ func PlaceOverlay(
b.WriteString(rightSeq)
}
b.WriteString("┃")
b.WriteString("\x1b[0m") // Reset all styles
if rightSeq != "" {
b.WriteString("\x1b[0m") // Reset all styles only if we applied any
}
pos++
} else {
// No border, just render the content
@@ -172,23 +182,25 @@ type ansiStyle struct {
// parseANSISequence parses an ANSI escape sequence into its components
func parseANSISequence(seq string) ansiStyle {
style := ansiStyle{}
// Extract the parameters from the sequence (e.g., \x1b[38;5;123;48;5;456m -> "38;5;123;48;5;456")
if !strings.HasPrefix(seq, "\x1b[") || !strings.HasSuffix(seq, "m") {
return style
}
params := seq[2 : len(seq)-1]
if params == "" {
return style
}
parts := strings.Split(params, ";")
i := 0
for i < len(parts) {
switch parts[i] {
case "0": // Reset
style = ansiStyle{}
// Mark this as a reset by adding it to attrs
style.attrs = append(style.attrs, "0")
// Don't clear the style here, let the caller handle it
case "1", "2", "3", "4", "5", "6", "7", "8", "9": // Various attributes
style.attrs = append(style.attrs, parts[i])
case "38": // Foreground color
@@ -222,7 +234,7 @@ func parseANSISequence(seq string) ansiStyle {
}
i++
}
return style
}
@@ -231,32 +243,30 @@ 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()
// Use the adaptive color which automatically selects based on terminal background
// The RGBA method already handles light/dark selection
r, g, b, _ := fgColor.RGBA()
// RGBA returns 16-bit values, we need 8-bit
parts = append(parts, fmt.Sprintf("38;2;%d;%d;%d", r>>8, g>>8, b>>8))
}
if len(parts) == 0 {
return ""
}
return fmt.Sprintf("\x1b[%sm", strings.Join(parts, ";"))
}
@@ -264,10 +274,10 @@ func combineStyles(bgStyle ansiStyle, fgColor *compat.AdaptiveColor) string {
func getStyleAtPosition(s string, targetPos int) ansiStyle {
// ANSI escape sequence regex
ansiRegex := regexp.MustCompile(`\x1b\[[0-9;]*m`)
visualPos := 0
currentStyle := ansiStyle{}
i := 0
for i < len(s) && visualPos <= targetPos {
// Check if we're at an ANSI escape sequence
@@ -275,18 +285,24 @@ func getStyleAtPosition(s string, targetPos int) ansiStyle {
// Found an ANSI sequence at current position
seq := s[i : i+match[1]]
parsedStyle := parseANSISequence(seq)
// Update current style (merge with existing)
if parsedStyle.fgColor != "" {
currentStyle.fgColor = parsedStyle.fgColor
// Check if this is a reset sequence
if len(parsedStyle.attrs) > 0 && parsedStyle.attrs[0] == "0" {
// Reset all styles
currentStyle = ansiStyle{}
} else {
// 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
}
}
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
@@ -298,7 +314,7 @@ func getStyleAtPosition(s string, targetPos int) ansiStyle {
visualPos++
}
}
return currentStyle
}

View File

@@ -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.BackgroundPanelColor = 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())
}

View File

@@ -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.BackgroundPanelColor = 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())
}

View File

@@ -0,0 +1,395 @@
package theme
import (
"embed"
"encoding/json"
"fmt"
"image/color"
"os"
"path"
"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(path.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
}

View File

@@ -0,0 +1,135 @@
package theme
import (
"os"
"path/filepath"
"slices"
"testing"
)
func TestLoadThemesFromJSON(t *testing.T) {
// Test loading themes
err := LoadThemesFromJSON()
if err != nil {
t.Fatalf("Failed to load themes: %v", err)
}
// Check that themes were loaded
themes := AvailableThemes()
if len(themes) == 0 {
t.Fatal("No themes were loaded")
}
// Check for expected themes
expectedThemes := []string{"tokyonight", "opencode", "everforest", "ayu", "example"}
for _, expected := range expectedThemes {
found := slices.Contains(themes, expected)
if !found {
t.Errorf("Expected theme %s not found", expected)
}
}
// Test getting a specific theme
tokyonight := GetTheme("tokyonight")
if tokyonight == nil {
t.Fatal("Failed to get tokyonight theme")
}
// Test theme colors
primary := tokyonight.Primary()
if primary.Dark == nil || primary.Light == nil {
t.Error("Primary color not properly set")
}
}
func TestColorReferenceResolution(t *testing.T) {
// Test the example theme which uses references
example := GetTheme("example")
if example == nil {
t.Fatal("Failed to get example theme")
}
// Check that brandBlue reference was resolved
primary := example.Primary()
if primary.Dark == nil || primary.Light == nil {
t.Error("Primary color (brandBlue reference) not resolved")
}
// Check that nested reference (borderActive -> primary -> brandBlue) works
borderActive := example.BorderActive()
if borderActive.Dark == nil || borderActive.Light == nil {
t.Error("BorderActive color (nested reference) not resolved")
}
}
func TestLoadThemesFromDirectories(t *testing.T) {
// Create temporary directories for testing
tempDir := t.TempDir()
userConfig := filepath.Join(tempDir, "config")
projectRoot := filepath.Join(tempDir, "project")
cwd := filepath.Join(tempDir, "cwd")
// Create theme directories
os.MkdirAll(filepath.Join(userConfig, "opencode", "themes"), 0755)
os.MkdirAll(filepath.Join(projectRoot, ".opencode", "themes"), 0755)
os.MkdirAll(filepath.Join(cwd, ".opencode", "themes"), 0755)
// Create test themes with same name to test override behavior
testTheme1 := `{
"theme": {
"primary": "#111111",
"secondary": "#222222",
"accent": "#333333",
"text": "#ffffff",
"textMuted": "#cccccc",
"background": "#000000"
}
}`
testTheme2 := `{
"theme": {
"primary": "#444444",
"secondary": "#555555",
"accent": "#666666",
"text": "#ffffff",
"textMuted": "#cccccc",
"background": "#000000"
}
}`
testTheme3 := `{
"theme": {
"primary": "#777777",
"secondary": "#888888",
"accent": "#999999",
"text": "#ffffff",
"textMuted": "#cccccc",
"background": "#000000"
}
}`
// Write themes to different directories
os.WriteFile(filepath.Join(userConfig, "opencode", "themes", "override-test.json"), []byte(testTheme1), 0644)
os.WriteFile(filepath.Join(projectRoot, ".opencode", "themes", "override-test.json"), []byte(testTheme2), 0644)
os.WriteFile(filepath.Join(cwd, ".opencode", "themes", "override-test.json"), []byte(testTheme3), 0644)
// Load themes
err := LoadThemesFromDirectories(userConfig, projectRoot, cwd)
if err != nil {
t.Fatalf("Failed to load themes from directories: %v", err)
}
// Check that the theme from CWD (highest priority) won
overrideTheme := GetTheme("override-test")
if overrideTheme == nil {
t.Fatal("Failed to get override-test theme")
}
// The primary color should be from testTheme3 (#777777)
primary := overrideTheme.Primary()
// We can't directly check the color value, but we can verify it was loaded
if primary.Dark == nil || primary.Light == nil {
t.Error("Override theme not properly loaded")
}
}

View File

@@ -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.BackgroundPanelColor = 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
}

View File

@@ -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.BackgroundPanelColor = 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())
}

View File

@@ -1,10 +1,6 @@
package theme
import (
"fmt"
"regexp"
"github.com/charmbracelet/lipgloss/v2"
"github.com/charmbracelet/lipgloss/v2/compat"
)
@@ -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")
}

View File

@@ -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"
}
}

View File

@@ -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" }
}
}

View File

@@ -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"
}
}
}

View File

@@ -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" }
}
}

View File

@@ -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" }
}
}

View File

@@ -0,0 +1,77 @@
{
"$schema": "https://opencode.ai/theme.json",
"defs": {
"matrixInk0": "#0a0e0a",
"matrixInk1": "#0e130d",
"matrixInk2": "#141c12",
"matrixInk3": "#1e2a1b",
"rainGreen": "#2eff6a",
"rainGreenDim": "#1cc24b",
"rainGreenHi": "#62ff94",
"rainCyan": "#00efff",
"rainTeal": "#24f6d9",
"rainPurple": "#c770ff",
"rainOrange": "#ffa83d",
"alertRed": "#ff4b4b",
"alertYellow": "#e6ff57",
"alertBlue": "#30b3ff",
"rainGray": "#8ca391",
"lightBg": "#eef3ea",
"lightPaper": "#e4ebe1",
"lightInk1": "#dae1d7",
"lightText": "#203022",
"lightGray": "#748476"
},
"theme": {
"primary": { "dark": "rainGreen", "light": "rainGreenDim" },
"secondary": { "dark": "rainCyan", "light": "rainTeal" },
"accent": { "dark": "rainPurple", "light": "rainPurple" },
"error": { "dark": "alertRed", "light": "alertRed" },
"warning": { "dark": "alertYellow", "light": "alertYellow" },
"success": { "dark": "rainGreenHi", "light": "rainGreenDim" },
"info": { "dark": "alertBlue", "light": "alertBlue" },
"text": { "dark": "rainGreenHi", "light": "lightText" },
"textMuted": { "dark": "rainGray", "light": "lightGray" },
"background": { "dark": "matrixInk0", "light": "lightBg" },
"backgroundPanel": { "dark": "matrixInk1", "light": "lightPaper" },
"backgroundElement": { "dark": "matrixInk2", "light": "lightInk1" },
"border": { "dark": "matrixInk3", "light": "lightGray" },
"borderActive": { "dark": "rainGreen", "light": "rainGreenDim" },
"borderSubtle": { "dark": "matrixInk2", "light": "lightInk1" },
"diffAdded": { "dark": "rainGreenDim", "light": "rainGreenDim" },
"diffRemoved": { "dark": "alertRed", "light": "alertRed" },
"diffContext": { "dark": "rainGray", "light": "lightGray" },
"diffHunkHeader": { "dark": "alertBlue", "light": "alertBlue" },
"diffHighlightAdded": { "dark": "#77ffaf", "light": "#5dac7e" },
"diffHighlightRemoved": { "dark": "#ff7171", "light": "#d53a3a" },
"diffAddedBg": { "dark": "#132616", "light": "#e0efde" },
"diffRemovedBg": { "dark": "#261212", "light": "#f9e5e5" },
"diffContextBg": { "dark": "matrixInk1", "light": "lightPaper" },
"diffLineNumber": { "dark": "matrixInk3", "light": "lightGray" },
"diffAddedLineNumberBg": { "dark": "#0f1b11", "light": "#d6e7d2" },
"diffRemovedLineNumberBg": { "dark": "#1b1414", "light": "#f2d2d2" },
"markdownText": { "dark": "rainGreenHi", "light": "lightText" },
"markdownHeading": { "dark": "rainCyan", "light": "rainTeal" },
"markdownLink": { "dark": "alertBlue", "light": "alertBlue" },
"markdownLinkText": { "dark": "rainTeal", "light": "rainTeal" },
"markdownCode": { "dark": "rainGreenDim", "light": "rainGreenDim" },
"markdownBlockQuote": { "dark": "rainGray", "light": "lightGray" },
"markdownEmph": { "dark": "rainOrange", "light": "rainOrange" },
"markdownStrong": { "dark": "alertYellow", "light": "alertYellow" },
"markdownHorizontalRule": { "dark": "rainGray", "light": "lightGray" },
"markdownListItem": { "dark": "alertBlue", "light": "alertBlue" },
"markdownListEnumeration": { "dark": "rainTeal", "light": "rainTeal" },
"markdownImage": { "dark": "alertBlue", "light": "alertBlue" },
"markdownImageText": { "dark": "rainTeal", "light": "rainTeal" },
"markdownCodeBlock": { "dark": "rainGreenHi", "light": "lightText" },
"syntaxComment": { "dark": "rainGray", "light": "lightGray" },
"syntaxKeyword": { "dark": "rainPurple", "light": "rainPurple" },
"syntaxFunction": { "dark": "alertBlue", "light": "alertBlue" },
"syntaxVariable": { "dark": "rainGreenHi", "light": "lightText" },
"syntaxString": { "dark": "rainGreenDim", "light": "rainGreenDim" },
"syntaxNumber": { "dark": "rainOrange", "light": "rainOrange" },
"syntaxType": { "dark": "alertYellow", "light": "alertYellow" },
"syntaxOperator": { "dark": "rainTeal", "light": "rainTeal" },
"syntaxPunctuation": { "dark": "rainGreenHi", "light": "lightText" }
}
}

View File

@@ -0,0 +1,223 @@
{
"$schema": "https://opencode.ai/theme.json",
"defs": {
"nord0": "#2E3440",
"nord1": "#3B4252",
"nord2": "#434C5E",
"nord3": "#4C566A",
"nord4": "#D8DEE9",
"nord5": "#E5E9F0",
"nord6": "#ECEFF4",
"nord7": "#8FBCBB",
"nord8": "#88C0D0",
"nord9": "#81A1C1",
"nord10": "#5E81AC",
"nord11": "#BF616A",
"nord12": "#D08770",
"nord13": "#EBCB8B",
"nord14": "#A3BE8C",
"nord15": "#B48EAD"
},
"theme": {
"primary": {
"dark": "nord8",
"light": "nord10"
},
"secondary": {
"dark": "nord9",
"light": "nord9"
},
"accent": {
"dark": "nord7",
"light": "nord7"
},
"error": {
"dark": "nord11",
"light": "nord11"
},
"warning": {
"dark": "nord12",
"light": "nord12"
},
"success": {
"dark": "nord14",
"light": "nord14"
},
"info": {
"dark": "nord8",
"light": "nord10"
},
"text": {
"dark": "nord6",
"light": "nord0"
},
"textMuted": {
"dark": "#8B95A7",
"light": "nord1"
},
"background": {
"dark": "nord0",
"light": "nord6"
},
"backgroundPanel": {
"dark": "nord1",
"light": "nord5"
},
"backgroundElement": {
"dark": "nord2",
"light": "nord4"
},
"border": {
"dark": "nord2",
"light": "nord3"
},
"borderActive": {
"dark": "nord3",
"light": "nord2"
},
"borderSubtle": {
"dark": "nord2",
"light": "nord3"
},
"diffAdded": {
"dark": "nord14",
"light": "nord14"
},
"diffRemoved": {
"dark": "nord11",
"light": "nord11"
},
"diffContext": {
"dark": "#8B95A7",
"light": "nord3"
},
"diffHunkHeader": {
"dark": "#8B95A7",
"light": "nord3"
},
"diffHighlightAdded": {
"dark": "nord14",
"light": "nord14"
},
"diffHighlightRemoved": {
"dark": "nord11",
"light": "nord11"
},
"diffAddedBg": {
"dark": "#3B4252",
"light": "#E5E9F0"
},
"diffRemovedBg": {
"dark": "#3B4252",
"light": "#E5E9F0"
},
"diffContextBg": {
"dark": "nord1",
"light": "nord5"
},
"diffLineNumber": {
"dark": "nord2",
"light": "nord4"
},
"diffAddedLineNumberBg": {
"dark": "#3B4252",
"light": "#E5E9F0"
},
"diffRemovedLineNumberBg": {
"dark": "#3B4252",
"light": "#E5E9F0"
},
"markdownText": {
"dark": "nord4",
"light": "nord0"
},
"markdownHeading": {
"dark": "nord8",
"light": "nord10"
},
"markdownLink": {
"dark": "nord9",
"light": "nord9"
},
"markdownLinkText": {
"dark": "nord7",
"light": "nord7"
},
"markdownCode": {
"dark": "nord14",
"light": "nord14"
},
"markdownBlockQuote": {
"dark": "#8B95A7",
"light": "nord3"
},
"markdownEmph": {
"dark": "nord12",
"light": "nord12"
},
"markdownStrong": {
"dark": "nord13",
"light": "nord13"
},
"markdownHorizontalRule": {
"dark": "#8B95A7",
"light": "nord3"
},
"markdownListItem": {
"dark": "nord8",
"light": "nord10"
},
"markdownListEnumeration": {
"dark": "nord7",
"light": "nord7"
},
"markdownImage": {
"dark": "nord9",
"light": "nord9"
},
"markdownImageText": {
"dark": "nord7",
"light": "nord7"
},
"markdownCodeBlock": {
"dark": "nord4",
"light": "nord0"
},
"syntaxComment": {
"dark": "#8B95A7",
"light": "nord3"
},
"syntaxKeyword": {
"dark": "nord9",
"light": "nord9"
},
"syntaxFunction": {
"dark": "nord8",
"light": "nord8"
},
"syntaxVariable": {
"dark": "nord7",
"light": "nord7"
},
"syntaxString": {
"dark": "nord14",
"light": "nord14"
},
"syntaxNumber": {
"dark": "nord15",
"light": "nord15"
},
"syntaxType": {
"dark": "nord7",
"light": "nord7"
},
"syntaxOperator": {
"dark": "nord9",
"light": "nord9"
},
"syntaxPunctuation": {
"dark": "nord4",
"light": "nord0"
}
}
}

View File

@@ -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" }
}
}

View File

@@ -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"
}
}
}

View File

@@ -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"
}
}
}

View File

@@ -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.BackgroundPanelColor = 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())
}

View File

@@ -6,6 +6,7 @@ import (
"os"
"os/exec"
"strings"
"time"
"github.com/charmbracelet/bubbles/v2/key"
tea "github.com/charmbracelet/bubbletea/v2"
@@ -25,6 +26,19 @@ import (
"github.com/sst/opencode/pkg/client"
)
// InterruptDebounceTimeoutMsg is sent when the interrupt key debounce timeout expires
type InterruptDebounceTimeoutMsg struct{}
// InterruptKeyState tracks the state of interrupt key presses for debouncing
type InterruptKeyState int
const (
InterruptKeyIdle InterruptKeyState = iota
InterruptKeyFirstPress
)
const interruptDebounceTimeout = 1 * time.Second
type appModel struct {
width, height int
app *app.App
@@ -40,6 +54,7 @@ type appModel struct {
leaderBinding *key.Binding
isLeaderSequence bool
toastManager *toast.ToastManager
interruptKeyState InterruptKeyState
}
func (a appModel) Init() tea.Cmd {
@@ -77,8 +92,9 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
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
@@ -116,7 +132,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
updated, cmd := a.completions.Update(
app.CompletionDialogTriggerdMsg{
app.CompletionDialogTriggeredMsg{
InitialValue: initialValue,
},
)
@@ -170,14 +186,37 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return a, nil
}
// 6. Check again for commands that don't require leader
// 6. Handle interrupt key debounce for session interrupt
interruptCommand := a.app.Commands[commands.SessionInterruptCommand]
if interruptCommand.Matches(msg, a.isLeaderSequence) && a.app.IsBusy() {
switch a.interruptKeyState {
case InterruptKeyIdle:
// First interrupt key press - start debounce timer
a.interruptKeyState = InterruptKeyFirstPress
a.editor.SetInterruptKeyInDebounce(true)
return a, tea.Tick(interruptDebounceTimeout, func(t time.Time) tea.Msg {
return InterruptDebounceTimeoutMsg{}
})
case InterruptKeyFirstPress:
// Second interrupt key press within timeout - actually interrupt
a.interruptKeyState = InterruptKeyIdle
a.editor.SetInterruptKeyInDebounce(false)
return a, util.CmdHandler(commands.ExecuteCommandMsg(interruptCommand))
}
}
// 7. Check again for commands that don't require leader (excluding interrupt when busy)
matches := a.app.Commands.Matches(msg, a.isLeaderSequence)
if len(matches) > 0 {
// Skip interrupt key if we're in debounce mode and app is busy
if interruptCommand.Matches(msg, a.isLeaderSequence) && a.app.IsBusy() && a.interruptKeyState != InterruptKeyIdle {
return a, nil
}
return a, util.CmdHandler(commands.ExecuteCommandsMsg(matches))
}
// 7. Fallback to editor. This is for other characters
// likek backspace, tab, etc.
// like backspace, tab, etc.
updatedEditor, cmd := a.editor.Update(msg)
a.editor = updatedEditor.(chat.EditorComponent)
return a, cmd
@@ -188,14 +227,19 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
updated, cmd := a.messages.Update(msg)
a.messages = updated.(chat.MessagesComponent)
cmds = append(cmds, cmd)
return a, tea.Batch(cmds...)
case tea.BackgroundColorMsg:
styles.Terminal = &styles.TerminalInfo{
BackgroundIsDark: msg.IsDark(),
}
slog.Debug("Background color", "isDark", msg.IsDark())
case modal.CloseModalMsg:
var cmd tea.Cmd
if a.modal != nil {
cmd = a.modal.Close()
}
a.modal = nil
return a, nil
return a, cmd
case commands.ExecuteCommandMsg:
updated, cmd := a.executeCommand(commands.Command(msg))
return updated, cmd
@@ -217,6 +261,12 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
"opencode updated to "+msg.Properties.Version+", restart to apply.",
toast.WithTitle("New version installed"),
)
case client.EventSessionDeleted:
if a.app.Session != nil && msg.Properties.Info.Id == a.app.Session.Id {
a.app.Session = &client.SessionInfo{}
a.app.Messages = []client.MessageInfo{}
}
return a, toast.NewSuccessToast("Session deleted successfully")
case client.EventSessionUpdated:
if msg.Properties.Info.Id == a.app.Session.Id {
a.app.Session = &msg.Properties.Info
@@ -224,12 +274,33 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case client.EventMessageUpdated:
if msg.Properties.Info.Metadata.SessionID == a.app.Session.Id {
exists := false
for i, m := range a.app.Messages {
if m.Id == msg.Properties.Info.Id {
a.app.Messages[i] = msg.Properties.Info
exists = true
optimisticReplaced := false
// First check if this is replacing an optimistic message
if msg.Properties.Info.Role == client.User {
// Look for optimistic messages to replace
for i, m := range a.app.Messages {
if strings.HasPrefix(m.Id, "optimistic-") && m.Role == client.User {
// Replace the optimistic message with the real one
a.app.Messages[i] = msg.Properties.Info
exists = true
optimisticReplaced = true
break
}
}
}
// If not replacing optimistic, check for existing message with same ID
if !optimisticReplaced {
for i, m := range a.app.Messages {
if m.Id == msg.Properties.Info.Id {
a.app.Messages[i] = msg.Properties.Info
exists = true
break
}
}
}
if !exists {
a.app.Messages = append(a.app.Messages, msg.Properties.Info)
}
@@ -278,6 +349,10 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
tm, cmd := a.toastManager.Update(msg)
a.toastManager = tm
cmds = append(cmds, cmd)
case InterruptDebounceTimeoutMsg:
// Reset interrupt key state after timeout
a.interruptKeyState = InterruptKeyIdle
a.editor.SetInterruptKeyInDebounce(false)
}
// update status bar
@@ -341,7 +416,7 @@ func (a appModel) View() string {
layoutView,
a.status.View(),
}
appView := lipgloss.JoinVertical(lipgloss.Top, components...)
appView := strings.Join(components, "\n")
if a.modal != nil {
appView = a.modal.Render(appView)
@@ -358,7 +433,7 @@ func (a appModel) executeCommand(command commands.Command) (tea.Model, tea.Cmd)
}
switch command.Name {
case commands.AppHelpCommand:
helpDialog := dialog.NewHelpDialog(a.app.Commands.Sorted())
helpDialog := dialog.NewHelpDialog(a.app)
a.modal = helpDialog
case commands.EditorOpenCommand:
if a.app.IsBusy() {
@@ -570,6 +645,7 @@ func NewModel(app *app.App) tea.Model {
showCompletionDialog: false,
editorContainer: editorContainer,
toastManager: toast.NewToastManager(),
interruptKeyState: InterruptKeyIdle,
layout: layout.NewFlexLayout(
[]tea.ViewModel{messagesContainer, editorContainer},
layout.WithDirection(layout.FlexDirectionVertical),

View File

@@ -230,6 +230,42 @@
}
}
},
"/session_unshare": {
"post": {
"responses": {
"200": {
"description": "Successfully unshared session",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/session.info"
}
}
}
}
},
"operationId": "postSession_unshare",
"parameters": [],
"description": "Unshare the session",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"sessionID": {
"type": "string"
}
},
"required": [
"sessionID"
]
}
}
}
}
}
},
"/session_messages": {
"post": {
"responses": {
@@ -327,6 +363,42 @@
}
}
},
"/session_delete": {
"post": {
"responses": {
"200": {
"description": "Successfully deleted session",
"content": {
"application/json": {
"schema": {
"type": "boolean"
}
}
}
}
},
"operationId": "postSession_delete",
"parameters": [],
"description": "Delete a session and all its data",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"sessionID": {
"type": "string"
}
},
"required": [
"sessionID"
]
}
}
}
}
}
},
"/session_summarize": {
"post": {
"responses": {
@@ -543,6 +615,9 @@
{
"$ref": "#/components/schemas/Event.session.updated"
},
{
"$ref": "#/components/schemas/Event.session.deleted"
},
{
"$ref": "#/components/schemas/Event.session.error"
}
@@ -557,6 +632,7 @@
"message.updated": "#/components/schemas/Event.message.updated",
"message.part.updated": "#/components/schemas/Event.message.part.updated",
"session.updated": "#/components/schemas/Event.session.updated",
"session.deleted": "#/components/schemas/Event.session.deleted",
"session.error": "#/components/schemas/Event.session.error"
}
}
@@ -734,160 +810,7 @@
}
},
"metadata": {
"type": "object",
"properties": {
"time": {
"type": "object",
"properties": {
"created": {
"type": "number"
},
"completed": {
"type": "number"
}
},
"required": [
"created"
]
},
"error": {
"oneOf": [
{
"$ref": "#/components/schemas/ProviderAuthError"
},
{
"$ref": "#/components/schemas/UnknownError"
}
],
"discriminator": {
"propertyName": "name",
"mapping": {
"ProviderAuthError": "#/components/schemas/ProviderAuthError",
"UnknownError": "#/components/schemas/UnknownError"
}
}
},
"sessionID": {
"type": "string"
},
"tool": {
"type": "object",
"additionalProperties": {
"type": "object",
"properties": {
"title": {
"type": "string"
},
"time": {
"type": "object",
"properties": {
"start": {
"type": "number"
},
"end": {
"type": "number"
}
},
"required": [
"start",
"end"
]
}
},
"required": [
"title",
"time"
],
"additionalProperties": {}
}
},
"assistant": {
"type": "object",
"properties": {
"system": {
"type": "array",
"items": {
"type": "string"
}
},
"modelID": {
"type": "string"
},
"providerID": {
"type": "string"
},
"path": {
"type": "object",
"properties": {
"cwd": {
"type": "string"
},
"root": {
"type": "string"
}
},
"required": [
"cwd",
"root"
]
},
"cost": {
"type": "number"
},
"summary": {
"type": "boolean"
},
"tokens": {
"type": "object",
"properties": {
"input": {
"type": "number"
},
"output": {
"type": "number"
},
"reasoning": {
"type": "number"
},
"cache": {
"type": "object",
"properties": {
"read": {
"type": "number"
},
"write": {
"type": "number"
}
},
"required": [
"read",
"write"
]
}
},
"required": [
"input",
"output",
"reasoning",
"cache"
]
}
},
"required": [
"system",
"modelID",
"providerID",
"path",
"cost",
"tokens"
]
}
},
"required": [
"time",
"sessionID",
"tool"
]
"$ref": "#/components/schemas/Message.Metadata"
}
},
"required": [
@@ -1141,6 +1064,162 @@
"type"
]
},
"Message.Metadata": {
"type": "object",
"properties": {
"time": {
"type": "object",
"properties": {
"created": {
"type": "number"
},
"completed": {
"type": "number"
}
},
"required": [
"created"
]
},
"error": {
"oneOf": [
{
"$ref": "#/components/schemas/ProviderAuthError"
},
{
"$ref": "#/components/schemas/UnknownError"
}
],
"discriminator": {
"propertyName": "name",
"mapping": {
"ProviderAuthError": "#/components/schemas/ProviderAuthError",
"UnknownError": "#/components/schemas/UnknownError"
}
}
},
"sessionID": {
"type": "string"
},
"tool": {
"type": "object",
"additionalProperties": {
"type": "object",
"properties": {
"title": {
"type": "string"
},
"time": {
"type": "object",
"properties": {
"start": {
"type": "number"
},
"end": {
"type": "number"
}
},
"required": [
"start",
"end"
]
}
},
"required": [
"title",
"time"
],
"additionalProperties": {}
}
},
"assistant": {
"type": "object",
"properties": {
"system": {
"type": "array",
"items": {
"type": "string"
}
},
"modelID": {
"type": "string"
},
"providerID": {
"type": "string"
},
"path": {
"type": "object",
"properties": {
"cwd": {
"type": "string"
},
"root": {
"type": "string"
}
},
"required": [
"cwd",
"root"
]
},
"cost": {
"type": "number"
},
"summary": {
"type": "boolean"
},
"tokens": {
"type": "object",
"properties": {
"input": {
"type": "number"
},
"output": {
"type": "number"
},
"reasoning": {
"type": "number"
},
"cache": {
"type": "object",
"properties": {
"read": {
"type": "number"
},
"write": {
"type": "number"
}
},
"required": [
"read",
"write"
]
}
},
"required": [
"input",
"output",
"reasoning",
"cache"
]
}
},
"required": [
"system",
"modelID",
"providerID",
"path",
"cost",
"tokens"
]
}
},
"required": [
"time",
"sessionID",
"tool"
]
},
"ProviderAuthError": {
"type": "object",
"properties": {
@@ -1300,6 +1379,30 @@
"time"
]
},
"Event.session.deleted": {
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "session.deleted"
},
"properties": {
"type": "object",
"properties": {
"info": {
"$ref": "#/components/schemas/session.info"
}
},
"required": [
"info"
]
}
},
"required": [
"type",
"properties"
]
},
"Event.session.error": {
"type": "object",
"properties": {
@@ -1461,6 +1564,9 @@
"temperature": {
"type": "boolean"
},
"tool_call": {
"type": "boolean"
},
"cost": {
"type": "object",
"properties": {
@@ -1499,6 +1605,10 @@
},
"id": {
"type": "string"
},
"options": {
"type": "object",
"additionalProperties": {}
}
}
}
@@ -1535,7 +1645,8 @@
},
"description": "MCP (Model Context Protocol) server configurations"
}
}
},
"additionalProperties": false
},
"Config.Keybinds": {
"type": "object",
@@ -1648,7 +1759,8 @@
"type": "string",
"description": "Exit the application"
}
}
},
"additionalProperties": false
},
"Provider.Info": {
"type": "object",
@@ -1700,6 +1812,9 @@
"temperature": {
"type": "boolean"
},
"tool_call": {
"type": "boolean"
},
"cost": {
"type": "object",
"properties": {
@@ -1738,6 +1853,10 @@
},
"id": {
"type": "string"
},
"options": {
"type": "object",
"additionalProperties": {}
}
},
"required": [
@@ -1745,9 +1864,11 @@
"attachment",
"reasoning",
"temperature",
"tool_call",
"cost",
"limit",
"id"
"id",
"options"
]
},
"Config.McpLocal": {
@@ -1776,7 +1897,8 @@
"required": [
"type",
"command"
]
],
"additionalProperties": false
},
"Config.McpRemote": {
"type": "object",
@@ -1794,7 +1916,8 @@
"required": [
"type",
"url"
]
],
"additionalProperties": false
},
"Error": {
"type": "object",

View File

@@ -78,9 +78,11 @@ type ConfigInfo struct {
Context float32 `json:"context"`
Output float32 `json:"output"`
} `json:"limit,omitempty"`
Name *string `json:"name,omitempty"`
Reasoning *bool `json:"reasoning,omitempty"`
Temperature *bool `json:"temperature,omitempty"`
Name *string `json:"name,omitempty"`
Options *map[string]interface{} `json:"options,omitempty"`
Reasoning *bool `json:"reasoning,omitempty"`
Temperature *bool `json:"temperature,omitempty"`
ToolCall *bool `json:"tool_call,omitempty"`
} `json:"models"`
Name *string `json:"name,omitempty"`
Npm *string `json:"npm,omitempty"`
@@ -252,6 +254,14 @@ type EventPermissionUpdated struct {
Type string `json:"type"`
}
// EventSessionDeleted defines model for Event.session.deleted.
type EventSessionDeleted struct {
Properties struct {
Info SessionInfo `json:"info"`
} `json:"properties"`
Type string `json:"type"`
}
// EventSessionError defines model for Event.session.error.
type EventSessionError struct {
Properties struct {
@@ -290,47 +300,53 @@ type InstallationInfo struct {
// MessageInfo defines model for Message.Info.
type MessageInfo struct {
Id string `json:"id"`
Metadata struct {
Assistant *struct {
Cost float32 `json:"cost"`
ModelID string `json:"modelID"`
Path struct {
Cwd string `json:"cwd"`
Root string `json:"root"`
} `json:"path"`
ProviderID string `json:"providerID"`
Summary *bool `json:"summary,omitempty"`
System []string `json:"system"`
Tokens struct {
Cache struct {
Read float32 `json:"read"`
Write float32 `json:"write"`
} `json:"cache"`
Input float32 `json:"input"`
Output float32 `json:"output"`
Reasoning float32 `json:"reasoning"`
} `json:"tokens"`
} `json:"assistant,omitempty"`
Error *MessageInfo_Metadata_Error `json:"error,omitempty"`
SessionID string `json:"sessionID"`
Time struct {
Completed *float32 `json:"completed,omitempty"`
Created float32 `json:"created"`
} `json:"time"`
Tool map[string]MessageInfo_Metadata_Tool_AdditionalProperties `json:"tool"`
} `json:"metadata"`
Parts []MessagePart `json:"parts"`
Role MessageInfoRole `json:"role"`
Id string `json:"id"`
Metadata MessageMetadata `json:"metadata"`
Parts []MessagePart `json:"parts"`
Role MessageInfoRole `json:"role"`
}
// MessageInfo_Metadata_Error defines model for MessageInfo.Metadata.Error.
type MessageInfo_Metadata_Error struct {
// MessageInfoRole defines model for MessageInfo.Role.
type MessageInfoRole string
// MessageMetadata defines model for Message.Metadata.
type MessageMetadata struct {
Assistant *struct {
Cost float32 `json:"cost"`
ModelID string `json:"modelID"`
Path struct {
Cwd string `json:"cwd"`
Root string `json:"root"`
} `json:"path"`
ProviderID string `json:"providerID"`
Summary *bool `json:"summary,omitempty"`
System []string `json:"system"`
Tokens struct {
Cache struct {
Read float32 `json:"read"`
Write float32 `json:"write"`
} `json:"cache"`
Input float32 `json:"input"`
Output float32 `json:"output"`
Reasoning float32 `json:"reasoning"`
} `json:"tokens"`
} `json:"assistant,omitempty"`
Error *MessageMetadata_Error `json:"error,omitempty"`
SessionID string `json:"sessionID"`
Time struct {
Completed *float32 `json:"completed,omitempty"`
Created float32 `json:"created"`
} `json:"time"`
Tool map[string]MessageMetadata_Tool_AdditionalProperties `json:"tool"`
}
// MessageMetadata_Error defines model for MessageMetadata.Error.
type MessageMetadata_Error struct {
union json.RawMessage
}
// MessageInfo_Metadata_Tool_AdditionalProperties defines model for MessageInfo.Metadata.Tool.AdditionalProperties.
type MessageInfo_Metadata_Tool_AdditionalProperties struct {
// MessageMetadata_Tool_AdditionalProperties defines model for Message.Metadata.tool.AdditionalProperties.
type MessageMetadata_Tool_AdditionalProperties struct {
Time struct {
End float32 `json:"end"`
Start float32 `json:"start"`
@@ -339,9 +355,6 @@ type MessageInfo_Metadata_Tool_AdditionalProperties struct {
AdditionalProperties map[string]interface{} `json:"-"`
}
// MessageInfoRole defines model for MessageInfo.Role.
type MessageInfoRole string
// MessagePart defines model for Message.Part.
type MessagePart struct {
union json.RawMessage
@@ -435,9 +448,11 @@ type ModelInfo struct {
Context float32 `json:"context"`
Output float32 `json:"output"`
} `json:"limit"`
Name string `json:"name"`
Reasoning bool `json:"reasoning"`
Temperature bool `json:"temperature"`
Name string `json:"name"`
Options map[string]interface{} `json:"options"`
Reasoning bool `json:"reasoning"`
Temperature bool `json:"temperature"`
ToolCall bool `json:"tool_call"`
}
// ProviderInfo defines model for Provider.Info.
@@ -511,6 +526,11 @@ type PostSessionChatJSONBody struct {
SessionID string `json:"sessionID"`
}
// PostSessionDeleteJSONBody defines parameters for PostSessionDelete.
type PostSessionDeleteJSONBody struct {
SessionID string `json:"sessionID"`
}
// PostSessionInitializeJSONBody defines parameters for PostSessionInitialize.
type PostSessionInitializeJSONBody struct {
ModelID string `json:"modelID"`
@@ -535,6 +555,11 @@ type PostSessionSummarizeJSONBody struct {
SessionID string `json:"sessionID"`
}
// PostSessionUnshareJSONBody defines parameters for PostSessionUnshare.
type PostSessionUnshareJSONBody struct {
SessionID string `json:"sessionID"`
}
// PostFileSearchJSONRequestBody defines body for PostFileSearch for application/json ContentType.
type PostFileSearchJSONRequestBody PostFileSearchJSONBody
@@ -544,6 +569,9 @@ type PostSessionAbortJSONRequestBody PostSessionAbortJSONBody
// PostSessionChatJSONRequestBody defines body for PostSessionChat for application/json ContentType.
type PostSessionChatJSONRequestBody PostSessionChatJSONBody
// PostSessionDeleteJSONRequestBody defines body for PostSessionDelete for application/json ContentType.
type PostSessionDeleteJSONRequestBody PostSessionDeleteJSONBody
// PostSessionInitializeJSONRequestBody defines body for PostSessionInitialize for application/json ContentType.
type PostSessionInitializeJSONRequestBody PostSessionInitializeJSONBody
@@ -556,25 +584,28 @@ type PostSessionShareJSONRequestBody PostSessionShareJSONBody
// PostSessionSummarizeJSONRequestBody defines body for PostSessionSummarize for application/json ContentType.
type PostSessionSummarizeJSONRequestBody PostSessionSummarizeJSONBody
// Getter for additional properties for MessageInfo_Metadata_Tool_AdditionalProperties. Returns the specified
// PostSessionUnshareJSONRequestBody defines body for PostSessionUnshare for application/json ContentType.
type PostSessionUnshareJSONRequestBody PostSessionUnshareJSONBody
// Getter for additional properties for MessageMetadata_Tool_AdditionalProperties. Returns the specified
// element and whether it was found
func (a MessageInfo_Metadata_Tool_AdditionalProperties) Get(fieldName string) (value interface{}, found bool) {
func (a MessageMetadata_Tool_AdditionalProperties) Get(fieldName string) (value interface{}, found bool) {
if a.AdditionalProperties != nil {
value, found = a.AdditionalProperties[fieldName]
}
return
}
// Setter for additional properties for MessageInfo_Metadata_Tool_AdditionalProperties
func (a *MessageInfo_Metadata_Tool_AdditionalProperties) Set(fieldName string, value interface{}) {
// Setter for additional properties for MessageMetadata_Tool_AdditionalProperties
func (a *MessageMetadata_Tool_AdditionalProperties) Set(fieldName string, value interface{}) {
if a.AdditionalProperties == nil {
a.AdditionalProperties = make(map[string]interface{})
}
a.AdditionalProperties[fieldName] = value
}
// Override default JSON handling for MessageInfo_Metadata_Tool_AdditionalProperties to handle AdditionalProperties
func (a *MessageInfo_Metadata_Tool_AdditionalProperties) UnmarshalJSON(b []byte) error {
// Override default JSON handling for MessageMetadata_Tool_AdditionalProperties to handle AdditionalProperties
func (a *MessageMetadata_Tool_AdditionalProperties) UnmarshalJSON(b []byte) error {
object := make(map[string]json.RawMessage)
err := json.Unmarshal(b, &object)
if err != nil {
@@ -611,8 +642,8 @@ func (a *MessageInfo_Metadata_Tool_AdditionalProperties) UnmarshalJSON(b []byte)
return nil
}
// Override default JSON handling for MessageInfo_Metadata_Tool_AdditionalProperties to handle AdditionalProperties
func (a MessageInfo_Metadata_Tool_AdditionalProperties) MarshalJSON() ([]byte, error) {
// Override default JSON handling for MessageMetadata_Tool_AdditionalProperties to handle AdditionalProperties
func (a MessageMetadata_Tool_AdditionalProperties) MarshalJSON() ([]byte, error) {
var err error
object := make(map[string]json.RawMessage)
@@ -920,6 +951,34 @@ func (t *Event) MergeEventSessionUpdated(v EventSessionUpdated) error {
return err
}
// AsEventSessionDeleted returns the union data inside the Event as a EventSessionDeleted
func (t Event) AsEventSessionDeleted() (EventSessionDeleted, error) {
var body EventSessionDeleted
err := json.Unmarshal(t.union, &body)
return body, err
}
// FromEventSessionDeleted overwrites any union data inside the Event as the provided EventSessionDeleted
func (t *Event) FromEventSessionDeleted(v EventSessionDeleted) error {
v.Type = "session.deleted"
b, err := json.Marshal(v)
t.union = b
return err
}
// MergeEventSessionDeleted performs a merge with any union data inside the Event, using the provided EventSessionDeleted
func (t *Event) MergeEventSessionDeleted(v EventSessionDeleted) error {
v.Type = "session.deleted"
b, err := json.Marshal(v)
if err != nil {
return err
}
merged, err := runtime.JSONMerge(t.union, b)
t.union = merged
return err
}
// AsEventSessionError returns the union data inside the Event as a EventSessionError
func (t Event) AsEventSessionError() (EventSessionError, error) {
var body EventSessionError
@@ -972,6 +1031,8 @@ func (t Event) ValueByDiscriminator() (interface{}, error) {
return t.AsEventMessageUpdated()
case "permission.updated":
return t.AsEventPermissionUpdated()
case "session.deleted":
return t.AsEventSessionDeleted()
case "session.error":
return t.AsEventSessionError()
case "session.updated":
@@ -1082,23 +1143,23 @@ func (t *EventSessionError_Properties_Error) UnmarshalJSON(b []byte) error {
return err
}
// AsProviderAuthError returns the union data inside the MessageInfo_Metadata_Error as a ProviderAuthError
func (t MessageInfo_Metadata_Error) AsProviderAuthError() (ProviderAuthError, error) {
// AsProviderAuthError returns the union data inside the MessageMetadata_Error as a ProviderAuthError
func (t MessageMetadata_Error) AsProviderAuthError() (ProviderAuthError, error) {
var body ProviderAuthError
err := json.Unmarshal(t.union, &body)
return body, err
}
// FromProviderAuthError overwrites any union data inside the MessageInfo_Metadata_Error as the provided ProviderAuthError
func (t *MessageInfo_Metadata_Error) FromProviderAuthError(v ProviderAuthError) error {
// FromProviderAuthError overwrites any union data inside the MessageMetadata_Error as the provided ProviderAuthError
func (t *MessageMetadata_Error) FromProviderAuthError(v ProviderAuthError) error {
v.Name = "ProviderAuthError"
b, err := json.Marshal(v)
t.union = b
return err
}
// MergeProviderAuthError performs a merge with any union data inside the MessageInfo_Metadata_Error, using the provided ProviderAuthError
func (t *MessageInfo_Metadata_Error) MergeProviderAuthError(v ProviderAuthError) error {
// MergeProviderAuthError performs a merge with any union data inside the MessageMetadata_Error, using the provided ProviderAuthError
func (t *MessageMetadata_Error) MergeProviderAuthError(v ProviderAuthError) error {
v.Name = "ProviderAuthError"
b, err := json.Marshal(v)
if err != nil {
@@ -1110,23 +1171,23 @@ func (t *MessageInfo_Metadata_Error) MergeProviderAuthError(v ProviderAuthError)
return err
}
// AsUnknownError returns the union data inside the MessageInfo_Metadata_Error as a UnknownError
func (t MessageInfo_Metadata_Error) AsUnknownError() (UnknownError, error) {
// AsUnknownError returns the union data inside the MessageMetadata_Error as a UnknownError
func (t MessageMetadata_Error) AsUnknownError() (UnknownError, error) {
var body UnknownError
err := json.Unmarshal(t.union, &body)
return body, err
}
// FromUnknownError overwrites any union data inside the MessageInfo_Metadata_Error as the provided UnknownError
func (t *MessageInfo_Metadata_Error) FromUnknownError(v UnknownError) error {
// FromUnknownError overwrites any union data inside the MessageMetadata_Error as the provided UnknownError
func (t *MessageMetadata_Error) FromUnknownError(v UnknownError) error {
v.Name = "UnknownError"
b, err := json.Marshal(v)
t.union = b
return err
}
// MergeUnknownError performs a merge with any union data inside the MessageInfo_Metadata_Error, using the provided UnknownError
func (t *MessageInfo_Metadata_Error) MergeUnknownError(v UnknownError) error {
// MergeUnknownError performs a merge with any union data inside the MessageMetadata_Error, using the provided UnknownError
func (t *MessageMetadata_Error) MergeUnknownError(v UnknownError) error {
v.Name = "UnknownError"
b, err := json.Marshal(v)
if err != nil {
@@ -1138,7 +1199,7 @@ func (t *MessageInfo_Metadata_Error) MergeUnknownError(v UnknownError) error {
return err
}
func (t MessageInfo_Metadata_Error) Discriminator() (string, error) {
func (t MessageMetadata_Error) Discriminator() (string, error) {
var discriminator struct {
Discriminator string `json:"name"`
}
@@ -1146,7 +1207,7 @@ func (t MessageInfo_Metadata_Error) Discriminator() (string, error) {
return discriminator.Discriminator, err
}
func (t MessageInfo_Metadata_Error) ValueByDiscriminator() (interface{}, error) {
func (t MessageMetadata_Error) ValueByDiscriminator() (interface{}, error) {
discriminator, err := t.Discriminator()
if err != nil {
return nil, err
@@ -1161,12 +1222,12 @@ func (t MessageInfo_Metadata_Error) ValueByDiscriminator() (interface{}, error)
}
}
func (t MessageInfo_Metadata_Error) MarshalJSON() ([]byte, error) {
func (t MessageMetadata_Error) MarshalJSON() ([]byte, error) {
b, err := t.union.MarshalJSON()
return b, err
}
func (t *MessageInfo_Metadata_Error) UnmarshalJSON(b []byte) error {
func (t *MessageMetadata_Error) UnmarshalJSON(b []byte) error {
err := t.union.UnmarshalJSON(b)
return err
}
@@ -1611,6 +1672,11 @@ type ClientInterface interface {
// PostSessionCreate request
PostSessionCreate(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error)
// PostSessionDeleteWithBody request with any body
PostSessionDeleteWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error)
PostSessionDelete(ctx context.Context, body PostSessionDeleteJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error)
// PostSessionInitializeWithBody request with any body
PostSessionInitializeWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error)
@@ -1633,6 +1699,11 @@ type ClientInterface interface {
PostSessionSummarizeWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error)
PostSessionSummarize(ctx context.Context, body PostSessionSummarizeJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error)
// PostSessionUnshareWithBody request with any body
PostSessionUnshareWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error)
PostSessionUnshare(ctx context.Context, body PostSessionUnshareJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error)
}
func (c *Client) PostAppInfo(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) {
@@ -1803,6 +1874,30 @@ func (c *Client) PostSessionCreate(ctx context.Context, reqEditors ...RequestEdi
return c.Client.Do(req)
}
func (c *Client) PostSessionDeleteWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) {
req, err := NewPostSessionDeleteRequestWithBody(c.Server, contentType, body)
if err != nil {
return nil, err
}
req = req.WithContext(ctx)
if err := c.applyEditors(ctx, req, reqEditors); err != nil {
return nil, err
}
return c.Client.Do(req)
}
func (c *Client) PostSessionDelete(ctx context.Context, body PostSessionDeleteJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) {
req, err := NewPostSessionDeleteRequest(c.Server, body)
if err != nil {
return nil, err
}
req = req.WithContext(ctx)
if err := c.applyEditors(ctx, req, reqEditors); err != nil {
return nil, err
}
return c.Client.Do(req)
}
func (c *Client) PostSessionInitializeWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) {
req, err := NewPostSessionInitializeRequestWithBody(c.Server, contentType, body)
if err != nil {
@@ -1911,6 +2006,30 @@ func (c *Client) PostSessionSummarize(ctx context.Context, body PostSessionSumma
return c.Client.Do(req)
}
func (c *Client) PostSessionUnshareWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) {
req, err := NewPostSessionUnshareRequestWithBody(c.Server, contentType, body)
if err != nil {
return nil, err
}
req = req.WithContext(ctx)
if err := c.applyEditors(ctx, req, reqEditors); err != nil {
return nil, err
}
return c.Client.Do(req)
}
func (c *Client) PostSessionUnshare(ctx context.Context, body PostSessionUnshareJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) {
req, err := NewPostSessionUnshareRequest(c.Server, body)
if err != nil {
return nil, err
}
req = req.WithContext(ctx)
if err := c.applyEditors(ctx, req, reqEditors); err != nil {
return nil, err
}
return c.Client.Do(req)
}
// NewPostAppInfoRequest generates requests for PostAppInfo
func NewPostAppInfoRequest(server string) (*http.Request, error) {
var err error
@@ -2247,6 +2366,46 @@ func NewPostSessionCreateRequest(server string) (*http.Request, error) {
return req, nil
}
// NewPostSessionDeleteRequest calls the generic PostSessionDelete builder with application/json body
func NewPostSessionDeleteRequest(server string, body PostSessionDeleteJSONRequestBody) (*http.Request, error) {
var bodyReader io.Reader
buf, err := json.Marshal(body)
if err != nil {
return nil, err
}
bodyReader = bytes.NewReader(buf)
return NewPostSessionDeleteRequestWithBody(server, "application/json", bodyReader)
}
// NewPostSessionDeleteRequestWithBody generates requests for PostSessionDelete with any type of body
func NewPostSessionDeleteRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) {
var err error
serverURL, err := url.Parse(server)
if err != nil {
return nil, err
}
operationPath := fmt.Sprintf("/session_delete")
if operationPath[0] == '/' {
operationPath = "." + operationPath
}
queryURL, err := serverURL.Parse(operationPath)
if err != nil {
return nil, err
}
req, err := http.NewRequest("POST", queryURL.String(), body)
if err != nil {
return nil, err
}
req.Header.Add("Content-Type", contentType)
return req, nil
}
// NewPostSessionInitializeRequest calls the generic PostSessionInitialize builder with application/json body
func NewPostSessionInitializeRequest(server string, body PostSessionInitializeJSONRequestBody) (*http.Request, error) {
var bodyReader io.Reader
@@ -2434,6 +2593,46 @@ func NewPostSessionSummarizeRequestWithBody(server string, contentType string, b
return req, nil
}
// NewPostSessionUnshareRequest calls the generic PostSessionUnshare builder with application/json body
func NewPostSessionUnshareRequest(server string, body PostSessionUnshareJSONRequestBody) (*http.Request, error) {
var bodyReader io.Reader
buf, err := json.Marshal(body)
if err != nil {
return nil, err
}
bodyReader = bytes.NewReader(buf)
return NewPostSessionUnshareRequestWithBody(server, "application/json", bodyReader)
}
// NewPostSessionUnshareRequestWithBody generates requests for PostSessionUnshare with any type of body
func NewPostSessionUnshareRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) {
var err error
serverURL, err := url.Parse(server)
if err != nil {
return nil, err
}
operationPath := fmt.Sprintf("/session_unshare")
if operationPath[0] == '/' {
operationPath = "." + operationPath
}
queryURL, err := serverURL.Parse(operationPath)
if err != nil {
return nil, err
}
req, err := http.NewRequest("POST", queryURL.String(), body)
if err != nil {
return nil, err
}
req.Header.Add("Content-Type", contentType)
return req, nil
}
func (c *Client) applyEditors(ctx context.Context, req *http.Request, additionalEditors []RequestEditorFn) error {
for _, r := range c.RequestEditors {
if err := r(ctx, req); err != nil {
@@ -2516,6 +2715,11 @@ type ClientWithResponsesInterface interface {
// PostSessionCreateWithResponse request
PostSessionCreateWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*PostSessionCreateResponse, error)
// PostSessionDeleteWithBodyWithResponse request with any body
PostSessionDeleteWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostSessionDeleteResponse, error)
PostSessionDeleteWithResponse(ctx context.Context, body PostSessionDeleteJSONRequestBody, reqEditors ...RequestEditorFn) (*PostSessionDeleteResponse, error)
// PostSessionInitializeWithBodyWithResponse request with any body
PostSessionInitializeWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostSessionInitializeResponse, error)
@@ -2538,6 +2742,11 @@ type ClientWithResponsesInterface interface {
PostSessionSummarizeWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostSessionSummarizeResponse, error)
PostSessionSummarizeWithResponse(ctx context.Context, body PostSessionSummarizeJSONRequestBody, reqEditors ...RequestEditorFn) (*PostSessionSummarizeResponse, error)
// PostSessionUnshareWithBodyWithResponse request with any body
PostSessionUnshareWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostSessionUnshareResponse, error)
PostSessionUnshareWithResponse(ctx context.Context, body PostSessionUnshareJSONRequestBody, reqEditors ...RequestEditorFn) (*PostSessionUnshareResponse, error)
}
type PostAppInfoResponse struct {
@@ -2791,6 +3000,28 @@ func (r PostSessionCreateResponse) StatusCode() int {
return 0
}
type PostSessionDeleteResponse struct {
Body []byte
HTTPResponse *http.Response
JSON200 *bool
}
// Status returns HTTPResponse.Status
func (r PostSessionDeleteResponse) Status() string {
if r.HTTPResponse != nil {
return r.HTTPResponse.Status
}
return http.StatusText(0)
}
// StatusCode returns HTTPResponse.StatusCode
func (r PostSessionDeleteResponse) StatusCode() int {
if r.HTTPResponse != nil {
return r.HTTPResponse.StatusCode
}
return 0
}
type PostSessionInitializeResponse struct {
Body []byte
HTTPResponse *http.Response
@@ -2901,6 +3132,28 @@ func (r PostSessionSummarizeResponse) StatusCode() int {
return 0
}
type PostSessionUnshareResponse struct {
Body []byte
HTTPResponse *http.Response
JSON200 *SessionInfo
}
// Status returns HTTPResponse.Status
func (r PostSessionUnshareResponse) Status() string {
if r.HTTPResponse != nil {
return r.HTTPResponse.Status
}
return http.StatusText(0)
}
// StatusCode returns HTTPResponse.StatusCode
func (r PostSessionUnshareResponse) StatusCode() int {
if r.HTTPResponse != nil {
return r.HTTPResponse.StatusCode
}
return 0
}
// PostAppInfoWithResponse request returning *PostAppInfoResponse
func (c *ClientWithResponses) PostAppInfoWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*PostAppInfoResponse, error) {
rsp, err := c.PostAppInfo(ctx, reqEditors...)
@@ -3024,6 +3277,23 @@ func (c *ClientWithResponses) PostSessionCreateWithResponse(ctx context.Context,
return ParsePostSessionCreateResponse(rsp)
}
// PostSessionDeleteWithBodyWithResponse request with arbitrary body returning *PostSessionDeleteResponse
func (c *ClientWithResponses) PostSessionDeleteWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostSessionDeleteResponse, error) {
rsp, err := c.PostSessionDeleteWithBody(ctx, contentType, body, reqEditors...)
if err != nil {
return nil, err
}
return ParsePostSessionDeleteResponse(rsp)
}
func (c *ClientWithResponses) PostSessionDeleteWithResponse(ctx context.Context, body PostSessionDeleteJSONRequestBody, reqEditors ...RequestEditorFn) (*PostSessionDeleteResponse, error) {
rsp, err := c.PostSessionDelete(ctx, body, reqEditors...)
if err != nil {
return nil, err
}
return ParsePostSessionDeleteResponse(rsp)
}
// PostSessionInitializeWithBodyWithResponse request with arbitrary body returning *PostSessionInitializeResponse
func (c *ClientWithResponses) PostSessionInitializeWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostSessionInitializeResponse, error) {
rsp, err := c.PostSessionInitializeWithBody(ctx, contentType, body, reqEditors...)
@@ -3101,6 +3371,23 @@ func (c *ClientWithResponses) PostSessionSummarizeWithResponse(ctx context.Conte
return ParsePostSessionSummarizeResponse(rsp)
}
// PostSessionUnshareWithBodyWithResponse request with arbitrary body returning *PostSessionUnshareResponse
func (c *ClientWithResponses) PostSessionUnshareWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostSessionUnshareResponse, error) {
rsp, err := c.PostSessionUnshareWithBody(ctx, contentType, body, reqEditors...)
if err != nil {
return nil, err
}
return ParsePostSessionUnshareResponse(rsp)
}
func (c *ClientWithResponses) PostSessionUnshareWithResponse(ctx context.Context, body PostSessionUnshareJSONRequestBody, reqEditors ...RequestEditorFn) (*PostSessionUnshareResponse, error) {
rsp, err := c.PostSessionUnshare(ctx, body, reqEditors...)
if err != nil {
return nil, err
}
return ParsePostSessionUnshareResponse(rsp)
}
// ParsePostAppInfoResponse parses an HTTP response from a PostAppInfoWithResponse call
func ParsePostAppInfoResponse(rsp *http.Response) (*PostAppInfoResponse, error) {
bodyBytes, err := io.ReadAll(rsp.Body)
@@ -3402,6 +3689,32 @@ func ParsePostSessionCreateResponse(rsp *http.Response) (*PostSessionCreateRespo
return response, nil
}
// ParsePostSessionDeleteResponse parses an HTTP response from a PostSessionDeleteWithResponse call
func ParsePostSessionDeleteResponse(rsp *http.Response) (*PostSessionDeleteResponse, error) {
bodyBytes, err := io.ReadAll(rsp.Body)
defer func() { _ = rsp.Body.Close() }()
if err != nil {
return nil, err
}
response := &PostSessionDeleteResponse{
Body: bodyBytes,
HTTPResponse: rsp,
}
switch {
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200:
var dest bool
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
return nil, err
}
response.JSON200 = &dest
}
return response, nil
}
// ParsePostSessionInitializeResponse parses an HTTP response from a PostSessionInitializeWithResponse call
func ParsePostSessionInitializeResponse(rsp *http.Response) (*PostSessionInitializeResponse, error) {
bodyBytes, err := io.ReadAll(rsp.Body)
@@ -3531,3 +3844,29 @@ func ParsePostSessionSummarizeResponse(rsp *http.Response) (*PostSessionSummariz
return response, nil
}
// ParsePostSessionUnshareResponse parses an HTTP response from a PostSessionUnshareWithResponse call
func ParsePostSessionUnshareResponse(rsp *http.Response) (*PostSessionUnshareResponse, error) {
bodyBytes, err := io.ReadAll(rsp.Body)
defer func() { _ = rsp.Body.Close() }()
if err != nil {
return nil, err
}
response := &PostSessionUnshareResponse{
Body: bodyBytes,
HTTPResponse: rsp,
}
switch {
case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200:
var dest SessionInfo
if err := json.Unmarshal(bodyBytes, &dest); err != nil {
return nil, err
}
response.JSON200 = &dest
}
return response, nil
}

View File

@@ -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",

View File

@@ -113,29 +113,83 @@ const DiffView: Component<DiffViewProps> = (props) => {
return (
<div class={`${styles.diff} ${props.class ?? ""}`}>
<div class={styles.column}>
{rows().map((r) => (
<CodeBlock
code={r.left}
lang={props.lang}
data-section="cell"
data-diff-type={r.type === "removed" || r.type === "modified" ? "removed" : ""}
/>
))}
</div>
{rows().map((r) => (
<div class={styles.row}>
<div class={styles.beforeColumn}>
<CodeBlock
code={r.left}
lang={props.lang}
data-section="cell"
data-diff-type={r.type === "removed" || r.type === "modified" ? "removed" : ""}
data-display-mobile={r.type === "added" && !r.left ? "false" : undefined}
/>
{(r.type === "added" || r.type === "modified") && r.right !== undefined && (
<CodeBlock
code={r.right}
lang={props.lang}
data-section="cell"
data-diff-type="added"
data-display-mobile="true"
/>
)}
</div>
<div class={styles.column}>
{rows().map((r) => (
<CodeBlock
code={r.right}
lang={props.lang}
data-section="cell"
data-diff-type={r.type === "added" || r.type === "modified" ? "added" : ""}
/>
))}
</div>
<div class={styles.afterColumn}>
<CodeBlock
code={r.right}
lang={props.lang}
data-section="cell"
data-diff-type={r.type === "added" || r.type === "modified" ? "added" : ""}
/>
</div>
</div>
))}
</div>
)
}
export default DiffView
// String to test diff viewer with
const testDiff = `--- combined_before.txt 2025-06-24 16:38:08
+++ combined_after.txt 2025-06-24 16:38:12
@@ -1,21 +1,25 @@
unchanged line
-deleted line
-old content
+added line
+new content
-removed empty line below
+added empty line above
- tab indented
-trailing spaces
-very long line that will definitely wrap in most editors and cause potential alignment issues when displayed in a two column diff view
-unicode content: 🚀 ✨ 中文
-mixed content with tabs and spaces
+ space indented
+no trailing spaces
+short line
+very long replacement line that will also wrap and test how the diff viewer handles long line additions after short line removals
+different unicode: 🎉 💻 日本語
+normalized content with consistent spacing
+newline to content
-content to remove
-whitespace only:
-multiple
-consecutive
-deletions
-single deletion
+
+single addition
+first addition
+second addition
+third addition
line before addition
+first added line
+
+third added line
line after addition
final unchanged line`

View File

@@ -11,6 +11,7 @@ import {
createEffect,
createSignal,
} from "solid-js"
import map from "lang-map"
import { DateTime } from "luxon"
import { createStore, reconcile } from "solid-js/store"
import type { Diagnostic } from "vscode-languageserver-types"
@@ -85,7 +86,7 @@ function scrollToAnchor(id: string) {
}
function stripWorkingDirectory(filePath: string, workingDir?: string) {
if (workingDir === undefined) return filePath
if (filePath === undefined || workingDir === undefined) return filePath
const prefix = workingDir.endsWith("/") ? workingDir : workingDir + "/"
@@ -100,8 +101,24 @@ function stripWorkingDirectory(filePath: string, workingDir?: string) {
return filePath
}
function getFileType(path: string) {
return path.split(".").pop()
function getShikiLang(filename: string) {
const ext = filename
.split('.')
.pop()
?.toLowerCase() ?? ''
// map.languages(ext) returns an array of matching Linguist language names (e.g. ['TypeScript'])
const langs = map.languages(ext)
const type = langs?.[0]?.toLowerCase()
// Overrride any specific language mappings
const overrides: Record<string, string> = {
"conf": "shellscript"
}
return type
? overrides[type] ?? type
: 'plaintext'
}
function formatDuration(ms: number): string {
@@ -427,7 +444,7 @@ function MarkdownPart(props: MarkdownPartProps) {
{...rest}
>
<MarkdownView
data-elment-markdown
data-element-markdown
markdown={local.text}
ref={(el) => (divEl = el)}
/>
@@ -709,7 +726,7 @@ export default function Share(props: {
for (let i = 0; i < messages().length; i++) {
const msg = messages()[i]
// TODO: Cleaup
// TODO: Cleanup
// const system = result.messages.length === 0 && msg.role === "system"
const assistant = msg.metadata?.assistant
@@ -980,7 +997,7 @@ export default function Share(props: {
const prompts = assistant().system || []
return prompts.filter(
(p: string) =>
!p.startsWith("You are Claude Code"),
!p.startsWith("You are Claude"),
)
})
return (
@@ -1379,7 +1396,7 @@ export default function Share(props: {
<Show when={showResults()}>
<div data-part-tool-code>
<CodeBlock
lang={getFileType(filePath())}
lang={getShikiLang(filePath())}
code={preview()}
/>
</div>
@@ -1483,8 +1500,8 @@ export default function Share(props: {
<Show when={showResults()}>
<div data-part-tool-code>
<CodeBlock
lang={getFileType(filePath())}
code={content()}
lang={getShikiLang(filePath())}
code={args.content}
/>
</div>
</Show>
@@ -1557,7 +1574,7 @@ export default function Share(props: {
<DiffView
class={styles["diff-code-block"]}
diff={diff()}
lang={getFileType(filePath())}
lang={getShikiLang(filePath())}
/>
</div>
</Match>

View File

@@ -1,39 +1,52 @@
.diff {
display: grid;
grid-template-columns: 1fr 1fr;
display: flex;
flex-direction: column;
border: 1px solid var(--sl-color-divider);
background-color: var(--sl-color-bg-surface);
border-radius: 0.25rem;
}
.column {
.row {
display: grid;
grid-template-columns: 1fr 1fr;
align-items: stretch;
}
.beforeColumn,
.afterColumn {
display: flex;
flex-direction: column;
overflow-x: visible;
min-width: 0;
align-items: stretch;
}
&:first-child {
border-right: 1px solid var(--sl-color-divider);
}
.beforeColumn {
border-right: 1px solid var(--sl-color-divider);
}
& > [data-section="cell"]:first-child {
padding-top: 0.5rem;
}
& > [data-section="cell"]:last-child {
padding-bottom: 0.5rem;
}
.diff > .row:first-child [data-section="cell"]:first-child {
padding-top: 0.5rem;
}
.diff > .row:last-child [data-section="cell"]:last-child {
padding-bottom: 0.5rem;
}
[data-section="cell"] {
position: relative;
flex: none;
flex: 1;
display: flex;
flex-direction: column;
width: 100%;
padding: 0.1875rem 0.5rem 0.1875rem 2.2ch;
margin: 0;
&[data-display-mobile="true"] {
display: none;
}
pre {
--shiki-dark-bg: var(--sl-color-bg-surface) !important;
background-color: var(--sl-color-bg-surface) !important;
@@ -83,3 +96,27 @@
color: var(--sl-color-green-high);
}
}
@media (max-width: 40rem) {
.row {
grid-template-columns: 1fr;
}
.afterColumn {
display: none;
}
.beforeColumn {
border-right: none;
}
[data-section="cell"] {
&[data-display-mobile="true"] {
display: flex;
}
&[data-display-mobile="false"] {
display: none;
}
}
}

View File

@@ -37,16 +37,26 @@
margin-bottom: 0;
}
pre {
white-space: pre-wrap;
border-radius: 0.25rem;
border: 1px solid rgba(0, 0, 0, 0.2);
padding: 0.5rem 0.75rem;
font-size: 0.75rem;
}
code {
font-weight: 500;
&::before {
content: "`";
font-weight: 600;
}
&::after {
content: "`";
font-weight: 600;
&:not(pre code) {
&::before {
content: "`";
font-weight: 700;
}
&::after {
content: "`";
font-weight: 700;
}
}
}
}

View File

@@ -63,7 +63,7 @@
h1 {
font-size: 2.75rem;
font-weight: 500;
line-height: 1.15;
line-height: 1.2;
letter-spacing: -0.05em;
display: -webkit-box;
-webkit-box-orient: vertical;
@@ -360,7 +360,7 @@
[data-part-type="tool-fetch"] {
[data-part-tool-result] {
[data-part-tool-code] {
width: var(--md-tool-width);
max-width: var(--md-tool-width);
border: 1px solid var(--sl-color-divider);
background-color: var(--sl-color-bg-surface);
border-radius: 0.25rem;
@@ -659,12 +659,12 @@
}
&[data-expanded="true"] {
[data-elment-markdown] {
[data-element-markdown] {
display: block;
}
}
&[data-expanded="false"] {
[data-elment-markdown] {
[data-element-markdown] {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;

View File

@@ -89,3 +89,21 @@ You can configure MCP servers you want to use through the `mcp` option.
```
[Learn more here](/docs/mcp-servers).
---
### Disabled providers
You can disable providers that are loaded automatically through the `disabled_providers` option. This is useful when you want to prevent certain providers from being loaded even if their credentials are available.
```json title="opencode.json"
{
"$schema": "https://opencode.ai/config.json",
"disabled_providers": ["openai", "gemini"]
}
```
The `disabled_providers` option accepts an array of provider IDs. When a provider is disabled:
- It won't be loaded even if environment variables are set
- It won't be loaded even if API keys are configured through `opencode auth login`
- The provider's models won't appear in the model selection list

View File

@@ -64,6 +64,10 @@ paru -S opencode-bin
---
##### Windows
Right now the automatic installation methods do not work properly on Windows. However you can grab the binary from the [Releases](https://github.com/sst/opencode/releases).
## Providers
We recommend signing up for Claude Pro or Max, running `opencode auth login` and selecting Anthropic. It's the most cost-effective way to use opencode.

View File

@@ -2,43 +2,290 @@
title: Themes
---
opencode will support most common terminal themes and you'll soon be able to create your own custom theme.
opencode supports a flexible JSON-based theme system that allows users to create and customize themes easily.
---
## Theme Loading Hierarchy
## Built-in
Themes are loaded from multiple directories in the following order (later directories override earlier ones):
The following built-in themes are available:
1. **Built-in themes** - Embedded in the binary
2. **User config directory** - `~/.config/opencode/themes/*.json` (or `$XDG_CONFIG_HOME/opencode/themes/*.json`)
3. **Project root directory** - `<project-root>/.opencode/themes/*.json`
4. **Current working directory** - `./.opencode/themes/*.json`
- `opencode`
If multiple directories contain a theme with the same name, the theme from the directory with higher priority will be used.
![opencode theme](../../../assets/themes/opencode.png)
## Creating a Custom Theme
- `ayu`
To create a custom theme, create a JSON file in one of the theme directories:
![ayu theme](../../../assets/themes/ayu.png)
```bash no-frame
# For user-wide themes
mkdir -p ~/.config/opencode/themes
vim ~/.config/opencode/themes/my-theme.json
- `everforest`
![everforest theme](../../../assets/themes/everforest.png)
- `tokyonight`
![tokyonight theme](../../../assets/themes/tokyonight.png)
---
## Configure
To select a theme, type in:
```bash frame="none"
/themes
# For project-specific themes
mkdir -p .opencode/themes
vim .opencode/themes/my-theme.json
```
Your selected theme will be used the next time you start opencode.
## Theme JSON Format
You can also configure it in your opencode config.
Themes use a flexible JSON format with support for:
- **Hex colors**: `"#ffffff"`
- **ANSI colors**: `3` (0-255)
- **Color references**: `"primary"` or custom definitions
- **Dark/light variants**: `{"dark": "#000", "light": "#fff"}`
### Example Theme
```json no-frame
{
"$schema": "https://opencode.ai/theme.json",
"defs": {
"nord0": "#2E3440",
"nord1": "#3B4252",
"nord2": "#434C5E",
"nord3": "#4C566A",
"nord4": "#D8DEE9",
"nord5": "#E5E9F0",
"nord6": "#ECEFF4",
"nord7": "#8FBCBB",
"nord8": "#88C0D0",
"nord9": "#81A1C1",
"nord10": "#5E81AC",
"nord11": "#BF616A",
"nord12": "#D08770",
"nord13": "#EBCB8B",
"nord14": "#A3BE8C",
"nord15": "#B48EAD"
},
"theme": {
"primary": {
"dark": "nord8",
"light": "nord10"
},
"secondary": {
"dark": "nord9",
"light": "nord9"
},
"accent": {
"dark": "nord7",
"light": "nord7"
},
"error": {
"dark": "nord11",
"light": "nord11"
},
"warning": {
"dark": "nord12",
"light": "nord12"
},
"success": {
"dark": "nord14",
"light": "nord14"
},
"info": {
"dark": "nord8",
"light": "nord10"
},
"text": {
"dark": "nord4",
"light": "nord0"
},
"textMuted": {
"dark": "nord3",
"light": "nord1"
},
"background": {
"dark": "nord0",
"light": "nord6"
},
"backgroundPanel": {
"dark": "nord1",
"light": "nord5"
},
"backgroundElement": {
"dark": "nord1",
"light": "nord4"
},
"border": {
"dark": "nord2",
"light": "nord3"
},
"borderActive": {
"dark": "nord3",
"light": "nord2"
},
"borderSubtle": {
"dark": "nord2",
"light": "nord3"
},
"diffAdded": {
"dark": "nord14",
"light": "nord14"
},
"diffRemoved": {
"dark": "nord11",
"light": "nord11"
},
"diffContext": {
"dark": "nord3",
"light": "nord3"
},
"diffHunkHeader": {
"dark": "nord3",
"light": "nord3"
},
"diffHighlightAdded": {
"dark": "nord14",
"light": "nord14"
},
"diffHighlightRemoved": {
"dark": "nord11",
"light": "nord11"
},
"diffAddedBg": {
"dark": "#3B4252",
"light": "#E5E9F0"
},
"diffRemovedBg": {
"dark": "#3B4252",
"light": "#E5E9F0"
},
"diffContextBg": {
"dark": "nord1",
"light": "nord5"
},
"diffLineNumber": {
"dark": "nord2",
"light": "nord4"
},
"diffAddedLineNumberBg": {
"dark": "#3B4252",
"light": "#E5E9F0"
},
"diffRemovedLineNumberBg": {
"dark": "#3B4252",
"light": "#E5E9F0"
},
"markdownText": {
"dark": "nord4",
"light": "nord0"
},
"markdownHeading": {
"dark": "nord8",
"light": "nord10"
},
"markdownLink": {
"dark": "nord9",
"light": "nord9"
},
"markdownLinkText": {
"dark": "nord7",
"light": "nord7"
},
"markdownCode": {
"dark": "nord14",
"light": "nord14"
},
"markdownBlockQuote": {
"dark": "nord3",
"light": "nord3"
},
"markdownEmph": {
"dark": "nord12",
"light": "nord12"
},
"markdownStrong": {
"dark": "nord13",
"light": "nord13"
},
"markdownHorizontalRule": {
"dark": "nord3",
"light": "nord3"
},
"markdownListItem": {
"dark": "nord8",
"light": "nord10"
},
"markdownListEnumeration": {
"dark": "nord7",
"light": "nord7"
},
"markdownImage": {
"dark": "nord9",
"light": "nord9"
},
"markdownImageText": {
"dark": "nord7",
"light": "nord7"
},
"markdownCodeBlock": {
"dark": "nord4",
"light": "nord0"
},
"syntaxComment": {
"dark": "nord3",
"light": "nord3"
},
"syntaxKeyword": {
"dark": "nord9",
"light": "nord9"
},
"syntaxFunction": {
"dark": "nord8",
"light": "nord8"
},
"syntaxVariable": {
"dark": "nord7",
"light": "nord7"
},
"syntaxString": {
"dark": "nord14",
"light": "nord14"
},
"syntaxNumber": {
"dark": "nord15",
"light": "nord15"
},
"syntaxType": {
"dark": "nord7",
"light": "nord7"
},
"syntaxOperator": {
"dark": "nord9",
"light": "nord9"
},
"syntaxPunctuation": {
"dark": "nord4",
"light": "nord0"
}
}
}
```
### Color Definitions
The `defs` section (optional) allows you to define reusable colors that can be referenced in the theme.
## Built-in Themes
opencode comes with several built-in themes:
- `opencode` - Default opencode theme
- `tokyonight` - Tokyonight theme
- `everforest` - Everforest theme
- `ayu` - Ayu dark theme
- `catppuccin` - Catppuccin theme
- `gruvbox` - Gruvbox theme
- `kanagawa` - Kanagawa theme
- `nord` - Nord theme
- and more (see ./packages/tui/internal/theme/themes)
## Using a Theme
To use a theme, set it in your opencode configuration or select it from the theme dialog in the TUI.
```json title="opencode.json" {3}
{

View File

@@ -11,6 +11,13 @@ const { id } = Astro.params;
const res = await fetch(`${apiUrl}/share_data?id=${id}`);
const data = await res.json();
if (!data.info) {
return new Response(null, {
status: 404,
statusText: 'Not found'
});
}
const models: Set<string> = new Set();
const version = data.info.version ? `v${data.info.version}` : "v0.0.1";

0
packages/web/src/types/lang-map.d.ts vendored Normal file
View File

16
scripts/hooks.bat Normal file
View File

@@ -0,0 +1,16 @@
@echo off
if not exist ".git" (
exit /b 0
)
if not exist ".git\hooks" (
mkdir ".git\hooks"
)
(
echo #!/bin/sh
echo bun run typecheck
) > ".git\hooks\pre-push"
echo ✅ Pre-push hook installed