Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
230d0a1510 | ||
|
|
46ff2c0ae0 | ||
|
|
b8a89dab0f | ||
|
|
7351e12886 | ||
|
|
38879dee2d | ||
|
|
c4ff8dd205 | ||
|
|
0e035b3115 | ||
|
|
b855511d9a | ||
|
|
783faf554d | ||
|
|
bfd4269d7d | ||
|
|
25f78b053b | ||
|
|
87f260ee17 | ||
|
|
12931a869d | ||
|
|
f759e1804d | ||
|
|
c9b4564d36 | ||
|
|
d097c546db | ||
|
|
adb54521b4 | ||
|
|
2ea0399aa7 | ||
|
|
fa1266263d | ||
|
|
fe109c921e | ||
|
|
37bb8895fe | ||
|
|
89b95be4de | ||
|
|
eaf295bac7 | ||
|
|
27d3cec477 | ||
|
|
574d494c3c | ||
|
|
0239761f31 | ||
|
|
a53f9165e9 | ||
|
|
ffc231bd8b | ||
|
|
3cf4ef56fb | ||
|
|
c738e26438 | ||
|
|
9c6aa82ac1 | ||
|
|
ef74d97491 | ||
|
|
af892e5432 | ||
|
|
d7aca6230d | ||
|
|
0f9c2c5c27 | ||
|
|
6a261dedb4 | ||
|
|
ec928d88b5 | ||
|
|
59a5f120c0 | ||
|
|
ce07f80b19 | ||
|
|
168fd9b2e3 |
@@ -1,37 +0,0 @@
|
||||
name: build
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- run: git fetch --force --tags
|
||||
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ">=1.23.2"
|
||||
cache: true
|
||||
cache-dependency-path: go.sum
|
||||
|
||||
- run: go mod download
|
||||
|
||||
- uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
distribution: goreleaser
|
||||
version: latest
|
||||
args: build --snapshot --clean
|
||||
@@ -4,7 +4,7 @@ on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- dontlook
|
||||
- dev
|
||||
tags:
|
||||
- "*"
|
||||
|
||||
|
||||
@@ -3,3 +3,5 @@ node_modules
|
||||
.opencode
|
||||
.sst
|
||||
.env
|
||||
.idea
|
||||
.vscode
|
||||
|
||||
@@ -1,46 +1,140 @@
|
||||
```
|
||||
█▀▀█ █▀▀█ █▀▀ █▀▀▄ █▀▀ █▀▀█ █▀▀▄ █▀▀
|
||||
█░░█ █░░█ █▀▀ █░░█ █░░ █░░█ █░░█ █▀▀
|
||||
▀▀▀▀ █▀▀▀ ▀▀▀ ▀ ▀ ▀▀▀ ▀▀▀▀ ▀▀▀ ▀▀▀
|
||||
```
|
||||
|
||||
[](https://github.com/sst/opencode)
|
||||
|
||||
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
|
||||
⚠️ **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 📚
|
||||
|
||||
## Installation
|
||||
### Installation
|
||||
|
||||
If you have a previous version of opencode < 0.1.x installed you might have to remove it first.
|
||||
|
||||
### Curl
|
||||
|
||||
```
|
||||
```bash
|
||||
# YOLO
|
||||
curl -fsSL https://opencode.ai/install | bash
|
||||
|
||||
# Package managers
|
||||
npm i -g opencode-ai@latest # or bun/pnpm/yarn
|
||||
brew install sst/tap/opencode # macOS
|
||||
paru -S opencode-bin # Arch Linux
|
||||
```
|
||||
|
||||
### NPM
|
||||
> **Note:** Remove previous versions < 0.1.x first if installed
|
||||
|
||||
```
|
||||
npm i -g opencode-ai@latest
|
||||
bun i -g opencode-ai@latest
|
||||
pnpm i -g opencode-ai@latest
|
||||
yarn global add opencode-ai@latest
|
||||
### Providers
|
||||
|
||||
The recommended approach is to sign up for claude pro or max and do `opencode auth login` and select Anthropic. It is the most cost effective way to use this tool.
|
||||
|
||||
Additionally 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
|
||||
│ ...
|
||||
└
|
||||
```
|
||||
|
||||
### Brew
|
||||
The models.dev dataset is also used to detect common environment variables like `OPENAI_API_KEY` to autoload that provider.
|
||||
|
||||
```
|
||||
brew install sst/tap/opencode
|
||||
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
|
||||
|
||||
### Project Config
|
||||
|
||||
Project configuration is optional. You can place an `opencode.json` file in the root of your repo and it will be loaded.
|
||||
|
||||
```json title="opencode.json"
|
||||
{
|
||||
"$schema": "http://opencode.ai/config.json"
|
||||
}
|
||||
```
|
||||
|
||||
### AUR
|
||||
#### MCP
|
||||
|
||||
```
|
||||
paru -S opencode-bin
|
||||
```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"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Usage
|
||||
#### Providers
|
||||
|
||||
We are working on proper keybinds - right now it's the various function keys press F1 to see them
|
||||
You can use opencode with any provider listed at [here](https://ai-sdk.dev/providers/ai-sdk-providers). Use the npm package name as the key in your config. Note we use v5 of the ai-sdk and not all providers support that yet.
|
||||
|
||||
```json title="opencode.json"
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"provider": {
|
||||
"@ai-sdk/openai-compatible": {
|
||||
"name": "ollama",
|
||||
"options": {
|
||||
"baseURL": "http://localhost:11434/v1"
|
||||
},
|
||||
"models": {
|
||||
"llama2": {
|
||||
"name": "llama2"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Contributing
|
||||
|
||||
To run opencode locally you need
|
||||
|
||||
- bun
|
||||
- golang 1.24.x
|
||||
|
||||
To run
|
||||
|
||||
```
|
||||
$ bun install
|
||||
$ cd packages/opencode
|
||||
$ bun run src/index.ts
|
||||
```
|
||||
|
||||
### FAQ
|
||||
|
||||
#### How do I use this with OpenRouter
|
||||
|
||||
OpenRouter is not yet in the models.dev database but you can configure it manually.
|
||||
|
||||
```json title="opencode.json"
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"provider": {
|
||||
"@openrouter/ai-sdk-provider": {
|
||||
"name": "OpenRouter",
|
||||
"options": {
|
||||
"apiKey": "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
},
|
||||
"models": {
|
||||
"anthropic/claude-3.5-sonnet": {
|
||||
"name": "Claude 3.5 Sonnet"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
"@types/turndown": "5.0.5",
|
||||
"@types/yargs": "17.0.33",
|
||||
"typescript": "catalog:",
|
||||
"zod-to-json-schema": "3.24.5",
|
||||
},
|
||||
},
|
||||
"packages/web": {
|
||||
@@ -90,16 +91,18 @@
|
||||
},
|
||||
"catalog": {
|
||||
"@types/node": "22.13.9",
|
||||
"ai": "5.0.0-alpha.7",
|
||||
"ai": "4.3.16",
|
||||
"typescript": "5.8.2",
|
||||
"zod": "3.24.2",
|
||||
},
|
||||
"packages": {
|
||||
"@ai-sdk/gateway": ["@ai-sdk/gateway@1.0.0-alpha.7", "", { "dependencies": { "@ai-sdk/provider": "2.0.0-alpha.7", "@ai-sdk/provider-utils": "3.0.0-alpha.7" }, "peerDependencies": { "zod": "^3.24.0" } }, "sha512-gz1V165eiJnQIexfLyKm11vimrmQ3zdcJhPpjeLFmDU9wrvZwLuklfZ0WgfYSb+EjiP1cKypwt6JSGvWkfKIAQ=="],
|
||||
"@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@2.0.0-alpha.7", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-lhdrARU3SSmt5p/GNNK7VhazvZpKSCIOjpHUfX7f5jIhVGi/vvlxP1rD6Go57nn1MtuGKNqL04AebSRFDQsQbw=="],
|
||||
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@2.2.8", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "nanoid": "^3.3.8", "secure-json-parse": "^2.7.0" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA=="],
|
||||
|
||||
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.0-alpha.7", "", { "dependencies": { "@ai-sdk/provider": "2.0.0-alpha.7", "@standard-schema/spec": "^1.0.0", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-AYkT3jskmo7Lwzijo/yHKD1jC+UZizsROO8ULTg9aJZUwR4ABZzAxh4NxDIEy4TWRfBGufp+/9ICHAn6pkU71w=="],
|
||||
"@ai-sdk/react": ["@ai-sdk/react@1.2.12", "", { "dependencies": { "@ai-sdk/provider-utils": "2.2.8", "@ai-sdk/ui-utils": "1.2.11", "swr": "^2.2.5", "throttleit": "2.1.0" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "zod": "^3.23.8" }, "optionalPeers": ["zod"] }, "sha512-jK1IZZ22evPZoQW3vlkZ7wvjYGYF+tRBKXtrcolduIkQ/m/sOAVcVeVDUDvh1T91xCnWCdUGCPZg2avZ90mv3g=="],
|
||||
|
||||
"@ai-sdk/ui-utils": ["@ai-sdk/ui-utils@1.2.11", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-3zcwCc8ezzFlwp3ZD15wAPjf2Au4s3vAbKsXQVyhxODHcmu0iyPO2Eua6D/vicq/AUm/BAo60r97O6HU+EI0+w=="],
|
||||
|
||||
"@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
|
||||
|
||||
@@ -429,6 +432,8 @@
|
||||
|
||||
"@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
|
||||
|
||||
"@types/diff-match-patch": ["@types/diff-match-patch@1.0.36", "", {}, "sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="],
|
||||
|
||||
"@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="],
|
||||
@@ -473,7 +478,7 @@
|
||||
|
||||
"acorn-walk": ["acorn-walk@8.3.2", "", {}, "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A=="],
|
||||
|
||||
"ai": ["ai@5.0.0-alpha.7", "", { "dependencies": { "@ai-sdk/gateway": "1.0.0-alpha.7", "@ai-sdk/provider": "2.0.0-alpha.7", "@ai-sdk/provider-utils": "3.0.0-alpha.7", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-ShCk3frIMdVtK9knvWKiFS7N6Vwnf8mLMv670+T//W9oqfoetSVPBhTF6Dy+oDM/bjVSsBf1BuYImLDvHICOIQ=="],
|
||||
"ai": ["ai@4.3.16", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8", "@ai-sdk/react": "1.2.12", "@ai-sdk/ui-utils": "1.2.11", "@opentelemetry/api": "1.9.0", "jsondiffpatch": "0.6.0" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "zod": "^3.23.8" }, "optionalPeers": ["react"] }, "sha512-KUDwlThJ5tr2Vw0A1ZkbDKNME3wzWhuVfAOwIvFUzl1TPVDFAXDFTXio3p+jaKneB+dKNCvFFlolYmmgHttG1g=="],
|
||||
|
||||
"ansi-align": ["ansi-align@3.0.1", "", { "dependencies": { "string-width": "^4.1.0" } }, "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w=="],
|
||||
|
||||
@@ -685,6 +690,8 @@
|
||||
|
||||
"diff": ["diff@8.0.2", "", {}, "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg=="],
|
||||
|
||||
"diff-match-patch": ["diff-match-patch@1.0.5", "", {}, "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw=="],
|
||||
|
||||
"direction": ["direction@2.0.1", "", { "bin": { "direction": "cli.js" } }, "sha512-9S6m9Sukh1cZNknO1CWAr2QAWsbKLafQiyM5gZ7VgXHeuaoUwffKN4q6NC4A/Mf9iiPlOXQEKW/Mv/mh9/3YFA=="],
|
||||
|
||||
"dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="],
|
||||
@@ -969,6 +976,8 @@
|
||||
|
||||
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
|
||||
|
||||
"jsondiffpatch": ["jsondiffpatch@0.6.0", "", { "dependencies": { "@types/diff-match-patch": "^1.0.36", "chalk": "^5.3.0", "diff-match-patch": "^1.0.5" }, "bin": { "jsondiffpatch": "bin/jsondiffpatch.js" } }, "sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ=="],
|
||||
|
||||
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
|
||||
|
||||
"klona": ["klona@2.0.6", "", {}, "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA=="],
|
||||
@@ -1273,6 +1282,8 @@
|
||||
|
||||
"rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="],
|
||||
|
||||
"react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="],
|
||||
|
||||
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
|
||||
|
||||
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
||||
@@ -1353,6 +1364,8 @@
|
||||
|
||||
"sax": ["sax@1.2.1", "", {}, "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA=="],
|
||||
|
||||
"secure-json-parse": ["secure-json-parse@2.7.0", "", {}, "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw=="],
|
||||
|
||||
"semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
||||
|
||||
"send": ["send@1.2.0", "", { "dependencies": { "debug": "^4.3.5", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.0", "mime-types": "^3.0.1", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.1" } }, "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw=="],
|
||||
@@ -1453,6 +1466,8 @@
|
||||
|
||||
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||
|
||||
"swr": ["swr@2.3.3", "", { "dependencies": { "dequal": "^2.0.3", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-dshNvs3ExOqtZ6kJBaAsabhPdHyeY4P2cKwRCniDVifBMoG/SVI7tfLWqPXriVspf2Rg4tPzXJTnwaihIeFw2A=="],
|
||||
|
||||
"tar-fs": ["tar-fs@3.0.9", "", { "dependencies": { "pump": "^3.0.0", "tar-stream": "^3.1.5" }, "optionalDependencies": { "bare-fs": "^4.0.1", "bare-path": "^3.0.0" } }, "sha512-XF4w9Xp+ZQgifKakjZYmFdkLoSWd34VGKcsTCwlNWM7QG3ZbaxnTsaBwnjFZqHRf/rROxaR8rXnbtwdvaDI+lA=="],
|
||||
|
||||
"tar-stream": ["tar-stream@3.1.7", "", { "dependencies": { "b4a": "^1.6.4", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ=="],
|
||||
@@ -1461,6 +1476,8 @@
|
||||
|
||||
"thread-stream": ["thread-stream@0.15.2", "", { "dependencies": { "real-require": "^0.1.0" } }, "sha512-UkEhKIg2pD+fjkHQKyJO3yoIvAP3N6RlNFt2dUhcS1FGvCD1cQa1M/PGknCLFIyZdtJOWQjejp7bdNqmN7zwdA=="],
|
||||
|
||||
"throttleit": ["throttleit@2.1.0", "", {}, "sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw=="],
|
||||
|
||||
"tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="],
|
||||
|
||||
"tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="],
|
||||
@@ -1545,6 +1562,8 @@
|
||||
|
||||
"url": ["url@0.10.3", "", { "dependencies": { "punycode": "1.3.2", "querystring": "0.2.0" } }, "sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ=="],
|
||||
|
||||
"use-sync-external-store": ["use-sync-external-store@1.5.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A=="],
|
||||
|
||||
"util": ["util@0.12.5", "", { "dependencies": { "inherits": "^2.0.3", "is-arguments": "^1.0.4", "is-generator-function": "^1.0.7", "is-typed-array": "^1.1.3", "which-typed-array": "^1.1.2" } }, "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA=="],
|
||||
|
||||
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
||||
|
||||
@@ -107,7 +107,9 @@ add_to_path() {
|
||||
local config_file=$1
|
||||
local command=$2
|
||||
|
||||
if [[ -w $config_file ]]; then
|
||||
if grep -Fxq "$command" "$config_file"; then
|
||||
print_message info "Command already exists in $config_file, skipping write."
|
||||
elif [[ -w $config_file ]]; then
|
||||
echo -e "\n# opencode" >> "$config_file"
|
||||
echo "$command" >> "$config_file"
|
||||
print_message info "Successfully added ${ORANGE}opencode ${GREEN}to \$PATH in $config_file"
|
||||
@@ -173,6 +175,7 @@ if [[ ":$PATH:" != *":$INSTALL_DIR:"* ]]; then
|
||||
add_to_path "$config_file" "export PATH=$INSTALL_DIR:\$PATH"
|
||||
;;
|
||||
*)
|
||||
export PATH=$INSTALL_DIR:$PATH
|
||||
print_message warning "Manually add the directory to $config_file (or similar):"
|
||||
print_message info " export PATH=$INSTALL_DIR:\$PATH"
|
||||
;;
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"provider": {
|
||||
"@ai-sdk/openai-compatible": {
|
||||
"name": "ollama",
|
||||
"options": {
|
||||
"baseURL": "http://localhost:11434/v1"
|
||||
},
|
||||
"models": {
|
||||
"qwen3": {},
|
||||
"deepseek-r1": {},
|
||||
"llama2": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-3
@@ -5,8 +5,7 @@
|
||||
"type": "module",
|
||||
"packageManager": "bun@1.2.14",
|
||||
"scripts": {
|
||||
"typecheck": "bun run --filter='*' typecheck",
|
||||
"dev": "sst dev"
|
||||
"typecheck": "bun run --filter='*' typecheck"
|
||||
},
|
||||
"workspaces": {
|
||||
"packages": [
|
||||
@@ -16,7 +15,7 @@
|
||||
"typescript": "5.8.2",
|
||||
"@types/node": "22.13.9",
|
||||
"zod": "3.24.2",
|
||||
"ai": "5.0.0-alpha.7"
|
||||
"ai": "4.3.16"
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# OpenCode Agent Guidelines
|
||||
# opencode agent guidelines
|
||||
|
||||
## Build/Test Commands
|
||||
|
||||
@@ -16,9 +16,19 @@
|
||||
- **Naming**: camelCase for variables/functions, PascalCase for classes/namespaces
|
||||
- **Error handling**: Use Result patterns, avoid throwing exceptions in tools
|
||||
- **File structure**: Namespace-based organization (e.g., `Tool.define()`, `Session.create()`)
|
||||
|
||||
## IMPORTANT
|
||||
|
||||
- Try to keep things in one function unless composable or reusablte
|
||||
- DO NOT do unnecessary destructuring of variables
|
||||
- DO NOT use else statements unless necessary
|
||||
- DO NOT use try catch if it can be avoided
|
||||
- AVOID try catch where possible
|
||||
- AVOID else statements
|
||||
- AVOID using `any` type
|
||||
- AVOID let statements
|
||||
- PREFER single word variable names where possible
|
||||
- Use as many bun apis as possible like Bun.file()
|
||||
|
||||
## Architecture
|
||||
|
||||
@@ -27,4 +37,3 @@
|
||||
- **Validation**: All inputs validated with Zod schemas
|
||||
- **Logging**: Use `Log.create({ service: "name" })` pattern
|
||||
- **Storage**: Use `Storage` namespace for persistence
|
||||
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"$schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"provider": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"env": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"models": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"attachment": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"reasoning": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"temperature": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"cost": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"input": {
|
||||
"type": "number"
|
||||
},
|
||||
"output": {
|
||||
"type": "number"
|
||||
},
|
||||
"inputCached": {
|
||||
"type": "number"
|
||||
},
|
||||
"outputCached": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"input",
|
||||
"output",
|
||||
"inputCached",
|
||||
"outputCached"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"limit": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"context": {
|
||||
"type": "number"
|
||||
},
|
||||
"output": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"context",
|
||||
"output"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"type": "object",
|
||||
"additionalProperties": {}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"models"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"mcp": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"const": "local"
|
||||
},
|
||||
"command": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"environment": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type",
|
||||
"command"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"const": "remote"
|
||||
},
|
||||
"url": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type",
|
||||
"url"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"$schema": "http://json-schema.org/draft-07/schema#"
|
||||
}
|
||||
@@ -5,7 +5,8 @@
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit"
|
||||
"typecheck": "tsc --noEmit",
|
||||
"dev": "bun run ./src/index.ts"
|
||||
},
|
||||
"exports": {
|
||||
"./*": [
|
||||
@@ -18,7 +19,8 @@
|
||||
"@types/bun": "latest",
|
||||
"@types/turndown": "5.0.5",
|
||||
"@types/yargs": "17.0.33",
|
||||
"typescript": "catalog:"
|
||||
"typescript": "catalog:",
|
||||
"zod-to-json-schema": "3.24.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@clack/prompts": "0.11.0",
|
||||
|
||||
@@ -64,7 +64,7 @@ for (const [os, arch] of targets) {
|
||||
|
||||
await $`mkdir -p ./dist/${pkg.name}`
|
||||
await $`cp -r ./bin ./dist/${pkg.name}/bin`
|
||||
await $`cp ./script/postinstall.js ./dist/${pkg.name}/postinstall.js`
|
||||
await $`cp ./script/postinstall.mjs ./dist/${pkg.name}/postinstall.mjs`
|
||||
await Bun.file(`./dist/${pkg.name}/package.json`).write(
|
||||
JSON.stringify(
|
||||
{
|
||||
@@ -73,7 +73,7 @@ await Bun.file(`./dist/${pkg.name}/package.json`).write(
|
||||
[pkg.name]: `./bin/${pkg.name}`,
|
||||
},
|
||||
scripts: {
|
||||
postinstall: "node ./postinstall.js",
|
||||
postinstall: "node ./postinstall.mjs",
|
||||
},
|
||||
version,
|
||||
optionalDependencies,
|
||||
@@ -108,9 +108,10 @@ if (!snapshot) {
|
||||
.filter((x: string) => {
|
||||
const lower = x.toLowerCase()
|
||||
return (
|
||||
!lower.includes("chore:") &&
|
||||
!lower.includes("ignore:") &&
|
||||
!lower.includes("ci:") &&
|
||||
!lower.includes("docs:")
|
||||
!lower.includes("docs:") &&
|
||||
!lower.includes("doc:")
|
||||
)
|
||||
})
|
||||
.join("\n")
|
||||
|
||||
Executable
+8
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import "zod-openapi/extend"
|
||||
import { Config } from "../src/config/config"
|
||||
import { zodToJsonSchema } from "zod-to-json-schema"
|
||||
|
||||
const result = zodToJsonSchema(Config.Info)
|
||||
await Bun.write("config.schema.json", JSON.stringify(result, null, 2))
|
||||
@@ -44,15 +44,24 @@ export namespace BunProc {
|
||||
}),
|
||||
)
|
||||
export async function install(pkg: string, version = "latest") {
|
||||
const dir = path.join(Global.Path.cache, `node_modules`, pkg)
|
||||
if (!(await Bun.file(path.join(dir, "package.json")).exists())) {
|
||||
log.info("installing", { pkg })
|
||||
await BunProc.run(["add", `${pkg}@${version}`], {
|
||||
cwd: Global.Path.cache,
|
||||
}).catch(() => {
|
||||
throw new InstallFailedError({ pkg, version })
|
||||
})
|
||||
}
|
||||
return dir
|
||||
const mod = path.join(Global.Path.cache, "node_modules", pkg)
|
||||
const pkgjson = Bun.file(path.join(Global.Path.cache, "package.json"))
|
||||
const parsed = await pkgjson.json().catch(() => ({
|
||||
dependencies: {},
|
||||
}))
|
||||
if (parsed.dependencies[pkg] === version) return mod
|
||||
parsed.dependencies[pkg] = version
|
||||
await Bun.write(pkgjson, JSON.stringify(parsed, null, 2))
|
||||
await BunProc.run(["install"], {
|
||||
cwd: Global.Path.cache,
|
||||
}).catch((e) => {
|
||||
new InstallFailedError(
|
||||
{ pkg, version },
|
||||
{
|
||||
cause: e,
|
||||
},
|
||||
)
|
||||
})
|
||||
return mod
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,8 +111,12 @@ export const AuthLoginCommand = cmd({
|
||||
// some weird bug where program exits without this
|
||||
await new Promise((resolve) => setTimeout(resolve, 10))
|
||||
const { url, verifier } = await AuthAnthropic.authorize()
|
||||
prompts.note("Opening browser...")
|
||||
await open(url)
|
||||
prompts.note("Trying to open browser...")
|
||||
try {
|
||||
await open(url)
|
||||
} catch (e) {
|
||||
prompts.log.error("Failed to open browser perhaps you are running without a display or X server, please open the following URL in your browser:")
|
||||
}
|
||||
prompts.log.info(url)
|
||||
|
||||
const code = await prompts.text({
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Server } from "../../server/server"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import type { CommandModule } from "yargs"
|
||||
import { Config } from "../../config/config"
|
||||
|
||||
export const GenerateCommand = {
|
||||
command: "generate",
|
||||
|
||||
@@ -115,6 +115,9 @@ export const RunCommand = {
|
||||
})
|
||||
|
||||
const { providerID, modelID } = await Provider.defaultModel()
|
||||
setTimeout(() => {
|
||||
Session.abort(session.id)
|
||||
}, 8000)
|
||||
await Session.chat({
|
||||
sessionID: session.id,
|
||||
providerID,
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
import type { Argv } from "yargs"
|
||||
import { UI } from "../ui"
|
||||
import { VERSION } from "../version"
|
||||
import path from "path"
|
||||
import fs from "fs/promises"
|
||||
import os from "os"
|
||||
import * as prompts from "@clack/prompts"
|
||||
import { Global } from "../../global"
|
||||
|
||||
const API = "https://api.github.com/repos/sst/opencode"
|
||||
|
||||
interface Release {
|
||||
tag_name: string
|
||||
name: string
|
||||
assets: Array<{
|
||||
name: string
|
||||
browser_download_url: string
|
||||
}>
|
||||
}
|
||||
|
||||
function asset(): string {
|
||||
const platform = os.platform()
|
||||
const arch = os.arch()
|
||||
|
||||
if (platform === "darwin") {
|
||||
return arch === "arm64"
|
||||
? "opencode-darwin-arm64.zip"
|
||||
: "opencode-darwin-x64.zip"
|
||||
}
|
||||
if (platform === "linux") {
|
||||
return arch === "arm64"
|
||||
? "opencode-linux-arm64.zip"
|
||||
: "opencode-linux-x64.zip"
|
||||
}
|
||||
if (platform === "win32") {
|
||||
return "opencode-windows-x64.zip"
|
||||
}
|
||||
|
||||
throw new Error(`Unsupported platform: ${platform}-${arch}`)
|
||||
}
|
||||
|
||||
function compare(current: string, latest: string): number {
|
||||
const a = current.replace(/^v/, "")
|
||||
const b = latest.replace(/^v/, "")
|
||||
|
||||
const aParts = a.split(".").map(Number)
|
||||
const bParts = b.split(".").map(Number)
|
||||
|
||||
for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) {
|
||||
const aPart = aParts[i] || 0
|
||||
const bPart = bParts[i] || 0
|
||||
|
||||
if (aPart < bPart) return -1
|
||||
if (aPart > bPart) return 1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
async function latest(): Promise<Release> {
|
||||
const response = await fetch(`${API}/releases/latest`)
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch latest release: ${response.statusText}`)
|
||||
}
|
||||
return response.json()
|
||||
}
|
||||
|
||||
async function specific(version: string): Promise<Release> {
|
||||
const tag = version.startsWith("v") ? version : `v${version}`
|
||||
const response = await fetch(`${API}/releases/tags/${tag}`)
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch release ${tag}: ${response.statusText}`)
|
||||
}
|
||||
return response.json()
|
||||
}
|
||||
|
||||
async function download(url: string): Promise<string> {
|
||||
const response = await fetch(url)
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to download: ${response.statusText}`)
|
||||
}
|
||||
|
||||
const buffer = await response.arrayBuffer()
|
||||
const temp = path.join(Global.Path.cache, `opencode-update-${Date.now()}.zip`)
|
||||
|
||||
await Bun.write(temp, buffer)
|
||||
|
||||
const extractDir = path.join(
|
||||
Global.Path.cache,
|
||||
`opencode-extract-${Date.now()}`,
|
||||
)
|
||||
await fs.mkdir(extractDir, { recursive: true })
|
||||
|
||||
const proc = Bun.spawn(["unzip", "-o", temp, "-d", extractDir], {
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
})
|
||||
|
||||
const result = await proc.exited
|
||||
if (result !== 0) {
|
||||
throw new Error("Failed to extract update")
|
||||
}
|
||||
|
||||
await fs.unlink(temp)
|
||||
|
||||
const binary = path.join(extractDir, "opencode")
|
||||
await fs.chmod(binary, 0o755)
|
||||
|
||||
return binary
|
||||
}
|
||||
|
||||
export const UpgradeCommand = {
|
||||
command: "upgrade [target]",
|
||||
describe: "Upgrade opencode to the latest version or a specific version",
|
||||
builder: (yargs: Argv) => {
|
||||
return yargs.positional("target", {
|
||||
describe: "Specific version to upgrade to (e.g., '0.1.48' or 'v0.1.48')",
|
||||
type: "string",
|
||||
})
|
||||
},
|
||||
handler: async (args: { target?: string }) => {
|
||||
UI.empty()
|
||||
UI.println(UI.logo(" "))
|
||||
UI.empty()
|
||||
prompts.intro("Upgrade")
|
||||
|
||||
if (!process.execPath.includes(path.join(".opencode", "bin")) && false) {
|
||||
prompts.log.error(
|
||||
`opencode is installed to ${process.execPath} and seems to be managed by a package manager`,
|
||||
)
|
||||
prompts.outro("Done")
|
||||
return
|
||||
}
|
||||
|
||||
const release = args.target
|
||||
? await specific(args.target).catch(() => {})
|
||||
: await latest().catch(() => {})
|
||||
if (!release) {
|
||||
prompts.log.error("Failed to fetch release information")
|
||||
prompts.outro("Done")
|
||||
return
|
||||
}
|
||||
|
||||
const target = release.tag_name
|
||||
|
||||
if (VERSION !== "dev" && compare(VERSION, target) >= 0) {
|
||||
prompts.log.success(`Already up to date`)
|
||||
prompts.outro("Done")
|
||||
return
|
||||
}
|
||||
|
||||
prompts.log.info(`From ${VERSION} → ${target}`)
|
||||
|
||||
const name = asset()
|
||||
const found = release.assets.find((a) => a.name === name)
|
||||
|
||||
if (!found) {
|
||||
prompts.log.error(`No binary found for platform: ${name}`)
|
||||
prompts.outro("Done")
|
||||
return
|
||||
}
|
||||
|
||||
const spinner = prompts.spinner()
|
||||
spinner.start("Downloading update...")
|
||||
|
||||
const downloadPath = await download(found.browser_download_url).catch(
|
||||
() => {},
|
||||
)
|
||||
if (!downloadPath) {
|
||||
spinner.stop("Download failed")
|
||||
prompts.log.error("Download failed")
|
||||
prompts.outro("Done")
|
||||
return
|
||||
}
|
||||
|
||||
spinner.stop("Download complete")
|
||||
|
||||
const renamed = await fs
|
||||
.rename(downloadPath, process.execPath)
|
||||
.catch(() => {})
|
||||
|
||||
if (renamed === undefined) {
|
||||
prompts.log.error("Install failed")
|
||||
await fs.unlink(downloadPath).catch(() => {})
|
||||
prompts.outro("Done")
|
||||
return
|
||||
}
|
||||
|
||||
prompts.log.success(`Successfully upgraded to ${target}`)
|
||||
prompts.outro("Done")
|
||||
},
|
||||
}
|
||||
@@ -1,193 +0,0 @@
|
||||
import { createCli, type TrpcCliMeta } from "trpc-cli"
|
||||
import { initTRPC } from "@trpc/server"
|
||||
import { z } from "zod"
|
||||
import { Server } from "../server/server"
|
||||
import { AuthAnthropic } from "../auth/anthropic"
|
||||
import { UI } from "./ui"
|
||||
import { App } from "../app/app"
|
||||
import { Bus } from "../bus"
|
||||
import { Provider } from "../provider/provider"
|
||||
import { Session } from "../session"
|
||||
import { Share } from "../share/share"
|
||||
import { Message } from "../session/message"
|
||||
import { VERSION } from "./version"
|
||||
import { LSP } from "../lsp"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
|
||||
const t = initTRPC.meta<TrpcCliMeta>().create()
|
||||
|
||||
export const router = t.router({
|
||||
generate: t.procedure
|
||||
.meta({
|
||||
description: "Generate OpenAPI and event specs",
|
||||
})
|
||||
.input(z.object({}))
|
||||
.mutation(async () => {
|
||||
const specs = await Server.openapi()
|
||||
const dir = "gen"
|
||||
await fs.rmdir(dir, { recursive: true }).catch(() => {})
|
||||
await fs.mkdir(dir, { recursive: true })
|
||||
await Bun.write(
|
||||
path.join(dir, "openapi.json"),
|
||||
JSON.stringify(specs, null, 2),
|
||||
)
|
||||
return "Generated OpenAPI specs in gen/ directory"
|
||||
}),
|
||||
|
||||
run: t.procedure
|
||||
.meta({
|
||||
description: "Run OpenCode with a message",
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
message: z.array(z.string()).default([]).describe("Message to send"),
|
||||
session: z.string().optional().describe("Session ID to continue"),
|
||||
}),
|
||||
)
|
||||
.mutation(
|
||||
async ({ input }: { input: { message: string[]; session?: string } }) => {
|
||||
const message = input.message.join(" ")
|
||||
await App.provide(
|
||||
{
|
||||
cwd: process.cwd(),
|
||||
version: "0.0.0",
|
||||
},
|
||||
async () => {
|
||||
await Share.init()
|
||||
const session = input.session
|
||||
? await Session.get(input.session)
|
||||
: await Session.create()
|
||||
|
||||
UI.println(UI.Style.TEXT_HIGHLIGHT_BOLD + "◍ OpenCode", VERSION)
|
||||
UI.empty()
|
||||
UI.println(UI.Style.TEXT_NORMAL_BOLD + "> ", message)
|
||||
UI.empty()
|
||||
UI.println(
|
||||
UI.Style.TEXT_INFO_BOLD +
|
||||
"~ https://dev.opencode.ai/s?id=" +
|
||||
session.id.slice(-8),
|
||||
)
|
||||
UI.empty()
|
||||
|
||||
function printEvent(color: string, type: string, title: string) {
|
||||
UI.println(
|
||||
color + `|`,
|
||||
UI.Style.TEXT_NORMAL +
|
||||
UI.Style.TEXT_DIM +
|
||||
` ${type.padEnd(7, " ")}`,
|
||||
"",
|
||||
UI.Style.TEXT_NORMAL + title,
|
||||
)
|
||||
}
|
||||
|
||||
Bus.subscribe(Message.Event.PartUpdated, async (message) => {
|
||||
const part = message.properties.part
|
||||
if (
|
||||
part.type === "tool-invocation" &&
|
||||
part.toolInvocation.state === "result"
|
||||
) {
|
||||
if (part.toolInvocation.toolName === "opencode_todowrite")
|
||||
return
|
||||
|
||||
const args = part.toolInvocation.args as any
|
||||
const tool = part.toolInvocation.toolName
|
||||
|
||||
if (tool === "opencode_edit")
|
||||
printEvent(UI.Style.TEXT_SUCCESS_BOLD, "Edit", args.filePath)
|
||||
if (tool === "opencode_bash")
|
||||
printEvent(
|
||||
UI.Style.TEXT_WARNING_BOLD,
|
||||
"Execute",
|
||||
args.command,
|
||||
)
|
||||
if (tool === "opencode_read")
|
||||
printEvent(UI.Style.TEXT_INFO_BOLD, "Read", args.filePath)
|
||||
if (tool === "opencode_write")
|
||||
printEvent(
|
||||
UI.Style.TEXT_SUCCESS_BOLD,
|
||||
"Create",
|
||||
args.filePath,
|
||||
)
|
||||
if (tool === "opencode_list")
|
||||
printEvent(UI.Style.TEXT_INFO_BOLD, "List", args.path)
|
||||
if (tool === "opencode_glob")
|
||||
printEvent(
|
||||
UI.Style.TEXT_INFO_BOLD,
|
||||
"Glob",
|
||||
args.pattern + (args.path ? " in " + args.path : ""),
|
||||
)
|
||||
}
|
||||
|
||||
if (part.type === "text") {
|
||||
if (part.text.includes("\n")) {
|
||||
UI.empty()
|
||||
UI.println(part.text)
|
||||
UI.empty()
|
||||
return
|
||||
}
|
||||
printEvent(UI.Style.TEXT_NORMAL_BOLD, "Text", part.text)
|
||||
}
|
||||
})
|
||||
|
||||
const { providerID, modelID } = await Provider.defaultModel()
|
||||
await Session.chat({
|
||||
sessionID: session.id,
|
||||
providerID,
|
||||
modelID,
|
||||
parts: [
|
||||
{
|
||||
type: "text",
|
||||
text: message,
|
||||
},
|
||||
],
|
||||
})
|
||||
UI.empty()
|
||||
},
|
||||
)
|
||||
return "Session completed"
|
||||
},
|
||||
),
|
||||
|
||||
scrap: t.procedure
|
||||
.meta({
|
||||
description: "Test command for scraping files",
|
||||
})
|
||||
.input(
|
||||
z.object({
|
||||
file: z.string().describe("File to process"),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input }: { input: { file: string } }) => {
|
||||
await App.provide({ cwd: process.cwd(), version: VERSION }, async () => {
|
||||
await LSP.touchFile(input.file, true)
|
||||
await LSP.diagnostics()
|
||||
})
|
||||
return `Processed file: ${input.file}`
|
||||
}),
|
||||
|
||||
login: t.router({
|
||||
anthropic: t.procedure
|
||||
.meta({
|
||||
description: "Login to Anthropic",
|
||||
})
|
||||
.input(z.object({}))
|
||||
.mutation(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)
|
||||
return "Successfully logged in to Anthropic"
|
||||
}),
|
||||
}),
|
||||
})
|
||||
|
||||
export function createOpenCodeCli() {
|
||||
return createCli({ router })
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@ export namespace Config {
|
||||
|
||||
export const Info = z
|
||||
.object({
|
||||
$schema: z.string().optional(),
|
||||
provider: z
|
||||
.record(
|
||||
ModelsDev.Provider.partial().extend({
|
||||
@@ -58,11 +59,6 @@ export namespace Config {
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
tool: z
|
||||
.object({
|
||||
provider: z.record(z.string(), z.string().array()).optional(),
|
||||
})
|
||||
.optional(),
|
||||
mcp: z.record(z.string(), Mcp).optional(),
|
||||
})
|
||||
.strict()
|
||||
|
||||
+11
-11
@@ -1,4 +1,3 @@
|
||||
import { App } from "../app/app"
|
||||
import path from "path"
|
||||
import { Global } from "../global"
|
||||
import fs from "fs/promises"
|
||||
@@ -6,6 +5,7 @@ import { z } from "zod"
|
||||
import { NamedError } from "../util/error"
|
||||
import { lazy } from "../util/lazy"
|
||||
import { Log } from "../util/log"
|
||||
import { $ } from "bun"
|
||||
|
||||
export namespace Fzf {
|
||||
const log = Log.create({ service: "fzf" })
|
||||
@@ -117,18 +117,18 @@ export namespace Fzf {
|
||||
}
|
||||
|
||||
export async function search(cwd: string, query: string) {
|
||||
const process = Bun.spawn({
|
||||
cwd,
|
||||
stdin: "inherit",
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
cmd: [await filepath(), "--filter", query],
|
||||
})
|
||||
await process.exited
|
||||
const stdout = await Bun.readableStreamToText(process.stdout)
|
||||
return stdout
|
||||
const results = await $`${await filepath()} --filter ${query}`
|
||||
.quiet()
|
||||
.throws(false)
|
||||
.cwd(cwd)
|
||||
.text()
|
||||
const split = results
|
||||
.trim()
|
||||
.split("\n")
|
||||
.filter((line) => line.length > 0)
|
||||
log.info("results", {
|
||||
count: split.length,
|
||||
})
|
||||
return split
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
export namespace Flag {
|
||||
export const OPENCODE_AUTO_SHARE = truthy("OPENCODE_AUTO_SHARE")
|
||||
|
||||
function truthy(key: string) {
|
||||
const value = process.env[key]?.toLowerCase()
|
||||
return value === "true" || value === "1"
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,8 @@ import { App } from "./app/app"
|
||||
import { Server } from "./server/server"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
|
||||
import { Share } from "./share/share"
|
||||
|
||||
import { Global } from "./global"
|
||||
|
||||
import yargs from "yargs"
|
||||
import { hideBin } from "yargs/helpers"
|
||||
import { RunCommand } from "./cli/cmd/run"
|
||||
@@ -16,6 +13,7 @@ import { VERSION } from "./cli/version"
|
||||
import { ScrapCommand } from "./cli/cmd/scrap"
|
||||
import { Log } from "./util/log"
|
||||
import { AuthCommand, AuthLoginCommand } from "./cli/cmd/auth"
|
||||
import { UpgradeCommand } from "./cli/cmd/upgrade"
|
||||
import { Provider } from "./provider/provider"
|
||||
import { UI } from "./cli/ui"
|
||||
|
||||
@@ -36,7 +34,7 @@ const cli = yargs(hideBin(process.argv))
|
||||
.usage("\n" + UI.logo())
|
||||
.command({
|
||||
command: "$0 [project]",
|
||||
describe: "Start OpenCode TUI",
|
||||
describe: "Start opencode TUI",
|
||||
builder: (yargs) =>
|
||||
yargs.positional("project", {
|
||||
type: "string",
|
||||
@@ -72,7 +70,7 @@ const cli = yargs(hideBin(process.argv))
|
||||
cmd = [binary]
|
||||
}
|
||||
const proc = Bun.spawn({
|
||||
cmd,
|
||||
cmd: [...cmd, ...process.argv.slice(2)],
|
||||
cwd,
|
||||
stdout: "inherit",
|
||||
stderr: "inherit",
|
||||
@@ -105,6 +103,7 @@ const cli = yargs(hideBin(process.argv))
|
||||
.command(GenerateCommand)
|
||||
.command(ScrapCommand)
|
||||
.command(AuthCommand)
|
||||
.command(UpgradeCommand)
|
||||
.fail((msg, err) => {
|
||||
if (
|
||||
msg.startsWith("Unknown argument") ||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Global } from "../global"
|
||||
import { lazy } from "../util/lazy"
|
||||
import { Log } from "../util/log"
|
||||
import path from "path"
|
||||
import { z } from "zod"
|
||||
@@ -62,4 +63,30 @@ export namespace ModelsDev {
|
||||
throw new Error(`Failed to fetch models.dev: ${result.statusText}`)
|
||||
await Bun.write(file, result)
|
||||
}
|
||||
|
||||
const aisdk = lazy(async () => {
|
||||
log.info("fetching ai-sdk")
|
||||
const response = await fetch(
|
||||
"https://registry.npmjs.org/-/v1/search?text=scope:@ai-sdk",
|
||||
)
|
||||
if (!response.ok)
|
||||
throw new Error(
|
||||
`Failed to fetch ai-sdk information: ${response.statusText}`,
|
||||
)
|
||||
const result = await response.json()
|
||||
log.info("found ai-sdk", result.objects.length)
|
||||
return result.objects
|
||||
.filter((obj: any) => obj.package.name.startsWith("@ai-sdk/"))
|
||||
.reduce((acc: any, obj: any) => {
|
||||
acc[obj.package.name] = obj
|
||||
return acc
|
||||
}, {})
|
||||
})
|
||||
|
||||
export async function pkg(providerID: string): Promise<[string, string]> {
|
||||
const packages = await aisdk()
|
||||
const match = packages[`@ai-sdk/${providerID}`]
|
||||
if (match) return [match.package.name, "latest"]
|
||||
return [providerID, "latest"]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,9 +186,8 @@ export namespace Provider {
|
||||
const s = await state()
|
||||
const existing = s.sdk.get(providerID)
|
||||
if (existing) return existing
|
||||
const mod = await import(
|
||||
await BunProc.install(`@ai-sdk/${providerID}`, "alpha")
|
||||
)
|
||||
const [pkg, version] = await ModelsDev.pkg(providerID)
|
||||
const mod = await import(await BunProc.install(pkg, version))
|
||||
const fn = mod[Object.keys(mod).find((key) => key.startsWith("create"))!]
|
||||
const loaded = fn(s.providers[providerID]?.options)
|
||||
s.sdk.set(providerID, loaded)
|
||||
@@ -289,11 +288,13 @@ export namespace Provider {
|
||||
google: TOOLS,
|
||||
}
|
||||
export async function tools(providerID: string) {
|
||||
/*
|
||||
const cfg = await Config.get()
|
||||
if (cfg.tool?.provider?.[providerID])
|
||||
return cfg.tool.provider[providerID].map(
|
||||
(id) => TOOLS.find((t) => t.id === id)!,
|
||||
)
|
||||
*/
|
||||
return TOOL_MAPPING[providerID] ?? TOOLS
|
||||
}
|
||||
|
||||
|
||||
@@ -56,12 +56,16 @@ export namespace Server {
|
||||
},
|
||||
)
|
||||
})
|
||||
.use((c, next) => {
|
||||
.use(async (c, next) => {
|
||||
log.info("request", {
|
||||
method: c.req.method,
|
||||
path: c.req.path,
|
||||
})
|
||||
return next()
|
||||
const start = Date.now()
|
||||
await next()
|
||||
log.info("response", {
|
||||
duration: Date.now() - start,
|
||||
})
|
||||
})
|
||||
.get(
|
||||
"/openapi",
|
||||
|
||||
@@ -4,14 +4,14 @@ import { Identifier } from "../id/id"
|
||||
import { Storage } from "../storage/storage"
|
||||
import { Log } from "../util/log"
|
||||
import {
|
||||
convertToModelMessages,
|
||||
generateText,
|
||||
LoadAPIKeyError,
|
||||
stepCountIs,
|
||||
convertToCoreMessages,
|
||||
streamText,
|
||||
tool,
|
||||
type Tool as AITool,
|
||||
type LanguageModelUsage,
|
||||
type CoreMessage,
|
||||
type UIMessage,
|
||||
} from "ai"
|
||||
import { z, ZodSchema } from "zod"
|
||||
@@ -27,6 +27,8 @@ import { MCP } from "../mcp"
|
||||
import { NamedError } from "../util/error"
|
||||
import type { Tool } from "../tool/tool"
|
||||
import { SystemPrompt } from "./system"
|
||||
import { Flag } from "../flag/flag"
|
||||
import type { ModelsDev } from "../provider/models"
|
||||
|
||||
export namespace Session {
|
||||
const log = Log.create({ service: "session" })
|
||||
@@ -92,6 +94,12 @@ export namespace Session {
|
||||
log.info("created", result)
|
||||
state().sessions.set(result.id, result)
|
||||
await Storage.writeJSON("session/info/" + result.id, result)
|
||||
if (!result.parentID && Flag.OPENCODE_AUTO_SHARE)
|
||||
share(result.id).then((share) => {
|
||||
update(result.id, (draft) => {
|
||||
draft.share = share
|
||||
})
|
||||
})
|
||||
Bus.publish(Event.Updated, {
|
||||
info: result,
|
||||
})
|
||||
@@ -220,34 +228,31 @@ export namespace Session {
|
||||
const session = await get(input.sessionID)
|
||||
if (msgs.length === 0 && !session.parentID) {
|
||||
generateText({
|
||||
maxOutputTokens: 20,
|
||||
messages: convertToModelMessages([
|
||||
maxTokens: input.providerID === "google" ? 1024 : 20,
|
||||
messages: [
|
||||
...SystemPrompt.title(input.providerID).map(
|
||||
(x): UIMessage => ({
|
||||
id: Identifier.ascending("message"),
|
||||
(x): CoreMessage => ({
|
||||
role: "system",
|
||||
parts: [
|
||||
{
|
||||
type: "text",
|
||||
text: x,
|
||||
},
|
||||
],
|
||||
content: x,
|
||||
}),
|
||||
),
|
||||
{
|
||||
role: "user",
|
||||
parts: input.parts,
|
||||
},
|
||||
]),
|
||||
temperature: 0,
|
||||
...convertToCoreMessages([
|
||||
{
|
||||
role: "user",
|
||||
content: "",
|
||||
parts: toParts(input.parts),
|
||||
},
|
||||
]),
|
||||
],
|
||||
model: model.language,
|
||||
})
|
||||
.then((result) => {
|
||||
return Session.update(input.sessionID, (draft) => {
|
||||
draft.title = result.text
|
||||
})
|
||||
if (result.text)
|
||||
return Session.update(input.sessionID, (draft) => {
|
||||
draft.title = result.text
|
||||
})
|
||||
})
|
||||
.catch(() => {})
|
||||
.catch((e) => {})
|
||||
}
|
||||
const msg: Message.Info = {
|
||||
role: "user",
|
||||
@@ -393,18 +398,88 @@ export namespace Session {
|
||||
}
|
||||
text = undefined
|
||||
},
|
||||
async onChunk(input) {
|
||||
const value = input.chunk
|
||||
async onFinish(input) {
|
||||
const assistant = next.metadata!.assistant!
|
||||
const usage = getUsage(input.usage, model.info)
|
||||
assistant.cost = usage.cost
|
||||
await updateMessage(next)
|
||||
},
|
||||
onError(err) {
|
||||
log.error("callback error", err)
|
||||
switch (true) {
|
||||
case LoadAPIKeyError.isInstance(err.error):
|
||||
next.metadata.error = new Provider.AuthError(
|
||||
{
|
||||
providerID: input.providerID,
|
||||
message: err.error.message,
|
||||
},
|
||||
{ cause: err.error },
|
||||
).toObject()
|
||||
break
|
||||
case err.error instanceof Error:
|
||||
next.metadata.error = new NamedError.Unknown(
|
||||
{ message: err.error.toString() },
|
||||
{ cause: err.error },
|
||||
).toObject()
|
||||
break
|
||||
default:
|
||||
next.metadata.error = new NamedError.Unknown(
|
||||
{ message: JSON.stringify(err.error) },
|
||||
{ cause: err.error },
|
||||
)
|
||||
}
|
||||
Bus.publish(Event.Error, {
|
||||
error: next.metadata.error,
|
||||
})
|
||||
},
|
||||
// async prepareStep(step) {
|
||||
// next.parts.push({
|
||||
// type: "step-start",
|
||||
// })
|
||||
// await updateMessage(next)
|
||||
// return step
|
||||
// },
|
||||
toolCallStreaming: true,
|
||||
abortSignal: abort.signal,
|
||||
maxSteps: 1000,
|
||||
messages: [
|
||||
...system.map(
|
||||
(x): CoreMessage => ({
|
||||
role: "system",
|
||||
content: x,
|
||||
}),
|
||||
),
|
||||
...convertToCoreMessages(
|
||||
msgs.map(toUIMessage).filter((x) => x.parts.length > 0),
|
||||
),
|
||||
],
|
||||
temperature: model.info.id === "codex-mini-latest" ? undefined : 0,
|
||||
tools: {
|
||||
...(await MCP.tools()),
|
||||
...tools,
|
||||
},
|
||||
model: model.language,
|
||||
})
|
||||
try {
|
||||
for await (const value of result.fullStream) {
|
||||
l.info("part", {
|
||||
type: value.type,
|
||||
})
|
||||
switch (value.type) {
|
||||
case "text":
|
||||
case "step-start":
|
||||
next.parts.push({
|
||||
type: "step-start",
|
||||
})
|
||||
break
|
||||
case "text-delta":
|
||||
if (!text) {
|
||||
text = value
|
||||
next.parts.push(value)
|
||||
text = {
|
||||
type: "text",
|
||||
text: value.textDelta,
|
||||
}
|
||||
next.parts.push(text)
|
||||
break
|
||||
} else text.text += value.text
|
||||
} else text.text += value.textDelta
|
||||
break
|
||||
|
||||
case "tool-call": {
|
||||
@@ -445,18 +520,25 @@ export namespace Session {
|
||||
case "tool-call-delta":
|
||||
break
|
||||
|
||||
// for some reason ai sdk claims to not send this part but it does
|
||||
// @ts-expect-error
|
||||
case "tool-result":
|
||||
const match = next.parts.find(
|
||||
(p) =>
|
||||
p.type === "tool-invocation" &&
|
||||
// @ts-expect-error
|
||||
p.toolInvocation.toolCallId === value.toolCallId,
|
||||
)
|
||||
if (match && match.type === "tool-invocation") {
|
||||
match.toolInvocation = {
|
||||
// @ts-expect-error
|
||||
args: value.args,
|
||||
// @ts-expect-error
|
||||
toolCallId: value.toolCallId,
|
||||
// @ts-expect-error
|
||||
toolName: value.toolName,
|
||||
state: "result",
|
||||
// @ts-expect-error
|
||||
result: value.result as string,
|
||||
}
|
||||
Bus.publish(Message.Event.PartUpdated, {
|
||||
@@ -473,80 +555,37 @@ export namespace Session {
|
||||
})
|
||||
}
|
||||
await updateMessage(next)
|
||||
},
|
||||
async onFinish(input) {
|
||||
const assistant = next.metadata!.assistant!
|
||||
const usage = getUsage(input.totalUsage, model.info)
|
||||
assistant.cost = usage.cost
|
||||
await updateMessage(next)
|
||||
},
|
||||
onError(err) {
|
||||
log.error("error", err)
|
||||
switch (true) {
|
||||
case LoadAPIKeyError.isInstance(err.error):
|
||||
next.metadata.error = new Provider.AuthError(
|
||||
{
|
||||
providerID: input.providerID,
|
||||
message: err.error.message,
|
||||
},
|
||||
{ cause: err.error },
|
||||
).toObject()
|
||||
break
|
||||
case err.error instanceof Error:
|
||||
next.metadata.error = new NamedError.Unknown(
|
||||
{ message: err.error.toString() },
|
||||
{ cause: err.error },
|
||||
).toObject()
|
||||
break
|
||||
default:
|
||||
next.metadata.error = new NamedError.Unknown(
|
||||
{ message: JSON.stringify(err.error) },
|
||||
{ cause: err.error },
|
||||
)
|
||||
}
|
||||
Bus.publish(Event.Error, {
|
||||
error: next.metadata.error,
|
||||
})
|
||||
},
|
||||
async prepareStep(step) {
|
||||
next.parts.push({
|
||||
type: "step-start",
|
||||
})
|
||||
await updateMessage(next)
|
||||
return step
|
||||
},
|
||||
toolCallStreaming: true,
|
||||
abortSignal: abort.signal,
|
||||
stopWhen: stepCountIs(1000),
|
||||
messages: convertToModelMessages([
|
||||
...system.map(
|
||||
(x): UIMessage => ({
|
||||
id: Identifier.ascending("message"),
|
||||
role: "system",
|
||||
parts: [
|
||||
{
|
||||
type: "text",
|
||||
text: x,
|
||||
},
|
||||
],
|
||||
}),
|
||||
),
|
||||
...msgs,
|
||||
]),
|
||||
temperature: model.info.id === "codex-mini-latest" ? undefined : 0,
|
||||
tools: {
|
||||
...(await MCP.tools()),
|
||||
...tools,
|
||||
},
|
||||
model: model.language,
|
||||
})
|
||||
await result.consumeStream({
|
||||
onError: (err) => {
|
||||
log.error("stream error", {
|
||||
err,
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
} catch (e: any) {
|
||||
log.error("stream error", {
|
||||
error: e,
|
||||
})
|
||||
switch (true) {
|
||||
case LoadAPIKeyError.isInstance(e):
|
||||
next.metadata.error = new Provider.AuthError(
|
||||
{
|
||||
providerID: input.providerID,
|
||||
message: e.message,
|
||||
},
|
||||
{ cause: e },
|
||||
).toObject()
|
||||
break
|
||||
case e instanceof Error:
|
||||
next.metadata.error = new NamedError.Unknown(
|
||||
{ message: e.toString() },
|
||||
{ cause: e },
|
||||
).toObject()
|
||||
break
|
||||
default:
|
||||
next.metadata.error = new NamedError.Unknown(
|
||||
{ message: JSON.stringify(e) },
|
||||
{ cause: e },
|
||||
)
|
||||
}
|
||||
Bus.publish(Event.Error, {
|
||||
error: next.metadata.error,
|
||||
})
|
||||
}
|
||||
next.metadata!.time.completed = Date.now()
|
||||
for (const part of next.parts) {
|
||||
if (
|
||||
@@ -611,30 +650,24 @@ export namespace Session {
|
||||
const result = await generateText({
|
||||
abortSignal: abort.signal,
|
||||
model: model.language,
|
||||
messages: convertToModelMessages([
|
||||
messages: [
|
||||
...system.map(
|
||||
(x): UIMessage => ({
|
||||
id: Identifier.ascending("message"),
|
||||
(x): CoreMessage => ({
|
||||
role: "system",
|
||||
parts: [
|
||||
{
|
||||
type: "text",
|
||||
text: x,
|
||||
},
|
||||
],
|
||||
content: x,
|
||||
}),
|
||||
),
|
||||
...filtered,
|
||||
...convertToCoreMessages(filtered.map(toUIMessage)),
|
||||
{
|
||||
role: "user",
|
||||
parts: [
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Provide a detailed but concise summary of our conversation above. Focus on information that would be helpful for continuing the conversation, including what we did, what we're doing, which files we're working on, and what we're going to do next.",
|
||||
},
|
||||
],
|
||||
},
|
||||
]),
|
||||
],
|
||||
})
|
||||
next.parts.push({
|
||||
type: "text",
|
||||
@@ -662,11 +695,11 @@ export namespace Session {
|
||||
}
|
||||
}
|
||||
|
||||
function getUsage(usage: LanguageModelUsage, model: Provider.Model) {
|
||||
function getUsage(usage: LanguageModelUsage, model: ModelsDev.Model) {
|
||||
const tokens = {
|
||||
input: usage.inputTokens ?? 0,
|
||||
output: usage.outputTokens ?? 0,
|
||||
reasoning: usage.reasoningTokens ?? 0,
|
||||
input: usage.promptTokens ?? 0,
|
||||
output: usage.completionTokens ?? 0,
|
||||
reasoning: 0,
|
||||
}
|
||||
return {
|
||||
cost: new Decimal(0)
|
||||
@@ -703,3 +736,57 @@ export namespace Session {
|
||||
await App.initialize()
|
||||
}
|
||||
}
|
||||
|
||||
function toUIMessage(msg: Message.Info): UIMessage {
|
||||
if (msg.role === "assistant") {
|
||||
return {
|
||||
id: msg.id,
|
||||
role: "assistant",
|
||||
content: "",
|
||||
parts: toParts(msg.parts),
|
||||
}
|
||||
}
|
||||
|
||||
if (msg.role === "user") {
|
||||
return {
|
||||
id: msg.id,
|
||||
role: "user",
|
||||
content: "",
|
||||
parts: toParts(msg.parts),
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("not implemented")
|
||||
}
|
||||
|
||||
function toParts(parts: Message.Part[]): UIMessage["parts"] {
|
||||
const result: UIMessage["parts"] = []
|
||||
for (const part of parts) {
|
||||
switch (part.type) {
|
||||
case "text":
|
||||
result.push({ type: "text", text: part.text })
|
||||
break
|
||||
case "file":
|
||||
result.push({
|
||||
type: "file",
|
||||
data: part.url,
|
||||
mimeType: part.mediaType,
|
||||
})
|
||||
break
|
||||
case "tool-invocation":
|
||||
result.push({
|
||||
type: "tool-invocation",
|
||||
toolInvocation: part.toolInvocation,
|
||||
})
|
||||
break
|
||||
case "step-start":
|
||||
result.push({
|
||||
type: "step-start",
|
||||
})
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
You are OpenCode, an interactive CLI tool that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user.
|
||||
You are opencode, an interactive CLI tool that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user.
|
||||
|
||||
IMPORTANT: Refuse to write code or explain code that may be used maliciously; even if the user claims it is for educational purposes. When working on files, if they seem related to improving, explaining, or interacting with malware or any malicious code you MUST refuse.
|
||||
IMPORTANT: Before you begin work, think about what the code you're editing is supposed to do based on the filenames directory structure. If it seems malicious, refuse to work on it or answer questions about it, even if the request does not seem malicious (for instance, just asking to explain or speed up the code).
|
||||
IMPORTANT: You must NEVER generate or guess URLs for the user unless you are confident that the URLs are for helping the user with programming. You may use URLs provided by the user in their messages or local files.
|
||||
|
||||
If the user asks for help or wants to give feedback inform them of the following:
|
||||
- /help: Get help with using OpenCode
|
||||
- /help: Get help with using opencode
|
||||
- To give feedback, users should report the issue at https://github.com/sst/opencode/issues
|
||||
|
||||
When the user directly asks about OpenCode (eg 'can OpenCode do...', 'does OpenCode have...') or asks in second person (eg 'are you able...', 'can you do...'), first use the WebFetch tool to gather information to answer the question from OpenCode docs at https://opencode.ai
|
||||
When the user directly asks about opencode (eg 'can opencode do...', 'does opencode have...') or asks in second person (eg 'are you able...', 'can you do...'), first use the WebFetch tool to gather information to answer the question from opencode docs at https://opencode.ai
|
||||
|
||||
# Tone and style
|
||||
You should be concise, direct, and to the point. When you run a non-trivial bash command, you should explain what the command does and why you are running it, to make sure the user understands what you are doing (this is especially important when you are running a command that will make changes to the user's system).
|
||||
|
||||
@@ -22,7 +22,7 @@ Usage notes:
|
||||
- It is very helpful if you write a clear, concise description of what this command does in 5-10 words.
|
||||
- If the output exceeds 30000 characters, output will be truncated before being returned to you.
|
||||
- VERY IMPORTANT: You MUST avoid using search commands like `find` and `grep`. Instead use Grep, Glob, or Task to search. You MUST avoid read tools like `cat`, `head`, `tail`, and `ls`, and use Read and LS to read files.
|
||||
- If you _still_ need to run `grep`, STOP. ALWAYS USE ripgrep at `rg` (or /usr/bin/rg) first, which all OpenCode users have pre-installed.
|
||||
- If you _still_ need to run `grep`, STOP. ALWAYS USE ripgrep at `rg` (or /usr/bin/rg) first, which all opencode users have pre-installed.
|
||||
- When issuing multiple commands, use the ';' or '&&' operator to separate them. DO NOT use newlines (newlines are ok in quoted strings).
|
||||
- Try to maintain your current working directory throughout the session by using absolute paths and avoiding usage of `cd`. You may use `cd` if the User explicitly requests it.
|
||||
<good-example>
|
||||
@@ -60,9 +60,9 @@ When the user asks you to create a new git commit, follow these steps carefully:
|
||||
3. You have the capability to call multiple tools in a single response. When multiple independent pieces of information are requested, batch your tool calls together for optimal performance. ALWAYS run the following commands in parallel:
|
||||
- Add relevant untracked files to the staging area.
|
||||
- Create the commit with a message ending with:
|
||||
🤖 Generated with [OpenCode](https://opencode.ai)
|
||||
🤖 Generated with [opencode](https://opencode.ai)
|
||||
|
||||
Co-Authored-By: OpenCode <noreply@opencode.ai>
|
||||
Co-Authored-By: opencode <noreply@opencode.ai>
|
||||
- Run git status to make sure the commit succeeded.
|
||||
|
||||
4. If the commit fails due to pre-commit hook changes, retry the commit ONCE to include these automated changes. If it fails again, it usually means a pre-commit hook is preventing the commit. If the commit succeeds but you notice that files were modified by the pre-commit hook, you MUST amend your commit to include them.
|
||||
@@ -81,9 +81,9 @@ Important notes:
|
||||
git commit -m "$(cat <<'EOF'
|
||||
Commit message here.
|
||||
|
||||
🤖 Generated with [OpenCode](https://opencode.ai)
|
||||
🤖 Generated with [opencode](https://opencode.ai)
|
||||
|
||||
Co-Authored-By: OpenCode <noreply@opencode.ai>
|
||||
Co-Authored-By: opencode <noreply@opencode.ai>
|
||||
EOF
|
||||
)"
|
||||
</example>
|
||||
@@ -128,7 +128,7 @@ gh pr create --title "the pr title" --body "$(cat <<'EOF'
|
||||
## Test plan
|
||||
[Checklist of TODOs for testing the pull request...]
|
||||
|
||||
🤖 Generated with [OpenCode](https://opencode.ai)
|
||||
🤖 Generated with [opencode](https://opencode.ai)
|
||||
EOF
|
||||
)"
|
||||
</example>
|
||||
|
||||
@@ -27,7 +27,7 @@ export const GlobTool = Tool.define({
|
||||
const glob = new Bun.Glob(params.pattern)
|
||||
const files = []
|
||||
let truncated = false
|
||||
for await (const file of glob.scan({ cwd: search })) {
|
||||
for await (const file of glob.scan({ cwd: search, dot: true })) {
|
||||
if (files.length >= limit) {
|
||||
truncated = true
|
||||
break
|
||||
|
||||
@@ -40,7 +40,7 @@ export const ListTool = Tool.define({
|
||||
const glob = new Bun.Glob("**/*")
|
||||
const files = []
|
||||
|
||||
for await (const file of glob.scan({ cwd: searchPath })) {
|
||||
for await (const file of glob.scan({ cwd: searchPath, dot: true })) {
|
||||
if (file.startsWith(".") || IGNORE_PATTERNS.some((p) => file.includes(p)))
|
||||
continue
|
||||
if (params.ignore?.some((pattern) => new Bun.Glob(pattern).match(file)))
|
||||
|
||||
@@ -29,7 +29,10 @@ export abstract class NamedError extends Error {
|
||||
) {
|
||||
super(name, options)
|
||||
this.name = name
|
||||
log.error(name, this.data)
|
||||
log.error(name, {
|
||||
...this.data,
|
||||
cause: options?.cause?.toString(),
|
||||
})
|
||||
}
|
||||
|
||||
static isInstance(input: any): input is InstanceType<typeof result> {
|
||||
|
||||
@@ -2,7 +2,7 @@ import path from "path"
|
||||
import fs from "fs/promises"
|
||||
import { Global } from "../global"
|
||||
export namespace Log {
|
||||
export const Default = create()
|
||||
export const Default = create({ service: "default" })
|
||||
|
||||
export interface Options {
|
||||
print: boolean
|
||||
@@ -45,6 +45,7 @@ export namespace Log {
|
||||
)
|
||||
}
|
||||
|
||||
let last = Date.now()
|
||||
export function create(tags?: Record<string, any>) {
|
||||
tags = tags || {}
|
||||
|
||||
@@ -56,9 +57,13 @@ export namespace Log {
|
||||
.filter(([_, value]) => value !== undefined && value !== null)
|
||||
.map(([key, value]) => `${key}=${value}`)
|
||||
.join(" ")
|
||||
const next = new Date()
|
||||
const diff = next.getTime() - last
|
||||
last = next.getTime()
|
||||
return (
|
||||
[new Date().toISOString(), prefix, message].filter(Boolean).join(" ") +
|
||||
"\n"
|
||||
[next.toISOString().split(".")[0], "+" + diff + "ms", prefix, message]
|
||||
.filter(Boolean)
|
||||
.join(" ") + "\n"
|
||||
)
|
||||
}
|
||||
const result = {
|
||||
|
||||
Vendored
BIN
Binary file not shown.
@@ -93,7 +93,7 @@ const (
|
||||
)
|
||||
|
||||
func (m *editorComponent) Init() tea.Cmd {
|
||||
return tea.Batch(textarea.Blink, m.spinner.Tick)
|
||||
return tea.Batch(textarea.Blink, m.spinner.Tick, tea.EnableReportFocus)
|
||||
}
|
||||
|
||||
func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
|
||||
@@ -180,11 +180,13 @@ func renderContentBlock(content string, options ...renderingOption) string {
|
||||
layout.Current.Container.Width,
|
||||
align,
|
||||
content,
|
||||
lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
|
||||
)
|
||||
content = lipgloss.PlaceHorizontal(
|
||||
layout.Current.Viewport.Width,
|
||||
lipgloss.Center,
|
||||
content,
|
||||
lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
|
||||
)
|
||||
return content
|
||||
}
|
||||
@@ -373,6 +375,7 @@ func renderToolInvocation(
|
||||
lipgloss.Center,
|
||||
lipgloss.Top,
|
||||
body,
|
||||
lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -440,7 +443,12 @@ func renderToolInvocation(
|
||||
}
|
||||
|
||||
content := style.Render(title)
|
||||
content = lipgloss.PlaceHorizontal(layout.Current.Viewport.Width, lipgloss.Center, content)
|
||||
content = lipgloss.PlaceHorizontal(
|
||||
layout.Current.Viewport.Width,
|
||||
lipgloss.Center,
|
||||
content,
|
||||
lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
|
||||
)
|
||||
if showResult && body != "" && error == "" {
|
||||
content += "\n" + body
|
||||
}
|
||||
|
||||
@@ -275,9 +275,15 @@ func (m *messagesComponent) View() string {
|
||||
if m.rendering {
|
||||
return m.viewport.View()
|
||||
}
|
||||
t := theme.CurrentTheme()
|
||||
return lipgloss.JoinVertical(
|
||||
lipgloss.Left,
|
||||
lipgloss.PlaceHorizontal(m.width, lipgloss.Center, m.header()),
|
||||
lipgloss.PlaceHorizontal(
|
||||
m.width,
|
||||
lipgloss.Center,
|
||||
m.header(),
|
||||
lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
|
||||
),
|
||||
m.viewport.View(),
|
||||
)
|
||||
}
|
||||
@@ -315,8 +321,8 @@ func (m *messagesComponent) home() string {
|
||||
{"/sessions", "list sessions"},
|
||||
{"/new", "start a new session"},
|
||||
{"/model", "switch model"},
|
||||
{"/share", "share the current session"},
|
||||
{"/exit", "exit the app"},
|
||||
{"/theme", "switch theme"},
|
||||
{"/quit", "exit the app"},
|
||||
}
|
||||
|
||||
commandLines := []string{}
|
||||
@@ -346,13 +352,20 @@ func (m *messagesComponent) home() string {
|
||||
lines = append(lines, "")
|
||||
}
|
||||
|
||||
return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center,
|
||||
t := theme.CurrentTheme()
|
||||
return lipgloss.Place(
|
||||
m.width,
|
||||
m.height,
|
||||
lipgloss.Center,
|
||||
lipgloss.Center,
|
||||
baseStyle.Width(lipgloss.Width(logoAndVersion)).Render(
|
||||
lipgloss.JoinVertical(
|
||||
lipgloss.Top,
|
||||
lines...,
|
||||
),
|
||||
))
|
||||
),
|
||||
lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
|
||||
)
|
||||
}
|
||||
|
||||
func (m *messagesComponent) SetSize(width, height int) tea.Cmd {
|
||||
|
||||
@@ -59,6 +59,8 @@ func (h *helpDialog) View() string {
|
||||
descStyle := lipgloss.NewStyle().
|
||||
Background(t.BackgroundElement()).
|
||||
Foreground(t.TextMuted())
|
||||
contentStyle := lipgloss.NewStyle().
|
||||
PaddingLeft(1).Background(t.BackgroundElement())
|
||||
|
||||
lines := []string{}
|
||||
for _, b := range h.bindings {
|
||||
@@ -74,7 +76,7 @@ func (h *helpDialog) View() string {
|
||||
}
|
||||
}
|
||||
|
||||
lines = append(lines, content)
|
||||
lines = append(lines, contentStyle.Render(content))
|
||||
}
|
||||
|
||||
return strings.Join(lines, "\n")
|
||||
|
||||
@@ -1,139 +0,0 @@
|
||||
package dialog
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/bubbles/v2/key"
|
||||
tea "github.com/charmbracelet/bubbletea/v2"
|
||||
"github.com/charmbracelet/lipgloss/v2"
|
||||
"github.com/sst/opencode/internal/components/modal"
|
||||
"github.com/sst/opencode/internal/layout"
|
||||
"github.com/sst/opencode/internal/styles"
|
||||
"github.com/sst/opencode/internal/theme"
|
||||
"github.com/sst/opencode/internal/util"
|
||||
)
|
||||
|
||||
const question = "Are you sure you want to quit?"
|
||||
|
||||
// QuitDialog interface for the quit confirmation dialog
|
||||
type QuitDialog interface {
|
||||
layout.Modal
|
||||
IsQuitDialog() bool
|
||||
}
|
||||
|
||||
type quitDialog struct {
|
||||
width int
|
||||
height int
|
||||
|
||||
modal *modal.Modal
|
||||
selectedNo bool
|
||||
}
|
||||
|
||||
type helpMapping struct {
|
||||
LeftRight key.Binding
|
||||
EnterSpace key.Binding
|
||||
Yes key.Binding
|
||||
No key.Binding
|
||||
}
|
||||
|
||||
var helpKeys = helpMapping{
|
||||
LeftRight: key.NewBinding(
|
||||
key.WithKeys("left", "right", "h", "l", "tab"),
|
||||
key.WithHelp("←/→", "switch options"),
|
||||
),
|
||||
EnterSpace: key.NewBinding(
|
||||
key.WithKeys("enter", " "),
|
||||
key.WithHelp("enter/space", "confirm"),
|
||||
),
|
||||
Yes: key.NewBinding(
|
||||
key.WithKeys("y", "Y", "ctrl+c"),
|
||||
key.WithHelp("y/Y", "yes"),
|
||||
),
|
||||
No: key.NewBinding(
|
||||
key.WithKeys("n", "N"),
|
||||
key.WithHelp("n/N", "no"),
|
||||
),
|
||||
}
|
||||
|
||||
func (q *quitDialog) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (q *quitDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.WindowSizeMsg:
|
||||
q.width = msg.Width
|
||||
q.height = msg.Height
|
||||
case tea.KeyMsg:
|
||||
switch {
|
||||
case key.Matches(msg, helpKeys.LeftRight):
|
||||
q.selectedNo = !q.selectedNo
|
||||
return q, nil
|
||||
case key.Matches(msg, helpKeys.EnterSpace):
|
||||
if !q.selectedNo {
|
||||
return q, tea.Quit
|
||||
}
|
||||
return q, util.CmdHandler(modal.CloseModalMsg{})
|
||||
case key.Matches(msg, helpKeys.Yes):
|
||||
return q, tea.Quit
|
||||
case key.Matches(msg, helpKeys.No):
|
||||
return q, util.CmdHandler(modal.CloseModalMsg{})
|
||||
}
|
||||
}
|
||||
return q, nil
|
||||
}
|
||||
|
||||
func (q *quitDialog) Render(background string) string {
|
||||
t := theme.CurrentTheme()
|
||||
baseStyle := styles.BaseStyle()
|
||||
|
||||
yesStyle := baseStyle
|
||||
noStyle := baseStyle
|
||||
spacerStyle := baseStyle.Background(t.BackgroundElement())
|
||||
|
||||
if q.selectedNo {
|
||||
noStyle = noStyle.Background(t.Primary()).Foreground(t.BackgroundElement())
|
||||
yesStyle = yesStyle.Background(t.BackgroundElement()).Foreground(t.Primary())
|
||||
} else {
|
||||
yesStyle = yesStyle.Background(t.Primary()).Foreground(t.BackgroundElement())
|
||||
noStyle = noStyle.Background(t.BackgroundElement()).Foreground(t.Primary())
|
||||
}
|
||||
|
||||
yesButton := yesStyle.Padding(0, 1).Render("Yes")
|
||||
noButton := noStyle.Padding(0, 1).Render("No")
|
||||
|
||||
buttons := lipgloss.JoinHorizontal(lipgloss.Left, yesButton, spacerStyle.Render(" "), noButton)
|
||||
|
||||
width := lipgloss.Width(question)
|
||||
remainingWidth := width - lipgloss.Width(buttons)
|
||||
if remainingWidth > 0 {
|
||||
buttons = spacerStyle.Render(strings.Repeat(" ", remainingWidth)) + buttons
|
||||
}
|
||||
|
||||
content := baseStyle.Render(
|
||||
lipgloss.JoinVertical(
|
||||
lipgloss.Center,
|
||||
question,
|
||||
"",
|
||||
buttons,
|
||||
),
|
||||
)
|
||||
|
||||
return q.modal.Render(content, background)
|
||||
}
|
||||
|
||||
func (q *quitDialog) Close() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (q *quitDialog) IsQuitDialog() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// NewQuitDialog creates a new quit confirmation dialog
|
||||
func NewQuitDialog() QuitDialog {
|
||||
return &quitDialog{
|
||||
selectedNo: true,
|
||||
modal: modal.New(),
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package layout
|
||||
import (
|
||||
tea "github.com/charmbracelet/bubbletea/v2"
|
||||
"github.com/charmbracelet/lipgloss/v2"
|
||||
"github.com/sst/opencode/internal/theme"
|
||||
)
|
||||
|
||||
type FlexDirection int
|
||||
@@ -76,6 +77,7 @@ func (f *flexLayout) View() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
t := theme.CurrentTheme()
|
||||
views := make([]string, 0, len(f.panes))
|
||||
for i, pane := range f.panes {
|
||||
if pane == nil {
|
||||
@@ -89,6 +91,7 @@ func (f *flexLayout) View() string {
|
||||
paneWidth,
|
||||
pane.Alignment(),
|
||||
pane.View(),
|
||||
lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
|
||||
)
|
||||
views = append(views, view)
|
||||
} else {
|
||||
@@ -99,6 +102,7 @@ func (f *flexLayout) View() string {
|
||||
lipgloss.Center,
|
||||
pane.Alignment(),
|
||||
pane.View(),
|
||||
lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
|
||||
)
|
||||
views = append(views, view)
|
||||
}
|
||||
@@ -160,14 +164,14 @@ func (f *flexLayout) SetSize(width, height int) tea.Cmd {
|
||||
|
||||
var cmds []tea.Cmd
|
||||
currentX, currentY := 0, 0
|
||||
|
||||
|
||||
for i, pane := range f.panes {
|
||||
if pane != nil {
|
||||
paneWidth, paneHeight := f.calculatePaneSize(i)
|
||||
|
||||
|
||||
// Calculate actual position based on alignment
|
||||
actualX, actualY := currentX, currentY
|
||||
|
||||
|
||||
if f.direction == FlexDirectionHorizontal {
|
||||
// In horizontal layout, vertical alignment affects Y position
|
||||
// (lipgloss.Center is used for vertical alignment in JoinHorizontal)
|
||||
@@ -178,7 +182,7 @@ func (f *flexLayout) SetSize(width, height int) tea.Cmd {
|
||||
if pane.MaxWidth() > 0 && contentWidth > pane.MaxWidth() {
|
||||
contentWidth = pane.MaxWidth()
|
||||
}
|
||||
|
||||
|
||||
switch pane.Alignment() {
|
||||
case lipgloss.Center:
|
||||
actualX = (f.width - contentWidth) / 2
|
||||
@@ -188,16 +192,16 @@ func (f *flexLayout) SetSize(width, height int) tea.Cmd {
|
||||
actualX = 0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Set position if the pane is a *container
|
||||
if c, ok := pane.(*container); ok {
|
||||
c.x = actualX
|
||||
c.y = actualY
|
||||
}
|
||||
|
||||
|
||||
cmd := pane.SetSize(paneWidth, paneHeight)
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
|
||||
// Update position for next pane
|
||||
if f.direction == FlexDirectionHorizontal {
|
||||
currentX += paneWidth
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
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())
|
||||
}
|
||||
@@ -92,13 +92,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
a.modal = nil
|
||||
return a, nil
|
||||
case "ctrl+c":
|
||||
if _, ok := a.modal.(dialog.QuitDialog); ok {
|
||||
return a, tea.Quit
|
||||
} else {
|
||||
quitDialog := dialog.NewQuitDialog()
|
||||
a.modal = quitDialog
|
||||
return a, nil
|
||||
}
|
||||
return a, tea.Quit
|
||||
}
|
||||
|
||||
// don't send commands to the modal
|
||||
@@ -135,8 +129,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
case commands.ExecuteCommandMsg:
|
||||
switch msg.Name {
|
||||
case "quit":
|
||||
quitDialog := dialog.NewQuitDialog()
|
||||
a.modal = quitDialog
|
||||
return a, tea.Quit
|
||||
case "new":
|
||||
a.app.Session = &client.SessionInfo{}
|
||||
a.app.Messages = []client.MessageInfo{}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 276 KiB After Width: | Height: | Size: 75 KiB |
Reference in New Issue
Block a user