Compare commits

..

48 Commits

Author SHA1 Message Date
opencode 13d4a802ac release: v0.4.36 2025-08-12 18:31:12 +00:00
adamdotdevin c4ae3e429c fix: markdown lists 2025-08-12 13:22:28 -05:00
Dax Raad e9cb360cb7 ci: sync 2025-08-12 14:21:34 -04:00
Dax Raad 596d4e4490 ci: sync 2025-08-12 14:17:18 -04:00
Dax Raad 45451095f7 ci: sync 2025-08-12 14:11:10 -04:00
adamdotdevin aae354c951 fix: word wrapping with hyphens 2025-08-12 13:03:35 -05:00
Dax Raad 4cddda3e16 ci: sync 2025-08-12 14:00:30 -04:00
Dax Raad 834aa036a4 ci: sync 2025-08-12 13:58:51 -04:00
Dax Raad 6d1b6a6fb1 ci: ignore 2025-08-12 13:54:53 -04:00
Dax Raad e78fe8dcba ci: tweak 2025-08-12 13:53:32 -04:00
Dax Raad f62d826037 ci: sync 2025-08-12 13:42:46 -04:00
Dax Raad 342f4239e4 ci: softer language 2025-08-12 13:39:53 -04:00
Dax Raad 5141fe8d7d ci: sync 2025-08-12 17:36:48 +00:00
opencode 3a9dd306db release: v0.4.35 2025-08-12 17:36:48 +00:00
Dax Raad e32b6e2cc2 ci: ignore 2025-08-12 13:31:23 -04:00
Dax Raad fab0e5de04 fix issue when @ tagging fiels throwing error 2025-08-12 13:30:52 -04:00
Dax Raad 61105b487f ci: ignore 2025-08-12 13:25:05 -04:00
Dax 6f584ec641 ci: improve guideline check (#1867) 2025-08-12 13:23:19 -04:00
opencode 3a2b2f13f2 release: v0.4.34 2025-08-12 17:14:23 +00:00
Dax Raad be13e71fb9 ci: sync 2025-08-12 13:09:24 -04:00
Dax Raad dd0c049119 ci: tweak 2025-08-12 13:01:45 -04:00
Dax Raad ee2b57958d ci: disable aur 2025-08-12 12:58:18 -04:00
Dax Raad 4178b3c6ae ci: sync 2025-08-12 12:53:55 -04:00
Dax Raad 2c2752ee02 ci: ignore 2025-08-12 12:33:10 -04:00
Dax Raad 5a17f44da4 support OPENCODE_PERMISSION json env variable 2025-08-12 12:28:08 -04:00
Dax Raad 354e55ecef ci: ignore 2025-08-12 12:12:54 -04:00
Dax 10735f93ca Add agent-level permissions with whitelist/blacklist support (#1862) 2025-08-12 11:39:39 -04:00
adamdotdevin ccaebdcd16 fix: long word and attachment wrapping in editor 2025-08-12 10:34:54 -05:00
opencode 2bbd7a167a release: v0.4.29 2025-08-12 14:00:15 +00:00
Aiden Cline f12d470b33 fix: Missing ~/.local/share/opencode/bin directory causes misleading … (#1860) 2025-08-12 13:54:33 +00:00
opencode 0835170224 release: v0.4.28 2025-08-12 13:54:32 +00:00
adamdotdevin 3530885f48 fix: vscode extension cursor placement 2025-08-12 08:48:42 -05:00
opencode a071a2b7f4 release: v0.4.27 2025-08-12 12:58:53 +00:00
adamdotdevin b2f2c9ac37 fix: use real cursor instead of virtual cursor 2025-08-12 07:52:19 -05:00
GitHub Action 631722213b ignore: update download stats 2025-08-12 2025-08-12 12:04:42 +00:00
Camden Clark 80b25c79bb fix: preserve process.env when spawning formatter commands (#1850) 2025-08-12 02:05:26 -04:00
Dax 3d9c5b4adf Update guidelines-check.yml 2025-08-12 00:34:34 -04:00
Dax a3064e2c32 Update duplicate-issues.yml 2025-08-12 00:34:16 -04:00
Dax Raad 275bc4d2c8 ci: add guidelines check workflow for PRs 2025-08-12 00:01:59 -04:00
Dax Raad 39106718ed ci: tweak 2025-08-11 23:52:16 -04:00
Dax Raad 02cfdfbf5b ci: add duplicate issue detection workflow 2025-08-11 23:51:08 -04:00
Dax Raad 1ec71e419b support wildcard matching tool names in config 2025-08-11 23:37:09 -04:00
opencode 5fbbdcaf64 release: v0.4.26 2025-08-12 03:25:36 +00:00
Dax Raad 2b6afe90d0 fix azure reasoningEffort 2025-08-11 23:20:19 -04:00
Dax Raad 5f34dcc792 azure reasoning effort 2025-08-11 23:04:32 -04:00
opencode 681abcbf2d release: v0.4.25 2025-08-12 02:51:32 +00:00
Dax Raad 603e81ef5a Simplify git-committer agent definition for better maintainability 2025-08-11 22:45:40 -04:00
Dax Raad fb0a200ecf refactor: replace OPENCODE_AGENTS env var with HTTP API call
Replace environment variable passing of agent data from Node.js to TUI
with proper HTTP API call to /agent endpoint. This improves architecture
by eliminating env var dependencies and allows dynamic agent data fetching.
2025-08-11 22:42:25 -04:00
44 changed files with 732 additions and 158 deletions
+50
View File
@@ -0,0 +1,50 @@
name: Duplicate Issue Detection
on:
issues:
types: [opened]
jobs:
check-duplicates:
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Install opencode
run: curl -fsSL https://opencode.ai/install | bash
- name: Check for duplicate issues
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
OPENCODE_PERMISSION: '{ "bash": { "gh*": "allow", "*": "deny" } }'
run: |
opencode run -m anthropic/claude-sonnet-4-20250514 "A new issue has been created: '${{ github.event.issue.title }}'
Issue body:
${{ github.event.issue.body }}
Please search through existing issues in this repository to find any potential duplicates of this new issue. Consider:
1. Similar titles or descriptions
2. Same error messages or symptoms
3. Related functionality or components
4. Similar feature requests
If you find any potential duplicates, please comment on the new issue with:
- A brief explanation of why it might be a duplicate
- Links to the potentially duplicate issues
- A suggestion to check those issues first
Use this format for the comment:
'👋 This issue might be a duplicate of existing issues. Please check:
- #[issue_number]: [brief description of similarity]
If none of these address your specific case, please let us know how this issue differs.'
If no clear duplicates are found, do not comment."
+52
View File
@@ -0,0 +1,52 @@
name: Guidelines Check
on:
pull_request_target:
types: [opened, synchronize]
jobs:
check-guidelines:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Install opencode
run: curl -fsSL https://opencode.ai/install | bash
- name: Check PR guidelines compliance
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
OPENCODE_PERMISSION: '{ "bash": { "gh*": "allow", "gh review*": "deny", "*": "deny" } }'
run: |
opencode run -m anthropic/claude-sonnet-4-20250514 "A new pull request has been created: '${{ github.event.pull_request.title }}'
<pr-number>
${{ github.event.pull_request.number }}
</pr-number>
<pr-description>
${{ github.event.pull_request.body }}
</pr-description>
Please check all the code changes in this pull request against the guidelines in AGENTS.md file in this repository. Diffs are important but make sure you read the entire file to get proper context. Make it clear the suggestions are merely suggestions and the human can decide what to do
Use the gh cli to create comments on the files for the violations. Try to leave the comment on the exact line number. If you have a suggested fix include it in a suggestion code block.
Command MUST be like this.
```
gh api \
--method POST \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/comments \
-f 'body=[summary of issue]' -f 'commit_id=${{ github.event.pull_request.head.sha }}' -f 'path=[path-to-file]' -F "line=[line]" -f 'side=RIGHT'
```
Only create comments for actual violations. If the code follows all guidelines, don't run any gh commands."
+6 -6
View File
@@ -48,20 +48,20 @@ jobs:
restore-keys: |
${{ runner.os }}-bun-
- name: Install makepkg
run: |
sudo apt-get update
sudo apt-get install -y pacman-package-manager
# Temporarily disabled AUR steps
# - name: Install makepkg
# run: |
# sudo apt-get update
# sudo apt-get install -y pacman-package-manager
- name: Setup SSH for AUR
run: |
mkdir -p ~/.ssh
echo "${{ secrets.AUR_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
ssh-keyscan -H aur.archlinux.org >> ~/.ssh/known_hosts
git config --global user.email "opencode@sst.dev"
git config --global user.name "opencode"
# ssh-keyscan -H aur.archlinux.org >> ~/.ssh/known_hosts
- name: Install dependencies
run: bun install
+10
View File
@@ -0,0 +1,10 @@
---
description: Use this agent when you are saked to commit and push code changes to a git repository.
mode: subagent
---
You commit and push to git
Commit messages should be brief since they are used to generate release notes.
Messages should say WHY the change was made and not WHAT was changed.
+46 -45
View File
@@ -1,47 +1,48 @@
# Download Stats
| Date | GitHub Downloads | npm Downloads | Total |
| ---------- | ---------------- | ---------------- | ---------------- |
| 2025-06-29 | 18,789 (+0) | 39,420 (+0) | 58,209 (+0) |
| 2025-06-30 | 20,127 (+1,338) | 41,059 (+1,639) | 61,186 (+2,977) |
| 2025-07-01 | 22,108 (+1,981) | 43,745 (+2,686) | 65,853 (+4,667) |
| 2025-07-02 | 24,814 (+2,706) | 46,168 (+2,423) | 70,982 (+5,129) |
| 2025-07-03 | 27,834 (+3,020) | 49,955 (+3,787) | 77,789 (+6,807) |
| 2025-07-04 | 30,608 (+2,774) | 54,758 (+4,803) | 85,366 (+7,577) |
| 2025-07-05 | 32,524 (+1,916) | 58,371 (+3,613) | 90,895 (+5,529) |
| 2025-07-06 | 33,766 (+1,242) | 59,694 (+1,323) | 93,460 (+2,565) |
| 2025-07-08 | 38,052 (+4,286) | 64,468 (+4,774) | 102,520 (+9,060) |
| 2025-07-09 | 40,924 (+2,872) | 67,935 (+3,467) | 108,859 (+6,339) |
| 2025-07-10 | 43,796 (+2,872) | 71,402 (+3,467) | 115,198 (+6,339) |
| 2025-07-11 | 46,982 (+3,186) | 77,462 (+6,060) | 124,444 (+9,246) |
| 2025-07-12 | 49,302 (+2,320) | 82,177 (+4,715) | 131,479 (+7,035) |
| 2025-07-13 | 50,803 (+1,501) | 86,394 (+4,217) | 137,197 (+5,718) |
| 2025-07-14 | 53,283 (+2,480) | 87,860 (+1,466) | 141,143 (+3,946) |
| 2025-07-15 | 57,590 (+4,307) | 91,036 (+3,176) | 148,626 (+7,483) |
| 2025-07-16 | 62,313 (+4,723) | 95,258 (+4,222) | 157,571 (+8,945) |
| 2025-07-17 | 66,684 (+4,371) | 100,048 (+4,790) | 166,732 (+9,161) |
| 2025-07-18 | 70,379 (+3,695) | 102,587 (+2,539) | 172,966 (+6,234) |
| 2025-07-19 | 73,497 (+3,117) | 105,904 (+3,317) | 179,401 (+6,434) |
| 2025-07-20 | 76,453 (+2,956) | 109,044 (+3,140) | 185,497 (+6,096) |
| 2025-07-21 | 80,197 (+3,744) | 113,537 (+4,493) | 193,734 (+8,237) |
| 2025-07-22 | 84,251 (+4,054) | 118,073 (+4,536) | 202,324 (+8,590) |
| 2025-07-23 | 88,589 (+4,338) | 121,436 (+3,363) | 210,025 (+7,701) |
| 2025-07-24 | 92,469 (+3,880) | 124,091 (+2,655) | 216,560 (+6,535) |
| 2025-07-25 | 96,417 (+3,948) | 126,985 (+2,894) | 223,402 (+6,842) |
| 2025-07-26 | 100,646 (+4,229) | 131,411 (+4,426) | 232,057 (+8,655) |
| 2025-07-27 | 102,644 (+1,998) | 134,736 (+3,325) | 237,380 (+5,323) |
| 2025-07-28 | 105,446 (+2,802) | 136,016 (+1,280) | 241,462 (+4,082) |
| 2025-07-29 | 108,998 (+3,552) | 137,542 (+1,526) | 246,540 (+5,078) |
| 2025-07-30 | 113,544 (+4,546) | 140,317 (+2,775) | 253,861 (+7,321) |
| 2025-07-31 | 118,339 (+4,795) | 143,344 (+3,027) | 261,683 (+7,822) |
| 2025-08-01 | 123,539 (+5,200) | 146,680 (+3,336) | 270,219 (+8,536) |
| 2025-08-02 | 127,864 (+4,325) | 149,236 (+2,556) | 277,100 (+6,881) |
| 2025-08-03 | 131,397 (+3,533) | 150,451 (+1,215) | 281,848 (+4,748) |
| 2025-08-04 | 136,266 (+4,869) | 153,260 (+2,809) | 289,526 (+7,678) |
| 2025-08-05 | 141,596 (+5,330) | 155,752 (+2,492) | 297,348 (+7,822) |
| 2025-08-06 | 147,067 (+5,471) | 158,309 (+2,557) | 305,376 (+8,028) |
| 2025-08-07 | 152,591 (+5,524) | 160,889 (+2,580) | 313,480 (+8,104) |
| 2025-08-08 | 158,187 (+5,596) | 163,448 (+2,559) | 321,635 (+8,155) |
| 2025-08-09 | 162,770 (+4,583) | 165,721 (+2,273) | 328,491 (+6,856) |
| 2025-08-10 | 165,695 (+2,925) | 167,109 (+1,388) | 332,804 (+4,313) |
| 2025-08-11 | 169,297 (+3,602) | 167,953 (+844) | 337,250 (+4,446) |
| Date | GitHub Downloads | npm Downloads | Total |
| ---------- | ---------------- | ---------------- | ----------------- |
| 2025-06-29 | 18,789 (+0) | 39,420 (+0) | 58,209 (+0) |
| 2025-06-30 | 20,127 (+1,338) | 41,059 (+1,639) | 61,186 (+2,977) |
| 2025-07-01 | 22,108 (+1,981) | 43,745 (+2,686) | 65,853 (+4,667) |
| 2025-07-02 | 24,814 (+2,706) | 46,168 (+2,423) | 70,982 (+5,129) |
| 2025-07-03 | 27,834 (+3,020) | 49,955 (+3,787) | 77,789 (+6,807) |
| 2025-07-04 | 30,608 (+2,774) | 54,758 (+4,803) | 85,366 (+7,577) |
| 2025-07-05 | 32,524 (+1,916) | 58,371 (+3,613) | 90,895 (+5,529) |
| 2025-07-06 | 33,766 (+1,242) | 59,694 (+1,323) | 93,460 (+2,565) |
| 2025-07-08 | 38,052 (+4,286) | 64,468 (+4,774) | 102,520 (+9,060) |
| 2025-07-09 | 40,924 (+2,872) | 67,935 (+3,467) | 108,859 (+6,339) |
| 2025-07-10 | 43,796 (+2,872) | 71,402 (+3,467) | 115,198 (+6,339) |
| 2025-07-11 | 46,982 (+3,186) | 77,462 (+6,060) | 124,444 (+9,246) |
| 2025-07-12 | 49,302 (+2,320) | 82,177 (+4,715) | 131,479 (+7,035) |
| 2025-07-13 | 50,803 (+1,501) | 86,394 (+4,217) | 137,197 (+5,718) |
| 2025-07-14 | 53,283 (+2,480) | 87,860 (+1,466) | 141,143 (+3,946) |
| 2025-07-15 | 57,590 (+4,307) | 91,036 (+3,176) | 148,626 (+7,483) |
| 2025-07-16 | 62,313 (+4,723) | 95,258 (+4,222) | 157,571 (+8,945) |
| 2025-07-17 | 66,684 (+4,371) | 100,048 (+4,790) | 166,732 (+9,161) |
| 2025-07-18 | 70,379 (+3,695) | 102,587 (+2,539) | 172,966 (+6,234) |
| 2025-07-19 | 73,497 (+3,117) | 105,904 (+3,317) | 179,401 (+6,434) |
| 2025-07-20 | 76,453 (+2,956) | 109,044 (+3,140) | 185,497 (+6,096) |
| 2025-07-21 | 80,197 (+3,744) | 113,537 (+4,493) | 193,734 (+8,237) |
| 2025-07-22 | 84,251 (+4,054) | 118,073 (+4,536) | 202,324 (+8,590) |
| 2025-07-23 | 88,589 (+4,338) | 121,436 (+3,363) | 210,025 (+7,701) |
| 2025-07-24 | 92,469 (+3,880) | 124,091 (+2,655) | 216,560 (+6,535) |
| 2025-07-25 | 96,417 (+3,948) | 126,985 (+2,894) | 223,402 (+6,842) |
| 2025-07-26 | 100,646 (+4,229) | 131,411 (+4,426) | 232,057 (+8,655) |
| 2025-07-27 | 102,644 (+1,998) | 134,736 (+3,325) | 237,380 (+5,323) |
| 2025-07-28 | 105,446 (+2,802) | 136,016 (+1,280) | 241,462 (+4,082) |
| 2025-07-29 | 108,998 (+3,552) | 137,542 (+1,526) | 246,540 (+5,078) |
| 2025-07-30 | 113,544 (+4,546) | 140,317 (+2,775) | 253,861 (+7,321) |
| 2025-07-31 | 118,339 (+4,795) | 143,344 (+3,027) | 261,683 (+7,822) |
| 2025-08-01 | 123,539 (+5,200) | 146,680 (+3,336) | 270,219 (+8,536) |
| 2025-08-02 | 127,864 (+4,325) | 149,236 (+2,556) | 277,100 (+6,881) |
| 2025-08-03 | 131,397 (+3,533) | 150,451 (+1,215) | 281,848 (+4,748) |
| 2025-08-04 | 136,266 (+4,869) | 153,260 (+2,809) | 289,526 (+7,678) |
| 2025-08-05 | 141,596 (+5,330) | 155,752 (+2,492) | 297,348 (+7,822) |
| 2025-08-06 | 147,067 (+5,471) | 158,309 (+2,557) | 305,376 (+8,028) |
| 2025-08-07 | 152,591 (+5,524) | 160,889 (+2,580) | 313,480 (+8,104) |
| 2025-08-08 | 158,187 (+5,596) | 163,448 (+2,559) | 321,635 (+8,155) |
| 2025-08-09 | 162,770 (+4,583) | 165,721 (+2,273) | 328,491 (+6,856) |
| 2025-08-10 | 165,695 (+2,925) | 167,109 (+1,388) | 332,804 (+4,313) |
| 2025-08-11 | 169,297 (+3,602) | 167,953 (+844) | 337,250 (+4,446) |
| 2025-08-12 | 176,307 (+7,010) | 171,876 (+3,923) | 348,183 (+10,933) |
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/cloud-core",
"version": "0.4.24",
"version": "0.4.36",
"private": true,
"type": "module",
"dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode/cloud-function",
"version": "0.4.24",
"version": "0.4.36",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode/cloud-web",
"version": "0.4.24",
"version": "0.4.36",
"private": true,
"description": "",
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode/function",
"version": "0.4.24",
"version": "0.4.36",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "0.4.24",
"version": "0.4.36",
"name": "opencode",
"type": "module",
"private": true,
+2
View File
@@ -97,6 +97,7 @@ if (!snapshot) {
const macX64Sha = await $`sha256sum ./dist/opencode-darwin-x64.zip | cut -d' ' -f1`.text().then((x) => x.trim())
const macArm64Sha = await $`sha256sum ./dist/opencode-darwin-arm64.zip | cut -d' ' -f1`.text().then((x) => x.trim())
/*
// AUR package
const pkgbuild = [
"# Maintainer: dax",
@@ -136,6 +137,7 @@ if (!snapshot) {
await $`cd ./dist/aur-${pkg} && git commit -m "Update to v${version}"`
if (!dry) await $`cd ./dist/aur-${pkg} && git push`
}
*/
// Homebrew formula
const homebrewFormula = [
+48 -9
View File
@@ -5,6 +5,7 @@ import { Provider } from "../provider/provider"
import { generateObject, type ModelMessage } from "ai"
import PROMPT_GENERATE from "./generate.txt"
import { SystemPrompt } from "../session/system"
import { mergeDeep } from "remeda"
export namespace Agent {
export const Info = z
@@ -14,6 +15,11 @@ export namespace Agent {
mode: z.union([z.literal("subagent"), z.literal("primary"), z.literal("all")]),
topP: z.number().optional(),
temperature: z.number().optional(),
permission: z.object({
edit: Config.Permission,
bash: z.record(z.string(), Config.Permission),
webfetch: Config.Permission.optional(),
}),
model: z
.object({
modelID: z.string(),
@@ -31,6 +37,13 @@ export namespace Agent {
const state = App.state("agent", async () => {
const cfg = await Config.get()
const defaultPermission: Info["permission"] = {
edit: "allow",
bash: {
"*": "allow",
},
webfetch: "allow",
}
const result: Record<string, Info> = {
general: {
name: "general",
@@ -41,17 +54,20 @@ export namespace Agent {
todowrite: false,
},
options: {},
permission: defaultPermission,
mode: "subagent",
},
build: {
name: "build",
tools: {},
options: {},
permission: defaultPermission,
mode: "primary",
},
plan: {
name: "plan",
options: {},
permission: defaultPermission,
tools: {
write: false,
edit: false,
@@ -70,25 +86,48 @@ export namespace Agent {
item = result[key] = {
name: key,
mode: "all",
permission: defaultPermission,
options: {},
tools: {},
}
const { model, prompt, tools, description, temperature, top_p, mode, ...extra } = value
const { model, prompt, tools, description, temperature, top_p, mode, permission, ...extra } = value
item.options = {
...item.options,
...extra,
}
if (value.model) item.model = Provider.parseModel(value.model)
if (value.prompt) item.prompt = value.prompt
if (value.tools)
if (model) item.model = Provider.parseModel(model)
if (prompt) item.prompt = prompt
if (tools)
item.tools = {
...item.tools,
...value.tools,
...tools,
}
if (value.description) item.description = value.description
if (value.temperature != undefined) item.temperature = value.temperature
if (value.top_p != undefined) item.topP = value.top_p
if (value.mode) item.mode = value.mode
if (description) item.description = description
if (temperature != undefined) item.temperature = temperature
if (top_p != undefined) item.topP = top_p
if (mode) item.mode = mode
if (permission ?? cfg.permission) {
const merged = mergeDeep(cfg.permission ?? {}, permission ?? {})
if (merged.edit) item.permission.edit = merged.edit
if (merged.webfetch) item.permission.webfetch = merged.webfetch
if (merged.bash) {
if (typeof merged.bash === "string") {
item.permission.bash = {
"*": merged.bash,
}
}
// if granular permissions are provided, default to "ask"
if (typeof merged.bash === "object") {
item.permission.bash = mergeDeep(
{
"*": "ask",
},
merged.bash,
)
}
}
}
}
return result
})
+1 -2
View File
@@ -12,7 +12,7 @@ import { Bus } from "../../bus"
import { Log } from "../../util/log"
import { FileWatcher } from "../../file/watch"
import { Ide } from "../../ide"
import { Agent } from "../../agent/agent"
import { Flag } from "../../flag/flag"
import { Session } from "../../session"
@@ -141,7 +141,6 @@ export const TuiCommand = cmd({
CGO_ENABLED: "0",
OPENCODE_SERVER: server.url.toString(),
OPENCODE_APP_INFO: JSON.stringify(app),
OPENCODE_AGENTS: JSON.stringify(await Agent.list()),
},
onExit: () => {
server.stop()
+14 -3
View File
@@ -110,6 +110,10 @@ export namespace Config {
].map((x) => "file://" + x),
)
if (Flag.OPENCODE_PERMISSION) {
result.permission = mergeDeep(result.permission ?? {}, JSON.parse(Flag.OPENCODE_PERMISSION))
}
// Handle migration from autoshare to share field
if (result.autoshare === true && !result.share) {
result.share = "auto"
@@ -164,6 +168,9 @@ export namespace Config {
export const Mcp = z.discriminatedUnion("type", [McpLocal, McpRemote])
export type Mcp = z.infer<typeof Mcp>
export const Permission = z.union([z.literal("ask"), z.literal("allow"), z.literal("deny")])
export type Permission = z.infer<typeof Permission>
export const Agent = z
.object({
model: z.string().optional(),
@@ -174,6 +181,13 @@ export namespace Config {
disable: z.boolean().optional(),
description: z.string().optional().describe("Description of when to use the agent"),
mode: z.union([z.literal("subagent"), z.literal("primary"), z.literal("all")]).optional(),
permission: z
.object({
edit: Permission.optional(),
bash: z.union([Permission, z.record(z.string(), Permission)]).optional(),
webfetch: Permission.optional(),
})
.optional(),
})
.catchall(z.any())
.openapi({
@@ -243,9 +257,6 @@ export namespace Config {
})
export type Layout = z.infer<typeof Layout>
export const Permission = z.union([z.literal("ask"), z.literal("allow"), z.literal("deny")])
export type Permission = z.infer<typeof Permission>
export const Info = z
.object({
$schema: z.string().optional().describe("JSON schema reference for configuration validation"),
+1
View File
@@ -3,6 +3,7 @@ export namespace Flag {
export const OPENCODE_DISABLE_WATCHER = truthy("OPENCODE_DISABLE_WATCHER")
export const OPENCODE_CONFIG = process.env["OPENCODE_CONFIG"]
export const OPENCODE_DISABLE_AUTOUPDATE = truthy("OPENCODE_DISABLE_AUTOUPDATE")
export const OPENCODE_PERMISSION = process.env["OPENCODE_PERMISSION"]
function truthy(key: string) {
const value = process.env[key]?.toLowerCase()
+1 -1
View File
@@ -71,7 +71,7 @@ export namespace Format {
const proc = Bun.spawn({
cmd: item.command.map((x) => x.replace("$FILE", file)),
cwd: App.info().path.cwd,
env: item.environment,
env: { ...process.env, ...item.environment },
stdout: "ignore",
stderr: "ignore",
})
+2 -1
View File
@@ -25,9 +25,10 @@ await Promise.all([
fs.mkdir(Global.Path.config, { recursive: true }),
fs.mkdir(Global.Path.state, { recursive: true }),
fs.mkdir(Global.Path.log, { recursive: true }),
fs.mkdir(Global.Path.bin, { recursive: true }),
])
const CACHE_VERSION = "6"
const CACHE_VERSION = "7"
const version = await Bun.file(path.join(Global.Path.cache, "version"))
.text()
+3
View File
@@ -21,6 +21,9 @@ import { GithubCommand } from "./cli/cmd/github"
const cancel = new AbortController()
try {
} catch (e) {}
process.on("unhandledRejection", (e) => {
Log.Default.error("rejection", {
e: e instanceof Error ? e.message : e,
+1 -1
View File
@@ -155,7 +155,7 @@ export namespace Permission {
public readonly permissionID: string,
public readonly toolCallID?: string,
) {
super(`The user rejected permission to use this functionality`)
super(`The user rejected permission to use this specific tool call. You may try again with different parameters.`)
}
}
}
+1 -2
View File
@@ -87,8 +87,7 @@ export namespace ProviderTransform {
if (modelID.includes("gpt-5")) {
if (providerID === "azure") {
return {
reasoning_effort: "minimal",
text_verbosity: "verbose",
reasoningEffort: "minimal",
}
}
return {
+6 -3
View File
@@ -42,6 +42,7 @@ import { ToolRegistry } from "../tool/registry"
import { Plugin } from "../plugin"
import { Agent } from "../agent/agent"
import { Permission } from "../permission"
import { Wildcard } from "../util/wildcard"
export namespace Session {
const log = Log.create({ service: "session" })
@@ -522,6 +523,7 @@ export namespace Session {
t.execute(args, {
sessionID: input.sessionID,
abort: new AbortController().signal,
agent: input.agent!,
messageID: userMsg.id,
metadata: async () => {},
}),
@@ -764,11 +766,11 @@ export namespace Session {
const enabledTools = pipe(
agent.tools,
mergeDeep(await ToolRegistry.enabled(input.providerID, input.modelID)),
mergeDeep(await ToolRegistry.enabled(input.providerID, input.modelID, agent)),
mergeDeep(input.tools ?? {}),
)
for (const item of await ToolRegistry.tools(input.providerID, input.modelID)) {
if (enabledTools[item.id] === false) continue
if (Wildcard.all(item.id, enabledTools) === false) continue
tools[item.id] = tool({
id: item.id as any,
description: item.description,
@@ -790,6 +792,7 @@ export namespace Session {
abort: options.abortSignal!,
messageID: assistantMsg.id,
callID: options.toolCallId,
agent: agent.name,
metadata: async (val) => {
const match = processor.partFromToolCall(options.toolCallId)
if (match && match.state.status === "running") {
@@ -829,7 +832,7 @@ export namespace Session {
}
for (const [key, item] of Object.entries(await MCP.tools())) {
if (enabledTools[key] === false) continue
if (Wildcard.all(key, enabledTools) === false) continue
const execute = item.execute
if (!execute) continue
item.execute = async (args, opts) => {
+4 -23
View File
@@ -5,12 +5,12 @@ import { Tool } from "./tool"
import DESCRIPTION from "./bash.txt"
import { App } from "../app/app"
import { Permission } from "../permission"
import { Config } from "../config/config"
import { Filesystem } from "../util/filesystem"
import { lazy } from "../util/lazy"
import { Log } from "../util/log"
import { Wildcard } from "../util/wildcard"
import { $ } from "bun"
import { Agent } from "../agent/agent"
const MAX_OUTPUT_LENGTH = 30000
const DEFAULT_TIMEOUT = 1 * 60 * 1000
@@ -40,20 +40,8 @@ export const BashTool = Tool.define("bash", {
async execute(params, ctx) {
const timeout = Math.min(params.timeout ?? DEFAULT_TIMEOUT, MAX_TIMEOUT)
const app = App.info()
const cfg = await Config.get()
const tree = await parser().then((p) => p.parse(params.command))
const permissions = (() => {
const value = cfg.permission?.bash
if (!value)
return {
"*": "allow",
}
if (typeof value === "string")
return {
"*": value,
}
return value
})()
const permissions = await Agent.get(ctx.agent).then((x) => x.permission.bash)
let needsAsk = false
for (const node of tree.rootNode.descendantsOfType("command")) {
@@ -93,17 +81,10 @@ export const BashTool = Tool.define("bash", {
// always allow cd if it passes above check
if (!needsAsk && command[0] !== "cd") {
const action = (() => {
for (const [pattern, value] of Object.entries(permissions)) {
const match = Wildcard.match(node.text, pattern)
log.info("checking", { text: node.text.trim(), pattern, match })
if (match) return value
}
return "ask"
})()
const action = Wildcard.all(node.text, permissions)
if (action === "deny") {
throw new Error(
"The user has specifically restricted access to this command, you are not allowed to execute it.",
`The user has specifically restricted access to this command, you are not allowed to execute it. Here is the configuration: ${JSON.stringify(permissions)}`,
)
}
if (action === "ask") needsAsk = true
+4 -4
View File
@@ -14,8 +14,8 @@ import { App } from "../app/app"
import { File } from "../file"
import { Bus } from "../bus"
import { FileTime } from "../file/time"
import { Config } from "../config/config"
import { Filesystem } from "../util/filesystem"
import { Agent } from "../agent/agent"
export const EditTool = Tool.define("edit", {
description: DESCRIPTION,
@@ -40,7 +40,7 @@ export const EditTool = Tool.define("edit", {
throw new Error(`File ${filePath} is not in the current working directory`)
}
const cfg = await Config.get()
const agent = await Agent.get(ctx.agent)
let diff = ""
let contentOld = ""
let contentNew = ""
@@ -48,7 +48,7 @@ export const EditTool = Tool.define("edit", {
if (params.oldString === "") {
contentNew = params.newString
diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew))
if (cfg.permission?.edit === "ask") {
if (agent.permission.edit === "ask") {
await Permission.ask({
type: "edit",
sessionID: ctx.sessionID,
@@ -77,7 +77,7 @@ export const EditTool = Tool.define("edit", {
contentNew = replace(contentOld, params.oldString, params.newString, params.replaceAll)
diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew))
if (cfg.permission?.edit === "ask") {
if (agent.permission.edit === "ask") {
await Permission.ask({
type: "edit",
sessionID: ctx.sessionID,
+9 -6
View File
@@ -11,7 +11,7 @@ import { TodoWriteTool, TodoReadTool } from "./todo"
import { WebFetchTool } from "./webfetch"
import { WriteTool } from "./write"
import { InvalidTool } from "./invalid"
import { Config } from "../config/config"
import type { Agent } from "../agent/agent"
export namespace ToolRegistry {
const ALL = [
@@ -66,20 +66,23 @@ export namespace ToolRegistry {
return result
}
export async function enabled(_providerID: string, _modelID: string): Promise<Record<string, boolean>> {
const cfg = await Config.get()
export async function enabled(
_providerID: string,
_modelID: string,
agent: Agent.Info,
): Promise<Record<string, boolean>> {
const result: Record<string, boolean> = {}
result["patch"] = false
if (cfg.permission?.edit === "deny") {
if (agent.permission.edit === "deny") {
result["edit"] = false
result["patch"] = false
result["write"] = false
}
if (cfg?.permission?.bash === "deny") {
if (agent.permission.bash["*"] === "deny" && Object.keys(agent.permission.bash).length === 1) {
result["bash"] = false
}
if (cfg?.permission?.webfetch === "deny") {
if (agent.permission.webfetch === "deny") {
result["webfetch"] = false
}
+1
View File
@@ -7,6 +7,7 @@ export namespace Tool {
export type Context<M extends Metadata = Metadata> = {
sessionID: string
messageID: string
agent: string
callID?: string
abort: AbortSignal
metadata(input: { title?: string; metadata?: M }): void
+3 -3
View File
@@ -8,8 +8,8 @@ import { App } from "../app/app"
import { Bus } from "../bus"
import { File } from "../file"
import { FileTime } from "../file/time"
import { Config } from "../config/config"
import { Filesystem } from "../util/filesystem"
import { Agent } from "../agent/agent"
export const WriteTool = Tool.define("write", {
description: DESCRIPTION,
@@ -28,8 +28,8 @@ export const WriteTool = Tool.define("write", {
const exists = await file.exists()
if (exists) await FileTime.assert(ctx.sessionID, filepath)
const cfg = await Config.get()
if (cfg.permission?.edit === "ask")
const agent = await Agent.get(ctx.agent)
if (agent.permission.edit === "ask")
await Permission.ask({
type: "write",
sessionID: ctx.sessionID,
+14
View File
@@ -1,3 +1,5 @@
import { sortBy, pipe } from "remeda"
export namespace Wildcard {
export function match(str: string, pattern: string) {
const regex = new RegExp(
@@ -11,4 +13,16 @@ export namespace Wildcard {
)
return regex.test(str)
}
export function all(input: string, patterns: Record<string, any>) {
const sorted = pipe(patterns, Object.entries, sortBy([([key]) => key.length, "asc"], [([key]) => key, "asc"]))
let result = undefined
for (const [pattern, value] of sorted) {
if (match(input, pattern)) {
result = value
continue
}
}
return result
}
}
+2 -1
View File
@@ -8,6 +8,7 @@ const ctx = {
sessionID: "test",
messageID: "",
toolCallID: "",
agent: "build",
abort: AbortSignal.any([]),
metadata: () => {},
}
@@ -33,7 +34,7 @@ describe("tool.bash", () => {
test("cd ../ should fail outside of project root", async () => {
await App.provide({ cwd: projectRoot }, async () => {
await expect(
expect(
bash.execute(
{
command: "cd ../",
+1
View File
@@ -8,6 +8,7 @@ const ctx = {
sessionID: "test",
messageID: "",
toolCallID: "",
agent: "build",
abort: AbortSignal.any([]),
metadata: () => {},
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/plugin",
"version": "0.4.24",
"version": "0.4.36",
"type": "module",
"scripts": {
"typecheck": "tsc --noEmit"
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/sdk",
"version": "0.4.24",
"version": "0.4.36",
"type": "module",
"scripts": {
"typecheck": "tsc --noEmit"
+25
View File
@@ -912,6 +912,15 @@ export type AgentConfig = {
*/
description?: string
mode?: string
permission?: {
edit?: string
bash?:
| string
| {
[key: string]: string
}
webfetch?: string
}
[key: string]:
| unknown
| string
@@ -921,6 +930,15 @@ export type AgentConfig = {
}
| boolean
| string
| {
edit?: string
bash?:
| string
| {
[key: string]: string
}
webfetch?: string
}
| undefined
}
@@ -1061,6 +1079,13 @@ export type Agent = {
mode: string
topP?: number
temperature?: number
permission: {
edit: string
bash: {
[key: string]: string
}
webfetch?: string
}
model?: {
modelID: string
providerID: string
+13 -9
View File
@@ -45,14 +45,6 @@ func main() {
os.Exit(1)
}
agentsStr := os.Getenv("OPENCODE_AGENTS")
var agents []opencode.Agent
err = json.Unmarshal([]byte(agentsStr), &agents)
if err != nil {
slog.Error("Failed to unmarshal modes", "error", err)
os.Exit(1)
}
stat, err := os.Stdin.Stat()
if err != nil {
slog.Error("Failed to stat stdin", "error", err)
@@ -81,13 +73,25 @@ func main() {
option.WithBaseURL(url),
)
// Fetch agents from the /agent endpoint
agentsPtr, err := httpClient.App.Agents(context.Background())
if err != nil {
slog.Error("Failed to fetch agents", "error", err)
os.Exit(1)
}
if agentsPtr == nil {
slog.Error("No agents returned from server")
os.Exit(1)
}
agents := *agentsPtr
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
apiHandler := util.NewAPILogHandler(ctx, httpClient, "tui", slog.LevelDebug)
logger := slog.New(apiHandler)
slog.SetDefault(logger)
slog.Debug("TUI launched", "app", appInfoStr, "modes", agentsStr, "url", url)
slog.Debug("TUI launched", "app", appInfoStr, "agents_count", len(agents), "url", url)
go func() {
err = clipboard.Init()
@@ -31,6 +31,7 @@ type EditorComponent interface {
tea.Model
tea.ViewModel
Content() string
Cursor() *tea.Cursor
Lines() int
Value() string
Length() int
@@ -407,6 +408,10 @@ func (m *editorComponent) Content() string {
return content
}
func (m *editorComponent) Cursor() *tea.Cursor {
return m.textarea.Cursor()
}
func (m *editorComponent) View() string {
width := m.width
if m.app.Session.ID == "" {
@@ -694,6 +699,7 @@ func NewEditorComponent(app *app.App) EditorComponent {
ta.Prompt = " "
ta.ShowLineNumbers = false
ta.CharLimit = -1
ta.VirtualCursor = false
ta = updateTextareaStyles(ta)
m := &editorComponent{
@@ -325,7 +325,9 @@ func renderText(
// wrap styled text
styledText := result.String()
wrappedText := ansi.WordwrapWc(styledText, width-6, " -")
styledText = strings.ReplaceAll(styledText, "-", "\u2011")
wrappedText := ansi.WordwrapWc(styledText, width-6, " ")
wrappedText = strings.ReplaceAll(wrappedText, "\u2011", "-")
content = base.Width(width - 6).Render(wrappedText)
}
@@ -83,10 +83,10 @@ func (c *commandsComponent) View() string {
}
commandsToShow = append(commandsToShow,
// empty line
commands.Command{
Name: "",
Description: "",
},
// commands.Command{
// Name: "",
// Description: "",
// },
commands.Command{
Name: commands.CommandName(util.Ide()),
Description: "open opencode",
@@ -2045,6 +2045,109 @@ func itemWidth(item any) int {
return 0
}
// forceWrapAttachment splits an attachment's display text across multiple lines
func forceWrapAttachment(att *attachment.Attachment, width int) [][]any {
if width <= 0 {
return [][]any{{att}}
}
display := att.Display
displayRunes := []rune(display)
if len(displayRunes) <= width {
return [][]any{{att}}
}
var lines [][]any
start := 0
for start < len(displayRunes) {
// Calculate how many runes fit in this line
end := start + width
if end > len(displayRunes) {
end = len(displayRunes)
}
// Create a wrapped attachment for this segment
wrappedAtt := &attachment.Attachment{
ID: att.ID,
Type: att.Type,
Display: string(displayRunes[start:end]),
URL: att.URL,
Filename: att.Filename,
MediaType: att.MediaType,
Source: att.Source,
}
lines = append(lines, []any{wrappedAtt})
start = end
}
return lines
}
// forceWrapWord splits a word that's too long to fit within the given width
func forceWrapWord(word []any, width int) [][]any {
if width <= 0 || len(word) == 0 {
return [][]any{word}
}
var lines [][]any
currentLine := []any{}
currentWidth := 0
for _, item := range word {
if att, ok := item.(*attachment.Attachment); ok {
// Handle attachment that might be too wide
attWidth := uniseg.StringWidth(att.Display)
// If the attachment display is too wide, split it
if attWidth > width {
// Finish current line if it has content
if len(currentLine) > 0 {
lines = append(lines, currentLine)
currentLine = []any{}
currentWidth = 0
}
// Split the attachment display across multiple lines
wrappedAttachment := forceWrapAttachment(att, width)
lines = append(lines, wrappedAttachment...)
continue
}
// If adding this attachment would exceed the width, start a new line
if currentWidth+attWidth > width && len(currentLine) > 0 {
lines = append(lines, currentLine)
currentLine = []any{}
currentWidth = 0
}
currentLine = append(currentLine, item)
currentWidth += attWidth
} else if r, ok := item.(rune); ok {
itemWidth := rw.RuneWidth(r)
// If adding this rune would exceed the width, start a new line
if currentWidth+itemWidth > width && len(currentLine) > 0 {
lines = append(lines, currentLine)
currentLine = []any{}
currentWidth = 0
}
currentLine = append(currentLine, item)
currentWidth += itemWidth
}
}
// Add the last line if it has content
if len(currentLine) > 0 {
lines = append(lines, currentLine)
}
return lines
}
func wrapInterfaces(content []any, width int) [][]any {
if width <= 0 {
return [][]any{content}
@@ -2076,11 +2179,49 @@ func wrapInterfaces(content []any, width int) [][]any {
if !inSpaces {
// End of a word
if lineW > 0 && lineW+wordW > width {
lines = append(lines, word)
lineW = wordW
// If the word itself is too long to fit on a line, force-wrap it
if wordW > width {
wrappedLines := forceWrapWord(word, width)
lines = append(lines, wrappedLines...)
// Calculate width of the last wrapped line
lastLine := wrappedLines[len(wrappedLines)-1]
lineW = 0
for _, item := range lastLine {
if r, ok := item.(rune); ok {
lineW += rw.RuneWidth(r)
} else if att, ok := item.(*attachment.Attachment); ok {
lineW += uniseg.StringWidth(att.Display)
}
}
} else {
lines = append(lines, word)
lineW = wordW
}
} else {
lines[len(lines)-1] = append(lines[len(lines)-1], word...)
lineW += wordW
// Check if the word needs to be force-wrapped even when it fits on the current line
if wordW > width {
currentLine := lines[len(lines)-1]
wrappedWord := forceWrapWord(word, width-lineW)
if len(wrappedWord) > 0 {
lines[len(lines)-1] = append(currentLine, wrappedWord[0]...)
for i := 1; i < len(wrappedWord); i++ {
lines = append(lines, wrappedWord[i])
}
// Calculate width of the last wrapped line
lastLine := wrappedWord[len(wrappedWord)-1]
lineW = 0
for _, item := range lastLine {
if r, ok := item.(rune); ok {
lineW += rw.RuneWidth(r)
} else if att, ok := item.(*attachment.Attachment); ok {
lineW += uniseg.StringWidth(att.Display)
}
}
}
} else {
lines[len(lines)-1] = append(lines[len(lines)-1], word...)
lineW += wordW
}
}
word = nil
wordW = 0
@@ -2110,11 +2251,49 @@ func wrapInterfaces(content []any, width int) [][]any {
// Handle any remaining word/spaces at the end of the content.
if wordW > 0 {
if lineW > 0 && lineW+wordW > width {
lines = append(lines, word)
lineW = wordW
// If the word itself is too long to fit on a line, force-wrap it
if wordW > width {
wrappedLines := forceWrapWord(word, width)
lines = append(lines, wrappedLines...)
// Calculate width of the last wrapped line
lastLine := wrappedLines[len(wrappedLines)-1]
lineW = 0
for _, item := range lastLine {
if r, ok := item.(rune); ok {
lineW += rw.RuneWidth(r)
} else if att, ok := item.(*attachment.Attachment); ok {
lineW += uniseg.StringWidth(att.Display)
}
}
} else {
lines = append(lines, word)
lineW = wordW
}
} else {
lines[len(lines)-1] = append(lines[len(lines)-1], word...)
lineW += wordW
// Check if the word needs to be force-wrapped even when it fits on the current line
if wordW > width {
currentLine := lines[len(lines)-1]
wrappedWord := forceWrapWord(word, width-lineW)
if len(wrappedWord) > 0 {
lines[len(lines)-1] = append(currentLine, wrappedWord[0]...)
for i := 1; i < len(wrappedWord); i++ {
lines = append(lines, wrappedWord[i])
}
// Calculate width of the last wrapped line
lastLine := wrappedWord[len(wrappedWord)-1]
lineW = 0
for _, item := range lastLine {
if r, ok := item.(rune); ok {
lineW += rw.RuneWidth(r)
} else if att, ok := item.(*attachment.Attachment); ok {
lineW += uniseg.StringWidth(att.Display)
}
}
}
} else {
lines[len(lines)-1] = append(lines[len(lines)-1], word...)
lineW += wordW
}
}
}
if spaceW > 0 {
+35 -13
View File
@@ -59,6 +59,8 @@ const interruptDebounceTimeout = 1 * time.Second
const exitDebounceTimeout = 1 * time.Second
type Model struct {
tea.Model
tea.CursorModel
width, height int
app *app.App
modal layout.Modal
@@ -748,15 +750,17 @@ func (a Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return a, tea.Batch(cmds...)
}
func (a Model) View() string {
func (a Model) View() (string, *tea.Cursor) {
t := theme.CurrentTheme()
var mainLayout string
var editorX int
var editorY int
if a.app.Session.ID == "" {
mainLayout = a.home()
mainLayout, editorX, editorY = a.home()
} else {
mainLayout = a.chat()
mainLayout, editorX, editorY = a.chat()
}
mainLayout = styles.NewStyle().
Background(t.Background()).
@@ -780,7 +784,12 @@ func (a Model) View() string {
if theme.CurrentThemeUsesAnsiColors() {
mainLayout = util.ConvertRGBToAnsi16Colors(mainLayout)
}
return mainLayout + "\n" + a.status.View()
cursor := a.editor.Cursor()
cursor.Position.X += editorX
cursor.Position.Y += editorY
return mainLayout + "\n" + a.status.View(), cursor
}
func (a Model) Cleanup() {
@@ -807,7 +816,7 @@ func (a Model) openFile(filepath string) (tea.Model, tea.Cmd) {
return a, cmd
}
func (a Model) home() string {
func (a Model) home() (string, int, int) {
t := theme.CurrentTheme()
effectiveWidth := a.width - 4
baseStyle := styles.NewStyle().Background(t.Background())
@@ -899,14 +908,21 @@ func (a Model) home() string {
styles.WhitespaceStyle(t.Background()),
)
editorX := (effectiveWidth - editorWidth) / 2
editorX := max(0, (effectiveWidth-editorWidth)/2)
editorY := (a.height / 2) + (mainHeight / 2) - 2
if editorLines > 1 {
content := a.editor.Content()
editorHeight := lipgloss.Height(content)
if editorY+editorHeight > a.height {
difference := (editorY + editorHeight) - a.height
editorY -= difference
}
mainLayout = layout.PlaceOverlay(
editorX,
editorY,
a.editor.Content(),
content,
mainLayout,
)
}
@@ -924,10 +940,10 @@ func (a Model) home() string {
)
}
return mainLayout
return mainLayout, editorX + 5, editorY + 2
}
func (a Model) chat() string {
func (a Model) chat() (string, int, int) {
effectiveWidth := a.width - 4
t := theme.CurrentTheme()
editorView := a.editor.View()
@@ -944,14 +960,20 @@ func (a Model) chat() string {
)
mainLayout := messagesView + "\n" + editorView
editorX := (effectiveWidth - editorWidth) / 2
editorX := max(0, (effectiveWidth-editorWidth)/2)
editorY := a.height - editorHeight
if lines > 1 {
editorY := a.height - editorHeight
content := a.editor.Content()
editorHeight := lipgloss.Height(content)
if editorY+editorHeight > a.height {
difference := (editorY + editorHeight) - a.height
editorY -= difference
}
mainLayout = layout.PlaceOverlay(
editorX,
editorY,
a.editor.Content(),
content,
mainLayout,
)
}
@@ -970,7 +992,7 @@ func (a Model) chat() string {
)
}
return mainLayout
return mainLayout, editorX + 5, editorY + 2
}
func (a Model) executeCommand(command commands.Command) (tea.Model, tea.Cmd) {
+4
View File
@@ -3,6 +3,7 @@ package util
import (
"fmt"
"path/filepath"
"regexp"
"strings"
"unicode"
@@ -85,6 +86,8 @@ func Extension(path string) string {
func ToMarkdown(content string, width int, backgroundColor compat.AdaptiveColor) string {
r := styles.GetMarkdownRenderer(width-6, backgroundColor)
content = strings.ReplaceAll(content, RootPath+"/", "")
hyphenRegex := regexp.MustCompile(`-([^ ]|$)`)
content = hyphenRegex.ReplaceAllString(content, "\u2011$1")
rendered, _ := r.Render(content)
lines := strings.Split(rendered, "\n")
@@ -105,5 +108,6 @@ func ToMarkdown(content string, width int, backgroundColor compat.AdaptiveColor)
}
}
content = strings.Join(lines, "\n")
content = strings.ReplaceAll(content, "\u2011", "-")
return strings.TrimSuffix(content, "\n")
}
+4 -3
View File
@@ -6,11 +6,11 @@ import (
)
var SUPPORTED_IDES = []struct {
Search string
Search string
ShortName string
}{
{"Windsurf", "Windsurf"},
{"Visual Studio Code", "VS Code"},
{"Visual Studio Code", "vscode"},
{"Cursor", "Cursor"},
{"VSCodium", "VSCodium"},
}
@@ -27,4 +27,5 @@ func Ide() string {
}
return "unknown"
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@opencode/web",
"type": "module",
"version": "0.4.24",
"version": "0.4.36",
"scripts": {
"dev": "astro dev",
"dev:remote": "sst shell --stage=dev --target=Web astro dev",
+158 -1
View File
@@ -318,6 +318,22 @@ Control which tools are available in this agent with the `tools` config. You can
}
```
You can also use wildcards to control multiple tools at once. For example, to disable all tools from an MCP server:
```json title="opencode.json"
{
"agent": {
"readonly": {
"tools": {
"mymcp_*": false,
"write": false,
"edit": false
}
}
}
}
```
If no tools are specified, all tools are enabled by default.
---
@@ -342,6 +358,147 @@ Here are all the tools can be controlled through the agent config.
---
### Permissions
Permissions control what actions an agent can take.
- edit, bash, webfetch
Each permission can be set to allow, ask, or deny.
- allow, ask, deny
Configure permissions globally in opencode.json.
```json title="opencode.json"
{
"$schema": "https://opencode.ai/config.json",
"permission": {
"edit": "ask",
"bash": "allow",
"webfetch": "deny"
}
}
```
You can override permissions per agent in JSON.
```json title="opencode.json" {7-18}
{
"$schema": "https://opencode.ai/config.json",
"agent": {
"build": {
"permission": {
"edit": "allow",
"bash": {
"*": "allow",
"git push": "ask",
"terraform *": "deny"
},
"webfetch": "ask"
}
}
}
}
```
You can also set permissions in Markdown agents.
```markdown title="~/.config/opencode/agent/review.md"
---
description: Code review without edits
mode: subagent
permission:
edit: deny
bash: ask
webfetch: deny
---
Only analyze code and suggest changes.
```
Bash permissions support granular patterns for fine-grained control.
```json title="Allow most, ask for risky, deny terraform"
{
"$schema": "https://opencode.ai/config.json",
"permission": {
"bash": {
"*": "allow",
"git push": "ask",
"terraform *": "deny"
}
}
}
```
If you provide a granular bash map, the default becomes ask unless you set \* explicitly.
```json title="Granular defaults to ask"
{
"$schema": "https://opencode.ai/config.json",
"permission": {
"bash": {
"git status": "allow"
}
}
}
```
Agent-level permissions merge over global settings.
- Global sets defaults; agent overrides when specified
Specific bash rules can override a global default.
```json title="Global ask, agent allows safe commands"
{
"$schema": "https://opencode.ai/config.json",
"permission": { "bash": "ask" },
"agent": {
"build": {
"permission": {
"bash": { "git status": "allow", "*": "ask" }
}
}
}
}
```
Permissions affect tool availability and prompts differently.
- deny hides tools (edit also hides write/patch); ask prompts; allow runs
For quick reference, here are common setups.
```json title="Read-only reviewer"
{
"$schema": "https://opencode.ai/config.json",
"agent": {
"review": {
"permission": { "edit": "deny", "bash": "deny", "webfetch": "allow" }
}
}
}
```
```json title="Planning agent that can browse but cannot change code"
{
"$schema": "https://opencode.ai/config.json",
"agent": {
"plan": {
"permission": { "edit": "deny", "bash": "deny", "webfetch": "ask" }
}
}
}
```
See the full permissions guide for more patterns.
- /docs/permissions
---
### Mode
Control the agent's mode with the `mode` config. The `mode` option is used to determine how the agent can be used.
@@ -371,7 +528,7 @@ For example, with OpenAI's reasoning models, you can control the reasoning effor
"agent": {
"deep-thinker": {
"description": "Agent that uses high reasoning effort for complex problems",
"model": "openai/gpt-5-turbo",
"model": "openai/gpt-5-turbo",
"reasoningEffort": "high",
"textVerbosity": "low"
}
@@ -21,6 +21,8 @@ Permissions are configured in your `opencode.json` file under the `permission` k
| `bash` | Control bash command execution |
| `webfetch` | Control web content fetching |
They can also be configured per agent, see [Agent Configuration](/docs/agents#agent-configuration) for more details.
---
### edit
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "opencode",
"displayName": "opencode",
"description": "opencode for VS Code",
"version": "0.4.24",
"version": "0.4.36",
"publisher": "sst-dev",
"repository": {
"type": "git",