Compare commits

...
117 Commits
Author SHA1 Message Date
Dax Raad fc7af31fe5 make edit tool more robust
publish / publish (push) Has been cancelled
2025-06-25 17:10:48 -04:00
Dax Raad 2f8d23ec66 ignore: typo 2025-06-25 11:02:57 -04:00
Dax Raad 77ae3fb9b9 add output length errors 2025-06-25 11:02:09 -04:00
Dax Raad 4e7f6c47fd fix codex not working
publish / publish (push) Has been cancelled
2025-06-25 10:01:35 -04:00
Dax Raad 50469ed750 redirect uncaught errors to log file
publish / publish (push) Has been cancelled
2025-06-25 08:41:10 -04:00
Dax Raad aaab785493 better error message when bad directory is specified to start in 2025-06-24 22:28:25 -04:00
Dax Raadandopencode 9751937894 Enhance auth command with environment variable display and add models command
publish / publish (push) Has been cancelled
🤖 Generated with [opencode](https://opencode.ai)

Co-Authored-By: opencode <noreply@opencode.ai>
2025-06-24 22:24:55 -04:00
Dax Raad 0fc8dfc77e do not print error on ctrl+c during prompts 2025-06-24 22:09:43 -04:00
Dax Raad 81b7df61ec ci: bun lock 2025-06-24 21:14:32 -04:00
Dax Raad 8217b96d4a ci: fix type issue 2025-06-24 21:12:32 -04:00
Dax Raad 7dd0918d32 remove accidental opanai autoloader
publish / publish (push) Has been cancelled
2025-06-24 21:11:11 -04:00
Dax Raad 4b26b43855 added opencode serve command
publish / publish (push) Has been cancelled
2025-06-24 20:52:09 -04:00
Jay V 9d7cfda9fe docs: share page styles 2025-06-24 19:34:35 -04:00
Jay V a3cf18c905 docs: share page bash tool output 2025-06-24 19:28:51 -04:00
Aiden ClineandGitHub 0b1a8ae699 fix: file completions replaced wrong text when paths overlap (#378) 2025-06-24 18:13:15 -05:00
Dax Raad eb70b1e5c8 docs: windows instructions
publish / publish (push) Has been cancelled
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
ec001ca02f windows fixes (#374)
Co-authored-by: Matthew Glazar <strager.nds@gmail.com>
2025-06-24 18:05:04 -04:00
JayandGitHub a2d3b9f0c8 docs: Share page diff view improvements (#373) 2025-06-24 17:11:43 -04:00
Dax Raad 9cfb6ff964 ignore: revert
publish / publish (push) Has been cancelled
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
6f18475428 feat: delete sessions (#362)
publish / publish (push) Has been cancelled
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
publish / publish (push) Has been cancelled
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
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
publish / publish (push) Has been cancelled
2025-06-23 20:36:08 -04:00
Dax Raad dbba4a97aa force use npm registry
publish / publish (push) Has been cancelled
2025-06-23 20:23:37 -04:00
GitMurfandGitHub 0dc586faef fix: typescript error (any) from models (#347) 2025-06-23 18:44:57 -04:00
Dax Raad f19c6b05f2 glob tool should respect .gitignore
publish / publish (push) Has been cancelled
2025-06-23 17:37:32 -04:00
Dax Raad bc34f08333 bundle models.dev at build time and ignore refresh errors
publish / publish (push) Has been cancelled
2025-06-23 14:50:19 -04:00
Dax Raad b7ee16aabd ignore: remove opencode.json 2025-06-23 14:32:57 -04:00
Lucas GrzegorczykandGitHub 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
publish / publish (push) Has been cancelled
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
GitMurfandGitHub 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
publish / publish (push) Has been cancelled
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
publish / publish (push) Has been cancelled
2025-06-22 19:13:25 -04:00
Martin PalmaandGitHub 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 MagyarandGitHub 333569bed3 ignore: fix typos and formatting (#294) 2025-06-22 14:26:46 -04:00
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
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
AdamandGitHub 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
publish / publish (push) Has been cancelled
2025-06-22 06:44:23 -05:00
phantomreactorandGitHub 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
publish / publish (push) Has been cancelled
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 MagyarandGitHub 137e964131 fix: session title generation (#293) 2025-06-21 14:32:11 -05:00
tyrellshawnandGitHub 8efbe497fd Created a Theme inspired by the matrix (#285) 2025-06-21 07:29:49 -05:00
Thomas MeireandGitHub 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
publish / publish (push) Has been cancelled
2025-06-20 22:43:04 -04:00
Dax Raadandopencode 59f0004d34 Add --method option to upgrade command for manual installation method selection
publish / publish (push) Has been cancelled
🤖 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
publish / publish (push) Has been cancelled
2025-06-20 17:39:59 -04:00
Saatvik AryaandGitHub 53c18a64b4 docs: add API client generation instructions to README and AGENTS.md (#273) 2025-06-20 17:27:58 -04:00
Saatvik AryaandGitHub 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
publish / publish (push) Has been cancelled
2025-06-20 15:49:51 -05:00
adamdottv ef3fd5900f docs: cleanup casing
publish / publish (push) Has been cancelled
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 MagyarandGitHub 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 Raadandopencode 98734ff28c Consolidate session context handling and add global config support
publish / publish (push) Has been cancelled
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
9991352663 feat: forward provider options from model config (#202)
publish / publish (push) Has been cancelled
Co-authored-by: Dax Raad <d@ironbay.co>
2025-06-20 15:03:41 -04:00
91c4da5dbd fix(#243): claude on aws bedrock (#241)
publish / publish (push) Has been cancelled
Co-authored-by: Dax Raad <d@ironbay.co>
2025-06-20 14:57:33 -04:00
nibaandGitHub 2fd0e7dd6b chore: use client_id everywhere (#260) 2025-06-20 14:56:33 -04:00
adamdottv d50b7ad481 docs: theme schema update 2025-06-20 13:51:32 -05:00
adamdottv df95c49401 docs: theme schema 2025-06-20 13:00:32 -05:00
adamdottv 8b73c52f00 chore(tui): rename theme colors 2025-06-20 13:00:31 -05:00
Jay V 5603098d17 docs: add config 2025-06-20 13:22:31 -04:00
Jay V f436a50125 docs: share header 2025-06-20 13:12:35 -04:00
Jay V e19e977591 docs: test 2025-06-20 13:02:05 -04:00
Jay V addbe295b1 docs: test 2025-06-20 12:59:32 -04:00
Jay V 9a573dedc6 docs: test 2025-06-20 12:56:00 -04:00
adamdottv 9ea0d71e8d fix(tui): async load messages on theme/session switch 2025-06-20 11:25:21 -05:00
adamdottv b1a3599017 fix(tui): input latency optimization 2025-06-20 11:08:08 -05:00
adamdottv 7b0329f67f fix(tui): fetch tool more defensive
publish / publish (push) Has been cancelled
2025-06-20 09:00:28 -05:00
adamdottv 311b9c74dd fix(tui): typeahead open/close perf 2025-06-20 08:20:10 -05:00
adamdottv f7e8dd2ff8 chore: fix typescript issues 2025-06-20 07:48:42 -05:00
adamdottv 40b1dd7ef2 fix(tui): insert newline correctly positioned 2025-06-20 07:42:04 -05:00
adamdottv 261e76e0a3 fix(tui): input feels laggy 2025-06-20 07:31:45 -05:00
Dax Raad a300bfaccb docs: remove opencode.json 2025-06-20 01:00:15 -04:00
Dax Raad 41dba0db08 config validation
publish / publish (push) Has been cancelled
2025-06-20 00:57:28 -04:00
Rohan GodhaandGitHub 6674c6083a fix: phantom input bug on wsl (#200) 2025-06-19 20:08:56 -05:00
Tom WatkinsandGitHub f6afa2c6bb docs: fix typo in config.mdx (#218) 2025-06-19 21:08:21 -04:00
Dax Raad b2fb0508ea fix for azure models not liking tool definitions
publish / publish (push) Has been cancelled
2025-06-19 18:28:42 -04:00
Jay V 93f4252bb1 docs: tweak lander 2025-06-19 18:19:35 -04:00
JayandGitHub 46ab9c16dd docs: Update README.md 2025-06-19 18:19:06 -04:00
Dax Raad d869df4fee remove unused permission timeout
publish / publish (push) Has been cancelled
2025-06-19 18:00:53 -04:00
Dax Raad b99d4650ec temporarily disable project details in system prompt
publish / publish (push) Has been cancelled
2025-06-19 17:37:23 -04:00
Frank 261bb7f110 Infra: fix DO tag 2025-06-19 17:20:13 -04:00
Dax Raad 0515fbb260 fix gopls download spewing into terminal
publish / publish (push) Has been cancelled
2025-06-19 17:08:58 -04:00
adamdottv 88211d8c5b fix(tui): upgrade notification
publish / publish (push) Has been cancelled
2025-06-19 16:03:45 -05:00
Jay V a812f95b9d docs: share 2025-06-19 16:57:42 -04:00
113 changed files with 6029 additions and 2590 deletions
+1 -1
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
+1 -1
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: |
+1 -1
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
+13 -25
View File
@@ -7,18 +7,17 @@
</picture>
</a>
</p>
<p align="center">AI coding agent, built for the terminal.</p>
<p align="center">
<a href="https://opencode.ai/docs"><img alt="View docs" src="https://img.shields.io/badge/view-docs-blue?style=flat-square" /></a>
<a href="https://www.npmjs.com/package/opencode-ai"><img alt="npm" src="https://img.shields.io/npm/v/opencode-ai?style=flat-square" /></a>
<a href="https://github.com/sst/opencode/actions/workflows/publish.yml"><img alt="Build status" src="https://img.shields.io/github/actions/workflow/status/sst/opencode/publish.yml?style=flat-square&branch=dev" /></a>
</p>
---
AI coding agent, built for the terminal.
[![opencode Terminal UI](packages/web/src/assets/themes/opencode.png)](https://opencode.ai)
---
### Installation
```bash
@@ -39,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
@@ -51,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?
+60 -2
View File
@@ -20,6 +20,9 @@
"packages/opencode": {
"name": "opencode",
"version": "0.0.0",
"bin": {
"opencode": "./bin/opencode",
},
"dependencies": {
"@clack/prompts": "0.11.0",
"@flystorage/file-storage": "1.1.0",
@@ -43,8 +46,10 @@
"yargs": "18.0.0",
"zod": "catalog:",
"zod-openapi": "4.2.4",
"zod-validation-error": "3.5.2",
},
"devDependencies": {
"@ai-sdk/amazon-bedrock": "2.2.10",
"@ai-sdk/anthropic": "1.2.12",
"@tsconfig/bun": "1.0.7",
"@types/bun": "latest",
@@ -69,6 +74,7 @@
"astro": "5.7.13",
"diff": "8.0.2",
"js-base64": "3.7.7",
"lang-map": "0.4.0",
"luxon": "3.6.1",
"marked": "15.0.12",
"rehype-autolink-headings": "7.1.0",
@@ -101,6 +107,8 @@
"zod": "3.24.2",
},
"packages": {
"@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@2.2.10", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.0.0" } }, "sha512-icLGO7Q0NinnHIPgT+y1QjHVwH4HwV+brWbvM+FfCG2Afpa89PyKa3Ret91kGjZpBgM/xnj1B7K5eM+rRlsXQA=="],
"@ai-sdk/anthropic": ["@ai-sdk/anthropic@1.2.12", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8" }, "peerDependencies": { "zod": "^3.0.0" } }, "sha512-YSzjlko7JvuiyQFmI9RN1tNZdEiZxc+6xld/0tq/VkJaHpEzGAb1yiNxxvmYVcjvfu/PcvCxAAYXmTYQQ63IHQ=="],
"@ai-sdk/provider": ["@ai-sdk/provider@1.1.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg=="],
@@ -137,6 +145,12 @@
"@astrojs/underscore-redirects": ["@astrojs/underscore-redirects@0.6.1", "", {}, "sha512-4bMLrs2KW+8/vHEE5Ffv2HbxCbbgXO+2N6MpoCsMXUlUoi7pgEEx8kbkzMXJ2dZtWF3gvwm9lvgjnFeanC2LGg=="],
"@aws-crypto/crc32": ["@aws-crypto/crc32@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg=="],
"@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="],
"@aws-sdk/types": ["@aws-sdk/types@3.821.0", "", { "dependencies": { "@smithy/types": "^4.3.1", "tslib": "^2.6.2" } }, "sha512-Znroqdai1a90TlxGaJ+FK1lwC0fHpo97Xjsp5UKGR5JODYm7f9+/fF17ebO1KdoBr/Rm0UIFiF5VmI8ts9F1eA=="],
"@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
"@babel/compat-data": ["@babel/compat-data@7.27.3", "", {}, "sha512-V42wFfx1ymFte+ecf6iXghnnP8kWTO+ZLXIyZq+1LAXHHvTZdVxicn4yiVYdYMGaCO3tmqub11AorKkv+iodqw=="],
@@ -417,6 +431,18 @@
"@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],
"@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.0.4", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.3.1", "@smithy/util-hex-encoding": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-7XoWfZqWb/QoR/rAU4VSi0mWnO2vu9/ltS6JZ5ZSZv0eovLVfDfu0/AX4ub33RsJTOth3TiFWSHS5YdztvFnig=="],
"@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw=="],
"@smithy/types": ["@smithy/types@4.3.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-UqKOQBL2x6+HWl3P+3QqFD4ncKq0I8Nuz9QItGv5WuKuMHuuwlhvqcZCoXGfc+P1QmfJE7VieykoYYmrOoFJxA=="],
"@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.0.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug=="],
"@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Yk5mLhHtfIgW2W2WQZWSg5kuMZCVbvhFmC7rV4IO2QqnZdbEFPmQnCcGMAX2z/8Qj3B9hYYNjZOhWym+RwhePw=="],
"@smithy/util-utf8": ["@smithy/util-utf8@4.0.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.0.0", "tslib": "^2.6.2" } }, "sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow=="],
"@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="],
"@swc/helpers": ["@swc/helpers@0.5.17", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A=="],
@@ -435,7 +461,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=="],
@@ -573,7 +599,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=="],
@@ -989,6 +1015,10 @@
"klona": ["klona@2.0.6", "", {}, "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA=="],
"lang-map": ["lang-map@0.4.0", "", { "dependencies": { "language-map": "^1.1.0" } }, "sha512-oiSqZIEUnWdFeDNsp4HId4tAxdFbx5iMBOwA3666Fn2L8Khj8NiD9xRvMsGmKXopPVkaDFtSv3CJOmXFUB0Hcg=="],
"language-map": ["language-map@1.5.0", "", {}, "sha512-n7gFZpe+DwEAX9cXVTw43i3wiudWDDtSn28RmdnS/HCPr284dQI/SztsamWanRr75oSlKSaGbV2nmWCTzGCoVg=="],
"leven": ["leven@2.1.0", "", {}, "sha512-nvVPLpIHUxCUoRLrFqTgSxXJ614d8AgQoWl7zPe/2VadE8+1dpU3LBhowRuBAcuwruWtOdD8oYC9jDNJjXDPyA=="],
"longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="],
@@ -1655,12 +1685,24 @@
"zod-to-ts": ["zod-to-ts@1.2.0", "", { "peerDependencies": { "typescript": "^4.9.4 || ^5.0.2", "zod": "^3" } }, "sha512-x30XE43V+InwGpvTySRNz9kB7qFU8DlyEy7BsSTCHPH1R0QasMmHWZDCzYm6bVXtj/9NNJAZF3jW8rzFvH5OFA=="],
"zod-validation-error": ["zod-validation-error@3.5.2", "", { "peerDependencies": { "zod": "^3.25.0" } }, "sha512-mdi7YOLtram5dzJ5aDtm1AG9+mxRma1iaMrZdYIpFO7epdKBUwLHIxTF8CPDeCQ828zAXYtizrKlEJAtzgfgrw=="],
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
"@ai-sdk/amazon-bedrock/aws4fetch": ["aws4fetch@1.0.20", "", {}, "sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g=="],
"@ampproject/remapping/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
"@astrojs/mdx/@astrojs/markdown-remark": ["@astrojs/markdown-remark@6.3.2", "", { "dependencies": { "@astrojs/internal-helpers": "0.6.1", "@astrojs/prism": "3.3.0", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "hast-util-to-text": "^4.0.2", "import-meta-resolve": "^4.1.0", "js-yaml": "^4.1.0", "mdast-util-definitions": "^6.0.0", "rehype-raw": "^7.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remark-smartypants": "^3.0.2", "shiki": "^3.2.1", "smol-toml": "^1.3.1", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.1", "vfile": "^6.0.3" } }, "sha512-bO35JbWpVvyKRl7cmSJD822e8YA8ThR/YbUsciWNA7yTcqpIAL2hJDToWP5KcZBWxGT6IOdOkHSXARSNZc4l/Q=="],
"@aws-crypto/crc32/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="],
"@aws-crypto/util/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/types/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
@@ -1681,6 +1723,18 @@
"@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
"@smithy/eventstream-codec/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@smithy/is-array-buffer/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@smithy/types/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@smithy/util-buffer-from/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@smithy/util-hex-encoding/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@smithy/util-utf8/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@swc/helpers/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"ansi-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
@@ -1751,6 +1805,8 @@
"@astrojs/mdx/@astrojs/markdown-remark/@astrojs/prism": ["@astrojs/prism@3.3.0", "", { "dependencies": { "prismjs": "^1.30.0" } }, "sha512-q8VwfU/fDZNoDOf+r7jUnMC2//H2l0TuQ6FkGJL8vD8nw/q5KiL3DS1KKBI3QhI9UQhpJ5dc7AtqfbXWuOgLCQ=="],
"@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="],
"@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
"ansi-align/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
@@ -1817,6 +1873,8 @@
"wrangler/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.4", "", { "os": "win32", "cpu": "x64" }, "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ=="],
"@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="],
"ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"args/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="],
+4 -3
View File
@@ -23,9 +23,10 @@ export const api = new sst.cloudflare.Worker("Api", {
},
])
args.migrations = {
// oldTag: "v1",
// newTag: "v1",
newSqliteClasses: ["SyncServer"],
// Note: when releasing the next tag, make sure all stages use tag v2
oldTag: $app.stage === "production" ? "" : "v1",
newTag: $app.stage === "production" ? "" : "v1",
//newSqliteClasses: ["SyncServer"],
}
},
},
+1 -3
View File
@@ -1,5 +1,3 @@
{
"$schema": "https://opencode.ai/config.json",
"keybinds": {},
"mcp": {}
"$schema": "https://opencode.ai/config.json"
}
+2
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.
+56
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%" %*
+9 -5
View File
@@ -1,24 +1,27 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "0.0.0",
"name": "opencode",
"version": "0.0.5", "name": "opencode",
"type": "module",
"private": true,
"scripts": {
"typecheck": "tsc --noEmit",
"dev": "bun run ./src/index.ts"
},
"bin": {
"opencode": "./bin/opencode"
},
"exports": {
"./*": "./src/*.ts"
},
"devDependencies": {
"@ai-sdk/amazon-bedrock": "2.2.10",
"@ai-sdk/anthropic": "1.2.12",
"@tsconfig/bun": "1.0.7",
"@types/bun": "latest",
"@types/turndown": "5.0.5",
"@types/yargs": "17.0.33",
"typescript": "catalog:",
"zod-to-json-schema": "3.24.5",
"@ai-sdk/anthropic": "1.2.12"
"zod-to-json-schema": "3.24.5"
},
"dependencies": {
"@clack/prompts": "0.11.0",
@@ -42,6 +45,7 @@
"xdg-basedir": "5.1.0",
"yargs": "18.0.0",
"zod": "catalog:",
"zod-openapi": "4.2.4"
"zod-openapi": "4.2.4",
"zod-validation-error": "3.5.2"
}
}
+1 -1
View File
@@ -29,7 +29,7 @@ const targets = [
["linux", "x64"],
["darwin", "x64"],
["darwin", "arm64"],
// ["windows", "x64"],
["windows", "x64"],
]
await $`rm -rf dist`
+11 -1
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, "-")
}
}
+2 -2
View File
@@ -8,7 +8,7 @@ export namespace AuthAnthropic {
const pkce = await generatePKCE()
const url = new URL("https://claude.ai/oauth/authorize", import.meta.url)
url.searchParams.set("code", "true")
url.searchParams.set("client_id", "9d1c250a-e61b-44d9-88ed-5944d1962f5e")
url.searchParams.set("client_id", CLIENT_ID)
url.searchParams.set("response_type", "code")
url.searchParams.set(
"redirect_uri",
@@ -38,7 +38,7 @@ export namespace AuthAnthropic {
code: splits[0],
state: splits[1],
grant_type: "authorization_code",
client_id: "9d1c250a-e61b-44d9-88ed-5944d1962f5e",
client_id: CLIENT_ID,
redirect_uri: "https://console.anthropic.com/oauth/code/callback",
code_verifier: verifier,
}),
+20
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
})
@@ -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(),
}),
)
}
+11 -2
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(
+81 -8
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"
@@ -6,17 +7,20 @@ import open from "open"
import { UI } from "../ui"
import { ModelsDev } from "../../provider/models"
import { map, pipe, sortBy, values } from "remeda"
import path from "path"
import os from "os"
import { Global } from "../../global"
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({
@@ -25,30 +29,61 @@ export const AuthListCommand = cmd({
describe: "list providers",
async handler() {
UI.empty()
prompts.intro("Credentials")
const authPath = path.join(Global.Path.data, "auth.json")
const homedir = os.homedir()
const displayPath = authPath.startsWith(homedir)
? authPath.replace(homedir, "~")
: authPath
prompts.intro(`Credentials ${UI.Style.TEXT_DIM}${displayPath}`)
const results = await Auth.all().then((x) => Object.entries(x))
const database = await ModelsDev.get()
for (const [providerID, result] of results) {
const name = database[providerID]?.name || providerID
prompts.log.info(`${name} ${UI.Style.TEXT_DIM}(${result.type})`)
prompts.log.info(`${name} ${UI.Style.TEXT_DIM}${result.type}`)
}
prompts.outro(`${results.length} credentials`)
// Environment variables section
const activeEnvVars: Array<{ provider: string, envVar: string }> = []
for (const [providerID, provider] of Object.entries(database)) {
for (const envVar of provider.env) {
if (process.env[envVar]) {
activeEnvVars.push({
provider: provider.name || providerID,
envVar
})
}
}
}
if (activeEnvVars.length > 0) {
UI.empty()
prompts.intro("Environment")
for (const { provider, envVar } of activeEnvVars) {
prompts.log.info(`${provider} ${UI.Style.TEXT_DIM}${envVar}`)
}
prompts.outro(`${activeEnvVars.length} environment variables`)
}
},
})
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 +181,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 +235,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))
@@ -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)
},
}
+19
View File
@@ -0,0 +1,19 @@
import { App } from "../../app/app"
import { Provider } from "../../provider/provider"
import { cmd } from "./cmd"
export const ModelsCommand = cmd({
command: "models",
describe: "list all available models",
handler: async () => {
await App.provide({ cwd: process.cwd() }, async () => {
const providers = await Provider.list()
for (const [providerID, provider] of Object.entries(providers)) {
for (const modelID of Object.keys(provider.info.models)) {
console.log(`${providerID}/${modelID}`)
}
}
})
},
})
+4 -4
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) => {
+50
View File
@@ -0,0 +1,50 @@
import { App } from "../../app/app"
import { Provider } from "../../provider/provider"
import { Server } from "../../server/server"
import { Share } from "../../share/share"
import { cmd } from "./cmd"
export const ServeCommand = cmd({
command: "serve",
builder: (yargs) =>
yargs
.option("port", {
alias: ["p"],
type: "number",
describe: "port to listen on",
default: 4096,
})
.option("hostname", {
alias: ["h"],
type: "string",
describe: "hostname to listen on",
default: "127.0.0.1",
}),
describe: "starts a headless opencode server",
handler: async (args) => {
const cwd = process.cwd()
await App.provide({ cwd }, async () => {
const providers = await Provider.list()
if (Object.keys(providers).length === 0) {
return "needs_provider"
}
const hostname = args.hostname
const port = args.port
await Share.init()
const server = Server.listen({
port,
hostname,
})
console.log(
`opencode server listening on http://${server.hostname}:${server.port}`,
)
await new Promise(() => {})
server.stop()
})
},
})
+16 -7
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()
+19
View File
@@ -0,0 +1,19 @@
import { Config } from "../config/config"
import { MCP } from "../mcp"
import { UI } from "./ui"
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))
return [
`Config file at ${input.data.path} is invalid`,
...(input.data.issues?.map(
(issue) => "↳ " + issue.message + " " + issue.path.join("."),
) ?? []),
].join("\n")
if (UI.CancelledError.isInstance(input)) return ""
}
+3 -2
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()
}
+107 -43
View File
@@ -8,6 +8,7 @@ import { mergeDeep } from "remeda"
import { Global } from "../global"
import fs from "fs/promises"
import { lazy } from "../util/lazy"
import { NamedError } from "../util/error"
export namespace Config {
const log = Log.create({ service: "config" })
@@ -15,27 +16,9 @@ export namespace Config {
export const state = App.state("config", async (app) => {
let result = await global()
for (const file of ["opencode.jsonc", "opencode.json"]) {
const [resolved] = await Filesystem.findUp(
file,
app.path.cwd,
app.path.root,
)
if (!resolved) continue
try {
result = mergeDeep(
result,
await import(resolved).then((mod) => Info.parse(mod.default)),
)
log.info("found", { path: resolved })
break
} catch (e) {
if (e instanceof z.ZodError) {
for (const issue of e.issues) {
log.info(issue.message)
}
throw e
}
continue
const found = await Filesystem.findUp(file, app.path.cwd, app.path.root)
for (const resolved of found.toReversed()) {
result = mergeDeep(result, await load(resolved))
}
}
log.info("loaded", result)
@@ -45,9 +28,16 @@ export namespace Config {
export const McpLocal = z
.object({
type: z.literal("local").describe("Type of MCP server connection"),
command: z.string().array().describe("Command and arguments to run the MCP server"),
environment: z.record(z.string(), z.string()).optional().describe("Environment variables to set when running the MCP server"),
command: z
.string()
.array()
.describe("Command and arguments to run the MCP server"),
environment: z
.record(z.string(), z.string())
.optional()
.describe("Environment variables to set when running the MCP server"),
})
.strict()
.openapi({
ref: "Config.McpLocal",
})
@@ -57,6 +47,7 @@ export namespace Config {
type: z.literal("remote").describe("Type of MCP server connection"),
url: z.string().describe("URL of the remote MCP server"),
})
.strict()
.openapi({
ref: "Config.McpRemote",
})
@@ -66,41 +57,84 @@ export namespace Config {
export const Keybinds = z
.object({
leader: z.string().optional().describe("Leader key for keybind combinations"),
leader: z
.string()
.optional()
.describe("Leader key for keybind combinations"),
help: z.string().optional().describe("Show help dialog"),
editor_open: z.string().optional().describe("Open external editor"),
session_new: z.string().optional().describe("Create a new session"),
session_list: z.string().optional().describe("List all sessions"),
session_share: z.string().optional().describe("Share current session"),
session_interrupt: z.string().optional().describe("Interrupt current session"),
session_compact: z.string().optional().describe("Toggle compact mode for session"),
session_interrupt: z
.string()
.optional()
.describe("Interrupt current session"),
session_compact: z
.string()
.optional()
.describe("Toggle compact mode for session"),
tool_details: z.string().optional().describe("Show tool details"),
model_list: z.string().optional().describe("List available models"),
theme_list: z.string().optional().describe("List available themes"),
project_init: z.string().optional().describe("Initialize project configuration"),
project_init: z
.string()
.optional()
.describe("Initialize project configuration"),
input_clear: z.string().optional().describe("Clear input field"),
input_paste: z.string().optional().describe("Paste from clipboard"),
input_submit: z.string().optional().describe("Submit input"),
input_newline: z.string().optional().describe("Insert newline in input"),
history_previous: z.string().optional().describe("Navigate to previous history item"),
history_next: z.string().optional().describe("Navigate to next history item"),
messages_page_up: z.string().optional().describe("Scroll messages up by one page"),
messages_page_down: z.string().optional().describe("Scroll messages down by one page"),
messages_half_page_up: z.string().optional().describe("Scroll messages up by half page"),
messages_half_page_down: z.string().optional().describe("Scroll messages down by half page"),
messages_previous: z.string().optional().describe("Navigate to previous message"),
history_previous: z
.string()
.optional()
.describe("Navigate to previous history item"),
history_next: z
.string()
.optional()
.describe("Navigate to next history item"),
messages_page_up: z
.string()
.optional()
.describe("Scroll messages up by one page"),
messages_page_down: z
.string()
.optional()
.describe("Scroll messages down by one page"),
messages_half_page_up: z
.string()
.optional()
.describe("Scroll messages up by half page"),
messages_half_page_down: z
.string()
.optional()
.describe("Scroll messages down by half page"),
messages_previous: z
.string()
.optional()
.describe("Navigate to previous message"),
messages_next: z.string().optional().describe("Navigate to next message"),
messages_first: z.string().optional().describe("Navigate to first message"),
messages_first: z
.string()
.optional()
.describe("Navigate to first message"),
messages_last: z.string().optional().describe("Navigate to last message"),
app_exit: z.string().optional().describe("Exit the application"),
})
.strict()
.openapi({
ref: "Config.Keybinds",
})
export const Info = z
.object({
$schema: z.string().optional().describe("JSON schema reference for configuration validation"),
theme: z.string().optional().describe("Theme name to use for the interface"),
$schema: z
.string()
.optional()
.describe("JSON schema reference for configuration validation"),
theme: z
.string()
.optional()
.describe("Theme name to use for the interface"),
keybinds: Keybinds.optional().describe("Custom keybind configurations"),
autoshare: z
.boolean()
@@ -129,8 +163,12 @@ export namespace Config {
)
.optional()
.describe("Custom provider configurations and model overrides"),
mcp: z.record(z.string(), Mcp).optional().describe("MCP (Model Context Protocol) server configurations"),
mcp: z
.record(z.string(), Mcp)
.optional()
.describe("MCP (Model Context Protocol) server configurations"),
})
.strict()
.openapi({
ref: "Config.Info",
})
@@ -138,10 +176,7 @@ export namespace Config {
export type Info = z.output<typeof Info>
export const global = lazy(async () => {
let result = await Bun.file(path.join(Global.Path.config, "config.json"))
.json()
.then((mod) => Info.parse(mod))
.catch(() => ({}) as Info)
let result = await load(path.join(Global.Path.config, "config.json"))
await import(path.join(Global.Path.config, "config"), {
with: {
@@ -160,9 +195,38 @@ export namespace Config {
await fs.unlink(path.join(Global.Path.config, "config"))
})
.catch(() => {})
return Info.parse(result)
return result
})
async function load(path: string) {
const data = await Bun.file(path)
.json()
.catch((err) => {
if (err.code === "ENOENT") return {}
throw new JsonError({ path }, { cause: err })
})
const parsed = Info.safeParse(data)
if (parsed.success) return parsed.data
throw new InvalidError({ path, issues: parsed.error.issues })
}
export const JsonError = NamedError.create(
"ConfigJsonError",
z.object({
path: z.string(),
}),
)
export const InvalidError = NamedError.create(
"ConfigInvalidError",
z.object({
path: z.string(),
issues: z.custom<z.ZodIssue[]>().optional(),
}),
)
export function get() {
return state()
}
+5 -2
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)
}
}
+12 -4
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)
)
}
}
+84 -25
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"
@@ -13,17 +14,37 @@ import { ScrapCommand } from "./cli/cmd/scrap"
import { Log } from "./util/log"
import { AuthCommand, AuthLoginCommand } from "./cli/cmd/auth"
import { UpgradeCommand } from "./cli/cmd/upgrade"
import { ModelsCommand } from "./cli/cmd/models"
import { Provider } from "./provider/provider"
import { UI } from "./cli/ui"
import { Installation } from "./installation"
import { Bus } from "./bus"
import { Config } from "./config/config"
import { NamedError } from "./util/error"
import { FormatError } from "./cli/error"
import { ServeCommand } from "./cli/cmd/serve"
const cancel = new AbortController()
process.on("unhandledRejection", (e) => {
Log.Default.error("rejection", {
e: e instanceof Error ? e.message : e,
})
})
process.on("uncaughtException", (e) => {
Log.Default.error("exception", {
e: e instanceof Error ? e.message : e,
})
})
const cli = yargs(hideBin(process.argv))
.scriptName("opencode")
.version(Installation.VERSION)
.help("help", "show help")
.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 () => {
@@ -36,7 +57,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",
@@ -45,7 +66,12 @@ const cli = yargs(hideBin(process.argv))
handler: async (args) => {
while (true) {
const cwd = args.project ? path.resolve(args.project) : process.cwd()
process.chdir(cwd)
try {
process.chdir(cwd)
} catch (e) {
UI.error("Failed to change directory to " + cwd)
return
}
const result = await App.provide({ cwd }, async (app) => {
const providers = await Provider.list()
if (Object.keys(providers).length === 0) {
@@ -53,13 +79,22 @@ const cli = yargs(hideBin(process.argv))
}
await Share.init()
const server = Server.listen()
const server = Server.listen({
port: 0,
hostname: "127.0.0.1",
})
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 })
@@ -70,6 +105,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",
@@ -84,21 +120,22 @@ const cli = yargs(hideBin(process.argv))
},
})
; (async () => {
if (Installation.VERSION === "dev") return
if (Installation.isSnapshot()) return
const config = await Config.global()
if (config.autoupdate === false) return
const latest = await Installation.latest()
if (Installation.VERSION === latest) return
const method = await Installation.method()
if (method === "unknown") return
await Installation.upgrade(method, latest)
.then(() => {
Bus.publish(Installation.Event.Updated, { version: latest })
})
.catch(() => { })
})()
;(async () => {
if (Installation.VERSION === "dev") return
if (Installation.isSnapshot()) return
const config = await Config.global()
if (config.autoupdate === false) return
const latest = await Installation.latest().catch(() => {})
if (!latest) return
if (Installation.VERSION === latest) return
const method = await Installation.method()
if (method === "unknown") return
await Installation.upgrade(method, latest)
.then(() => {
Bus.publish(Installation.Event.Updated, { version: latest })
})
.catch(() => {})
})()
await proc.exited
server.stop()
@@ -120,6 +157,8 @@ const cli = yargs(hideBin(process.argv))
.command(ScrapCommand)
.command(AuthCommand)
.command(UpgradeCommand)
.command(ServeCommand)
.command(ModelsCommand)
.fail((msg) => {
if (
msg.startsWith("Unknown argument") ||
@@ -133,7 +172,27 @@ const cli = yargs(hideBin(process.argv))
try {
await cli.parse()
} catch (e) {
Log.Default.error(e, {
stack: e instanceof Error ? e.stack : undefined,
})
const data: Record<string, any> = {}
if (e instanceof NamedError) {
const obj = e.toObject()
Object.assign(data, {
...obj.data,
})
}
if (e instanceof Error) {
Object.assign(data, {
name: e.name,
message: e.message,
cause: e.cause?.toString(),
})
}
Log.Default.error("fatal", data)
const formatted = FormatError(e)
if (formatted) UI.error(formatted)
if (formatted === undefined)
UI.error(
"Unexpected error, check log file at " + Log.file() + " for more details",
)
}
cancel.abort()
+21 -1
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"),
+3
View File
@@ -70,6 +70,9 @@ export namespace LSPServer {
const proc = Bun.spawn({
cmd: ["go", "install", "golang.org/x/tools/gopls@latest"],
env: { ...process.env, GOBIN: Global.Path.bin },
stdout: "pipe",
stderr: "pipe",
stdin: "pipe",
})
const exit = await proc.exited
if (exit !== 0) {
+48 -5
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
}
}
@@ -64,6 +64,7 @@ export namespace Permission {
title: Info["title"]
metadata: Info["metadata"]
}) {
return
const { pending, approved } = state()
log.info("asking", {
sessionID: input.sessionID,
@@ -0,0 +1,4 @@
export async function data() {
const json = await fetch("https://models.dev/api.json").then((x) => x.text())
return json
}
+8 -6
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" })
@@ -13,6 +14,7 @@ export namespace ModelsDev {
attachment: z.boolean(),
reasoning: z.boolean(),
temperature: z.boolean(),
tool_call: z.boolean(),
cost: z.object({
input: z.number(),
output: z.number(),
@@ -24,6 +26,7 @@ export namespace ModelsDev {
output: z.number(),
}),
id: z.string(),
options: z.record(z.any()),
})
.openapi({
ref: "Model.Info",
@@ -52,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)
}
}
+133 -30
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"
@@ -29,14 +30,19 @@ export namespace Provider {
type CustomLoader = (
provider: ModelsDev.Provider,
) => Promise<Record<string, any> | false>
api?: string,
) => Promise<{
autoload: boolean
getModel?: (sdk: any, modelID: string) => Promise<any>
options?: Record<string, any>
}>
type Source = "env" | "config" | "custom" | "api"
const CUSTOM_LOADERS: Record<string, CustomLoader> = {
async anthropic(provider) {
const access = await AuthAnthropic.access()
if (!access) return false
if (!access) return { autoload: false }
for (const model of Object.values(provider.models)) {
model.cost = {
input: 0,
@@ -44,30 +50,103 @@ export namespace Provider {
}
}
return {
apiKey: "",
async fetch(input: any, init: any) {
const access = await AuthAnthropic.access()
const headers = {
...init.headers,
authorization: `Bearer ${access}`,
"anthropic-beta": "oauth-2025-04-20",
}
delete headers["x-api-key"]
return fetch(input, {
...init,
headers,
})
autoload: true,
options: {
apiKey: "",
async fetch(input: any, init: any) {
const access = await AuthAnthropic.access()
const headers = {
...init.headers,
authorization: `Bearer ${access}`,
"anthropic-beta": "oauth-2025-04-20",
}
delete headers["x-api-key"]
return fetch(input, {
...init,
headers,
})
},
},
}
},
"github-copilot": async (provider) => {
const copilot = await AuthCopilot()
if (!copilot) return { autoload: false }
let info = await Auth.get("github-copilot")
if (!info || info.type !== "oauth") return { autoload: false }
if (provider && provider.models) {
for (const model of Object.values(provider.models)) {
model.cost = {
input: 0,
output: 0,
}
}
}
return {
autoload: true,
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 {
autoload: false,
async getModel(sdk: any, modelID: string) {
return sdk.responses(modelID)
},
options: {},
}
},
"amazon-bedrock": async () => {
if (!process.env["AWS_PROFILE"]) return false
if (!process.env["AWS_PROFILE"] && !process.env["AWS_ACCESS_KEY_ID"])
return { autoload: false }
const region = process.env["AWS_REGION"] ?? "us-east-1"
const { fromNodeProviderChain } = await import(
await BunProc.install("@aws-sdk/credential-providers")
)
return {
region: process.env["AWS_REGION"] ?? "us-east-1",
credentialProvider: fromNodeProviderChain(),
autoload: true,
options: {
region,
credentialProvider: fromNodeProviderChain(),
},
async getModel(sdk: any, modelID: string) {
if (modelID.includes("claude")) {
const prefix = region.split("-")[0]
modelID = `${prefix}.${modelID}`
}
return sdk.languageModel(modelID)
},
}
},
}
@@ -80,6 +159,7 @@ export namespace Provider {
[providerID: string]: {
source: Source
info: ModelsDev.Provider
getModel?: (sdk: any, modelID: string) => Promise<any>
options: Record<string, any>
}
} = {}
@@ -95,6 +175,7 @@ export namespace Provider {
id: string,
options: Record<string, any>,
source: Source,
getModel?: (sdk: any, modelID: string) => Promise<any>,
) {
const provider = providers[id]
if (!provider) {
@@ -110,6 +191,7 @@ export namespace Provider {
}
provider.options = mergeDeep(provider.options, options)
provider.source = source
provider.getModel = getModel ?? provider.getModel
}
const configProviders = Object.entries(config.provider ?? {})
@@ -132,13 +214,19 @@ export namespace Provider {
attachment: model.attachment ?? existing?.attachment ?? false,
reasoning: model.reasoning ?? existing?.reasoning ?? false,
temperature: model.temperature ?? existing?.temperature ?? false,
cost: model.cost ??
existing?.cost ?? {
input: 0,
output: 0,
inputCached: 0,
outputCached: 0,
},
tool_call: model.tool_call ?? existing?.tool_call ?? true,
cost: {
...existing?.cost,
...model.cost,
input: 0,
output: 0,
cache_read: 0,
cache_write: 0,
},
options: {
...existing?.options,
...model.options,
},
limit: model.limit ??
existing?.limit ?? {
context: 0,
@@ -173,7 +261,14 @@ export namespace Provider {
for (const [providerID, fn] of Object.entries(CUSTOM_LOADERS)) {
if (disabled.has(providerID)) continue
const result = await fn(database[providerID])
if (result) mergeProvider(providerID, result, "custom")
if (result && (result.autoload || providers[providerID])) {
mergeProvider(
providerID,
result.options ?? {},
"custom",
result.getModel,
)
}
}
// load config
@@ -181,7 +276,11 @@ export namespace Provider {
mergeProvider(providerID, provider.options ?? {}, "config")
}
for (const providerID of Object.keys(providers)) {
for (const [providerID, provider] of Object.entries(providers)) {
if (Object.keys(provider.info.models).length === 0) {
delete providers[providerID]
continue
}
log.info("found", { providerID })
}
@@ -232,9 +331,9 @@ export namespace Provider {
const sdk = await getSDK(provider.info)
try {
const language =
// @ts-expect-error
"responses" in sdk ? sdk.responses(modelID) : sdk.languageModel(modelID)
const language = provider.getModel
? await provider.getModel(sdk, modelID)
: sdk.languageModel(modelID)
log.info("found", { providerID, modelID })
s.models.set(key, {
info,
@@ -322,6 +421,10 @@ export namespace Provider {
...t,
parameters: optionalToNullable(t.parameters),
})),
azure: TOOLS.map((t) => ({
...t,
parameters: optionalToNullable(t.parameters),
})),
google: TOOLS,
}
+15 -14
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
}
}
+58 -3
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({
@@ -524,10 +579,10 @@ export namespace Server {
return result
}
export function listen() {
export function listen(opts: { port: number; hostname: string }) {
const server = Bun.serve({
port: 0,
hostname: "0.0.0.0",
port: opts.port,
hostname: opts.hostname,
idleTimeout: 0,
fetch: app().fetch,
})
-19
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"))
}
}
+130 -38
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,
@@ -262,6 +310,7 @@ export namespace Session {
if (msgs.length === 0 && !session.parentID) {
generateText({
maxTokens: input.providerID === "google" ? 1024 : 20,
providerOptions: model.info.options,
messages: [
...SystemPrompt.title(input.providerID).map(
(x): CoreMessage => ({
@@ -276,9 +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) => {
@@ -425,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 })
@@ -507,6 +536,7 @@ export namespace Session {
toolCallStreaming: true,
abortSignal: abort.signal,
maxSteps: 1000,
providerOptions: model.info.options,
messages: [
...system.map(
(x): CoreMessage => ({
@@ -517,14 +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: {
...tools,
},
model: model.language,
tools: model.info.tool_call === false ? undefined : tools,
model: wrapLanguageModel({
model: model.language,
middleware: [
{
async transformParams(args) {
if (args.type === "stream") {
args.params.prompt = ProviderTransform.message(
args.params.prompt,
input.providerID,
input.modelID,
)
}
return args.params
},
},
],
}),
})
try {
for await (const value of result.fullStream) {
@@ -615,6 +657,21 @@ export namespace Session {
}
break
case "finish":
log.info("message finish", {
reason: value.finishReason,
})
const assistant = next.metadata!.assistant!
const usage = getUsage(
model.info,
value.usage,
value.providerMetadata,
)
assistant.cost = usage.cost
await updateMessage(next)
if (value.finishReason === "length")
throw new Message.OutputLengthError({})
break
default:
l.info("unhandled", {
type: value.type,
@@ -628,6 +685,9 @@ export namespace Session {
error: e,
})
switch (true) {
case Message.OutputLengthError.isInstance(e):
next.metadata.error = e
break
case LoadAPIKeyError.isInstance(e):
next.metadata.error = new Provider.AuthError(
{
@@ -715,7 +775,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 +798,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) {
+52 -44
View File
@@ -4,6 +4,11 @@ import { Provider } from "../provider/provider"
import { NamedError } from "../util/error"
export namespace Message {
export const OutputLengthError = NamedError.create(
"MessageOutputLengthError",
z.object({}),
)
export const ToolCall = z
.object({
state: z.literal("call"),
@@ -135,53 +140,56 @@ 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,
OutputLengthError.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",
+11 -7
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.
+18 -5
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"
@@ -25,7 +28,7 @@ export namespace SystemPrompt {
export async function environment() {
const app = App.info()
const tree = async () => {
;async () => {
const files = await Ripgrep.files({
cwd: app.path.cwd,
})
@@ -82,9 +85,9 @@ export namespace SystemPrompt {
` Platform: ${process.platform}`,
` Today's date: ${new Date().toDateString()}`,
`</env>`,
`<project>`,
` ${app.git ? await tree() : ""}`,
`</project>`,
// `<project>`,
// ` ${app.git ? await tree() : ""}`,
// `</project>`,
].join("\n"),
]
}
@@ -101,7 +104,17 @@ export namespace SystemPrompt {
const matches = await Filesystem.findUp(item, cwd, root)
found.push(...matches.map((x) => Bun.file(x).text()))
}
return Promise.all(found)
found.push(
Bun.file(path.join(Global.Path.config, "AGENTS.md"))
.text()
.catch(() => ""),
)
found.push(
Bun.file(path.join(os.homedir(), ".claude", "CLAUDE.md"))
.text()
.catch(() => ""),
)
return Promise.all(found).then((result) => result.filter(Boolean))
}
export function summarize(providerID: string) {
+7
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())
}
}
+10
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>
}
+9 -1
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"),
}
},
})
+206 -25
View File
@@ -1,3 +1,6 @@
// the approaches in this edit tool are sourced from
// https://github.com/cline/cline/blob/main/evals/diff-edits/diff-apply/diff-06-23-25.ts
import { z } from "zod"
import * as path from "path"
import { Tool } from "./tool"
@@ -22,7 +25,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) {
@@ -55,35 +58,19 @@ export const EditTool = Tool.define({
}
const file = Bun.file(filepath)
if (!(await file.exists())) throw new Error(`File ${filepath} not found`)
const stats = await file.stat()
const stats = await file.stat().catch(() => {})
if (!stats) throw new Error(`File ${filepath} not found`)
if (stats.isDirectory())
throw new Error(`Path is a directory, not a file: ${filepath}`)
await FileTimes.assert(ctx.sessionID, filepath)
contentOld = await file.text()
const index = contentOld.indexOf(params.oldString)
if (index === -1)
throw new Error(
`oldString not found in file. Make sure it matches exactly, including whitespace and line breaks`,
)
if (params.replaceAll) {
contentNew = contentOld.replaceAll(params.oldString, params.newString)
}
if (!params.replaceAll) {
const lastIndex = contentOld.lastIndexOf(params.oldString)
if (index !== lastIndex)
throw new Error(
`oldString appears multiple times in the file. Please provide more context to ensure a unique match`,
)
contentNew =
contentOld.substring(0, index) +
params.newString +
contentOld.substring(index + params.oldString.length)
}
contentNew = replace(
contentOld,
params.oldString,
params.newString,
params.replaceAll,
)
await file.write(contentNew)
})()
@@ -116,6 +103,169 @@ export const EditTool = Tool.define({
},
})
export type Replacer = (
content: string,
find: string,
) => Generator<string, void, unknown>
export const SimpleReplacer: Replacer = function* (_content, find) {
yield find
}
export const LineTrimmedReplacer: Replacer = function* (content, find) {
const originalLines = content.split("\n")
const searchLines = find.split("\n")
if (searchLines[searchLines.length - 1] === "") {
searchLines.pop()
}
for (let i = 0; i <= originalLines.length - searchLines.length; i++) {
let matches = true
for (let j = 0; j < searchLines.length; j++) {
const originalTrimmed = originalLines[i + j].trim()
const searchTrimmed = searchLines[j].trim()
if (originalTrimmed !== searchTrimmed) {
matches = false
break
}
}
if (matches) {
let matchStartIndex = 0
for (let k = 0; k < i; k++) {
matchStartIndex += originalLines[k].length + 1
}
let matchEndIndex = matchStartIndex
for (let k = 0; k < searchLines.length; k++) {
matchEndIndex += originalLines[i + k].length + 1
}
yield content.substring(matchStartIndex, matchEndIndex)
}
}
}
export const BlockAnchorReplacer: Replacer = function* (content, find) {
const originalLines = content.split("\n")
const searchLines = find.split("\n")
if (searchLines.length < 3) {
return
}
if (searchLines[searchLines.length - 1] === "") {
searchLines.pop()
}
const firstLineSearch = searchLines[0].trim()
const lastLineSearch = searchLines[searchLines.length - 1].trim()
// Find blocks where first line matches the search first line
for (let i = 0; i < originalLines.length; i++) {
if (originalLines[i].trim() !== firstLineSearch) {
continue
}
// Look for the matching last line after this first line
for (let j = i + 2; j < originalLines.length; j++) {
if (originalLines[j].trim() === lastLineSearch) {
// Found a potential block from i to j
let matchStartIndex = 0
for (let k = 0; k < i; k++) {
matchStartIndex += originalLines[k].length + 1
}
let matchEndIndex = matchStartIndex
for (let k = 0; k <= j - i; k++) {
matchEndIndex += originalLines[i + k].length + 1
}
yield content.substring(matchStartIndex, matchEndIndex)
break // Only match the first occurrence of the last line
}
}
}
}
export const WhitespaceNormalizedReplacer: Replacer = function* (
content,
find,
) {
const normalizeWhitespace = (text: string) => text.replace(/\s+/g, " ").trim()
const normalizedFind = normalizeWhitespace(find)
// Handle single line matches
const lines = content.split("\n")
for (let i = 0; i < lines.length; i++) {
const line = lines[i]
if (normalizeWhitespace(line) === normalizedFind) {
yield line
}
// Also check for substring matches within lines
const normalizedLine = normalizeWhitespace(line)
if (normalizedLine.includes(normalizedFind)) {
// Find the actual substring in the original line that matches
const words = find.trim().split(/\s+/)
if (words.length > 0) {
const pattern = words
.map((word) => word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
.join("\\s+")
const regex = new RegExp(pattern)
const match = line.match(regex)
if (match) {
yield match[0]
}
}
}
}
// Handle multi-line matches
const findLines = find.split("\n")
if (findLines.length > 1) {
for (let i = 0; i <= lines.length - findLines.length; i++) {
const block = lines.slice(i, i + findLines.length)
if (normalizeWhitespace(block.join("\n")) === normalizedFind) {
yield block.join("\n")
}
}
}
}
export const IndentationFlexibleReplacer: Replacer = function* (content, find) {
const removeIndentation = (text: string) => {
const lines = text.split("\n")
const nonEmptyLines = lines.filter((line) => line.trim().length > 0)
if (nonEmptyLines.length === 0) return text
const minIndent = Math.min(
...nonEmptyLines.map((line) => {
const match = line.match(/^(\s*)/)
return match ? match[1].length : 0
}),
)
return lines
.map((line) => (line.trim().length === 0 ? line : line.slice(minIndent)))
.join("\n")
}
const normalizedFind = removeIndentation(find)
const contentLines = content.split("\n")
const findLines = find.split("\n")
for (let i = 0; i <= contentLines.length - findLines.length; i++) {
const block = contentLines.slice(i, i + findLines.length).join("\n")
if (removeIndentation(block) === normalizedFind) {
yield block
}
}
}
function trimDiff(diff: string): string {
const lines = diff.split("\n")
const contentLines = lines.filter(
@@ -151,3 +301,34 @@ function trimDiff(diff: string): string {
return trimmedLines.join("\n")
}
export function replace(
content: string,
oldString: string,
newString: string,
replaceAll = false,
): string {
for (const replacer of [
SimpleReplacer,
LineTrimmedReplacer,
BlockAnchorReplacer,
WhitespaceNormalizedReplacer,
IndentationFlexibleReplacer,
]) {
for (const search of replacer(content, oldString)) {
const index = content.indexOf(search)
if (index === -1) continue
if (replaceAll) {
return content.replaceAll(search, newString)
}
const lastIndex = content.lastIndexOf(search)
if (index !== lastIndex) continue
return (
content.substring(0, index) +
newString +
content.substring(index + search.length)
)
}
}
throw new Error("oldString not found in content or was found multiple times")
}
+5 -2
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
+1 -1
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
+2 -6
View File
@@ -1,7 +1,7 @@
import { z, type ZodSchema } from "zod"
import { Log } from "./log"
// import { Log } from "./log"
const log = Log.create()
// const log = Log.create()
export abstract class NamedError extends Error {
abstract schema(): ZodSchema
@@ -30,10 +30,6 @@ export abstract class NamedError extends Error {
) {
super(name, options)
this.name = name
log.error(name, {
...this.data,
cause: options?.cause?.toString(),
})
}
static isInstance(input: any): input is InstanceType<typeof result> {
+4 -4
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()
@@ -68,13 +68,13 @@ export namespace Log {
}
const result = {
info(message?: any, extra?: Record<string, any>) {
process.stderr.write(build(message, extra))
process.stderr.write("INFO " + build(message, extra))
},
error(message?: any, extra?: Record<string, any>) {
process.stderr.write(build(message, extra))
process.stderr.write("ERROR " + build(message, extra))
},
warn(message?: any, extra?: Record<string, any>) {
process.stderr.write(build(message, extra))
process.stderr.write("WARN " + build(message, extra))
},
tag(key: string, value: string) {
if (tags) tags[key] = value
+209
View File
@@ -0,0 +1,209 @@
import { describe, expect, test } from "bun:test"
import { replace } from "../../src/tool/edit"
interface TestCase {
content: string
find: string
replace: string
all?: boolean
fail?: boolean
}
const testCases: TestCase[] = [
// SimpleReplacer cases
{
content: ["function hello() {", ' console.log("world");', "}"].join("\n"),
find: 'console.log("world");',
replace: 'console.log("universe");',
},
{
content: [
"if (condition) {",
" doSomething();",
" doSomethingElse();",
"}",
].join("\n"),
find: [" doSomething();", " doSomethingElse();"].join("\n"),
replace: [" doNewThing();", " doAnotherThing();"].join("\n"),
},
// LineTrimmedReplacer cases
{
content: ["function test() {", ' console.log("hello");', "}"].join("\n"),
find: 'console.log("hello");',
replace: 'console.log("goodbye");',
},
{
content: ["const x = 5; ", "const y = 10;"].join("\n"),
find: "const x = 5;",
replace: "const x = 15;",
},
{
content: [" if (true) {", " return false;", " }"].join("\n"),
find: ["if (true) {", "return false;", "}"].join("\n"),
replace: ["if (false) {", "return true;", "}"].join("\n"),
},
// BlockAnchorReplacer cases
{
content: [
"function calculate(a, b) {",
" const temp = a + b;",
" const result = temp * 2;",
" return result;",
"}",
].join("\n"),
find: [
"function calculate(a, b) {",
" // different middle content",
" return result;",
"}",
].join("\n"),
replace: ["function calculate(a, b) {", " return a * b * 2;", "}"].join(
"\n",
),
},
{
content: [
"class MyClass {",
" constructor() {",
" this.value = 0;",
" }",
" ",
" getValue() {",
" return this.value;",
" }",
"}",
].join("\n"),
find: ["class MyClass {", " // different implementation", "}"].join("\n"),
replace: [
"class MyClass {",
" constructor() {",
" this.value = 42;",
" }",
"}",
].join("\n"),
},
// WhitespaceNormalizedReplacer cases
{
content: ["function test() {", '\tconsole.log("hello");', "}"].join("\n"),
find: ' console.log("hello");',
replace: ' console.log("world");',
},
{
content: "const x = 5;",
find: "const x = 5;",
replace: "const x = 10;",
},
{
content: "if\t( condition\t) {",
find: "if ( condition ) {",
replace: "if (newCondition) {",
},
// IndentationFlexibleReplacer cases
{
content: [
" function nested() {",
' console.log("deeply nested");',
" return true;",
" }",
].join("\n"),
find: [
"function nested() {",
' console.log("deeply nested");',
" return true;",
"}",
].join("\n"),
replace: [
"function nested() {",
' console.log("updated");',
" return false;",
"}",
].join("\n"),
},
{
content: [
" if (true) {",
' console.log("level 1");',
' console.log("level 2");',
" }",
].join("\n"),
find: [
"if (true) {",
'console.log("level 1");',
' console.log("level 2");',
"}",
].join("\n"),
replace: ["if (true) {", 'console.log("updated");', "}"].join("\n"),
},
// replaceAll option cases
{
content: [
'console.log("test");',
'console.log("test");',
'console.log("test");',
].join("\n"),
find: 'console.log("test");',
replace: 'console.log("updated");',
all: true,
},
{
content: ['console.log("test");', 'console.log("test");'].join("\n"),
find: 'console.log("test");',
replace: 'console.log("updated");',
all: false,
},
// Error cases
{
content: 'console.log("hello");',
find: "nonexistent string",
replace: "updated",
fail: true,
},
{
content: ["test", "test", "different content", "test"].join("\n"),
find: "test",
replace: "updated",
all: false,
fail: true,
},
// Edge cases
{
content: "",
find: "",
replace: "new content",
},
{
content: "const regex = /[.*+?^${}()|[\\\\]\\\\\\\\]/g;",
find: "/[.*+?^${}()|[\\\\]\\\\\\\\]/g",
replace: "/\\\\w+/g",
},
{
content: 'const message = "Hello 世界! 🌍";',
find: "Hello 世界! 🌍",
replace: "Hello World! 🌎",
},
]
describe("EditTool Replacers", () => {
test.each(testCases)("case %#", (testCase) => {
if (testCase.fail) {
expect(() => {
replace(testCase.content, testCase.find, testCase.replace, testCase.all)
}).toThrow()
} else {
const result = replace(
testCase.content,
testCase.find,
testCase.replace,
testCase.all,
)
expect(result).toContain(testCase.replace)
}
})
})
+2 -2
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,
})
})
})
+1
View File
@@ -0,0 +1 @@
opencode-test
+5 -1
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) {
+77 -26
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 {
@@ -35,8 +35,7 @@ func (c *CommandCompletionProvider) GetEmptyMessage() string {
return "no matching commands"
}
func getCommandCompletionItem(cmd commands.Command, space int) dialog.CompletionItemI {
t := theme.CurrentTheme()
func getCommandCompletionItem(cmd commands.Command, space int, t theme.Theme) dialog.CompletionItemI {
spacer := strings.Repeat(" ", space)
title := " /" + cmd.Trigger + lipgloss.NewStyle().Foreground(t.TextMuted()).Render(spacer+cmd.Description)
value := string(cmd.Name)
@@ -47,6 +46,9 @@ func getCommandCompletionItem(cmd commands.Command, space int) dialog.Completion
}
func (c *CommandCompletionProvider) GetChildEntries(query string) ([]dialog.CompletionItemI, error) {
t := theme.CurrentTheme()
commands := c.app.Commands
space := 1
for _, cmd := range c.app.Commands {
if lipgloss.Width(cmd.Trigger) > space {
@@ -55,15 +57,16 @@ func (c *CommandCompletionProvider) GetChildEntries(query string) ([]dialog.Comp
}
space += 2
sorted := commands.Sorted()
if query == "" {
// If no query, return all commands
items := []dialog.CompletionItemI{}
for _, cmd := range c.app.Commands {
for _, cmd := range sorted {
if cmd.Trigger == "" {
continue
}
space := space - lipgloss.Width(cmd.Trigger)
items = append(items, getCommandCompletionItem(cmd, space))
items = append(items, getCommandCompletionItem(cmd, space, t))
}
return items, nil
}
@@ -72,13 +75,13 @@ func (c *CommandCompletionProvider) GetChildEntries(query string) ([]dialog.Comp
var commandNames []string
commandMap := make(map[string]dialog.CompletionItemI)
for _, cmd := range c.app.Commands {
for _, cmd := range sorted {
if cmd.Trigger == "" {
continue
}
space := space - lipgloss.Width(cmd.Trigger)
commandNames = append(commandNames, cmd.Trigger)
commandMap[cmd.Trigger] = getCommandCompletionItem(cmd, space)
commandMap[cmd.Trigger] = getCommandCompletionItem(cmd, space, t)
}
// Find fuzzy matches
+49 -20
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 {
@@ -54,6 +56,9 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
switch msg := msg.(type) {
case spinner.TickMsg:
m.spinner, cmd = m.spinner.Update(msg)
return m, cmd
case tea.KeyPressMsg:
// Maximize editor responsiveness for printable characters
if msg.Text != "" {
@@ -75,8 +80,15 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, tea.Batch(cmds...)
} else {
existingValue := m.textarea.Value()
modifiedValue := strings.Replace(existingValue, msg.SearchString, msg.CompletionValue, 1)
m.textarea.SetValue(modifiedValue + " ")
// Replace the current token (after last space)
lastSpaceIndex := strings.LastIndex(existingValue, " ")
if lastSpaceIndex == -1 {
m.textarea.SetValue(msg.CompletionValue + " ")
} else {
modifiedValue := existingValue[:lastSpaceIndex+1] + msg.CompletionValue
m.textarea.SetValue(modifiedValue + " ")
}
return m, nil
}
}
@@ -112,9 +124,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 := ""
@@ -214,8 +231,7 @@ func (m *editorComponent) Paste() (tea.Model, tea.Cmd) {
}
func (m *editorComponent) Newline() (tea.Model, tea.Cmd) {
value := m.textarea.Value()
m.textarea.SetValue(value + "\n")
m.textarea.Newline()
return m, nil
}
@@ -261,6 +277,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()
@@ -309,11 +337,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,
}
}
@@ -44,7 +44,6 @@ func toMarkdown(content string, width int, backgroundColor compat.AdaptiveColor)
}
}
}
content = strings.Join(lines, "\n")
return strings.TrimSuffix(content, "\n")
}
@@ -137,7 +136,7 @@ func renderContentBlock(content string, options ...renderingOption) string {
PaddingBottom(renderer.paddingBottom).
PaddingLeft(renderer.paddingLeft).
PaddingRight(renderer.paddingRight).
Background(t.BackgroundSubtle()).
Background(t.BackgroundPanel()).
Foreground(t.TextMuted()).
BorderStyle(lipgloss.ThickBorder())
@@ -146,7 +145,7 @@ func renderContentBlock(content string, options ...renderingOption) string {
align = *renderer.align
}
borderColor := t.BackgroundSubtle()
borderColor := t.BackgroundPanel()
if renderer.borderColor != nil {
borderColor = *renderer.borderColor
}
@@ -159,7 +158,7 @@ func renderContentBlock(content string, options ...renderingOption) string {
AlignHorizontal(align).
BorderLeftForeground(borderColor).
BorderLeftBackground(t.Background()).
BorderRightForeground(t.BackgroundSubtle()).
BorderRightForeground(t.BackgroundPanel()).
BorderRightBackground(t.Background())
case lipgloss.Right:
style = style.
@@ -168,7 +167,7 @@ func renderContentBlock(content string, options ...renderingOption) string {
AlignHorizontal(align).
BorderRightForeground(borderColor).
BorderRightBackground(t.Background()).
BorderLeftForeground(t.BackgroundSubtle()).
BorderLeftForeground(t.BackgroundPanel()).
BorderLeftBackground(t.Background())
}
@@ -227,9 +226,13 @@ 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
}
content := toMarkdown(text, markdownWidth, t.BackgroundSubtle())
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")
switch message.Role {
@@ -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,
@@ -274,7 +277,7 @@ func renderToolInvocation(
t := theme.CurrentTheme()
style := styles.Muted().
Width(outerWidth).
Background(t.BackgroundSubtle()).
Background(t.BackgroundPanel()).
PaddingTop(paddingTop).
PaddingBottom(paddingBottom).
PaddingLeft(2).
@@ -282,7 +285,7 @@ func renderToolInvocation(
BorderLeft(true).
BorderRight(true).
BorderBackground(t.Background()).
BorderForeground(t.BackgroundSubtle()).
BorderForeground(t.BackgroundPanel()).
BorderStyle(lipgloss.ThickBorder())
if toolCall.State == "partial-call" {
@@ -290,7 +293,7 @@ func renderToolInvocation(
if !showDetails {
title = "∟ " + title
padding := calculatePadding()
style := lipgloss.NewStyle().Width(outerWidth - padding - 4).Background(t.BackgroundSubtle())
style := lipgloss.NewStyle().Width(outerWidth - padding - 4).Background(t.BackgroundPanel())
return renderContentBlock(style.Render(title),
WithAlign(lipgloss.Left),
WithBorderColor(t.Accent()),
@@ -332,7 +335,7 @@ func renderToolInvocation(
if m, ok := metadata.Get("message"); ok {
style = style.BorderLeftForeground(t.Error())
error = styles.BaseStyle().
Background(t.BackgroundSubtle()).
Background(t.BackgroundPanel()).
Foreground(t.Error()).
Render(m.(string))
error = renderContentBlock(
@@ -374,7 +377,7 @@ func renderToolInvocation(
formattedDiff = lipgloss.NewStyle().
BorderStyle(lipgloss.ThickBorder()).
BorderBackground(t.Background()).
BorderForeground(t.BackgroundSubtle()).
BorderForeground(t.BackgroundPanel()).
BorderLeft(true).
BorderRight(true).
Render(formattedDiff)
@@ -410,19 +413,21 @@ func renderToolInvocation(
command := toolArgsMap["command"].(string)
stdout := stdout.(string)
body = fmt.Sprintf("```console\n> %s\n%s```", command, stdout)
body = toMarkdown(body, innerWidth, t.BackgroundSubtle())
body = toMarkdown(body, innerWidth, t.BackgroundPanel())
body = renderContentBlock(body, WithFullWidth(), WithMarginBottom(1))
}
case "webfetch":
toolArgs = renderArgs(&toolArgsMap, "url")
title = fmt.Sprintf("FETCH %s", toolArgs)
if format, ok := toolArgsMap["format"].(string); ok {
body = *result
body = truncateHeight(body, 10)
if format == "html" || format == "markdown" {
body = toMarkdown(body, innerWidth, t.BackgroundSubtle())
if result != nil {
body = *result
body = truncateHeight(body, 10)
if format == "html" || format == "markdown" {
body = toMarkdown(body, innerWidth, t.BackgroundPanel())
}
body = renderContentBlock(body, WithFullWidth(), WithMarginBottom(1))
}
body = renderContentBlock(body, WithFullWidth(), WithMarginBottom(1))
}
case "todowrite":
title = fmt.Sprintf("PLAN")
@@ -441,7 +446,7 @@ func renderToolInvocation(
body += fmt.Sprintf("- [ ] %s\n", content)
}
}
body = toMarkdown(body, innerWidth, t.BackgroundSubtle())
body = toMarkdown(body, innerWidth, t.BackgroundPanel())
body = renderContentBlock(body, WithFullWidth(), WithMarginBottom(1))
}
case "task":
@@ -461,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(
@@ -501,7 +506,7 @@ func renderToolInvocation(
if !showDetails {
title = "∟ " + title
padding := calculatePadding()
style := lipgloss.NewStyle().Width(outerWidth - padding - 4).Background(t.BackgroundSubtle())
style := lipgloss.NewStyle().Width(outerWidth - padding - 4).Background(t.BackgroundPanel())
paddingBottom := 0
if isLast {
paddingBottom = 1
@@ -590,7 +595,7 @@ func renderFile(filename string, content string, options ...fileRenderingOption)
content = truncateHeight(content, renderer.height)
}
content = fmt.Sprintf("```%s\n%s\n```", extension(renderer.filename), content)
content = toMarkdown(content, width, t.BackgroundSubtle())
content = toMarkdown(content, width, t.BackgroundPanel())
return renderContentBlock(content, WithFullWidth(), WithMarginBottom(1))
}
@@ -604,7 +609,7 @@ func renderToolAction(name string) string {
case "edit":
return "Preparing edit..."
case "webfetch":
return "Writing fetch..."
return "Fetching from the web..."
case "glob":
return "Finding files..."
case "grep":
@@ -58,19 +58,22 @@ 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()
m.renderView()
return m, nil
return m, m.Reload()
case ToggleToolDetailsMsg:
m.showToolDetails = !m.showToolDetails
m.renderView()
return m, nil
return m, m.Reload()
case app.SessionSelectedMsg:
m.cache.Clear()
cmd := m.Reload()
m.viewport.GotoBottom()
return m, cmd
m.tail = true
return m, m.Reload()
case app.SessionClearedMsg:
m.cache.Clear()
cmd := m.Reload()
@@ -174,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]
}
@@ -346,6 +349,7 @@ func (m *messagesComponent) home() string {
logoAndVersion,
lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
)
m.commands.SetBackgroundColor(t.Background())
commands := lipgloss.PlaceHorizontal(
m.width,
lipgloss.Center,
@@ -18,12 +18,14 @@ type CommandsComponent interface {
tea.Model
tea.ViewModel
layout.Sizeable
SetBackgroundColor(color compat.AdaptiveColor)
}
type commandsComponent struct {
app *app.App
width, height int
showKeybinds bool
showAll bool
background *compat.AdaptiveColor
limit *int
}
@@ -38,6 +40,10 @@ func (c *commandsComponent) GetSize() (int, int) {
return c.width, c.height
}
func (c *commandsComponent) SetBackgroundColor(color compat.AdaptiveColor) {
c.background = &color
}
func (c *commandsComponent) Init() tea.Cmd {
return nil
}
@@ -70,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")
}
@@ -96,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
@@ -139,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)
@@ -155,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
}
@@ -183,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)
@@ -116,7 +116,7 @@ func (c *completionDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case []CompletionItemI:
c.list.SetItems(msg)
case app.CompletionDialogTriggerdMsg:
case app.CompletionDialogTriggeredMsg:
c.pseudoSearchTextArea.SetValue(msg.InitialValue)
case tea.KeyMsg:
if c.pseudoSearchTextArea.Focused() {
@@ -173,7 +173,6 @@ func (c *completionDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
cmds = append(cmds, cmd)
cmds = append(cmds, c.pseudoSearchTextArea.Focus())
// c.pseudoSearchTextArea.SetValue(msg.String())
return c, tea.Batch(cmds...)
}
case tea.WindowSizeMsg:
@@ -218,12 +217,12 @@ func (c *completionDialogComponent) SetProvider(provider CompletionProvider) {
if c.completionProvider.GetId() != provider.GetId() {
c.completionProvider = provider
c.list.SetEmptyMessage(" " + provider.GetEmptyMessage())
c.list.SetItems([]CompletionItemI{})
}
}
func (c *completionDialogComponent) complete(item CompletionItemI) tea.Cmd {
value := c.pseudoSearchTextArea.Value()
if value == "" {
return nil
}
@@ -242,10 +241,8 @@ func (c *completionDialogComponent) complete(item CompletionItemI) tea.Cmd {
}
func (c *completionDialogComponent) close() tea.Cmd {
c.list.SetItems([]CompletionItemI{})
c.pseudoSearchTextArea.Reset()
c.pseudoSearchTextArea.Blur()
return util.CmdHandler(CompletionDialogCloseMsg{})
}
+43 -45
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,
}
}
+69 -151
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
}
@@ -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),
),
}
}
@@ -5,7 +5,6 @@ import (
list "github.com/sst/opencode/internal/components/list"
"github.com/sst/opencode/internal/components/modal"
"github.com/sst/opencode/internal/layout"
"github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme"
"github.com/sst/opencode/internal/util"
)
@@ -20,35 +19,14 @@ type ThemeDialog interface {
layout.Modal
}
type themeItem struct {
name string
}
func (t themeItem) Render(selected bool, width int) string {
th := theme.CurrentTheme()
baseStyle := styles.BaseStyle().
Width(width - 2).
Background(th.BackgroundElement())
if selected {
baseStyle = baseStyle.
Background(th.Primary()).
Foreground(th.BackgroundElement()).
Bold(true)
} else {
baseStyle = baseStyle.
Foreground(th.Text())
}
return baseStyle.Padding(0, 1).Render(t.name)
}
type themeDialog struct {
width int
height int
modal *modal.Modal
list list.List[themeItem]
modal *modal.Modal
list list.List[list.StringItem]
originalTheme string
themeApplied bool
}
func (t *themeDialog) Init() tea.Cmd {
@@ -64,26 +42,31 @@ func (t *themeDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg.String() {
case "enter":
if item, idx := t.list.GetSelectedItem(); idx >= 0 {
previousTheme := theme.CurrentThemeName()
selectedTheme := item.name
if previousTheme == selectedTheme {
return t, util.CmdHandler(modal.CloseModalMsg{})
}
selectedTheme := string(item)
if err := theme.SetTheme(selectedTheme); err != nil {
// status.Error(err.Error())
return t, nil
}
t.themeApplied = true
return t, tea.Sequence(
util.CmdHandler(modal.CloseModalMsg{}),
util.CmdHandler(ThemeSelectedMsg{ThemeName: selectedTheme}),
)
}
}
}
_, prevIdx := t.list.GetSelectedItem()
var cmd tea.Cmd
listModel, cmd := t.list.Update(msg)
t.list = listModel.(list.List[themeItem])
t.list = listModel.(list.List[list.StringItem])
if item, newIdx := t.list.GetSelectedItem(); newIdx >= 0 && newIdx != prevIdx {
theme.SetTheme(string(item))
return t, util.CmdHandler(ThemeSelectedMsg{ThemeName: string(item)})
}
return t, cmd
}
@@ -92,6 +75,10 @@ func (t *themeDialog) Render(background string) string {
}
func (t *themeDialog) Close() tea.Cmd {
if !t.themeApplied {
theme.SetTheme(t.originalTheme)
return util.CmdHandler(ThemeSelectedMsg{ThemeName: t.originalTheme})
}
return nil
}
@@ -100,17 +87,15 @@ func NewThemeDialog() ThemeDialog {
themes := theme.AvailableThemes()
currentTheme := theme.CurrentThemeName()
var themeItems []themeItem
var selectedIdx int
for i, name := range themes {
themeItems = append(themeItems, themeItem{name: name})
if name == currentTheme {
selectedIdx = i
}
}
list := list.NewListComponent(
themeItems,
list := list.NewStringList(
themes,
10, // maxVisibleThemes
"No themes available",
true,
@@ -118,9 +103,14 @@ func NewThemeDialog() ThemeDialog {
// Set the initial selection to the current theme
list.SetSelectedIndex(selectedIdx)
// Set the max width for the list to match the modal width
list.SetMaxWidth(36) // 40 (modal max width) - 4 (modal padding)
return &themeDialog{
list: list,
modal: modal.New(modal.WithTitle("Select Theme"), modal.WithMaxWidth(40)),
list: list,
modal: modal.New(modal.WithTitle("Select Theme"), modal.WithMaxWidth(40)),
originalTheme: currentTheme,
themeApplied: false,
}
}
@@ -441,10 +441,10 @@ func SyntaxHighlight(w io.Writer, source, fileName, formatter string, bg color.C
<entry type="TextWhitespace" style="%s"/>
</style>
`,
getColor(t.BackgroundSubtle()), // Background
getColor(t.Text()), // Text
getColor(t.Text()), // Other
getColor(t.Error()), // Error
getColor(t.BackgroundPanel()), // Background
getColor(t.Text()), // Text
getColor(t.Text()), // Other
getColor(t.Error()), // Error
getColor(t.SyntaxKeyword()), // Keyword
getColor(t.SyntaxKeyword()), // KeywordConstant
@@ -614,7 +614,7 @@ func applyHighlighting(content string, segments []Segment, segmentType LineType,
// Get the appropriate color based on terminal background
bgColor := lipgloss.Color(getColor(highlightBg))
fgColor := lipgloss.Color(getColor(theme.CurrentTheme().BackgroundSubtle()))
fgColor := lipgloss.Color(getColor(theme.CurrentTheme().BackgroundPanel()))
for i := 0; i < len(content); {
// Check if we're at an ANSI sequence
@@ -5,6 +5,10 @@ import (
"github.com/charmbracelet/bubbles/v2/key"
tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/lipgloss/v2"
"github.com/muesli/reflow/truncate"
"github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme"
)
type ListItem interface {
@@ -123,6 +127,9 @@ func (c *listComponent[T]) SetSelectedIndex(idx int) {
func (c *listComponent[T]) View() string {
items := c.items
maxWidth := c.maxWidth
if maxWidth == 0 {
maxWidth = 80 // Default width if not set
}
maxVisibleItems := min(c.maxVisibleItems, len(items))
startIdx := 0
@@ -161,3 +168,36 @@ func NewListComponent[T ListItem](items []T, maxVisibleItems int, fallbackMsg st
selectedIdx: 0,
}
}
// StringItem is a simple implementation of ListItem for string values
type StringItem string
func (s StringItem) Render(selected bool, width int) string {
t := theme.CurrentTheme()
baseStyle := styles.BaseStyle()
truncatedStr := truncate.StringWithTail(string(s), uint(width-1), "...")
var itemStyle lipgloss.Style
if selected {
itemStyle = baseStyle.
Background(t.Primary()).
Foreground(t.Background()).
Width(width).
PaddingLeft(1)
} else {
itemStyle = baseStyle.
PaddingLeft(1)
}
return itemStyle.Render(truncatedStr)
}
// NewStringList creates a new list component with string items
func NewStringList(items []string, maxVisibleItems int, fallbackMsg string, useAlphaNumericKeys bool) List[StringItem] {
stringItems := make([]StringItem, len(items))
for i, item := range items {
stringItems[i] = StringItem(item)
}
return NewListComponent(stringItems, maxVisibleItems, fallbackMsg, useAlphaNumericKeys)
}
@@ -103,18 +103,18 @@ func (m *Modal) Render(contentView string, background string) string {
Bold(true).
Padding(0, 1)
escStyle := baseStyle.Foreground(t.TextMuted()).Bold(false)
escStyle := baseStyle.Foreground(t.TextMuted())
escText := escStyle.Render("esc")
// Calculate position for esc text
titleWidth := lipgloss.Width(m.title)
escWidth := lipgloss.Width(escText)
spacesNeeded := max(0, innerWidth-titleWidth-escWidth-3)
spacesNeeded := max(0, innerWidth-titleWidth-escWidth-2)
spacer := strings.Repeat(" ", spacesNeeded)
titleLine := m.title + spacer + escText
titleLine = titleStyle.Render(titleLine)
finalContent = strings.Join([]string{titleLine, contentView}, "\n") + "\n"
finalContent = strings.Join([]string{titleLine, "", contentView}, "\n")
} else {
finalContent = contentView
}
@@ -71,7 +71,7 @@ func formatTokensAndCost(tokens float32, contextWindow float32, cost float32) st
formattedCost := fmt.Sprintf("$%.2f", cost)
percentage := (float64(tokens) / float64(contextWindow)) * 100
return fmt.Sprintf("Tokens: %s (%d%%), Cost: %s", formattedTokens, int(percentage), formattedCost)
return fmt.Sprintf("Context: %s (%d%%), Cost: %s", formattedTokens, int(percentage), formattedCost)
}
func (m statusComponent) View() string {
@@ -88,7 +88,7 @@ func (m statusComponent) View() string {
cwd := styles.Padded().
Foreground(t.TextMuted()).
Background(t.BackgroundSubtle()).
Background(t.BackgroundPanel()).
Render(m.app.Info.Path.Cwd)
sessionInfo := ""
@@ -123,7 +123,7 @@ func (m statusComponent) View() string {
0,
m.width-lipgloss.Width(logo)-lipgloss.Width(cwd)-lipgloss.Width(sessionInfo),
)
spacer := lipgloss.NewStyle().Background(t.BackgroundSubtle()).Width(space).Render("")
spacer := lipgloss.NewStyle().Background(t.BackgroundPanel()).Width(space).Render("")
status := logo + cwd + spacer + sessionInfo
@@ -553,6 +553,14 @@ func (m Model) Line() int {
return m.row
}
func (m *Model) Newline() {
if m.MaxHeight > 0 && len(m.value) >= m.MaxHeight {
return
}
m.col = clamp(m.col, 0, len(m.value[m.row]))
m.splitLine(m.row, m.col)
}
// CursorDown moves the cursor down by one line.
// Returns whether or not the cursor blink should be reset.
func (m *Model) CursorDown() {
@@ -1113,11 +1121,7 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
}
m.deleteWordRight()
case key.Matches(msg, m.KeyMap.InsertNewline):
if m.MaxHeight > 0 && len(m.value) >= m.MaxHeight {
return m, nil
}
m.col = clamp(m.col, 0, len(m.value[m.row]))
m.splitLine(m.row, m.col)
m.Newline()
case key.Matches(msg, m.KeyMap.LineEnd):
m.CursorEnd()
case key.Matches(msg, m.KeyMap.LineStart):
@@ -1161,8 +1165,6 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
}
var cmd tea.Cmd
cmds = append(cmds, cmd)
newRow, newCol := m.cursorLineNumber(), m.col
m.virtualCursor, cmd = m.virtualCursor.Update(msg)
if (newRow != oldRow || newCol != oldCol) && m.virtualCursor.Mode() == cursor.CursorBlink {
@@ -1171,7 +1173,6 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
}
cmds = append(cmds, cmd)
m.SetHeight(m.ContentHeight())
return m, tea.Batch(cmds...)
}
@@ -5,8 +5,8 @@ package image
import (
"bytes"
"fmt"
"image"
"github.com/atotto/clipboard"
"image"
)
func GetImageFromClipboard() ([]byte, string, error) {
@@ -28,8 +28,6 @@ func GetImageFromClipboard() ([]byte, string, error) {
}
func binaryToImage(data []byte) ([]byte, error) {
reader := bytes.NewReader(data)
img, _, err := image.Decode(reader)
@@ -40,7 +38,6 @@ func binaryToImage(data []byte) ([]byte, error) {
return ImageToBytes(img)
}
func min(a, b int) int {
if a < b {
return a
+52 -36
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
}
+4 -4
View File
@@ -15,7 +15,7 @@ func BaseStyle() lipgloss.Style {
func Panel() lipgloss.Style {
t := theme.CurrentTheme()
return lipgloss.NewStyle().
Background(t.BackgroundSubtle()).
Background(t.BackgroundPanel()).
Border(lipgloss.NormalBorder(), true, false, true, false).
BorderForeground(t.BorderSubtle()).
Foreground(t.Text())
@@ -131,9 +131,9 @@ func BackgroundColor() compat.AdaptiveColor {
return theme.CurrentTheme().Background()
}
// BackgroundSubtleColor returns the subtle background color from the current theme
func BackgroundSubtleColor() compat.AdaptiveColor {
return theme.CurrentTheme().BackgroundSubtle()
// BackgroundPanelColor returns the subtle background color from the current theme
func BackgroundPanelColor() compat.AdaptiveColor {
return theme.CurrentTheme().BackgroundPanel()
}
// BackgroundElementColor returns the darker background color from the current theme
-276
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.BackgroundSubtleColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkBgAlt),
Light: lipgloss.Color(darkBgAlt),
}
theme.BackgroundElementColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkPanel),
Light: lipgloss.Color(darkPanel),
}
// Border colors
theme.BorderColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkGutter),
Light: lipgloss.Color(darkGutter),
}
theme.BorderActiveColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkIndentActive),
Light: lipgloss.Color(darkIndentActive),
}
theme.BorderSubtleColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkLine),
Light: lipgloss.Color(darkLine),
}
// Diff view colors
theme.DiffAddedColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkAdded),
Light: lipgloss.Color(darkAdded),
}
theme.DiffRemovedColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkRemoved),
Light: lipgloss.Color(darkRemoved),
}
theme.DiffContextColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkFgMuted),
Light: lipgloss.Color(darkFgMuted),
}
theme.DiffHunkHeaderColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkGutter),
Light: lipgloss.Color(darkGutter),
}
theme.DiffHighlightAddedColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkAdded),
Light: lipgloss.Color(darkAdded),
}
theme.DiffHighlightRemovedColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkRemoved),
Light: lipgloss.Color(darkRemoved),
}
theme.DiffAddedBgColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#1a2b1a"),
Light: lipgloss.Color("#1a2b1a"),
}
theme.DiffRemovedBgColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#2b1a1a"),
Light: lipgloss.Color("#2b1a1a"),
}
theme.DiffContextBgColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkBgAlt),
Light: lipgloss.Color(darkBgAlt),
}
theme.DiffLineNumberColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkGutter),
Light: lipgloss.Color(darkGutter),
}
theme.DiffAddedLineNumberBgColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#152b15"),
Light: lipgloss.Color("#152b15"),
}
theme.DiffRemovedLineNumberBgColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#2b1515"),
Light: lipgloss.Color("#2b1515"),
}
// Markdown colors
theme.MarkdownTextColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkFg),
Light: lipgloss.Color(darkFg),
}
theme.MarkdownHeadingColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkFunc),
Light: lipgloss.Color(darkFunc),
}
theme.MarkdownLinkColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkTag),
Light: lipgloss.Color(darkTag),
}
theme.MarkdownLinkTextColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkEntity),
Light: lipgloss.Color(darkEntity),
}
theme.MarkdownCodeColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkString),
Light: lipgloss.Color(darkString),
}
theme.MarkdownBlockQuoteColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkSpecial),
Light: lipgloss.Color(darkSpecial),
}
theme.MarkdownEmphColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkKeyword),
Light: lipgloss.Color(darkKeyword),
}
theme.MarkdownStrongColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkMarkup),
Light: lipgloss.Color(darkMarkup),
}
theme.MarkdownHorizontalRuleColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkGutter),
Light: lipgloss.Color(darkGutter),
}
theme.MarkdownListItemColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkOperator),
Light: lipgloss.Color(darkOperator),
}
theme.MarkdownListEnumerationColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkConstant),
Light: lipgloss.Color(darkConstant),
}
theme.MarkdownImageColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkRegexp),
Light: lipgloss.Color(darkRegexp),
}
theme.MarkdownImageTextColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkEntity),
Light: lipgloss.Color(darkEntity),
}
theme.MarkdownCodeBlockColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkString),
Light: lipgloss.Color(darkString),
}
// Syntax highlighting colors
theme.SyntaxCommentColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkComment),
Light: lipgloss.Color(darkComment),
}
theme.SyntaxKeywordColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkKeyword),
Light: lipgloss.Color(darkKeyword),
}
theme.SyntaxFunctionColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkFunc),
Light: lipgloss.Color(darkFunc),
}
theme.SyntaxVariableColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkEntity),
Light: lipgloss.Color(darkEntity),
}
theme.SyntaxStringColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkString),
Light: lipgloss.Color(darkString),
}
theme.SyntaxNumberColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkConstant),
Light: lipgloss.Color(darkConstant),
}
theme.SyntaxTypeColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkSpecial),
Light: lipgloss.Color(darkSpecial),
}
theme.SyntaxOperatorColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkOperator),
Light: lipgloss.Color(darkOperator),
}
theme.SyntaxPunctuationColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkFg),
Light: lipgloss.Color(darkFg),
}
return theme
}
func init() {
// Register the Ayu theme with the theme manager
RegisterTheme("ayu", NewAyuTheme())
}
-298
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.BackgroundSubtleColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep2),
Light: lipgloss.Color(lightStep2),
}
theme.BackgroundElementColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep3),
Light: lipgloss.Color(lightStep3),
}
// Border colors
theme.BorderColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep7),
Light: lipgloss.Color(lightStep7),
}
theme.BorderActiveColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep8),
Light: lipgloss.Color(lightStep8),
}
theme.BorderSubtleColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep6),
Light: lipgloss.Color(lightStep6),
}
// Diff view colors
theme.DiffAddedColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#A7C080"),
Light: lipgloss.Color("#8DA101"),
}
theme.DiffRemovedColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#E67E80"),
Light: lipgloss.Color("#F85552"),
}
theme.DiffContextColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#7A8478"),
Light: lipgloss.Color("#A6B0A0"),
}
theme.DiffHunkHeaderColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#859289"),
Light: lipgloss.Color("#939F91"),
}
theme.DiffHighlightAddedColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#A7C080"),
Light: lipgloss.Color("#8DA101"),
}
theme.DiffHighlightRemovedColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#E67E80"),
Light: lipgloss.Color("#F85552"),
}
theme.DiffAddedBgColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#425047"),
Light: lipgloss.Color("#F0F1D2"),
}
theme.DiffRemovedBgColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#543A48"),
Light: lipgloss.Color("#FBE3DA"),
}
theme.DiffContextBgColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep2),
Light: lipgloss.Color(lightStep2),
}
theme.DiffLineNumberColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep3),
Light: lipgloss.Color(lightStep3),
}
theme.DiffAddedLineNumberBgColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#3A4A3F"),
Light: lipgloss.Color("#E8F2D1"),
}
theme.DiffRemovedLineNumberBgColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#4A3A40"),
Light: lipgloss.Color("#FBDAD2"),
}
// Markdown colors
theme.MarkdownTextColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep12),
Light: lipgloss.Color(lightStep12),
}
theme.MarkdownHeadingColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkSecondary),
Light: lipgloss.Color(lightSecondary),
}
theme.MarkdownLinkColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkPrimary),
Light: lipgloss.Color(lightPrimary),
}
theme.MarkdownLinkTextColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkCyan),
Light: lipgloss.Color(lightCyan),
}
theme.MarkdownCodeColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkGreen),
Light: lipgloss.Color(lightGreen),
}
theme.MarkdownBlockQuoteColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkYellow),
Light: lipgloss.Color(lightYellow),
}
theme.MarkdownEmphColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkYellow),
Light: lipgloss.Color(lightYellow),
}
theme.MarkdownStrongColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkAccent),
Light: lipgloss.Color(lightAccent),
}
theme.MarkdownHorizontalRuleColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep11),
Light: lipgloss.Color(lightStep11),
}
theme.MarkdownListItemColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkPrimary),
Light: lipgloss.Color(lightPrimary),
}
theme.MarkdownListEnumerationColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkCyan),
Light: lipgloss.Color(lightCyan),
}
theme.MarkdownImageColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkPrimary),
Light: lipgloss.Color(lightPrimary),
}
theme.MarkdownImageTextColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkCyan),
Light: lipgloss.Color(lightCyan),
}
theme.MarkdownCodeBlockColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep12),
Light: lipgloss.Color(lightStep12),
}
// Syntax highlighting colors
theme.SyntaxCommentColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep11),
Light: lipgloss.Color(lightStep11),
}
theme.SyntaxKeywordColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkPrimary),
Light: lipgloss.Color(lightPrimary),
}
theme.SyntaxFunctionColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkSecondary),
Light: lipgloss.Color(lightSecondary),
}
theme.SyntaxVariableColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkRed),
Light: lipgloss.Color(lightRed),
}
theme.SyntaxStringColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkGreen),
Light: lipgloss.Color(lightGreen),
}
theme.SyntaxNumberColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkAccent),
Light: lipgloss.Color(lightAccent),
}
theme.SyntaxTypeColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkYellow),
Light: lipgloss.Color(lightYellow),
}
theme.SyntaxOperatorColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkCyan),
Light: lipgloss.Color(lightCyan),
}
theme.SyntaxPunctuationColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep12),
Light: lipgloss.Color(lightStep12),
}
return theme
}
func init() {
// Register the Everforest theme with the theme manager
RegisterTheme("everforest", NewEverforestTheme())
}
+395
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
}
+135
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")
}
}
+1 -132
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.BackgroundSubtleColor = adaptiveColor
case "backgroundelement":
theme.BackgroundElementColor = adaptiveColor
case "border":
theme.BorderColor = adaptiveColor
case "borderactive":
theme.BorderActiveColor = adaptiveColor
case "bordersubtle":
theme.BorderSubtleColor = adaptiveColor
case "diffadded":
theme.DiffAddedColor = adaptiveColor
case "diffremoved":
theme.DiffRemovedColor = adaptiveColor
case "diffcontext":
theme.DiffContextColor = adaptiveColor
case "diffhunkheader":
theme.DiffHunkHeaderColor = adaptiveColor
case "diffhighlightadded":
theme.DiffHighlightAddedColor = adaptiveColor
case "diffhighlightremoved":
theme.DiffHighlightRemovedColor = adaptiveColor
case "diffaddedbg":
theme.DiffAddedBgColor = adaptiveColor
case "diffremovedbg":
theme.DiffRemovedBgColor = adaptiveColor
case "diffcontextbg":
theme.DiffContextBgColor = adaptiveColor
case "difflinenumber":
theme.DiffLineNumberColor = adaptiveColor
case "diffaddedlinenumberbg":
theme.DiffAddedLineNumberBgColor = adaptiveColor
case "diffremovedlinenumberbg":
theme.DiffRemovedLineNumberBgColor = adaptiveColor
case "syntaxcomment":
theme.SyntaxCommentColor = adaptiveColor
case "syntaxkeyword":
theme.SyntaxKeywordColor = adaptiveColor
case "syntaxfunction":
theme.SyntaxFunctionColor = adaptiveColor
case "syntaxvariable":
theme.SyntaxVariableColor = adaptiveColor
case "syntaxstring":
theme.SyntaxStringColor = adaptiveColor
case "syntaxnumber":
theme.SyntaxNumberColor = adaptiveColor
case "syntaxtype":
theme.SyntaxTypeColor = adaptiveColor
case "syntaxoperator":
theme.SyntaxOperatorColor = adaptiveColor
case "syntaxpunctuation":
theme.SyntaxPunctuationColor = adaptiveColor
case "markdowntext":
theme.MarkdownTextColor = adaptiveColor
case "markdownheading":
theme.MarkdownHeadingColor = adaptiveColor
case "markdownlink":
theme.MarkdownLinkColor = adaptiveColor
case "markdownlinktext":
theme.MarkdownLinkTextColor = adaptiveColor
case "markdowncode":
theme.MarkdownCodeColor = adaptiveColor
case "markdownblockquote":
theme.MarkdownBlockQuoteColor = adaptiveColor
case "markdownemph":
theme.MarkdownEmphColor = adaptiveColor
case "markdownstrong":
theme.MarkdownStrongColor = adaptiveColor
case "markdownhorizontalrule":
theme.MarkdownHorizontalRuleColor = adaptiveColor
case "markdownlistitem":
theme.MarkdownListItemColor = adaptiveColor
case "markdownlistitemenum":
theme.MarkdownListEnumerationColor = adaptiveColor
case "markdownimage":
theme.MarkdownImageColor = adaptiveColor
case "markdownimagetext":
theme.MarkdownImageTextColor = adaptiveColor
case "markdowncodeblock":
theme.MarkdownCodeBlockColor = adaptiveColor
case "markdownlistenumeration":
theme.MarkdownListEnumerationColor = adaptiveColor
default:
slog.Warn("Unknown color key in custom theme", "key", key)
}
}
return theme, nil
}
-297
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.BackgroundSubtleColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep2),
Light: lipgloss.Color(lightStep2),
}
theme.BackgroundElementColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep3),
Light: lipgloss.Color(lightStep3),
}
// Border colors
theme.BorderColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep7),
Light: lipgloss.Color(lightStep7),
}
theme.BorderActiveColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep8),
Light: lipgloss.Color(lightStep8),
}
theme.BorderSubtleColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep6),
Light: lipgloss.Color(lightStep6),
}
// Diff view colors
theme.DiffAddedColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#478247"),
Light: lipgloss.Color("#2E7D32"),
}
theme.DiffRemovedColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#7C4444"),
Light: lipgloss.Color("#C62828"),
}
theme.DiffContextColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#a0a0a0"),
Light: lipgloss.Color("#757575"),
}
theme.DiffHunkHeaderColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#a0a0a0"),
Light: lipgloss.Color("#757575"),
}
theme.DiffHighlightAddedColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#DAFADA"),
Light: lipgloss.Color("#A5D6A7"),
}
theme.DiffHighlightRemovedColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#FADADD"),
Light: lipgloss.Color("#EF9A9A"),
}
theme.DiffAddedBgColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#303A30"),
Light: lipgloss.Color("#E8F5E9"),
}
theme.DiffRemovedBgColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#3A3030"),
Light: lipgloss.Color("#FFEBEE"),
}
theme.DiffContextBgColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep2),
Light: lipgloss.Color(lightStep2),
}
theme.DiffLineNumberColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep3),
Light: lipgloss.Color(lightStep3),
}
theme.DiffAddedLineNumberBgColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#293229"),
Light: lipgloss.Color("#C8E6C9"),
}
theme.DiffRemovedLineNumberBgColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#332929"),
Light: lipgloss.Color("#FFCDD2"),
}
// Markdown colors
theme.MarkdownTextColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep12),
Light: lipgloss.Color(lightStep12),
}
theme.MarkdownHeadingColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkSecondary),
Light: lipgloss.Color(lightSecondary),
}
theme.MarkdownLinkColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkPrimary),
Light: lipgloss.Color(lightPrimary),
}
theme.MarkdownLinkTextColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkCyan),
Light: lipgloss.Color(lightCyan),
}
theme.MarkdownCodeColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkGreen),
Light: lipgloss.Color(lightGreen),
}
theme.MarkdownBlockQuoteColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkYellow),
Light: lipgloss.Color(lightYellow),
}
theme.MarkdownEmphColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkYellow),
Light: lipgloss.Color(lightYellow),
}
theme.MarkdownStrongColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkAccent),
Light: lipgloss.Color(lightAccent),
}
theme.MarkdownHorizontalRuleColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep11),
Light: lipgloss.Color(lightStep11),
}
theme.MarkdownListItemColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkPrimary),
Light: lipgloss.Color(lightPrimary),
}
theme.MarkdownListEnumerationColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkCyan),
Light: lipgloss.Color(lightCyan),
}
theme.MarkdownImageColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkPrimary),
Light: lipgloss.Color(lightPrimary),
}
theme.MarkdownImageTextColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkCyan),
Light: lipgloss.Color(lightCyan),
}
theme.MarkdownCodeBlockColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep12),
Light: lipgloss.Color(lightStep12),
}
// Syntax highlighting colors
theme.SyntaxCommentColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep11),
Light: lipgloss.Color(lightStep11),
}
theme.SyntaxKeywordColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkPrimary),
Light: lipgloss.Color(lightPrimary),
}
theme.SyntaxFunctionColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkPrimary),
Light: lipgloss.Color(lightPrimary),
}
theme.SyntaxVariableColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkRed),
Light: lipgloss.Color(lightRed),
}
theme.SyntaxStringColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkGreen),
Light: lipgloss.Color(lightGreen),
}
theme.SyntaxNumberColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkAccent),
Light: lipgloss.Color(lightAccent),
}
theme.SyntaxTypeColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkYellow),
Light: lipgloss.Color(lightYellow),
}
theme.SyntaxOperatorColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkCyan),
Light: lipgloss.Color(lightCyan),
}
theme.SyntaxPunctuationColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep12),
Light: lipgloss.Color(lightStep12),
}
return theme
}
func init() {
// Register the OpenCode theme with the theme manager
RegisterTheme("opencode", NewOpenCodeTheme())
}
+3 -78
View File
@@ -1,10 +1,6 @@
package theme
import (
"fmt"
"regexp"
"github.com/charmbracelet/lipgloss/v2"
"github.com/charmbracelet/lipgloss/v2/compat"
)
@@ -14,7 +10,7 @@ import (
type Theme interface {
// Background colors
Background() compat.AdaptiveColor // Radix 1
BackgroundSubtle() compat.AdaptiveColor // Radix 2
BackgroundPanel() compat.AdaptiveColor // Radix 2
BackgroundElement() compat.AdaptiveColor // Radix 3
// Border colors
@@ -84,7 +80,7 @@ type Theme interface {
type BaseTheme struct {
// Background colors
BackgroundColor compat.AdaptiveColor
BackgroundSubtleColor compat.AdaptiveColor
BackgroundPanelColor compat.AdaptiveColor
BackgroundElementColor compat.AdaptiveColor
// Border colors
@@ -163,7 +159,7 @@ func (t *BaseTheme) Text() compat.AdaptiveColor { return t.TextColor }
func (t *BaseTheme) TextMuted() compat.AdaptiveColor { return t.TextMutedColor }
func (t *BaseTheme) Background() compat.AdaptiveColor { return t.BackgroundColor }
func (t *BaseTheme) BackgroundSubtle() compat.AdaptiveColor { return t.BackgroundSubtleColor }
func (t *BaseTheme) BackgroundPanel() compat.AdaptiveColor { return t.BackgroundPanelColor }
func (t *BaseTheme) BackgroundElement() compat.AdaptiveColor { return t.BackgroundElementColor }
func (t *BaseTheme) Border() compat.AdaptiveColor { return t.BorderColor }
@@ -215,74 +211,3 @@ func (t *BaseTheme) SyntaxNumber() compat.AdaptiveColor { return t.SyntaxNu
func (t *BaseTheme) SyntaxType() compat.AdaptiveColor { return t.SyntaxTypeColor }
func (t *BaseTheme) SyntaxOperator() compat.AdaptiveColor { return t.SyntaxOperatorColor }
func (t *BaseTheme) SyntaxPunctuation() compat.AdaptiveColor { return t.SyntaxPunctuationColor }
// ParseAdaptiveColor parses a color value from the config file into a compat.AdaptiveColor.
// It accepts either a string (hex color) or a map with "dark" and "light" keys.
func ParseAdaptiveColor(value any) (compat.AdaptiveColor, error) {
// Regular expression to validate hex color format
hexColorRegex := regexp.MustCompile(`^#[0-9a-fA-F]{6}$`)
// Case 1: String value (same color for both dark and light modes)
if hexColor, ok := value.(string); ok {
if !hexColorRegex.MatchString(hexColor) {
return compat.AdaptiveColor{}, fmt.Errorf("invalid hex color format: %s", hexColor)
}
return compat.AdaptiveColor{
Dark: lipgloss.Color(hexColor),
Light: lipgloss.Color(hexColor),
}, nil
}
// Case 2: Int value between 0 and 255
if numericVal, ok := value.(float64); ok {
intVal := int(numericVal)
if intVal < 0 || intVal > 255 {
return compat.AdaptiveColor{}, fmt.Errorf("invalid int color value (must be between 0 and 255): %d", intVal)
}
return compat.AdaptiveColor{
Dark: lipgloss.Color(fmt.Sprintf("%d", intVal)),
Light: lipgloss.Color(fmt.Sprintf("%d", intVal)),
}, nil
}
// Case 3: Map with dark and light keys
if colorMap, ok := value.(map[string]any); ok {
darkVal, darkOk := colorMap["dark"]
lightVal, lightOk := colorMap["light"]
if !darkOk || !lightOk {
return compat.AdaptiveColor{}, fmt.Errorf("color map must contain both 'dark' and 'light' keys")
}
darkHex, darkIsString := darkVal.(string)
lightHex, lightIsString := lightVal.(string)
if !darkIsString || !lightIsString {
darkVal, darkIsNumber := darkVal.(float64)
lightVal, lightIsNumber := lightVal.(float64)
if !darkIsNumber || !lightIsNumber {
return compat.AdaptiveColor{}, fmt.Errorf("color map values must be strings or ints")
}
darkInt := int(darkVal)
lightInt := int(lightVal)
return compat.AdaptiveColor{
Dark: lipgloss.Color(fmt.Sprintf("%d", darkInt)),
Light: lipgloss.Color(fmt.Sprintf("%d", lightInt)),
}, nil
}
if !hexColorRegex.MatchString(darkHex) || !hexColorRegex.MatchString(lightHex) {
return compat.AdaptiveColor{}, fmt.Errorf("invalid hex color format")
}
return compat.AdaptiveColor{
Dark: lipgloss.Color(darkHex),
Light: lipgloss.Color(lightHex),
}, nil
}
return compat.AdaptiveColor{}, fmt.Errorf("color must be either a hex string or an object with dark/light keys")
}
@@ -0,0 +1,81 @@
{
"$schema": "https://opencode.ai/theme.json",
"defs": {
"darkBg": "#0B0E14",
"darkBgAlt": "#0D1017",
"darkLine": "#11151C",
"darkPanel": "#0F131A",
"darkFg": "#BFBDB6",
"darkFgMuted": "#565B66",
"darkGutter": "#6C7380",
"darkTag": "#39BAE6",
"darkFunc": "#FFB454",
"darkEntity": "#59C2FF",
"darkString": "#AAD94C",
"darkRegexp": "#95E6CB",
"darkMarkup": "#F07178",
"darkKeyword": "#FF8F40",
"darkSpecial": "#E6B673",
"darkComment": "#ACB6BF",
"darkConstant": "#D2A6FF",
"darkOperator": "#F29668",
"darkAdded": "#7FD962",
"darkRemoved": "#F26D78",
"darkAccent": "#E6B450",
"darkError": "#D95757",
"darkIndentActive": "#6C7380"
},
"theme": {
"primary": "darkEntity",
"secondary": "darkConstant",
"accent": "darkAccent",
"error": "darkError",
"warning": "darkSpecial",
"success": "darkAdded",
"info": "darkTag",
"text": "darkFg",
"textMuted": "darkFgMuted",
"background": "darkBg",
"backgroundPanel": "darkPanel",
"backgroundElement": "darkBgAlt",
"border": "darkGutter",
"borderActive": "darkIndentActive",
"borderSubtle": "darkLine",
"diffAdded": "darkAdded",
"diffRemoved": "darkRemoved",
"diffContext": "darkComment",
"diffHunkHeader": "darkComment",
"diffHighlightAdded": "darkString",
"diffHighlightRemoved": "darkMarkup",
"diffAddedBg": "#20303b",
"diffRemovedBg": "#37222c",
"diffContextBg": "darkPanel",
"diffLineNumber": "darkGutter",
"diffAddedLineNumberBg": "#1b2b34",
"diffRemovedLineNumberBg": "#2d1f26",
"markdownText": "darkFg",
"markdownHeading": "darkConstant",
"markdownLink": "darkEntity",
"markdownLinkText": "darkTag",
"markdownCode": "darkString",
"markdownBlockQuote": "darkSpecial",
"markdownEmph": "darkSpecial",
"markdownStrong": "darkFunc",
"markdownHorizontalRule": "darkFgMuted",
"markdownListItem": "darkEntity",
"markdownListEnumeration": "darkTag",
"markdownImage": "darkEntity",
"markdownImageText": "darkTag",
"markdownCodeBlock": "darkFg",
"syntaxComment": "darkComment",
"syntaxKeyword": "darkKeyword",
"syntaxFunction": "darkFunc",
"syntaxVariable": "darkEntity",
"syntaxString": "darkString",
"syntaxNumber": "darkConstant",
"syntaxType": "darkSpecial",
"syntaxOperator": "darkOperator",
"syntaxPunctuation": "darkFg"
}
}
@@ -0,0 +1,113 @@
{
"$schema": "https://opencode.ai/theme.json",
"defs": {
"lightRosewater": "#dc8a78",
"lightFlamingo": "#dd7878",
"lightPink": "#ea76cb",
"lightMauve": "#8839ef",
"lightRed": "#d20f39",
"lightMaroon": "#e64553",
"lightPeach": "#fe640b",
"lightYellow": "#df8e1d",
"lightGreen": "#40a02b",
"lightTeal": "#179299",
"lightSky": "#04a5e5",
"lightSapphire": "#209fb5",
"lightBlue": "#1e66f5",
"lightLavender": "#7287fd",
"lightText": "#4c4f69",
"lightSubtext1": "#5c5f77",
"lightSubtext0": "#6c6f85",
"lightOverlay2": "#7c7f93",
"lightOverlay1": "#8c8fa1",
"lightOverlay0": "#9ca0b0",
"lightSurface2": "#acb0be",
"lightSurface1": "#bcc0cc",
"lightSurface0": "#ccd0da",
"lightBase": "#eff1f5",
"lightMantle": "#e6e9ef",
"lightCrust": "#dce0e8",
"darkRosewater": "#f5e0dc",
"darkFlamingo": "#f2cdcd",
"darkPink": "#f5c2e7",
"darkMauve": "#cba6f7",
"darkRed": "#f38ba8",
"darkMaroon": "#eba0ac",
"darkPeach": "#fab387",
"darkYellow": "#f9e2af",
"darkGreen": "#a6e3a1",
"darkTeal": "#94e2d5",
"darkSky": "#89dceb",
"darkSapphire": "#74c7ec",
"darkBlue": "#89b4fa",
"darkLavender": "#b4befe",
"darkText": "#cdd6f4",
"darkSubtext1": "#bac2de",
"darkSubtext0": "#a6adc8",
"darkOverlay2": "#9399b2",
"darkOverlay1": "#7f849c",
"darkOverlay0": "#6c7086",
"darkSurface2": "#585b70",
"darkSurface1": "#45475a",
"darkSurface0": "#313244",
"darkBase": "#1e1e2e",
"darkMantle": "#181825",
"darkCrust": "#11111b"
},
"theme": {
"primary": { "dark": "darkBlue", "light": "lightBlue" },
"secondary": { "dark": "darkMauve", "light": "lightMauve" },
"accent": { "dark": "darkPink", "light": "lightPink" },
"error": { "dark": "darkRed", "light": "lightRed" },
"warning": { "dark": "darkYellow", "light": "lightYellow" },
"success": { "dark": "darkGreen", "light": "lightGreen" },
"info": { "dark": "darkTeal", "light": "lightTeal" },
"text": { "dark": "darkText", "light": "lightText" },
"textMuted": { "dark": "darkSubtext1", "light": "lightSubtext1" },
"background": { "dark": "darkBase", "light": "lightBase" },
"backgroundPanel": { "dark": "darkMantle", "light": "lightMantle" },
"backgroundElement": { "dark": "darkCrust", "light": "lightCrust" },
"border": { "dark": "darkSurface0", "light": "lightSurface0" },
"borderActive": { "dark": "darkSurface1", "light": "lightSurface1" },
"borderSubtle": { "dark": "darkSurface2", "light": "lightSurface2" },
"diffAdded": { "dark": "darkGreen", "light": "lightGreen" },
"diffRemoved": { "dark": "darkRed", "light": "lightRed" },
"diffContext": { "dark": "darkOverlay2", "light": "lightOverlay2" },
"diffHunkHeader": { "dark": "darkPeach", "light": "lightPeach" },
"diffHighlightAdded": { "dark": "darkGreen", "light": "lightGreen" },
"diffHighlightRemoved": { "dark": "darkRed", "light": "lightRed" },
"diffAddedBg": { "dark": "#24312b", "light": "#d6f0d9" },
"diffRemovedBg": { "dark": "#3c2a32", "light": "#f6dfe2" },
"diffContextBg": { "dark": "darkMantle", "light": "lightMantle" },
"diffLineNumber": { "dark": "darkSurface1", "light": "lightSurface1" },
"diffAddedLineNumberBg": { "dark": "#1e2a25", "light": "#c9e3cb" },
"diffRemovedLineNumberBg": { "dark": "#32232a", "light": "#e9d3d6" },
"markdownText": { "dark": "darkText", "light": "lightText" },
"markdownHeading": { "dark": "darkMauve", "light": "lightMauve" },
"markdownLink": { "dark": "darkBlue", "light": "lightBlue" },
"markdownLinkText": { "dark": "darkSky", "light": "lightSky" },
"markdownCode": { "dark": "darkGreen", "light": "lightGreen" },
"markdownBlockQuote": { "dark": "darkYellow", "light": "lightYellow" },
"markdownEmph": { "dark": "darkYellow", "light": "lightYellow" },
"markdownStrong": { "dark": "darkPeach", "light": "lightPeach" },
"markdownHorizontalRule": {
"dark": "darkSubtext0",
"light": "lightSubtext0"
},
"markdownListItem": { "dark": "darkBlue", "light": "lightBlue" },
"markdownListEnumeration": { "dark": "darkSky", "light": "lightSky" },
"markdownImage": { "dark": "darkBlue", "light": "lightBlue" },
"markdownImageText": { "dark": "darkSky", "light": "lightSky" },
"markdownCodeBlock": { "dark": "darkText", "light": "lightText" },
"syntaxComment": { "dark": "darkOverlay2", "light": "lightOverlay2" },
"syntaxKeyword": { "dark": "darkMauve", "light": "lightMauve" },
"syntaxFunction": { "dark": "darkBlue", "light": "lightBlue" },
"syntaxVariable": { "dark": "darkRed", "light": "lightRed" },
"syntaxString": { "dark": "darkGreen", "light": "lightGreen" },
"syntaxNumber": { "dark": "darkPeach", "light": "lightPeach" },
"syntaxType": { "dark": "darkYellow", "light": "lightYellow" },
"syntaxOperator": { "dark": "darkSky", "light": "lightSky" },
"syntaxPunctuation": { "dark": "darkText", "light": "lightText" }
}
}
@@ -0,0 +1,242 @@
{
"$schema": "https://opencode.ai/theme.json",
"defs": {
"darkStep1": "#2d353b",
"darkStep2": "#333c43",
"darkStep3": "#343f44",
"darkStep4": "#3d484d",
"darkStep5": "#475258",
"darkStep6": "#7a8478",
"darkStep7": "#859289",
"darkStep8": "#9da9a0",
"darkStep9": "#a7c080",
"darkStep10": "#83c092",
"darkStep11": "#7a8478",
"darkStep12": "#d3c6aa",
"darkRed": "#e67e80",
"darkOrange": "#e69875",
"darkGreen": "#a7c080",
"darkCyan": "#83c092",
"darkYellow": "#dbbc7f",
"lightStep1": "#fdf6e3",
"lightStep2": "#efebd4",
"lightStep3": "#f4f0d9",
"lightStep4": "#efebd4",
"lightStep5": "#e6e2cc",
"lightStep6": "#a6b0a0",
"lightStep7": "#939f91",
"lightStep8": "#829181",
"lightStep9": "#8da101",
"lightStep10": "#35a77c",
"lightStep11": "#a6b0a0",
"lightStep12": "#5c6a72",
"lightRed": "#f85552",
"lightOrange": "#f57d26",
"lightGreen": "#8da101",
"lightCyan": "#35a77c",
"lightYellow": "#dfa000"
},
"theme": {
"primary": {
"dark": "darkStep9",
"light": "lightStep9"
},
"secondary": {
"dark": "#7fbbb3",
"light": "#3a94c5"
},
"accent": {
"dark": "#d699b6",
"light": "#df69ba"
},
"error": {
"dark": "darkRed",
"light": "lightRed"
},
"warning": {
"dark": "darkOrange",
"light": "lightOrange"
},
"success": {
"dark": "darkGreen",
"light": "lightGreen"
},
"info": {
"dark": "darkCyan",
"light": "lightCyan"
},
"text": {
"dark": "darkStep12",
"light": "lightStep12"
},
"textMuted": {
"dark": "darkStep11",
"light": "lightStep11"
},
"background": {
"dark": "darkStep1",
"light": "lightStep1"
},
"backgroundPanel": {
"dark": "darkStep2",
"light": "lightStep2"
},
"backgroundElement": {
"dark": "darkStep3",
"light": "lightStep3"
},
"border": {
"dark": "darkStep7",
"light": "lightStep7"
},
"borderActive": {
"dark": "darkStep8",
"light": "lightStep8"
},
"borderSubtle": {
"dark": "darkStep6",
"light": "lightStep6"
},
"diffAdded": {
"dark": "#4fd6be",
"light": "#1e725c"
},
"diffRemoved": {
"dark": "#c53b53",
"light": "#c53b53"
},
"diffContext": {
"dark": "#828bb8",
"light": "#7086b5"
},
"diffHunkHeader": {
"dark": "#828bb8",
"light": "#7086b5"
},
"diffHighlightAdded": {
"dark": "#b8db87",
"light": "#4db380"
},
"diffHighlightRemoved": {
"dark": "#e26a75",
"light": "#f52a65"
},
"diffAddedBg": {
"dark": "#20303b",
"light": "#d5e5d5"
},
"diffRemovedBg": {
"dark": "#37222c",
"light": "#f7d8db"
},
"diffContextBg": {
"dark": "darkStep2",
"light": "lightStep2"
},
"diffLineNumber": {
"dark": "darkStep3",
"light": "lightStep3"
},
"diffAddedLineNumberBg": {
"dark": "#1b2b34",
"light": "#c5d5c5"
},
"diffRemovedLineNumberBg": {
"dark": "#2d1f26",
"light": "#e7c8cb"
},
"markdownText": {
"dark": "darkStep12",
"light": "lightStep12"
},
"markdownHeading": {
"dark": "#d699b6",
"light": "#df69ba"
},
"markdownLink": {
"dark": "darkStep9",
"light": "lightStep9"
},
"markdownLinkText": {
"dark": "darkCyan",
"light": "lightCyan"
},
"markdownCode": {
"dark": "darkGreen",
"light": "lightGreen"
},
"markdownBlockQuote": {
"dark": "darkYellow",
"light": "lightYellow"
},
"markdownEmph": {
"dark": "darkYellow",
"light": "lightYellow"
},
"markdownStrong": {
"dark": "darkOrange",
"light": "lightOrange"
},
"markdownHorizontalRule": {
"dark": "darkStep11",
"light": "lightStep11"
},
"markdownListItem": {
"dark": "darkStep9",
"light": "lightStep9"
},
"markdownListEnumeration": {
"dark": "darkCyan",
"light": "lightCyan"
},
"markdownImage": {
"dark": "darkStep9",
"light": "lightStep9"
},
"markdownImageText": {
"dark": "darkCyan",
"light": "lightCyan"
},
"markdownCodeBlock": {
"dark": "darkStep12",
"light": "lightStep12"
},
"syntaxComment": {
"dark": "darkStep11",
"light": "lightStep11"
},
"syntaxKeyword": {
"dark": "#d699b6",
"light": "#df69ba"
},
"syntaxFunction": {
"dark": "darkStep9",
"light": "lightStep9"
},
"syntaxVariable": {
"dark": "darkRed",
"light": "lightRed"
},
"syntaxString": {
"dark": "darkGreen",
"light": "lightGreen"
},
"syntaxNumber": {
"dark": "darkOrange",
"light": "lightOrange"
},
"syntaxType": {
"dark": "darkYellow",
"light": "lightYellow"
},
"syntaxOperator": {
"dark": "darkCyan",
"light": "lightCyan"
},
"syntaxPunctuation": {
"dark": "darkStep12",
"light": "lightStep12"
}
}
}
@@ -0,0 +1,95 @@
{
"$schema": "https://opencode.ai/theme.json",
"defs": {
"darkBg0": "#282828",
"darkBg1": "#3c3836",
"darkBg2": "#504945",
"darkBg3": "#665c54",
"darkFg0": "#fbf1c7",
"darkFg1": "#ebdbb2",
"darkGray": "#928374",
"darkRed": "#cc241d",
"darkGreen": "#98971a",
"darkYellow": "#d79921",
"darkBlue": "#458588",
"darkPurple": "#b16286",
"darkAqua": "#689d6a",
"darkOrange": "#d65d0e",
"darkRedBright": "#fb4934",
"darkGreenBright": "#b8bb26",
"darkYellowBright": "#fabd2f",
"darkBlueBright": "#83a598",
"darkPurpleBright": "#d3869b",
"darkAquaBright": "#8ec07c",
"darkOrangeBright": "#fe8019",
"lightBg0": "#fbf1c7",
"lightBg1": "#ebdbb2",
"lightBg2": "#d5c4a1",
"lightBg3": "#bdae93",
"lightFg0": "#282828",
"lightFg1": "#3c3836",
"lightGray": "#7c6f64",
"lightRed": "#9d0006",
"lightGreen": "#79740e",
"lightYellow": "#b57614",
"lightBlue": "#076678",
"lightPurple": "#8f3f71",
"lightAqua": "#427b58",
"lightOrange": "#af3a03"
},
"theme": {
"primary": { "dark": "darkBlueBright", "light": "lightBlue" },
"secondary": { "dark": "darkPurpleBright", "light": "lightPurple" },
"accent": { "dark": "darkAquaBright", "light": "lightAqua" },
"error": { "dark": "darkRedBright", "light": "lightRed" },
"warning": { "dark": "darkOrangeBright", "light": "lightOrange" },
"success": { "dark": "darkGreenBright", "light": "lightGreen" },
"info": { "dark": "darkYellowBright", "light": "lightYellow" },
"text": { "dark": "darkFg1", "light": "lightFg1" },
"textMuted": { "dark": "darkGray", "light": "lightGray" },
"background": { "dark": "darkBg0", "light": "lightBg0" },
"backgroundPanel": { "dark": "darkBg1", "light": "lightBg1" },
"backgroundElement": { "dark": "darkBg2", "light": "lightBg2" },
"border": { "dark": "darkBg3", "light": "lightBg3" },
"borderActive": { "dark": "darkFg1", "light": "lightFg1" },
"borderSubtle": { "dark": "darkBg2", "light": "lightBg2" },
"diffAdded": { "dark": "darkGreen", "light": "lightGreen" },
"diffRemoved": { "dark": "darkRed", "light": "lightRed" },
"diffContext": { "dark": "darkGray", "light": "lightGray" },
"diffHunkHeader": { "dark": "darkAqua", "light": "lightAqua" },
"diffHighlightAdded": { "dark": "darkGreenBright", "light": "lightGreen" },
"diffHighlightRemoved": { "dark": "darkRedBright", "light": "lightRed" },
"diffAddedBg": { "dark": "#32302f", "light": "#e2e0b5" },
"diffRemovedBg": { "dark": "#322929", "light": "#e9d8d5" },
"diffContextBg": { "dark": "darkBg1", "light": "lightBg1" },
"diffLineNumber": { "dark": "darkBg3", "light": "lightBg3" },
"diffAddedLineNumberBg": { "dark": "#2a2827", "light": "#d4d2a9" },
"diffRemovedLineNumberBg": { "dark": "#2a2222", "light": "#d8cbc8" },
"markdownText": { "dark": "darkFg1", "light": "lightFg1" },
"markdownHeading": { "dark": "darkBlueBright", "light": "lightBlue" },
"markdownLink": { "dark": "darkAquaBright", "light": "lightAqua" },
"markdownLinkText": { "dark": "darkGreenBright", "light": "lightGreen" },
"markdownCode": { "dark": "darkYellowBright", "light": "lightYellow" },
"markdownBlockQuote": { "dark": "darkGray", "light": "lightGray" },
"markdownEmph": { "dark": "darkPurpleBright", "light": "lightPurple" },
"markdownStrong": { "dark": "darkOrangeBright", "light": "lightOrange" },
"markdownHorizontalRule": { "dark": "darkGray", "light": "lightGray" },
"markdownListItem": { "dark": "darkBlueBright", "light": "lightBlue" },
"markdownListEnumeration": {
"dark": "darkAquaBright",
"light": "lightAqua"
},
"markdownImage": { "dark": "darkAquaBright", "light": "lightAqua" },
"markdownImageText": { "dark": "darkGreenBright", "light": "lightGreen" },
"markdownCodeBlock": { "dark": "darkFg1", "light": "lightFg1" },
"syntaxComment": { "dark": "darkGray", "light": "lightGray" },
"syntaxKeyword": { "dark": "darkRedBright", "light": "lightRed" },
"syntaxFunction": { "dark": "darkGreenBright", "light": "lightGreen" },
"syntaxVariable": { "dark": "darkBlueBright", "light": "lightBlue" },
"syntaxString": { "dark": "darkYellowBright", "light": "lightYellow" },
"syntaxNumber": { "dark": "darkPurpleBright", "light": "lightPurple" },
"syntaxType": { "dark": "darkAquaBright", "light": "lightAqua" },
"syntaxOperator": { "dark": "darkOrangeBright", "light": "lightOrange" },
"syntaxPunctuation": { "dark": "darkFg1", "light": "lightFg1" }
}
}
@@ -0,0 +1,77 @@
{
"$schema": "https://opencode.ai/theme.json",
"defs": {
"sumiInk0": "#1F1F28",
"sumiInk1": "#2A2A37",
"sumiInk2": "#363646",
"sumiInk3": "#54546D",
"fujiWhite": "#DCD7BA",
"oldWhite": "#C8C093",
"fujiGray": "#727169",
"oniViolet": "#957FB8",
"crystalBlue": "#7E9CD8",
"carpYellow": "#C38D9D",
"sakuraPink": "#D27E99",
"waveAqua": "#76946A",
"roninYellow": "#D7A657",
"dragonRed": "#E82424",
"lotusGreen": "#98BB6C",
"waveBlue": "#2D4F67",
"lightBg": "#F2E9DE",
"lightPaper": "#EAE4D7",
"lightText": "#54433A",
"lightGray": "#9E9389"
},
"theme": {
"primary": { "dark": "crystalBlue", "light": "waveBlue" },
"secondary": { "dark": "oniViolet", "light": "oniViolet" },
"accent": { "dark": "sakuraPink", "light": "sakuraPink" },
"error": { "dark": "dragonRed", "light": "dragonRed" },
"warning": { "dark": "roninYellow", "light": "roninYellow" },
"success": { "dark": "lotusGreen", "light": "lotusGreen" },
"info": { "dark": "waveAqua", "light": "waveAqua" },
"text": { "dark": "fujiWhite", "light": "lightText" },
"textMuted": { "dark": "fujiGray", "light": "lightGray" },
"background": { "dark": "sumiInk0", "light": "lightBg" },
"backgroundPanel": { "dark": "sumiInk1", "light": "lightPaper" },
"backgroundElement": { "dark": "sumiInk2", "light": "#E3DCD2" },
"border": { "dark": "sumiInk3", "light": "#D4CBBF" },
"borderActive": { "dark": "carpYellow", "light": "carpYellow" },
"borderSubtle": { "dark": "sumiInk2", "light": "#DCD4C9" },
"diffAdded": { "dark": "lotusGreen", "light": "lotusGreen" },
"diffRemoved": { "dark": "dragonRed", "light": "dragonRed" },
"diffContext": { "dark": "fujiGray", "light": "lightGray" },
"diffHunkHeader": { "dark": "waveBlue", "light": "waveBlue" },
"diffHighlightAdded": { "dark": "#A9D977", "light": "#89AF5B" },
"diffHighlightRemoved": { "dark": "#F24A4A", "light": "#D61F1F" },
"diffAddedBg": { "dark": "#252E25", "light": "#EAF3E4" },
"diffRemovedBg": { "dark": "#362020", "light": "#FBE6E6" },
"diffContextBg": { "dark": "sumiInk1", "light": "lightPaper" },
"diffLineNumber": { "dark": "sumiInk3", "light": "#C7BEB4" },
"diffAddedLineNumberBg": { "dark": "#202820", "light": "#DDE8D6" },
"diffRemovedLineNumberBg": { "dark": "#2D1C1C", "light": "#F2DADA" },
"markdownText": { "dark": "fujiWhite", "light": "lightText" },
"markdownHeading": { "dark": "oniViolet", "light": "oniViolet" },
"markdownLink": { "dark": "crystalBlue", "light": "waveBlue" },
"markdownLinkText": { "dark": "waveAqua", "light": "waveAqua" },
"markdownCode": { "dark": "lotusGreen", "light": "lotusGreen" },
"markdownBlockQuote": { "dark": "fujiGray", "light": "lightGray" },
"markdownEmph": { "dark": "carpYellow", "light": "carpYellow" },
"markdownStrong": { "dark": "roninYellow", "light": "roninYellow" },
"markdownHorizontalRule": { "dark": "fujiGray", "light": "lightGray" },
"markdownListItem": { "dark": "crystalBlue", "light": "waveBlue" },
"markdownListEnumeration": { "dark": "waveAqua", "light": "waveAqua" },
"markdownImage": { "dark": "crystalBlue", "light": "waveBlue" },
"markdownImageText": { "dark": "waveAqua", "light": "waveAqua" },
"markdownCodeBlock": { "dark": "fujiWhite", "light": "lightText" },
"syntaxComment": { "dark": "fujiGray", "light": "lightGray" },
"syntaxKeyword": { "dark": "oniViolet", "light": "oniViolet" },
"syntaxFunction": { "dark": "crystalBlue", "light": "waveBlue" },
"syntaxVariable": { "dark": "fujiWhite", "light": "lightText" },
"syntaxString": { "dark": "lotusGreen", "light": "lotusGreen" },
"syntaxNumber": { "dark": "roninYellow", "light": "roninYellow" },
"syntaxType": { "dark": "carpYellow", "light": "carpYellow" },
"syntaxOperator": { "dark": "sakuraPink", "light": "sakuraPink" },
"syntaxPunctuation": { "dark": "fujiWhite", "light": "lightText" }
}
}
@@ -0,0 +1,77 @@
{
"$schema": "https://opencode.ai/theme.json",
"defs": {
"matrixInk0": "#0a0e0a",
"matrixInk1": "#0e130d",
"matrixInk2": "#141c12",
"matrixInk3": "#1e2a1b",
"rainGreen": "#2eff6a",
"rainGreenDim": "#1cc24b",
"rainGreenHi": "#62ff94",
"rainCyan": "#00efff",
"rainTeal": "#24f6d9",
"rainPurple": "#c770ff",
"rainOrange": "#ffa83d",
"alertRed": "#ff4b4b",
"alertYellow": "#e6ff57",
"alertBlue": "#30b3ff",
"rainGray": "#8ca391",
"lightBg": "#eef3ea",
"lightPaper": "#e4ebe1",
"lightInk1": "#dae1d7",
"lightText": "#203022",
"lightGray": "#748476"
},
"theme": {
"primary": { "dark": "rainGreen", "light": "rainGreenDim" },
"secondary": { "dark": "rainCyan", "light": "rainTeal" },
"accent": { "dark": "rainPurple", "light": "rainPurple" },
"error": { "dark": "alertRed", "light": "alertRed" },
"warning": { "dark": "alertYellow", "light": "alertYellow" },
"success": { "dark": "rainGreenHi", "light": "rainGreenDim" },
"info": { "dark": "alertBlue", "light": "alertBlue" },
"text": { "dark": "rainGreenHi", "light": "lightText" },
"textMuted": { "dark": "rainGray", "light": "lightGray" },
"background": { "dark": "matrixInk0", "light": "lightBg" },
"backgroundPanel": { "dark": "matrixInk1", "light": "lightPaper" },
"backgroundElement": { "dark": "matrixInk2", "light": "lightInk1" },
"border": { "dark": "matrixInk3", "light": "lightGray" },
"borderActive": { "dark": "rainGreen", "light": "rainGreenDim" },
"borderSubtle": { "dark": "matrixInk2", "light": "lightInk1" },
"diffAdded": { "dark": "rainGreenDim", "light": "rainGreenDim" },
"diffRemoved": { "dark": "alertRed", "light": "alertRed" },
"diffContext": { "dark": "rainGray", "light": "lightGray" },
"diffHunkHeader": { "dark": "alertBlue", "light": "alertBlue" },
"diffHighlightAdded": { "dark": "#77ffaf", "light": "#5dac7e" },
"diffHighlightRemoved": { "dark": "#ff7171", "light": "#d53a3a" },
"diffAddedBg": { "dark": "#132616", "light": "#e0efde" },
"diffRemovedBg": { "dark": "#261212", "light": "#f9e5e5" },
"diffContextBg": { "dark": "matrixInk1", "light": "lightPaper" },
"diffLineNumber": { "dark": "matrixInk3", "light": "lightGray" },
"diffAddedLineNumberBg": { "dark": "#0f1b11", "light": "#d6e7d2" },
"diffRemovedLineNumberBg": { "dark": "#1b1414", "light": "#f2d2d2" },
"markdownText": { "dark": "rainGreenHi", "light": "lightText" },
"markdownHeading": { "dark": "rainCyan", "light": "rainTeal" },
"markdownLink": { "dark": "alertBlue", "light": "alertBlue" },
"markdownLinkText": { "dark": "rainTeal", "light": "rainTeal" },
"markdownCode": { "dark": "rainGreenDim", "light": "rainGreenDim" },
"markdownBlockQuote": { "dark": "rainGray", "light": "lightGray" },
"markdownEmph": { "dark": "rainOrange", "light": "rainOrange" },
"markdownStrong": { "dark": "alertYellow", "light": "alertYellow" },
"markdownHorizontalRule": { "dark": "rainGray", "light": "lightGray" },
"markdownListItem": { "dark": "alertBlue", "light": "alertBlue" },
"markdownListEnumeration": { "dark": "rainTeal", "light": "rainTeal" },
"markdownImage": { "dark": "alertBlue", "light": "alertBlue" },
"markdownImageText": { "dark": "rainTeal", "light": "rainTeal" },
"markdownCodeBlock": { "dark": "rainGreenHi", "light": "lightText" },
"syntaxComment": { "dark": "rainGray", "light": "lightGray" },
"syntaxKeyword": { "dark": "rainPurple", "light": "rainPurple" },
"syntaxFunction": { "dark": "alertBlue", "light": "alertBlue" },
"syntaxVariable": { "dark": "rainGreenHi", "light": "lightText" },
"syntaxString": { "dark": "rainGreenDim", "light": "rainGreenDim" },
"syntaxNumber": { "dark": "rainOrange", "light": "rainOrange" },
"syntaxType": { "dark": "alertYellow", "light": "alertYellow" },
"syntaxOperator": { "dark": "rainTeal", "light": "rainTeal" },
"syntaxPunctuation": { "dark": "rainGreenHi", "light": "lightText" }
}
}
@@ -0,0 +1,223 @@
{
"$schema": "https://opencode.ai/theme.json",
"defs": {
"nord0": "#2E3440",
"nord1": "#3B4252",
"nord2": "#434C5E",
"nord3": "#4C566A",
"nord4": "#D8DEE9",
"nord5": "#E5E9F0",
"nord6": "#ECEFF4",
"nord7": "#8FBCBB",
"nord8": "#88C0D0",
"nord9": "#81A1C1",
"nord10": "#5E81AC",
"nord11": "#BF616A",
"nord12": "#D08770",
"nord13": "#EBCB8B",
"nord14": "#A3BE8C",
"nord15": "#B48EAD"
},
"theme": {
"primary": {
"dark": "nord8",
"light": "nord10"
},
"secondary": {
"dark": "nord9",
"light": "nord9"
},
"accent": {
"dark": "nord7",
"light": "nord7"
},
"error": {
"dark": "nord11",
"light": "nord11"
},
"warning": {
"dark": "nord12",
"light": "nord12"
},
"success": {
"dark": "nord14",
"light": "nord14"
},
"info": {
"dark": "nord8",
"light": "nord10"
},
"text": {
"dark": "nord6",
"light": "nord0"
},
"textMuted": {
"dark": "#8B95A7",
"light": "nord1"
},
"background": {
"dark": "nord0",
"light": "nord6"
},
"backgroundPanel": {
"dark": "nord1",
"light": "nord5"
},
"backgroundElement": {
"dark": "nord2",
"light": "nord4"
},
"border": {
"dark": "nord2",
"light": "nord3"
},
"borderActive": {
"dark": "nord3",
"light": "nord2"
},
"borderSubtle": {
"dark": "nord2",
"light": "nord3"
},
"diffAdded": {
"dark": "nord14",
"light": "nord14"
},
"diffRemoved": {
"dark": "nord11",
"light": "nord11"
},
"diffContext": {
"dark": "#8B95A7",
"light": "nord3"
},
"diffHunkHeader": {
"dark": "#8B95A7",
"light": "nord3"
},
"diffHighlightAdded": {
"dark": "nord14",
"light": "nord14"
},
"diffHighlightRemoved": {
"dark": "nord11",
"light": "nord11"
},
"diffAddedBg": {
"dark": "#3B4252",
"light": "#E5E9F0"
},
"diffRemovedBg": {
"dark": "#3B4252",
"light": "#E5E9F0"
},
"diffContextBg": {
"dark": "nord1",
"light": "nord5"
},
"diffLineNumber": {
"dark": "nord2",
"light": "nord4"
},
"diffAddedLineNumberBg": {
"dark": "#3B4252",
"light": "#E5E9F0"
},
"diffRemovedLineNumberBg": {
"dark": "#3B4252",
"light": "#E5E9F0"
},
"markdownText": {
"dark": "nord4",
"light": "nord0"
},
"markdownHeading": {
"dark": "nord8",
"light": "nord10"
},
"markdownLink": {
"dark": "nord9",
"light": "nord9"
},
"markdownLinkText": {
"dark": "nord7",
"light": "nord7"
},
"markdownCode": {
"dark": "nord14",
"light": "nord14"
},
"markdownBlockQuote": {
"dark": "#8B95A7",
"light": "nord3"
},
"markdownEmph": {
"dark": "nord12",
"light": "nord12"
},
"markdownStrong": {
"dark": "nord13",
"light": "nord13"
},
"markdownHorizontalRule": {
"dark": "#8B95A7",
"light": "nord3"
},
"markdownListItem": {
"dark": "nord8",
"light": "nord10"
},
"markdownListEnumeration": {
"dark": "nord7",
"light": "nord7"
},
"markdownImage": {
"dark": "nord9",
"light": "nord9"
},
"markdownImageText": {
"dark": "nord7",
"light": "nord7"
},
"markdownCodeBlock": {
"dark": "nord4",
"light": "nord0"
},
"syntaxComment": {
"dark": "#8B95A7",
"light": "nord3"
},
"syntaxKeyword": {
"dark": "nord9",
"light": "nord9"
},
"syntaxFunction": {
"dark": "nord8",
"light": "nord8"
},
"syntaxVariable": {
"dark": "nord7",
"light": "nord7"
},
"syntaxString": {
"dark": "nord14",
"light": "nord14"
},
"syntaxNumber": {
"dark": "nord15",
"light": "nord15"
},
"syntaxType": {
"dark": "nord7",
"light": "nord7"
},
"syntaxOperator": {
"dark": "nord9",
"light": "nord9"
},
"syntaxPunctuation": {
"dark": "nord4",
"light": "nord0"
}
}
}
@@ -0,0 +1,84 @@
{
"$schema": "https://opencode.ai/theme.json",
"defs": {
"darkBg": "#282c34",
"darkBgAlt": "#21252b",
"darkBgPanel": "#353b45",
"darkFg": "#abb2bf",
"darkFgMuted": "#5c6370",
"darkPurple": "#c678dd",
"darkBlue": "#61afef",
"darkRed": "#e06c75",
"darkGreen": "#98c379",
"darkYellow": "#e5c07b",
"darkOrange": "#d19a66",
"darkCyan": "#56b6c2",
"lightBg": "#fafafa",
"lightBgAlt": "#f0f0f1",
"lightBgPanel": "#eaeaeb",
"lightFg": "#383a42",
"lightFgMuted": "#a0a1a7",
"lightPurple": "#a626a4",
"lightBlue": "#4078f2",
"lightRed": "#e45649",
"lightGreen": "#50a14f",
"lightYellow": "#c18401",
"lightOrange": "#986801",
"lightCyan": "#0184bc"
},
"theme": {
"primary": { "dark": "darkBlue", "light": "lightBlue" },
"secondary": { "dark": "darkPurple", "light": "lightPurple" },
"accent": { "dark": "darkCyan", "light": "lightCyan" },
"error": { "dark": "darkRed", "light": "lightRed" },
"warning": { "dark": "darkYellow", "light": "lightYellow" },
"success": { "dark": "darkGreen", "light": "lightGreen" },
"info": { "dark": "darkOrange", "light": "lightOrange" },
"text": { "dark": "darkFg", "light": "lightFg" },
"textMuted": { "dark": "darkFgMuted", "light": "lightFgMuted" },
"background": { "dark": "darkBg", "light": "lightBg" },
"backgroundPanel": { "dark": "darkBgAlt", "light": "lightBgAlt" },
"backgroundElement": { "dark": "darkBgPanel", "light": "lightBgPanel" },
"border": { "dark": "#393f4a", "light": "#d1d1d2" },
"borderActive": { "dark": "darkBlue", "light": "lightBlue" },
"borderSubtle": { "dark": "#2c313a", "light": "#e0e0e1" },
"diffAdded": { "dark": "darkGreen", "light": "lightGreen" },
"diffRemoved": { "dark": "darkRed", "light": "lightRed" },
"diffContext": { "dark": "darkFgMuted", "light": "lightFgMuted" },
"diffHunkHeader": { "dark": "darkCyan", "light": "lightCyan" },
"diffHighlightAdded": { "dark": "#aad482", "light": "#489447" },
"diffHighlightRemoved": { "dark": "#e8828b", "light": "#d65145" },
"diffAddedBg": { "dark": "#2c382b", "light": "#eafbe9" },
"diffRemovedBg": { "dark": "#3a2d2f", "light": "#fce9e8" },
"diffContextBg": { "dark": "darkBgAlt", "light": "lightBgAlt" },
"diffLineNumber": { "dark": "#495162", "light": "#c9c9ca" },
"diffAddedLineNumberBg": { "dark": "#283427", "light": "#e1f3df" },
"diffRemovedLineNumberBg": { "dark": "#36292b", "light": "#f5e2e1" },
"markdownText": { "dark": "darkFg", "light": "lightFg" },
"markdownHeading": { "dark": "darkPurple", "light": "lightPurple" },
"markdownLink": { "dark": "darkBlue", "light": "lightBlue" },
"markdownLinkText": { "dark": "darkCyan", "light": "lightCyan" },
"markdownCode": { "dark": "darkGreen", "light": "lightGreen" },
"markdownBlockQuote": { "dark": "darkFgMuted", "light": "lightFgMuted" },
"markdownEmph": { "dark": "darkYellow", "light": "lightYellow" },
"markdownStrong": { "dark": "darkOrange", "light": "lightOrange" },
"markdownHorizontalRule": {
"dark": "darkFgMuted",
"light": "lightFgMuted"
},
"markdownListItem": { "dark": "darkBlue", "light": "lightBlue" },
"markdownListEnumeration": { "dark": "darkCyan", "light": "lightCyan" },
"markdownImage": { "dark": "darkBlue", "light": "lightBlue" },
"markdownImageText": { "dark": "darkCyan", "light": "lightCyan" },
"markdownCodeBlock": { "dark": "darkFg", "light": "lightFg" },
"syntaxComment": { "dark": "darkFgMuted", "light": "lightFgMuted" },
"syntaxKeyword": { "dark": "darkPurple", "light": "lightPurple" },
"syntaxFunction": { "dark": "darkBlue", "light": "lightBlue" },
"syntaxVariable": { "dark": "darkRed", "light": "lightRed" },
"syntaxString": { "dark": "darkGreen", "light": "lightGreen" },
"syntaxNumber": { "dark": "darkOrange", "light": "lightOrange" },
"syntaxType": { "dark": "darkYellow", "light": "lightYellow" },
"syntaxOperator": { "dark": "darkCyan", "light": "lightCyan" },
"syntaxPunctuation": { "dark": "darkFg", "light": "lightFg" }
}
}
@@ -0,0 +1,246 @@
{
"$schema": "https://opencode.ai/theme.json",
"defs": {
"darkStep1": "#0a0a0a",
"darkStep2": "#141414",
"darkStep3": "#1e1e1e",
"darkStep4": "#282828",
"darkStep5": "#323232",
"darkStep6": "#3c3c3c",
"darkStep7": "#484848",
"darkStep8": "#606060",
"darkStep9": "#fab283",
"darkStep10": "#ffc09f",
"darkStep11": "#808080",
"darkStep12": "#eeeeee",
"darkSecondary": "#5c9cf5",
"darkAccent": "#9d7cd8",
"darkRed": "#e06c75",
"darkOrange": "#f5a742",
"darkGreen": "#7fd88f",
"darkCyan": "#56b6c2",
"darkYellow": "#e5c07b",
"lightStep1": "#ffffff",
"lightStep2": "#fafafa",
"lightStep3": "#f5f5f5",
"lightStep4": "#ebebeb",
"lightStep5": "#e1e1e1",
"lightStep6": "#d4d4d4",
"lightStep7": "#b8b8b8",
"lightStep8": "#a0a0a0",
"lightStep9": "#3b7dd8",
"lightStep10": "#2968c3",
"lightStep11": "#8a8a8a",
"lightStep12": "#1a1a1a",
"lightSecondary": "#7b5bb6",
"lightAccent": "#d68c27",
"lightRed": "#d1383d",
"lightOrange": "#d68c27",
"lightGreen": "#3d9a57",
"lightCyan": "#318795",
"lightYellow": "#b0851f"
},
"theme": {
"primary": {
"dark": "darkStep9",
"light": "lightStep9"
},
"secondary": {
"dark": "darkSecondary",
"light": "lightSecondary"
},
"accent": {
"dark": "darkAccent",
"light": "lightAccent"
},
"error": {
"dark": "darkRed",
"light": "lightRed"
},
"warning": {
"dark": "darkOrange",
"light": "lightOrange"
},
"success": {
"dark": "darkGreen",
"light": "lightGreen"
},
"info": {
"dark": "darkCyan",
"light": "lightCyan"
},
"text": {
"dark": "darkStep12",
"light": "lightStep12"
},
"textMuted": {
"dark": "darkStep11",
"light": "lightStep11"
},
"background": {
"dark": "darkStep1",
"light": "lightStep1"
},
"backgroundPanel": {
"dark": "darkStep2",
"light": "lightStep2"
},
"backgroundElement": {
"dark": "darkStep3",
"light": "lightStep3"
},
"border": {
"dark": "darkStep7",
"light": "lightStep7"
},
"borderActive": {
"dark": "darkStep8",
"light": "lightStep8"
},
"borderSubtle": {
"dark": "darkStep6",
"light": "lightStep6"
},
"diffAdded": {
"dark": "#4fd6be",
"light": "#1e725c"
},
"diffRemoved": {
"dark": "#c53b53",
"light": "#c53b53"
},
"diffContext": {
"dark": "#828bb8",
"light": "#7086b5"
},
"diffHunkHeader": {
"dark": "#828bb8",
"light": "#7086b5"
},
"diffHighlightAdded": {
"dark": "#b8db87",
"light": "#4db380"
},
"diffHighlightRemoved": {
"dark": "#e26a75",
"light": "#f52a65"
},
"diffAddedBg": {
"dark": "#20303b",
"light": "#d5e5d5"
},
"diffRemovedBg": {
"dark": "#37222c",
"light": "#f7d8db"
},
"diffContextBg": {
"dark": "darkStep2",
"light": "lightStep2"
},
"diffLineNumber": {
"dark": "darkStep3",
"light": "lightStep3"
},
"diffAddedLineNumberBg": {
"dark": "#1b2b34",
"light": "#c5d5c5"
},
"diffRemovedLineNumberBg": {
"dark": "#2d1f26",
"light": "#e7c8cb"
},
"markdownText": {
"dark": "darkStep12",
"light": "lightStep12"
},
"markdownHeading": {
"dark": "darkAccent",
"light": "lightAccent"
},
"markdownLink": {
"dark": "darkStep9",
"light": "lightStep9"
},
"markdownLinkText": {
"dark": "darkCyan",
"light": "lightCyan"
},
"markdownCode": {
"dark": "darkGreen",
"light": "lightGreen"
},
"markdownBlockQuote": {
"dark": "darkYellow",
"light": "lightYellow"
},
"markdownEmph": {
"dark": "darkYellow",
"light": "lightYellow"
},
"markdownStrong": {
"dark": "darkOrange",
"light": "lightOrange"
},
"markdownHorizontalRule": {
"dark": "darkStep11",
"light": "lightStep11"
},
"markdownListItem": {
"dark": "darkStep9",
"light": "lightStep9"
},
"markdownListEnumeration": {
"dark": "darkCyan",
"light": "lightCyan"
},
"markdownImage": {
"dark": "darkStep9",
"light": "lightStep9"
},
"markdownImageText": {
"dark": "darkCyan",
"light": "lightCyan"
},
"markdownCodeBlock": {
"dark": "darkStep12",
"light": "lightStep12"
},
"syntaxComment": {
"dark": "darkStep11",
"light": "lightStep11"
},
"syntaxKeyword": {
"dark": "darkAccent",
"light": "lightAccent"
},
"syntaxFunction": {
"dark": "darkStep9",
"light": "lightStep9"
},
"syntaxVariable": {
"dark": "darkRed",
"light": "lightRed"
},
"syntaxString": {
"dark": "darkGreen",
"light": "lightGreen"
},
"syntaxNumber": {
"dark": "darkOrange",
"light": "lightOrange"
},
"syntaxType": {
"dark": "darkYellow",
"light": "lightYellow"
},
"syntaxOperator": {
"dark": "darkCyan",
"light": "lightCyan"
},
"syntaxPunctuation": {
"dark": "darkStep12",
"light": "lightStep12"
}
}
}
@@ -0,0 +1,244 @@
{
"$schema": "https://opencode.ai/theme.json",
"defs": {
"darkStep1": "#1a1b26",
"darkStep2": "#1e2030",
"darkStep3": "#222436",
"darkStep4": "#292e42",
"darkStep5": "#3b4261",
"darkStep6": "#545c7e",
"darkStep7": "#737aa2",
"darkStep8": "#9099b2",
"darkStep9": "#82aaff",
"darkStep10": "#89b4fa",
"darkStep11": "#828bb8",
"darkStep12": "#c8d3f5",
"darkRed": "#ff757f",
"darkOrange": "#ff966c",
"darkYellow": "#ffc777",
"darkGreen": "#c3e88d",
"darkCyan": "#86e1fc",
"darkPurple": "#c099ff",
"lightStep1": "#e1e2e7",
"lightStep2": "#d5d6db",
"lightStep3": "#c8c9ce",
"lightStep4": "#b9bac1",
"lightStep5": "#a8aecb",
"lightStep6": "#9699a8",
"lightStep7": "#737a8c",
"lightStep8": "#5a607d",
"lightStep9": "#2e7de9",
"lightStep10": "#1a6ce7",
"lightStep11": "#8990a3",
"lightStep12": "#3760bf",
"lightRed": "#f52a65",
"lightOrange": "#b15c00",
"lightYellow": "#8c6c3e",
"lightGreen": "#587539",
"lightCyan": "#007197",
"lightPurple": "#9854f1"
},
"theme": {
"primary": {
"dark": "darkStep9",
"light": "lightStep9"
},
"secondary": {
"dark": "darkPurple",
"light": "lightPurple"
},
"accent": {
"dark": "darkOrange",
"light": "lightOrange"
},
"error": {
"dark": "darkRed",
"light": "lightRed"
},
"warning": {
"dark": "darkOrange",
"light": "lightOrange"
},
"success": {
"dark": "darkGreen",
"light": "lightGreen"
},
"info": {
"dark": "darkStep9",
"light": "lightStep9"
},
"text": {
"dark": "darkStep12",
"light": "lightStep12"
},
"textMuted": {
"dark": "darkStep11",
"light": "lightStep11"
},
"background": {
"dark": "darkStep1",
"light": "lightStep1"
},
"backgroundPanel": {
"dark": "darkStep2",
"light": "lightStep2"
},
"backgroundElement": {
"dark": "darkStep3",
"light": "lightStep3"
},
"border": {
"dark": "darkStep7",
"light": "lightStep7"
},
"borderActive": {
"dark": "darkStep8",
"light": "lightStep8"
},
"borderSubtle": {
"dark": "darkStep6",
"light": "lightStep6"
},
"diffAdded": {
"dark": "#4fd6be",
"light": "#1e725c"
},
"diffRemoved": {
"dark": "#c53b53",
"light": "#c53b53"
},
"diffContext": {
"dark": "#828bb8",
"light": "#7086b5"
},
"diffHunkHeader": {
"dark": "#828bb8",
"light": "#7086b5"
},
"diffHighlightAdded": {
"dark": "#b8db87",
"light": "#4db380"
},
"diffHighlightRemoved": {
"dark": "#e26a75",
"light": "#f52a65"
},
"diffAddedBg": {
"dark": "#20303b",
"light": "#d5e5d5"
},
"diffRemovedBg": {
"dark": "#37222c",
"light": "#f7d8db"
},
"diffContextBg": {
"dark": "darkStep2",
"light": "lightStep2"
},
"diffLineNumber": {
"dark": "darkStep3",
"light": "lightStep3"
},
"diffAddedLineNumberBg": {
"dark": "#1b2b34",
"light": "#c5d5c5"
},
"diffRemovedLineNumberBg": {
"dark": "#2d1f26",
"light": "#e7c8cb"
},
"markdownText": {
"dark": "darkStep12",
"light": "lightStep12"
},
"markdownHeading": {
"dark": "darkPurple",
"light": "lightPurple"
},
"markdownLink": {
"dark": "darkStep9",
"light": "lightStep9"
},
"markdownLinkText": {
"dark": "darkCyan",
"light": "lightCyan"
},
"markdownCode": {
"dark": "darkGreen",
"light": "lightGreen"
},
"markdownBlockQuote": {
"dark": "darkYellow",
"light": "lightYellow"
},
"markdownEmph": {
"dark": "darkYellow",
"light": "lightYellow"
},
"markdownStrong": {
"dark": "darkOrange",
"light": "lightOrange"
},
"markdownHorizontalRule": {
"dark": "darkStep11",
"light": "lightStep11"
},
"markdownListItem": {
"dark": "darkStep9",
"light": "lightStep9"
},
"markdownListEnumeration": {
"dark": "darkCyan",
"light": "lightCyan"
},
"markdownImage": {
"dark": "darkStep9",
"light": "lightStep9"
},
"markdownImageText": {
"dark": "darkCyan",
"light": "lightCyan"
},
"markdownCodeBlock": {
"dark": "darkStep12",
"light": "lightStep12"
},
"syntaxComment": {
"dark": "darkStep11",
"light": "lightStep11"
},
"syntaxKeyword": {
"dark": "darkPurple",
"light": "lightPurple"
},
"syntaxFunction": {
"dark": "darkStep9",
"light": "lightStep9"
},
"syntaxVariable": {
"dark": "darkRed",
"light": "lightRed"
},
"syntaxString": {
"dark": "darkGreen",
"light": "lightGreen"
},
"syntaxNumber": {
"dark": "darkOrange",
"light": "lightOrange"
},
"syntaxType": {
"dark": "darkYellow",
"light": "lightYellow"
},
"syntaxOperator": {
"dark": "darkCyan",
"light": "lightCyan"
},
"syntaxPunctuation": {
"dark": "darkStep12",
"light": "lightStep12"
}
}
}
-295
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.BackgroundSubtleColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep2),
Light: lipgloss.Color(lightStep2),
}
theme.BackgroundElementColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep3),
Light: lipgloss.Color(lightStep3),
}
// Border colors
theme.BorderColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep7),
Light: lipgloss.Color(lightStep7),
}
theme.BorderActiveColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep8),
Light: lipgloss.Color(lightStep8),
}
theme.BorderSubtleColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep6),
Light: lipgloss.Color(lightStep6),
}
// Diff view colors
theme.DiffAddedColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#4fd6be"), // teal from palette
Light: lipgloss.Color("#1e725c"),
}
theme.DiffRemovedColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#c53b53"), // red1 from palette
Light: lipgloss.Color("#c53b53"),
}
theme.DiffContextColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#828bb8"), // fg_dark from palette
Light: lipgloss.Color("#7086b5"),
}
theme.DiffHunkHeaderColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#828bb8"), // fg_dark from palette
Light: lipgloss.Color("#7086b5"),
}
theme.DiffHighlightAddedColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#b8db87"), // git.add from palette
Light: lipgloss.Color("#4db380"),
}
theme.DiffHighlightRemovedColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#e26a75"), // git.delete from palette
Light: lipgloss.Color("#f52a65"),
}
theme.DiffAddedBgColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#20303b"),
Light: lipgloss.Color("#d5e5d5"),
}
theme.DiffRemovedBgColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#37222c"),
Light: lipgloss.Color("#f7d8db"),
}
theme.DiffContextBgColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep2),
Light: lipgloss.Color(lightStep2),
}
theme.DiffLineNumberColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep3), // dark3 from palette
Light: lipgloss.Color(lightStep3),
}
theme.DiffAddedLineNumberBgColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#1b2b34"),
Light: lipgloss.Color("#c5d5c5"),
}
theme.DiffRemovedLineNumberBgColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#2d1f26"),
Light: lipgloss.Color("#e7c8cb"),
}
// Markdown colors
theme.MarkdownTextColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep12),
Light: lipgloss.Color(lightStep12),
}
theme.MarkdownHeadingColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkPurple),
Light: lipgloss.Color(lightPurple),
}
theme.MarkdownLinkColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkBlue),
Light: lipgloss.Color(lightBlue),
}
theme.MarkdownLinkTextColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkCyan),
Light: lipgloss.Color(lightCyan),
}
theme.MarkdownCodeColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkGreen),
Light: lipgloss.Color(lightGreen),
}
theme.MarkdownBlockQuoteColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkYellow),
Light: lipgloss.Color(lightYellow),
}
theme.MarkdownEmphColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkYellow),
Light: lipgloss.Color(lightYellow),
}
theme.MarkdownStrongColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkOrange),
Light: lipgloss.Color(lightOrange),
}
theme.MarkdownHorizontalRuleColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep11),
Light: lipgloss.Color(lightStep11),
}
theme.MarkdownListItemColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkBlue),
Light: lipgloss.Color(lightBlue),
}
theme.MarkdownListEnumerationColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkCyan),
Light: lipgloss.Color(lightCyan),
}
theme.MarkdownImageColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkBlue),
Light: lipgloss.Color(lightBlue),
}
theme.MarkdownImageTextColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkCyan),
Light: lipgloss.Color(lightCyan),
}
theme.MarkdownCodeBlockColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep12),
Light: lipgloss.Color(lightStep12),
}
// Syntax highlighting colors
theme.SyntaxCommentColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep11),
Light: lipgloss.Color(lightStep11),
}
theme.SyntaxKeywordColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkPurple),
Light: lipgloss.Color(lightPurple),
}
theme.SyntaxFunctionColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkBlue),
Light: lipgloss.Color(lightBlue),
}
theme.SyntaxVariableColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkRed),
Light: lipgloss.Color(lightRed),
}
theme.SyntaxStringColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkGreen),
Light: lipgloss.Color(lightGreen),
}
theme.SyntaxNumberColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkOrange),
Light: lipgloss.Color(lightOrange),
}
theme.SyntaxTypeColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkYellow),
Light: lipgloss.Color(lightYellow),
}
theme.SyntaxOperatorColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkCyan),
Light: lipgloss.Color(lightCyan),
}
theme.SyntaxPunctuationColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep12),
Light: lipgloss.Color(lightStep12),
}
return theme
}
func init() {
// Register the Tokyo Night theme with the theme manager
RegisterTheme("tokyonight", NewTokyoNightTheme())
}
+145 -72
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,11 +54,16 @@ type appModel struct {
leaderBinding *key.Binding
isLeaderSequence bool
toastManager *toast.ToastManager
interruptKeyState InterruptKeyState
}
func (a appModel) Init() tea.Cmd {
var cmds []tea.Cmd
cmds = append(cmds, tea.RequestBackgroundColor)
// https://github.com/charmbracelet/bubbletea/issues/1440
// https://github.com/sst/opencode/issues/127
if !util.IsWsl() {
cmds = append(cmds, tea.RequestBackgroundColor)
}
cmds = append(cmds, a.app.InitializeProvider())
cmds = append(cmds, a.editor.Init())
cmds = append(cmds, a.messages.Init())
@@ -66,13 +85,16 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyPressMsg:
keyString := msg.String()
// 1. Handle active modal
if a.modal != nil {
switch msg.String() {
switch keyString {
// Escape always closes current modal
case "esc", "ctrl+c":
cmd := a.modal.Close()
a.modal = nil
return a, nil
return a, cmd
}
// Pass all other key presses to the modal
@@ -84,7 +106,6 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// 2. Check for commands that require leader
if a.isLeaderSequence {
matches := a.app.Commands.Matches(msg, a.isLeaderSequence)
// Reset leader state
a.isLeaderSequence = false
if len(matches) > 0 {
return a, util.CmdHandler(commands.ExecuteCommandsMsg(matches))
@@ -92,48 +113,49 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
// 3. Handle completions trigger
switch msg.String() {
case "/":
if !a.showCompletionDialog {
a.showCompletionDialog = true
if keyString == "/" && !a.showCompletionDialog {
a.showCompletionDialog = true
initialValue := "/"
currentInput := a.editor.Value()
// if the input doesn't end with a space,
// then we want to include the last word
if !strings.HasSuffix(currentInput, " ") {
words := strings.Split(a.editor.Value(), " ")
if len(words) > 0 {
lastWord := words[len(words)-1]
lastWord = strings.TrimSpace(lastWord)
initialValue = lastWord + "/"
}
initialValue := "/"
currentInput := a.editor.Value()
// if the input doesn't end with a space,
// then we want to include the last word
// (ie, `packages/`)
if !strings.HasSuffix(currentInput, " ") {
words := strings.Split(a.editor.Value(), " ")
if len(words) > 0 {
lastWord := words[len(words)-1]
lastWord = strings.TrimSpace(lastWord)
initialValue = lastWord + "/"
}
updated, cmd := a.completions.Update(
app.CompletionDialogTriggerdMsg{
InitialValue: initialValue,
},
)
a.completions = updated.(dialog.CompletionDialog)
cmds = append(cmds, cmd)
updated, cmd = a.completions.Update(msg)
a.completions = updated.(dialog.CompletionDialog)
cmds = append(cmds, cmd)
updated, cmd = a.editor.Update(msg)
a.editor = updated.(chat.EditorComponent)
cmds = append(cmds, cmd)
return a, tea.Sequence(cmds...)
}
updated, cmd := a.completions.Update(
app.CompletionDialogTriggeredMsg{
InitialValue: initialValue,
},
)
a.completions = updated.(dialog.CompletionDialog)
cmds = append(cmds, cmd)
updated, cmd = a.editor.Update(msg)
a.editor = updated.(chat.EditorComponent)
cmds = append(cmds, cmd)
updated, cmd = a.updateCompletions(msg)
a.completions = updated.(dialog.CompletionDialog)
cmds = append(cmds, cmd)
return a, tea.Sequence(cmds...)
}
if a.showCompletionDialog {
switch msg.String() {
switch keyString {
case "tab", "enter", "esc", "ctrl+c":
context, contextCmd := a.completions.Update(msg)
a.completions = context.(dialog.CompletionDialog)
cmds = append(cmds, contextCmd)
updated, cmd := a.updateCompletions(msg)
a.completions = updated.(dialog.CompletionDialog)
cmds = append(cmds, cmd)
return a, tea.Batch(cmds...)
}
@@ -141,13 +163,10 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
a.editor = updated.(chat.EditorComponent)
cmds = append(cmds, cmd)
currentInput := a.editor.Value()
provider := a.completionManager.GetProvider(currentInput)
a.completions.SetProvider(provider)
updated, cmd = a.updateCompletions(msg)
a.completions = updated.(dialog.CompletionDialog)
cmds = append(cmds, cmd)
context, contextCmd := a.completions.Update(msg)
a.completions = context.(dialog.CompletionDialog)
cmds = append(cmds, contextCmd)
return a, tea.Batch(cmds...)
}
@@ -167,16 +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 shouldn't happen?
// All printable characters were already sent, and
// any other keypress that didn't match a command
// is likely a noop.
// 7. Fallback to editor. This is for other characters
// like backspace, tab, etc.
updatedEditor, cmd := a.editor.Update(msg)
a.editor = updatedEditor.(chat.EditorComponent)
return a, cmd
@@ -187,14 +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
@@ -211,12 +256,17 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
cmds = append(cmds, cmd)
case dialog.CompletionDialogCloseMsg:
a.showCompletionDialog = false
a.completions.SetProvider(a.completionManager.DefaultProvider())
case client.EventInstallationUpdated:
return a, toast.NewSuccessToast(
"New version installed",
toast.WithTitle("opencode updated to "+msg.Properties.Version+", restart to apply."),
"opencode updated to "+msg.Properties.Version+", restart to apply.",
toast.WithTitle("New version installed"),
)
case client.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() {
@@ -507,30 +582,18 @@ func (a appModel) executeCommand(command commands.Command) (tea.Model, tea.Cmd)
a.messages = updated.(chat.MessagesComponent)
cmds = append(cmds, cmd)
case commands.MessagesPageUpCommand:
if a.showCompletionDialog {
return a, nil
}
updated, cmd := a.messages.PageUp()
a.messages = updated.(chat.MessagesComponent)
cmds = append(cmds, cmd)
case commands.MessagesPageDownCommand:
if a.showCompletionDialog {
return a, nil
}
updated, cmd := a.messages.PageDown()
a.messages = updated.(chat.MessagesComponent)
cmds = append(cmds, cmd)
case commands.MessagesHalfPageUpCommand:
if a.showCompletionDialog {
return a, nil
}
updated, cmd := a.messages.HalfPageUp()
a.messages = updated.(chat.MessagesComponent)
cmds = append(cmds, cmd)
case commands.MessagesHalfPageDownCommand:
if a.showCompletionDialog {
return a, nil
}
updated, cmd := a.messages.HalfPageDown()
a.messages = updated.(chat.MessagesComponent)
cmds = append(cmds, cmd)
@@ -540,6 +603,15 @@ func (a appModel) executeCommand(command commands.Command) (tea.Model, tea.Cmd)
return a, tea.Batch(cmds...)
}
func (a appModel) updateCompletions(msg tea.Msg) (tea.Model, tea.Cmd) {
currentInput := a.editor.Value()
if currentInput != "" {
provider := a.completionManager.GetProvider(currentInput)
a.completions.SetProvider(provider)
}
return a.completions.Update(msg)
}
func NewModel(app *app.App) tea.Model {
completionManager := completions.NewCompletionManager(app)
initialProvider := completionManager.DefaultProvider()
@@ -573,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),
+18
View File
@@ -1,6 +1,9 @@
package util
import (
"os"
"strings"
tea "github.com/charmbracelet/bubbletea/v2"
)
@@ -17,3 +20,18 @@ func Clamp(v, low, high int) int {
}
return min(high, max(low, v))
}
func IsWsl() bool {
// Check for WSL environment variables
if os.Getenv("WSL_DISTRO_NAME") != "" {
return true
}
// Check /proc/version for WSL signature
if data, err := os.ReadFile("/proc/version"); err == nil {
version := strings.ToLower(string(data))
return strings.Contains(version, "microsoft") || strings.Contains(version, "wsl")
}
return false
}
+282 -159
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",
+408 -69
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
}
+7 -6
View File
@@ -4,10 +4,15 @@ import starlight from "@astrojs/starlight"
import solidJs from "@astrojs/solid-js"
import cloudflare from "@astrojs/cloudflare"
import theme from "toolbeam-docs-theme"
import config from "./config.mjs"
import { rehypeHeadingIds } from "@astrojs/markdown-remark"
import rehypeAutolinkHeadings from "rehype-autolink-headings"
const github = "https://github.com/sst/opencode"
const headerLinks = [
{ name: "Docs", url: "/docs/" },
{ name: "GitHub", url: github },
]
// https://astro.build/config
export default defineConfig({
@@ -30,7 +35,7 @@ export default defineConfig({
title: "opencode",
expressiveCode: { themes: ["github-light", "github-dark"] },
social: [
{ icon: "github", label: "GitHub", href: github },
{ icon: "github", label: "GitHub", href: config.github },
],
editLink: {
baseUrl: `${github}/edit/master/www/`,
@@ -59,11 +64,7 @@ export default defineConfig({
},
plugins: [
theme({
// Optionally, add your own header links
headerLinks: [
{ name: "Home", url: "/" },
{ name: "Docs", url: "/docs/" },
],
headerLinks: config.headerLinks,
}),
],
}),
+7
View File
@@ -0,0 +1,7 @@
export default {
github: "https://github.com/sst/opencode",
headerLinks: [
{ name: "Home", url: "/" },
{ name: "Docs", url: "/docs/" },
],
}
+1
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",
+169
View File
@@ -0,0 +1,169 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"$schema": {
"type": "string",
"description": "JSON schema reference for configuration validation"
},
"defs": {
"type": "object",
"description": "Color definitions that can be referenced in the theme",
"patternProperties": {
"^[a-zA-Z][a-zA-Z0-9_]*$": {
"oneOf": [
{
"type": "string",
"pattern": "^#[0-9a-fA-F]{6}$",
"description": "Hex color value"
},
{
"type": "integer",
"minimum": 0,
"maximum": 255,
"description": "ANSI color code (0-255)"
}
]
}
},
"additionalProperties": false
},
"theme": {
"type": "object",
"description": "Theme color definitions",
"properties": {
"primary": { "$ref": "#/definitions/colorValue" },
"secondary": { "$ref": "#/definitions/colorValue" },
"accent": { "$ref": "#/definitions/colorValue" },
"error": { "$ref": "#/definitions/colorValue" },
"warning": { "$ref": "#/definitions/colorValue" },
"success": { "$ref": "#/definitions/colorValue" },
"info": { "$ref": "#/definitions/colorValue" },
"text": { "$ref": "#/definitions/colorValue" },
"textMuted": { "$ref": "#/definitions/colorValue" },
"background": { "$ref": "#/definitions/colorValue" },
"backgroundPanel": { "$ref": "#/definitions/colorValue" },
"backgroundElement": { "$ref": "#/definitions/colorValue" },
"border": { "$ref": "#/definitions/colorValue" },
"borderActive": { "$ref": "#/definitions/colorValue" },
"borderSubtle": { "$ref": "#/definitions/colorValue" },
"diffAdded": { "$ref": "#/definitions/colorValue" },
"diffRemoved": { "$ref": "#/definitions/colorValue" },
"diffContext": { "$ref": "#/definitions/colorValue" },
"diffHunkHeader": { "$ref": "#/definitions/colorValue" },
"diffHighlightAdded": { "$ref": "#/definitions/colorValue" },
"diffHighlightRemoved": { "$ref": "#/definitions/colorValue" },
"diffAddedBg": { "$ref": "#/definitions/colorValue" },
"diffRemovedBg": { "$ref": "#/definitions/colorValue" },
"diffContextBg": { "$ref": "#/definitions/colorValue" },
"diffLineNumber": { "$ref": "#/definitions/colorValue" },
"diffAddedLineNumberBg": { "$ref": "#/definitions/colorValue" },
"diffRemovedLineNumberBg": { "$ref": "#/definitions/colorValue" },
"markdownText": { "$ref": "#/definitions/colorValue" },
"markdownHeading": { "$ref": "#/definitions/colorValue" },
"markdownLink": { "$ref": "#/definitions/colorValue" },
"markdownLinkText": { "$ref": "#/definitions/colorValue" },
"markdownCode": { "$ref": "#/definitions/colorValue" },
"markdownBlockQuote": { "$ref": "#/definitions/colorValue" },
"markdownEmph": { "$ref": "#/definitions/colorValue" },
"markdownStrong": { "$ref": "#/definitions/colorValue" },
"markdownHorizontalRule": { "$ref": "#/definitions/colorValue" },
"markdownListItem": { "$ref": "#/definitions/colorValue" },
"markdownListEnumeration": { "$ref": "#/definitions/colorValue" },
"markdownImage": { "$ref": "#/definitions/colorValue" },
"markdownImageText": { "$ref": "#/definitions/colorValue" },
"markdownCodeBlock": { "$ref": "#/definitions/colorValue" },
"syntaxComment": { "$ref": "#/definitions/colorValue" },
"syntaxKeyword": { "$ref": "#/definitions/colorValue" },
"syntaxFunction": { "$ref": "#/definitions/colorValue" },
"syntaxVariable": { "$ref": "#/definitions/colorValue" },
"syntaxString": { "$ref": "#/definitions/colorValue" },
"syntaxNumber": { "$ref": "#/definitions/colorValue" },
"syntaxType": { "$ref": "#/definitions/colorValue" },
"syntaxOperator": { "$ref": "#/definitions/colorValue" },
"syntaxPunctuation": { "$ref": "#/definitions/colorValue" }
},
"required": [
"primary",
"secondary",
"accent",
"text",
"textMuted",
"background"
],
"additionalProperties": false
}
},
"required": ["theme"],
"additionalProperties": false,
"definitions": {
"colorValue": {
"oneOf": [
{
"type": "string",
"pattern": "^#[0-9a-fA-F]{6}$",
"description": "Hex color value (same for dark and light)"
},
{
"type": "integer",
"minimum": 0,
"maximum": 255,
"description": "ANSI color code (0-255, same for dark and light)"
},
{
"type": "string",
"pattern": "^[a-zA-Z][a-zA-Z0-9_]*$",
"description": "Reference to another color in the theme or defs"
},
{
"type": "object",
"properties": {
"dark": {
"oneOf": [
{
"type": "string",
"pattern": "^#[0-9a-fA-F]{6}$",
"description": "Hex color value for dark mode"
},
{
"type": "integer",
"minimum": 0,
"maximum": 255,
"description": "ANSI color code for dark mode"
},
{
"type": "string",
"pattern": "^[a-zA-Z][a-zA-Z0-9_]*$",
"description": "Reference to another color for dark mode"
}
]
},
"light": {
"oneOf": [
{
"type": "string",
"pattern": "^#[0-9a-fA-F]{6}$",
"description": "Hex color value for light mode"
},
{
"type": "integer",
"minimum": 0,
"maximum": 255,
"description": "ANSI color code for light mode"
},
{
"type": "string",
"pattern": "^[a-zA-Z][a-zA-Z0-9_]*$",
"description": "Reference to another color for light mode"
}
]
}
},
"required": ["dark", "light"],
"additionalProperties": false,
"description": "Separate colors for dark and light modes"
}
]
}
}
}
+74 -20
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`

Some files were not shown because too many files have changed in this diff Show More