Compare commits

..
100 Commits
Author SHA1 Message Date
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
GitMurf 5140e83012 feat(copilot): edit headers for better rate limit avoidance (#321) 2025-06-23 10:44:19 -04:00
Dax Raad 100d6212be more graceful mcp failures
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 Palma 6e6fe6e013 Add Github Copilot OAuth authentication flow (#305) 2025-06-22 19:11:37 -04:00
Dax Raad d05b60291e docs: contributing 2025-06-22 17:55:10 -04:00
adamdottv 5162361372 fix(tui): color contrast fixes for nord 2025-06-22 15:17:18 -05:00
adamdottv d271b9f75b fix(tui): help dialog visuals 2025-06-22 14:28:16 -05:00
Márk Magyar 333569bed3 ignore: fix typos and formatting (#294) 2025-06-22 14:26:46 -04:00
Tomandopencode 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
Adam 37e0a7050f fix(tui): mouse wheel escape codes leaking into input 2025-06-22 10:26:44 -05:00
adamdottv 774dcb6980 fix(tui): cleanup help dialog
publish / publish (push) Has been cancelled
2025-06-22 06:44:23 -05:00
phantomreactor 28bc49ad17 fix: invisible html tags and compact long delay (#304) 2025-06-22 06:29:04 -05:00
adamdottv dc1947838c fix(tui): cleanup modal visuals
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 Magyar 137e964131 fix: session title generation (#293) 2025-06-21 14:32:11 -05:00
tyrellshawn 8efbe497fd Created a Theme inspired by the matrix (#285) 2025-06-21 07:29:49 -05:00
Thomas Meire 119d2d966c Add error handling on the calls to the server to debug issue #132 (#137) 2025-06-21 07:24:39 -05:00
Dax Raad 194415e785 footer clarifies it's showing context usage, not input token usage 2025-06-20 22:52:51 -04:00
Dax Raad 1684042fb6 huge optimization for token usage with anthropic
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 Arya 53c18a64b4 docs: add API client generation instructions to README and AGENTS.md (#273) 2025-06-20 17:27:58 -04:00
Saatvik Arya b8144c5654 fix: return false for missing AWS_PROFILE in amazon-bedrock provider (#277) 2025-06-20 17:27:27 -04:00
adamdottv 9081e17fcc fix(tui): visual tweaks to themes
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 Magyar eee396f903 feat(tui): theme switcher with preview (#264) 2025-06-20 15:14:05 -05:00
Jay V 0d2f8e175a docs: share bugs 2025-06-20 15:50:12 -04:00
Jay V 4df40e0d9b docs: share page bugs 2025-06-20 15:50:12 -04:00
Dax Raad b72e17a8b7 fix issue with conversations hanging 2025-06-20 15:49:49 -04:00
Dax Raad 61160dc220 docs: readme 2025-06-20 15:22:41 -04:00
Dax 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
JoshandDax Raad 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
Dmytro YankovskyiandDax Raad 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
niba 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 Godha 6674c6083a fix: phantom input bug on wsl (#200) 2025-06-19 20:08:56 -05:00
Tom Watkins 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
Jay 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
adamdottv 3728a12bee fix(tui): better help on home
publish / publish (push) Has been cancelled
2025-06-19 15:56:28 -05:00
Jay V af07e51213 docs: tweak 2025-06-19 16:40:15 -04:00
Jay V 3113788c92 docs: copy 2025-06-19 16:39:36 -04:00
Jay V efb5fe6d4e docs: styles 2025-06-19 16:38:37 -04:00
Jay V 54dd6c644d docs: adding to config 2025-06-19 16:36:17 -04:00
Dax Raad 39ad8f2667 ignore: do migration 2025-06-19 16:32:32 -04:00
Jay V c4a2c84e53 docs: readme
publish / publish (push) Has been cancelled
2025-06-19 16:29:20 -04:00
Jay V 44fe012812 docs: edits 2025-06-19 16:28:11 -04:00
Jay V f5e7f079ea Copy changes 2025-06-19 16:28:03 -04:00
adamdottv 15a8936806 fix(tui): better tool titles 2025-06-19 15:11:53 -05:00
adamdottv 4e4cff49c0 feat(tui): better task tool rendering
publish / publish (push) Has been cancelled
2025-06-19 15:02:13 -05:00
adamdottv 5540503bee fix(tui): sorted tool arg maps 2025-06-19 14:07:33 -05:00
adamdottv 193718034b fix: typescript error 2025-06-19 13:57:25 -05:00
adamdottv 72108c0296 fix(tui): sorted tool arg maps 2025-06-19 13:56:09 -05:00
Dax Raad ec1c9f8cd1 use production share url
publish / publish (push) Has been cancelled
2025-06-19 14:21:00 -04:00
Dax Raad a85b0a370e ci: share 2025-06-19 13:26:15 -04:00
Dax Raadandopencode e7784d2864 add schema descriptions to config fields
Enhance configuration schema with descriptive text for all fields to improve developer experience and auto-generated documentation.

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

Co-Authored-By: opencode <noreply@opencode.ai>
2025-06-19 13:12:13 -04:00
Dax Raad 97c4815444 fix task agent performance issues
publish / publish (push) Has been cancelled
2025-06-19 13:00:57 -04:00
Dax Raad 7d1a1663c8 allow selecting model and continuing previous session for opencode run 2025-06-19 13:00:57 -04:00
adamdottv 24c0ce6e53 fix(tui): vscode and mac terminal colors 2025-06-19 11:46:08 -05:00
adamdottv 4cdc86612c fix(tui): overlay border backgrounds 2025-06-19 11:41:30 -05:00
116 changed files with 5938 additions and 3194 deletions
+2 -1
View File
@@ -4,6 +4,7 @@ on:
push: push:
branches: branches:
- dev - dev
- production
workflow_dispatch: workflow_dispatch:
concurrency: ${{ github.workflow }}-${{ github.ref }} concurrency: ${{ github.workflow }}-${{ github.ref }}
@@ -20,6 +21,6 @@ jobs:
- run: bun install - run: bun install
- run: bun sst deploy --stage=dev - run: bun sst deploy --stage=${{ github.ref_name }}
env: env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2025 OpenCode Copyright (c) 2025 opencode
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
+17 -148
View File
@@ -7,19 +7,17 @@
</picture> </picture>
</a> </a>
</p> </p>
<p align="center">AI coding agent, built for the terminal.</p>
<p align="center"> <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://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> <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> </p>
[![opencode Terminal UI](packages/web/src/assets/themes/opencode.png)](https://opencode.ai)
--- ---
AI coding agent, built for the terminal.
**Note:** Version 0.1.x is a full rewrite, and we do not have proper documentation for it yet. Should have this out week of June 17th 2025.
[![opencode Terminal UI](screenshot.png)](https://opencode.ai)
### Installation ### Installation
```bash ```bash
@@ -34,167 +32,38 @@ paru -S opencode-bin # Arch Linux
> **Note:** Remove versions older than 0.1.x before installing > **Note:** Remove versions older than 0.1.x before installing
### Providers ### Documentation
The recommended approach is to sign up for Claude Pro or Max, run `opencode auth login`, and select Anthropic. It's the most cost-effective way to use opencode. For more info on how to configure opencode [**head over to our docs**](https://opencode.ai/docs).
opencode is powered by the provider list at [Models.dev](https://models.dev), so you can use `opencode auth login` to configure API keys for any provider you'd like to use. This is stored in `~/.local/share/opencode/auth.json`.
```bash
$ opencode auth login
┌ Add credential
◆ Select provider
│ ● Anthropic (recommended)
│ ○ OpenAI
│ ○ Google
│ ○ Amazon Bedrock
│ ○ Azure
│ ○ DeepSeek
│ ○ Groq
│ ...
```
The Models.dev dataset is also used to detect common environment variables like `OPENAI_API_KEY` to autoload that provider.
If there are additional providers you want to use you can submit a PR to the [Models.dev repo](https://github.com/sst/models.dev). If configuring just for yourself check out the Config section below.
### Config
Config is optional and can be placed in the root of your repo or globally in `~/.config/opencode/config.json`. It can be checked in and shared with your team.
```json title="opencode.json"
{
"$schema": "http://opencode.ai/config.json"
"theme": "opencode",
"model": "anthropic/claude-sonnet-4-20250514" // format is provider/model
"autoshare": false,
"autoupdate": true,
}
```
#### Keybinds
You can configure custom keybinds, the values listed below are the defaults.
```json title="opencode.json"
{
"$schema": "http://opencode.ai/config.json",
"keybinds": {
"leader": "ctrl+x",
"help": "<leader>h",
"editor_open": "<leader>e",
"session_new": "<leader>n",
"session_list": "<leader>l",
"session_share": "<leader>s",
"session_interrupt": "esc",
"session_compact": "<leader>c",
"tool_details": "<leader>d",
"model_list": "<leader>m",
"theme_list": "<leader>t",
"project_init": "<leader>i",
"input_clear": "ctrl+c",
"input_paste": "ctrl+v",
"input_submit": "enter",
"input_newline": "shift+enter,ctrl+j",
"history_previous": "up",
"history_next": "down",
"messages_page_up": "pgup",
"messages_page_down": "pgdown",
"messages_half_page_up": "ctrl+alt+u",
"messages_half_page_down": "ctrl+alt+d",
"messages_previous": "ctrl+alt+k",
"messages_next": "ctrl+alt+j",
"messages_first": "ctrl+g",
"messages_last": "ctrl+alt+g",
"app_exit": "ctrl+c,<leader>q"
}
}
```
#### MCP
```json title="opencode.json"
{
"$schema": "http://opencode.ai/config.json",
"mcp": {
"localmcp": {
"type": "local",
"command": ["bun", "x", "my-mcp-command"],
"environment": {
"MY_ENV_VAR": "my_env_var_value"
}
},
"remotemcp": {
"type": "remote",
"url": "https://my-mcp-server.com"
}
}
}
```
#### Providers
You can use opencode with any provider listed at [here](https://ai-sdk.dev/providers/ai-sdk-providers). Be sure to specify the npm package to use to load the provider. Remember most popular providers are preloaded from [models.dev](https://models.dev)
```json title="opencode.json"
{
"$schema": "https://opencode.ai/config.json",
"provider": {
"ollama": {
"npm": "@ai-sdk/openai-compatible",
"options": {
"baseURL": "http://localhost:11434/v1"
},
"models": {
"llama2": {}
}
}
}
}
```
### Contributing ### 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. To run opencode locally you need.
- Bun - Bun
- Golang 1.24.x - Golang 1.24.x
To run. And run.
```bash ```bash
$ bun install $ bun install
$ bun run packages/opencode/src/index.ts $ 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. ```bash
$ cd packages/tui
```json title="opencode.json" $ go generate ./pkg/client/
{
"$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"
}
}
}
}
}
``` ```
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? #### How is this different than Claude Code?
+57 -2
View File
@@ -43,8 +43,10 @@
"yargs": "18.0.0", "yargs": "18.0.0",
"zod": "catalog:", "zod": "catalog:",
"zod-openapi": "4.2.4", "zod-openapi": "4.2.4",
"zod-validation-error": "3.5.2",
}, },
"devDependencies": { "devDependencies": {
"@ai-sdk/amazon-bedrock": "2.2.10",
"@ai-sdk/anthropic": "1.2.12", "@ai-sdk/anthropic": "1.2.12",
"@tsconfig/bun": "1.0.7", "@tsconfig/bun": "1.0.7",
"@types/bun": "latest", "@types/bun": "latest",
@@ -69,6 +71,7 @@
"astro": "5.7.13", "astro": "5.7.13",
"diff": "8.0.2", "diff": "8.0.2",
"js-base64": "3.7.7", "js-base64": "3.7.7",
"lang-map": "0.4.0",
"luxon": "3.6.1", "luxon": "3.6.1",
"marked": "15.0.12", "marked": "15.0.12",
"rehype-autolink-headings": "7.1.0", "rehype-autolink-headings": "7.1.0",
@@ -101,6 +104,8 @@
"zod": "3.24.2", "zod": "3.24.2",
}, },
"packages": { "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/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=="], "@ai-sdk/provider": ["@ai-sdk/provider@1.1.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg=="],
@@ -137,6 +142,12 @@
"@astrojs/underscore-redirects": ["@astrojs/underscore-redirects@0.6.1", "", {}, "sha512-4bMLrs2KW+8/vHEE5Ffv2HbxCbbgXO+2N6MpoCsMXUlUoi7pgEEx8kbkzMXJ2dZtWF3gvwm9lvgjnFeanC2LGg=="], "@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/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=="], "@babel/compat-data": ["@babel/compat-data@7.27.3", "", {}, "sha512-V42wFfx1ymFte+ecf6iXghnnP8kWTO+ZLXIyZq+1LAXHHvTZdVxicn4yiVYdYMGaCO3tmqub11AorKkv+iodqw=="],
@@ -417,6 +428,18 @@
"@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="], "@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=="], "@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=="], "@swc/helpers": ["@swc/helpers@0.5.17", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A=="],
@@ -435,7 +458,7 @@
"@types/babel__traverse": ["@types/babel__traverse@7.20.7", "", { "dependencies": { "@babel/types": "^7.20.7" } }, "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng=="], "@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=="], "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
@@ -573,7 +596,7 @@
"buffer": ["buffer@4.9.2", "", { "dependencies": { "base64-js": "^1.0.2", "ieee754": "^1.1.4", "isarray": "^1.0.0" } }, "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg=="], "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=="], "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
@@ -989,6 +1012,10 @@
"klona": ["klona@2.0.6", "", {}, "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA=="], "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=="], "leven": ["leven@2.1.0", "", {}, "sha512-nvVPLpIHUxCUoRLrFqTgSxXJ614d8AgQoWl7zPe/2VadE8+1dpU3LBhowRuBAcuwruWtOdD8oYC9jDNJjXDPyA=="],
"longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="],
@@ -1655,12 +1682,24 @@
"zod-to-ts": ["zod-to-ts@1.2.0", "", { "peerDependencies": { "typescript": "^4.9.4 || ^5.0.2", "zod": "^3" } }, "sha512-x30XE43V+InwGpvTySRNz9kB7qFU8DlyEy7BsSTCHPH1R0QasMmHWZDCzYm6bVXtj/9NNJAZF3jW8rzFvH5OFA=="], "zod-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=="], "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=="], "@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=="], "@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/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=="], "@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
@@ -1681,6 +1720,18 @@
"@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], "@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=="], "@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=="], "ansi-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
@@ -1751,6 +1802,8 @@
"@astrojs/mdx/@astrojs/markdown-remark/@astrojs/prism": ["@astrojs/prism@3.3.0", "", { "dependencies": { "prismjs": "^1.30.0" } }, "sha512-q8VwfU/fDZNoDOf+r7jUnMC2//H2l0TuQ6FkGJL8vD8nw/q5KiL3DS1KKBI3QhI9UQhpJ5dc7AtqfbXWuOgLCQ=="], "@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=="], "@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=="], "ansi-align/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
@@ -1817,6 +1870,8 @@
"wrangler/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.4", "", { "os": "win32", "cpu": "x64" }, "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ=="], "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=="], "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=="], "args/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="],
+3 -2
View File
@@ -23,8 +23,9 @@ export const api = new sst.cloudflare.Worker("Api", {
}, },
]) ])
args.migrations = { args.migrations = {
oldTag: "v1", // Note: when releasing the next tag, make sure all stages use tag v2
newTag: "v1", oldTag: $app.stage === "production" ? "" : "v1",
newTag: $app.stage === "production" ? "" : "v1",
//newSqliteClasses: ["SyncServer"], //newSqliteClasses: ["SyncServer"],
} }
}, },
+6 -1
View File
@@ -1,4 +1,9 @@
{ {
"$schema": "https://opencode.ai/config.json", "$schema": "https://opencode.ai/config.json",
"mcp": {} "mcp": {
"sentry": {
"type": "remote",
"url": "https://mcp.sentry.dev/sse"
}
}
} }
+1 -1
View File
@@ -120,7 +120,7 @@ export default {
return new Response( return new Response(
JSON.stringify({ JSON.stringify({
secret, secret,
url: "https://dev.opencode.ai/s/" + short, url: "https://opencode.ai/s/" + short,
}), }),
{ {
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
+2
View File
@@ -7,6 +7,7 @@
- **Typecheck**: `bun run typecheck` (npm run typecheck) - **Typecheck**: `bun run typecheck` (npm run typecheck)
- **Test**: `bun test` (runs all tests) - **Test**: `bun test` (runs all tests)
- **Single test**: `bun test test/tool/tool.test.ts` (specific test file) - **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 ## Code Style
@@ -37,3 +38,4 @@
- **Validation**: All inputs validated with Zod schemas - **Validation**: All inputs validated with Zod schemas
- **Logging**: Use `Log.create({ service: "name" })` pattern - **Logging**: Use `Log.create({ service: "name" })` pattern
- **Storage**: Use `Storage` namespace for persistence - **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.
+74 -37
View File
@@ -2,97 +2,127 @@
"type": "object", "type": "object",
"properties": { "properties": {
"$schema": { "$schema": {
"type": "string" "type": "string",
"description": "JSON schema reference for configuration validation"
}, },
"theme": { "theme": {
"type": "string" "type": "string",
"description": "Theme name to use for the interface"
}, },
"keybinds": { "keybinds": {
"type": "object", "type": "object",
"properties": { "properties": {
"leader": { "leader": {
"type": "string" "type": "string",
"description": "Leader key for keybind combinations"
}, },
"help": { "help": {
"type": "string" "type": "string",
"description": "Show help dialog"
}, },
"editor_open": { "editor_open": {
"type": "string" "type": "string",
"description": "Open external editor"
}, },
"session_new": { "session_new": {
"type": "string" "type": "string",
"description": "Create a new session"
}, },
"session_list": { "session_list": {
"type": "string" "type": "string",
"description": "List all sessions"
}, },
"session_share": { "session_share": {
"type": "string" "type": "string",
"description": "Share current session"
}, },
"session_interrupt": { "session_interrupt": {
"type": "string" "type": "string",
"description": "Interrupt current session"
}, },
"session_compact": { "session_compact": {
"type": "string" "type": "string",
"description": "Toggle compact mode for session"
}, },
"tool_details": { "tool_details": {
"type": "string" "type": "string",
"description": "Show tool details"
}, },
"model_list": { "model_list": {
"type": "string" "type": "string",
"description": "List available models"
}, },
"theme_list": { "theme_list": {
"type": "string" "type": "string",
"description": "List available themes"
}, },
"project_init": { "project_init": {
"type": "string" "type": "string",
"description": "Initialize project configuration"
}, },
"input_clear": { "input_clear": {
"type": "string" "type": "string",
"description": "Clear input field"
}, },
"input_paste": { "input_paste": {
"type": "string" "type": "string",
"description": "Paste from clipboard"
}, },
"input_submit": { "input_submit": {
"type": "string" "type": "string",
"description": "Submit input"
}, },
"input_newline": { "input_newline": {
"type": "string" "type": "string",
"description": "Insert newline in input"
}, },
"history_previous": { "history_previous": {
"type": "string" "type": "string",
"description": "Navigate to previous history item"
}, },
"history_next": { "history_next": {
"type": "string" "type": "string",
"description": "Navigate to next history item"
}, },
"messages_page_up": { "messages_page_up": {
"type": "string" "type": "string",
"description": "Scroll messages up by one page"
}, },
"messages_page_down": { "messages_page_down": {
"type": "string" "type": "string",
"description": "Scroll messages down by one page"
}, },
"messages_half_page_up": { "messages_half_page_up": {
"type": "string" "type": "string",
"description": "Scroll messages up by half page"
}, },
"messages_half_page_down": { "messages_half_page_down": {
"type": "string" "type": "string",
"description": "Scroll messages down by half page"
}, },
"messages_previous": { "messages_previous": {
"type": "string" "type": "string",
"description": "Navigate to previous message"
}, },
"messages_next": { "messages_next": {
"type": "string" "type": "string",
"description": "Navigate to next message"
}, },
"messages_first": { "messages_first": {
"type": "string" "type": "string",
"description": "Navigate to first message"
}, },
"messages_last": { "messages_last": {
"type": "string" "type": "string",
"description": "Navigate to last message"
}, },
"app_exit": { "app_exit": {
"type": "string" "type": "string",
"description": "Exit the application"
} }
}, },
"additionalProperties": false "additionalProperties": false,
"description": "Custom keybind configurations"
}, },
"autoshare": { "autoshare": {
"type": "boolean", "type": "boolean",
@@ -207,7 +237,8 @@
"models" "models"
], ],
"additionalProperties": false "additionalProperties": false
} },
"description": "Custom provider configurations and model overrides"
}, },
"mcp": { "mcp": {
"type": "object", "type": "object",
@@ -218,19 +249,22 @@
"properties": { "properties": {
"type": { "type": {
"type": "string", "type": "string",
"const": "local" "const": "local",
"description": "Type of MCP server connection"
}, },
"command": { "command": {
"type": "array", "type": "array",
"items": { "items": {
"type": "string" "type": "string"
} },
"description": "Command and arguments to run the MCP server"
}, },
"environment": { "environment": {
"type": "object", "type": "object",
"additionalProperties": { "additionalProperties": {
"type": "string" "type": "string"
} },
"description": "Environment variables to set when running the MCP server"
} }
}, },
"required": [ "required": [
@@ -244,10 +278,12 @@
"properties": { "properties": {
"type": { "type": {
"type": "string", "type": "string",
"const": "remote" "const": "remote",
"description": "Type of MCP server connection"
}, },
"url": { "url": {
"type": "string" "type": "string",
"description": "URL of the remote MCP server"
} }
}, },
"required": [ "required": [
@@ -257,7 +293,8 @@
"additionalProperties": false "additionalProperties": false
} }
] ]
} },
"description": "MCP (Model Context Protocol) server configurations"
} }
}, },
"additionalProperties": false, "additionalProperties": false,
+5 -3
View File
@@ -12,13 +12,14 @@
"./*": "./src/*.ts" "./*": "./src/*.ts"
}, },
"devDependencies": { "devDependencies": {
"@ai-sdk/amazon-bedrock": "2.2.10",
"@ai-sdk/anthropic": "1.2.12",
"@tsconfig/bun": "1.0.7", "@tsconfig/bun": "1.0.7",
"@types/bun": "latest", "@types/bun": "latest",
"@types/turndown": "5.0.5", "@types/turndown": "5.0.5",
"@types/yargs": "17.0.33", "@types/yargs": "17.0.33",
"typescript": "catalog:", "typescript": "catalog:",
"zod-to-json-schema": "3.24.5", "zod-to-json-schema": "3.24.5"
"@ai-sdk/anthropic": "1.2.12"
}, },
"dependencies": { "dependencies": {
"@clack/prompts": "0.11.0", "@clack/prompts": "0.11.0",
@@ -42,6 +43,7 @@
"xdg-basedir": "5.1.0", "xdg-basedir": "5.1.0",
"yargs": "18.0.0", "yargs": "18.0.0",
"zod": "catalog:", "zod": "catalog:",
"zod-openapi": "4.2.4" "zod-openapi": "4.2.4",
"zod-validation-error": "3.5.2"
} }
} }
+2 -2
View File
@@ -80,9 +80,9 @@ function main() {
// Create symlink to the actual binary // Create symlink to the actual binary
fs.symlinkSync(binaryPath, binScript) fs.symlinkSync(binaryPath, binScript)
console.log(`OpenCode binary symlinked: ${binScript} -> ${binaryPath}`) console.log(`opencode binary symlinked: ${binScript} -> ${binaryPath}`)
} catch (error) { } catch (error) {
console.error("Failed to create OpenCode binary symlink:", error.message) console.error("Failed to create opencode binary symlink:", error.message)
process.exit(1) process.exit(1)
} }
} }
+1
View File
@@ -1,3 +1,4 @@
import "zod-openapi/extend"
import { Log } from "../util/log" import { Log } from "../util/log"
import { Context } from "../util/context" import { Context } from "../util/context"
import { Filesystem } from "../util/filesystem" import { Filesystem } from "../util/filesystem"
+2 -2
View File
@@ -8,7 +8,7 @@ export namespace AuthAnthropic {
const pkce = await generatePKCE() const pkce = await generatePKCE()
const url = new URL("https://claude.ai/oauth/authorize", import.meta.url) const url = new URL("https://claude.ai/oauth/authorize", import.meta.url)
url.searchParams.set("code", "true") 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("response_type", "code")
url.searchParams.set( url.searchParams.set(
"redirect_uri", "redirect_uri",
@@ -38,7 +38,7 @@ export namespace AuthAnthropic {
code: splits[0], code: splits[0],
state: splits[1], state: splits[1],
grant_type: "authorization_code", grant_type: "authorization_code",
client_id: "9d1c250a-e61b-44d9-88ed-5944d1962f5e", client_id: CLIENT_ID,
redirect_uri: "https://console.anthropic.com/oauth/code/callback", redirect_uri: "https://console.anthropic.com/oauth/code/callback",
code_verifier: verifier, code_verifier: verifier,
}), }),
@@ -0,0 +1,150 @@
import { z } from "zod"
import { Auth } from "./index"
import { NamedError } from "../util/error"
export namespace AuthGithubCopilot {
const CLIENT_ID = "Iv1.b507a08c87ecfe98"
const DEVICE_CODE_URL = "https://github.com/login/device/code"
const ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token"
const COPILOT_API_KEY_URL = "https://api.github.com/copilot_internal/v2/token"
interface DeviceCodeResponse {
device_code: string
user_code: string
verification_uri: string
expires_in: number
interval: number
}
interface AccessTokenResponse {
access_token?: string
error?: string
error_description?: string
}
interface CopilotTokenResponse {
token: string
expires_at: number
refresh_in: number
endpoints: {
api: string
}
}
export async function authorize() {
const deviceResponse = await fetch(DEVICE_CODE_URL, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
"User-Agent": "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(),
}),
)
}
+38 -5
View File
@@ -1,4 +1,5 @@
import { AuthAnthropic } from "../../auth/anthropic" import { AuthAnthropic } from "../../auth/anthropic"
import { AuthGithubCopilot } from "../../auth/github-copilot"
import { Auth } from "../../auth" import { Auth } from "../../auth"
import { cmd } from "./cmd" import { cmd } from "./cmd"
import * as prompts from "@clack/prompts" import * as prompts from "@clack/prompts"
@@ -16,7 +17,7 @@ export const AuthCommand = cmd({
.command(AuthLogoutCommand) .command(AuthLogoutCommand)
.command(AuthListCommand) .command(AuthListCommand)
.demandCommand(), .demandCommand(),
async handler() {}, async handler() { },
}) })
export const AuthListCommand = cmd({ export const AuthListCommand = cmd({
@@ -40,15 +41,16 @@ export const AuthListCommand = cmd({
export const AuthLoginCommand = cmd({ export const AuthLoginCommand = cmd({
command: "login", command: "login",
describe: "login to a provider", describe: "log in to a provider",
async handler() { async handler() {
UI.empty() UI.empty()
prompts.intro("Add credential") prompts.intro("Add credential")
const providers = await ModelsDev.get() const providers = await ModelsDev.get()
const priority: Record<string, number> = { const priority: Record<string, number> = {
anthropic: 0, anthropic: 0,
openai: 1, "github-copilot": 1,
google: 2, openai: 2,
google: 3,
} }
let provider = await prompts.select({ let provider = await prompts.select({
message: "Select provider", message: "Select provider",
@@ -146,6 +148,37 @@ export const AuthLoginCommand = cmd({
} }
} }
if (provider === "github-copilot") {
await new Promise((resolve) => setTimeout(resolve, 10))
const deviceInfo = await AuthGithubCopilot.authorize()
prompts.note(
`Please visit: ${deviceInfo.verification}\nEnter code: ${deviceInfo.user}`,
)
const spinner = prompts.spinner()
spinner.start("Waiting for authorization...")
while (true) {
await new Promise((resolve) =>
setTimeout(resolve, deviceInfo.interval * 1000),
)
const status = await AuthGithubCopilot.poll(deviceInfo.device)
if (status === "pending") continue
if (status === "complete") {
spinner.stop("Login successful")
break
}
if (status === "failed") {
spinner.stop("Failed to authorize", 1)
break
}
}
prompts.outro("Done")
return
}
const key = await prompts.password({ const key = await prompts.password({
message: "Enter your API key", message: "Enter your API key",
validate: (x) => (x.length > 0 ? undefined : "Required"), validate: (x) => (x.length > 0 ? undefined : "Required"),
@@ -162,7 +195,7 @@ export const AuthLoginCommand = cmd({
export const AuthLogoutCommand = cmd({ export const AuthLogoutCommand = cmd({
command: "logout", command: "logout",
describe: "logout from a configured provider", describe: "log out from a configured provider",
async handler() { async handler() {
UI.empty() UI.empty()
const credentials = await Auth.all().then((x) => Object.entries(x)) 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)
},
}
+11 -10
View File
@@ -20,37 +20,38 @@ const TOOL: Record<string, [string, string]> = {
list: ["List", UI.Style.TEXT_INFO_BOLD], list: ["List", UI.Style.TEXT_INFO_BOLD],
read: ["Read", UI.Style.TEXT_HIGHLIGHT_BOLD], read: ["Read", UI.Style.TEXT_HIGHLIGHT_BOLD],
write: ["Write", UI.Style.TEXT_SUCCESS_BOLD], write: ["Write", UI.Style.TEXT_SUCCESS_BOLD],
websearch: ["Search", UI.Style.TEXT_DIM_BOLD],
} }
export const RunCommand = cmd({ export const RunCommand = cmd({
command: "run [message..]", command: "run [message..]",
describe: "Run OpenCode with a message", describe: "run opencode with a message",
builder: (yargs: Argv) => { builder: (yargs: Argv) => {
return yargs return yargs
.positional("message", { .positional("message", {
describe: "Message to send", describe: "message to send",
type: "string", type: "string",
array: true, array: true,
default: [], default: [],
}) })
.option("continue", { .option("continue", {
alias: ["c"], alias: ["c"],
describe: "Continue the last session", describe: "continue the last session",
type: "boolean", type: "boolean",
}) })
.option("session", { .option("session", {
alias: ["s"], alias: ["s"],
describe: "Session ID to continue", describe: "session id to continue",
type: "string", type: "string",
}) })
.option("share", { .option("share", {
type: "boolean", type: "boolean",
describe: "Share the session", describe: "share the session",
}) })
.option("model", { .option("model", {
type: "string", type: "string",
alias: ["m"], 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) => { handler: async (args) => {
@@ -89,8 +90,8 @@ export const RunCommand = cmd({
await Session.share(session.id) await Session.share(session.id)
UI.println( UI.println(
UI.Style.TEXT_INFO_BOLD + UI.Style.TEXT_INFO_BOLD +
"~ https://dev.opencode.ai/s/" + "~ https://opencode.ai/s/" +
session.id.slice(-8), session.id.slice(-8),
) )
} }
UI.empty() UI.empty()
@@ -108,8 +109,8 @@ export const RunCommand = cmd({
UI.println( UI.println(
color + `|`, color + `|`,
UI.Style.TEXT_NORMAL + UI.Style.TEXT_NORMAL +
UI.Style.TEXT_DIM + UI.Style.TEXT_DIM +
` ${type.padEnd(7, " ")}`, ` ${type.padEnd(7, " ")}`,
"", "",
UI.Style.TEXT_NORMAL + title, UI.Style.TEXT_NORMAL + title,
) )
+16 -7
View File
@@ -5,19 +5,27 @@ import { Installation } from "../../installation"
export const UpgradeCommand = { export const UpgradeCommand = {
command: "upgrade [target]", 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) => { builder: (yargs: Argv) => {
return yargs.positional("target", { return yargs
describe: "Specific version to upgrade to (e.g., '0.1.48' or 'v0.1.48')", .positional("target", {
type: "string", 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.empty()
UI.println(UI.logo(" ")) UI.println(UI.logo(" "))
UI.empty() UI.empty()
prompts.intro("Upgrade") prompts.intro("Upgrade")
const method = await Installation.method() const detectedMethod = await Installation.method()
const method = (args.method as Installation.Method) ?? detectedMethod
if (method === "unknown") { if (method === "unknown") {
prompts.log.error( prompts.log.error(
`opencode is installed to ${process.execPath} and seems to be managed by a package manager`, `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") prompts.outro("Done")
return return
} }
prompts.log.info("Using method: " + method)
const target = args.target ?? (await Installation.latest()) const target = args.target ?? (await Installation.latest())
prompts.log.info(`From ${Installation.VERSION}${target}`) prompts.log.info(`From ${Installation.VERSION}${target}`)
const spinner = prompts.spinner() const spinner = prompts.spinner()
+16
View File
@@ -0,0 +1,16 @@
import { Config } from "../config/config"
import { MCP } from "../mcp"
export function FormatError(input: unknown) {
if (MCP.Failed.isInstance(input))
return `MCP server "${input.data.name}" failed. Note, opencode does not support MCP authentication yet.`
if (Config.JsonError.isInstance(input))
return `Config file at ${input.data.path} is not valid JSON`
if (Config.InvalidError.isInstance(input))
return [
`Config file at ${input.data.path} is invalid`,
...(input.data.issues?.map(
(issue) => "↳ " + issue.message + " " + issue.path.join("."),
) ?? []),
].join("\n")
}
+128 -63
View File
@@ -8,6 +8,7 @@ import { mergeDeep } from "remeda"
import { Global } from "../global" import { Global } from "../global"
import fs from "fs/promises" import fs from "fs/promises"
import { lazy } from "../util/lazy" import { lazy } from "../util/lazy"
import { NamedError } from "../util/error"
export namespace Config { export namespace Config {
const log = Log.create({ service: "config" }) const log = Log.create({ service: "config" })
@@ -15,27 +16,9 @@ export namespace Config {
export const state = App.state("config", async (app) => { export const state = App.state("config", async (app) => {
let result = await global() let result = await global()
for (const file of ["opencode.jsonc", "opencode.json"]) { for (const file of ["opencode.jsonc", "opencode.json"]) {
const [resolved] = await Filesystem.findUp( const found = await Filesystem.findUp(file, app.path.cwd, app.path.root)
file, for (const resolved of found.toReversed()) {
app.path.cwd, result = mergeDeep(result, await load(resolved))
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
} }
} }
log.info("loaded", result) log.info("loaded", result)
@@ -44,19 +27,27 @@ export namespace Config {
export const McpLocal = z export const McpLocal = z
.object({ .object({
type: z.literal("local"), type: z.literal("local").describe("Type of MCP server connection"),
command: z.string().array(), command: z
environment: z.record(z.string(), z.string()).optional(), .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({ .openapi({
ref: "Config.McpLocal", ref: "Config.McpLocal",
}) })
export const McpRemote = z export const McpRemote = z
.object({ .object({
type: z.literal("remote"), type: z.literal("remote").describe("Type of MCP server connection"),
url: z.string(), url: z.string().describe("URL of the remote MCP server"),
}) })
.strict()
.openapi({ .openapi({
ref: "Config.McpRemote", ref: "Config.McpRemote",
}) })
@@ -66,42 +57,85 @@ export namespace Config {
export const Keybinds = z export const Keybinds = z
.object({ .object({
leader: z.string().optional(), leader: z
help: z.string().optional(), .string()
editor_open: z.string().optional(), .optional()
session_new: z.string().optional(), .describe("Leader key for keybind combinations"),
session_list: z.string().optional(), help: z.string().optional().describe("Show help dialog"),
session_share: z.string().optional(), editor_open: z.string().optional().describe("Open external editor"),
session_interrupt: z.string().optional(), session_new: z.string().optional().describe("Create a new session"),
session_compact: z.string().optional(), session_list: z.string().optional().describe("List all sessions"),
tool_details: z.string().optional(), session_share: z.string().optional().describe("Share current session"),
model_list: z.string().optional(), session_interrupt: z
theme_list: z.string().optional(), .string()
project_init: z.string().optional(), .optional()
input_clear: z.string().optional(), .describe("Interrupt current session"),
input_paste: z.string().optional(), session_compact: z
input_submit: z.string().optional(), .string()
input_newline: z.string().optional(), .optional()
history_previous: z.string().optional(), .describe("Toggle compact mode for session"),
history_next: z.string().optional(), tool_details: z.string().optional().describe("Show tool details"),
messages_page_up: z.string().optional(), model_list: z.string().optional().describe("List available models"),
messages_page_down: z.string().optional(), theme_list: z.string().optional().describe("List available themes"),
messages_half_page_up: z.string().optional(), project_init: z
messages_half_page_down: z.string().optional(), .string()
messages_previous: z.string().optional(), .optional()
messages_next: z.string().optional(), .describe("Initialize project configuration"),
messages_first: z.string().optional(), input_clear: z.string().optional().describe("Clear input field"),
messages_last: z.string().optional(), input_paste: z.string().optional().describe("Paste from clipboard"),
app_exit: z.string().optional(), input_submit: z.string().optional().describe("Submit input"),
input_newline: z.string().optional().describe("Insert newline in input"),
history_previous: z
.string()
.optional()
.describe("Navigate to previous history item"),
history_next: z
.string()
.optional()
.describe("Navigate to next history item"),
messages_page_up: z
.string()
.optional()
.describe("Scroll messages up by one page"),
messages_page_down: z
.string()
.optional()
.describe("Scroll messages down by one page"),
messages_half_page_up: z
.string()
.optional()
.describe("Scroll messages up by half page"),
messages_half_page_down: z
.string()
.optional()
.describe("Scroll messages down by half page"),
messages_previous: z
.string()
.optional()
.describe("Navigate to previous message"),
messages_next: z.string().optional().describe("Navigate to next message"),
messages_first: z
.string()
.optional()
.describe("Navigate to first message"),
messages_last: z.string().optional().describe("Navigate to last message"),
app_exit: z.string().optional().describe("Exit the application"),
}) })
.strict()
.openapi({ .openapi({
ref: "Config.Keybinds", ref: "Config.Keybinds",
}) })
export const Info = z export const Info = z
.object({ .object({
$schema: z.string().optional(), $schema: z
theme: z.string().optional(), .string()
keybinds: Keybinds.optional(), .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 autoshare: z
.boolean() .boolean()
.optional() .optional()
@@ -127,9 +161,14 @@ export namespace Config {
options: z.record(z.any()).optional(), options: z.record(z.any()).optional(),
}), }),
) )
.optional(), .optional()
mcp: z.record(z.string(), Mcp).optional(), .describe("Custom provider configurations and model overrides"),
mcp: z
.record(z.string(), Mcp)
.optional()
.describe("MCP (Model Context Protocol) server configurations"),
}) })
.strict()
.openapi({ .openapi({
ref: "Config.Info", ref: "Config.Info",
}) })
@@ -137,10 +176,7 @@ export namespace Config {
export type Info = z.output<typeof Info> export type Info = z.output<typeof Info>
export const global = lazy(async () => { export const global = lazy(async () => {
let result = await Bun.file(path.join(Global.Path.config, "config.json")) let result = await load(path.join(Global.Path.config, "config.json"))
.json()
.then((mod) => Info.parse(mod))
.catch(() => ({}) as Info)
await import(path.join(Global.Path.config, "config"), { await import(path.join(Global.Path.config, "config"), {
with: { with: {
@@ -159,9 +195,38 @@ export namespace Config {
await fs.unlink(path.join(Global.Path.config, "config")) await fs.unlink(path.join(Global.Path.config, "config"))
}) })
.catch(() => {}) .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() { export function get() {
return state() return state()
} }
+12 -4
View File
@@ -41,6 +41,17 @@ export namespace Identifier {
return given 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( function generateNewID(
prefix: keyof typeof prefixes, prefix: keyof typeof prefixes,
descending: boolean, descending: boolean,
@@ -62,14 +73,11 @@ export namespace Identifier {
timeBytes[i] = Number((now >> BigInt(40 - 8 * i)) & BigInt(0xff)) timeBytes[i] = Number((now >> BigInt(40 - 8 * i)) & BigInt(0xff))
} }
const randLength = (LENGTH - 12) / 2
const random = randomBytes(randLength)
return ( return (
prefixes[prefix] + prefixes[prefix] +
"_" + "_" +
timeBytes.toString("hex") + timeBytes.toString("hex") +
random.toString("hex") randomBase62(LENGTH - 12)
) )
} }
} }
+49 -21
View File
@@ -18,12 +18,19 @@ import { UI } from "./cli/ui"
import { Installation } from "./installation" import { Installation } from "./installation"
import { Bus } from "./bus" import { Bus } from "./bus"
import { Config } from "./config/config" import { Config } from "./config/config"
import { NamedError } from "./util/error"
import { FormatError } from "./cli/error"
const cancel = new AbortController()
const cli = yargs(hideBin(process.argv)) const cli = yargs(hideBin(process.argv))
.scriptName("opencode") .scriptName("opencode")
.version(Installation.VERSION) .help("help", "show help")
.alias("help", "h")
.version("version", "show version number", Installation.VERSION)
.alias("version", "v")
.option("print-logs", { .option("print-logs", {
describe: "Print logs to stderr", describe: "print logs to stderr",
type: "boolean", type: "boolean",
}) })
.middleware(async () => { .middleware(async () => {
@@ -36,7 +43,7 @@ const cli = yargs(hideBin(process.argv))
.usage("\n" + UI.logo()) .usage("\n" + UI.logo())
.command({ .command({
command: "$0 [project]", command: "$0 [project]",
describe: "Start opencode TUI", describe: "start opencode tui",
builder: (yargs) => builder: (yargs) =>
yargs.positional("project", { yargs.positional("project", {
type: "string", type: "string",
@@ -70,6 +77,7 @@ const cli = yargs(hideBin(process.argv))
} }
const proc = Bun.spawn({ const proc = Bun.spawn({
cmd: [...cmd, ...process.argv.slice(2)], cmd: [...cmd, ...process.argv.slice(2)],
signal: cancel.signal,
cwd, cwd,
stdout: "inherit", stdout: "inherit",
stderr: "inherit", stderr: "inherit",
@@ -84,21 +92,21 @@ const cli = yargs(hideBin(process.argv))
}, },
}) })
;(async () => { ; (async () => {
if (Installation.VERSION === "dev") return if (Installation.VERSION === "dev") return
if (Installation.isSnapshot()) return if (Installation.isSnapshot()) return
const config = await Config.global() const config = await Config.global()
if (config.autoupdate === false) return if (config.autoupdate === false) return
const latest = await Installation.latest() const latest = await Installation.latest()
if (Installation.VERSION === latest) return if (Installation.VERSION === latest) return
const method = await Installation.method() const method = await Installation.method()
if (method === "unknown") return if (method === "unknown") return
await Installation.upgrade(method, latest) await Installation.upgrade(method, latest)
.then(() => { .then(() => {
Bus.publish(Installation.Event.Updated, { version: latest }) Bus.publish(Installation.Event.Updated, { version: latest })
}) })
.catch(() => {}) .catch(() => { })
})() })()
await proc.exited await proc.exited
server.stop() server.stop()
@@ -133,7 +141,27 @@ const cli = yargs(hideBin(process.argv))
try { try {
await cli.parse() await cli.parse()
} catch (e) { } catch (e) {
Log.Default.error(e, { const data: Record<string, any> = {}
stack: e instanceof Error ? e.stack : undefined, if (e instanceof NamedError) {
}) const obj = e.toObject()
Object.assign(data, {
...obj.data,
})
}
if (e instanceof Error) {
Object.assign(data, {
name: e.name,
message: e.message,
cause: e.cause?.toString(),
})
}
Log.Default.error("fatal", data)
const formatted = FormatError(e)
if (formatted) UI.error(formatted)
if (!formatted)
UI.error(
"Unexpected error, check log file at " + Log.file() + " for more details",
)
} }
cancel.abort()
+23 -1
View File
@@ -3,12 +3,15 @@ import { $ } from "bun"
import { z } from "zod" import { z } from "zod"
import { NamedError } from "../util/error" import { NamedError } from "../util/error"
import { Bus } from "../bus" import { Bus } from "../bus"
import { Log } from "../util/log"
declare global { declare global {
const OPENCODE_VERSION: string const OPENCODE_VERSION: string
} }
export namespace Installation { export namespace Installation {
const log = Log.create({ service: "installation" })
export type Method = Awaited<ReturnType<typeof method>> export type Method = Awaited<ReturnType<typeof method>>
export const Event = { export const Event = {
@@ -41,6 +44,10 @@ export namespace Installation {
return VERSION.startsWith("0.0.0") return VERSION.startsWith("0.0.0")
} }
export function isDev() {
return VERSION === "dev"
}
export async function method() { export async function method() {
if (process.execPath.includes(path.join(".opencode", "bin"))) return "curl" if (process.execPath.includes(path.join(".opencode", "bin"))) return "curl"
const exec = process.execPath.toLowerCase() const exec = process.execPath.toLowerCase()
@@ -62,6 +69,10 @@ export namespace Installation {
name: "bun" as const, name: "bun" as const,
command: () => $`bun pm ls -g`.throws(false).text(), 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) => { checks.sort((a, b) => {
@@ -93,18 +104,29 @@ export namespace Installation {
const cmd = (() => { const cmd = (() => {
switch (method) { switch (method) {
case "curl": 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": case "npm":
return $`npm install -g opencode-ai@${target}` return $`npm install -g opencode-ai@${target}`
case "pnpm": case "pnpm":
return $`pnpm install -g opencode-ai@${target}` return $`pnpm install -g opencode-ai@${target}`
case "bun": case "bun":
return $`bun install -g opencode-ai@${target}` return $`bun install -g opencode-ai@${target}`
case "brew":
return $`brew install sst/tap/opencode`
default: default:
throw new Error(`Unknown method: ${method}`) throw new Error(`Unknown method: ${method}`)
} }
})() })()
const result = await cmd.quiet().throws(false) const result = await cmd.quiet().throws(false)
log.info("upgraded", {
method,
target,
stdout: result.stdout.toString(),
stderr: result.stderr.toString(),
})
if (result.exitCode !== 0) if (result.exitCode !== 0)
throw new UpgradeFailedError({ throw new UpgradeFailedError({
stderr: result.stderr.toString("utf8"), stderr: result.stderr.toString("utf8"),
+3
View File
@@ -70,6 +70,9 @@ export namespace LSPServer {
const proc = Bun.spawn({ const proc = Bun.spawn({
cmd: ["go", "install", "golang.org/x/tools/gopls@latest"], cmd: ["go", "install", "golang.org/x/tools/gopls@latest"],
env: { ...process.env, GOBIN: Global.Path.bin }, env: { ...process.env, GOBIN: Global.Path.bin },
stdout: "pipe",
stderr: "pipe",
stdin: "pipe",
}) })
const exit = await proc.exited const exit = await proc.exited
if (exit !== 0) { 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 { Experimental_StdioMCPTransport } from "ai/mcp-stdio"
import { App } from "../app/app" import { App } from "../app/app"
import { Config } from "../config/config" 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 { export namespace MCP {
const log = Log.create({ service: "mcp" })
export const Failed = NamedError.create(
"MCPFailed",
z.object({
name: z.string(),
}),
)
const state = App.state( const state = App.state(
"mcp", "mcp",
async () => { async () => {
@@ -12,27 +26,56 @@ export namespace MCP {
[name: string]: Awaited<ReturnType<typeof experimental_createMCPClient>> [name: string]: Awaited<ReturnType<typeof experimental_createMCPClient>>
} = {} } = {}
for (const [key, mcp] of Object.entries(cfg.mcp ?? {})) { for (const [key, mcp] of Object.entries(cfg.mcp ?? {})) {
log.info("found", { key, type: mcp.type })
if (mcp.type === "remote") { if (mcp.type === "remote") {
clients[key] = await experimental_createMCPClient({ const client = await experimental_createMCPClient({
name: key, name: key,
transport: { transport: {
type: "sse", type: "sse",
url: mcp.url, 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") { if (mcp.type === "local") {
const [cmd, ...args] = mcp.command const [cmd, ...args] = mcp.command
clients[key] = await experimental_createMCPClient({ const client = await experimental_createMCPClient({
name: key, name: key,
transport: new Experimental_StdioMCPTransport({ transport: new Experimental_StdioMCPTransport({
stderr: "ignore", stderr: "ignore",
command: cmd, command: cmd,
args, 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"] title: Info["title"]
metadata: Info["metadata"] metadata: Info["metadata"]
}) { }) {
return
const { pending, approved } = state() const { pending, approved } = state()
log.info("asking", { log.info("asking", {
sessionID: input.sessionID, sessionID: input.sessionID,
+2
View File
@@ -13,6 +13,7 @@ export namespace ModelsDev {
attachment: z.boolean(), attachment: z.boolean(),
reasoning: z.boolean(), reasoning: z.boolean(),
temperature: z.boolean(), temperature: z.boolean(),
tool_call: z.boolean(),
cost: z.object({ cost: z.object({
input: z.number(), input: z.number(),
output: z.number(), output: z.number(),
@@ -24,6 +25,7 @@ export namespace ModelsDev {
output: z.number(), output: z.number(),
}), }),
id: z.string(), id: z.string(),
options: z.record(z.any()),
}) })
.openapi({ .openapi({
ref: "Model.Info", ref: "Model.Info",
+115 -31
View File
@@ -19,6 +19,7 @@ import type { Tool } from "../tool/tool"
import { WriteTool } from "../tool/write" import { WriteTool } from "../tool/write"
import { TodoReadTool, TodoWriteTool } from "../tool/todo" import { TodoReadTool, TodoWriteTool } from "../tool/todo"
import { AuthAnthropic } from "../auth/anthropic" import { AuthAnthropic } from "../auth/anthropic"
import { AuthGithubCopilot } from "../auth/github-copilot"
import { ModelsDev } from "./models" import { ModelsDev } from "./models"
import { NamedError } from "../util/error" import { NamedError } from "../util/error"
import { Auth } from "../auth" import { Auth } from "../auth"
@@ -27,9 +28,13 @@ import { TaskTool } from "../tool/task"
export namespace Provider { export namespace Provider {
const log = Log.create({ service: "provider" }) const log = Log.create({ service: "provider" })
type CustomLoader = ( type CustomLoader = (provider: ModelsDev.Provider) => Promise<
provider: ModelsDev.Provider, | {
) => Promise<Record<string, any> | false> getModel?: (sdk: any, modelID: string) => Promise<any>
options: Record<string, any>
}
| false
>
type Source = "env" | "config" | "custom" | "api" type Source = "env" | "config" | "custom" | "api"
@@ -44,30 +49,90 @@ export namespace Provider {
} }
} }
return { return {
apiKey: "", options: {
async fetch(input: any, init: any) { apiKey: "",
const access = await AuthAnthropic.access() async fetch(input: any, init: any) {
const headers = { const access = await AuthAnthropic.access()
...init.headers, const headers = {
authorization: `Bearer ${access}`, ...init.headers,
"anthropic-beta": "oauth-2025-04-20", authorization: `Bearer ${access}`,
} "anthropic-beta": "oauth-2025-04-20",
delete headers["x-api-key"] }
return fetch(input, { delete headers["x-api-key"]
...init, return fetch(input, {
headers, ...init,
}) headers,
})
},
}, },
} }
}, },
"github-copilot": async (provider) => {
const info = await AuthGithubCopilot.access()
if (!info) return false
if (provider && provider.models) {
for (const model of Object.values(provider.models)) {
model.cost = {
input: 0,
output: 0,
}
}
}
return {
options: {
apiKey: "",
async fetch(input: any, init: any) {
const token = await AuthGithubCopilot.access()
if (!token) throw new Error("GitHub Copilot authentication expired")
const headers = {
...init.headers,
Authorization: `Bearer ${token}`,
"User-Agent": "GitHubCopilotChat/0.26.7",
"Editor-Version": "vscode/1.99.3",
"Editor-Plugin-Version": "copilot-chat/0.26.7",
"Copilot-Integration-Id": "vscode-chat",
"Openai-Intent": "conversation-edits",
}
delete headers["x-api-key"]
return fetch(input, {
...init,
headers,
})
},
},
}
},
openai: async () => {
return {
async getModel(sdk: any, modelID: string) {
return sdk.responses(modelID)
},
options: {},
}
},
"amazon-bedrock": async () => { "amazon-bedrock": async () => {
if (!process.env["AWS_PROFILE"]) return false if (!process.env["AWS_PROFILE"] && !process.env["AWS_ACCESS_KEY_ID"])
return false
const region = process.env["AWS_REGION"] ?? "us-east-1"
const { fromNodeProviderChain } = await import( const { fromNodeProviderChain } = await import(
await BunProc.install("@aws-sdk/credential-providers") await BunProc.install("@aws-sdk/credential-providers")
) )
return { return {
region: process.env["AWS_REGION"] ?? "us-east-1", options: {
credentialProvider: fromNodeProviderChain(), 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 +145,7 @@ export namespace Provider {
[providerID: string]: { [providerID: string]: {
source: Source source: Source
info: ModelsDev.Provider info: ModelsDev.Provider
getModel?: (sdk: any, modelID: string) => Promise<any>
options: Record<string, any> options: Record<string, any>
} }
} = {} } = {}
@@ -95,6 +161,7 @@ export namespace Provider {
id: string, id: string,
options: Record<string, any>, options: Record<string, any>,
source: Source, source: Source,
getModel?: (sdk: any, modelID: string) => Promise<any>,
) { ) {
const provider = providers[id] const provider = providers[id]
if (!provider) { if (!provider) {
@@ -110,6 +177,7 @@ export namespace Provider {
} }
provider.options = mergeDeep(provider.options, options) provider.options = mergeDeep(provider.options, options)
provider.source = source provider.source = source
provider.getModel = getModel ?? provider.getModel
} }
const configProviders = Object.entries(config.provider ?? {}) const configProviders = Object.entries(config.provider ?? {})
@@ -132,13 +200,19 @@ export namespace Provider {
attachment: model.attachment ?? existing?.attachment ?? false, attachment: model.attachment ?? existing?.attachment ?? false,
reasoning: model.reasoning ?? existing?.reasoning ?? false, reasoning: model.reasoning ?? existing?.reasoning ?? false,
temperature: model.temperature ?? existing?.temperature ?? false, temperature: model.temperature ?? existing?.temperature ?? false,
cost: model.cost ?? tool_call: model.tool_call ?? existing?.tool_call ?? true,
existing?.cost ?? { cost: {
input: 0, ...existing?.cost,
output: 0, ...model.cost,
inputCached: 0, input: 0,
outputCached: 0, output: 0,
}, cache_read: 0,
cache_write: 0,
},
options: {
...existing?.options,
...model.options,
},
limit: model.limit ?? limit: model.limit ??
existing?.limit ?? { existing?.limit ?? {
context: 0, context: 0,
@@ -173,7 +247,9 @@ export namespace Provider {
for (const [providerID, fn] of Object.entries(CUSTOM_LOADERS)) { for (const [providerID, fn] of Object.entries(CUSTOM_LOADERS)) {
if (disabled.has(providerID)) continue if (disabled.has(providerID)) continue
const result = await fn(database[providerID]) const result = await fn(database[providerID])
if (result) mergeProvider(providerID, result, "custom") if (result) {
mergeProvider(providerID, result.options, "custom", result.getModel)
}
} }
// load config // load config
@@ -181,7 +257,11 @@ export namespace Provider {
mergeProvider(providerID, provider.options ?? {}, "config") 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 }) log.info("found", { providerID })
} }
@@ -232,9 +312,9 @@ export namespace Provider {
const sdk = await getSDK(provider.info) const sdk = await getSDK(provider.info)
try { try {
const language = const language = provider.getModel
// @ts-expect-error ? await provider.getModel(sdk, modelID)
"responses" in sdk ? sdk.responses(modelID) : sdk.languageModel(modelID) : sdk.languageModel(modelID)
log.info("found", { providerID, modelID }) log.info("found", { providerID, modelID })
s.models.set(key, { s.models.set(key, {
info, info,
@@ -322,6 +402,10 @@ export namespace Provider {
...t, ...t,
parameters: optionalToNullable(t.parameters), parameters: optionalToNullable(t.parameters),
})), })),
azure: TOOLS.map((t) => ({
...t,
parameters: optionalToNullable(t.parameters),
})),
google: TOOLS, 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 namespace ProviderTransform {
export function message( export function message(
msg: CoreMessage, msgs: LanguageModelV1Prompt,
index: number,
providerID: string, providerID: string,
modelID: string, modelID: string,
) { ) {
if ( if (providerID === "anthropic" || modelID.includes("anthropic")) {
(providerID === "anthropic" || modelID.includes("anthropic")) && const system = msgs.filter((msg) => msg.role === "system").slice(0, 2)
index < 4 const final = msgs.filter((msg) => msg.role !== "system").slice(-2)
) {
msg.providerOptions = { for (const msg of unique([...system, ...final])) {
...msg.providerOptions, msg.providerMetadata = {
anthropic: { ...msg.providerMetadata,
cacheControl: { type: "ephemeral" }, anthropic: {
}, cacheControl: { type: "ephemeral" },
},
}
} }
} }
return msgs
return msg
} }
} }
+28
View File
@@ -290,6 +290,34 @@ export namespace Server {
return c.json(session) 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( .post(
"/session_messages", "/session_messages",
describeRoute({ describeRoute({
-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"))
}
}
+78 -24
View File
@@ -14,6 +14,7 @@ import {
type CoreMessage, type CoreMessage,
type UIMessage, type UIMessage,
type ProviderMetadata, type ProviderMetadata,
wrapLanguageModel,
} from "ai" } from "ai"
import { z, ZodSchema } from "zod" import { z, ZodSchema } from "zod"
import { Decimal } from "decimal.js" import { Decimal } from "decimal.js"
@@ -159,6 +160,14 @@ export namespace Session {
return share 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) { export async function update(id: string, editor: (session: Info) => void) {
const { sessions } = state() const { sessions } = state()
const session = await get(id) const session = await get(id)
@@ -239,7 +248,7 @@ export namespace Session {
if ( if (
model.info.limit.context && model.info.limit.context &&
tokens > tokens >
(model.info.limit.context - (model.info.limit.output ?? 0)) * 0.9 (model.info.limit.context - (model.info.limit.output ?? 0)) * 0.9
) { ) {
await summarize({ await summarize({
sessionID: input.sessionID, sessionID: input.sessionID,
@@ -262,6 +271,7 @@ export namespace Session {
if (msgs.length === 0 && !session.parentID) { if (msgs.length === 0 && !session.parentID) {
generateText({ generateText({
maxTokens: input.providerID === "google" ? 1024 : 20, maxTokens: input.providerID === "google" ? 1024 : 20,
providerOptions: model.info.options,
messages: [ messages: [
...SystemPrompt.title(input.providerID).map( ...SystemPrompt.title(input.providerID).map(
(x): CoreMessage => ({ (x): CoreMessage => ({
@@ -276,9 +286,7 @@ export namespace Session {
parts: toParts(input.parts), parts: toParts(input.parts),
}, },
]), ]),
].map((msg, i) => ],
ProviderTransform.message(msg, i, input.providerID, input.modelID),
),
model: model.language, model: model.language,
}) })
.then((result) => { .then((result) => {
@@ -287,7 +295,7 @@ export namespace Session {
draft.title = result.text draft.title = result.text
}) })
}) })
.catch(() => {}) .catch(() => { })
} }
const msg: Message.Info = { const msg: Message.Info = {
role: "user", role: "user",
@@ -507,6 +515,7 @@ export namespace Session {
toolCallStreaming: true, toolCallStreaming: true,
abortSignal: abort.signal, abortSignal: abort.signal,
maxSteps: 1000, maxSteps: 1000,
providerOptions: model.info.options,
messages: [ messages: [
...system.map( ...system.map(
(x): CoreMessage => ({ (x): CoreMessage => ({
@@ -517,14 +526,26 @@ export namespace Session {
...convertToCoreMessages( ...convertToCoreMessages(
msgs.map(toUIMessage).filter((x) => x.parts.length > 0), 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, temperature: model.info.temperature ? 0 : undefined,
tools: { tools: model.info.tool_call === false ? undefined : tools,
...tools, model: wrapLanguageModel({
}, model: model.language,
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 { try {
for await (const value of result.fullStream) { for await (const value of result.fullStream) {
@@ -551,7 +572,7 @@ export namespace Session {
case "tool-call": { case "tool-call": {
const [match] = next.parts.flatMap((p) => const [match] = next.parts.flatMap((p) =>
p.type === "tool-invocation" && p.type === "tool-invocation" &&
p.toolInvocation.toolCallId === value.toolCallId p.toolInvocation.toolCallId === value.toolCallId
? [p] ? [p]
: [], : [],
) )
@@ -584,7 +605,7 @@ export namespace Session {
break break
case "tool-call-delta": case "tool-call-delta":
break continue
// for some reason ai sdk claims to not send this part but it does // for some reason ai sdk claims to not send this part but it does
// @ts-expect-error // @ts-expect-error
@@ -619,6 +640,7 @@ export namespace Session {
l.info("unhandled", { l.info("unhandled", {
type: value.type, type: value.type,
}) })
continue
} }
await updateMessage(next) await updateMessage(next)
} }
@@ -714,7 +736,9 @@ export namespace Session {
}, },
} }
await updateMessage(next) await updateMessage(next)
const result = await generateText({
let text: Message.TextPart | undefined
const result = streamText({
abortSignal: abort.signal, abortSignal: abort.signal,
model: model.language, model: model.language,
messages: [ messages: [
@@ -735,16 +759,46 @@ export namespace Session {
], ],
}, },
], ],
onStepFinish: async (step) => {
const assistant = next.metadata!.assistant!
const usage = getUsage(model.info, step.usage, step.providerMetadata)
assistant.cost += usage.cost
assistant.tokens = usage.tokens
await updateMessage(next)
if (text) {
Bus.publish(Message.Event.PartUpdated, {
part: text,
messageID: next.id,
sessionID: next.metadata.sessionID,
})
}
text = undefined
},
async onFinish(input) {
const assistant = next.metadata!.assistant!
const usage = getUsage(model.info, input.usage, input.providerMetadata)
assistant.cost = usage.cost
assistant.tokens = usage.tokens
next.metadata!.time.completed = Date.now()
await updateMessage(next)
},
}) })
next.parts.push({
type: "text", for await (const value of result.fullStream) {
text: result.text, switch (value.type) {
}) case "text-delta":
const assistant = next.metadata!.assistant! if (!text) {
const usage = getUsage(model.info, result.usage, result.providerMetadata) text = {
assistant.cost = usage.cost type: "text",
assistant.tokens = usage.tokens text: value.textDelta,
await updateMessage(next) }
next.parts.push(text)
} else text.text += value.textDelta
await updateMessage(next)
break
}
}
} }
function lock(sessionID: string) { function lock(sessionID: string) {
+46 -44
View File
@@ -135,53 +135,55 @@ export namespace Message {
id: z.string(), id: z.string(),
role: z.enum(["user", "assistant"]), role: z.enum(["user", "assistant"]),
parts: z.array(Part), parts: z.array(Part),
metadata: z.object({ metadata: z
time: z.object({ .object({
created: z.number(), time: z.object({
completed: z.number().optional(), created: z.number(),
}), completed: z.number().optional(),
error: z }),
.discriminatedUnion("name", [ error: z
Provider.AuthError.Schema, .discriminatedUnion("name", [
NamedError.Unknown.Schema, Provider.AuthError.Schema,
]) NamedError.Unknown.Schema,
.optional(), ])
sessionID: z.string(), .optional(),
tool: z.record( sessionID: z.string(),
z.string(), tool: z.record(
z z.string(),
z
.object({
title: z.string(),
time: z.object({
start: z.number(),
end: z.number(),
}),
})
.catchall(z.any()),
),
assistant: z
.object({ .object({
title: z.string(), system: z.string().array(),
time: z.object({ modelID: z.string(),
start: z.number(), providerID: z.string(),
end: z.number(), 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()), .optional(),
), })
assistant: z .openapi({ ref: "Message.Metadata" }),
.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(),
}),
}) })
.openapi({ .openapi({
ref: "Message.Info", 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 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.
- ensure it is not more than 50 characters long
- the title should be a summary of the user's message Requirements:
- it should be one line long - Maximum 50 characters
- do not use quotes or colons - Single line only - NO newlines or line breaks
- the entire text you return will be used as the title - Summary of the user's message
- never return anything that is more than one sentence (one line) long - 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 { App } from "../app/app"
import { Ripgrep } from "../external/ripgrep" import { Ripgrep } from "../external/ripgrep"
import { Global } from "../global"
import { Filesystem } from "../util/filesystem" import { Filesystem } from "../util/filesystem"
import path from "path"
import os from "os"
import PROMPT_ANTHROPIC from "./prompt/anthropic.txt" import PROMPT_ANTHROPIC from "./prompt/anthropic.txt"
import PROMPT_ANTHROPIC_SPOOF from "./prompt/anthropic_spoof.txt" import PROMPT_ANTHROPIC_SPOOF from "./prompt/anthropic_spoof.txt"
@@ -25,7 +28,7 @@ export namespace SystemPrompt {
export async function environment() { export async function environment() {
const app = App.info() const app = App.info()
const tree = async () => { ;async () => {
const files = await Ripgrep.files({ const files = await Ripgrep.files({
cwd: app.path.cwd, cwd: app.path.cwd,
}) })
@@ -82,9 +85,9 @@ export namespace SystemPrompt {
` Platform: ${process.platform}`, ` Platform: ${process.platform}`,
` Today's date: ${new Date().toDateString()}`, ` Today's date: ${new Date().toDateString()}`,
`</env>`, `</env>`,
`<project>`, // `<project>`,
` ${app.git ? await tree() : ""}`, // ` ${app.git ? await tree() : ""}`,
`</project>`, // `</project>`,
].join("\n"), ].join("\n"),
] ]
} }
@@ -101,7 +104,17 @@ export namespace SystemPrompt {
const matches = await Filesystem.findUp(item, cwd, root) const matches = await Filesystem.findUp(item, cwd, root)
found.push(...matches.map((x) => Bun.file(x).text())) 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) { export function summarize(providerID: string) {
+12 -1
View File
@@ -1,5 +1,6 @@
import { App } from "../app/app" import { App } from "../app/app"
import { Bus } from "../bus" import { Bus } from "../bus"
import { Installation } from "../installation"
import { Session } from "../session" import { Session } from "../session"
import { Storage } from "../storage/storage" import { Storage } from "../storage/storage"
import { Log } from "../util/log" import { Log } from "../util/log"
@@ -56,7 +57,10 @@ export namespace Share {
} }
export const URL = export const URL =
process.env["OPENCODE_API"] ?? "https://api.dev.opencode.ai" process.env["OPENCODE_API"] ??
(Installation.isSnapshot() || Installation.isDev()
? "https://api.dev.opencode.ai"
: "https://api.opencode.ai")
export async function create(sessionID: string) { export async function create(sessionID: string) {
return fetch(`${URL}/share_create`, { return fetch(`${URL}/share_create`, {
@@ -66,4 +70,11 @@ export namespace Share {
.then((x) => x.json()) .then((x) => x.json())
.then((x) => x as { url: string; secret: string }) .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())
}
} }
+5
View File
@@ -24,6 +24,11 @@ export namespace Storage {
} }
}) })
export async function remove(key: string) {
const target = path.join(state().dir, key + ".json")
await fs.unlink(target).catch(() => {})
}
export async function readJSON<T>(key: string) { export async function readJSON<T>(key: string) {
return Bun.file(path.join(state().dir, key + ".json")).json() as Promise<T> 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: { metadata: {
stderr, stderr,
stdout, stdout,
exit: process.exitCode,
description: params.description, description: params.description,
title: params.command, title: params.command,
}, },
output: stdout.replaceAll(/\x1b\[[0-9;]*m/g, ""), output: [
`<stdout>`,
stdout ?? "",
`</stdout>`,
`<stderr>`,
stderr ?? "",
`</stderr>`,
].join("\n"),
} }
}, },
}) })
+1 -1
View File
@@ -22,7 +22,7 @@ export const EditTool = Tool.define({
replaceAll: z replaceAll: z
.boolean() .boolean()
.optional() .optional()
.describe("Replace all occurences of old_string (default false)"), .describe("Replace all occurrences of old_string (default false)"),
}), }),
async execute(params, ctx) { async execute(params, ctx) {
if (!params.filePath) { if (!params.filePath) {
+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: 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) - 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 - 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: IMPORTANT:
- All edits are applied in sequence, in the order they are provided - All edits are applied in sequence, in the order they are provided
+4 -1
View File
@@ -34,13 +34,16 @@ export const TaskTool = Tool.define({
} }
const unsub = Bus.subscribe(Message.Event.Updated, async (evt) => { const unsub = Bus.subscribe(Message.Event.Updated, async (evt) => {
if (evt.properties.info.metadata.sessionID !== ctx.sessionID) return if (evt.properties.info.metadata.sessionID !== session.id) return
ctx.metadata({ ctx.metadata({
title: params.description, title: params.description,
summary: summary(evt.properties.info), summary: summary(evt.properties.info),
}) })
}) })
ctx.abort.addEventListener("abort", () => {
Session.abort(session.id)
})
const result = await Session.chat({ const result = await Session.chat({
sessionID: session.id, sessionID: session.id,
modelID: metadata.modelID, modelID: metadata.modelID,
+1 -1
View File
@@ -1,5 +1,5 @@
- Allows OpenCode to search the web and use the results to inform responses - Allows opencode to search the web and use the results to inform responses
- Provides up-to-date information for current events and recent data - Provides up-to-date information for current events and recent data
- Returns search result information formatted as search result blocks - Returns search result information formatted as search result blocks
- Use this tool for accessing information beyond Claude's knowledge cutoff - Use this tool for accessing information beyond Claude's knowledge cutoff
+2 -6
View File
@@ -1,7 +1,7 @@
import { z, type ZodSchema } from "zod" 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 { export abstract class NamedError extends Error {
abstract schema(): ZodSchema abstract schema(): ZodSchema
@@ -30,10 +30,6 @@ export abstract class NamedError extends Error {
) { ) {
super(name, options) super(name, options)
this.name = name this.name = name
log.error(name, {
...this.data,
cause: options?.cause?.toString(),
})
} }
static isInstance(input: any): input is InstanceType<typeof result> { static isInstance(input: any): input is InstanceType<typeof result> {
+3 -3
View File
@@ -68,13 +68,13 @@ export namespace Log {
} }
const result = { const result = {
info(message?: any, extra?: Record<string, any>) { 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>) { 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>) { 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) { tag(key: string, value: string) {
if (tags) tags[key] = value if (tags) tags[key] = value
+2 -2
View File
@@ -14,7 +14,7 @@ describe("tool.glob", () => {
await App.provide({ cwd: process.cwd() }, async () => { await App.provide({ cwd: process.cwd() }, async () => {
let result = await GlobTool.execute( let result = await GlobTool.execute(
{ {
pattern: "./node_modules/**/*", pattern: "../../node_modules/**/*",
path: undefined, path: undefined,
}, },
ctx, ctx,
@@ -33,7 +33,7 @@ describe("tool.glob", () => {
) )
expect(result.metadata).toMatchObject({ expect(result.metadata).toMatchObject({
truncated: false, truncated: false,
count: 2, count: 3,
}) })
}) })
}) })
+5 -1
View File
@@ -26,7 +26,11 @@ func main() {
appInfoStr := os.Getenv("OPENCODE_APP_INFO") appInfoStr := os.Getenv("OPENCODE_APP_INFO")
var appInfo client.AppInfo 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") logfile := filepath.Join(appInfo.Path.Data, "log", "tui.log")
if _, err := os.Stat(filepath.Dir(logfile)); os.IsNotExist(err) { if _, err := os.Stat(filepath.Dir(logfile)); os.IsNotExist(err) {
+66 -28
View File
@@ -6,11 +6,13 @@ import (
"path/filepath" "path/filepath"
"sort" "sort"
"strings" "strings"
"time"
"log/slog" "log/slog"
tea "github.com/charmbracelet/bubbletea/v2" tea "github.com/charmbracelet/bubbletea/v2"
"github.com/sst/opencode/internal/commands" "github.com/sst/opencode/internal/commands"
"github.com/sst/opencode/internal/components/toast"
"github.com/sst/opencode/internal/config" "github.com/sst/opencode/internal/config"
"github.com/sst/opencode/internal/theme" "github.com/sst/opencode/internal/theme"
"github.com/sst/opencode/internal/util" "github.com/sst/opencode/internal/util"
@@ -23,7 +25,7 @@ type App struct {
Info client.AppInfo Info client.AppInfo
Version string Version string
StatePath string StatePath string
Configg *client.ConfigInfo Config *client.ConfigInfo
Client *client.ClientWithResponses Client *client.ClientWithResponses
State *config.State State *config.State
Provider *client.ProviderInfo Provider *client.ProviderInfo
@@ -44,9 +46,12 @@ type SendMsg struct {
Text string Text string
Attachments []Attachment Attachments []Attachment
} }
type CompletionDialogTriggerdMsg struct { type CompletionDialogTriggeredMsg struct {
InitialValue string InitialValue string
} }
type OptimisticMessageAddedMsg struct {
Message client.MessageInfo
}
func New( func New(
ctx context.Context, ctx context.Context,
@@ -60,6 +65,9 @@ func New(
if err != nil { if err != nil {
return nil, err return nil, err
} }
if configResponse.StatusCode() != 200 || configResponse.JSON200 == nil {
return nil, fmt.Errorf("failed to get config: %d", configResponse.StatusCode())
}
configInfo := configResponse.JSON200 configInfo := configResponse.JSON200
if configInfo.Keybinds == nil { if configInfo.Keybinds == nil {
leader := "ctrl+x" leader := "ctrl+x"
@@ -85,6 +93,15 @@ func New(
appState.Model = strings.Join(splits[1:], "/") 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 != "" { if appState.Theme != "" {
theme.SetTheme(appState.Theme) theme.SetTheme(appState.Theme)
} }
@@ -95,7 +112,7 @@ func New(
Info: appInfo, Info: appInfo,
Version: version, Version: version,
StatePath: appStatePath, StatePath: appStatePath,
Configg: configInfo, Config: configInfo,
State: appState, State: appState,
Client: httpClient, Client: httpClient,
Session: &client.SessionInfo{}, Session: &client.SessionInfo{},
@@ -114,6 +131,10 @@ func (a *App) InitializeProvider() tea.Cmd {
// TODO: notify user // TODO: notify user
return nil 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{} providers := []client.ProviderInfo{}
var defaultProvider *client.ProviderInfo var defaultProvider *client.ProviderInfo
var defaultModel *client.ModelInfo 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 { func (a *App) CompactSession(ctx context.Context) tea.Cmd {
response, err := a.Client.PostSessionSummarizeWithResponse(ctx, client.PostSessionSummarizeJSONRequestBody{ go func() {
SessionID: a.Session.Id, response, err := a.Client.PostSessionSummarizeWithResponse(ctx, client.PostSessionSummarizeJSONRequestBody{
ProviderID: a.Provider.Id, SessionID: a.Session.Id,
ModelID: a.Model.Id, ProviderID: a.Provider.Id,
}) ModelID: a.Model.Id,
if err != nil { })
slog.Error("Failed to compact session", "error", err) 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) if response != nil && response.StatusCode() != 200 {
} slog.Error("Failed to compact session", "error", response.StatusCode)
}
}()
return nil return nil
} }
@@ -279,19 +302,12 @@ func (a *App) SendChatMessage(ctx context.Context, text string, attachments []At
if a.Session.Id == "" { if a.Session.Id == "" {
session, err := a.CreateSession(ctx) session, err := a.CreateSession(ctx)
if err != nil { if err != nil {
// status.Error(err.Error()) return toast.NewErrorToast(err.Error())
return nil
} }
a.Session = session a.Session = session
cmds = append(cmds, util.CmdHandler(SessionSelectedMsg(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 := client.MessagePart{}
part.FromMessagePartText(client.MessagePartText{ part.FromMessagePartText(client.MessagePartText{
Type: "text", Type: "text",
@@ -299,7 +315,26 @@ func (a *App) SendChatMessage(ctx context.Context, text string, attachments []At
}) })
parts := []client.MessagePart{part} 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{ response, err := a.Client.PostSessionChat(ctx, client.PostSessionChatJSONRequestBody{
SessionID: a.Session.Id, SessionID: a.Session.Id,
Parts: parts, Parts: parts,
@@ -307,14 +342,17 @@ func (a *App) SendChatMessage(ctx context.Context, text string, attachments []At
ModelID: a.Model.Id, ModelID: a.Model.Id,
}) })
if err != nil { if err != nil {
slog.Error("Failed to send message", "error", err) errormsg := fmt.Sprintf("failed to send message: %v", err)
// status.Error(err.Error()) slog.Error(errormsg)
return toast.NewErrorToast(errormsg)()
} }
if response != nil && response.StatusCode != 200 { if response != nil && response.StatusCode != 200 {
slog.Error("Failed to send message", "error", fmt.Sprintf("failed to send message: %d", response.StatusCode)) errormsg := fmt.Sprintf("failed to send message: %d", response.StatusCode)
// status.Error(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 // The actual response will come through SSE
// For now, just return success // For now, just return success
+1 -1
View File
@@ -186,7 +186,7 @@ func LoadFromConfig(config *client.ConfigInfo) CommandRegistry {
}, },
{ {
Name: ProjectInitCommand, Name: ProjectInitCommand,
Description: "create or update AGENTS.md", Description: "create/update AGENTS.md",
Keybindings: parseBindings("<leader>i"), Keybindings: parseBindings("<leader>i"),
Trigger: "init", Trigger: "init",
}, },
@@ -35,8 +35,7 @@ func (c *CommandCompletionProvider) GetEmptyMessage() string {
return "no matching commands" return "no matching commands"
} }
func getCommandCompletionItem(cmd commands.Command, space int) dialog.CompletionItemI { func getCommandCompletionItem(cmd commands.Command, space int, t theme.Theme) dialog.CompletionItemI {
t := theme.CurrentTheme()
spacer := strings.Repeat(" ", space) spacer := strings.Repeat(" ", space)
title := " /" + cmd.Trigger + lipgloss.NewStyle().Foreground(t.TextMuted()).Render(spacer+cmd.Description) title := " /" + cmd.Trigger + lipgloss.NewStyle().Foreground(t.TextMuted()).Render(spacer+cmd.Description)
value := string(cmd.Name) 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) { func (c *CommandCompletionProvider) GetChildEntries(query string) ([]dialog.CompletionItemI, error) {
t := theme.CurrentTheme()
commands := c.app.Commands
space := 1 space := 1
for _, cmd := range c.app.Commands { for _, cmd := range c.app.Commands {
if lipgloss.Width(cmd.Trigger) > space { if lipgloss.Width(cmd.Trigger) > space {
@@ -55,15 +57,16 @@ func (c *CommandCompletionProvider) GetChildEntries(query string) ([]dialog.Comp
} }
space += 2 space += 2
sorted := commands.Sorted()
if query == "" { if query == "" {
// If no query, return all commands // If no query, return all commands
items := []dialog.CompletionItemI{} items := []dialog.CompletionItemI{}
for _, cmd := range c.app.Commands { for _, cmd := range sorted {
if cmd.Trigger == "" { if cmd.Trigger == "" {
continue continue
} }
space := space - lipgloss.Width(cmd.Trigger) space := space - lipgloss.Width(cmd.Trigger)
items = append(items, getCommandCompletionItem(cmd, space)) items = append(items, getCommandCompletionItem(cmd, space, t))
} }
return items, nil return items, nil
} }
@@ -72,13 +75,13 @@ func (c *CommandCompletionProvider) GetChildEntries(query string) ([]dialog.Comp
var commandNames []string var commandNames []string
commandMap := make(map[string]dialog.CompletionItemI) commandMap := make(map[string]dialog.CompletionItemI)
for _, cmd := range c.app.Commands { for _, cmd := range sorted {
if cmd.Trigger == "" { if cmd.Trigger == "" {
continue continue
} }
space := space - lipgloss.Width(cmd.Trigger) space := space - lipgloss.Width(cmd.Trigger)
commandNames = append(commandNames, cmd.Trigger) commandNames = append(commandNames, cmd.Trigger)
commandMap[cmd.Trigger] = getCommandCompletionItem(cmd, space) commandMap[cmd.Trigger] = getCommandCompletionItem(cmd, space, t)
} }
// Find fuzzy matches // Find fuzzy matches
@@ -54,6 +54,9 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd var cmd tea.Cmd
switch msg := msg.(type) { switch msg := msg.(type) {
case spinner.TickMsg:
m.spinner, cmd = m.spinner.Update(msg)
return m, cmd
case tea.KeyPressMsg: case tea.KeyPressMsg:
// Maximize editor responsiveness for printable characters // Maximize editor responsiveness for printable characters
if msg.Text != "" { if msg.Text != "" {
@@ -110,9 +113,6 @@ func (m *editorComponent) Content() string {
PaddingTop(1). PaddingTop(1).
PaddingBottom(1). PaddingBottom(1).
Background(t.BackgroundElement()). Background(t.BackgroundElement()).
Border(lipgloss.ThickBorder(), false, true).
BorderForeground(t.BackgroundElement()).
BorderBackground(t.Background()).
Render(textarea) Render(textarea)
hint := base("enter") + muted(" send ") hint := base("enter") + muted(" send ")
@@ -217,8 +217,7 @@ func (m *editorComponent) Paste() (tea.Model, tea.Cmd) {
} }
func (m *editorComponent) Newline() (tea.Model, tea.Cmd) { func (m *editorComponent) Newline() (tea.Model, tea.Cmd) {
value := m.textarea.Value() m.textarea.Newline()
m.textarea.SetValue(value + "\n")
return m, nil return m, nil
} }
+104 -51
View File
@@ -1,6 +1,7 @@
package chat package chat
import ( import (
"encoding/json"
"fmt" "fmt"
"path/filepath" "path/filepath"
"slices" "slices"
@@ -43,7 +44,6 @@ func toMarkdown(content string, width int, backgroundColor compat.AdaptiveColor)
} }
} }
} }
content = strings.Join(lines, "\n") content = strings.Join(lines, "\n")
return strings.TrimSuffix(content, "\n") return strings.TrimSuffix(content, "\n")
} }
@@ -136,7 +136,7 @@ func renderContentBlock(content string, options ...renderingOption) string {
PaddingBottom(renderer.paddingBottom). PaddingBottom(renderer.paddingBottom).
PaddingLeft(renderer.paddingLeft). PaddingLeft(renderer.paddingLeft).
PaddingRight(renderer.paddingRight). PaddingRight(renderer.paddingRight).
Background(t.BackgroundSubtle()). Background(t.BackgroundPanel()).
Foreground(t.TextMuted()). Foreground(t.TextMuted()).
BorderStyle(lipgloss.ThickBorder()) BorderStyle(lipgloss.ThickBorder())
@@ -145,7 +145,7 @@ func renderContentBlock(content string, options ...renderingOption) string {
align = *renderer.align align = *renderer.align
} }
borderColor := t.BackgroundSubtle() borderColor := t.BackgroundPanel()
if renderer.borderColor != nil { if renderer.borderColor != nil {
borderColor = *renderer.borderColor borderColor = *renderer.borderColor
} }
@@ -158,7 +158,7 @@ func renderContentBlock(content string, options ...renderingOption) string {
AlignHorizontal(align). AlignHorizontal(align).
BorderLeftForeground(borderColor). BorderLeftForeground(borderColor).
BorderLeftBackground(t.Background()). BorderLeftBackground(t.Background()).
BorderRightForeground(t.BackgroundSubtle()). BorderRightForeground(t.BackgroundPanel()).
BorderRightBackground(t.Background()) BorderRightBackground(t.Background())
case lipgloss.Right: case lipgloss.Right:
style = style. style = style.
@@ -167,7 +167,7 @@ func renderContentBlock(content string, options ...renderingOption) string {
AlignHorizontal(align). AlignHorizontal(align).
BorderRightForeground(borderColor). BorderRightForeground(borderColor).
BorderRightBackground(t.Background()). BorderRightBackground(t.Background()).
BorderLeftForeground(t.BackgroundSubtle()). BorderLeftForeground(t.BackgroundPanel()).
BorderLeftBackground(t.Background()) BorderLeftBackground(t.Background())
} }
@@ -226,9 +226,13 @@ func renderText(message client.MessageInfo, text string, author string) string {
textWidth := max(lipgloss.Width(text), lipgloss.Width(info)) textWidth := max(lipgloss.Width(text), lipgloss.Width(info))
markdownWidth := min(textWidth, width-padding-4) // -4 for the border and padding markdownWidth := min(textWidth, width-padding-4) // -4 for the border and padding
if message.Role == client.Assistant { 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") content = strings.Join([]string{content, info}, "\n")
switch message.Role { switch message.Role {
@@ -249,9 +253,10 @@ func renderText(message client.MessageInfo, text string, author string) string {
func renderToolInvocation( func renderToolInvocation(
toolCall client.MessageToolInvocationToolCall, toolCall client.MessageToolInvocationToolCall,
result *string, result *string,
metadata client.MessageInfo_Metadata_Tool_AdditionalProperties, metadata client.MessageMetadata_Tool_AdditionalProperties,
showDetails bool, showDetails bool,
isLast bool, isLast bool,
contentOnly bool,
) string { ) string {
ignoredTools := []string{"todoread"} ignoredTools := []string{"todoread"}
if slices.Contains(ignoredTools, toolCall.ToolName) { if slices.Contains(ignoredTools, toolCall.ToolName) {
@@ -272,7 +277,7 @@ func renderToolInvocation(
t := theme.CurrentTheme() t := theme.CurrentTheme()
style := styles.Muted(). style := styles.Muted().
Width(outerWidth). Width(outerWidth).
Background(t.BackgroundSubtle()). Background(t.BackgroundPanel()).
PaddingTop(paddingTop). PaddingTop(paddingTop).
PaddingBottom(paddingBottom). PaddingBottom(paddingBottom).
PaddingLeft(2). PaddingLeft(2).
@@ -280,7 +285,7 @@ func renderToolInvocation(
BorderLeft(true). BorderLeft(true).
BorderRight(true). BorderRight(true).
BorderBackground(t.Background()). BorderBackground(t.Background()).
BorderForeground(t.BackgroundSubtle()). BorderForeground(t.BackgroundPanel()).
BorderStyle(lipgloss.ThickBorder()) BorderStyle(lipgloss.ThickBorder())
if toolCall.State == "partial-call" { if toolCall.State == "partial-call" {
@@ -288,7 +293,7 @@ func renderToolInvocation(
if !showDetails { if !showDetails {
title = "∟ " + title title = "∟ " + title
padding := calculatePadding() 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), return renderContentBlock(style.Render(title),
WithAlign(lipgloss.Left), WithAlign(lipgloss.Left),
WithBorderColor(t.Accent()), WithBorderColor(t.Accent()),
@@ -305,14 +310,19 @@ func renderToolInvocation(
toolArgsMap := make(map[string]any) toolArgsMap := make(map[string]any)
if toolCall.Args != nil { if toolCall.Args != nil {
value := *toolCall.Args value := *toolCall.Args
m, ok := value.(map[string]any) if m, ok := value.(map[string]any); ok {
if ok {
toolArgsMap = m toolArgsMap = m
firstKey := ""
keys := make([]string, 0, len(toolArgsMap))
for key := range toolArgsMap { for key := range toolArgsMap {
firstKey = key keys = append(keys, key)
break
} }
slices.Sort(keys)
firstKey := ""
if len(keys) > 0 {
firstKey = keys[0]
}
toolArgs = renderArgs(&toolArgsMap, firstKey) toolArgs = renderArgs(&toolArgsMap, firstKey)
} }
} }
@@ -325,7 +335,7 @@ func renderToolInvocation(
if m, ok := metadata.Get("message"); ok { if m, ok := metadata.Get("message"); ok {
style = style.BorderLeftForeground(t.Error()) style = style.BorderLeftForeground(t.Error())
error = styles.BaseStyle(). error = styles.BaseStyle().
Background(t.BackgroundSubtle()). Background(t.BackgroundPanel()).
Foreground(t.Error()). Foreground(t.Error()).
Render(m.(string)) Render(m.(string))
error = renderContentBlock( error = renderContentBlock(
@@ -337,22 +347,11 @@ func renderToolInvocation(
} }
} }
elapsed := ""
start := metadata.Time.Start
end := metadata.Time.End
durationMs := end - start
duration := time.Duration(durationMs * float32(time.Millisecond))
roundedDuration := time.Duration(duration.Round(time.Millisecond))
if durationMs > 1000 {
roundedDuration = time.Duration(duration.Round(time.Second))
}
elapsed = styles.Muted().Render(roundedDuration.String())
title := "" title := ""
switch toolCall.ToolName { switch toolCall.ToolName {
case "read": case "read":
toolArgs = renderArgs(&toolArgsMap, "filePath") toolArgs = renderArgs(&toolArgsMap, "filePath")
title = fmt.Sprintf("READ %s %s", toolArgs, elapsed) title = fmt.Sprintf("READ %s", toolArgs)
if preview, ok := metadata.Get("preview"); ok && toolArgsMap["filePath"] != nil { if preview, ok := metadata.Get("preview"); ok && toolArgsMap["filePath"] != nil {
filename := toolArgsMap["filePath"].(string) filename := toolArgsMap["filePath"].(string)
body = preview.(string) body = preview.(string)
@@ -360,7 +359,7 @@ func renderToolInvocation(
} }
case "edit": case "edit":
if filename, ok := toolArgsMap["filePath"].(string); ok { if filename, ok := toolArgsMap["filePath"].(string); ok {
title = fmt.Sprintf("EDIT %s %s", relative(filename), elapsed) title = fmt.Sprintf("EDIT %s", relative(filename))
if d, ok := metadata.Get("diff"); ok { if d, ok := metadata.Get("diff"); ok {
patch := d.(string) patch := d.(string)
var formattedDiff string var formattedDiff string
@@ -378,7 +377,7 @@ func renderToolInvocation(
formattedDiff = lipgloss.NewStyle(). formattedDiff = lipgloss.NewStyle().
BorderStyle(lipgloss.ThickBorder()). BorderStyle(lipgloss.ThickBorder()).
BorderBackground(t.Background()). BorderBackground(t.Background()).
BorderForeground(t.BackgroundSubtle()). BorderForeground(t.BackgroundPanel()).
BorderLeft(true). BorderLeft(true).
BorderRight(true). BorderRight(true).
Render(formattedDiff) Render(formattedDiff)
@@ -401,35 +400,37 @@ func renderToolInvocation(
} }
case "write": case "write":
if filename, ok := toolArgsMap["filePath"].(string); ok { if filename, ok := toolArgsMap["filePath"].(string); ok {
title = fmt.Sprintf("WRITE %s %s", relative(filename), elapsed) title = fmt.Sprintf("WRITE %s", relative(filename))
if content, ok := toolArgsMap["content"].(string); ok { if content, ok := toolArgsMap["content"].(string); ok {
body = renderFile(filename, content) body = renderFile(filename, content)
} }
} }
case "bash": case "bash":
if description, ok := toolArgsMap["description"].(string); ok { if description, ok := toolArgsMap["description"].(string); ok {
title = fmt.Sprintf("SHELL %s %s", description, elapsed) title = fmt.Sprintf("SHELL %s", description)
} }
if stdout, ok := metadata.Get("stdout"); ok { if stdout, ok := metadata.Get("stdout"); ok {
command := toolArgsMap["command"].(string) command := toolArgsMap["command"].(string)
stdout := stdout.(string) stdout := stdout.(string)
body = fmt.Sprintf("```console\n> %s\n%s```", command, stdout) 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)) body = renderContentBlock(body, WithFullWidth(), WithMarginBottom(1))
} }
case "webfetch": case "webfetch":
toolArgs = renderArgs(&toolArgsMap, "url") toolArgs = renderArgs(&toolArgsMap, "url")
title = fmt.Sprintf("FETCH %s %s", toolArgs, elapsed) title = fmt.Sprintf("FETCH %s", toolArgs)
if format, ok := toolArgsMap["format"].(string); ok { if format, ok := toolArgsMap["format"].(string); ok {
body = *result if result != nil {
body = truncateHeight(body, 10) body = *result
if format == "html" || format == "markdown" { body = truncateHeight(body, 10)
body = toMarkdown(body, innerWidth, t.BackgroundSubtle()) 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": case "todowrite":
title = fmt.Sprintf("PLAN %s", elapsed) title = fmt.Sprintf("PLAN")
if to, ok := metadata.Get("todos"); ok && finished { if to, ok := metadata.Get("todos"); ok && finished {
todos := to.([]any) todos := to.([]any)
@@ -445,21 +446,67 @@ func renderToolInvocation(
body += fmt.Sprintf("- [ ] %s\n", content) body += fmt.Sprintf("- [ ] %s\n", content)
} }
} }
body = toMarkdown(body, innerWidth, t.BackgroundSubtle()) body = toMarkdown(body, innerWidth, t.BackgroundPanel())
body = renderContentBlock(body, WithFullWidth(), WithMarginBottom(1)) body = renderContentBlock(body, WithFullWidth(), WithMarginBottom(1))
} }
case "task":
if description, ok := toolArgsMap["description"].(string); ok {
title = fmt.Sprintf("TASK %s", description)
if summary, ok := metadata.Get("summary"); ok {
toolcalls := summary.([]any)
// toolcalls :=
steps := []string{}
for _, toolcall := range toolcalls {
call := toolcall.(map[string]any)
if toolInvocation, ok := call["toolInvocation"].(map[string]any); ok {
data, _ := json.Marshal(toolInvocation)
var toolCall client.MessageToolInvocationToolCall
_ = json.Unmarshal(data, &toolCall)
if metadata, ok := call["metadata"].(map[string]any); ok {
data, _ = json.Marshal(metadata)
var toolMetadata client.MessageMetadata_Tool_AdditionalProperties
_ = json.Unmarshal(data, &toolMetadata)
step := renderToolInvocation(
toolCall,
nil,
toolMetadata,
false,
false,
true,
)
steps = append(steps, step)
}
}
}
body = strings.Join(steps, "\n")
body = renderContentBlock(body, WithFullWidth(), WithMarginBottom(1))
}
}
default: default:
toolName := renderToolName(toolCall.ToolName) toolName := renderToolName(toolCall.ToolName)
title = fmt.Sprintf("%s %s %s", toolName, toolArgs, elapsed) title = fmt.Sprintf("%s %s", toolName, toolArgs)
if result == nil {
empty := ""
result = &empty
}
body = *result body = *result
body = truncateHeight(body, 10) body = truncateHeight(body, 10)
body = renderContentBlock(body, WithFullWidth(), WithMarginBottom(1)) body = renderContentBlock(body, WithFullWidth(), WithMarginBottom(1))
} }
if contentOnly {
title = "∟ " + title
return title
}
if !showDetails { if !showDetails {
title = "∟ " + title title = "∟ " + title
padding := calculatePadding() padding := calculatePadding()
style := lipgloss.NewStyle().Width(outerWidth - padding - 4).Background(t.BackgroundSubtle()) style := lipgloss.NewStyle().Width(outerWidth - padding - 4).Background(t.BackgroundPanel())
paddingBottom := 0 paddingBottom := 0
if isLast { if isLast {
paddingBottom = 1 paddingBottom = 1
@@ -496,8 +543,6 @@ func renderToolInvocation(
func renderToolName(name string) string { func renderToolName(name string) string {
switch name { switch name {
// case agent.AgentToolName:
// return "Task"
case "list": case "list":
return "LIST" return "LIST"
case "webfetch": case "webfetch":
@@ -550,21 +595,21 @@ func renderFile(filename string, content string, options ...fileRenderingOption)
content = truncateHeight(content, renderer.height) content = truncateHeight(content, renderer.height)
} }
content = fmt.Sprintf("```%s\n%s\n```", extension(renderer.filename), content) 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)) return renderContentBlock(content, WithFullWidth(), WithMarginBottom(1))
} }
func renderToolAction(name string) string { func renderToolAction(name string) string {
switch name { switch name {
// case agent.AgentToolName: case "task":
// return "Preparing prompt..." return "Searching..."
case "bash": case "bash":
return "Building command..." return "Building command..."
case "edit": case "edit":
return "Preparing edit..." return "Preparing edit..."
case "webfetch": case "webfetch":
return "Writing fetch..." return "Fetching from the web..."
case "glob": case "glob":
return "Finding files..." return "Finding files..."
case "grep": case "grep":
@@ -589,9 +634,17 @@ func renderArgs(args *map[string]any, titleKey string) string {
if args == nil || len(*args) == 0 { if args == nil || len(*args) == 0 {
return "" return ""
} }
keys := make([]string, 0, len(*args))
for key := range *args {
keys = append(keys, key)
}
slices.Sort(keys)
title := "" title := ""
parts := []string{} parts := []string{}
for key, value := range *args { for _, key := range keys {
value := (*args)[key]
if value == nil { if value == nil {
continue continue
} }
@@ -10,6 +10,7 @@ import (
tea "github.com/charmbracelet/bubbletea/v2" tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/lipgloss/v2" "github.com/charmbracelet/lipgloss/v2"
"github.com/sst/opencode/internal/app" "github.com/sst/opencode/internal/app"
"github.com/sst/opencode/internal/components/commands"
"github.com/sst/opencode/internal/components/dialog" "github.com/sst/opencode/internal/components/dialog"
"github.com/sst/opencode/internal/layout" "github.com/sst/opencode/internal/layout"
"github.com/sst/opencode/internal/styles" "github.com/sst/opencode/internal/styles"
@@ -37,6 +38,7 @@ type messagesComponent struct {
viewport viewport.Model viewport viewport.Model
spinner spinner.Model spinner spinner.Model
attachments viewport.Model attachments viewport.Model
commands commands.CommandsComponent
cache *MessageCache cache *MessageCache
rendering bool rendering bool
showToolDetails bool showToolDetails bool
@@ -46,7 +48,7 @@ type renderFinishedMsg struct{}
type ToggleToolDetailsMsg struct{} type ToggleToolDetailsMsg struct{}
func (m *messagesComponent) Init() tea.Cmd { func (m *messagesComponent) Init() tea.Cmd {
return tea.Batch(m.viewport.Init(), m.spinner.Tick) return tea.Batch(m.viewport.Init(), m.spinner.Tick, m.commands.Init())
} }
func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
@@ -56,19 +58,22 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.viewport.GotoBottom() m.viewport.GotoBottom()
m.tail = true m.tail = true
return m, nil return m, nil
case app.OptimisticMessageAddedMsg:
m.renderView()
if m.tail {
m.viewport.GotoBottom()
}
return m, nil
case dialog.ThemeSelectedMsg: case dialog.ThemeSelectedMsg:
m.cache.Clear() m.cache.Clear()
m.renderView() return m, m.Reload()
return m, nil
case ToggleToolDetailsMsg: case ToggleToolDetailsMsg:
m.showToolDetails = !m.showToolDetails m.showToolDetails = !m.showToolDetails
m.renderView() return m, m.Reload()
return m, nil
case app.SessionSelectedMsg: case app.SessionSelectedMsg:
m.cache.Clear() m.cache.Clear()
cmd := m.Reload() m.tail = true
m.viewport.GotoBottom() return m, m.Reload()
return m, cmd
case app.SessionClearedMsg: case app.SessionClearedMsg:
m.cache.Clear() m.cache.Clear()
cmd := m.Reload() cmd := m.Reload()
@@ -93,6 +98,11 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
spinner, cmd := m.spinner.Update(msg) spinner, cmd := m.spinner.Update(msg)
m.spinner = spinner m.spinner = spinner
cmds = append(cmds, cmd) cmds = append(cmds, cmd)
updated, cmd := m.commands.Update(msg)
m.commands = updated.(commands.CommandsComponent)
cmds = append(cmds, cmd)
return m, tea.Batch(cmds...) return m, tea.Batch(cmds...)
} }
@@ -167,7 +177,7 @@ func (m *messagesComponent) renderView() {
isLastToolInvocation := slices.Contains(lastToolIndices, i) isLastToolInvocation := slices.Contains(lastToolIndices, i)
toolInvocationPart := part.(client.MessagePartToolInvocation) toolInvocationPart := part.(client.MessagePartToolInvocation)
toolCall, _ := toolInvocationPart.ToolInvocation.AsMessageToolInvocationToolCall() toolCall, _ := toolInvocationPart.ToolInvocation.AsMessageToolInvocationToolCall()
metadata := client.MessageInfo_Metadata_Tool_AdditionalProperties{} metadata := client.MessageMetadata_Tool_AdditionalProperties{}
if _, ok := message.Metadata.Tool[toolCall.ToolCallId]; ok { if _, ok := message.Metadata.Tool[toolCall.ToolCallId]; ok {
metadata = message.Metadata.Tool[toolCall.ToolCallId] metadata = message.Metadata.Tool[toolCall.ToolCallId]
} }
@@ -191,6 +201,7 @@ func (m *messagesComponent) renderView() {
metadata, metadata,
m.showToolDetails, m.showToolDetails,
isLastToolInvocation, isLastToolInvocation,
false,
) )
m.cache.Set(key, content) m.cache.Set(key, content)
} }
@@ -202,6 +213,7 @@ func (m *messagesComponent) renderView() {
metadata, metadata,
m.showToolDetails, m.showToolDetails,
isLastToolInvocation, isLastToolInvocation,
false,
) )
} }
@@ -279,7 +291,13 @@ func (m *messagesComponent) View() string {
return m.home() return m.home()
} }
if m.rendering { if m.rendering {
return m.viewport.View() return lipgloss.Place(
m.width,
m.height,
lipgloss.Center,
lipgloss.Center,
"Loading session...",
)
} }
t := theme.CurrentTheme() t := theme.CurrentTheme()
return lipgloss.JoinVertical( return lipgloss.JoinVertical(
@@ -317,50 +335,43 @@ func (m *messagesComponent) home() string {
// cwd := app.Info.Path.Cwd // cwd := app.Info.Path.Cwd
// config := app.Info.Path.Config // config := app.Info.Path.Config
commands := [][]string{ versionStyle := lipgloss.NewStyle().
{"/help", "show help"}, Background(t.Background()).
{"/sessions", "list sessions"}, Foreground(t.TextMuted()).
{"/new", "start a new session"}, Width(lipgloss.Width(logo)).
{"/model", "switch model"}, Align(lipgloss.Right)
{"/theme", "switch theme"}, version := versionStyle.Render(m.app.Version)
{"/exit", "exit the app"},
}
commandLines := []string{} logoAndVersion := strings.Join([]string{logo, version}, "\n")
for _, command := range commands { logoAndVersion = lipgloss.PlaceHorizontal(
commandLines = append(commandLines, (base(command[0]+" ") + muted(command[1]))) m.width,
} lipgloss.Center,
logoAndVersion,
logoAndVersion := lipgloss.JoinVertical( lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
lipgloss.Right, )
logo, m.commands.SetBackgroundColor(t.Background())
muted(m.app.Version), commands := lipgloss.PlaceHorizontal(
m.width,
lipgloss.Center,
m.commands.View(),
lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
) )
lines := []string{} lines := []string{}
lines = append(lines, "")
lines = append(lines, "")
lines = append(lines, logoAndVersion) lines = append(lines, logoAndVersion)
lines = append(lines, "") lines = append(lines, "")
lines = append(lines, "")
// lines = append(lines, base("cwd ")+muted(cwd)) // lines = append(lines, base("cwd ")+muted(cwd))
// lines = append(lines, base("config ")+muted(config)) // lines = append(lines, base("config ")+muted(config))
// lines = append(lines, "") // lines = append(lines, "")
lines = append(lines, commandLines...) lines = append(lines, commands)
lines = append(lines, "")
if m.rendering {
lines = append(lines, base("Loading session..."))
} else {
lines = append(lines, "")
}
return lipgloss.Place( return lipgloss.Place(
m.width, m.width,
m.height, m.height,
lipgloss.Center, lipgloss.Center,
lipgloss.Center, lipgloss.Center,
baseStyle.Width(lipgloss.Width(logoAndVersion)).Render( baseStyle.Render(strings.Join(lines, "\n")),
strings.Join(lines, "\n"),
),
lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())), lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
) )
} }
@@ -379,6 +390,7 @@ func (m *messagesComponent) SetSize(width, height int) tea.Cmd {
m.viewport.SetHeight(height - lipgloss.Height(m.header())) m.viewport.SetHeight(height - lipgloss.Height(m.header()))
m.attachments.SetWidth(width + 40) m.attachments.SetWidth(width + 40)
m.attachments.SetHeight(3) m.attachments.SetHeight(3)
m.commands.SetSize(width, height)
m.renderView() m.renderView()
return nil return nil
} }
@@ -442,11 +454,19 @@ func NewMessagesComponent(app *app.App) MessagesComponent {
attachments := viewport.New() attachments := viewport.New()
vp.KeyMap = viewport.KeyMap{} vp.KeyMap = viewport.KeyMap{}
t := theme.CurrentTheme()
commandsView := commands.New(
app,
commands.WithBackground(t.Background()),
commands.WithLimit(6),
)
return &messagesComponent{ return &messagesComponent{
app: app, app: app,
viewport: vp, viewport: vp,
spinner: s, spinner: s,
attachments: attachments, attachments: attachments,
commands: commandsView,
showToolDetails: true, showToolDetails: true,
cache: NewMessageCache(), cache: NewMessageCache(),
tail: true, tail: true,
@@ -0,0 +1,234 @@
package commands
import (
"fmt"
"strings"
tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/lipgloss/v2"
"github.com/charmbracelet/lipgloss/v2/compat"
"github.com/sst/opencode/internal/app"
"github.com/sst/opencode/internal/commands"
"github.com/sst/opencode/internal/layout"
"github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme"
)
type CommandsComponent interface {
tea.Model
tea.ViewModel
layout.Sizeable
SetBackgroundColor(color compat.AdaptiveColor)
}
type commandsComponent struct {
app *app.App
width, height int
showKeybinds bool
showAll bool
background *compat.AdaptiveColor
limit *int
}
func (c *commandsComponent) SetSize(width, height int) tea.Cmd {
c.width = width
c.height = height
return nil
}
func (c *commandsComponent) GetSize() (int, int) {
return c.width, c.height
}
func (c *commandsComponent) SetBackgroundColor(color compat.AdaptiveColor) {
c.background = &color
}
func (c *commandsComponent) Init() tea.Cmd {
return nil
}
func (c *commandsComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
c.width = msg.Width
c.height = msg.Height
}
return c, nil
}
func (c *commandsComponent) View() string {
t := theme.CurrentTheme()
triggerStyle := lipgloss.NewStyle().
Foreground(t.Primary()).
Bold(true)
descriptionStyle := lipgloss.NewStyle().
Foreground(t.Text())
keybindStyle := lipgloss.NewStyle().
Foreground(t.TextMuted())
if c.background != nil {
triggerStyle = triggerStyle.Background(*c.background)
descriptionStyle = descriptionStyle.Background(*c.background)
keybindStyle = keybindStyle.Background(*c.background)
}
var commandsToShow []commands.Command
var triggeredCommands []commands.Command
var untriggeredCommands []commands.Command
for _, cmd := range c.app.Commands.Sorted() {
if c.showAll || cmd.Trigger != "" {
if cmd.Trigger != "" {
triggeredCommands = append(triggeredCommands, cmd)
} else if c.showAll {
untriggeredCommands = append(untriggeredCommands, cmd)
}
}
}
// Combine triggered commands first, then untriggered
commandsToShow = append(commandsToShow, triggeredCommands...)
commandsToShow = append(commandsToShow, untriggeredCommands...)
if c.limit != nil && len(commandsToShow) > *c.limit {
commandsToShow = commandsToShow[:*c.limit]
}
if len(commandsToShow) == 0 {
if c.showAll {
return styles.Muted().Render("No commands available")
}
return styles.Muted().Render("No commands with triggers available")
}
// Calculate column widths
maxTriggerWidth := 0
maxDescriptionWidth := 0
maxKeybindWidth := 0
// Prepare command data
type commandRow struct {
trigger string
description string
keybinds string
}
rows := make([]commandRow, 0, len(commandsToShow))
for _, cmd := range commandsToShow {
trigger := ""
if cmd.Trigger != "" {
trigger = "/" + cmd.Trigger
} else {
trigger = string(cmd.Name)
}
description := cmd.Description
// Format keybindings
var keybindStrs []string
if c.showKeybinds {
for _, kb := range cmd.Keybindings {
if kb.RequiresLeader {
keybindStrs = append(keybindStrs, *c.app.Config.Keybinds.Leader+" "+kb.Key)
} else {
keybindStrs = append(keybindStrs, kb.Key)
}
}
}
keybinds := strings.Join(keybindStrs, ", ")
rows = append(rows, commandRow{
trigger: trigger,
description: description,
keybinds: keybinds,
})
// Update max widths
if len(trigger) > maxTriggerWidth {
maxTriggerWidth = len(trigger)
}
if len(description) > maxDescriptionWidth {
maxDescriptionWidth = len(description)
}
if len(keybinds) > maxKeybindWidth {
maxKeybindWidth = len(keybinds)
}
}
// Add padding between columns
columnPadding := 3
// Build the output
var output strings.Builder
maxWidth := 0
for _, row := range rows {
// Pad each column to align properly
trigger := fmt.Sprintf("%-*s", maxTriggerWidth, row.trigger)
description := fmt.Sprintf("%-*s", maxDescriptionWidth, row.description)
// Apply styles and combine
line := triggerStyle.Render(trigger) +
triggerStyle.Render(strings.Repeat(" ", columnPadding)) +
descriptionStyle.Render(description)
if c.showKeybinds && row.keybinds != "" {
line += keybindStyle.Render(strings.Repeat(" ", columnPadding)) +
keybindStyle.Render(row.keybinds)
}
output.WriteString(line + "\n")
maxWidth = max(maxWidth, lipgloss.Width(line))
}
// Remove trailing newline
result := strings.TrimSuffix(output.String(), "\n")
if c.background != nil {
result = lipgloss.NewStyle().Background(c.background).Width(maxWidth).Render(result)
}
return result
}
type Option func(*commandsComponent)
func WithKeybinds(show bool) Option {
return func(c *commandsComponent) {
c.showKeybinds = show
}
}
func WithBackground(background compat.AdaptiveColor) Option {
return func(c *commandsComponent) {
c.background = &background
}
}
func WithLimit(limit int) Option {
return func(c *commandsComponent) {
c.limit = &limit
}
}
func WithShowAll(showAll bool) Option {
return func(c *commandsComponent) {
c.showAll = showAll
}
}
func New(app *app.App, opts ...Option) CommandsComponent {
c := &commandsComponent{
app: app,
background: nil,
showKeybinds: true,
showAll: false,
}
for _, opt := range opts {
opt(c)
}
return c
}
@@ -6,7 +6,6 @@ import (
"github.com/charmbracelet/bubbles/v2/key" "github.com/charmbracelet/bubbles/v2/key"
"github.com/charmbracelet/bubbles/v2/textarea" "github.com/charmbracelet/bubbles/v2/textarea"
tea "github.com/charmbracelet/bubbletea/v2" tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/lipgloss/v2"
"github.com/sst/opencode/internal/app" "github.com/sst/opencode/internal/app"
"github.com/sst/opencode/internal/components/list" "github.com/sst/opencode/internal/components/list"
"github.com/sst/opencode/internal/styles" "github.com/sst/opencode/internal/styles"
@@ -117,7 +116,7 @@ func (c *completionDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) { switch msg := msg.(type) {
case []CompletionItemI: case []CompletionItemI:
c.list.SetItems(msg) c.list.SetItems(msg)
case app.CompletionDialogTriggerdMsg: case app.CompletionDialogTriggeredMsg:
c.pseudoSearchTextArea.SetValue(msg.InitialValue) c.pseudoSearchTextArea.SetValue(msg.InitialValue)
case tea.KeyMsg: case tea.KeyMsg:
if c.pseudoSearchTextArea.Focused() { if c.pseudoSearchTextArea.Focused() {
@@ -174,7 +173,6 @@ func (c *completionDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
} }
cmds = append(cmds, cmd) cmds = append(cmds, cmd)
cmds = append(cmds, c.pseudoSearchTextArea.Focus()) cmds = append(cmds, c.pseudoSearchTextArea.Focus())
// c.pseudoSearchTextArea.SetValue(msg.String())
return c, tea.Batch(cmds...) return c, tea.Batch(cmds...)
} }
case tea.WindowSizeMsg: case tea.WindowSizeMsg:
@@ -203,13 +201,6 @@ func (c *completionDialogComponent) View() string {
return baseStyle.Padding(0, 0). return baseStyle.Padding(0, 0).
Background(t.BackgroundElement()). Background(t.BackgroundElement()).
Border(lipgloss.ThickBorder()).
BorderTop(false).
BorderBottom(false).
BorderRight(true).
BorderLeft(true).
BorderBackground(t.Background()).
BorderForeground(t.BackgroundElement()).
Width(c.width). Width(c.width).
Render(c.list.View()) Render(c.list.View())
} }
@@ -226,12 +217,12 @@ func (c *completionDialogComponent) SetProvider(provider CompletionProvider) {
if c.completionProvider.GetId() != provider.GetId() { if c.completionProvider.GetId() != provider.GetId() {
c.completionProvider = provider c.completionProvider = provider
c.list.SetEmptyMessage(" " + provider.GetEmptyMessage()) c.list.SetEmptyMessage(" " + provider.GetEmptyMessage())
c.list.SetItems([]CompletionItemI{})
} }
} }
func (c *completionDialogComponent) complete(item CompletionItemI) tea.Cmd { func (c *completionDialogComponent) complete(item CompletionItemI) tea.Cmd {
value := c.pseudoSearchTextArea.Value() value := c.pseudoSearchTextArea.Value()
if value == "" { if value == "" {
return nil return nil
} }
@@ -250,10 +241,8 @@ func (c *completionDialogComponent) complete(item CompletionItemI) tea.Cmd {
} }
func (c *completionDialogComponent) close() tea.Cmd { func (c *completionDialogComponent) close() tea.Cmd {
c.list.SetItems([]CompletionItemI{})
c.pseudoSearchTextArea.Reset() c.pseudoSearchTextArea.Reset()
c.pseudoSearchTextArea.Blur() c.pseudoSearchTextArea.Blur()
return util.CmdHandler(CompletionDialogCloseMsg{}) return util.CmdHandler(CompletionDialogCloseMsg{})
} }
+43 -45
View File
@@ -1,71 +1,62 @@
package dialog package dialog
import ( import (
"strings" "github.com/charmbracelet/bubbles/v2/viewport"
tea "github.com/charmbracelet/bubbletea/v2" tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/lipgloss/v2" "github.com/sst/opencode/internal/app"
"github.com/sst/opencode/internal/commands" commandsComponent "github.com/sst/opencode/internal/components/commands"
"github.com/sst/opencode/internal/components/modal" "github.com/sst/opencode/internal/components/modal"
"github.com/sst/opencode/internal/layout" "github.com/sst/opencode/internal/layout"
"github.com/sst/opencode/internal/theme" "github.com/sst/opencode/internal/theme"
) )
type helpDialog struct { type helpDialog struct {
width int width int
height int height int
modal *modal.Modal modal *modal.Modal
commands []commands.Command app *app.App
commandsComponent commandsComponent.CommandsComponent
viewport viewport.Model
} }
func (h *helpDialog) Init() tea.Cmd { 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) { func (h *helpDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmds []tea.Cmd
switch msg := msg.(type) { switch msg := msg.(type) {
case tea.WindowSizeMsg: case tea.WindowSizeMsg:
h.width = msg.Width h.width = msg.Width
h.height = msg.Height 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 { func (h *helpDialog) View() string {
t := theme.CurrentTheme() t := theme.CurrentTheme()
keyStyle := lipgloss.NewStyle(). h.commandsComponent.SetBackgroundColor(t.BackgroundElement())
Background(t.BackgroundElement()). return h.viewport.View()
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")
} }
func (h *helpDialog) Render(background string) string { func (h *helpDialog) Render(background string) string {
@@ -80,9 +71,16 @@ type HelpDialog interface {
layout.Modal layout.Modal
} }
func NewHelpDialog(commands []commands.Command) HelpDialog { func NewHelpDialog(app *app.App) HelpDialog {
vp := viewport.New(viewport.WithHeight(12))
return &helpDialog{ 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")), modal: modal.New(modal.WithTitle("Help")),
viewport: vp,
} }
} }
+69 -151
View File
@@ -11,6 +11,7 @@ import (
tea "github.com/charmbracelet/bubbletea/v2" tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/lipgloss/v2" "github.com/charmbracelet/lipgloss/v2"
"github.com/sst/opencode/internal/app" "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/modal"
"github.com/sst/opencode/internal/layout" "github.com/sst/opencode/internal/layout"
"github.com/sst/opencode/internal/styles" "github.com/sst/opencode/internal/styles"
@@ -33,20 +34,15 @@ type modelDialog struct {
app *app.App app *app.App
availableProviders []client.ProviderInfo availableProviders []client.ProviderInfo
provider client.ProviderInfo provider client.ProviderInfo
width int
selectedIdx int height int
width int hScrollOffset int
height int hScrollPossible bool
scrollOffset int modal *modal.Modal
hScrollOffset int modelList list.List[list.StringItem]
hScrollPossible bool
modal *modal.Modal
} }
type modelKeyMap struct { type modelKeyMap struct {
Up key.Binding
Down key.Binding
Left key.Binding Left key.Binding
Right key.Binding Right key.Binding
Enter key.Binding Enter key.Binding
@@ -54,14 +50,6 @@ type modelKeyMap struct {
} }
var modelKeys = modelKeyMap{ 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( Left: key.NewBinding(
key.WithKeys("left", "h"), key.WithKeys("left", "h"),
key.WithHelp("←", "scroll left"), key.WithHelp("←", "scroll left"),
@@ -81,15 +69,7 @@ var modelKeys = modelKeyMap{
} }
func (m *modelDialog) Init() tea.Cmd { func (m *modelDialog) Init() tea.Cmd {
// cfg := config.Get() m.setupModelsForProvider(m.provider.Id)
// 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)
return nil return nil
} }
@@ -97,26 +77,32 @@ func (m *modelDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) { switch msg := msg.(type) {
case tea.KeyMsg: case tea.KeyMsg:
switch { switch {
case key.Matches(msg, modelKeys.Up):
m.moveSelectionUp()
case key.Matches(msg, modelKeys.Down):
m.moveSelectionDown()
case key.Matches(msg, modelKeys.Left): case key.Matches(msg, modelKeys.Left):
if m.hScrollPossible { if m.hScrollPossible {
m.switchProvider(-1) m.switchProvider(-1)
} }
return m, nil
case key.Matches(msg, modelKeys.Right): case key.Matches(msg, modelKeys.Right):
if m.hScrollPossible { if m.hScrollPossible {
m.switchProvider(1) m.switchProvider(1)
} }
return m, nil
case key.Matches(msg, modelKeys.Enter): case key.Matches(msg, modelKeys.Enter):
selectedItem, _ := m.modelList.GetSelectedItem()
models := m.models() models := m.models()
var selectedModel client.ModelInfo
for _, model := range models {
if model.Name == string(selectedItem) {
selectedModel = model
break
}
}
return m, tea.Sequence( return m, tea.Sequence(
util.CmdHandler(modal.CloseModalMsg{}), util.CmdHandler(modal.CloseModalMsg{}),
util.CmdHandler( util.CmdHandler(
app.ModelSelectedMsg{ app.ModelSelectedMsg{
Provider: m.provider, Provider: m.provider,
Model: models[m.selectedIdx], Model: selectedModel,
}), }),
) )
case key.Matches(msg, modelKeys.Escape): 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 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 { func (m *modelDialog) models() []client.ModelInfo {
@@ -137,40 +126,9 @@ func (m *modelDialog) models() []client.ModelInfo {
return models 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) { func (m *modelDialog) switchProvider(offset int) {
newOffset := m.hScrollOffset + offset newOffset := m.hScrollOffset + offset
// Ensure we stay within bounds
if newOffset < 0 { if newOffset < 0 {
newOffset = len(m.availableProviders) - 1 newOffset = len(m.availableProviders) - 1
} }
@@ -185,105 +143,46 @@ func (m *modelDialog) switchProvider(offset int) {
} }
func (m *modelDialog) View() string { func (m *modelDialog) View() string {
t := theme.CurrentTheme() listView := m.modelList.View()
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))
}
scrollIndicator := m.getScrollIndicators(maxDialogWidth) scrollIndicator := m.getScrollIndicators(maxDialogWidth)
return strings.Join([]string{listView, scrollIndicator}, "\n")
content := lipgloss.JoinVertical(
lipgloss.Left,
baseStyle.
Width(maxDialogWidth).
Render(lipgloss.JoinVertical(lipgloss.Left, modelItems...)),
scrollIndicator,
)
return content
} }
func (m *modelDialog) getScrollIndicators(maxWidth int) string { func (m *modelDialog) getScrollIndicators(maxWidth int) string {
var indicator 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 { if m.hScrollPossible {
indicator = "← " + indicator + "→" indicator = "← → (switch provider) "
} }
if indicator == "" { if indicator == "" {
return "" return ""
} }
t := theme.CurrentTheme() t := theme.CurrentTheme()
baseStyle := styles.BaseStyle() return styles.BaseStyle().
Foreground(t.TextMuted()).
return baseStyle.
Foreground(t.Primary()).
Width(maxWidth). Width(maxWidth).
Align(lipgloss.Right). Align(lipgloss.Right).
Bold(true).
Render(indicator) Render(indicator)
} }
// findProviderIndex returns the index of the provider in the list, or -1 if not found func (m *modelDialog) setupModelsForProvider(providerId string) {
// func findProviderIndex(providers []string, provider string) int { models := m.models()
// for i, p := range providers { modelNames := make([]string, len(models))
// if p == provider { for i, model := range models {
// return i modelNames[i] = model.Name
// } }
// }
// return -1
// }
func (m *modelDialog) setupModelsForProvider(_ string) { m.modelList = list.NewStringList(modelNames, numVisibleModels, "No models available", true)
m.selectedIdx = 0 m.modelList.SetMaxWidth(maxDialogWidth)
m.scrollOffset = 0
// cfg := config.Get() if m.app.Provider != nil && m.app.Model != nil && m.app.Provider.Id == providerId {
// agentCfg := cfg.Agents[config.AgentPrimary] for i, model := range models {
// selectedModelId := agentCfg.Model if model.Id == m.app.Model.Id {
m.modelList.SetSelectedIndex(i)
// m.provider = provider break
// 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
// }
// }
// }
} }
func (m *modelDialog) Render(background string) string { func (m *modelDialog) Render(background string) string {
@@ -297,11 +196,30 @@ func (s *modelDialog) Close() tea.Cmd {
func NewModelDialog(app *app.App) ModelDialog { func NewModelDialog(app *app.App) ModelDialog {
availableProviders, _ := app.ListProviders(context.Background()) availableProviders, _ := app.ListProviders(context.Background())
return &modelDialog{ currentProvider := availableProviders[0]
availableProviders: availableProviders, hScrollOffset := 0
hScrollOffset: 0, if app.Provider != nil {
hScrollPossible: len(availableProviders) > 1, for i, provider := range availableProviders {
provider: availableProviders[0], if provider.Id == app.Provider.Id {
modal: modal.New(modal.WithTitle(fmt.Sprintf("Select %s Model", availableProviders[0].Name))), currentProvider = provider
hScrollOffset = i
break
}
}
} }
dialog := &modelDialog{
app: app,
availableProviders: availableProviders,
hScrollOffset: hScrollOffset,
hScrollPossible: len(availableProviders) > 1,
provider: currentProvider,
modal: modal.New(
modal.WithTitle(fmt.Sprintf("Select %s Model", currentProvider.Name)),
modal.WithMaxWidth(maxDialogWidth+4),
),
}
dialog.setupModelsForProvider(currentProvider.Id)
return dialog
} }
@@ -8,8 +8,6 @@ import (
"github.com/sst/opencode/internal/components/list" "github.com/sst/opencode/internal/components/list"
"github.com/sst/opencode/internal/components/modal" "github.com/sst/opencode/internal/components/modal"
"github.com/sst/opencode/internal/layout" "github.com/sst/opencode/internal/layout"
"github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme"
"github.com/sst/opencode/internal/util" "github.com/sst/opencode/internal/util"
"github.com/sst/opencode/pkg/client" "github.com/sst/opencode/pkg/client"
) )
@@ -19,33 +17,12 @@ type SessionDialog interface {
layout.Modal layout.Modal
} }
type sessionItem client.SessionInfo
func (s sessionItem) Render(selected bool, width int) string {
t := theme.CurrentTheme()
baseStyle := styles.BaseStyle().
Width(width - 4).
Background(t.BackgroundElement())
if selected {
baseStyle = baseStyle.
Background(t.Primary()).
Foreground(t.BackgroundElement()).
Bold(true)
} else {
baseStyle = baseStyle.
Foreground(t.Text())
}
return baseStyle.Padding(0, 1).Render(s.Title)
}
type sessionDialog struct { type sessionDialog struct {
width int width int
height int height int
modal *modal.Modal modal *modal.Modal
selectedSessionID string sessions []client.SessionInfo
list list.List[sessionItem] list list.List[list.StringItem]
} }
func (s *sessionDialog) Init() tea.Cmd { func (s *sessionDialog) Init() tea.Cmd {
@@ -61,11 +38,11 @@ func (s *sessionDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case tea.KeyPressMsg: case tea.KeyPressMsg:
switch msg.String() { switch msg.String() {
case "enter": case "enter":
if item, idx := s.list.GetSelectedItem(); idx >= 0 { if _, idx := s.list.GetSelectedItem(); idx >= 0 && idx < len(s.sessions) {
s.selectedSessionID = item.Id selectedSession := s.sessions[idx]
return s, tea.Sequence( return s, tea.Sequence(
util.CmdHandler(modal.CloseModalMsg{}), util.CmdHandler(modal.CloseModalMsg{}),
util.CmdHandler(app.SessionSelectedMsg(&item)), util.CmdHandler(app.SessionSelectedMsg(&selectedSession)),
) )
} }
} }
@@ -73,7 +50,7 @@ func (s *sessionDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd var cmd tea.Cmd
listModel, cmd := s.list.Update(msg) listModel, cmd := s.list.Update(msg)
s.list = listModel.(list.List[sessionItem]) s.list = listModel.(list.List[list.StringItem])
return s, cmd return s, cmd
} }
@@ -89,23 +66,30 @@ func (s *sessionDialog) Close() tea.Cmd {
func NewSessionDialog(app *app.App) SessionDialog { func NewSessionDialog(app *app.App) SessionDialog {
sessions, _ := app.ListSessions(context.Background()) sessions, _ := app.ListSessions(context.Background())
var sessionItems []sessionItem var filteredSessions []client.SessionInfo
var sessionTitles []string
for _, sess := range sessions { for _, sess := range sessions {
if sess.ParentID != nil { if sess.ParentID != nil {
continue continue
} }
sessionItems = append(sessionItems, sessionItem(sess)) filteredSessions = append(filteredSessions, sess)
sessionTitles = append(sessionTitles, sess.Title)
} }
list := list.NewListComponent( list := list.NewStringList(
sessionItems, sessionTitles,
10, // maxVisibleSessions 10, // maxVisibleSessions
"No sessions available", "No sessions available",
true, // useAlphaNumericKeys true, // useAlphaNumericKeys
) )
list.SetMaxWidth(layout.Current.Container.Width - 12)
return &sessionDialog{ return &sessionDialog{
list: list, sessions: filteredSessions,
modal: modal.New(modal.WithTitle("Switch Session"), modal.WithMaxWidth(80)), list: list,
modal: modal.New(
modal.WithTitle("Switch Session"),
modal.WithMaxWidth(layout.Current.Container.Width-8),
),
} }
} }
@@ -5,7 +5,6 @@ import (
list "github.com/sst/opencode/internal/components/list" list "github.com/sst/opencode/internal/components/list"
"github.com/sst/opencode/internal/components/modal" "github.com/sst/opencode/internal/components/modal"
"github.com/sst/opencode/internal/layout" "github.com/sst/opencode/internal/layout"
"github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme" "github.com/sst/opencode/internal/theme"
"github.com/sst/opencode/internal/util" "github.com/sst/opencode/internal/util"
) )
@@ -20,35 +19,14 @@ type ThemeDialog interface {
layout.Modal 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 { type themeDialog struct {
width int width int
height int height int
modal *modal.Modal modal *modal.Modal
list list.List[themeItem] list list.List[list.StringItem]
originalTheme string
themeApplied bool
} }
func (t *themeDialog) Init() tea.Cmd { func (t *themeDialog) Init() tea.Cmd {
@@ -64,26 +42,31 @@ func (t *themeDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg.String() { switch msg.String() {
case "enter": case "enter":
if item, idx := t.list.GetSelectedItem(); idx >= 0 { if item, idx := t.list.GetSelectedItem(); idx >= 0 {
previousTheme := theme.CurrentThemeName() selectedTheme := string(item)
selectedTheme := item.name
if previousTheme == selectedTheme {
return t, util.CmdHandler(modal.CloseModalMsg{})
}
if err := theme.SetTheme(selectedTheme); err != nil { if err := theme.SetTheme(selectedTheme); err != nil {
// status.Error(err.Error()) // status.Error(err.Error())
return t, nil return t, nil
} }
t.themeApplied = true
return t, tea.Sequence( return t, tea.Sequence(
util.CmdHandler(modal.CloseModalMsg{}), util.CmdHandler(modal.CloseModalMsg{}),
util.CmdHandler(ThemeSelectedMsg{ThemeName: selectedTheme}), util.CmdHandler(ThemeSelectedMsg{ThemeName: selectedTheme}),
) )
} }
} }
} }
_, prevIdx := t.list.GetSelectedItem()
var cmd tea.Cmd var cmd tea.Cmd
listModel, cmd := t.list.Update(msg) 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 return t, cmd
} }
@@ -92,6 +75,10 @@ func (t *themeDialog) Render(background string) string {
} }
func (t *themeDialog) Close() tea.Cmd { func (t *themeDialog) Close() tea.Cmd {
if !t.themeApplied {
theme.SetTheme(t.originalTheme)
return util.CmdHandler(ThemeSelectedMsg{ThemeName: t.originalTheme})
}
return nil return nil
} }
@@ -100,17 +87,15 @@ func NewThemeDialog() ThemeDialog {
themes := theme.AvailableThemes() themes := theme.AvailableThemes()
currentTheme := theme.CurrentThemeName() currentTheme := theme.CurrentThemeName()
var themeItems []themeItem
var selectedIdx int var selectedIdx int
for i, name := range themes { for i, name := range themes {
themeItems = append(themeItems, themeItem{name: name})
if name == currentTheme { if name == currentTheme {
selectedIdx = i selectedIdx = i
} }
} }
list := list.NewListComponent( list := list.NewStringList(
themeItems, themes,
10, // maxVisibleThemes 10, // maxVisibleThemes
"No themes available", "No themes available",
true, true,
@@ -118,9 +103,14 @@ func NewThemeDialog() ThemeDialog {
// Set the initial selection to the current theme // Set the initial selection to the current theme
list.SetSelectedIndex(selectedIdx) 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{ return &themeDialog{
list: list, list: list,
modal: modal.New(modal.WithTitle("Select Theme"), modal.WithMaxWidth(40)), 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"/> <entry type="TextWhitespace" style="%s"/>
</style> </style>
`, `,
getColor(t.BackgroundSubtle()), // Background getColor(t.BackgroundPanel()), // Background
getColor(t.Text()), // Text getColor(t.Text()), // Text
getColor(t.Text()), // Other getColor(t.Text()), // Other
getColor(t.Error()), // Error getColor(t.Error()), // Error
getColor(t.SyntaxKeyword()), // Keyword getColor(t.SyntaxKeyword()), // Keyword
getColor(t.SyntaxKeyword()), // KeywordConstant getColor(t.SyntaxKeyword()), // KeywordConstant
@@ -614,7 +614,7 @@ func applyHighlighting(content string, segments []Segment, segmentType LineType,
// Get the appropriate color based on terminal background // Get the appropriate color based on terminal background
bgColor := lipgloss.Color(getColor(highlightBg)) 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); { for i := 0; i < len(content); {
// Check if we're at an ANSI sequence // Check if we're at an ANSI sequence
@@ -5,6 +5,10 @@ import (
"github.com/charmbracelet/bubbles/v2/key" "github.com/charmbracelet/bubbles/v2/key"
tea "github.com/charmbracelet/bubbletea/v2" 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 { type ListItem interface {
@@ -123,6 +127,9 @@ func (c *listComponent[T]) SetSelectedIndex(idx int) {
func (c *listComponent[T]) View() string { func (c *listComponent[T]) View() string {
items := c.items items := c.items
maxWidth := c.maxWidth maxWidth := c.maxWidth
if maxWidth == 0 {
maxWidth = 80 // Default width if not set
}
maxVisibleItems := min(c.maxVisibleItems, len(items)) maxVisibleItems := min(c.maxVisibleItems, len(items))
startIdx := 0 startIdx := 0
@@ -161,3 +168,36 @@ func NewListComponent[T ListItem](items []T, maxVisibleItems int, fallbackMsg st
selectedIdx: 0, 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)
}
@@ -100,21 +100,21 @@ func (m *Modal) Render(contentView string, background string) string {
if m.title != "" { if m.title != "" {
titleStyle := baseStyle. titleStyle := baseStyle.
Foreground(t.Primary()). Foreground(t.Primary()).
Bold(true) Bold(true).
Padding(0, 1)
// titleView := titleStyle.Render(m.title) escStyle := baseStyle.Foreground(t.TextMuted())
escStyle := baseStyle.Foreground(t.TextMuted()).Bold(false)
escText := escStyle.Render("esc") escText := escStyle.Render("esc")
// Calculate position for esc text // Calculate position for esc text
titleWidth := lipgloss.Width(m.title) titleWidth := lipgloss.Width(m.title)
escWidth := lipgloss.Width(escText) escWidth := lipgloss.Width(escText)
spacesNeeded := max(0, innerWidth-titleWidth-escWidth-3) spacesNeeded := max(0, innerWidth-titleWidth-escWidth-2)
spacer := strings.Repeat(" ", spacesNeeded) spacer := strings.Repeat(" ", spacesNeeded)
titleLine := m.title + spacer + escText titleLine := m.title + spacer + escText
titleLine = titleStyle.Render(titleLine) titleLine = titleStyle.Render(titleLine)
finalContent = strings.Join([]string{titleLine, contentView}, "\n") + "\n" finalContent = strings.Join([]string{titleLine, "", contentView}, "\n")
} else { } else {
finalContent = contentView finalContent = contentView
} }
@@ -123,14 +123,7 @@ func (m *Modal) Render(contentView string, background string) string {
PaddingTop(1). PaddingTop(1).
PaddingBottom(1). PaddingBottom(1).
PaddingLeft(2). PaddingLeft(2).
PaddingRight(2). PaddingRight(2)
BorderStyle(lipgloss.ThickBorder()).
BorderLeft(true).
BorderRight(true).
BorderLeftForeground(t.BackgroundSubtle()).
BorderLeftBackground(t.Background()).
BorderRightForeground(t.BackgroundSubtle()).
BorderRightBackground(t.Background())
modalView := modalStyle. modalView := modalStyle.
Width(outerWidth). Width(outerWidth).
@@ -150,5 +143,7 @@ func (m *Modal) Render(contentView string, background string) string {
row, row,
modalView, modalView,
background, background,
layout.WithOverlayBorder(),
layout.WithOverlayBorderColor(t.Primary()),
) )
} }
@@ -71,7 +71,7 @@ func formatTokensAndCost(tokens float32, contextWindow float32, cost float32) st
formattedCost := fmt.Sprintf("$%.2f", cost) formattedCost := fmt.Sprintf("$%.2f", cost)
percentage := (float64(tokens) / float64(contextWindow)) * 100 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 { func (m statusComponent) View() string {
@@ -88,7 +88,7 @@ func (m statusComponent) View() string {
cwd := styles.Padded(). cwd := styles.Padded().
Foreground(t.TextMuted()). Foreground(t.TextMuted()).
Background(t.BackgroundSubtle()). Background(t.BackgroundPanel()).
Render(m.app.Info.Path.Cwd) Render(m.app.Info.Path.Cwd)
sessionInfo := "" sessionInfo := ""
@@ -123,7 +123,7 @@ func (m statusComponent) View() string {
0, 0,
m.width-lipgloss.Width(logo)-lipgloss.Width(cwd)-lipgloss.Width(sessionInfo), 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 status := logo + cwd + spacer + sessionInfo
@@ -553,6 +553,14 @@ func (m Model) Line() int {
return m.row 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. // CursorDown moves the cursor down by one line.
// Returns whether or not the cursor blink should be reset. // Returns whether or not the cursor blink should be reset.
func (m *Model) CursorDown() { func (m *Model) CursorDown() {
@@ -1113,11 +1121,7 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
} }
m.deleteWordRight() m.deleteWordRight()
case key.Matches(msg, m.KeyMap.InsertNewline): case key.Matches(msg, m.KeyMap.InsertNewline):
if m.MaxHeight > 0 && len(m.value) >= m.MaxHeight { m.Newline()
return m, nil
}
m.col = clamp(m.col, 0, len(m.value[m.row]))
m.splitLine(m.row, m.col)
case key.Matches(msg, m.KeyMap.LineEnd): case key.Matches(msg, m.KeyMap.LineEnd):
m.CursorEnd() m.CursorEnd()
case key.Matches(msg, m.KeyMap.LineStart): 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 var cmd tea.Cmd
cmds = append(cmds, cmd)
newRow, newCol := m.cursorLineNumber(), m.col newRow, newCol := m.cursorLineNumber(), m.col
m.virtualCursor, cmd = m.virtualCursor.Update(msg) m.virtualCursor, cmd = m.virtualCursor.Update(msg)
if (newRow != oldRow || newCol != oldCol) && m.virtualCursor.Mode() == cursor.CursorBlink { 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) cmds = append(cmds, cmd)
m.SetHeight(m.ContentHeight())
return m, tea.Batch(cmds...) return m, tea.Batch(cmds...)
} }
+19 -24
View File
@@ -93,12 +93,7 @@ func (tm *ToastManager) renderSingleToast(toast Toast) string {
baseStyle := styles.BaseStyle(). baseStyle := styles.BaseStyle().
Background(t.BackgroundElement()). Background(t.BackgroundElement()).
Foreground(t.Text()). Foreground(t.Text()).
Padding(1, 2). Padding(1, 2)
BorderStyle(lipgloss.ThickBorder()).
BorderBackground(t.Background()).
BorderForeground(toast.Color).
BorderLeft(true).
BorderRight(true)
maxWidth := max(40, layout.Current.Viewport.Width/3) maxWidth := max(40, layout.Current.Viewport.Width/3)
contentMaxWidth := max(maxWidth-6, 20) contentMaxWidth := max(maxWidth-6, 20)
@@ -137,9 +132,7 @@ func (tm *ToastManager) View() string {
toastViews = append(toastViews, toastView+"\n") toastViews = append(toastViews, toastView+"\n")
} }
t := theme.CurrentTheme() return strings.Join(toastViews, "\n")
content := lipgloss.JoinVertical(lipgloss.Right, toastViews...)
return lipgloss.NewStyle().Background(t.Background()).Render(content)
} }
// RenderOverlay renders the toasts as an overlay on the given background // RenderOverlay renders the toasts as an overlay on the given background
@@ -151,38 +144,40 @@ func (tm *ToastManager) RenderOverlay(background string) string {
bgWidth := lipgloss.Width(background) bgWidth := lipgloss.Width(background)
bgHeight := lipgloss.Height(background) bgHeight := lipgloss.Height(background)
result := background result := background
// Start from top with 2 character padding // Start from top with 2 character padding
currentY := 2 currentY := 2
// Render each toast individually // Render each toast individually
for _, toast := range tm.toasts { for _, toast := range tm.toasts {
// Render individual toast // Render individual toast
toastView := tm.renderSingleToast(toast) toastView := tm.renderSingleToast(toast)
toastWidth := lipgloss.Width(toastView) toastWidth := lipgloss.Width(toastView)
toastHeight := lipgloss.Height(toastView) toastHeight := lipgloss.Height(toastView)
// Position at top-right with 2 character padding from right edge // Position at top-right with 2 character padding from right edge
x := bgWidth - toastWidth - 2 x := max(bgWidth-toastWidth-4, 0)
// Ensure we don't go negative
if x < 0 {
x = 0
}
// Check if toast fits vertically // Check if toast fits vertically
if currentY + toastHeight > bgHeight - 2 { if currentY+toastHeight > bgHeight-2 {
// No more room for toasts // No more room for toasts
break break
} }
// Place this toast // Place this toast
result = layout.PlaceOverlay(x, currentY, toastView, result) result = layout.PlaceOverlay(
x,
currentY,
toastView,
result,
layout.WithOverlayBorder(),
layout.WithOverlayBorderColor(toast.Color),
)
// Move down for next toast (add 1 for spacing between toasts) // Move down for next toast (add 1 for spacing between toasts)
currentY += toastHeight + 1 currentY += toastHeight + 1
} }
return result return result
} }
@@ -5,8 +5,8 @@ package image
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"image"
"github.com/atotto/clipboard" "github.com/atotto/clipboard"
"image"
) )
func GetImageFromClipboard() ([]byte, string, error) { func GetImageFromClipboard() ([]byte, string, error) {
@@ -28,8 +28,6 @@ func GetImageFromClipboard() ([]byte, string, error) {
} }
func binaryToImage(data []byte) ([]byte, error) { func binaryToImage(data []byte) ([]byte, error) {
reader := bytes.NewReader(data) reader := bytes.NewReader(data)
img, _, err := image.Decode(reader) img, _, err := image.Decode(reader)
@@ -40,7 +38,6 @@ func binaryToImage(data []byte) ([]byte, error) {
return ImageToBytes(img) return ImageToBytes(img)
} }
func min(a, b int) int { func min(a, b int) int {
if a < b { if a < b {
return a return a
+264 -15
View File
@@ -1,9 +1,13 @@
package layout package layout
import ( import (
"fmt"
"regexp"
"strings" "strings"
"unicode/utf8"
"github.com/charmbracelet/lipgloss/v2" "github.com/charmbracelet/lipgloss/v2"
"github.com/charmbracelet/lipgloss/v2/compat"
chAnsi "github.com/charmbracelet/x/ansi" chAnsi "github.com/charmbracelet/x/ansi"
"github.com/muesli/ansi" "github.com/muesli/ansi"
"github.com/muesli/reflow/truncate" "github.com/muesli/reflow/truncate"
@@ -23,29 +27,58 @@ func getLines(s string) (lines []string, widest int) {
return lines, widest return lines, widest
} }
// overlayOptions holds configuration for overlay rendering
type overlayOptions struct {
whitespace *whitespace
border bool
borderColor *compat.AdaptiveColor
}
// OverlayOption sets options for overlay rendering
type OverlayOption func(*overlayOptions)
// PlaceOverlay places fg on top of bg. // PlaceOverlay places fg on top of bg.
func PlaceOverlay( func PlaceOverlay(
x, y int, x, y int,
fg, bg string, fg, bg string,
opts ...WhitespaceOption, opts ...OverlayOption,
) string { ) string {
fgLines, fgWidth := getLines(fg) fgLines, fgWidth := getLines(fg)
bgLines, bgWidth := getLines(bg) bgLines, bgWidth := getLines(bg)
bgHeight := len(bgLines) bgHeight := len(bgLines)
fgHeight := len(fgLines) fgHeight := len(fgLines)
if fgWidth >= bgWidth && fgHeight >= bgHeight { // Parse options
// FIXME: return fg or bg? options := &overlayOptions{
return fg whitespace: &whitespace{},
}
for _, opt := range opts {
opt(options)
} }
// TODO: allow placement outside of the bg box? // Adjust for borders if enabled
x = util.Clamp(x, 0, bgWidth-fgWidth) if options.border {
y = util.Clamp(y, 0, bgHeight-fgHeight) // Add space for left and right borders
adjustedFgWidth := fgWidth + 2
// Adjust placement to account for borders
x = util.Clamp(x, 0, bgWidth-adjustedFgWidth)
y = util.Clamp(y, 0, bgHeight-fgHeight)
ws := &whitespace{} // Pad all foreground lines to the same width for consistent borders
for _, opt := range opts { for i := range fgLines {
opt(ws) lineWidth := ansi.PrintableRuneWidth(fgLines[i])
if lineWidth < fgWidth {
fgLines[i] += strings.Repeat(" ", fgWidth-lineWidth)
}
}
} else {
if fgWidth >= bgWidth && fgHeight >= bgHeight {
// FIXME: return fg or bg?
return fg
}
// TODO: allow placement outside of the bg box?
x = util.Clamp(x, 0, bgWidth-fgWidth)
y = util.Clamp(y, 0, bgHeight-fgHeight)
} }
var b strings.Builder var b strings.Builder
@@ -59,25 +92,72 @@ func PlaceOverlay(
} }
pos := 0 pos := 0
// Handle left side of the line up to the overlay
if x > 0 { if x > 0 {
left := truncate.String(bgLine, uint(x)) left := truncate.String(bgLine, uint(x))
pos = ansi.PrintableRuneWidth(left) pos = ansi.PrintableRuneWidth(left)
b.WriteString(left) b.WriteString(left)
if pos < x { if pos < x {
b.WriteString(ws.render(x - pos)) b.WriteString(options.whitespace.render(x - pos))
pos = x pos = x
} }
} }
fgLine := fgLines[i-y] // Render the overlay content with optional borders
b.WriteString(fgLine) if options.border {
pos += ansi.PrintableRuneWidth(fgLine) // Get the foreground line
fgLine := fgLines[i-y]
fgLineWidth := ansi.PrintableRuneWidth(fgLine)
// Extract the styles at the border positions
// 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("┃")
if leftSeq != "" {
b.WriteString("\x1b[0m") // Reset all styles only if we applied any
}
pos++
// Content
b.WriteString(fgLine)
pos += fgLineWidth
// Right border - combine background from original with border foreground
rightSeq := combineStyles(rightStyle, options.borderColor)
if rightSeq != "" {
b.WriteString(rightSeq)
}
b.WriteString("┃")
if rightSeq != "" {
b.WriteString("\x1b[0m") // Reset all styles only if we applied any
}
pos++
} else {
// No border, just render the content
fgLine := fgLines[i-y]
b.WriteString(fgLine)
pos += ansi.PrintableRuneWidth(fgLine)
}
// Handle right side of the line after the overlay
right := cutLeft(bgLine, pos) right := cutLeft(bgLine, pos)
bgWidth := ansi.PrintableRuneWidth(bgLine) bgWidth := ansi.PrintableRuneWidth(bgLine)
rightWidth := ansi.PrintableRuneWidth(right) rightWidth := ansi.PrintableRuneWidth(right)
if rightWidth <= bgWidth-pos { if rightWidth <= bgWidth-pos {
b.WriteString(ws.render(bgWidth - rightWidth - pos)) b.WriteString(options.whitespace.render(bgWidth - rightWidth - pos))
} }
b.WriteString(right) b.WriteString(right)
@@ -92,6 +172,152 @@ func cutLeft(s string, cutWidth int) string {
return chAnsi.Cut(s, cutWidth, lipgloss.Width(s)) return chAnsi.Cut(s, cutWidth, lipgloss.Width(s))
} }
// ansiStyle represents parsed ANSI style attributes
type ansiStyle struct {
fgColor string
bgColor string
attrs []string
}
// parseANSISequence parses an ANSI escape sequence into its components
func parseANSISequence(seq string) ansiStyle {
style := ansiStyle{}
// Extract the parameters from the sequence (e.g., \x1b[38;5;123;48;5;456m -> "38;5;123;48;5;456")
if !strings.HasPrefix(seq, "\x1b[") || !strings.HasSuffix(seq, "m") {
return style
}
params := seq[2 : len(seq)-1]
if params == "" {
return style
}
parts := strings.Split(params, ";")
i := 0
for i < len(parts) {
switch parts[i] {
case "0": // Reset
// 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
if i+1 < len(parts) && parts[i+1] == "5" && i+2 < len(parts) {
// 256 color mode
style.fgColor = strings.Join(parts[i:i+3], ";")
i += 2
} else if i+1 < len(parts) && parts[i+1] == "2" && i+4 < len(parts) {
// RGB color mode
style.fgColor = strings.Join(parts[i:i+5], ";")
i += 4
}
case "48": // Background color
if i+1 < len(parts) && parts[i+1] == "5" && i+2 < len(parts) {
// 256 color mode
style.bgColor = strings.Join(parts[i:i+3], ";")
i += 2
} else if i+1 < len(parts) && parts[i+1] == "2" && i+4 < len(parts) {
// RGB color mode
style.bgColor = strings.Join(parts[i:i+5], ";")
i += 4
}
case "30", "31", "32", "33", "34", "35", "36", "37": // Standard foreground colors
style.fgColor = parts[i]
case "40", "41", "42", "43", "44", "45", "46", "47": // Standard background colors
style.bgColor = parts[i]
case "90", "91", "92", "93", "94", "95", "96", "97": // Bright foreground colors
style.fgColor = parts[i]
case "100", "101", "102", "103", "104", "105", "106", "107": // Bright background colors
style.bgColor = parts[i]
}
i++
}
return style
}
// combineStyles creates an ANSI sequence that combines background from one style with foreground from another
func combineStyles(bgStyle ansiStyle, fgColor *compat.AdaptiveColor) string {
if fgColor == nil && bgStyle.bgColor == "" && len(bgStyle.attrs) == 0 {
return ""
}
var parts []string
// Add attributes
parts = append(parts, bgStyle.attrs...)
// Add background color from the original style
if bgStyle.bgColor != "" {
parts = append(parts, bgStyle.bgColor)
}
// Add foreground color if specified
if fgColor != nil {
// Use the 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, ";"))
}
// getStyleAtPosition extracts the active ANSI style at a given visual position
func getStyleAtPosition(s string, targetPos int) ansiStyle {
// ANSI escape sequence regex
ansiRegex := regexp.MustCompile(`\x1b\[[0-9;]*m`)
visualPos := 0
currentStyle := ansiStyle{}
i := 0
for i < len(s) && visualPos <= targetPos {
// Check if we're at an ANSI escape sequence
if match := ansiRegex.FindStringIndex(s[i:]); match != nil && match[0] == 0 {
// Found an ANSI sequence at current position
seq := s[i : i+match[1]]
parsedStyle := parseANSISequence(seq)
// 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
}
}
i += match[1]
} else if i < len(s) {
// Regular character
if visualPos == targetPos {
return currentStyle
}
_, size := utf8.DecodeRuneInString(s[i:])
i += size
visualPos++
}
}
return currentStyle
}
type whitespace struct { type whitespace struct {
style termenv.Style style termenv.Style
chars string chars string
@@ -129,3 +355,26 @@ func (w whitespace) render(width int) string {
// WhitespaceOption sets a styling rule for rendering whitespace. // WhitespaceOption sets a styling rule for rendering whitespace.
type WhitespaceOption func(*whitespace) type WhitespaceOption func(*whitespace)
// WithWhitespace sets whitespace options for the overlay
func WithWhitespace(opts ...WhitespaceOption) OverlayOption {
return func(o *overlayOptions) {
for _, opt := range opts {
opt(o.whitespace)
}
}
}
// WithOverlayBorder enables border rendering for the overlay
func WithOverlayBorder() OverlayOption {
return func(o *overlayOptions) {
o.border = true
}
}
// WithOverlayBorderColor sets the border color for the overlay
func WithOverlayBorderColor(color compat.AdaptiveColor) OverlayOption {
return func(o *overlayOptions) {
o.borderColor = &color
}
}
+1 -1
View File
@@ -8,6 +8,6 @@ var Terminal *TerminalInfo
func init() { func init() {
Terminal = &TerminalInfo{ Terminal = &TerminalInfo{
BackgroundIsDark: false, BackgroundIsDark: true,
} }
} }
+4 -4
View File
@@ -15,7 +15,7 @@ func BaseStyle() lipgloss.Style {
func Panel() lipgloss.Style { func Panel() lipgloss.Style {
t := theme.CurrentTheme() t := theme.CurrentTheme()
return lipgloss.NewStyle(). return lipgloss.NewStyle().
Background(t.BackgroundSubtle()). Background(t.BackgroundPanel()).
Border(lipgloss.NormalBorder(), true, false, true, false). Border(lipgloss.NormalBorder(), true, false, true, false).
BorderForeground(t.BorderSubtle()). BorderForeground(t.BorderSubtle()).
Foreground(t.Text()) Foreground(t.Text())
@@ -131,9 +131,9 @@ func BackgroundColor() compat.AdaptiveColor {
return theme.CurrentTheme().Background() return theme.CurrentTheme().Background()
} }
// BackgroundSubtleColor returns the subtle background color from the current theme // BackgroundPanelColor returns the subtle background color from the current theme
func BackgroundSubtleColor() compat.AdaptiveColor { func BackgroundPanelColor() compat.AdaptiveColor {
return theme.CurrentTheme().BackgroundSubtle() return theme.CurrentTheme().BackgroundPanel()
} }
// BackgroundElementColor returns the darker background color from the current theme // 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())
}
+394
View File
@@ -0,0 +1,394 @@
package theme
import (
"embed"
"encoding/json"
"fmt"
"image/color"
"os"
"path/filepath"
"strings"
"github.com/charmbracelet/lipgloss/v2"
"github.com/charmbracelet/lipgloss/v2/compat"
)
//go:embed themes/*.json
var themesFS embed.FS
type JSONTheme struct {
Defs map[string]any `json:"defs,omitempty"`
Theme map[string]any `json:"theme"`
}
type LoadedTheme struct {
BaseTheme
name string
}
type colorRef struct {
value any
resolved bool
}
func LoadThemesFromJSON() error {
entries, err := themesFS.ReadDir("themes")
if err != nil {
return fmt.Errorf("failed to read themes directory: %w", err)
}
for _, entry := range entries {
if !strings.HasSuffix(entry.Name(), ".json") {
continue
}
themeName := strings.TrimSuffix(entry.Name(), ".json")
data, err := themesFS.ReadFile(filepath.Join("themes", entry.Name()))
if err != nil {
return fmt.Errorf("failed to read theme file %s: %w", entry.Name(), err)
}
theme, err := parseJSONTheme(themeName, data)
if err != nil {
return fmt.Errorf("failed to parse theme %s: %w", themeName, err)
}
RegisterTheme(themeName, theme)
}
return nil
}
// LoadThemesFromDirectories loads themes from user directories in the correct override order.
// The hierarchy is (from lowest to highest priority):
// 1. Built-in themes (embedded)
// 2. USER_CONFIG/opencode/themes/*.json
// 3. PROJECT_ROOT/.opencode/themes/*.json
// 4. CWD/.opencode/themes/*.json
func LoadThemesFromDirectories(userConfig, projectRoot, cwd string) error {
if err := LoadThemesFromJSON(); err != nil {
return fmt.Errorf("failed to load built-in themes: %w", err)
}
dirs := []string{
filepath.Join(userConfig, "themes"),
filepath.Join(projectRoot, ".opencode", "themes"),
}
if cwd != projectRoot {
dirs = append(dirs, filepath.Join(cwd, ".opencode", "themes"))
}
for _, dir := range dirs {
if err := loadThemesFromDirectory(dir); err != nil {
fmt.Printf("Warning: Failed to load themes from %s: %v\n", dir, err)
}
}
return nil
}
func loadThemesFromDirectory(dir string) error {
if _, err := os.Stat(dir); os.IsNotExist(err) {
return nil // Directory doesn't exist, which is fine
}
entries, err := os.ReadDir(dir)
if err != nil {
return fmt.Errorf("failed to read directory: %w", err)
}
for _, entry := range entries {
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".json") {
continue
}
themeName := strings.TrimSuffix(entry.Name(), ".json")
filePath := filepath.Join(dir, entry.Name())
data, err := os.ReadFile(filePath)
if err != nil {
fmt.Printf("Warning: Failed to read theme file %s: %v\n", filePath, err)
continue
}
theme, err := parseJSONTheme(themeName, data)
if err != nil {
fmt.Printf("Warning: Failed to parse theme %s: %v\n", filePath, err)
continue
}
RegisterTheme(themeName, theme)
}
return nil
}
func parseJSONTheme(name string, data []byte) (Theme, error) {
var jsonTheme JSONTheme
if err := json.Unmarshal(data, &jsonTheme); err != nil {
return nil, fmt.Errorf("failed to unmarshal JSON: %w", err)
}
theme := &LoadedTheme{
name: name,
}
colorMap := make(map[string]*colorRef)
for key, value := range jsonTheme.Defs {
colorMap[key] = &colorRef{value: value, resolved: false}
}
for key, value := range jsonTheme.Theme {
colorMap[key] = &colorRef{value: value, resolved: false}
}
resolver := &colorResolver{
colors: colorMap,
visited: make(map[string]bool),
}
for key, value := range jsonTheme.Theme {
resolved, err := resolver.resolveColor(key, value)
if err != nil {
return nil, fmt.Errorf("failed to resolve color %s: %w", key, err)
}
adaptiveColor, err := parseResolvedColor(resolved)
if err != nil {
return nil, fmt.Errorf("failed to parse color %s: %w", key, err)
}
if err := setThemeColor(theme, key, adaptiveColor); err != nil {
return nil, fmt.Errorf("failed to set color %s: %w", key, err)
}
}
return theme, nil
}
type colorResolver struct {
colors map[string]*colorRef
visited map[string]bool
}
func (r *colorResolver) resolveColor(key string, value any) (any, error) {
if r.visited[key] {
return nil, fmt.Errorf("circular reference detected for color %s", key)
}
r.visited[key] = true
defer func() { r.visited[key] = false }()
switch v := value.(type) {
case string:
if strings.HasPrefix(v, "#") {
return v, nil
}
return r.resolveReference(v)
case float64:
return v, nil
case map[string]any:
resolved := make(map[string]any)
if dark, ok := v["dark"]; ok {
resolvedDark, err := r.resolveColorValue(dark)
if err != nil {
return nil, fmt.Errorf("failed to resolve dark variant: %w", err)
}
resolved["dark"] = resolvedDark
}
if light, ok := v["light"]; ok {
resolvedLight, err := r.resolveColorValue(light)
if err != nil {
return nil, fmt.Errorf("failed to resolve light variant: %w", err)
}
resolved["light"] = resolvedLight
}
return resolved, nil
default:
return nil, fmt.Errorf("invalid color value type: %T", value)
}
}
func (r *colorResolver) resolveColorValue(value any) (any, error) {
switch v := value.(type) {
case string:
if strings.HasPrefix(v, "#") {
return v, nil
}
return r.resolveReference(v)
case float64:
return v, nil
default:
return nil, fmt.Errorf("invalid color value type: %T", value)
}
}
func (r *colorResolver) resolveReference(ref string) (any, error) {
colorRef, exists := r.colors[ref]
if !exists {
return nil, fmt.Errorf("color reference '%s' not found", ref)
}
if colorRef.resolved {
return colorRef.value, nil
}
resolved, err := r.resolveColor(ref, colorRef.value)
if err != nil {
return nil, err
}
colorRef.value = resolved
colorRef.resolved = true
return resolved, nil
}
func parseResolvedColor(value any) (compat.AdaptiveColor, error) {
switch v := value.(type) {
case string:
return compat.AdaptiveColor{
Dark: lipgloss.Color(v),
Light: lipgloss.Color(v),
}, nil
case float64:
colorStr := fmt.Sprintf("%d", int(v))
return compat.AdaptiveColor{
Dark: lipgloss.Color(colorStr),
Light: lipgloss.Color(colorStr),
}, nil
case map[string]any:
dark, darkOk := v["dark"]
light, lightOk := v["light"]
if !darkOk || !lightOk {
return compat.AdaptiveColor{}, fmt.Errorf("color object must have both 'dark' and 'light' keys")
}
darkColor, err := parseColorValue(dark)
if err != nil {
return compat.AdaptiveColor{}, fmt.Errorf("failed to parse dark color: %w", err)
}
lightColor, err := parseColorValue(light)
if err != nil {
return compat.AdaptiveColor{}, fmt.Errorf("failed to parse light color: %w", err)
}
return compat.AdaptiveColor{
Dark: darkColor,
Light: lightColor,
}, nil
default:
return compat.AdaptiveColor{}, fmt.Errorf("invalid resolved color type: %T", value)
}
}
func parseColorValue(value any) (color.Color, error) {
switch v := value.(type) {
case string:
return lipgloss.Color(v), nil
case float64:
return lipgloss.Color(fmt.Sprintf("%d", int(v))), nil
default:
return nil, fmt.Errorf("invalid color value type: %T", value)
}
}
func setThemeColor(theme *LoadedTheme, key string, color compat.AdaptiveColor) error {
switch key {
case "primary":
theme.PrimaryColor = color
case "secondary":
theme.SecondaryColor = color
case "accent":
theme.AccentColor = color
case "error":
theme.ErrorColor = color
case "warning":
theme.WarningColor = color
case "success":
theme.SuccessColor = color
case "info":
theme.InfoColor = color
case "text":
theme.TextColor = color
case "textMuted":
theme.TextMutedColor = color
case "background":
theme.BackgroundColor = color
case "backgroundPanel":
theme.BackgroundPanelColor = color
case "backgroundElement":
theme.BackgroundElementColor = color
case "border":
theme.BorderColor = color
case "borderActive":
theme.BorderActiveColor = color
case "borderSubtle":
theme.BorderSubtleColor = color
case "diffAdded":
theme.DiffAddedColor = color
case "diffRemoved":
theme.DiffRemovedColor = color
case "diffContext":
theme.DiffContextColor = color
case "diffHunkHeader":
theme.DiffHunkHeaderColor = color
case "diffHighlightAdded":
theme.DiffHighlightAddedColor = color
case "diffHighlightRemoved":
theme.DiffHighlightRemovedColor = color
case "diffAddedBg":
theme.DiffAddedBgColor = color
case "diffRemovedBg":
theme.DiffRemovedBgColor = color
case "diffContextBg":
theme.DiffContextBgColor = color
case "diffLineNumber":
theme.DiffLineNumberColor = color
case "diffAddedLineNumberBg":
theme.DiffAddedLineNumberBgColor = color
case "diffRemovedLineNumberBg":
theme.DiffRemovedLineNumberBgColor = color
case "markdownText":
theme.MarkdownTextColor = color
case "markdownHeading":
theme.MarkdownHeadingColor = color
case "markdownLink":
theme.MarkdownLinkColor = color
case "markdownLinkText":
theme.MarkdownLinkTextColor = color
case "markdownCode":
theme.MarkdownCodeColor = color
case "markdownBlockQuote":
theme.MarkdownBlockQuoteColor = color
case "markdownEmph":
theme.MarkdownEmphColor = color
case "markdownStrong":
theme.MarkdownStrongColor = color
case "markdownHorizontalRule":
theme.MarkdownHorizontalRuleColor = color
case "markdownListItem":
theme.MarkdownListItemColor = color
case "markdownListEnumeration":
theme.MarkdownListEnumerationColor = color
case "markdownImage":
theme.MarkdownImageColor = color
case "markdownImageText":
theme.MarkdownImageTextColor = color
case "markdownCodeBlock":
theme.MarkdownCodeBlockColor = color
case "syntaxComment":
theme.SyntaxCommentColor = color
case "syntaxKeyword":
theme.SyntaxKeywordColor = color
case "syntaxFunction":
theme.SyntaxFunctionColor = color
case "syntaxVariable":
theme.SyntaxVariableColor = color
case "syntaxString":
theme.SyntaxStringColor = color
case "syntaxNumber":
theme.SyntaxNumberColor = color
case "syntaxType":
theme.SyntaxTypeColor = color
case "syntaxOperator":
theme.SyntaxOperatorColor = color
case "syntaxPunctuation":
theme.SyntaxPunctuationColor = color
default:
// Ignore unknown keys for forward compatibility
return nil
}
return nil
}
+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 ( import (
"fmt" "fmt"
"log/slog"
"slices" "slices"
"strings" "strings"
"sync" "sync"
"github.com/alecthomas/chroma/v2/styles" "github.com/alecthomas/chroma/v2/styles"
// "github.com/alecthomas/chroma/v2/styles"
) )
// Manager handles theme registration, selection, and retrieval. // Manager handles theme registration, selection, and retrieval.
@@ -25,9 +23,6 @@ var globalManager = &Manager{
currentName: "", currentName: "",
} }
// Default theme instance for custom theme defaulting
var defaultThemeColors = NewOpenCodeTheme()
// RegisterTheme adds a new theme to the registry. // RegisterTheme adds a new theme to the registry.
// If this is the first theme registered, it becomes the default. // If this is the first theme registered, it becomes the default.
func RegisterTheme(name string, theme Theme) { func RegisterTheme(name string, theme Theme) {
@@ -89,6 +84,7 @@ func AvailableThemes() []string {
names = append(names, name) names = append(names, name)
} }
slices.SortFunc(names, func(a, b string) int { slices.SortFunc(names, func(a, b string) int {
// list system theme first
if a == "opencode" { if a == "opencode" {
return -1 return -1
} else if b == "opencode" { } else if b == "opencode" {
@@ -107,130 +103,3 @@ func GetTheme(name string) Theme {
return globalManager.themes[name] 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 package theme
import ( import (
"fmt"
"regexp"
"github.com/charmbracelet/lipgloss/v2"
"github.com/charmbracelet/lipgloss/v2/compat" "github.com/charmbracelet/lipgloss/v2/compat"
) )
@@ -14,7 +10,7 @@ import (
type Theme interface { type Theme interface {
// Background colors // Background colors
Background() compat.AdaptiveColor // Radix 1 Background() compat.AdaptiveColor // Radix 1
BackgroundSubtle() compat.AdaptiveColor // Radix 2 BackgroundPanel() compat.AdaptiveColor // Radix 2
BackgroundElement() compat.AdaptiveColor // Radix 3 BackgroundElement() compat.AdaptiveColor // Radix 3
// Border colors // Border colors
@@ -84,7 +80,7 @@ type Theme interface {
type BaseTheme struct { type BaseTheme struct {
// Background colors // Background colors
BackgroundColor compat.AdaptiveColor BackgroundColor compat.AdaptiveColor
BackgroundSubtleColor compat.AdaptiveColor BackgroundPanelColor compat.AdaptiveColor
BackgroundElementColor compat.AdaptiveColor BackgroundElementColor compat.AdaptiveColor
// Border colors // 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) TextMuted() compat.AdaptiveColor { return t.TextMutedColor }
func (t *BaseTheme) Background() compat.AdaptiveColor { return t.BackgroundColor } 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) BackgroundElement() compat.AdaptiveColor { return t.BackgroundElementColor }
func (t *BaseTheme) Border() compat.AdaptiveColor { return t.BorderColor } 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) SyntaxType() compat.AdaptiveColor { return t.SyntaxTypeColor }
func (t *BaseTheme) SyntaxOperator() compat.AdaptiveColor { return t.SyntaxOperatorColor } func (t *BaseTheme) SyntaxOperator() compat.AdaptiveColor { return t.SyntaxOperatorColor }
func (t *BaseTheme) SyntaxPunctuation() compat.AdaptiveColor { return t.SyntaxPunctuationColor } 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())
}
+110 -74
View File
@@ -44,7 +44,11 @@ type appModel struct {
func (a appModel) Init() tea.Cmd { func (a appModel) Init() tea.Cmd {
var cmds []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.app.InitializeProvider())
cmds = append(cmds, a.editor.Init()) cmds = append(cmds, a.editor.Init())
cmds = append(cmds, a.messages.Init()) cmds = append(cmds, a.messages.Init())
@@ -66,13 +70,16 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) { switch msg := msg.(type) {
case tea.KeyPressMsg: case tea.KeyPressMsg:
keyString := msg.String()
// 1. Handle active modal // 1. Handle active modal
if a.modal != nil { if a.modal != nil {
switch msg.String() { switch keyString {
// Escape always closes current modal // Escape always closes current modal
case "esc", "ctrl+c": case "esc", "ctrl+c":
cmd := a.modal.Close()
a.modal = nil a.modal = nil
return a, nil return a, cmd
} }
// Pass all other key presses to the modal // Pass all other key presses to the modal
@@ -84,7 +91,6 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// 2. Check for commands that require leader // 2. Check for commands that require leader
if a.isLeaderSequence { if a.isLeaderSequence {
matches := a.app.Commands.Matches(msg, a.isLeaderSequence) matches := a.app.Commands.Matches(msg, a.isLeaderSequence)
// Reset leader state
a.isLeaderSequence = false a.isLeaderSequence = false
if len(matches) > 0 { if len(matches) > 0 {
return a, util.CmdHandler(commands.ExecuteCommandsMsg(matches)) return a, util.CmdHandler(commands.ExecuteCommandsMsg(matches))
@@ -92,48 +98,49 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
} }
// 3. Handle completions trigger // 3. Handle completions trigger
switch msg.String() { if keyString == "/" && !a.showCompletionDialog {
case "/": a.showCompletionDialog = true
if !a.showCompletionDialog {
a.showCompletionDialog = true
initialValue := "/" initialValue := "/"
currentInput := a.editor.Value() currentInput := a.editor.Value()
// if the input doesn't end with a space,
// then we want to include the last word // if the input doesn't end with a space,
if !strings.HasSuffix(currentInput, " ") { // then we want to include the last word
words := strings.Split(a.editor.Value(), " ") // (ie, `packages/`)
if len(words) > 0 { if !strings.HasSuffix(currentInput, " ") {
lastWord := words[len(words)-1] words := strings.Split(a.editor.Value(), " ")
lastWord = strings.TrimSpace(lastWord) if len(words) > 0 {
initialValue = lastWord + "/" 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 { if a.showCompletionDialog {
switch msg.String() { switch keyString {
case "tab", "enter", "esc", "ctrl+c": case "tab", "enter", "esc", "ctrl+c":
context, contextCmd := a.completions.Update(msg) updated, cmd := a.updateCompletions(msg)
a.completions = context.(dialog.CompletionDialog) a.completions = updated.(dialog.CompletionDialog)
cmds = append(cmds, contextCmd) cmds = append(cmds, cmd)
return a, tea.Batch(cmds...) return a, tea.Batch(cmds...)
} }
@@ -141,13 +148,10 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
a.editor = updated.(chat.EditorComponent) a.editor = updated.(chat.EditorComponent)
cmds = append(cmds, cmd) cmds = append(cmds, cmd)
currentInput := a.editor.Value() updated, cmd = a.updateCompletions(msg)
provider := a.completionManager.GetProvider(currentInput) a.completions = updated.(dialog.CompletionDialog)
a.completions.SetProvider(provider) cmds = append(cmds, cmd)
context, contextCmd := a.completions.Update(msg)
a.completions = context.(dialog.CompletionDialog)
cmds = append(cmds, contextCmd)
return a, tea.Batch(cmds...) return a, tea.Batch(cmds...)
} }
@@ -173,10 +177,8 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return a, util.CmdHandler(commands.ExecuteCommandsMsg(matches)) return a, util.CmdHandler(commands.ExecuteCommandsMsg(matches))
} }
// 7. Fallback to editor. This shouldn't happen? // 7. Fallback to editor. This is for other characters
// All printable characters were already sent, and // like backspace, tab, etc.
// any other keypress that didn't match a command
// is likely a noop.
updatedEditor, cmd := a.editor.Update(msg) updatedEditor, cmd := a.editor.Update(msg)
a.editor = updatedEditor.(chat.EditorComponent) a.editor = updatedEditor.(chat.EditorComponent)
return a, cmd return a, cmd
@@ -187,14 +189,19 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
updated, cmd := a.messages.Update(msg) updated, cmd := a.messages.Update(msg)
a.messages = updated.(chat.MessagesComponent) a.messages = updated.(chat.MessagesComponent)
cmds = append(cmds, cmd) cmds = append(cmds, cmd)
return a, tea.Batch(cmds...)
case tea.BackgroundColorMsg: case tea.BackgroundColorMsg:
styles.Terminal = &styles.TerminalInfo{ styles.Terminal = &styles.TerminalInfo{
BackgroundIsDark: msg.IsDark(), BackgroundIsDark: msg.IsDark(),
} }
slog.Debug("Background color", "isDark", msg.IsDark()) slog.Debug("Background color", "isDark", msg.IsDark())
case modal.CloseModalMsg: case modal.CloseModalMsg:
var cmd tea.Cmd
if a.modal != nil {
cmd = a.modal.Close()
}
a.modal = nil a.modal = nil
return a, nil return a, cmd
case commands.ExecuteCommandMsg: case commands.ExecuteCommandMsg:
updated, cmd := a.executeCommand(commands.Command(msg)) updated, cmd := a.executeCommand(commands.Command(msg))
return updated, cmd return updated, cmd
@@ -211,7 +218,11 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
cmds = append(cmds, cmd) cmds = append(cmds, cmd)
case dialog.CompletionDialogCloseMsg: case dialog.CompletionDialogCloseMsg:
a.showCompletionDialog = false a.showCompletionDialog = false
a.completions.SetProvider(a.completionManager.DefaultProvider()) case client.EventInstallationUpdated:
return a, toast.NewSuccessToast(
"opencode updated to "+msg.Properties.Version+", restart to apply.",
toast.WithTitle("New version installed"),
)
case client.EventSessionUpdated: case client.EventSessionUpdated:
if msg.Properties.Info.Id == a.app.Session.Id { if msg.Properties.Info.Id == a.app.Session.Id {
a.app.Session = &msg.Properties.Info a.app.Session = &msg.Properties.Info
@@ -219,16 +230,43 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case client.EventMessageUpdated: case client.EventMessageUpdated:
if msg.Properties.Info.Metadata.SessionID == a.app.Session.Id { if msg.Properties.Info.Metadata.SessionID == a.app.Session.Id {
exists := false exists := false
for i, m := range a.app.Messages { optimisticReplaced := false
if m.Id == msg.Properties.Info.Id {
a.app.Messages[i] = msg.Properties.Info // First check if this is replacing an optimistic message
exists = true 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 { if !exists {
a.app.Messages = append(a.app.Messages, msg.Properties.Info) a.app.Messages = append(a.app.Messages, msg.Properties.Info)
} }
} }
case client.EventSessionError:
unknownError, err := msg.Properties.Error.AsUnknownError()
if err == nil {
slog.Error("Server error", "name", unknownError.Name, "message", unknownError.Data.Message)
return a, toast.NewErrorToast(unknownError.Data.Message, toast.WithTitle(unknownError.Name))
}
case tea.WindowSizeMsg: case tea.WindowSizeMsg:
msg.Height -= 2 // Make space for the status bar msg.Height -= 2 // Make space for the status bar
a.width, a.height = msg.Width, msg.Height a.width, a.height = msg.Width, msg.Height
@@ -243,12 +281,13 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
} }
a.layout.SetSize(a.width, a.height) a.layout.SetSize(a.width, a.height)
case app.SessionSelectedMsg: case app.SessionSelectedMsg:
var err error messages, err := a.app.ListMessages(context.Background(), msg.Id)
a.app.Session = msg
a.app.Messages, err = a.app.ListMessages(context.Background(), msg.Id)
if err != nil { if err != nil {
slog.Error("Failed to list messages", "error", err) slog.Error("Failed to list messages", "error", err)
return a, toast.NewErrorToast("Failed to open session")
} }
a.app.Session = msg
a.app.Messages = messages
case app.ModelSelectedMsg: case app.ModelSelectedMsg:
a.app.Provider = &msg.Provider a.app.Provider = &msg.Provider
a.app.Model = &msg.Model a.app.Model = &msg.Model
@@ -329,7 +368,7 @@ func (a appModel) View() string {
layoutView, layoutView,
a.status.View(), a.status.View(),
} }
appView := lipgloss.JoinVertical(lipgloss.Top, components...) appView := strings.Join(components, "\n")
if a.modal != nil { if a.modal != nil {
appView = a.modal.Render(appView) appView = a.modal.Render(appView)
@@ -346,7 +385,7 @@ func (a appModel) executeCommand(command commands.Command) (tea.Model, tea.Cmd)
} }
switch command.Name { switch command.Name {
case commands.AppHelpCommand: case commands.AppHelpCommand:
helpDialog := dialog.NewHelpDialog(a.app.Commands.Sorted()) helpDialog := dialog.NewHelpDialog(a.app)
a.modal = helpDialog a.modal = helpDialog
case commands.EditorOpenCommand: case commands.EditorOpenCommand:
if a.app.IsBusy() { if a.app.IsBusy() {
@@ -495,30 +534,18 @@ func (a appModel) executeCommand(command commands.Command) (tea.Model, tea.Cmd)
a.messages = updated.(chat.MessagesComponent) a.messages = updated.(chat.MessagesComponent)
cmds = append(cmds, cmd) cmds = append(cmds, cmd)
case commands.MessagesPageUpCommand: case commands.MessagesPageUpCommand:
if a.showCompletionDialog {
return a, nil
}
updated, cmd := a.messages.PageUp() updated, cmd := a.messages.PageUp()
a.messages = updated.(chat.MessagesComponent) a.messages = updated.(chat.MessagesComponent)
cmds = append(cmds, cmd) cmds = append(cmds, cmd)
case commands.MessagesPageDownCommand: case commands.MessagesPageDownCommand:
if a.showCompletionDialog {
return a, nil
}
updated, cmd := a.messages.PageDown() updated, cmd := a.messages.PageDown()
a.messages = updated.(chat.MessagesComponent) a.messages = updated.(chat.MessagesComponent)
cmds = append(cmds, cmd) cmds = append(cmds, cmd)
case commands.MessagesHalfPageUpCommand: case commands.MessagesHalfPageUpCommand:
if a.showCompletionDialog {
return a, nil
}
updated, cmd := a.messages.HalfPageUp() updated, cmd := a.messages.HalfPageUp()
a.messages = updated.(chat.MessagesComponent) a.messages = updated.(chat.MessagesComponent)
cmds = append(cmds, cmd) cmds = append(cmds, cmd)
case commands.MessagesHalfPageDownCommand: case commands.MessagesHalfPageDownCommand:
if a.showCompletionDialog {
return a, nil
}
updated, cmd := a.messages.HalfPageDown() updated, cmd := a.messages.HalfPageDown()
a.messages = updated.(chat.MessagesComponent) a.messages = updated.(chat.MessagesComponent)
cmds = append(cmds, cmd) cmds = append(cmds, cmd)
@@ -528,6 +555,15 @@ func (a appModel) executeCommand(command commands.Command) (tea.Model, tea.Cmd)
return a, tea.Batch(cmds...) 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 { func NewModel(app *app.App) tea.Model {
completionManager := completions.NewCompletionManager(app) completionManager := completions.NewCompletionManager(app)
initialProvider := completionManager.DefaultProvider() initialProvider := completionManager.DefaultProvider()
@@ -544,8 +580,8 @@ func NewModel(app *app.App) tea.Model {
messagesContainer := layout.NewContainer(messages) messagesContainer := layout.NewContainer(messages)
var leaderBinding *key.Binding var leaderBinding *key.Binding
if (*app.Configg.Keybinds).Leader != nil { if (*app.Config.Keybinds).Leader != nil {
binding := key.NewBinding(key.WithKeys(*app.Configg.Keybinds.Leader)) binding := key.NewBinding(key.WithKeys(*app.Config.Keybinds.Leader))
leaderBinding = &binding leaderBinding = &binding
} }
+18
View File
@@ -1,6 +1,9 @@
package util package util
import ( import (
"os"
"strings"
tea "github.com/charmbracelet/bubbletea/v2" tea "github.com/charmbracelet/bubbletea/v2"
) )
@@ -17,3 +20,18 @@ func Clamp(v, low, high int) int {
} }
return min(high, max(low, v)) 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
}
+320 -224
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": { "/session_messages": {
"post": { "post": {
"responses": { "responses": {
@@ -525,6 +561,9 @@
{ {
"$ref": "#/components/schemas/Event.storage.write" "$ref": "#/components/schemas/Event.storage.write"
}, },
{
"$ref": "#/components/schemas/Event.installation.updated"
},
{ {
"$ref": "#/components/schemas/Event.lsp.client.diagnostics" "$ref": "#/components/schemas/Event.lsp.client.diagnostics"
}, },
@@ -537,9 +576,6 @@
{ {
"$ref": "#/components/schemas/Event.message.part.updated" "$ref": "#/components/schemas/Event.message.part.updated"
}, },
{
"$ref": "#/components/schemas/Event.installation.updated"
},
{ {
"$ref": "#/components/schemas/Event.session.updated" "$ref": "#/components/schemas/Event.session.updated"
}, },
@@ -551,11 +587,11 @@
"propertyName": "type", "propertyName": "type",
"mapping": { "mapping": {
"storage.write": "#/components/schemas/Event.storage.write", "storage.write": "#/components/schemas/Event.storage.write",
"installation.updated": "#/components/schemas/Event.installation.updated",
"lsp.client.diagnostics": "#/components/schemas/Event.lsp.client.diagnostics", "lsp.client.diagnostics": "#/components/schemas/Event.lsp.client.diagnostics",
"permission.updated": "#/components/schemas/Event.permission.updated", "permission.updated": "#/components/schemas/Event.permission.updated",
"message.updated": "#/components/schemas/Event.message.updated", "message.updated": "#/components/schemas/Event.message.updated",
"message.part.updated": "#/components/schemas/Event.message.part.updated", "message.part.updated": "#/components/schemas/Event.message.part.updated",
"installation.updated": "#/components/schemas/Event.installation.updated",
"session.updated": "#/components/schemas/Event.session.updated", "session.updated": "#/components/schemas/Event.session.updated",
"session.error": "#/components/schemas/Event.session.error" "session.error": "#/components/schemas/Event.session.error"
} }
@@ -586,6 +622,30 @@
"properties" "properties"
] ]
}, },
"Event.installation.updated": {
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "installation.updated"
},
"properties": {
"type": "object",
"properties": {
"version": {
"type": "string"
}
},
"required": [
"version"
]
}
},
"required": [
"type",
"properties"
]
},
"Event.lsp.client.diagnostics": { "Event.lsp.client.diagnostics": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -710,160 +770,7 @@
} }
}, },
"metadata": { "metadata": {
"type": "object", "$ref": "#/components/schemas/Message.Metadata"
"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"
]
} }
}, },
"required": [ "required": [
@@ -1117,6 +1024,162 @@
"type" "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": { "ProviderAuthError": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -1201,30 +1264,6 @@
"properties" "properties"
] ]
}, },
"Event.installation.updated": {
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "installation.updated"
},
"properties": {
"type": "object",
"properties": {
"version": {
"type": "string"
}
},
"required": [
"version"
]
}
},
"required": [
"type",
"properties"
]
},
"Event.session.updated": { "Event.session.updated": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -1391,13 +1430,16 @@
"type": "object", "type": "object",
"properties": { "properties": {
"$schema": { "$schema": {
"type": "string" "type": "string",
"description": "JSON schema reference for configuration validation"
}, },
"theme": { "theme": {
"type": "string" "type": "string",
"description": "Theme name to use for the interface"
}, },
"keybinds": { "keybinds": {
"$ref": "#/components/schemas/Config.Keybinds" "$ref": "#/components/schemas/Config.Keybinds",
"description": "Custom keybind configurations"
}, },
"autoshare": { "autoshare": {
"type": "boolean", "type": "boolean",
@@ -1458,6 +1500,9 @@
"temperature": { "temperature": {
"type": "boolean" "type": "boolean"
}, },
"tool_call": {
"type": "boolean"
},
"cost": { "cost": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -1496,6 +1541,10 @@
}, },
"id": { "id": {
"type": "string" "type": "string"
},
"options": {
"type": "object",
"additionalProperties": {}
} }
} }
} }
@@ -1508,7 +1557,8 @@
"required": [ "required": [
"models" "models"
] ]
} },
"description": "Custom provider configurations and model overrides"
}, },
"mcp": { "mcp": {
"type": "object", "type": "object",
@@ -1528,95 +1578,125 @@
"remote": "#/components/schemas/Config.McpRemote" "remote": "#/components/schemas/Config.McpRemote"
} }
} }
} },
"description": "MCP (Model Context Protocol) server configurations"
} }
} },
"additionalProperties": false
}, },
"Config.Keybinds": { "Config.Keybinds": {
"type": "object", "type": "object",
"properties": { "properties": {
"leader": { "leader": {
"type": "string" "type": "string",
"description": "Leader key for keybind combinations"
}, },
"help": { "help": {
"type": "string" "type": "string",
"description": "Show help dialog"
}, },
"editor_open": { "editor_open": {
"type": "string" "type": "string",
"description": "Open external editor"
}, },
"session_new": { "session_new": {
"type": "string" "type": "string",
"description": "Create a new session"
}, },
"session_list": { "session_list": {
"type": "string" "type": "string",
"description": "List all sessions"
}, },
"session_share": { "session_share": {
"type": "string" "type": "string",
"description": "Share current session"
}, },
"session_interrupt": { "session_interrupt": {
"type": "string" "type": "string",
"description": "Interrupt current session"
}, },
"session_compact": { "session_compact": {
"type": "string" "type": "string",
"description": "Toggle compact mode for session"
}, },
"tool_details": { "tool_details": {
"type": "string" "type": "string",
"description": "Show tool details"
}, },
"model_list": { "model_list": {
"type": "string" "type": "string",
"description": "List available models"
}, },
"theme_list": { "theme_list": {
"type": "string" "type": "string",
"description": "List available themes"
}, },
"project_init": { "project_init": {
"type": "string" "type": "string",
"description": "Initialize project configuration"
}, },
"input_clear": { "input_clear": {
"type": "string" "type": "string",
"description": "Clear input field"
}, },
"input_paste": { "input_paste": {
"type": "string" "type": "string",
"description": "Paste from clipboard"
}, },
"input_submit": { "input_submit": {
"type": "string" "type": "string",
"description": "Submit input"
}, },
"input_newline": { "input_newline": {
"type": "string" "type": "string",
"description": "Insert newline in input"
}, },
"history_previous": { "history_previous": {
"type": "string" "type": "string",
"description": "Navigate to previous history item"
}, },
"history_next": { "history_next": {
"type": "string" "type": "string",
"description": "Navigate to next history item"
}, },
"messages_page_up": { "messages_page_up": {
"type": "string" "type": "string",
"description": "Scroll messages up by one page"
}, },
"messages_page_down": { "messages_page_down": {
"type": "string" "type": "string",
"description": "Scroll messages down by one page"
}, },
"messages_half_page_up": { "messages_half_page_up": {
"type": "string" "type": "string",
"description": "Scroll messages up by half page"
}, },
"messages_half_page_down": { "messages_half_page_down": {
"type": "string" "type": "string",
"description": "Scroll messages down by half page"
}, },
"messages_previous": { "messages_previous": {
"type": "string" "type": "string",
"description": "Navigate to previous message"
}, },
"messages_next": { "messages_next": {
"type": "string" "type": "string",
"description": "Navigate to next message"
}, },
"messages_first": { "messages_first": {
"type": "string" "type": "string",
"description": "Navigate to first message"
}, },
"messages_last": { "messages_last": {
"type": "string" "type": "string",
"description": "Navigate to last message"
}, },
"app_exit": { "app_exit": {
"type": "string" "type": "string",
"description": "Exit the application"
} }
} },
"additionalProperties": false
}, },
"Provider.Info": { "Provider.Info": {
"type": "object", "type": "object",
@@ -1668,6 +1748,9 @@
"temperature": { "temperature": {
"type": "boolean" "type": "boolean"
}, },
"tool_call": {
"type": "boolean"
},
"cost": { "cost": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -1706,6 +1789,10 @@
}, },
"id": { "id": {
"type": "string" "type": "string"
},
"options": {
"type": "object",
"additionalProperties": {}
} }
}, },
"required": [ "required": [
@@ -1713,9 +1800,11 @@
"attachment", "attachment",
"reasoning", "reasoning",
"temperature", "temperature",
"tool_call",
"cost", "cost",
"limit", "limit",
"id" "id",
"options"
] ]
}, },
"Config.McpLocal": { "Config.McpLocal": {
@@ -1723,41 +1812,48 @@
"properties": { "properties": {
"type": { "type": {
"type": "string", "type": "string",
"const": "local" "const": "local",
"description": "Type of MCP server connection"
}, },
"command": { "command": {
"type": "array", "type": "array",
"items": { "items": {
"type": "string" "type": "string"
} },
"description": "Command and arguments to run the MCP server"
}, },
"environment": { "environment": {
"type": "object", "type": "object",
"additionalProperties": { "additionalProperties": {
"type": "string" "type": "string"
} },
"description": "Environment variables to set when running the MCP server"
} }
}, },
"required": [ "required": [
"type", "type",
"command" "command"
] ],
"additionalProperties": false
}, },
"Config.McpRemote": { "Config.McpRemote": {
"type": "object", "type": "object",
"properties": { "properties": {
"type": { "type": {
"type": "string", "type": "string",
"const": "remote" "const": "remote",
"description": "Type of MCP server connection"
}, },
"url": { "url": {
"type": "string" "type": "string",
"description": "URL of the remote MCP server"
} }
}, },
"required": [ "required": [
"type", "type",
"url" "url"
] ],
"additionalProperties": false
}, },
"Error": { "Error": {
"type": "object", "type": "object",
+352 -130
View File
@@ -41,6 +41,7 @@ type AppInfo struct {
// ConfigInfo defines model for Config.Info. // ConfigInfo defines model for Config.Info.
type ConfigInfo struct { type ConfigInfo struct {
// Schema JSON schema reference for configuration validation
Schema *string `json:"$schema,omitempty"` Schema *string `json:"$schema,omitempty"`
// Autoshare Share newly created sessions automatically // Autoshare Share newly created sessions automatically
@@ -50,12 +51,16 @@ type ConfigInfo struct {
Autoupdate *bool `json:"autoupdate,omitempty"` Autoupdate *bool `json:"autoupdate,omitempty"`
// DisabledProviders Disable providers that are loaded automatically // DisabledProviders Disable providers that are loaded automatically
DisabledProviders *[]string `json:"disabled_providers,omitempty"` DisabledProviders *[]string `json:"disabled_providers,omitempty"`
Keybinds *ConfigKeybinds `json:"keybinds,omitempty"` Keybinds *ConfigKeybinds `json:"keybinds,omitempty"`
Mcp *map[string]ConfigInfo_Mcp_AdditionalProperties `json:"mcp,omitempty"`
// Mcp MCP (Model Context Protocol) server configurations
Mcp *map[string]ConfigInfo_Mcp_AdditionalProperties `json:"mcp,omitempty"`
// Model Model to use in the format of provider/model, eg anthropic/claude-2 // Model Model to use in the format of provider/model, eg anthropic/claude-2
Model *string `json:"model,omitempty"` Model *string `json:"model,omitempty"`
// Provider Custom provider configurations and model overrides
Provider *map[string]struct { Provider *map[string]struct {
Api *string `json:"api,omitempty"` Api *string `json:"api,omitempty"`
Env *[]string `json:"env,omitempty"` Env *[]string `json:"env,omitempty"`
@@ -73,14 +78,18 @@ type ConfigInfo struct {
Context float32 `json:"context"` Context float32 `json:"context"`
Output float32 `json:"output"` Output float32 `json:"output"`
} `json:"limit,omitempty"` } `json:"limit,omitempty"`
Name *string `json:"name,omitempty"` Name *string `json:"name,omitempty"`
Reasoning *bool `json:"reasoning,omitempty"` Options *map[string]interface{} `json:"options,omitempty"`
Temperature *bool `json:"temperature,omitempty"` Reasoning *bool `json:"reasoning,omitempty"`
Temperature *bool `json:"temperature,omitempty"`
ToolCall *bool `json:"tool_call,omitempty"`
} `json:"models"` } `json:"models"`
Name *string `json:"name,omitempty"` Name *string `json:"name,omitempty"`
Npm *string `json:"npm,omitempty"` Npm *string `json:"npm,omitempty"`
Options *map[string]interface{} `json:"options,omitempty"` Options *map[string]interface{} `json:"options,omitempty"`
} `json:"provider,omitempty"` } `json:"provider,omitempty"`
// Theme Theme name to use for the interface
Theme *string `json:"theme,omitempty"` Theme *string `json:"theme,omitempty"`
} }
@@ -91,46 +100,107 @@ type ConfigInfo_Mcp_AdditionalProperties struct {
// ConfigKeybinds defines model for Config.Keybinds. // ConfigKeybinds defines model for Config.Keybinds.
type ConfigKeybinds struct { type ConfigKeybinds struct {
AppExit *string `json:"app_exit,omitempty"` // AppExit Exit the application
EditorOpen *string `json:"editor_open,omitempty"` AppExit *string `json:"app_exit,omitempty"`
Help *string `json:"help,omitempty"`
HistoryNext *string `json:"history_next,omitempty"` // EditorOpen Open external editor
HistoryPrevious *string `json:"history_previous,omitempty"` EditorOpen *string `json:"editor_open,omitempty"`
InputClear *string `json:"input_clear,omitempty"`
InputNewline *string `json:"input_newline,omitempty"` // Help Show help dialog
InputPaste *string `json:"input_paste,omitempty"` Help *string `json:"help,omitempty"`
InputSubmit *string `json:"input_submit,omitempty"`
Leader *string `json:"leader,omitempty"` // HistoryNext Navigate to next history item
MessagesFirst *string `json:"messages_first,omitempty"` HistoryNext *string `json:"history_next,omitempty"`
// HistoryPrevious Navigate to previous history item
HistoryPrevious *string `json:"history_previous,omitempty"`
// InputClear Clear input field
InputClear *string `json:"input_clear,omitempty"`
// InputNewline Insert newline in input
InputNewline *string `json:"input_newline,omitempty"`
// InputPaste Paste from clipboard
InputPaste *string `json:"input_paste,omitempty"`
// InputSubmit Submit input
InputSubmit *string `json:"input_submit,omitempty"`
// Leader Leader key for keybind combinations
Leader *string `json:"leader,omitempty"`
// MessagesFirst Navigate to first message
MessagesFirst *string `json:"messages_first,omitempty"`
// MessagesHalfPageDown Scroll messages down by half page
MessagesHalfPageDown *string `json:"messages_half_page_down,omitempty"` MessagesHalfPageDown *string `json:"messages_half_page_down,omitempty"`
MessagesHalfPageUp *string `json:"messages_half_page_up,omitempty"`
MessagesLast *string `json:"messages_last,omitempty"` // MessagesHalfPageUp Scroll messages up by half page
MessagesNext *string `json:"messages_next,omitempty"` MessagesHalfPageUp *string `json:"messages_half_page_up,omitempty"`
MessagesPageDown *string `json:"messages_page_down,omitempty"`
MessagesPageUp *string `json:"messages_page_up,omitempty"` // MessagesLast Navigate to last message
MessagesPrevious *string `json:"messages_previous,omitempty"` MessagesLast *string `json:"messages_last,omitempty"`
ModelList *string `json:"model_list,omitempty"`
ProjectInit *string `json:"project_init,omitempty"` // MessagesNext Navigate to next message
SessionCompact *string `json:"session_compact,omitempty"` MessagesNext *string `json:"messages_next,omitempty"`
SessionInterrupt *string `json:"session_interrupt,omitempty"`
SessionList *string `json:"session_list,omitempty"` // MessagesPageDown Scroll messages down by one page
SessionNew *string `json:"session_new,omitempty"` MessagesPageDown *string `json:"messages_page_down,omitempty"`
SessionShare *string `json:"session_share,omitempty"`
ThemeList *string `json:"theme_list,omitempty"` // MessagesPageUp Scroll messages up by one page
ToolDetails *string `json:"tool_details,omitempty"` MessagesPageUp *string `json:"messages_page_up,omitempty"`
// MessagesPrevious Navigate to previous message
MessagesPrevious *string `json:"messages_previous,omitempty"`
// ModelList List available models
ModelList *string `json:"model_list,omitempty"`
// ProjectInit Initialize project configuration
ProjectInit *string `json:"project_init,omitempty"`
// SessionCompact Toggle compact mode for session
SessionCompact *string `json:"session_compact,omitempty"`
// SessionInterrupt Interrupt current session
SessionInterrupt *string `json:"session_interrupt,omitempty"`
// SessionList List all sessions
SessionList *string `json:"session_list,omitempty"`
// SessionNew Create a new session
SessionNew *string `json:"session_new,omitempty"`
// SessionShare Share current session
SessionShare *string `json:"session_share,omitempty"`
// ThemeList List available themes
ThemeList *string `json:"theme_list,omitempty"`
// ToolDetails Show tool details
ToolDetails *string `json:"tool_details,omitempty"`
} }
// ConfigMcpLocal defines model for Config.McpLocal. // ConfigMcpLocal defines model for Config.McpLocal.
type ConfigMcpLocal struct { type ConfigMcpLocal struct {
Command []string `json:"command"` // Command Command and arguments to run the MCP server
Command []string `json:"command"`
// Environment Environment variables to set when running the MCP server
Environment *map[string]string `json:"environment,omitempty"` Environment *map[string]string `json:"environment,omitempty"`
Type string `json:"type"`
// Type Type of MCP server connection
Type string `json:"type"`
} }
// ConfigMcpRemote defines model for Config.McpRemote. // ConfigMcpRemote defines model for Config.McpRemote.
type ConfigMcpRemote struct { type ConfigMcpRemote struct {
// Type Type of MCP server connection
Type string `json:"type"` Type string `json:"type"`
Url string `json:"url"`
// Url URL of the remote MCP server
Url string `json:"url"`
} }
// Error defines model for Error. // Error defines model for Error.
@@ -222,47 +292,53 @@ type InstallationInfo struct {
// MessageInfo defines model for Message.Info. // MessageInfo defines model for Message.Info.
type MessageInfo struct { type MessageInfo struct {
Id string `json:"id"` Id string `json:"id"`
Metadata struct { Metadata MessageMetadata `json:"metadata"`
Assistant *struct { Parts []MessagePart `json:"parts"`
Cost float32 `json:"cost"` Role MessageInfoRole `json:"role"`
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"`
} }
// MessageInfo_Metadata_Error defines model for MessageInfo.Metadata.Error. // MessageInfoRole defines model for MessageInfo.Role.
type MessageInfo_Metadata_Error struct { 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 union json.RawMessage
} }
// MessageInfo_Metadata_Tool_AdditionalProperties defines model for MessageInfo.Metadata.Tool.AdditionalProperties. // MessageMetadata_Tool_AdditionalProperties defines model for Message.Metadata.tool.AdditionalProperties.
type MessageInfo_Metadata_Tool_AdditionalProperties struct { type MessageMetadata_Tool_AdditionalProperties struct {
Time struct { Time struct {
End float32 `json:"end"` End float32 `json:"end"`
Start float32 `json:"start"` Start float32 `json:"start"`
@@ -271,9 +347,6 @@ type MessageInfo_Metadata_Tool_AdditionalProperties struct {
AdditionalProperties map[string]interface{} `json:"-"` AdditionalProperties map[string]interface{} `json:"-"`
} }
// MessageInfoRole defines model for MessageInfo.Role.
type MessageInfoRole string
// MessagePart defines model for Message.Part. // MessagePart defines model for Message.Part.
type MessagePart struct { type MessagePart struct {
union json.RawMessage union json.RawMessage
@@ -367,9 +440,11 @@ type ModelInfo struct {
Context float32 `json:"context"` Context float32 `json:"context"`
Output float32 `json:"output"` Output float32 `json:"output"`
} `json:"limit"` } `json:"limit"`
Name string `json:"name"` Name string `json:"name"`
Reasoning bool `json:"reasoning"` Options map[string]interface{} `json:"options"`
Temperature bool `json:"temperature"` Reasoning bool `json:"reasoning"`
Temperature bool `json:"temperature"`
ToolCall bool `json:"tool_call"`
} }
// ProviderInfo defines model for Provider.Info. // ProviderInfo defines model for Provider.Info.
@@ -467,6 +542,11 @@ type PostSessionSummarizeJSONBody struct {
SessionID string `json:"sessionID"` 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. // PostFileSearchJSONRequestBody defines body for PostFileSearch for application/json ContentType.
type PostFileSearchJSONRequestBody PostFileSearchJSONBody type PostFileSearchJSONRequestBody PostFileSearchJSONBody
@@ -488,25 +568,28 @@ type PostSessionShareJSONRequestBody PostSessionShareJSONBody
// PostSessionSummarizeJSONRequestBody defines body for PostSessionSummarize for application/json ContentType. // PostSessionSummarizeJSONRequestBody defines body for PostSessionSummarize for application/json ContentType.
type PostSessionSummarizeJSONRequestBody PostSessionSummarizeJSONBody 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 // 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 { if a.AdditionalProperties != nil {
value, found = a.AdditionalProperties[fieldName] value, found = a.AdditionalProperties[fieldName]
} }
return return
} }
// Setter for additional properties for MessageInfo_Metadata_Tool_AdditionalProperties // Setter for additional properties for MessageMetadata_Tool_AdditionalProperties
func (a *MessageInfo_Metadata_Tool_AdditionalProperties) Set(fieldName string, value interface{}) { func (a *MessageMetadata_Tool_AdditionalProperties) Set(fieldName string, value interface{}) {
if a.AdditionalProperties == nil { if a.AdditionalProperties == nil {
a.AdditionalProperties = make(map[string]interface{}) a.AdditionalProperties = make(map[string]interface{})
} }
a.AdditionalProperties[fieldName] = value a.AdditionalProperties[fieldName] = value
} }
// Override default JSON handling for MessageInfo_Metadata_Tool_AdditionalProperties to handle AdditionalProperties // Override default JSON handling for MessageMetadata_Tool_AdditionalProperties to handle AdditionalProperties
func (a *MessageInfo_Metadata_Tool_AdditionalProperties) UnmarshalJSON(b []byte) error { func (a *MessageMetadata_Tool_AdditionalProperties) UnmarshalJSON(b []byte) error {
object := make(map[string]json.RawMessage) object := make(map[string]json.RawMessage)
err := json.Unmarshal(b, &object) err := json.Unmarshal(b, &object)
if err != nil { if err != nil {
@@ -543,8 +626,8 @@ func (a *MessageInfo_Metadata_Tool_AdditionalProperties) UnmarshalJSON(b []byte)
return nil return nil
} }
// Override default JSON handling for MessageInfo_Metadata_Tool_AdditionalProperties to handle AdditionalProperties // Override default JSON handling for MessageMetadata_Tool_AdditionalProperties to handle AdditionalProperties
func (a MessageInfo_Metadata_Tool_AdditionalProperties) MarshalJSON() ([]byte, error) { func (a MessageMetadata_Tool_AdditionalProperties) MarshalJSON() ([]byte, error) {
var err error var err error
object := make(map[string]json.RawMessage) object := make(map[string]json.RawMessage)
@@ -684,6 +767,34 @@ func (t *Event) MergeEventStorageWrite(v EventStorageWrite) error {
return err return err
} }
// AsEventInstallationUpdated returns the union data inside the Event as a EventInstallationUpdated
func (t Event) AsEventInstallationUpdated() (EventInstallationUpdated, error) {
var body EventInstallationUpdated
err := json.Unmarshal(t.union, &body)
return body, err
}
// FromEventInstallationUpdated overwrites any union data inside the Event as the provided EventInstallationUpdated
func (t *Event) FromEventInstallationUpdated(v EventInstallationUpdated) error {
v.Type = "installation.updated"
b, err := json.Marshal(v)
t.union = b
return err
}
// MergeEventInstallationUpdated performs a merge with any union data inside the Event, using the provided EventInstallationUpdated
func (t *Event) MergeEventInstallationUpdated(v EventInstallationUpdated) error {
v.Type = "installation.updated"
b, err := json.Marshal(v)
if err != nil {
return err
}
merged, err := runtime.JSONMerge(t.union, b)
t.union = merged
return err
}
// AsEventLspClientDiagnostics returns the union data inside the Event as a EventLspClientDiagnostics // AsEventLspClientDiagnostics returns the union data inside the Event as a EventLspClientDiagnostics
func (t Event) AsEventLspClientDiagnostics() (EventLspClientDiagnostics, error) { func (t Event) AsEventLspClientDiagnostics() (EventLspClientDiagnostics, error) {
var body EventLspClientDiagnostics var body EventLspClientDiagnostics
@@ -796,34 +907,6 @@ func (t *Event) MergeEventMessagePartUpdated(v EventMessagePartUpdated) error {
return err return err
} }
// AsEventInstallationUpdated returns the union data inside the Event as a EventInstallationUpdated
func (t Event) AsEventInstallationUpdated() (EventInstallationUpdated, error) {
var body EventInstallationUpdated
err := json.Unmarshal(t.union, &body)
return body, err
}
// FromEventInstallationUpdated overwrites any union data inside the Event as the provided EventInstallationUpdated
func (t *Event) FromEventInstallationUpdated(v EventInstallationUpdated) error {
v.Type = "installation.updated"
b, err := json.Marshal(v)
t.union = b
return err
}
// MergeEventInstallationUpdated performs a merge with any union data inside the Event, using the provided EventInstallationUpdated
func (t *Event) MergeEventInstallationUpdated(v EventInstallationUpdated) error {
v.Type = "installation.updated"
b, err := json.Marshal(v)
if err != nil {
return err
}
merged, err := runtime.JSONMerge(t.union, b)
t.union = merged
return err
}
// AsEventSessionUpdated returns the union data inside the Event as a EventSessionUpdated // AsEventSessionUpdated returns the union data inside the Event as a EventSessionUpdated
func (t Event) AsEventSessionUpdated() (EventSessionUpdated, error) { func (t Event) AsEventSessionUpdated() (EventSessionUpdated, error) {
var body EventSessionUpdated var body EventSessionUpdated
@@ -1014,23 +1097,23 @@ func (t *EventSessionError_Properties_Error) UnmarshalJSON(b []byte) error {
return err return err
} }
// AsProviderAuthError returns the union data inside the MessageInfo_Metadata_Error as a ProviderAuthError // AsProviderAuthError returns the union data inside the MessageMetadata_Error as a ProviderAuthError
func (t MessageInfo_Metadata_Error) AsProviderAuthError() (ProviderAuthError, error) { func (t MessageMetadata_Error) AsProviderAuthError() (ProviderAuthError, error) {
var body ProviderAuthError var body ProviderAuthError
err := json.Unmarshal(t.union, &body) err := json.Unmarshal(t.union, &body)
return body, err return body, err
} }
// FromProviderAuthError overwrites any union data inside the MessageInfo_Metadata_Error as the provided ProviderAuthError // FromProviderAuthError overwrites any union data inside the MessageMetadata_Error as the provided ProviderAuthError
func (t *MessageInfo_Metadata_Error) FromProviderAuthError(v ProviderAuthError) error { func (t *MessageMetadata_Error) FromProviderAuthError(v ProviderAuthError) error {
v.Name = "ProviderAuthError" v.Name = "ProviderAuthError"
b, err := json.Marshal(v) b, err := json.Marshal(v)
t.union = b t.union = b
return err return err
} }
// MergeProviderAuthError performs a merge with any union data inside the MessageInfo_Metadata_Error, using the provided ProviderAuthError // MergeProviderAuthError performs a merge with any union data inside the MessageMetadata_Error, using the provided ProviderAuthError
func (t *MessageInfo_Metadata_Error) MergeProviderAuthError(v ProviderAuthError) error { func (t *MessageMetadata_Error) MergeProviderAuthError(v ProviderAuthError) error {
v.Name = "ProviderAuthError" v.Name = "ProviderAuthError"
b, err := json.Marshal(v) b, err := json.Marshal(v)
if err != nil { if err != nil {
@@ -1042,23 +1125,23 @@ func (t *MessageInfo_Metadata_Error) MergeProviderAuthError(v ProviderAuthError)
return err return err
} }
// AsUnknownError returns the union data inside the MessageInfo_Metadata_Error as a UnknownError // AsUnknownError returns the union data inside the MessageMetadata_Error as a UnknownError
func (t MessageInfo_Metadata_Error) AsUnknownError() (UnknownError, error) { func (t MessageMetadata_Error) AsUnknownError() (UnknownError, error) {
var body UnknownError var body UnknownError
err := json.Unmarshal(t.union, &body) err := json.Unmarshal(t.union, &body)
return body, err return body, err
} }
// FromUnknownError overwrites any union data inside the MessageInfo_Metadata_Error as the provided UnknownError // FromUnknownError overwrites any union data inside the MessageMetadata_Error as the provided UnknownError
func (t *MessageInfo_Metadata_Error) FromUnknownError(v UnknownError) error { func (t *MessageMetadata_Error) FromUnknownError(v UnknownError) error {
v.Name = "UnknownError" v.Name = "UnknownError"
b, err := json.Marshal(v) b, err := json.Marshal(v)
t.union = b t.union = b
return err return err
} }
// MergeUnknownError performs a merge with any union data inside the MessageInfo_Metadata_Error, using the provided UnknownError // MergeUnknownError performs a merge with any union data inside the MessageMetadata_Error, using the provided UnknownError
func (t *MessageInfo_Metadata_Error) MergeUnknownError(v UnknownError) error { func (t *MessageMetadata_Error) MergeUnknownError(v UnknownError) error {
v.Name = "UnknownError" v.Name = "UnknownError"
b, err := json.Marshal(v) b, err := json.Marshal(v)
if err != nil { if err != nil {
@@ -1070,7 +1153,7 @@ func (t *MessageInfo_Metadata_Error) MergeUnknownError(v UnknownError) error {
return err return err
} }
func (t MessageInfo_Metadata_Error) Discriminator() (string, error) { func (t MessageMetadata_Error) Discriminator() (string, error) {
var discriminator struct { var discriminator struct {
Discriminator string `json:"name"` Discriminator string `json:"name"`
} }
@@ -1078,7 +1161,7 @@ func (t MessageInfo_Metadata_Error) Discriminator() (string, error) {
return discriminator.Discriminator, err return discriminator.Discriminator, err
} }
func (t MessageInfo_Metadata_Error) ValueByDiscriminator() (interface{}, error) { func (t MessageMetadata_Error) ValueByDiscriminator() (interface{}, error) {
discriminator, err := t.Discriminator() discriminator, err := t.Discriminator()
if err != nil { if err != nil {
return nil, err return nil, err
@@ -1093,12 +1176,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() b, err := t.union.MarshalJSON()
return b, err 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) err := t.union.UnmarshalJSON(b)
return err return err
} }
@@ -1565,6 +1648,11 @@ type ClientInterface interface {
PostSessionSummarizeWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) 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) 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) { func (c *Client) PostAppInfo(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) {
@@ -1843,6 +1931,30 @@ func (c *Client) PostSessionSummarize(ctx context.Context, body PostSessionSumma
return c.Client.Do(req) 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 // NewPostAppInfoRequest generates requests for PostAppInfo
func NewPostAppInfoRequest(server string) (*http.Request, error) { func NewPostAppInfoRequest(server string) (*http.Request, error) {
var err error var err error
@@ -2366,6 +2478,46 @@ func NewPostSessionSummarizeRequestWithBody(server string, contentType string, b
return req, nil 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 { func (c *Client) applyEditors(ctx context.Context, req *http.Request, additionalEditors []RequestEditorFn) error {
for _, r := range c.RequestEditors { for _, r := range c.RequestEditors {
if err := r(ctx, req); err != nil { if err := r(ctx, req); err != nil {
@@ -2470,6 +2622,11 @@ type ClientWithResponsesInterface interface {
PostSessionSummarizeWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostSessionSummarizeResponse, error) PostSessionSummarizeWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostSessionSummarizeResponse, error)
PostSessionSummarizeWithResponse(ctx context.Context, body PostSessionSummarizeJSONRequestBody, 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 { type PostAppInfoResponse struct {
@@ -2833,6 +2990,28 @@ func (r PostSessionSummarizeResponse) StatusCode() int {
return 0 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 // PostAppInfoWithResponse request returning *PostAppInfoResponse
func (c *ClientWithResponses) PostAppInfoWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*PostAppInfoResponse, error) { func (c *ClientWithResponses) PostAppInfoWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*PostAppInfoResponse, error) {
rsp, err := c.PostAppInfo(ctx, reqEditors...) rsp, err := c.PostAppInfo(ctx, reqEditors...)
@@ -3033,6 +3212,23 @@ func (c *ClientWithResponses) PostSessionSummarizeWithResponse(ctx context.Conte
return ParsePostSessionSummarizeResponse(rsp) 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 // ParsePostAppInfoResponse parses an HTTP response from a PostAppInfoWithResponse call
func ParsePostAppInfoResponse(rsp *http.Response) (*PostAppInfoResponse, error) { func ParsePostAppInfoResponse(rsp *http.Response) (*PostAppInfoResponse, error) {
bodyBytes, err := io.ReadAll(rsp.Body) bodyBytes, err := io.ReadAll(rsp.Body)
@@ -3463,3 +3659,29 @@ func ParsePostSessionSummarizeResponse(rsp *http.Response) (*PostSessionSummariz
return response, nil 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
}
+9 -11
View File
@@ -4,11 +4,15 @@ import starlight from "@astrojs/starlight"
import solidJs from "@astrojs/solid-js" import solidJs from "@astrojs/solid-js"
import cloudflare from "@astrojs/cloudflare" import cloudflare from "@astrojs/cloudflare"
import theme from "toolbeam-docs-theme" import theme from "toolbeam-docs-theme"
import config from "./config.mjs"
import { rehypeHeadingIds } from "@astrojs/markdown-remark" import { rehypeHeadingIds } from "@astrojs/markdown-remark"
import rehypeAutolinkHeadings from "rehype-autolink-headings" import rehypeAutolinkHeadings from "rehype-autolink-headings"
const discord = "https://discord.gg/sst"
const github = "https://github.com/sst/opencode" const github = "https://github.com/sst/opencode"
const headerLinks = [
{ name: "Docs", url: "/docs/" },
{ name: "GitHub", url: github },
]
// https://astro.build/config // https://astro.build/config
export default defineConfig({ export default defineConfig({
@@ -28,11 +32,10 @@ export default defineConfig({
integrations: [ integrations: [
solidJs(), solidJs(),
starlight({ starlight({
title: "OpenCode", title: "opencode",
expressiveCode: { themes: ["github-light", "github-dark"] }, expressiveCode: { themes: ["github-light", "github-dark"] },
social: [ social: [
{ icon: "discord", label: "Discord", href: discord }, { icon: "github", label: "GitHub", href: config.github },
{ icon: "github", label: "GitHub", href: github },
], ],
editLink: { editLink: {
baseUrl: `${github}/edit/master/www/`, baseUrl: `${github}/edit/master/www/`,
@@ -52,8 +55,7 @@ export default defineConfig({
"docs/config", "docs/config",
"docs/models", "docs/models",
"docs/themes", "docs/themes",
"docs/shortcuts", "docs/keybinds",
"docs/lsp-servers",
"docs/mcp-servers", "docs/mcp-servers",
], ],
components: { components: {
@@ -62,11 +64,7 @@ export default defineConfig({
}, },
plugins: [ plugins: [
theme({ theme({
// Optionally, add your own header links headerLinks: config.headerLinks,
headerLinks: [
{ name: "Home", url: "/" },
{ name: "Docs", url: "/docs/" },
],
}), }),
], ],
}), }),
+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", "astro": "5.7.13",
"diff": "8.0.2", "diff": "8.0.2",
"js-base64": "3.7.7", "js-base64": "3.7.7",
"lang-map": "0.4.0",
"luxon": "3.6.1", "luxon": "3.6.1",
"marked": "15.0.12", "marked": "15.0.12",
"rehype-autolink-headings": "7.1.0", "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"
}
]
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 439 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 438 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 432 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 KiB

+83 -11
View File
@@ -1,29 +1,50 @@
--- ---
import config from 'virtual:starlight/user-config'; import config from '../../config.mjs';
import astroConfig from 'virtual:starlight/user-config';
import { Icon } from '@astrojs/starlight/components'; import { Icon } from '@astrojs/starlight/components';
import { HeaderLinks } from 'toolbeam-docs-theme/components'; import { HeaderLinks } from 'toolbeam-docs-theme/components';
import Default from 'toolbeam-docs-theme/overrides/Header.astro'; import Default from 'toolbeam-docs-theme/overrides/Header.astro';
import SocialIcons from 'virtual:starlight/components/SocialIcons';
import SiteTitle from '@astrojs/starlight/components/SiteTitle.astro'; import SiteTitle from '@astrojs/starlight/components/SiteTitle.astro';
const path = Astro.url.pathname; const path = Astro.url.pathname;
const links = config.social || []; const links = astroConfig.social || [];
const headerLinks = config.headerLinks;
--- ---
{ path.startsWith("/s") { path.startsWith("/s")
? <div class="header sl-flex"> ? <div class="header sl-flex">
<div class="title-wrapper sl-flex"> <div class="title-wrapper sl-flex">
<SiteTitle {...Astro.props} /> <SiteTitle {...Astro.props} />
</div> </div>
<div class="middle-group sl-flex"> <div class="middle-group sl-flex">
<HeaderLinks {...Astro.props} /> {
</div> headerLinks?.map(({ name, url }) => (
</div> <a class="links" href={url}>{name}</a>
))
}
</div>
<div class="sl-hidden md:sl-flex right-group">
{
links.length > 0 && (
<div class="sl-flex social-icons">
{links.map(({ href, icon }) => (
<a {href} rel="me" target="_blank">
<Icon name={icon} size="1rem" />
</a>
))}
</div>
)
}
</div>
</div>
: <Default {...Astro.props}><slot /></Default> : <Default {...Astro.props}><slot /></Default>
} }
<style> <style>
.header { .header {
gap: var(--sl-nav-gap);
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
height: 100%; height: 100%;
@@ -53,4 +74,55 @@ const links = config.social || [];
display: flex; display: flex;
} }
} }
.right-group,
.social-icons {
gap: 1rem;
align-items: center;
a {
line-height: 1;
svg {
color: var(--sl-color-text-dimmed);
}
}
a.links {
text-transform: uppercase;
font-size: var(--sl-text-sm);
color: var(--sl-color-text-secondary);
line-height: normal;
}
}
@media (min-width: 50rem) {
:global(:root[data-has-sidebar]) {
--__sidebar-pad: calc(2 * var(--sl-nav-pad-x));
}
:global(:root:not([data-has-toc])) {
--__toc-width: 0rem;
}
.header {
--__sidebar-width: max(0rem, var(--sl-content-inline-start, 0rem) - var(--sl-nav-pad-x));
--__main-column-fr: calc(
(
100% + var(--__sidebar-pad, 0rem) - var(--__toc-width, var(--sl-sidebar-width)) -
(2 * var(--__toc-width, var(--sl-nav-pad-x))) - var(--sl-content-inline-start, 0rem) -
var(--sl-content-width)
) / 2
);
display: grid;
grid-template-columns:
/* 1 (site title): runs up until the main content columns left edge or the width of the title, whichever is the largest */
minmax(
calc(var(--__sidebar-width) + max(0rem, var(--__main-column-fr) - var(--sl-nav-gap))),
auto
)
/* 2 (search box): all free space that is available. */
1fr
/* 3 (right items): use the space that these need. */
auto;
align-content: center;
}
}
</style> </style>
+98 -20
View File
@@ -5,6 +5,7 @@ import type { Props } from '@astrojs/starlight/props';
import CopyIcon from "../assets/lander/copy.svg"; import CopyIcon from "../assets/lander/copy.svg";
import CheckIcon from "../assets/lander/check.svg"; import CheckIcon from "../assets/lander/check.svg";
import Screenshot from "../assets/themes/tokyonight.png";
const { data } = Astro.locals.starlightRoute.entry; const { data } = Astro.locals.starlightRoute.entry;
const { title = data.title, tagline, image, actions = [] } = data.hero || {}; const { title = data.title, tagline, image, actions = [] } = data.hero || {};
@@ -19,7 +20,7 @@ const imageAttrs = {
const github = config.social.filter(s => s.icon === 'github')[0]; const github = config.social.filter(s => s.icon === 'github')[0];
const command = "npm i -g"; const command = "npm i -g";
const pkg = "opencode"; const pkg = "opencode-ai";
let darkImage: ImageMetadata | undefined; let darkImage: ImageMetadata | undefined;
let lightImage: ImageMetadata | undefined; let lightImage: ImageMetadata | undefined;
@@ -68,15 +69,22 @@ if (image) {
<section class="content"> <section class="content">
<ul> <ul>
<li><b>Native TUI</b>: A native terminal UI for a smoother, snappier experience.</li> <li><b>Native TUI</b>: A responsive, native, themeable terminal UI.</li>
<li><b>LSP enabled</b>: Loads the right LSPs for your codebase. Helps the LLM make fewer mistakes.</li> <li><b>LSP enabled</b>: Automatically loads the right LSPs for the LLM.</li>
<li><b>Multi-session</b>: Start multiple conversations in a project to have agents working in parallel.</li> <li><b>Multi-session</b>: Start multiple agents in parallel on the same project.</li>
<li><b>Use any model</b>: Supports all the models from OpenAI, Anthropic, Google, OpenRouter, and more.</li> <li><b>Shareable links</b>: Share a link to any sessions for reference or to debug.</li>
<li><b>Change tracking</b>: View the file changes from the current conversation in the sidebar.</li> <li><b>Claude Pro</b>: Log in with Anthropic to use your Claude Pro or Max account.</li>
<li><b>Edit with Vim</b>: Use Vim as an external editor to compose longer messages.</li> <li><b>Use any model</b>: Supports 75+ LLM providers through <a href="https://models.dev">Models.dev</a>, including local models.</li>
</ul> </ul>
</section> </section>
<section class="images">
<div>
<p>opencode TUI with the tokyonight theme</p>
<Image width={600} src={Screenshot} alt="opencode TUI with the tokyonight theme" />
</div>
</section>
<section class="footer"> <section class="footer">
<div class="col1"> <div class="col1">
<span>Version: Beta</span> <span>Version: Beta</span>
@@ -94,7 +102,7 @@ if (image) {
--heading-font-size: var(--sl-text-3xl); --heading-font-size: var(--sl-text-3xl);
margin: 1rem; margin: 1rem;
border: 2px solid var(--sl-color-white); border: 2px solid var(--sl-color-border);
} }
@media (max-width: 30rem) { @media (max-width: 30rem) {
.hero { .hero {
@@ -126,26 +134,41 @@ section.cta {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: space-between; justify-content: space-between;
border-top: 2px solid var(--sl-color-white); border-top: 2px solid var(--sl-color-border);
@media (max-width: 40rem) {
flex-direction: column;
}
& > div { & > div {
flex: 1; flex: 1;
line-height: 1.4; line-height: 1.4;
padding: calc(var(--padding) / 2) 0.5rem; padding: calc(var(--padding) / 2) 0.5rem;
@media (max-width: 40rem) {
padding-bottom: calc(var(--padding) / 2 + 4px);
}
a {
font-size: 1rem;
}
}
& > div.col2 {
@media (max-width: 50rem) {
flex: 0 0 auto;
}
} }
& > div:not(.col2) { & > div:not(.col2) {
text-align: center; text-align: center;
text-transform: uppercase; text-transform: uppercase;
} }
@media (max-width: 30rem) {
& > div {
padding-bottom: calc(var(--padding) / 2 + 4px);
}
}
& > div + div { & > div + div {
border-left: 2px solid var(--sl-color-white); border-left: 2px solid var(--sl-color-border);
@media (max-width: 40rem) {
border-left: none;
border-top: 2px solid var(--sl-color-border);
}
} }
.command { .command {
@@ -195,7 +218,7 @@ section.cta {
} }
section.content { section.content {
border-top: 2px solid var(--sl-color-white); border-top: 2px solid var(--sl-color-border);
padding: var(--padding); padding: var(--padding);
ul { ul {
@@ -211,8 +234,63 @@ section.content {
} }
} }
section.images {
display: flex;
flex-direction: row;
align-items: stretch;
justify-content: space-between;
border-top: 2px solid var(--sl-color-border);
& > div {
flex: 1;
display: flex;
flex-direction: column;
gap: calc(var(--padding) / 4);
padding: calc(var(--padding) / 2);
border-width: 0;
border-style: solid;
border-color: var(--sl-color-border);
& > div, p {
flex: 1;
display: flex;
align-items: center;
}
}
p {
letter-spacing: -0.03125rem;
text-transform: uppercase;
color: var(--sl-color-text-dimmed);
@media (max-width: 30rem) {
font-size: 0.75rem;
}
}
img {
align-self: center;
width: 100%;
max-width: 600px;
height: auto;
}
& > div + div {
border-width: 0 0 0 2px;
}
@media (max-width: 30rem) {
& {
flex-direction: column;
}
& > div + div {
border-width: 2px 0 0 0;
}
}
}
section.approach { section.approach {
border-top: 2px solid var(--sl-color-white); border-top: 2px solid var(--sl-color-border);
padding: var(--padding); padding: var(--padding);
p + p { p + p {
@@ -221,7 +299,7 @@ section.approach {
} }
section.footer { section.footer {
border-top: 2px solid var(--sl-color-white); border-top: 2px solid var(--sl-color-border);
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@@ -233,7 +311,7 @@ section.footer {
} }
& > div + div { & > div + div {
border-left: 2px solid var(--sl-color-white); border-left: 2px solid var(--sl-color-border);
} }
} }
</style> </style>

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