Compare commits

..

2 Commits

Author SHA1 Message Date
opencode 03f0b6dd7a release: v1.0.163 2025-12-16 14:56:33 +00:00
Brendan Allan f7f3ed1fa9 don't make release public until finished 2025-12-16 22:51:31 +08:00
250 changed files with 1117 additions and 2696 deletions
-2
View File
@@ -16,8 +16,6 @@ jobs:
with:
fetch-depth: 1
- uses: ./.github/actions/setup-bun
- name: Install opencode
run: curl -fsSL https://opencode.ai/install | bash
+2 -12
View File
@@ -165,7 +165,6 @@ jobs:
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
RUST_TARGET: ${{ matrix.settings.target }}
GH_TOKEN: ${{ github.token }}
OPENCODE_RELEASE_TAG: ${{ needs.publish.outputs.tagName }}
# Fixes AppImage build issues, can be removed when https://github.com/tauri-apps/tauri/pull/12491 is released
- name: Install tauri-cli from portable appimage branch
@@ -193,25 +192,16 @@ jobs:
projectPath: packages/tauri
uploadWorkflowArtifacts: true
tauriScript: ${{ (contains(matrix.settings.host, 'ubuntu') && 'cargo tauri') || '' }}
args: --target ${{ matrix.settings.target }} --config src-tauri/tauri.prod.conf.json
args: --target ${{ matrix.settings.target }}
updaterJsonPreferNsis: true
releaseId: ${{ needs.publish.outputs.releaseId }}
tagName: ${{ needs.publish.outputs.tagName }}
releaseAssetNamePattern: opencode-desktop-[platform]-[arch][ext]
releaseDraft: true
publish-release:
needs:
- publish
- publish-tauri
if: needs.publish.outputs.tagName
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
ref: ${{ needs.publish.outputs.tagName }}
- run: gh release edit ${{ needs.publish.outputs.tagName }} --draft=false
env:
GH_TOKEN: ${{ github.token }}
- run: gh release edit ${{ steps.publish.outputs.tagName }} --draft=false
@@ -1,29 +0,0 @@
name: release-github-action
on:
push:
branches:
- dev
paths:
- "github/**"
concurrency: ${{ github.workflow }}-${{ github.ref }}
permissions:
contents: write
jobs:
release:
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- run: git fetch --force --tags
- name: Release
run: |
git config --global user.email "opencode@sst.dev"
git config --global user.name "opencode"
./github/script/release
-2
View File
@@ -29,8 +29,6 @@ jobs:
with:
fetch-depth: 1
- uses: ./.github/actions/setup-bun
- name: Install opencode
run: curl -fsSL https://opencode.ai/install | bash
-3
View File
@@ -16,9 +16,6 @@ jobs:
with:
fetch-depth: 1
- name: Setup Bun
uses: ./.github/actions/setup-bun
- name: Install opencode
run: curl -fsSL https://opencode.ai/install | bash
+1 -68
View File
@@ -1,7 +1,7 @@
---
mode: primary
hidden: true
model: opencode/claude-haiku-4-5
model: opencode/gpt-5-nano
tools:
"*": false
"github-triage": true
@@ -10,70 +10,3 @@ tools:
You are a triage agent responsible for triaging github issues.
Use your github-triage tool to triage issues.
## Labels
### windows
Use for any issue that is mentions windows (the OS). Be sure they are saying that they are on windows.
- Use if they mention WSL too
#### perf
Performance-related issues:
- Slow performance
- High RAM usage
- High CPU usage
**Only** add if it's likely a RAM or CPU issue. **Do not** add for LLM slowness.
#### desktop
Desktop app issues:
- `opencode web` command
- The desktop app itself
**Only** add if it's specifically about the Desktop application or `opencode web` view. **Do not** add for terminal, TUI, or general opencode issues.
#### nix
**Only** add if the issue explicitly mentions nix.
#### zen
**Only** add if the issue mentions "zen" or "opencode zen". Zen is our gateway for coding models. **Do not** add for other gateways or inference providers.
If the issue doesn't have "zen" in it then don't add zen label
#### docs
Add if the issue requests better documentation or docs updates.
#### opentui
TUI issues potentially caused by our underlying TUI library:
- Keybindings not working
- Scroll speed issues (too fast/slow/laggy)
- Screen flickering
- Crashes with opentui in the log
**Do not** add for general TUI bugs.
---
When assigning to people here are the following rules:
adamdotdev:
ONLY assign adam if the issue will have the "desktop" label.
fwang:
ONLY assign fwang if the issue will have the "zen" label.
jayair:
ONLY assign jayair if the issue will have the "docs" label.
In all other cases use best judgment. Avoid assigning to kommander needlessly, when in doubt assign to rekram1-node.
+49
View File
@@ -0,0 +1,49 @@
{
"lockfileVersion": 1,
"configVersion": 0,
"workspaces": {
"": {
"dependencies": {
"@octokit/rest": "^22.0.1",
"@opencode-ai/plugin": "1.0.143",
},
},
},
"packages": {
"@octokit/auth-token": ["@octokit/auth-token@6.0.0", "", {}, "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w=="],
"@octokit/core": ["@octokit/core@7.0.6", "", { "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.3", "@octokit/request": "^10.0.6", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "before-after-hook": "^4.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q=="],
"@octokit/endpoint": ["@octokit/endpoint@11.0.2", "", { "dependencies": { "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-4zCpzP1fWc7QlqunZ5bSEjxc6yLAlRTnDwKtgXfcI/FxxGoqedDG8V2+xJ60bV2kODqcGB+nATdtap/XYq2NZQ=="],
"@octokit/graphql": ["@octokit/graphql@9.0.3", "", { "dependencies": { "@octokit/request": "^10.0.6", "@octokit/types": "^16.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA=="],
"@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="],
"@octokit/plugin-paginate-rest": ["@octokit/plugin-paginate-rest@14.0.0", "", { "dependencies": { "@octokit/types": "^16.0.0" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-fNVRE7ufJiAA3XUrha2omTA39M6IXIc6GIZLvlbsm8QOQCYvpq/LkMNGyFlB1d8hTDzsAXa3OKtybdMAYsV/fw=="],
"@octokit/plugin-request-log": ["@octokit/plugin-request-log@6.0.0", "", { "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q=="],
"@octokit/plugin-rest-endpoint-methods": ["@octokit/plugin-rest-endpoint-methods@17.0.0", "", { "dependencies": { "@octokit/types": "^16.0.0" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-B5yCyIlOJFPqUUeiD0cnBJwWJO8lkJs5d8+ze9QDP6SvfiXSz1BF+91+0MeI1d2yxgOhU/O+CvtiZ9jSkHhFAw=="],
"@octokit/request": ["@octokit/request@10.0.7", "", { "dependencies": { "@octokit/endpoint": "^11.0.2", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "fast-content-type-parse": "^3.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-v93h0i1yu4idj8qFPZwjehoJx4j3Ntn+JhXsdJrG9pYaX6j/XRz2RmasMUHtNgQD39nrv/VwTWSqK0RNXR8upA=="],
"@octokit/request-error": ["@octokit/request-error@7.1.0", "", { "dependencies": { "@octokit/types": "^16.0.0" } }, "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw=="],
"@octokit/rest": ["@octokit/rest@22.0.1", "", { "dependencies": { "@octokit/core": "^7.0.6", "@octokit/plugin-paginate-rest": "^14.0.0", "@octokit/plugin-request-log": "^6.0.0", "@octokit/plugin-rest-endpoint-methods": "^17.0.0" } }, "sha512-Jzbhzl3CEexhnivb1iQ0KJ7s5vvjMWcmRtq5aUsKmKDrRW6z3r84ngmiFKFvpZjpiU/9/S6ITPFRpn5s/3uQJw=="],
"@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="],
"@opencode-ai/plugin": ["@opencode-ai/plugin@1.0.143", "", { "dependencies": { "@opencode-ai/sdk": "1.0.143", "zod": "4.1.8" } }, "sha512-yzaCmdazVJMDADJLbMM8KGp1X+Hd/HVyIXMlNt9qcvz/fcs/ET4EwHJsJaQi/9m/jLJ+plwBJAeIW08BMrECPg=="],
"@opencode-ai/sdk": ["@opencode-ai/sdk@1.0.143", "", {}, "sha512-dtmkBfJ7IIAHzL6KCzAlwc9GybfJONVeCsF6ePYySpkuhslDbRkZBJYb5vqGd1H5zdsgjc6JjuvmOf0rPWUL6A=="],
"before-after-hook": ["before-after-hook@4.0.0", "", {}, "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ=="],
"fast-content-type-parse": ["fast-content-type-parse@3.0.0", "", {}, "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg=="],
"universal-user-agent": ["universal-user-agent@7.0.3", "", {}, "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A=="],
"zod": ["zod@4.1.8", "", {}, "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ=="],
}
}
-1
View File
@@ -1,7 +1,6 @@
---
description: git commit and push
model: opencode/glm-4.6
subtask: true
---
commit and push
+6
View File
@@ -0,0 +1,6 @@
{
"dependencies": {
"@octokit/rest": "^22.0.1",
"@opencode-ai/plugin": "1.0.143"
}
}
+4 -19
View File
@@ -1,4 +1,3 @@
/// <reference path="../env.d.ts" />
import { Octokit } from "@octokit/rest"
import { tool } from "@opencode-ai/plugin"
import DESCRIPTION from "./github-triage.txt"
@@ -17,9 +16,9 @@ export default tool({
.describe("The username of the assignee")
.default("rekram1-node"),
labels: tool.schema
.array(tool.schema.enum(["nix", "opentui", "perf", "desktop", "zen", "docs", "windows"]))
.array(tool.schema.enum(["nix", "opentui", "perf", "web", "zen", "docs"]))
.describe("The labels(s) to add to the issue")
.default([]),
.optional(),
},
async execute(args) {
const issue = getIssueNumber()
@@ -29,18 +28,6 @@ export default tool({
const results: string[] = []
if (args.assignee === "adamdotdevin" && !args.labels.includes("desktop")) {
throw new Error("Only desktop issues should be assigned to adamdotdevin")
}
if (args.assignee === "fwang" && !args.labels.includes("zen")) {
throw new Error("Only zen issues should be assigned to fwang")
}
if (args.assignee === "kommander" && !args.labels.includes("opentui")) {
throw new Error("Only opentui issues should be assigned to kommander")
}
await octokit.rest.issues.addAssignees({
owner,
repo,
@@ -49,14 +36,12 @@ export default tool({
})
results.push(`Assigned @${args.assignee} to issue #${issue}`)
const labels: string[] = args.labels.map((label) => (label === "desktop" ? "web" : label))
if (labels.length > 0) {
if (args.labels && args.labels.length > 0) {
await octokit.rest.issues.addLabels({
owner,
repo,
issue_number: issue,
labels,
labels: args.labels,
})
results.push(`Added labels: ${args.labels.join(", ")}`)
}
+2 -10
View File
@@ -50,13 +50,9 @@ Then assign the issue to Him.
### rekram1-node
ALL BUGS SHOULD BE assigned to rekram1-node unless they have the `opentui` label.
Assign Aiden to an issue as a catch all, if you can't assign anyone else. Most of the time this will be bugs/polish things.
If no one else makes sense to assign, assign rekram1-node to it.
Always assign to aiden if the issue mentions "acp", "zed", or model performance issues
## Breakdown of Labels:
### nix
@@ -71,9 +67,9 @@ Anything relating to the TUI itself should have an opentui label
Anything related to slow performance, high ram, high cpu usage, or any other performance related issue should have a perf label
### desktop
### web
Anything related to `opencode web` command or the desktop app should have a desktop label. Never add this label for anything terminal/tui related
Anything related to `opencode web` or the desktop app should have a web label. Never add this label for anything terminal/tui related
### zen
@@ -82,7 +78,3 @@ Anything related to OpenCode Zen, billing, or model quality from Zen should have
### docs
Anything related to the documentation should have a docs label
### windows
Use for any issue that involves the windows OS
-16
View File
@@ -37,22 +37,6 @@ nix run nixpkgs#opencode # or github:sst/opencode for latest dev branc
> [!TIP]
> Remove versions older than 0.1.x before installing.
### Desktop App (BETA)
OpenCode is also available as a desktop application. Download directly from the [releases page](https://github.com/sst/opencode/releases) or [opencode.ai/download](https://opencode.ai/download).
| Platform | Download |
| --------------------- | ------------------------------------- |
| macOS (Apple Silicon) | `opencode-desktop-darwin-aarch64.dmg` |
| macOS (Intel) | `opencode-desktop-darwin-x64.dmg` |
| Windows | `opencode-desktop-windows-x64.exe` |
| Linux | `.deb`, `.rpm`, or AppImage |
```bash
# macOS (Homebrew)
brew install --cask opencode-desktop
```
#### Installation Directory
The install script respects the following priority order for the installation path:
-1
View File
@@ -172,4 +172,3 @@
| 2025-12-14 | 1,082,042 (+8,481) | 1,052,425 (+7,817) | 2,134,467 (+16,298) |
| 2025-12-15 | 1,093,632 (+11,590) | 1,059,078 (+6,653) | 2,152,710 (+18,243) |
| 2025-12-16 | 1,120,477 (+26,845) | 1,078,022 (+18,944) | 2,198,499 (+45,789) |
| 2025-12-17 | 1,151,067 (+30,590) | 1,097,661 (+19,639) | 2,248,728 (+50,229) |
+55 -64
View File
@@ -6,7 +6,6 @@
"name": "opencode",
"dependencies": {
"@aws-sdk/client-s3": "3.933.0",
"@octokit/rest": "22.0.1",
"@opencode-ai/script": "workspace:*",
"@opencode-ai/sdk": "workspace:*",
"typescript": "catalog:",
@@ -21,7 +20,7 @@
},
"packages/console/app": {
"name": "@opencode-ai/console-app",
"version": "1.0.165",
"version": "1.0.163",
"dependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@ibm/plex": "6.4.1",
@@ -49,7 +48,7 @@
},
"packages/console/core": {
"name": "@opencode-ai/console-core",
"version": "1.0.165",
"version": "1.0.163",
"dependencies": {
"@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1",
@@ -76,7 +75,7 @@
},
"packages/console/function": {
"name": "@opencode-ai/console-function",
"version": "1.0.165",
"version": "1.0.163",
"dependencies": {
"@ai-sdk/anthropic": "2.0.0",
"@ai-sdk/openai": "2.0.2",
@@ -100,7 +99,7 @@
},
"packages/console/mail": {
"name": "@opencode-ai/console-mail",
"version": "1.0.165",
"version": "1.0.163",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
@@ -124,7 +123,7 @@
},
"packages/desktop": {
"name": "@opencode-ai/desktop",
"version": "1.0.165",
"version": "1.0.163",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -137,7 +136,7 @@
"@solid-primitives/media": "2.3.3",
"@solid-primitives/resize-observer": "2.1.3",
"@solid-primitives/scroll": "2.1.3",
"@solid-primitives/storage": "catalog:",
"@solid-primitives/storage": "4.3.3",
"@solid-primitives/websocket": "1.3.1",
"@solidjs/meta": "catalog:",
"@solidjs/router": "catalog:",
@@ -171,7 +170,7 @@
},
"packages/enterprise": {
"name": "@opencode-ai/enterprise",
"version": "1.0.165",
"version": "1.0.163",
"dependencies": {
"@opencode-ai/ui": "workspace:*",
"@opencode-ai/util": "workspace:*",
@@ -200,7 +199,7 @@
},
"packages/function": {
"name": "@opencode-ai/function",
"version": "1.0.165",
"version": "1.0.163",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "catalog:",
@@ -216,7 +215,7 @@
},
"packages/opencode": {
"name": "opencode",
"version": "1.0.165",
"version": "1.0.163",
"bin": {
"opencode": "./bin/opencode",
},
@@ -246,8 +245,8 @@
"@opencode-ai/sdk": "workspace:*",
"@opencode-ai/util": "workspace:*",
"@openrouter/ai-sdk-provider": "1.5.2",
"@opentui/core": "0.1.61",
"@opentui/solid": "0.1.61",
"@opentui/core": "0.0.0-20251211-4403a69a",
"@opentui/solid": "0.0.0-20251211-4403a69a",
"@parcel/watcher": "2.5.1",
"@pierre/diffs": "catalog:",
"@solid-primitives/event-bus": "1.1.2",
@@ -308,7 +307,7 @@
},
"packages/plugin": {
"name": "@opencode-ai/plugin",
"version": "1.0.165",
"version": "1.0.163",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"zod": "catalog:",
@@ -328,7 +327,7 @@
},
"packages/sdk/js": {
"name": "@opencode-ai/sdk",
"version": "1.0.165",
"version": "1.0.163",
"devDependencies": {
"@hey-api/openapi-ts": "0.88.1",
"@tsconfig/node22": "catalog:",
@@ -339,7 +338,7 @@
},
"packages/slack": {
"name": "@opencode-ai/slack",
"version": "1.0.165",
"version": "1.0.163",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1",
@@ -352,10 +351,9 @@
},
"packages/tauri": {
"name": "@opencode-ai/tauri",
"version": "1.0.165",
"version": "1.0.163",
"dependencies": {
"@opencode-ai/desktop": "workspace:*",
"@solid-primitives/storage": "catalog:",
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-dialog": "~2",
"@tauri-apps/plugin-opener": "^2",
@@ -378,7 +376,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
"version": "1.0.165",
"version": "1.0.163",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -413,7 +411,7 @@
},
"packages/util": {
"name": "@opencode-ai/util",
"version": "1.0.165",
"version": "1.0.163",
"dependencies": {
"zod": "catalog:",
},
@@ -424,7 +422,7 @@
},
"packages/web": {
"name": "@opencode-ai/web",
"version": "1.0.165",
"version": "1.0.163",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",
@@ -475,7 +473,6 @@
"@octokit/rest": "22.0.0",
"@openauthjs/openauth": "0.0.0-20250322224806",
"@pierre/diffs": "1.0.0-beta.3",
"@solid-primitives/storage": "4.3.3",
"@solidjs/meta": "0.29.4",
"@solidjs/router": "0.15.4",
"@solidjs/start": "https://pkg.pr.new/@solidjs/start@dfb2020",
@@ -1100,11 +1097,11 @@
"@octokit/openapi-types": ["@octokit/openapi-types@25.1.0", "", {}, "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA=="],
"@octokit/plugin-paginate-rest": ["@octokit/plugin-paginate-rest@14.0.0", "", { "dependencies": { "@octokit/types": "^16.0.0" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-fNVRE7ufJiAA3XUrha2omTA39M6IXIc6GIZLvlbsm8QOQCYvpq/LkMNGyFlB1d8hTDzsAXa3OKtybdMAYsV/fw=="],
"@octokit/plugin-paginate-rest": ["@octokit/plugin-paginate-rest@13.2.1", "", { "dependencies": { "@octokit/types": "^15.0.1" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-Tj4PkZyIL6eBMYcG/76QGsedF0+dWVeLhYprTmuFVVxzDW7PQh23tM0TP0z+1MvSkxB29YFZwnUX+cXfTiSdyw=="],
"@octokit/plugin-request-log": ["@octokit/plugin-request-log@6.0.0", "", { "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q=="],
"@octokit/plugin-rest-endpoint-methods": ["@octokit/plugin-rest-endpoint-methods@17.0.0", "", { "dependencies": { "@octokit/types": "^16.0.0" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-B5yCyIlOJFPqUUeiD0cnBJwWJO8lkJs5d8+ze9QDP6SvfiXSz1BF+91+0MeI1d2yxgOhU/O+CvtiZ9jSkHhFAw=="],
"@octokit/plugin-rest-endpoint-methods": ["@octokit/plugin-rest-endpoint-methods@16.1.1", "", { "dependencies": { "@octokit/types": "^15.0.1" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-VztDkhM0ketQYSh5Im3IcKWFZl7VIrrsCaHbDINkdYeiiAsJzjhS2xRFCSJgfN6VOcsoW4laMtsmf3HcNqIimg=="],
"@octokit/plugin-retry": ["@octokit/plugin-retry@3.0.9", "", { "dependencies": { "@octokit/types": "^6.0.3", "bottleneck": "^2.15.3" } }, "sha512-r+fArdP5+TG6l1Rv/C9hVoty6tldw6cE2pRHNGmFPdyfrc696R6JjrQ3d7HdVqGwuzfyrcaLAKD7K8TX8aehUQ=="],
@@ -1112,7 +1109,7 @@
"@octokit/request-error": ["@octokit/request-error@7.1.0", "", { "dependencies": { "@octokit/types": "^16.0.0" } }, "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw=="],
"@octokit/rest": ["@octokit/rest@22.0.1", "", { "dependencies": { "@octokit/core": "^7.0.6", "@octokit/plugin-paginate-rest": "^14.0.0", "@octokit/plugin-request-log": "^6.0.0", "@octokit/plugin-rest-endpoint-methods": "^17.0.0" } }, "sha512-Jzbhzl3CEexhnivb1iQ0KJ7s5vvjMWcmRtq5aUsKmKDrRW6z3r84ngmiFKFvpZjpiU/9/S6ITPFRpn5s/3uQJw=="],
"@octokit/rest": ["@octokit/rest@22.0.0", "", { "dependencies": { "@octokit/core": "^7.0.2", "@octokit/plugin-paginate-rest": "^13.0.1", "@octokit/plugin-request-log": "^6.0.0", "@octokit/plugin-rest-endpoint-methods": "^16.0.0" } }, "sha512-z6tmTu9BTnw51jYGulxrlernpsQYXpui1RK21vmXn8yF5bp6iX16yfTtJYGK5Mh1qDkvDOmp2n8sRMcQmR8jiA=="],
"@octokit/types": ["@octokit/types@14.1.0", "", { "dependencies": { "@octokit/openapi-types": "^25.1.0" } }, "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g=="],
@@ -1160,21 +1157,21 @@
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
"@opentui/core": ["@opentui/core@0.1.61", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.61", "@opentui/core-darwin-x64": "0.1.61", "@opentui/core-linux-arm64": "0.1.61", "@opentui/core-linux-x64": "0.1.61", "@opentui/core-win32-arm64": "0.1.61", "@opentui/core-win32-x64": "0.1.61", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-WrVbdki0tnsgmWCB3Iix6n8eXGXUheTqr/tcnBN7gLA/TqT9udcX+DW3/qRdgtTNJS1sVBVeuwSTYU3eqDSUJQ=="],
"@opentui/core": ["@opentui/core@0.0.0-20251211-4403a69a", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.0.0-20251211-4403a69a", "@opentui/core-darwin-x64": "0.0.0-20251211-4403a69a", "@opentui/core-linux-arm64": "0.0.0-20251211-4403a69a", "@opentui/core-linux-x64": "0.0.0-20251211-4403a69a", "@opentui/core-win32-arm64": "0.0.0-20251211-4403a69a", "@opentui/core-win32-x64": "0.0.0-20251211-4403a69a", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-wTZKcokyU9yiDqyC0Pvf9eRSdT73s4Ynerkit/z8Af++tynqrTlZHZCXK3o42Ff7itCSILmijcTU94n69aEypA=="],
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.61", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zX7EK8PwBJFwsZ2tDnScLFD0GbBfHE7sqpzGDXP2luMnBZJ0OOO95a4Hzu9dQWqxEr4RgfGDT8uIRhgimKNQEg=="],
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.0.0-20251211-4403a69a", "", { "os": "darwin", "cpu": "arm64" }, "sha512-VAYjTa+Eiauy8gETXadD8y0PE6ppnKasDK1X354VoexZiWFR3r7rkL+TfDfk7whhqXDYyT44JDT1QmCAhVXRzQ=="],
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.61", "", { "os": "darwin", "cpu": "x64" }, "sha512-xfvl8EnyN0XwlYpyTskVhHOpbMdgt++ntcuTh7M7IEFYQGzJux19NBwJl17mOxB1McG+KTa7kNx5/zu0VB9eVQ=="],
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.0.0-20251211-4403a69a", "", { "os": "darwin", "cpu": "x64" }, "sha512-n9oVMpsojlILj1soORZzZ2Mjh8Zl73ZNcY7ot0iRmOjBDccrjDTsqKfxoGjKNd/xJSphLeu1LYGlcI5O5OczWQ=="],
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.61", "", { "os": "linux", "cpu": "arm64" }, "sha512-Ghg7j4H6bz7CLxhgDcWx3Ann3AblDIjKFUu4vFrVysuiwfmDHwdKm8awLj8tnmC/0y8juG4ODUQbR0BXBIkE+Q=="],
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.0.0-20251211-4403a69a", "", { "os": "linux", "cpu": "arm64" }, "sha512-vf4eUjPMI4ANitK4MpTGenZFddKgQD/K21aN6cZjusnH3mTEJAoIR7GbNtMdz3qclU43ajpzTID9sAwhshwdVQ=="],
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.61", "", { "os": "linux", "cpu": "x64" }, "sha512-Xs9czMEOuHtnX4tigC4fNb1MU7+Gaohbk+k4teraulIgYZf19nRHIKNvXissDjOfqvOGygCkxMQIG0zeUFsPEA=="],
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.0.0-20251211-4403a69a", "", { "os": "linux", "cpu": "x64" }, "sha512-61635Up0YvVJ8gZ2eMiL1c8OfA+U6wAzT++LoaurNjbmsUAlKHws6MZdqTLw7aspJJVGsRFbA6d1Y+gXFxbDrQ=="],
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.61", "", { "os": "win32", "cpu": "arm64" }, "sha512-2CYAEPqArJqE36LkSRAs0csRzWwVJY99S/7EuY7abBm58BIL6RUw5kSw1r75oDo4I3W6v6WwW0u8B5Ik98m0Kg=="],
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.0.0-20251211-4403a69a", "", { "os": "win32", "cpu": "arm64" }, "sha512-3lUddTJGKZ6uU388eU79MY//IEbgGENCITetDrrRp7v9L1AxMntE1ihf6HniziwBvKKJcsUfqLiJWcq0WPZw2w=="],
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.61", "", { "os": "win32", "cpu": "x64" }, "sha512-c0OK5YwcKH51Qj6wPmwTZP3X8LHA0I0dKz4fO4mOh4f+OqgU9WOG4hpbf7lv0bVlHoTvgP4zDUsjmtIVA8l6Lg=="],
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.0.0-20251211-4403a69a", "", { "os": "win32", "cpu": "x64" }, "sha512-Xwc1gqYsn8UZNTzNKkigZozAhBNBGbfX2B/I/aSbyqL0h8+XIInOodI0urzJWc0B6aEv/IDiT6Rm3coXFikLIg=="],
"@opentui/solid": ["@opentui/solid@0.1.61", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.61", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-CiZHduIoeABoS0ev+eGeHA/LiRl/SpdL6io4jrwiwFi/rToKtc7YgJ8MWxIgeHScHUbpQnIr1v7jzsGI3DAYvw=="],
"@opentui/solid": ["@opentui/solid@0.0.0-20251211-4403a69a", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.0.0-20251211-4403a69a", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-vuLppAdd1Qgaqhie3q2TuEr+8udjT4d8uVg5arvCe1AUDVs19I8kvadVCfzGUVmtXgFIOEakbiv6AxDq5v9Zig=="],
"@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
@@ -4084,9 +4081,9 @@
"@octokit/oauth-methods/@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="],
"@octokit/plugin-paginate-rest/@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="],
"@octokit/plugin-paginate-rest/@octokit/types": ["@octokit/types@15.0.2", "", { "dependencies": { "@octokit/openapi-types": "^26.0.0" } }, "sha512-rR+5VRjhYSer7sC51krfCctQhVTmjyUMAaShfPB8mscVa8tSoLyon3coxQmXu0ahJoLVWl8dSGD/3OGZlFV44Q=="],
"@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="],
"@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@15.0.2", "", { "dependencies": { "@octokit/openapi-types": "^26.0.0" } }, "sha512-rR+5VRjhYSer7sC51krfCctQhVTmjyUMAaShfPB8mscVa8tSoLyon3coxQmXu0ahJoLVWl8dSGD/3OGZlFV44Q=="],
"@octokit/plugin-retry/@octokit/types": ["@octokit/types@6.41.0", "", { "dependencies": { "@octokit/openapi-types": "^12.11.0" } }, "sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg=="],
@@ -4098,8 +4095,6 @@
"@openauthjs/openauth/jose": ["jose@5.9.6", "", {}, "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ=="],
"@opencode-ai/function/@octokit/rest": ["@octokit/rest@22.0.0", "", { "dependencies": { "@octokit/core": "^7.0.2", "@octokit/plugin-paginate-rest": "^13.0.1", "@octokit/plugin-request-log": "^6.0.0", "@octokit/plugin-rest-endpoint-methods": "^16.0.0" } }, "sha512-z6tmTu9BTnw51jYGulxrlernpsQYXpui1RK21vmXn8yF5bp6iX16yfTtJYGK5Mh1qDkvDOmp2n8sRMcQmR8jiA=="],
"@opencode-ai/tauri/typescript": ["typescript@5.6.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw=="],
"@opencode-ai/web/@shikijs/transformers": ["@shikijs/transformers@3.4.2", "", { "dependencies": { "@shikijs/core": "3.4.2", "@shikijs/types": "3.4.2" } }, "sha512-I5baLVi/ynLEOZoWSAMlACHNnG+yw5HDmse0oe+GW6U1u+ULdEB3UHiVWaHoJSSONV7tlcVxuaMy74sREDkSvg=="],
@@ -4292,8 +4287,6 @@
"opencode/@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.27", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.17" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-bpYruxVLhrTbVH6CCq48zMJNeHu6FmHtEedl9FXckEgcIEAi036idFhJlcRwC1jNCwlacbzb8dPD7OAH1EKJaQ=="],
"opencode/@octokit/rest": ["@octokit/rest@22.0.0", "", { "dependencies": { "@octokit/core": "^7.0.2", "@octokit/plugin-paginate-rest": "^13.0.1", "@octokit/plugin-request-log": "^6.0.0", "@octokit/plugin-rest-endpoint-methods": "^16.0.0" } }, "sha512-z6tmTu9BTnw51jYGulxrlernpsQYXpui1RK21vmXn8yF5bp6iX16yfTtJYGK5Mh1qDkvDOmp2n8sRMcQmR8jiA=="],
"opencontrol/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.6.1", "", { "dependencies": { "content-type": "^1.0.5", "cors": "^2.8.5", "eventsource": "^3.0.2", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^4.1.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-oxzMzYCkZHMntzuyerehK3fV6A2Kwh5BD6CGEJSVDU2QNEhfLOptf2X7esQgaHZXHZY0oHmMsOtIDLP71UJXgA=="],
"opencontrol/@tsconfig/bun": ["@tsconfig/bun@1.0.7", "", {}, "sha512-udGrGJBNQdXGVulehc1aWT73wkR9wdaGBtB6yL70RJsqwW/yJhIg6ZbRlPOfIUiFNrnBuYLBi9CSmMKfDC7dvA=="],
@@ -4306,6 +4299,10 @@
"openid-client/jose": ["jose@4.15.9", "", {}, "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA=="],
"opentui-spinner/@opentui/core": ["@opentui/core@0.1.60", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.60", "@opentui/core-darwin-x64": "0.1.60", "@opentui/core-linux-arm64": "0.1.60", "@opentui/core-linux-x64": "0.1.60", "@opentui/core-win32-arm64": "0.1.60", "@opentui/core-win32-x64": "0.1.60", "bun-webgpu": "0.1.4", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-28jphd0AJo48uvEuKXcT9pJhgAu8I2rEJhPt25cc5ipJ2iw/eDk1uoxrbID80MPDqgOEzN21vXmzXwCd6ao+hg=="],
"opentui-spinner/@opentui/solid": ["@opentui/solid@0.1.60", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.60", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-pn91stzAHNGWaNL6h39q55bq3G1/DLqxKtT3wVsRAV68dHfPpwmqikX1nEJZK8OU84ZTPS9Ly9fz8po2Mot2uQ=="],
"p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
"parse-bmfont-xml/xml2js": ["xml2js@0.5.0", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA=="],
@@ -4648,9 +4645,9 @@
"@octokit/oauth-methods/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="],
"@octokit/plugin-paginate-rest/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="],
"@octokit/plugin-paginate-rest/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@26.0.0", "", {}, "sha512-7AtcfKtpo77j7Ts73b4OWhOZHTKo/gGY8bB3bNBQz4H+GRSWqx2yvj8TXRsbdTE0eRmYmXOEY66jM7mJ7LzfsA=="],
"@octokit/plugin-rest-endpoint-methods/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="],
"@octokit/plugin-rest-endpoint-methods/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@26.0.0", "", {}, "sha512-7AtcfKtpo77j7Ts73b4OWhOZHTKo/gGY8bB3bNBQz4H+GRSWqx2yvj8TXRsbdTE0eRmYmXOEY66jM7mJ7LzfsA=="],
"@octokit/plugin-retry/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@12.11.0", "", {}, "sha512-VsXyi8peyRq9PqIz/tpqiL2w3w80OgVMwBHltTml3LmVvXiphgeqmY9mvBw9Wu7e0QWk/fqD37ux8yP5uVekyQ=="],
@@ -4658,10 +4655,6 @@
"@octokit/request/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="],
"@opencode-ai/function/@octokit/rest/@octokit/plugin-paginate-rest": ["@octokit/plugin-paginate-rest@13.2.1", "", { "dependencies": { "@octokit/types": "^15.0.1" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-Tj4PkZyIL6eBMYcG/76QGsedF0+dWVeLhYprTmuFVVxzDW7PQh23tM0TP0z+1MvSkxB29YFZwnUX+cXfTiSdyw=="],
"@opencode-ai/function/@octokit/rest/@octokit/plugin-rest-endpoint-methods": ["@octokit/plugin-rest-endpoint-methods@16.1.1", "", { "dependencies": { "@octokit/types": "^15.0.1" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-VztDkhM0ketQYSh5Im3IcKWFZl7VIrrsCaHbDINkdYeiiAsJzjhS2xRFCSJgfN6VOcsoW4laMtsmf3HcNqIimg=="],
"@opencode-ai/web/@shikijs/transformers/@shikijs/core": ["@shikijs/core@3.4.2", "", { "dependencies": { "@shikijs/types": "3.4.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-AG8vnSi1W2pbgR2B911EfGqtLE9c4hQBYkv/x7Z+Kt0VxhgQKcW7UNDVYsu9YxwV6u+OJrvdJrMq6DNWoBjihQ=="],
"@opencode-ai/web/@shikijs/transformers/@shikijs/types": ["@shikijs/types@3.4.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-zHC1l7L+eQlDXLnxvM9R91Efh2V4+rN3oMVS2swCBssbj2U/FBwybD1eeLaq8yl/iwT+zih8iUbTBCgGZOYlVg=="],
@@ -4876,10 +4869,6 @@
"opencode/@ai-sdk/openai-compatible/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.17", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-TR3Gs4I3Tym4Ll+EPdzRdvo/rc8Js6c4nVhFLuvGLX/Y4V9ZcQMa/HTiYsHEgmYrf1zVi6Q145UEZUfleOwOjw=="],
"opencode/@octokit/rest/@octokit/plugin-paginate-rest": ["@octokit/plugin-paginate-rest@13.2.1", "", { "dependencies": { "@octokit/types": "^15.0.1" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-Tj4PkZyIL6eBMYcG/76QGsedF0+dWVeLhYprTmuFVVxzDW7PQh23tM0TP0z+1MvSkxB29YFZwnUX+cXfTiSdyw=="],
"opencode/@octokit/rest/@octokit/plugin-rest-endpoint-methods": ["@octokit/plugin-rest-endpoint-methods@16.1.1", "", { "dependencies": { "@octokit/types": "^15.0.1" }, "peerDependencies": { "@octokit/core": ">=6" } }, "sha512-VztDkhM0ketQYSh5Im3IcKWFZl7VIrrsCaHbDINkdYeiiAsJzjhS2xRFCSJgfN6VOcsoW4laMtsmf3HcNqIimg=="],
"opencontrol/@modelcontextprotocol/sdk/express": ["express@5.1.0", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="],
"opencontrol/@modelcontextprotocol/sdk/pkce-challenge": ["pkce-challenge@4.1.0", "", {}, "sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ=="],
@@ -4890,6 +4879,22 @@
"opencontrol/@modelcontextprotocol/sdk/zod-to-json-schema": ["zod-to-json-schema@3.24.5", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g=="],
"opentui-spinner/@opentui/core/@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.60", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N4feqnOBDA4O4yocpat5vOiV06HqJVwJGx8rEZE9DiOtl1i+1cPQ1Lx6+zWdLhbrVBJ0ENhb7Azox8sXkm/+5Q=="],
"opentui-spinner/@opentui/core/@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.60", "", { "os": "darwin", "cpu": "x64" }, "sha512-+z3q4WaoIs7ANU8+eTFlvnfCjAS81rk81TOdZm4TJ53Ti3/B+yheWtnV/mLpLLhvZDz2VUVxxRmfDrGMnJb4fQ=="],
"opentui-spinner/@opentui/core/@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.60", "", { "os": "linux", "cpu": "arm64" }, "sha512-/Q65sjqVGB9ygJ6lStI8n1X6RyfmJZC8XofRGEuFiMLiWcWC/xoBtztdL8LAIvHQy42y2+pl9zIiW0fWSQ0wjw=="],
"opentui-spinner/@opentui/core/@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.60", "", { "os": "linux", "cpu": "x64" }, "sha512-AegF+g7OguIpjZKN+PS55sc3ZFY6fj+fLwfETbSRGw6NqX+aiwpae0Y3gXX1s298Yq5yQEzMXnARTCJTGH4uzg=="],
"opentui-spinner/@opentui/core/@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.60", "", { "os": "win32", "cpu": "arm64" }, "sha512-fbkq8MOZJgT3r9q3JWqsfVxRpQ1SlbmhmvB35BzukXnZBK8eA178wbSadGH6irMDrkSIYye9WYddHI/iXjmgVQ=="],
"opentui-spinner/@opentui/core/@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.60", "", { "os": "win32", "cpu": "x64" }, "sha512-OebCL7f9+CKodBw0G+NvKIcc74bl6/sBEHfb73cACdJDJKh+T3C3Vt9H3kQQ0m1C8wRAqX6rh706OArk1pUb2A=="],
"opentui-spinner/@opentui/solid/@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="],
"opentui-spinner/@opentui/solid/babel-preset-solid": ["babel-preset-solid@1.9.9", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.40.1" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^1.9.8" }, "optionalPeers": ["solid-js"] }, "sha512-pCnxWrciluXCeli/dj5PIEHgbNzim3evtTn12snjqqg8QZWJNMjH1AWIp4iG/tbVjqQ72aBEymMSagvmgxubXw=="],
"parse-bmfont-xml/xml2js/sax": ["sax@1.4.3", "", {}, "sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ=="],
"pkg-up/find-up/locate-path": ["locate-path@3.0.0", "", { "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" } }, "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A=="],
@@ -5020,10 +5025,6 @@
"@modelcontextprotocol/sdk/raw-body/http-errors/statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
"@opencode-ai/function/@octokit/rest/@octokit/plugin-paginate-rest/@octokit/types": ["@octokit/types@15.0.2", "", { "dependencies": { "@octokit/openapi-types": "^26.0.0" } }, "sha512-rR+5VRjhYSer7sC51krfCctQhVTmjyUMAaShfPB8mscVa8tSoLyon3coxQmXu0ahJoLVWl8dSGD/3OGZlFV44Q=="],
"@opencode-ai/function/@octokit/rest/@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@15.0.2", "", { "dependencies": { "@octokit/openapi-types": "^26.0.0" } }, "sha512-rR+5VRjhYSer7sC51krfCctQhVTmjyUMAaShfPB8mscVa8tSoLyon3coxQmXu0ahJoLVWl8dSGD/3OGZlFV44Q=="],
"@slack/web-api/form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
"@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es": ["oniguruma-to-es@2.3.0", "", { "dependencies": { "emoji-regex-xs": "^1.0.0", "regex": "^5.1.1", "regex-recursion": "^5.1.1" } }, "sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g=="],
@@ -5044,10 +5045,6 @@
"js-beautify/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
"opencode/@octokit/rest/@octokit/plugin-paginate-rest/@octokit/types": ["@octokit/types@15.0.2", "", { "dependencies": { "@octokit/openapi-types": "^26.0.0" } }, "sha512-rR+5VRjhYSer7sC51krfCctQhVTmjyUMAaShfPB8mscVa8tSoLyon3coxQmXu0ahJoLVWl8dSGD/3OGZlFV44Q=="],
"opencode/@octokit/rest/@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@15.0.2", "", { "dependencies": { "@octokit/openapi-types": "^26.0.0" } }, "sha512-rR+5VRjhYSer7sC51krfCctQhVTmjyUMAaShfPB8mscVa8tSoLyon3coxQmXu0ahJoLVWl8dSGD/3OGZlFV44Q=="],
"opencontrol/@modelcontextprotocol/sdk/express/accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
"opencontrol/@modelcontextprotocol/sdk/express/body-parser": ["body-parser@2.2.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg=="],
@@ -5076,6 +5073,8 @@
"opencontrol/@modelcontextprotocol/sdk/raw-body/http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
"opentui-spinner/@opentui/solid/@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"pkg-up/find-up/locate-path/p-locate": ["p-locate@3.0.0", "", { "dependencies": { "p-limit": "^2.0.0" } }, "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ=="],
"pkg-up/find-up/locate-path/path-exists": ["path-exists@3.0.0", "", {}, "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ=="],
@@ -5140,18 +5139,10 @@
"@jsx-email/cli/tailwindcss/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"@opencode-ai/function/@octokit/rest/@octokit/plugin-paginate-rest/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@26.0.0", "", {}, "sha512-7AtcfKtpo77j7Ts73b4OWhOZHTKo/gGY8bB3bNBQz4H+GRSWqx2yvj8TXRsbdTE0eRmYmXOEY66jM7mJ7LzfsA=="],
"@opencode-ai/function/@octokit/rest/@octokit/plugin-rest-endpoint-methods/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@26.0.0", "", {}, "sha512-7AtcfKtpo77j7Ts73b4OWhOZHTKo/gGY8bB3bNBQz4H+GRSWqx2yvj8TXRsbdTE0eRmYmXOEY66jM7mJ7LzfsA=="],
"@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es/regex": ["regex@5.1.1", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-dN5I359AVGPnwzJm2jN1k0W9LPZ+ePvoOeVMMfqIMFz53sSwXkxaJoxr50ptnsC771lK95BnTrVSZxq0b9yCGw=="],
"@solidjs/start/shiki/@shikijs/engine-javascript/oniguruma-to-es/regex-recursion": ["regex-recursion@5.1.1", "", { "dependencies": { "regex": "^5.1.1", "regex-utilities": "^2.3.0" } }, "sha512-ae7SBCbzVNrIjgSbh7wMznPcQel1DNlDtzensnFxpiNpXt1U2ju/bHugH422r+4LAVS1FpW1YCwilmnNsjum9w=="],
"opencode/@octokit/rest/@octokit/plugin-paginate-rest/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@26.0.0", "", {}, "sha512-7AtcfKtpo77j7Ts73b4OWhOZHTKo/gGY8bB3bNBQz4H+GRSWqx2yvj8TXRsbdTE0eRmYmXOEY66jM7mJ7LzfsA=="],
"opencode/@octokit/rest/@octokit/plugin-rest-endpoint-methods/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@26.0.0", "", {}, "sha512-7AtcfKtpo77j7Ts73b4OWhOZHTKo/gGY8bB3bNBQz4H+GRSWqx2yvj8TXRsbdTE0eRmYmXOEY66jM7mJ7LzfsA=="],
"opencontrol/@modelcontextprotocol/sdk/express/accepts/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
"opencontrol/@modelcontextprotocol/sdk/express/body-parser/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
Generated
+3 -3
View File
@@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1765934234,
"narHash": "sha256-pJjWUzNnjbIAMIc5gRFUuKCDQ9S1cuh3b2hKgA7Mc4A=",
"lastModified": 1765772535,
"narHash": "sha256-aq+dQoaPONOSjtFIBnAXseDm9TUhIbe215TPmkfMYww=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "af84f9d270d404c17699522fab95bbf928a2d92f",
"rev": "09b8fda8959d761445f12b55f380d90375a1d6bb",
"type": "github"
},
"original": {
-5
View File
@@ -22,10 +22,6 @@ inputs:
required: false
default: "false"
mentions:
description: "Comma-separated list of trigger phrases (case-insensitive). Defaults to '/opencode,/oc'"
required: false
runs:
using: "composite"
steps:
@@ -61,4 +57,3 @@ runs:
SHARE: ${{ inputs.share }}
PROMPT: ${{ inputs.prompt }}
USE_GITHUB_TOKEN: ${{ inputs.use_github_token }}
MENTIONS: ${{ inputs.mentions }}
+1 -1
View File
@@ -1,3 +1,3 @@
{
"nodeModules": "sha256-g6XHWk9IoDoeXbvENs+U2fqk185xKMLb0BRopCbXaIk="
"nodeModules": "sha256-PyoVOza+3WnwZbtpPF6uSN1zkyLsSG2VsgBfIMvIFAs="
}
-2
View File
@@ -32,7 +32,6 @@
"@cloudflare/workers-types": "4.20251008.0",
"@openauthjs/openauth": "0.0.0-20250322224806",
"@pierre/diffs": "1.0.0-beta.3",
"@solid-primitives/storage": "4.3.3",
"@tailwindcss/vite": "4.1.11",
"diff": "8.0.2",
"ai": "5.0.97",
@@ -64,7 +63,6 @@
},
"dependencies": {
"@aws-sdk/client-s3": "3.933.0",
"@octokit/rest": "22.0.1",
"@opencode-ai/script": "workspace:*",
"@opencode-ai/sdk": "workspace:*",
"typescript": "catalog:"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-app",
"version": "1.0.165",
"version": "1.0.163",
"type": "module",
"scripts": {
"typecheck": "tsgo --noEmit",
@@ -110,13 +110,10 @@
[data-slot="cta-button"] {
background: var(--color-background-strong);
color: var(--color-text-inverted);
padding: 8px 16px 8px 10px;
padding: 8px 16px;
border-radius: 4px;
font-weight: 500;
text-decoration: none;
display: flex;
align-items: center;
gap: 8px;
@media (max-width: 55rem) {
display: none;
@@ -1,37 +0,0 @@
import { APIEvent } from "@solidjs/start"
import { DownloadPlatform } from "./types"
const assetNames: Record<string, string> = {
"darwin-aarch64-dmg": "opencode-desktop-darwin-aarch64.dmg",
"darwin-x64-dmg": "opencode-desktop-darwin-x64.dmg",
"windows-x64-nsis": "opencode-desktop-windows-x64.exe",
"linux-x64-deb": "opencode-desktop-linux-amd64.deb",
"linux-x64-rpm": "opencode-desktop-linux-x86_64.rpm",
} satisfies Record<DownloadPlatform, string>
// Doing this on the server lets us preserve the original name for platforms we don't care to rename for
const downloadNames: Record<string, string> = {
"darwin-aarch64-dmg": "OpenCode Desktop.dmg",
"darwin-x64-dmg": "OpenCode Desktop.dmg",
"windows-x64-nsis": "OpenCode Desktop Installer.exe",
} satisfies { [K in DownloadPlatform]?: string }
export async function GET({ params: { platform } }: APIEvent) {
const assetName = assetNames[platform]
if (!assetName) return new Response("Not Found", { status: 404 })
const resp = await fetch(`https://github.com/sst/opencode/releases/latest/download/${assetName}`, {
cf: {
// in case gh releases has rate limits
cacheTtl: 60 * 60 * 24,
cacheEverything: true,
},
} as any)
const downloadName = downloadNames[platform]
const headers = new Headers(resp.headers)
if (downloadName) headers.set("content-disposition", `attachment; filename="${downloadName}"`)
return new Response(resp.body, { ...resp, headers })
}
@@ -8,51 +8,6 @@ import { Faq } from "~/component/faq"
import desktopAppIcon from "../../asset/lander/opencode-desktop-icon.png"
import { Legal } from "~/component/legal"
import { config } from "~/config"
import { createSignal, onMount, Show, JSX } from "solid-js"
import { DownloadPlatform } from "./types"
type OS = "macOS" | "Windows" | "Linux" | null
function detectOS(): OS {
if (typeof navigator === "undefined") return null
const platform = navigator.platform.toLowerCase()
const userAgent = navigator.userAgent.toLowerCase()
if (platform.includes("mac") || userAgent.includes("mac")) return "macOS"
if (platform.includes("win") || userAgent.includes("win")) return "Windows"
if (platform.includes("linux") || userAgent.includes("linux")) return "Linux"
return null
}
function getDownloadPlatform(os: OS): DownloadPlatform {
switch (os) {
case "macOS":
return "darwin-aarch64-dmg"
case "Windows":
return "windows-x64-nsis"
case "Linux":
return "linux-x64-deb"
default:
return "darwin-aarch64-dmg"
}
}
function getDownloadHref(platform: DownloadPlatform) {
return `/download/${platform}`
}
function IconDownload(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
return (
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
d="M13.9583 10.6247L10 14.583L6.04167 10.6247M10 2.08301V13.958M16.25 17.9163H3.75"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="square"
/>
</svg>
)
}
function CopyStatus() {
return (
@@ -64,12 +19,7 @@ function CopyStatus() {
}
export default function Download() {
const [detectedOS, setDetectedOS] = createSignal<OS>(null)
onMount(() => {
setDetectedOS(detectOS())
})
const downloadUrl = "https://github.com/sst/opencode/releases/latest/download"
const handleCopyClick = (command: string) => (event: Event) => {
const button = event.currentTarget as HTMLButtonElement
navigator.clipboard.writeText(command)
@@ -94,12 +44,6 @@ export default function Download() {
<div data-component="hero-text">
<h1>Download OpenCode</h1>
<p>Available in Beta for macOS, Windows, and Linux</p>
<Show when={detectedOS()}>
<a href={getDownloadHref(getDownloadPlatform(detectedOS()))} data-component="download-button">
<IconDownload />
Download for {detectedOS()}
</a>
</Show>
</div>
</section>
@@ -149,12 +93,6 @@ export default function Download() {
<span>[2]</span> OpenCode Desktop (Beta)
</div>
<div data-component="section-content">
<button data-component="cli-row" onClick={handleCopyClick("brew install --cask opencode-desktop")}>
<code>
brew install --cask <strong>opencode-desktop</strong>
</code>
<CopyStatus />
</button>
<div data-component="download-row">
<div data-component="download-info">
<span data-slot="icon">
@@ -169,7 +107,7 @@ export default function Download() {
macOS (<span data-slot="hide-narrow">Apple </span>Silicon)
</span>
</div>
<a href={getDownloadHref("darwin-aarch64-dmg")} data-component="action-button">
<a href={downloadUrl + "/opencode-desktop-darwin-aarch64.dmg"} data-component="action-button">
Download
</a>
</div>
@@ -185,7 +123,7 @@ export default function Download() {
</span>
<span>macOS (Intel)</span>
</div>
<a href={getDownloadHref("darwin-x64-dmg")} data-component="action-button">
<a href={downloadUrl + "/opencode-desktop-darwin-x64.dmg"} data-component="action-button">
Download
</a>
</div>
@@ -208,7 +146,7 @@ export default function Download() {
</span>
<span>Windows (x64)</span>
</div>
<a href={getDownloadHref("windows-x64-nsis")} data-component="action-button">
<a href={downloadUrl + "/opencode-desktop-windows-x64.exe"} data-component="action-button">
Download
</a>
</div>
@@ -224,7 +162,7 @@ export default function Download() {
</span>
<span>Linux (.deb)</span>
</div>
<a href={getDownloadHref("linux-x64-deb")} data-component="action-button">
<a href={downloadUrl + "/opencode-desktop-linux-amd64.deb"} data-component="action-button">
Download
</a>
</div>
@@ -240,7 +178,7 @@ export default function Download() {
</span>
<span>Linux (.rpm)</span>
</div>
<a href={getDownloadHref("linux-x64-rpm")} data-component="action-button">
<a href={downloadUrl + "/opencode-desktop-linux-x86_64.rpm"} data-component="action-button">
Download
</a>
</div>
@@ -1 +0,0 @@
export type DownloadPlatform = `darwin-${"x64" | "aarch64"}-dmg` | "windows-x64-nsis" | `linux-x64-${"deb" | "rpm"}`
@@ -110,13 +110,10 @@
[data-slot="cta-button"] {
background: var(--color-background-strong);
color: var(--color-text-inverted);
padding: 8px 16px 8px 10px;
padding: 8px 16px;
border-radius: 4px;
font-weight: 500;
text-decoration: none;
display: flex;
align-items: center;
gap: 8px;
@media (max-width: 55rem) {
display: none;
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core",
"version": "1.0.165",
"version": "1.0.163",
"private": true,
"type": "module",
"dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-function",
"version": "1.0.165",
"version": "1.0.163",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-mail",
"version": "1.0.165",
"version": "1.0.163",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/desktop",
"version": "1.0.165",
"version": "1.0.163",
"description": "",
"type": "module",
"exports": {
@@ -40,7 +40,7 @@
"@solid-primitives/media": "2.3.3",
"@solid-primitives/resize-observer": "2.1.3",
"@solid-primitives/scroll": "2.1.3",
"@solid-primitives/storage": "catalog:",
"@solid-primitives/storage": "4.3.3",
"@solid-primitives/websocket": "1.3.1",
"@solidjs/meta": "catalog:",
"@solidjs/router": "catalog:",
+41 -45
View File
@@ -5,9 +5,7 @@ import { MetaProvider } from "@solidjs/meta"
import { Font } from "@opencode-ai/ui/font"
import { MarkedProvider } from "@opencode-ai/ui/context/marked"
import { DiffComponentProvider } from "@opencode-ai/ui/context/diff"
import { CodeComponentProvider } from "@opencode-ai/ui/context/code"
import { Diff } from "@opencode-ai/ui/diff"
import { Code } from "@opencode-ai/ui/code"
import { GlobalSyncProvider } from "@/context/global-sync"
import { LayoutProvider } from "@/context/layout"
import { GlobalSDKProvider } from "@/context/global-sdk"
@@ -38,48 +36,46 @@ const url =
export function App() {
return (
<MetaProvider>
<Font />
<DialogProvider>
<MarkedProvider>
<DiffComponentProvider component={Diff}>
<CodeComponentProvider component={Code}>
<GlobalSDKProvider url={url}>
<GlobalSyncProvider>
<LayoutProvider>
<NotificationProvider>
<Router
root={(props) => (
<CommandProvider>
<Layout>{props.children}</Layout>
</CommandProvider>
)}
>
<Route path="/" component={Home} />
<Route path="/:dir" component={DirectoryLayout}>
<Route path="/" component={() => <Navigate href="session" />} />
<Route
path="/session/:id?"
component={(p) => (
<Show when={p.params.id || true} keyed>
<TerminalProvider>
<PromptProvider>
<Session />
</PromptProvider>
</TerminalProvider>
</Show>
)}
/>
</Route>
</Router>
</NotificationProvider>
</LayoutProvider>
</GlobalSyncProvider>
</GlobalSDKProvider>
</CodeComponentProvider>
</DiffComponentProvider>
</MarkedProvider>
</DialogProvider>
</MetaProvider>
<DialogProvider>
<MarkedProvider>
<DiffComponentProvider component={Diff}>
<GlobalSDKProvider url={url}>
<GlobalSyncProvider>
<LayoutProvider>
<NotificationProvider>
<MetaProvider>
<Font />
<Router
root={(props) => (
<CommandProvider>
<Layout>{props.children}</Layout>
</CommandProvider>
)}
>
<Route path="/" component={Home} />
<Route path="/:dir" component={DirectoryLayout}>
<Route path="/" component={() => <Navigate href="session" />} />
<Route
path="/session/:id?"
component={(p) => (
<Show when={p.params.id || true} keyed>
<TerminalProvider>
<PromptProvider>
<Session />
</PromptProvider>
</TerminalProvider>
</Show>
)}
/>
</Route>
</Router>
</MetaProvider>
</NotificationProvider>
</LayoutProvider>
</GlobalSyncProvider>
</GlobalSDKProvider>
</DiffComponentProvider>
</MarkedProvider>
</DialogProvider>
)
}
+1 -35
View File
@@ -1,34 +1,27 @@
import { useGlobalSync } from "@/context/global-sync"
import { useGlobalSDK } from "@/context/global-sdk"
import { useLayout } from "@/context/layout"
import { Session } from "@opencode-ai/sdk/v2/client"
import { Button } from "@opencode-ai/ui/button"
import { Icon } from "@opencode-ai/ui/icon"
import { Mark } from "@opencode-ai/ui/logo"
import { Popover } from "@opencode-ai/ui/popover"
import { Select } from "@opencode-ai/ui/select"
import { TextField } from "@opencode-ai/ui/text-field"
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { base64Decode } from "@opencode-ai/util/encode"
import { getFilename } from "@opencode-ai/util/path"
import { A, useParams } from "@solidjs/router"
import { createMemo, createResource, Show } from "solid-js"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { iife } from "@opencode-ai/util/iife"
import { createMemo, Show } from "solid-js"
export function Header(props: {
navigateToProject: (directory: string) => void
navigateToSession: (session: Session | undefined) => void
}) {
const globalSync = useGlobalSync()
const globalSDK = useGlobalSDK()
const layout = useLayout()
const params = useParams()
const currentDirectory = createMemo(() => base64Decode(params.dir ?? ""))
const store = createMemo(() => globalSync.child(currentDirectory())[0])
const sessions = createMemo(() => store().session ?? [])
const currentSession = createMemo(() => sessions().find((s) => s.id === params.id))
const shareEnabled = createMemo(() => store().config.share !== "disabled")
return (
<header class="h-12 shrink-0 bg-background-base border-b border-border-weak-base flex" data-tauri-drag-region>
@@ -112,33 +105,6 @@ export function Header(props: {
</div>
</Button>
</Tooltip>
<Show when={shareEnabled() && currentSession()}>
<Popover
title="Share session"
trigger={
<Tooltip class="shrink-0" value="Share session">
<IconButton icon="share" variant="ghost" class="" />
</Tooltip>
}
>
{iife(() => {
const [url] = createResource(
() => currentSession(),
async (session) => {
if (!session) return
let shareURL = session.share?.url
if (!shareURL) {
shareURL = await globalSDK.client.session
.share({ sessionID: session.id, directory: currentDirectory() })
.then((r) => r.data?.share?.url)
}
return shareURL
},
)
return <Show when={url()}>{(url) => <TextField value={url()} readOnly copyable class="w-72" />}</Show>
})}
</Popover>
</Show>
</div>
</Show>
</div>
@@ -1,6 +1,7 @@
import { useFilteredList } from "@opencode-ai/ui/hooks"
import { createEffect, on, Component, Show, For, onMount, onCleanup, Switch, Match, createMemo } from "solid-js"
import { createStore, produce } from "solid-js/store"
import { makePersisted } from "@solid-primitives/storage"
import { createFocusSignal } from "@solid-primitives/active-element"
import { useLocal } from "@/context/local"
import { ContentPart, DEFAULT_PROMPT, isPromptEqual, Prompt, usePrompt, ImageAttachmentPart } from "@/context/prompt"
@@ -20,7 +21,6 @@ import { DialogSelectModel } from "@/components/dialog-select-model"
import { DialogSelectModelUnpaid } from "@/components/dialog-select-model-unpaid"
import { useProviders } from "@/hooks/use-providers"
import { useCommand, formatKeybind } from "@/context/command"
import { persisted } from "@/utils/persist"
const ACCEPTED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"]
const ACCEPTED_FILE_TYPES = [...ACCEPTED_IMAGE_TYPES, "application/pdf"]
@@ -109,13 +109,15 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
})
const MAX_HISTORY = 100
const [history, setHistory] = persisted(
"prompt-history.v1",
const [history, setHistory] = makePersisted(
createStore<{
entries: Prompt[]
}>({
entries: [],
}),
{
name: "prompt-history.v1",
},
)
const clonePromptParts = (prompt: Prompt): Prompt =>
@@ -378,47 +380,31 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const parseFromDOM = (): Prompt => {
const newParts: Prompt = []
let position = 0
const pushText = (content: string) => {
if (!content) return
newParts.push({ type: "text", content, start: position, end: position + content.length })
position += content.length
}
const rangeText = (range: Range) => {
const fragment = range.cloneContents()
const container = document.createElement("div")
container.append(fragment)
return container.innerText
}
const files = Array.from(editorRef.querySelectorAll<HTMLElement>("[data-type=file]"))
let last: HTMLElement | undefined
files.forEach((file) => {
const before = document.createRange()
before.selectNodeContents(editorRef)
if (last) before.setStartAfter(last)
before.setEndBefore(file)
pushText(rangeText(before))
const content = file.textContent ?? ""
newParts.push({
type: "file",
path: file.dataset.path!,
content,
start: position,
end: position + content.length,
})
position += content.length
last = file
editorRef.childNodes.forEach((node) => {
if (node.nodeType === Node.TEXT_NODE) {
if (node.textContent) {
const content = node.textContent
newParts.push({ type: "text", content, start: position, end: position + content.length })
position += content.length
}
} else if (node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).dataset.type) {
switch ((node as HTMLElement).dataset.type) {
case "file":
const content = node.textContent!
newParts.push({
type: "file",
path: (node as HTMLElement).dataset.path!,
content,
start: position,
end: position + content.length,
})
position += content.length
break
default:
break
}
}
})
const after = document.createRange()
after.selectNodeContents(editorRef)
if (last) after.setStartAfter(last)
pushText(rangeText(after))
if (newParts.length === 0) newParts.push(...DEFAULT_PROMPT)
return newParts
}
@@ -591,19 +577,13 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
if (event.key === "ArrowUp" || event.key === "ArrowDown") {
if (event.altKey || event.ctrlKey || event.metaKey) return
const { collapsed } = getCaretState()
const { collapsed, cursorPosition, textLength } = getCaretState()
if (!collapsed) return
const cursorPosition = getCursorPosition(editorRef)
const textLength = promptLength(prompt.current())
const textContent = editorRef.textContent ?? ""
const isEmpty = textContent.trim() === "" || textLength <= 1
const hasNewlines = textContent.includes("\n")
const inHistory = store.historyIndex >= 0
const atStart = cursorPosition <= (isEmpty ? 1 : 0)
const atEnd = cursorPosition >= (isEmpty ? textLength - 1 : textLength)
const allowUp = isEmpty || atStart || (!hasNewlines && !inHistory) || (inHistory && atEnd)
const allowDown = isEmpty || atEnd || (!hasNewlines && !inHistory) || (inHistory && atStart)
const atAbsoluteStart = cursorPosition === 0
const atAbsoluteEnd = cursorPosition === textLength
const allowUp = (inHistory && atAbsoluteEnd) || atAbsoluteStart
const allowDown = (inHistory && atAbsoluteStart) || atAbsoluteEnd
if (event.key === "ArrowUp") {
if (!allowUp) return
@@ -709,28 +689,13 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
}
}
const model = {
modelID: local.model.current()!.id,
providerID: local.model.current()!.provider.id,
}
const agent = local.agent.current()!.name
sync.session.addOptimisticMessage({
sessionID: existing.id,
text,
parts: [
{ type: "text", text } as import("@opencode-ai/sdk/v2/client").Part,
...(fileAttachmentParts as import("@opencode-ai/sdk/v2/client").Part[]),
...(imageAttachmentParts as import("@opencode-ai/sdk/v2/client").Part[]),
],
agent,
model,
})
sdk.client.session.prompt({
sessionID: existing.id,
agent,
model,
agent: local.agent.current()!.name,
model: {
modelID: local.model.current()!.id,
providerID: local.model.current()!.provider.id,
},
parts: [
{
type: "text",
@@ -868,7 +833,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
</Show>
<div class="relative max-h-[240px] overflow-y-auto">
<div
data-component="prompt-input"
ref={(el) => {
editorRef = el
props.ref?.(el)
+2 -2
View File
@@ -1,5 +1,5 @@
import { Ghostty, Terminal as Term, FitAddon } from "ghostty-web"
import { ComponentProps, onCleanup, onMount, splitProps } from "solid-js"
import { ComponentProps, createEffect, onCleanup, onMount, splitProps } from "solid-js"
import { useSDK } from "@/context/sdk"
import { SerializeAddon } from "@/addons/serialize"
import { LocalPTY } from "@/context/terminal"
@@ -31,7 +31,7 @@ export const Terminal = (props: TerminalProps) => {
term = new Term({
cursorBlink: true,
fontSize: 14,
fontFamily: "IBM Plex Mono, monospace",
fontFamily: "TX-02, monospace",
allowTransparency: true,
theme: prefersDark()
? {
@@ -10,7 +10,6 @@ export const { use: useGlobalSDK, provider: GlobalSDKProvider } = createSimpleCo
const sdk = createOpencodeClient({
baseUrl: props.url,
signal: abort.signal,
throwOnError: true,
})
const emitter = createGlobalEmitter<{
+225 -259
View File
@@ -15,13 +15,12 @@ import {
type ProviderAuthResponse,
type Command,
createOpencodeClient,
EventSessionError,
} from "@opencode-ai/sdk/v2/client"
import { createStore, produce, reconcile } from "solid-js/store"
import { Binary } from "@opencode-ai/util/binary"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { useGlobalSDK } from "./global-sdk"
import { ErrorPage } from "../pages/error"
import { createContext, useContext, onMount, type ParentProps, Switch, Match, createEffect } from "solid-js"
import { onMount } from "solid-js"
type State = {
ready: boolean
@@ -52,140 +51,190 @@ type State = {
changes: File[]
}
function createGlobalSync() {
const globalSDK = useGlobalSDK()
const [globalStore, setGlobalStore] = createStore<{
ready: boolean
error?: EventSessionError["properties"]["error"]
path: Path
project: Project[]
provider: ProviderListResponse
provider_auth: ProviderAuthResponse
children: Record<string, State>
}>({
ready: false,
path: { state: "", config: "", worktree: "", directory: "", home: "" },
project: [],
provider: { all: [], connected: [], default: {} },
provider_auth: {},
children: {},
})
export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimpleContext({
name: "GlobalSync",
init: () => {
const globalSDK = useGlobalSDK()
const [globalStore, setGlobalStore] = createStore<{
ready: boolean
path: Path
project: Project[]
provider: ProviderListResponse
provider_auth: ProviderAuthResponse
children: Record<string, State>
}>({
ready: false,
path: { state: "", config: "", worktree: "", directory: "", home: "" },
project: [],
provider: { all: [], connected: [], default: {} },
provider_auth: {},
children: {},
})
const children: Record<string, ReturnType<typeof createStore<State>>> = {}
function child(directory: string) {
if (!children[directory]) {
setGlobalStore("children", directory, {
project: "",
provider: { all: [], connected: [], default: {} },
config: {},
path: { state: "", config: "", worktree: "", directory: "", home: "" },
ready: false,
agent: [],
command: [],
session: [],
session_status: {},
session_diff: {},
todo: {},
limit: 5,
message: {},
part: {},
node: [],
changes: [],
})
children[directory] = createStore(globalStore.children[directory])
bootstrapInstance(directory)
const children: Record<string, ReturnType<typeof createStore<State>>> = {}
function child(directory: string) {
if (!children[directory]) {
setGlobalStore("children", directory, {
project: "",
provider: { all: [], connected: [], default: {} },
config: {},
path: { state: "", config: "", worktree: "", directory: "", home: "" },
ready: false,
agent: [],
command: [],
session: [],
session_status: {},
session_diff: {},
todo: {},
limit: 5,
message: {},
part: {},
node: [],
changes: [],
})
children[directory] = createStore(globalStore.children[directory])
bootstrapInstance(directory)
}
return children[directory]
}
return children[directory]
}
async function loadSessions(directory: string) {
const [store, setStore] = child(directory)
globalSDK.client.session
.list({ directory })
.then((x) => {
async function loadSessions(directory: string) {
globalSDK.client.session.list({ directory }).then((x) => {
const fourHoursAgo = Date.now() - 4 * 60 * 60 * 1000
const nonArchived = (x.data ?? [])
.slice()
.filter((s) => !s.time.archived)
.sort((a, b) => a.id.localeCompare(b.id))
// Include sessions up to the limit, plus any updated in the last hour
// Include at least 5 sessions, plus any updated in the last hour
const sessions = nonArchived.filter((s, i) => {
if (i < store.limit) return true
if (i < 5) return true
const updated = new Date(s.time.updated).getTime()
return updated > fourHoursAgo
})
const [, setStore] = child(directory)
setStore("session", sessions)
})
.catch((err) => {
console.error("Failed to load sessions", err)
setGlobalStore("error", err)
})
}
async function bootstrapInstance(directory: string) {
const [, setStore] = child(directory)
const sdk = createOpencodeClient({
baseUrl: globalSDK.url,
directory,
throwOnError: true,
})
const load = {
project: () => sdk.project.current().then((x) => setStore("project", x.data!.id)),
provider: () => sdk.provider.list().then((x) => setStore("provider", x.data!)),
path: () => sdk.path.get().then((x) => setStore("path", x.data!)),
agent: () => sdk.app.agents().then((x) => setStore("agent", x.data ?? [])),
command: () => sdk.command.list().then((x) => setStore("command", x.data ?? [])),
session: () => loadSessions(directory),
status: () => sdk.session.status().then((x) => setStore("session_status", x.data!)),
config: () => sdk.config.get().then((x) => setStore("config", x.data!)),
changes: () => sdk.file.status().then((x) => setStore("changes", x.data!)),
node: () => sdk.file.list({ path: "/" }).then((x) => setStore("node", x.data!)),
}
await Promise.all(Object.values(load).map((p) => p().catch((e) => setGlobalStore("error", e))))
.then(() => setStore("ready", true))
.catch((e) => setGlobalStore("error", e))
}
globalSDK.event.listen((e) => {
const directory = e.name
const event = e.details
async function bootstrapInstance(directory: string) {
const [, setStore] = child(directory)
const sdk = createOpencodeClient({
baseUrl: globalSDK.url,
directory,
})
const load = {
project: () => sdk.project.current().then((x) => setStore("project", x.data!.id)),
provider: () => sdk.provider.list().then((x) => setStore("provider", x.data!)),
path: () => sdk.path.get().then((x) => setStore("path", x.data!)),
agent: () => sdk.app.agents().then((x) => setStore("agent", x.data ?? [])),
command: () => sdk.command.list().then((x) => setStore("command", x.data ?? [])),
session: () => loadSessions(directory),
status: () => sdk.session.status().then((x) => setStore("session_status", x.data!)),
config: () => sdk.config.get().then((x) => setStore("config", x.data!)),
changes: () => sdk.file.status().then((x) => setStore("changes", x.data!)),
node: () => sdk.file.list({ path: "/" }).then((x) => setStore("node", x.data!)),
}
await Promise.all(Object.values(load).map((p) => p())).then(() => setStore("ready", true))
}
if (directory === "global") {
switch (event?.type) {
case "global.disposed": {
bootstrap()
globalSDK.event.listen((e) => {
console.log(e)
const directory = e.name
const event = e.details
if (directory === "global") {
switch (event?.type) {
case "global.disposed": {
bootstrap()
break
}
case "project.updated": {
const result = Binary.search(globalStore.project, event.properties.id, (s) => s.id)
if (result.found) {
setGlobalStore("project", result.index, reconcile(event.properties))
return
}
setGlobalStore(
"project",
produce((draft) => {
draft.splice(result.index, 0, event.properties)
}),
)
break
}
}
return
}
const [store, setStore] = child(directory)
switch (event.type) {
case "server.instance.disposed": {
bootstrapInstance(directory)
break
}
case "project.updated": {
const result = Binary.search(globalStore.project, event.properties.id, (s) => s.id)
if (result.found) {
setGlobalStore("project", result.index, reconcile(event.properties))
return
case "session.updated": {
const result = Binary.search(store.session, event.properties.info.id, (s) => s.id)
if (event.properties.info.time.archived) {
if (result.found) {
setStore(
"session",
produce((draft) => {
draft.splice(result.index, 1)
}),
)
}
break
}
setGlobalStore(
"project",
if (result.found) {
setStore("session", result.index, reconcile(event.properties.info))
break
}
setStore(
"session",
produce((draft) => {
draft.splice(result.index, 0, event.properties)
draft.splice(result.index, 0, event.properties.info)
}),
)
break
}
}
return
}
const [store, setStore] = child(directory)
switch (event.type) {
case "server.instance.disposed": {
bootstrapInstance(directory)
break
}
case "session.updated": {
const result = Binary.search(store.session, event.properties.info.id, (s) => s.id)
if (event.properties.info.time.archived) {
case "session.diff":
setStore("session_diff", event.properties.sessionID, event.properties.diff)
break
case "todo.updated":
setStore("todo", event.properties.sessionID, event.properties.todos)
break
case "session.status": {
setStore("session_status", event.properties.sessionID, event.properties.status)
break
}
case "message.updated": {
const messages = store.message[event.properties.info.sessionID]
if (!messages) {
setStore("message", event.properties.info.sessionID, [event.properties.info])
break
}
const result = Binary.search(messages, event.properties.info.id, (m) => m.id)
if (result.found) {
setStore("message", event.properties.info.sessionID, result.index, reconcile(event.properties.info))
break
}
setStore(
"message",
event.properties.info.sessionID,
produce((draft) => {
draft.splice(result.index, 0, event.properties.info)
}),
)
break
}
case "message.removed": {
const messages = store.message[event.properties.sessionID]
if (!messages) break
const result = Binary.search(messages, event.properties.messageID, (m) => m.id)
if (result.found) {
setStore(
"session",
"message",
event.properties.sessionID,
produce((draft) => {
draft.splice(result.index, 1)
}),
@@ -193,162 +242,79 @@ function createGlobalSync() {
}
break
}
if (result.found) {
setStore("session", result.index, reconcile(event.properties.info))
break
}
setStore(
"session",
produce((draft) => {
draft.splice(result.index, 0, event.properties.info)
}),
)
break
}
case "session.diff":
setStore("session_diff", event.properties.sessionID, event.properties.diff)
break
case "todo.updated":
setStore("todo", event.properties.sessionID, event.properties.todos)
break
case "session.status": {
setStore("session_status", event.properties.sessionID, event.properties.status)
break
}
case "message.updated": {
const messages = store.message[event.properties.info.sessionID]
if (!messages) {
setStore("message", event.properties.info.sessionID, [event.properties.info])
break
}
const result = Binary.search(messages, event.properties.info.id, (m) => m.id)
if (result.found) {
setStore("message", event.properties.info.sessionID, result.index, reconcile(event.properties.info))
break
}
setStore(
"message",
event.properties.info.sessionID,
produce((draft) => {
draft.splice(result.index, 0, event.properties.info)
}),
)
break
}
case "message.removed": {
const messages = store.message[event.properties.sessionID]
if (!messages) break
const result = Binary.search(messages, event.properties.messageID, (m) => m.id)
if (result.found) {
setStore(
"message",
event.properties.sessionID,
produce((draft) => {
draft.splice(result.index, 1)
}),
)
}
break
}
case "message.part.updated": {
const part = event.properties.part
const parts = store.part[part.messageID]
if (!parts) {
setStore("part", part.messageID, [part])
break
}
const result = Binary.search(parts, part.id, (p) => p.id)
if (result.found) {
setStore("part", part.messageID, result.index, reconcile(part))
break
}
setStore(
"part",
part.messageID,
produce((draft) => {
draft.splice(result.index, 0, part)
}),
)
break
}
case "message.part.removed": {
const parts = store.part[event.properties.messageID]
if (!parts) break
const result = Binary.search(parts, event.properties.partID, (p) => p.id)
if (result.found) {
case "message.part.updated": {
const part = event.properties.part
const parts = store.part[part.messageID]
if (!parts) {
setStore("part", part.messageID, [part])
break
}
const result = Binary.search(parts, part.id, (p) => p.id)
if (result.found) {
setStore("part", part.messageID, result.index, reconcile(part))
break
}
setStore(
"part",
event.properties.messageID,
part.messageID,
produce((draft) => {
draft.splice(result.index, 1)
draft.splice(result.index, 0, part)
}),
)
break
}
case "message.part.removed": {
const parts = store.part[event.properties.messageID]
if (!parts) break
const result = Binary.search(parts, event.properties.partID, (p) => p.id)
if (result.found) {
setStore(
"part",
event.properties.messageID,
produce((draft) => {
draft.splice(result.index, 1)
}),
)
}
break
}
break
}
})
async function bootstrap() {
return Promise.all([
globalSDK.client.path.get().then((x) => {
setGlobalStore("path", x.data!)
}),
globalSDK.client.project.list().then(async (x) => {
setGlobalStore(
"project",
x.data!.filter((p) => !p.worktree.includes("opencode-test")).sort((a, b) => a.id.localeCompare(b.id)),
)
}),
globalSDK.client.provider.list().then((x) => {
setGlobalStore("provider", x.data ?? {})
}),
globalSDK.client.provider.auth().then((x) => {
setGlobalStore("provider_auth", x.data ?? {})
}),
]).then(() => setGlobalStore("ready", true))
}
})
async function bootstrap() {
return Promise.all([
globalSDK.client.path.get().then((x) => {
setGlobalStore("path", x.data!)
}),
globalSDK.client.project.list().then(async (x) => {
setGlobalStore(
"project",
x.data!.filter((p) => !p.worktree.includes("opencode-test")).sort((a, b) => a.id.localeCompare(b.id)),
)
}),
globalSDK.client.provider.list().then((x) => {
setGlobalStore("provider", x.data ?? {})
}),
globalSDK.client.provider.auth().then((x) => {
setGlobalStore("provider_auth", x.data ?? {})
}),
])
.then(() => setGlobalStore("ready", true))
.catch((e) => setGlobalStore("error", e))
}
onMount(() => {
bootstrap()
})
onMount(() => {
bootstrap()
})
return {
data: globalStore,
get ready() {
return globalStore.ready
},
get error() {
return globalStore.error
},
child,
bootstrap,
project: {
loadSessions,
},
}
}
const GlobalSyncContext = createContext<ReturnType<typeof createGlobalSync>>()
export function GlobalSyncProvider(props: ParentProps) {
const value = createGlobalSync()
return (
<Switch>
<Match when={value.error}>
<ErrorPage error={value.error} />
</Match>
<Match when={value.ready}>
<GlobalSyncContext.Provider value={value}>{props.children}</GlobalSyncContext.Provider>
</Match>
</Switch>
)
}
export function useGlobalSync() {
const context = useContext(GlobalSyncContext)
if (!context) throw new Error("useGlobalSync must be used within GlobalSyncProvider")
return context
}
return {
data: globalStore,
get ready() {
return globalStore.ready
},
child,
bootstrap,
project: {
loadSessions,
},
}
},
})
+5 -4
View File
@@ -1,10 +1,10 @@
import { createStore, produce } from "solid-js/store"
import { batch, createMemo, onMount } from "solid-js"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { makePersisted } from "@solid-primitives/storage"
import { useGlobalSync } from "./global-sync"
import { useGlobalSDK } from "./global-sdk"
import { Project } from "@opencode-ai/sdk/v2"
import { persisted } from "@/utils/persist"
const AVATAR_COLOR_KEYS = ["pink", "mint", "orange", "purple", "cyan", "lime"] as const
export type AvatarColorKey = (typeof AVATAR_COLOR_KEYS)[number]
@@ -32,8 +32,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
init: () => {
const globalSdk = useGlobalSDK()
const globalSync = useGlobalSync()
const [store, setStore, _, ready] = persisted(
"layout.v3",
const [store, setStore] = makePersisted(
createStore({
projects: [] as { worktree: string; expanded: boolean }[],
sidebar: {
@@ -49,6 +48,9 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
},
sessionTabs: {} as Record<string, SessionTabs>,
}),
{
name: "layout.v3",
},
)
const usedColors = new Set<AvatarColorKey>()
@@ -91,7 +93,6 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
})
return {
ready,
projects: {
list,
open(directory: string) {
+3 -4
View File
@@ -7,8 +7,8 @@ import { useSDK } from "./sdk"
import { useSync } from "./sync"
import { base64Encode } from "@opencode-ai/util/encode"
import { useProviders } from "@/hooks/use-providers"
import { makePersisted } from "@solid-primitives/storage"
import { DateTime } from "luxon"
import { persisted } from "@/utils/persist"
export type LocalFile = FileNode &
Partial<{
@@ -110,8 +110,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
})()
const model = (() => {
const [store, setStore, _, modelReady] = persisted(
"model.v1",
const [store, setStore] = makePersisted(
createStore<{
user: (ModelKey & { visibility: "show" | "hide"; favorite?: boolean })[]
recent: ModelKey[]
@@ -119,6 +118,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
user: [],
recent: [],
}),
{ name: "model.v1" },
)
const [ephemeral, setEphemeral] = createStore<{
@@ -242,7 +242,6 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
}
return {
ready: modelReady,
current,
recent,
list,
+15 -20
View File
@@ -1,5 +1,6 @@
import { createStore } from "solid-js/store"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { makePersisted } from "@solid-primitives/storage"
import { useGlobalSDK } from "./global-sdk"
import { useGlobalSync } from "./global-sync"
import { Binary } from "@opencode-ai/util/binary"
@@ -7,7 +8,6 @@ import { EventSessionError } from "@opencode-ai/sdk/v2"
import { makeAudioPlayer } from "@solid-primitives/audio"
import idleSound from "@opencode-ai/ui/audio/staplebops-01.aac"
import errorSound from "@opencode-ai/ui/audio/nope-03.aac"
import { persisted } from "@/utils/persist"
type NotificationBase = {
directory?: string
@@ -31,27 +31,27 @@ export type Notification = TurnCompleteNotification | ErrorNotification
export const { use: useNotification, provider: NotificationProvider } = createSimpleContext({
name: "Notification",
init: () => {
let idlePlayer: ReturnType<typeof makeAudioPlayer> | undefined
let errorPlayer: ReturnType<typeof makeAudioPlayer> | undefined
try {
idlePlayer = makeAudioPlayer(idleSound)
errorPlayer = makeAudioPlayer(errorSound)
} catch (err) {
console.log("Failed to load audio", err)
}
const idlePlayer = makeAudioPlayer(idleSound)
const errorPlayer = makeAudioPlayer(errorSound)
const globalSDK = useGlobalSDK()
const globalSync = useGlobalSync()
const [store, setStore, _, ready] = persisted(
"notification.v1",
const [store, setStore] = makePersisted(
createStore({
list: [] as Notification[],
}),
{
name: "notification.v1",
},
)
// onMount(() => {
// const daysToKeep = 7
// // setStore("list", (n) => n.filter((n) => !n.viewed && n.time + 1000 * 60 * 60 * 24 * daysToKeep < Date.now()))
// })
globalSDK.event.listen((e) => {
console.log(e)
const directory = e.name
const event = e.details
const base = {
@@ -66,9 +66,7 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
const match = Binary.search(syncStore.session, sessionID, (s) => s.id)
const isChild = match.found && syncStore.session[match.index].parentID
if (isChild) break
try {
idlePlayer?.play()
} catch {}
idlePlayer.play()
setStore("list", store.list.length, {
...base,
type: "turn-complete",
@@ -84,9 +82,7 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
const isChild = match.found && syncStore.session[match.index].parentID
if (isChild) break
}
try {
errorPlayer?.play()
} catch {}
errorPlayer.play()
setStore("list", store.list.length, {
...base,
type: "error",
@@ -99,7 +95,6 @@ export const { use: useNotification, provider: NotificationProvider } = createSi
})
return {
ready,
session: {
all(session: string) {
return store.list.filter((n) => n.session === session)
@@ -1,5 +1,4 @@
import { createSimpleContext } from "@opencode-ai/ui/context"
import { AsyncStorage, SyncStorage } from "@solid-primitives/storage"
export type Platform = {
/** Platform discriminator */
@@ -16,9 +15,6 @@ export type Platform = {
/** Open a URL in the default browser */
openLink(url: string): void
/** Storage mechanism, defaults to localStorage */
storage?: (name?: string) => SyncStorage | AsyncStorage
}
export const { use: usePlatform, provider: PlatformProvider } = createSimpleContext({
+5 -4
View File
@@ -1,9 +1,9 @@
import { createStore } from "solid-js/store"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { batch, createMemo } from "solid-js"
import { makePersisted } from "@solid-primitives/storage"
import { useParams } from "@solidjs/router"
import { TextSelection } from "./local"
import { persisted } from "@/utils/persist"
interface PartBase {
content: string
@@ -77,8 +77,7 @@ export const { use: usePrompt, provider: PromptProvider } = createSimpleContext(
const params = useParams()
const name = createMemo(() => `${params.dir}/prompt${params.id ? "/" + params.id : ""}.v1`)
const [store, setStore, _, ready] = persisted(
name(),
const [store, setStore] = makePersisted(
createStore<{
prompt: Prompt
cursor?: number
@@ -86,10 +85,12 @@ export const { use: usePrompt, provider: PromptProvider } = createSimpleContext(
prompt: clonePrompt(DEFAULT_PROMPT),
cursor: undefined,
}),
{
name: name(),
},
)
return {
ready,
current: createMemo(() => store.prompt),
cursor: createMemo(() => store.cursor),
dirty: createMemo(() => !isPromptEqual(store.prompt, DEFAULT_PROMPT)),
-1
View File
@@ -13,7 +13,6 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
baseUrl: globalSDK.url,
signal: abort.signal,
directory: props.directory,
throwOnError: true,
})
const emitter = createGlobalEmitter<{
-35
View File
@@ -4,7 +4,6 @@ import { Binary } from "@opencode-ai/util/binary"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { useGlobalSync } from "./global-sync"
import { useSDK } from "./sdk"
import type { Message, Part } from "@opencode-ai/sdk/v2/client"
export const { use: useSync, provider: SyncProvider } = createSimpleContext({
name: "Sync",
@@ -31,40 +30,6 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
if (match.found) return store.session[match.index]
return undefined
},
addOptimisticMessage(input: {
sessionID: string
text: string
parts: Part[]
agent: string
model: { providerID: string; modelID: string }
}) {
const messageID = crypto.randomUUID()
const message: Message = {
id: messageID,
sessionID: input.sessionID,
role: "user",
time: { created: Date.now() },
agent: input.agent,
model: input.model,
}
setStore(
produce((draft) => {
const messages = draft.message[input.sessionID]
if (!messages) {
draft.message[input.sessionID] = [message]
} else {
const result = Binary.search(messages, messageID, (m) => m.id)
messages.splice(result.index, 0, message)
}
draft.part[messageID] = input.parts.map((part, i) => ({
...part,
id: `${messageID}-${i}`,
sessionID: input.sessionID,
messageID,
}))
}),
)
},
async sync(sessionID: string, _isRetry = false) {
const [session, messages, todo, diff] = await Promise.all([
sdk.client.session.get({ sessionID }, { throwOnError: true }),
+5 -4
View File
@@ -1,9 +1,9 @@
import { createStore, produce } from "solid-js/store"
import { createSimpleContext } from "@opencode-ai/ui/context"
import { batch, createMemo } from "solid-js"
import { makePersisted } from "@solid-primitives/storage"
import { useParams } from "@solidjs/router"
import { useSDK } from "./sdk"
import { persisted } from "@/utils/persist"
export type LocalPTY = {
id: string
@@ -21,18 +21,19 @@ export const { use: useTerminal, provider: TerminalProvider } = createSimpleCont
const params = useParams()
const name = createMemo(() => `${params.dir}/terminal${params.id ? "/" + params.id : ""}.v1`)
const [store, setStore, _, ready] = persisted(
name(),
const [store, setStore] = makePersisted(
createStore<{
active?: string
all: LocalPTY[]
}>({
all: [],
}),
{
name: name(),
},
)
return {
ready,
all: createMemo(() => Object.values(store.all)),
active: createMemo(() => store.active),
new() {
-44
View File
@@ -1,44 +0,0 @@
import { TextField } from "@opencode-ai/ui/text-field"
import { Logo } from "@opencode-ai/ui/logo"
import { Component } from "solid-js"
import { usePlatform } from "@/context/platform"
import { Icon } from "@opencode-ai/ui/icon"
interface ErrorPageProps {
error: any
}
export const ErrorPage: Component<ErrorPageProps> = (props) => {
const platform = usePlatform()
return (
<div class="relative flex-1 h-screen w-screen min-h-0 flex flex-col items-center justify-center">
<div class="w-2/3 max-w-3xl flex flex-col items-center justify-center gap-8">
<Logo class="h-8 w-auto text-text-strong" />
<div class="flex flex-col items-center gap-2 text-center">
<h1 class="text-lg font-medium text-text-strong">Something went wrong</h1>
<p class="text-sm text-text-weak">An error occurred while loading the application.</p>
</div>
<TextField
value={String(props.error?.data?.message || props.error?.message || props.error)}
readOnly
copyable
multiline
class="max-h-96 w-full font-mono text-xs no-scrollbar whitespace-pre"
label="Error Details"
hideLabel
/>
<div class="flex items-center justify-center gap-1">
Please report this error to the OpenCode team
<button
type="button"
class="flex items-center text-text-interactive-base gap-1"
onClick={() => platform.openLink("https://opencode.ai/desktop-feedback")}
>
<div>on Discord</div>
<Icon name="discord" class="text-text-interactive-base" />
</button>
</div>
</div>
</div>
)
}
+32 -27
View File
@@ -25,8 +25,9 @@ import {
SortableProvider,
closestCenter,
createSortable,
useDragDropContext,
} from "@thisbeyond/solid-dnd"
import type { DragEvent } from "@thisbeyond/solid-dnd"
import type { DragEvent, Transformer } from "@thisbeyond/solid-dnd"
import { useProviders } from "@/hooks/use-providers"
import { Toast } from "@opencode-ai/ui/toast"
import { useGlobalSDK } from "@/context/global-sdk"
@@ -36,7 +37,6 @@ import { Header } from "@/components/header"
import { useDialog } from "@opencode-ai/ui/context/dialog"
import { DialogSelectProvider } from "@/components/dialog-select-provider"
import { useCommand } from "@/context/command"
import { ConstrainDragXAxis } from "@/utils/solid-dnd"
export default function Layout(props: ParentProps) {
const [store, setStore] = createStore({
@@ -301,6 +301,28 @@ export default function Layout(props: ParentProps) {
setStore("activeDraggable", undefined)
}
const ConstrainDragXAxis = (): JSX.Element => {
const context = useDragDropContext()
if (!context) return <></>
const [, { onDragStart, onDragEnd, addTransformer, removeTransformer }] = context
const transformer: Transformer = {
id: "constrain-x-axis",
order: 100,
callback: (transform) => ({ ...transform, x: 0 }),
}
onDragStart((event) => {
const id = getDraggableId(event)
if (!id) return
addTransformer("draggables", id, transformer)
})
onDragEnd((event) => {
const id = getDraggableId(event)
if (!id) return
removeTransformer("draggables", id, transformer.id)
})
return <></>
}
const ProjectAvatar = (props: {
project: Project
class?: string
@@ -391,11 +413,11 @@ export default function Layout(props: ParentProps) {
const updated = createMemo(() => DateTime.fromMillis(props.session.time.updated))
const notifications = createMemo(() => notification.session.unseen(props.session.id))
const hasError = createMemo(() => notifications().some((n) => n.type === "error"))
const isWorking = createMemo(() => {
if (props.session.id === params.id) return false
const status = globalSync.child(props.project.worktree)[0].session_status[props.session.id]
return status?.type === "busy" || status?.type === "retry"
})
const isWorking = createMemo(
() =>
props.session.id !== params.id &&
globalSync.child(props.project.worktree)[0].session_status[props.session.id]?.type === "busy",
)
return (
<>
<div
@@ -475,7 +497,7 @@ export default function Layout(props: ParentProps) {
const sortable = createSortable(props.project.worktree)
const slug = createMemo(() => base64Encode(props.project.worktree))
const name = createMemo(() => getFilename(props.project.worktree))
const [store, setProjectStore] = globalSync.child(props.project.worktree)
const [store] = globalSync.child(props.project.worktree)
const sessions = createMemo(() => store.session ?? [])
const rootSessions = createMemo(() => sessions().filter((s) => !s.parentID))
const childSessionsByParent = createMemo(() => {
@@ -489,11 +511,6 @@ export default function Layout(props: ParentProps) {
}
return map
})
const hasMoreSessions = createMemo(() => store.session.length >= store.limit)
const loadMoreSessions = async () => {
setProjectStore("limit", (limit) => limit + 10)
await globalSync.project.loadSessions(props.project.worktree)
}
const [expanded, setExpanded] = createSignal(true)
return (
// @ts-ignore
@@ -566,18 +583,6 @@ export default function Layout(props: ParentProps) {
</div>
</div>
</Show>
<Show when={hasMoreSessions()}>
<div class="relative w-full py-1">
<Button
variant="ghost"
class="flex w-full text-left justify-start text-12-medium opacity-50 px-3.5"
size="large"
onClick={loadMoreSessions}
>
Load more
</Button>
</div>
</Show>
</nav>
</Collapsible.Content>
</Collapsible>
@@ -613,7 +618,7 @@ export default function Layout(props: ParentProps) {
classList={{
"relative @container w-12 pb-5 shrink-0 bg-background-base": true,
"flex flex-col gap-5.5 items-start self-stretch justify-between": true,
"border-r border-border-weak-base contain-strict": true,
"border-r border-border-weak-base": true,
}}
style={{ width: layout.sidebar.opened() ? `${layout.sidebar.width()}px` : undefined }}
>
@@ -755,7 +760,7 @@ export default function Layout(props: ParentProps) {
</Tooltip>
</div>
</div>
<main class="size-full overflow-x-hidden flex flex-col items-start contain-strict">{props.children}</main>
<main class="size-full overflow-x-hidden flex flex-col items-start">{props.children}</main>
</div>
<Toast.Region />
</div>
+57 -26
View File
@@ -1,5 +1,4 @@
import { For, onCleanup, onMount, Show, Match, Switch, createResource, createMemo, createEffect, on } from "solid-js"
import { Dynamic } from "solid-js/web"
import { useLocal, type LocalFile } from "@/context/local"
import { createStore } from "solid-js/store"
import { PromptInput } from "@/components/prompt-input"
@@ -12,7 +11,7 @@ import { DiffChanges } from "@opencode-ai/ui/diff-changes"
import { ProgressCircle } from "@opencode-ai/ui/progress-circle"
import { ResizeHandle } from "@opencode-ai/ui/resize-handle"
import { Tabs } from "@opencode-ai/ui/tabs"
import { useCodeComponent } from "@opencode-ai/ui/context/code"
import { Code } from "@opencode-ai/ui/code"
import { SessionTurn } from "@opencode-ai/ui/session-turn"
import { SessionMessageRail } from "@opencode-ai/ui/session-message-rail"
import { SessionReview } from "@opencode-ai/ui/session-review"
@@ -23,8 +22,9 @@ import {
SortableProvider,
closestCenter,
createSortable,
useDragDropContext,
} from "@thisbeyond/solid-dnd"
import type { DragEvent } from "@thisbeyond/solid-dnd"
import type { DragEvent, Transformer } from "@thisbeyond/solid-dnd"
import type { JSX } from "solid-js"
import { useSync } from "@/context/sync"
import { useTerminal, type LocalPTY } from "@/context/terminal"
@@ -41,7 +41,6 @@ import { AssistantMessage, UserMessage } from "@opencode-ai/sdk/v2"
import { useSDK } from "@/context/sdk"
import { usePrompt } from "@/context/prompt"
import { extractPromptFromParts } from "@/utils/prompt"
import { ConstrainDragYAxis, getDraggableId } from "@/utils/solid-dnd"
export default function Page() {
const layout = useLayout()
@@ -49,7 +48,6 @@ export default function Page() {
const sync = useSync()
const terminal = useTerminal()
const dialog = useDialog()
const codeComponent = useCodeComponent()
const command = useCommand()
const params = useParams()
const navigate = useNavigate()
@@ -258,19 +256,12 @@ export default function Page() {
slash: "agent",
onSelect: () => local.agent.move(1),
},
{
id: "agent.cycle.reverse",
title: "Cycle agent backwards",
description: "Switch to the previous agent",
category: "Agent",
keybind: "shift+mod+.",
onSelect: () => local.agent.move(-1),
},
{
id: "session.undo",
title: "Undo",
description: "Undo the last message",
category: "Session",
keybind: "mod+z",
slash: "undo",
disabled: !params.id || visibleUserMessages().length === 0,
onSelect: async () => {
@@ -300,6 +291,7 @@ export default function Page() {
title: "Redo",
description: "Redo the last undone message",
category: "Session",
keybind: "mod+shift+z",
slash: "redo",
disabled: !params.id || !info()?.revert?.messageID,
onSelect: async () => {
@@ -330,6 +322,19 @@ export default function Page() {
if ((document.activeElement as HTMLElement)?.dataset?.component === "terminal") return
if (dialog.active) return
if (event.key === "PageUp" || event.key === "PageDown") {
const scrollContainer = document.querySelector('[data-slot="session-turn-content"]') as HTMLElement
if (scrollContainer) {
event.preventDefault()
const scrollAmount = scrollContainer.clientHeight * 0.8
scrollContainer.scrollBy({
top: event.key === "PageUp" ? -scrollAmount : scrollAmount,
behavior: "instant",
})
}
return
}
const focused = document.activeElement === inputRef
if (focused) {
if (event.key === "Escape") inputRef?.blur()
@@ -512,6 +517,36 @@ export default function Page() {
)
}
const ConstrainDragYAxis = (): JSX.Element => {
const context = useDragDropContext()
if (!context) return <></>
const [, { onDragStart, onDragEnd, addTransformer, removeTransformer }] = context
const transformer: Transformer = {
id: "constrain-y-axis",
order: 100,
callback: (transform) => ({ ...transform, y: 0 }),
}
onDragStart((event) => {
const id = getDraggableId(event)
if (!id) return
addTransformer("draggables", id, transformer)
})
onDragEnd((event) => {
const id = getDraggableId(event)
if (!id) return
removeTransformer("draggables", id, transformer.id)
})
return <></>
}
const getDraggableId = (event: unknown): string | undefined => {
if (typeof event !== "object" || event === null) return undefined
if (!("draggable" in event)) return undefined
const draggable = (event as { draggable?: { id?: unknown } }).draggable
if (!draggable) return undefined
return typeof draggable.id === "string" ? draggable.id : undefined
}
const wide = createMemo(() => layout.review.state() === "tab" || !diffs().length)
return (
@@ -584,10 +619,7 @@ export default function Page() {
</div>
</Tabs.List>
</div>
<Tabs.Content
value="chat"
class="@container select-text flex flex-col flex-1 min-h-0 overflow-y-hidden contain-strict"
>
<Tabs.Content value="chat" class="@container select-text flex flex-col flex-1 min-h-0 overflow-y-hidden">
<div
classList={{
"w-full flex-1 min-h-0": true,
@@ -598,7 +630,7 @@ export default function Page() {
<div
classList={{
"relative shrink-0 py-3 flex flex-col gap-6 flex-1 min-h-0 w-full": true,
"max-w-200 mx-auto": !wide(),
"max-w-146 mx-auto": !wide(),
}}
>
<Switch>
@@ -622,7 +654,7 @@ export default function Page() {
container:
"w-full " +
(wide()
? "max-w-200 mx-auto px-6"
? "max-w-146 mx-auto px-6"
: visibleUserMessages().length > 1
? "pr-6 pl-18"
: "px-6"),
@@ -632,7 +664,7 @@ export default function Page() {
</div>
</Match>
<Match when={true}>
<div class="size-full flex flex-col pb-45 justify-end items-start gap-4 flex-[1_0_0] self-stretch max-w-200 mx-auto px-6">
<div class="size-full flex flex-col pb-45 justify-end items-start gap-4 flex-[1_0_0] self-stretch max-w-146 mx-auto px-6">
<div class="text-20-medium text-text-weaker">New session</div>
<div class="flex justify-center items-center gap-3">
<Icon name="folder" size="small" />
@@ -658,7 +690,7 @@ export default function Page() {
</Match>
</Switch>
<div class="absolute inset-x-0 bottom-8 flex flex-col justify-center items-center z-50">
<div class="w-full max-w-200 px-6">
<div class="w-full max-w-146 px-6">
<PromptInput
ref={(el) => {
inputRef = el
@@ -670,7 +702,7 @@ export default function Page() {
<Show when={layout.review.state() === "pane" && diffs().length}>
<div
classList={{
"relative grow pt-3 flex-1 min-h-0 border-l border-border-weak-base contain-strict": true,
"relative grow pt-3 flex-1 min-h-0 border-l border-border-weak-base": true,
}}
>
<SessionReview
@@ -698,7 +730,7 @@ export default function Page() {
</div>
</Tabs.Content>
<Show when={layout.review.state() === "tab" && diffs().length}>
<Tabs.Content value="review" class="select-text flex flex-col h-full overflow-hidden contain-strict">
<Tabs.Content value="review" class="select-text flex flex-col h-full overflow-hidden">
<div
classList={{
"relative pt-3 flex-1 min-h-0 overflow-hidden": true,
@@ -732,8 +764,7 @@ export default function Page() {
<Switch>
<Match when={file()}>
{(f) => (
<Dynamic
component={codeComponent}
<Code
file={{
name: f().path,
contents: f().content?.content ?? "",
@@ -772,7 +803,7 @@ export default function Page() {
</DragOverlay>
</DragDropProvider>
<Show when={tabs().active()}>
<div class="absolute inset-x-0 px-6 max-w-200 flex flex-col justify-center items-center z-50 mx-auto bottom-8">
<div class="absolute inset-x-0 px-6 max-w-146 flex flex-col justify-center items-center z-50 mx-auto bottom-8">
<PromptInput
ref={(el) => {
inputRef = el
-26
View File
@@ -1,26 +0,0 @@
import { usePlatform } from "@/context/platform"
import { makePersisted } from "@solid-primitives/storage"
import { createResource, type Accessor } from "solid-js"
import type { SetStoreFunction, Store } from "solid-js/store"
type InitType = Promise<string> | string | null
type PersistedWithReady<T> = [Store<T>, SetStoreFunction<T>, InitType, Accessor<boolean>]
export function persisted<T>(key: string, store: [Store<T>, SetStoreFunction<T>]): PersistedWithReady<T> {
const platform = usePlatform()
const [state, setState, init] = makePersisted(store, { name: key, storage: platform.storage?.() ?? localStorage })
// Create a resource that resolves when the store is initialized
// This integrates with Suspense and provides a ready signal
const isAsync = init instanceof Promise
const [ready] = createResource(
() => init,
async (initValue) => {
if (initValue instanceof Promise) await initValue
return true
},
{ initialValue: !isAsync },
)
return [state, setState, init, () => ready() === true]
}
-55
View File
@@ -1,55 +0,0 @@
import { useDragDropContext } from "@thisbeyond/solid-dnd"
import { JSXElement } from "solid-js"
import type { Transformer } from "@thisbeyond/solid-dnd"
export const getDraggableId = (event: unknown): string | undefined => {
if (typeof event !== "object" || event === null) return undefined
if (!("draggable" in event)) return undefined
const draggable = (event as { draggable?: { id?: unknown } }).draggable
if (!draggable) return undefined
return typeof draggable.id === "string" ? draggable.id : undefined
}
export const ConstrainDragXAxis = (): JSXElement => {
const context = useDragDropContext()
if (!context) return <></>
const [, { onDragStart, onDragEnd, addTransformer, removeTransformer }] = context
const transformer: Transformer = {
id: "constrain-x-axis",
order: 100,
callback: (transform) => ({ ...transform, x: 0 }),
}
onDragStart((event) => {
const id = getDraggableId(event)
if (!id) return
addTransformer("draggables", id, transformer)
})
onDragEnd((event) => {
const id = getDraggableId(event)
if (!id) return
removeTransformer("draggables", id, transformer.id)
})
return <></>
}
export const ConstrainDragYAxis = (): JSXElement => {
const context = useDragDropContext()
if (!context) return <></>
const [, { onDragStart, onDragEnd, addTransformer, removeTransformer }] = context
const transformer: Transformer = {
id: "constrain-y-axis",
order: 100,
callback: (transform) => ({ ...transform, y: 0 }),
}
onDragStart((event) => {
const id = getDraggableId(event)
if (!id) return
addTransformer("draggables", id, transformer)
})
onDragEnd((event) => {
const id = getDraggableId(event)
if (!id) return
removeTransformer("draggables", id, transformer.id)
})
return <></>
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/enterprise",
"version": "1.0.165",
"version": "1.0.163",
"private": true,
"type": "module",
"scripts": {
+215 -244
View File
@@ -3,8 +3,6 @@ import { SessionTurn } from "@opencode-ai/ui/session-turn"
import { SessionReview } from "@opencode-ai/ui/session-review"
import { DataProvider } from "@opencode-ai/ui/context"
import { DiffComponentProvider } from "@opencode-ai/ui/context/diff"
import { CodeComponentProvider } from "@opencode-ai/ui/context/code"
import { WorkerPoolProvider } from "@opencode-ai/ui/context/worker-pool"
import { createAsync, query, useParams } from "@solidjs/router"
import { createEffect, createMemo, ErrorBoundary, For, Match, Show, Switch } from "solid-js"
import { Share } from "~/core/share"
@@ -29,14 +27,6 @@ import { Meta } from "@solidjs/meta"
import { Base64 } from "js-base64"
const ClientOnlyDiff = clientOnly(() => import("@opencode-ai/ui/diff").then((m) => ({ default: m.Diff })))
const ClientOnlyCode = clientOnly(() => import("@opencode-ai/ui/code").then((m) => ({ default: m.Code })))
const ClientOnlyWorkerPoolProvider = clientOnly(() =>
import("@opencode-ai/ui/pierre/worker").then((m) => ({
default: (props: { children: any }) => (
<WorkerPoolProvider pool={m.workerPool}>{props.children}</WorkerPoolProvider>
),
})),
)
const SessionDataMissingError = NamedError.create(
"SessionDataMissingError",
@@ -205,260 +195,241 @@ export default function () {
<Meta name="description" content="opencode - The AI coding agent built for the terminal." />
<Meta property="og:image" content={ogImage()} />
<Meta name="twitter:image" content={ogImage()} />
<ClientOnlyWorkerPoolProvider>
<DiffComponentProvider component={ClientOnlyDiff}>
<CodeComponentProvider component={ClientOnlyCode}>
<DataProvider data={data()} directory={info().directory}>
{iife(() => {
const [store, setStore] = createStore({
messageId: undefined as string | undefined,
})
const messages = createMemo(() =>
data().sessionID
? (data().message[data().sessionID]?.filter((m) => m.role === "user") ?? []).sort(
(a, b) => a.time.created - b.time.created,
)
: [],
)
const firstUserMessage = createMemo(() => messages().at(0))
const activeMessage = createMemo(
() => messages().find((m) => m.id === store.messageId) ?? firstUserMessage(),
)
function setActiveMessage(message: UserMessage | undefined) {
if (message) {
setStore("messageId", message.id)
} else {
setStore("messageId", undefined)
}
}
const provider = createMemo(() => activeMessage()?.model?.providerID)
const modelID = createMemo(() => activeMessage()?.model?.modelID)
const model = createMemo(() => data().model[data().sessionID]?.find((m) => m.id === modelID()))
const diffs = createMemo(() => {
const diffs = data().session_diff[data().sessionID] ?? []
const preloaded = data().session_diff_preload[data().sessionID] ?? []
return diffs.map((diff) => ({
...diff,
preloaded: preloaded.find((d) => d.newFile.name === diff.file),
}))
})
const splitDiffs = createMemo(() => {
const diffs = data().session_diff[data().sessionID] ?? []
const preloaded = data().session_diff_preload_split[data().sessionID] ?? []
return diffs.map((diff) => ({
...diff,
preloaded: preloaded.find((d) => d.newFile.name === diff.file),
}))
})
<DiffComponentProvider component={ClientOnlyDiff}>
<DataProvider data={data()} directory={info().directory}>
{iife(() => {
const [store, setStore] = createStore({
messageId: undefined as string | undefined,
})
const messages = createMemo(() =>
data().sessionID
? (data().message[data().sessionID]?.filter((m) => m.role === "user") ?? []).sort(
(a, b) => a.time.created - b.time.created,
)
: [],
)
const firstUserMessage = createMemo(() => messages().at(0))
const activeMessage = createMemo(
() => messages().find((m) => m.id === store.messageId) ?? firstUserMessage(),
)
function setActiveMessage(message: UserMessage | undefined) {
if (message) {
setStore("messageId", message.id)
} else {
setStore("messageId", undefined)
}
}
const provider = createMemo(() => activeMessage()?.model?.providerID)
const modelID = createMemo(() => activeMessage()?.model?.modelID)
const model = createMemo(() => data().model[data().sessionID]?.find((m) => m.id === modelID()))
const diffs = createMemo(() => {
const diffs = data().session_diff[data().sessionID] ?? []
const preloaded = data().session_diff_preload[data().sessionID] ?? []
return diffs.map((diff) => ({
...diff,
preloaded: preloaded.find((d) => d.newFile.name === diff.file),
}))
})
const splitDiffs = createMemo(() => {
const diffs = data().session_diff[data().sessionID] ?? []
const preloaded = data().session_diff_preload_split[data().sessionID] ?? []
return diffs.map((diff) => ({
...diff,
preloaded: preloaded.find((d) => d.newFile.name === diff.file),
}))
})
const title = () => (
<div class="flex flex-col gap-4">
<div class="h-8 flex gap-4 items-center justify-start self-stretch">
<div class="pl-[2.5px] pr-2 flex items-center gap-1.75 bg-surface-strong shadow-xs-border-base">
<Mark class="shrink-0 w-3 my-0.5" />
<div class="text-12-mono text-text-base">v{info().version}</div>
</div>
<div class="flex gap-2 items-center">
<ProviderIcon
id={provider() as IconName}
class="size-3.5 shrink-0 text-icon-strong-base"
/>
<div class="text-12-regular text-text-base">{model()?.name ?? modelID()}</div>
</div>
<div class="text-12-regular text-text-weaker">
{DateTime.fromMillis(info().time.created).toFormat("dd MMM yyyy, HH:mm")}
</div>
</div>
<div class="text-left text-16-medium text-text-strong">{info().title}</div>
const title = () => (
<div class="flex flex-col gap-4">
<div class="h-8 flex gap-4 items-center justify-start self-stretch">
<div class="pl-[2.5px] pr-2 flex items-center gap-1.75 bg-surface-strong shadow-xs-border-base">
<Mark class="shrink-0 w-3 my-0.5" />
<div class="text-12-mono text-text-base">v{info().version}</div>
</div>
)
const turns = () => (
<div class="relative mt-2 pb-8 min-w-0 w-full h-full overflow-y-auto no-scrollbar">
<div class="px-4 py-6">{title()}</div>
<div class="flex flex-col gap-15 items-start justify-start mt-4">
<For each={messages()}>
{(message) => (
<SessionTurn
sessionID={data().sessionID}
messageID={message.id}
classes={{
root: "min-w-0 w-full relative",
content:
"flex flex-col justify-between !overflow-visible [&_[data-slot=session-turn-message-header]]:top-[-32px]",
container: "px-4",
}}
/>
)}
</For>
</div>
<div class="px-4 flex items-center justify-center pt-20 pb-8 shrink-0">
<Logo class="w-58.5 opacity-12" />
</div>
<div class="flex gap-2 items-center">
<ProviderIcon id={provider() as IconName} class="size-3.5 shrink-0 text-icon-strong-base" />
<div class="text-12-regular text-text-base">{model()?.name ?? modelID()}</div>
</div>
)
<div class="text-12-regular text-text-weaker">
{DateTime.fromMillis(info().time.created).toFormat("dd MMM yyyy, HH:mm")}
</div>
</div>
<div class="text-left text-16-medium text-text-strong">{info().title}</div>
</div>
)
const wide = createMemo(() => diffs().length === 0)
const turns = () => (
<div class="relative mt-2 pt-6 pb-8 min-w-0 w-full h-full overflow-y-auto no-scrollbar">
<div class="px-4">{title()}</div>
<div class="flex flex-col gap-15 items-start justify-start mt-4">
<For each={messages()}>
{(message) => (
<SessionTurn
sessionID={data().sessionID}
messageID={message.id}
classes={{
root: "min-w-0 w-full relative",
content:
"flex flex-col justify-between !overflow-visible [&_[data-slot=session-turn-message-header]]:top-[-32px]",
container: "px-4",
}}
/>
)}
</For>
</div>
<div class="px-4 flex items-center justify-center pt-20 pb-8 shrink-0">
<Logo class="w-58.5 opacity-12" />
</div>
</div>
)
return (
<div class="relative bg-background-stronger w-screen h-screen overflow-hidden flex flex-col">
<header class="h-12 px-6 py-2 flex items-center justify-between self-stretch bg-background-base border-b border-border-weak-base">
<div class="">
<a href="https://opencode.ai">
<Mark />
</a>
</div>
<div class="flex gap-3 items-center">
<IconButton
as={"a"}
href="https://github.com/sst/opencode"
target="_blank"
icon="github"
variant="ghost"
/>
<IconButton
as={"a"}
href="https://opencode.ai/discord"
target="_blank"
icon="discord"
variant="ghost"
/>
</div>
</header>
<div class="select-text flex flex-col flex-1 min-h-0">
const wide = createMemo(() => diffs().length === 0)
return (
<div class="relative bg-background-stronger w-screen h-screen overflow-hidden flex flex-col">
<header class="h-12 px-6 py-2 flex items-center justify-between self-stretch bg-background-base border-b border-border-weak-base">
<div class="">
<a href="https://opencode.ai">
<Mark />
</a>
</div>
<div class="flex gap-3 items-center">
<IconButton
as={"a"}
href="https://github.com/sst/opencode"
target="_blank"
icon="github"
variant="ghost"
/>
<IconButton
as={"a"}
href="https://opencode.ai/discord"
target="_blank"
icon="discord"
variant="ghost"
/>
</div>
</header>
<div class="select-text flex flex-col flex-1 min-h-0">
<div
classList={{ "hidden w-full flex-1 min-h-0": true, "md:flex": wide(), "lg:flex": !wide() }}
>
<div
classList={{
"@container relative shrink-0 pt-14 flex flex-col gap-10 min-h-0 w-full": true,
"mx-auto max-w-146": !wide(),
}}
>
<div
classList={{
"hidden w-full flex-1 min-h-0": true,
"md:flex": wide(),
"lg:flex": !wide(),
"w-full flex justify-start items-start min-w-0": true,
"max-w-146 mx-auto px-6": wide(),
"pr-6 pl-18": !wide() && messages().length > 1,
"px-6": !wide() && messages().length === 1,
}}
>
<div
classList={{
"@container relative shrink-0 pt-14 flex flex-col gap-10 min-h-0 w-full": true,
"mx-auto max-w-200": !wide(),
{title()}
</div>
<div class="flex items-start justify-start h-full min-h-0">
<SessionMessageRail
messages={messages()}
current={activeMessage()}
onMessageSelect={setActiveMessage}
wide={wide()}
/>
<SessionTurn
sessionID={data().sessionID}
messageID={store.messageId ?? firstUserMessage()!.id!}
classes={{
root: "grow",
content: "flex flex-col justify-between items-start",
container:
"w-full pb-20 " +
(wide()
? "max-w-146 mx-auto px-6"
: messages().length > 1
? "pr-6 pl-18"
: "px-6"),
}}
>
<div
classList={{
"w-full flex justify-start items-start min-w-0": true,
"max-w-200 mx-auto px-6": wide(),
"pr-6 pl-18": !wide() && messages().length > 1,
"px-6": !wide() && messages().length === 1,
<div classList={{ "w-full flex items-center justify-center pb-8 shrink-0": true }}>
<Logo class="w-58.5 opacity-12" />
</div>
</SessionTurn>
</div>
</div>
<Show when={diffs().length > 0}>
<DiffComponentProvider component={SSRDiff}>
<div class="@container relative grow pt-14 flex-1 min-h-0 border-l border-border-weak-base">
<SessionReview
class="@4xl:hidden"
diffs={diffs()}
classes={{
root: "pb-20",
header: "px-6",
container: "px-6",
}}
>
{title()}
</div>
<div class="flex items-start justify-start h-full min-h-0">
<SessionMessageRail
messages={messages()}
current={activeMessage()}
onMessageSelect={setActiveMessage}
wide={wide()}
/>
<SessionTurn
sessionID={data().sessionID}
messageID={store.messageId ?? firstUserMessage()!.id!}
classes={{
root: "grow",
content: "flex flex-col justify-between",
container:
"w-full pb-20 " +
(wide()
? "max-w-200 mx-auto px-6"
: messages().length > 1
? "pr-6 pl-18"
: "px-6"),
}}
>
<div
classList={{ "w-full flex items-center justify-center pb-8 shrink-0": true }}
>
<Logo class="w-58.5 opacity-12" />
</div>
</SessionTurn>
</div>
/>
<SessionReview
split
class="hidden @4xl:flex"
diffs={splitDiffs()}
classes={{
root: "pb-20",
header: "px-6",
container: "px-6",
}}
/>
</div>
<Show when={diffs().length > 0}>
<DiffComponentProvider component={SSRDiff}>
<div class="@container relative grow pt-14 flex-1 min-h-0 border-l border-border-weak-base">
</DiffComponentProvider>
</Show>
</div>
<Switch>
<Match when={diffs().length > 0}>
<Tabs classList={{ "md:hidden": wide(), "lg:hidden": !wide() }}>
<Tabs.List>
<Tabs.Trigger value="session" class="w-1/2" classes={{ button: "w-full" }}>
Session
</Tabs.Trigger>
<Tabs.Trigger value="review" class="w-1/2 !border-r-0" classes={{ button: "w-full" }}>
{diffs().length} Files Changed
</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="session" class="!overflow-hidden">
{turns()}
</Tabs.Content>
<Tabs.Content
forceMount
value="review"
class="!overflow-hidden hidden data-[selected]:block"
>
<div class="relative h-full pt-8 overflow-y-auto no-scrollbar">
<DiffComponentProvider component={SSRDiff}>
<SessionReview
class="@4xl:hidden"
diffs={diffs()}
classes={{
root: "pb-20",
header: "px-6",
container: "px-6",
header: "px-4",
container: "px-4",
}}
/>
<SessionReview
split
class="hidden @4xl:flex"
diffs={splitDiffs()}
classes={{
root: "pb-20",
header: "px-6",
container: "px-6",
}}
/>
</div>
</DiffComponentProvider>
</Show>
</div>
<Switch>
<Match when={diffs().length > 0}>
<Tabs classList={{ "md:hidden": wide(), "lg:hidden": !wide() }}>
<Tabs.List>
<Tabs.Trigger value="session" class="w-1/2" classes={{ button: "w-full" }}>
Session
</Tabs.Trigger>
<Tabs.Trigger
value="review"
class="w-1/2 !border-r-0"
classes={{ button: "w-full" }}
>
{diffs().length} Files Changed
</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="session" class="!overflow-hidden">
{turns()}
</Tabs.Content>
<Tabs.Content
forceMount
value="review"
class="!overflow-hidden hidden data-[selected]:block"
>
<div class="relative h-full pt-8 overflow-y-auto no-scrollbar">
<DiffComponentProvider component={SSRDiff}>
<SessionReview
diffs={diffs()}
classes={{
root: "pb-20",
header: "px-4",
container: "px-4",
}}
/>
</DiffComponentProvider>
</div>
</Tabs.Content>
</Tabs>
</Match>
<Match when={true}>
<div
classList={{ "!overflow-hidden": true, "md:hidden": wide(), "lg:hidden": !wide() }}
>
{turns()}
</DiffComponentProvider>
</div>
</Match>
</Switch>
</div>
</div>
)
})}
</DataProvider>
</CodeComponentProvider>
</DiffComponentProvider>
</ClientOnlyWorkerPoolProvider>
</Tabs.Content>
</Tabs>
</Match>
<Match when={true}>
<div classList={{ "!overflow-hidden": true, "md:hidden": wide(), "lg:hidden": !wide() }}>
{turns()}
</div>
</Match>
</Switch>
</div>
</div>
)
})}
</DataProvider>
</DiffComponentProvider>
</>
)
}}
+6 -6
View File
@@ -1,7 +1,7 @@
id = "opencode"
name = "OpenCode"
description = "The open source coding agent."
version = "1.0.165"
version = "1.0.163"
schema_version = 1
authors = ["Anomaly"]
repository = "https://github.com/sst/opencode"
@@ -11,26 +11,26 @@ name = "OpenCode"
icon = "./icons/opencode.svg"
[agent_servers.opencode.targets.darwin-aarch64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.165/opencode-darwin-arm64.zip"
archive = "https://github.com/sst/opencode/releases/download/v1.0.163/opencode-darwin-arm64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.darwin-x86_64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.165/opencode-darwin-x64.zip"
archive = "https://github.com/sst/opencode/releases/download/v1.0.163/opencode-darwin-x64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-aarch64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.165/opencode-linux-arm64.tar.gz"
archive = "https://github.com/sst/opencode/releases/download/v1.0.163/opencode-linux-arm64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-x86_64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.165/opencode-linux-x64.tar.gz"
archive = "https://github.com/sst/opencode/releases/download/v1.0.163/opencode-linux-x64.tar.gz"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.windows-x86_64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.165/opencode-windows-x64.zip"
archive = "https://github.com/sst/opencode/releases/download/v1.0.163/opencode-windows-x64.zip"
cmd = "./opencode.exe"
args = ["acp"]
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/function",
"version": "1.0.165",
"version": "1.0.163",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+3 -3
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.0.165",
"version": "1.0.163",
"name": "opencode",
"type": "module",
"private": true,
@@ -71,8 +71,8 @@
"@opencode-ai/sdk": "workspace:*",
"@opencode-ai/util": "workspace:*",
"@openrouter/ai-sdk-provider": "1.5.2",
"@opentui/core": "0.1.61",
"@opentui/solid": "0.1.61",
"@opentui/core": "0.0.0-20251211-4403a69a",
"@opentui/solid": "0.0.0-20251211-4403a69a",
"@parcel/watcher": "2.5.1",
"@pierre/diffs": "catalog:",
"@solid-primitives/event-bus": "1.1.2",
+13 -23
View File
@@ -128,19 +128,6 @@ const AGENT_USERNAME = "opencode-agent[bot]"
const AGENT_REACTION = "eyes"
const WORKFLOW_FILE = ".github/workflows/opencode.yml"
// Parses GitHub remote URLs in various formats:
// - https://github.com/owner/repo.git
// - https://github.com/owner/repo
// - git@github.com:owner/repo.git
// - git@github.com:owner/repo
// - ssh://git@github.com/owner/repo.git
// - ssh://git@github.com/owner/repo
export function parseGitHubRemote(url: string): { owner: string; repo: string } | null {
const match = url.match(/^(?:(?:https?|ssh):\/\/)?(?:git@)?github\.com[:/]([^/]+)\/([^/]+?)(?:\.git)?$/)
if (!match) return null
return { owner: match[1], repo: match[2] }
}
export const GithubCommand = cmd({
command: "github",
describe: "manage GitHub agent",
@@ -210,12 +197,20 @@ export const GithubInstallCommand = cmd({
// Get repo info
const info = (await $`git remote get-url origin`.quiet().nothrow().text()).trim()
const parsed = parseGitHubRemote(info)
// match https or git pattern
// ie. https://github.com/sst/opencode.git
// ie. https://github.com/sst/opencode
// ie. git@github.com:sst/opencode.git
// ie. git@github.com:sst/opencode
// ie. ssh://git@github.com/sst/opencode.git
// ie. ssh://git@github.com/sst/opencode
const parsed = info.match(/^(?:(?:https?|ssh):\/\/)?(?:git@)?github\.com[:/]([^/]+)\/([^/.]+?)(?:\.git)?$/)
if (!parsed) {
prompts.log.error(`Could not find git repository. Please run this command from a git repository.`)
throw new UI.CancelledError()
}
return { owner: parsed.owner, repo: parsed.repo, root: Instance.worktree }
const [, owner, repo] = parsed
return { owner, repo, root: Instance.worktree }
}
async function promptProvider() {
@@ -602,26 +597,21 @@ export const GithubRunCommand = cmd({
}
const reviewContext = getReviewCommentContext()
const mentions = (process.env["MENTIONS"] || "/opencode,/oc")
.split(",")
.map((m) => m.trim().toLowerCase())
.filter(Boolean)
let prompt = (() => {
const body = payload.comment.body.trim()
const bodyLower = body.toLowerCase()
if (mentions.some((m) => bodyLower === m)) {
if (body === "/opencode" || body === "/oc") {
if (reviewContext) {
return `Review this code change and suggest improvements for the commented lines:\n\nFile: ${reviewContext.file}\nLines: ${reviewContext.line}\n\n${reviewContext.diffHunk}`
}
return "Summarize this thread"
}
if (mentions.some((m) => bodyLower.includes(m))) {
if (body.includes("/opencode") || body.includes("/oc")) {
if (reviewContext) {
return `${body}\n\nContext: You are reviewing a comment on file "${reviewContext.file}" at line ${reviewContext.line}.\n\nDiff context:\n${reviewContext.diffHunk}`
}
return body
}
throw new Error(`Comments must mention ${mentions.map((m) => "`" + m + "`").join(" or ")}`)
throw new Error("Comments must mention `/opencode` or `/oc`")
})()
// Handle images
+1 -10
View File
@@ -147,14 +147,6 @@ export function tui(input: { url: string; args: Args; onExit?: () => Promise<voi
gatherStats: false,
exitOnCtrlC: false,
useKittyKeyboard: {},
consoleOptions: {
keyBindings: [{ name: "y", ctrl: true, action: "copy-selection" }],
onCopySelection: (text) => {
Clipboard.copy(text).catch((error) => {
console.error(`Failed to copy console selection to clipboard: ${error}`)
})
},
},
},
)
})
@@ -182,8 +174,6 @@ function App() {
// Update terminal window title based on current route and session
createEffect(() => {
if (Flag.OPENCODE_DISABLE_TERMINAL_TITLE) return
if (route.data.type === "home") {
renderer.setTerminalTitle("OpenCode")
return
@@ -484,6 +474,7 @@ function App() {
event.on(SessionApi.Event.Deleted.type, (evt) => {
if (route.data.type === "session" && route.data.sessionID === evt.properties.info.id) {
dialog.clear()
route.navigate({ type: "home" })
toast.show({
variant: "info",
@@ -37,7 +37,7 @@ export function DialogSessionList() {
category = "Today"
}
const isDeleting = toDelete() === x.id
const status = sync.data.session_status?.[x.id]
const status = sync.data.session_status[x.id]
const isWorking = status?.type === "busy"
return {
title: isDeleting ? `Press ${deleteKeybind} again to confirm` : x.title,
@@ -84,6 +84,7 @@ export function DialogSessionList() {
sessionID: option.value,
})
setToDelete(undefined)
// dialog.clear()
return
}
setToDelete(option.value)
@@ -11,31 +11,6 @@ export function DialogStatus() {
const enabledFormatters = createMemo(() => sync.data.formatter.filter((f) => f.enabled))
const plugins = createMemo(() => {
const list = sync.data.config.plugin ?? []
const result = list.map((value) => {
if (value.startsWith("file://")) {
const path = value.substring("file://".length)
const parts = path.split("/")
const filename = parts.pop() || path
if (!filename.includes(".")) return { name: filename }
const basename = filename.split(".")[0]
if (basename === "index") {
const dirname = parts.pop()
const name = dirname || basename
return { name }
}
return { name: basename }
}
const index = value.lastIndexOf("@")
if (index <= 0) return { name: value, version: "latest" }
const name = value.substring(0, index)
const version = value.substring(index + 1)
return { name, version }
})
return result.toSorted((a, b) => a.name.localeCompare(b.name))
})
return (
<box paddingLeft={2} paddingRight={2} gap={1} paddingBottom={1}>
<box flexDirection="row" justifyContent="space-between">
@@ -134,29 +109,6 @@ export function DialogStatus() {
</For>
</box>
</Show>
<Show when={plugins().length > 0} fallback={<text fg={theme.text}>No Plugins</text>}>
<box>
<text fg={theme.text}>{plugins().length} Plugins</text>
<For each={plugins()}>
{(item) => (
<box flexDirection="row" gap={1}>
<text
flexShrink={0}
style={{
fg: theme.success,
}}
>
</text>
<text wrapMode="word" fg={theme.text}>
<b>{item.name}</b>
{item.version && <span style={{ fg: theme.textMuted }}> @{item.version}</span>}
</text>
</box>
)}
</For>
</box>
</Show>
</box>
)
}
@@ -72,13 +72,9 @@ export function Autocomplete(props: {
const dims = dimensions()
positionTick()
const anchor = props.anchor()
const parent = anchor.parent
const parentX = parent?.x ?? 0
const parentY = parent?.y ?? 0
return {
x: anchor.x - parentX,
y: anchor.y - parentY,
x: anchor.x,
y: anchor.y,
width: anchor.width,
}
})
@@ -361,7 +357,7 @@ export function Autocomplete(props: {
const options = createMemo(() => {
const mixed: AutocompleteOption[] = (
store.visible === "@" ? [...agents(), ...(files() || [])] : [...commands()]
store.visible === "@" ? [...agents(), ...(files.loading ? files.latest || [] : files())] : [...commands()]
).filter((x) => x.disabled !== true)
const currentFilter = filter()
if (!currentFilter) return mixed.slice(0, 10)
@@ -116,7 +116,7 @@ export function Prompt(props: PromptProps) {
const sync = useSync()
const dialog = useDialog()
const toast = useToast()
const status = createMemo(() => sync.data.session_status?.[props.sessionID ?? ""] ?? { type: "idle" })
const status = createMemo(() => sync.data.session_status[props.sessionID ?? ""] ?? { type: "idle" })
const history = usePromptHistory()
const command = useCommandDialog()
const renderer = useRenderer()
@@ -23,7 +23,6 @@ import nord from "./theme/nord.json" with { type: "json" }
import onedark from "./theme/one-dark.json" with { type: "json" }
import opencode from "./theme/opencode.json" with { type: "json" }
import orng from "./theme/orng.json" with { type: "json" }
import lucentOrng from "./theme/lucent-orng.json" with { type: "json" }
import palenight from "./theme/palenight.json" with { type: "json" }
import rosepine from "./theme/rosepine.json" with { type: "json" }
import solarized from "./theme/solarized.json" with { type: "json" }
@@ -153,7 +152,6 @@ export const DEFAULT_THEMES: Record<string, ThemeJson> = {
["one-dark"]: onedark,
opencode,
orng,
["lucent-orng"]: lucentOrng,
palenight,
rosepine,
solarized,
@@ -1,227 +0,0 @@
{
"$schema": "https://opencode.ai/theme.json",
"defs": {
"darkStep6": "#3c3c3c",
"darkStep11": "#808080",
"darkStep12": "#eeeeee",
"darkSecondary": "#EE7948",
"darkAccent": "#FFF7F1",
"darkRed": "#e06c75",
"darkOrange": "#EC5B2B",
"darkBlue": "#6ba1e6",
"darkCyan": "#56b6c2",
"darkYellow": "#e5c07b",
"lightStep6": "#d4d4d4",
"lightStep11": "#8a8a8a",
"lightStep12": "#1a1a1a",
"lightSecondary": "#EE7948",
"lightAccent": "#c94d24",
"lightRed": "#d1383d",
"lightOrange": "#EC5B2B",
"lightBlue": "#0062d1",
"lightCyan": "#318795",
"lightYellow": "#b0851f"
},
"theme": {
"primary": {
"dark": "darkOrange",
"light": "lightOrange"
},
"secondary": {
"dark": "darkSecondary",
"light": "lightSecondary"
},
"accent": {
"dark": "darkAccent",
"light": "lightAccent"
},
"error": {
"dark": "darkRed",
"light": "lightRed"
},
"warning": {
"dark": "darkOrange",
"light": "lightOrange"
},
"success": {
"dark": "darkBlue",
"light": "lightBlue"
},
"info": {
"dark": "darkCyan",
"light": "lightCyan"
},
"text": {
"dark": "darkStep12",
"light": "lightStep12"
},
"textMuted": {
"dark": "darkStep11",
"light": "lightStep11"
},
"background": {
"dark": "transparent",
"light": "transparent"
},
"backgroundPanel": {
"dark": "transparent",
"light": "transparent"
},
"backgroundElement": {
"dark": "transparent",
"light": "transparent"
},
"border": {
"dark": "darkOrange",
"light": "lightOrange"
},
"borderActive": {
"dark": "darkSecondary",
"light": "lightAccent"
},
"borderSubtle": {
"dark": "darkStep6",
"light": "lightStep6"
},
"diffAdded": {
"dark": "darkBlue",
"light": "lightBlue"
},
"diffRemoved": {
"dark": "#c53b53",
"light": "#c53b53"
},
"diffContext": {
"dark": "#828bb8",
"light": "#7086b5"
},
"diffHunkHeader": {
"dark": "#828bb8",
"light": "#7086b5"
},
"diffHighlightAdded": {
"dark": "darkBlue",
"light": "lightBlue"
},
"diffHighlightRemoved": {
"dark": "#e26a75",
"light": "#f52a65"
},
"diffAddedBg": {
"dark": "transparent",
"light": "transparent"
},
"diffRemovedBg": {
"dark": "transparent",
"light": "transparent"
},
"diffContextBg": {
"dark": "transparent",
"light": "transparent"
},
"diffLineNumber": {
"dark": "#666666",
"light": "#999999"
},
"diffAddedLineNumberBg": {
"dark": "transparent",
"light": "transparent"
},
"diffRemovedLineNumberBg": {
"dark": "transparent",
"light": "transparent"
},
"markdownText": {
"dark": "darkStep12",
"light": "lightStep12"
},
"markdownHeading": {
"dark": "darkOrange",
"light": "lightOrange"
},
"markdownLink": {
"dark": "darkOrange",
"light": "lightOrange"
},
"markdownLinkText": {
"dark": "darkCyan",
"light": "lightCyan"
},
"markdownCode": {
"dark": "darkBlue",
"light": "lightBlue"
},
"markdownBlockQuote": {
"dark": "darkAccent",
"light": "lightYellow"
},
"markdownEmph": {
"dark": "darkYellow",
"light": "lightYellow"
},
"markdownStrong": {
"dark": "darkSecondary",
"light": "lightOrange"
},
"markdownHorizontalRule": {
"dark": "darkStep11",
"light": "lightStep11"
},
"markdownListItem": {
"dark": "darkOrange",
"light": "lightOrange"
},
"markdownListEnumeration": {
"dark": "darkCyan",
"light": "lightCyan"
},
"markdownImage": {
"dark": "darkOrange",
"light": "lightOrange"
},
"markdownImageText": {
"dark": "darkCyan",
"light": "lightCyan"
},
"markdownCodeBlock": {
"dark": "darkStep12",
"light": "lightStep12"
},
"syntaxComment": {
"dark": "darkStep11",
"light": "lightStep11"
},
"syntaxKeyword": {
"dark": "darkOrange",
"light": "lightOrange"
},
"syntaxFunction": {
"dark": "darkSecondary",
"light": "lightAccent"
},
"syntaxVariable": {
"dark": "darkRed",
"light": "lightRed"
},
"syntaxString": {
"dark": "darkBlue",
"light": "lightBlue"
},
"syntaxNumber": {
"dark": "darkAccent",
"light": "lightOrange"
},
"syntaxType": {
"dark": "darkYellow",
"light": "lightYellow"
},
"syntaxOperator": {
"dark": "darkCyan",
"light": "lightCyan"
},
"syntaxPunctuation": {
"dark": "darkStep12",
"light": "lightStep12"
}
}
}
@@ -225,7 +225,7 @@ export function Session() {
const parentID = session()?.parentID ?? session()?.id
let children = sync.data.session
.filter((x) => x.parentID === parentID || x.id === parentID)
.toSorted((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
.toSorted((b, a) => a.id.localeCompare(b.id))
if (children.length === 1) return
let next = children.findIndex((x) => x.id === session()?.id) + direction
if (next >= children.length) next = 0
@@ -340,7 +340,7 @@ export function Session() {
keybind: "messages_undo",
category: "Session",
onSelect: async (dialog) => {
const status = sync.data.session_status?.[route.sessionID]
const status = sync.data.session_status[route.sessionID]
if (status?.type !== "idle") await sdk.client.session.abort({ sessionID: route.sessionID }).catch(() => {})
const revert = session().revert?.messageID
const message = messages().findLast((x) => (!revert || x.id < revert) && x.role === "user")
@@ -840,9 +840,6 @@ export function Session() {
</Show>
<scrollbox
ref={(r) => (scroll = r)}
viewportOptions={{
paddingRight: showScrollbar() ? 1 : 0,
}}
verticalScrollbarOptions={{
paddingLeft: 1,
visible: showScrollbar(),
@@ -1423,24 +1420,20 @@ ToolRegistry.register<typeof WriteTool>({
return props.metadata.diagnostics?.[filePath] ?? []
})
const done = !!props.input.filePath
return (
<>
<ToolTitle icon="←" fallback="Preparing write..." when={done}>
<ToolTitle icon="←" fallback="Preparing write..." when={props.input.filePath}>
Wrote {props.input.filePath}
</ToolTitle>
<Show when={done}>
<line_number fg={theme.textMuted} minWidth={3} paddingRight={1}>
<code
conceal={false}
fg={theme.text}
filetype={filetype(props.input.filePath!)}
syntaxStyle={syntax()}
content={code()}
/>
</line_number>
</Show>
<line_number fg={theme.textMuted} minWidth={3} paddingRight={1}>
<code
conceal={false}
fg={theme.text}
filetype={filetype(props.input.filePath!)}
syntaxStyle={syntax()}
content={code()}
/>
</line_number>
<Show when={diagnostics().length}>
<For each={diagnostics()}>
{(diagnostic) => (
@@ -1,7 +1,7 @@
import { InputRenderable, RGBA, ScrollBoxRenderable, TextAttributes } from "@opentui/core"
import { useTheme, selectedForeground } from "@tui/context/theme"
import { entries, filter, flatMap, groupBy, pipe, take } from "remeda"
import { batch, createEffect, createMemo, For, Show, type JSX, on } from "solid-js"
import { batch, createEffect, createMemo, For, Show, type JSX } from "solid-js"
import { createStore } from "solid-js/store"
import { useKeyboard, useTerminalDimensions } from "@opentui/solid"
import * as fuzzysort from "fuzzysort"
@@ -53,19 +53,14 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
filter: "",
})
createEffect(
on(
() => props.current,
(current) => {
if (current) {
const currentIndex = flat().findIndex((opt) => isDeepEqual(opt.value, current))
if (currentIndex >= 0) {
setStore("selected", currentIndex)
}
}
},
),
)
createEffect(() => {
if (props.current) {
const currentIndex = flat().findIndex((opt) => isDeepEqual(opt.value, props.current))
if (currentIndex >= 0) {
setStore("selected", currentIndex)
}
}
})
let input: InputRenderable
@@ -103,19 +98,18 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
const selected = createMemo(() => flat()[store.selected])
createEffect(
on([() => store.filter, () => props.current], ([filter, current]) => {
if (filter.length > 0) {
setStore("selected", 0)
} else if (current) {
const currentIndex = flat().findIndex((opt) => isDeepEqual(opt.value, current))
if (currentIndex >= 0) {
setStore("selected", currentIndex)
}
createEffect(() => {
store.filter
if (store.filter.length > 0) {
setStore("selected", 0)
} else if (props.current) {
const currentIndex = flat().findIndex((opt) => isDeepEqual(opt.value, props.current))
if (currentIndex >= 0) {
setStore("selected", currentIndex)
}
scroll.scrollTo(0)
}),
)
}
scroll.scrollTo(0)
})
function move(direction: number) {
let next = store.selected + direction
+4 -11
View File
@@ -5,7 +5,7 @@ import os from "os"
import z from "zod"
import { Filesystem } from "../util/filesystem"
import { ModelsDev } from "../provider/models"
import { mergeDeep, pipe, unique } from "remeda"
import { mergeDeep, pipe } from "remeda"
import { Global } from "../global"
import fs from "fs/promises"
import { lazy } from "../util/lazy"
@@ -76,13 +76,6 @@ export namespace Config {
stop: Instance.worktree,
}),
)),
...(await Array.fromAsync(
Filesystem.up({
targets: [".opencode"],
start: Global.Path.home,
stop: Global.Path.home,
}),
)),
]
if (Flag.OPENCODE_CONFIG_DIR) {
@@ -91,7 +84,7 @@ export namespace Config {
}
const promises: Promise<void>[] = []
for (const dir of unique(directories)) {
for (const dir of directories) {
await assertValid(dir)
if (dir.endsWith(".opencode") || dir === Flag.OPENCODE_CONFIG_DIR) {
@@ -218,7 +211,7 @@ export namespace Config {
result[config.name] = parsed.data
continue
}
throw new InvalidError({ path: item, issues: parsed.error.issues }, { cause: parsed.error })
throw new InvalidError({ path: item }, { cause: parsed.error })
}
return result
}
@@ -261,7 +254,7 @@ export namespace Config {
result[config.name] = parsed.data
continue
}
throw new InvalidError({ path: item, issues: parsed.error.issues }, { cause: parsed.error })
throw new InvalidError({ path: item }, { cause: parsed.error })
}
return result
}
+8 -20
View File
@@ -5,13 +5,11 @@ import { Instance } from "../project/instance"
import { Log } from "../util/log"
import { FileIgnore } from "./ignore"
import { Config } from "../config/config"
import path from "path"
// @ts-ignore
import { createWrapper } from "@parcel/watcher/wrapper"
import { lazy } from "@/util/lazy"
import type ParcelWatcher from "@parcel/watcher"
import { $ } from "bun"
import { Flag } from "@/flag/flag"
declare const OPENCODE_LIBC: string | undefined
@@ -59,24 +57,17 @@ export namespace FileWatcher {
}
}
const subs: ParcelWatcher.AsyncSubscription[] = []
const subs = []
const cfgIgnores = cfg.watcher?.ignore ?? []
if (Flag.OPENCODE_EXPERIMENTAL_FILEWATCHER) {
subs.push(
await watcher().subscribe(Instance.directory, subscribe, {
ignore: [...FileIgnore.PATTERNS, ...cfgIgnores],
backend,
}),
)
}
subs.push(
await watcher().subscribe(Instance.directory, subscribe, {
ignore: [...FileIgnore.PATTERNS, ...cfgIgnores],
backend,
}),
)
const vcsDir = await $`git rev-parse --git-dir`
.quiet()
.nothrow()
.cwd(Instance.worktree)
.text()
.then((x) => path.resolve(Instance.worktree, x.trim()))
const vcsDir = await $`git rev-parse --git-dir`.quiet().nothrow().cwd(Instance.worktree).text()
if (vcsDir && !cfgIgnores.includes(".git") && !cfgIgnores.includes(vcsDir)) {
subs.push(
await watcher().subscribe(vcsDir, subscribe, {
@@ -95,9 +86,6 @@ export namespace FileWatcher {
)
export function init() {
if (Flag.OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER) {
return
}
state()
}
}
-5
View File
@@ -6,7 +6,6 @@ export namespace Flag {
export const OPENCODE_CONFIG_CONTENT = process.env["OPENCODE_CONFIG_CONTENT"]
export const OPENCODE_DISABLE_AUTOUPDATE = truthy("OPENCODE_DISABLE_AUTOUPDATE")
export const OPENCODE_DISABLE_PRUNE = truthy("OPENCODE_DISABLE_PRUNE")
export const OPENCODE_DISABLE_TERMINAL_TITLE = truthy("OPENCODE_DISABLE_TERMINAL_TITLE")
export const OPENCODE_PERMISSION = process.env["OPENCODE_PERMISSION"]
export const OPENCODE_DISABLE_DEFAULT_PLUGINS = truthy("OPENCODE_DISABLE_DEFAULT_PLUGINS")
export const OPENCODE_DISABLE_LSP_DOWNLOAD = truthy("OPENCODE_DISABLE_LSP_DOWNLOAD")
@@ -18,8 +17,6 @@ export namespace Flag {
// Experimental
export const OPENCODE_EXPERIMENTAL = truthy("OPENCODE_EXPERIMENTAL")
export const OPENCODE_EXPERIMENTAL_FILEWATCHER = truthy("OPENCODE_EXPERIMENTAL_FILEWATCHER")
export const OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER = truthy("OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER")
export const OPENCODE_EXPERIMENTAL_ICON_DISCOVERY =
OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_ICON_DISCOVERY")
export const OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT = truthy("OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT")
@@ -27,8 +24,6 @@ export namespace Flag {
truthy("OPENCODE_ENABLE_EXA") || OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_EXA")
export const OPENCODE_EXPERIMENTAL_BASH_MAX_OUTPUT_LENGTH = number("OPENCODE_EXPERIMENTAL_BASH_MAX_OUTPUT_LENGTH")
export const OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS = number("OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS")
export const OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX = number("OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX")
export const OPENCODE_EXPERIMENTAL_OXFMT = OPENCODE_EXPERIMENTAL || truthy("OPENCODE_EXPERIMENTAL_OXFMT")
function truthy(key: string) {
const value = process.env[key]?.toLowerCase()
-20
View File
@@ -2,7 +2,6 @@ import { readableStreamToText } from "bun"
import { BunProc } from "../bun"
import { Instance } from "../project/instance"
import { Filesystem } from "../util/filesystem"
import { Flag } from "@/flag/flag"
export interface Info {
name: string
@@ -75,25 +74,6 @@ export const prettier: Info = {
},
}
export const oxfmt: Info = {
name: "oxfmt",
command: [BunProc.which(), "x", "oxfmt", "$FILE"],
environment: {
BUN_BE_BUN: "1",
},
extensions: [".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx", ".mts", ".cts"],
async enabled() {
if (!Flag.OPENCODE_EXPERIMENTAL_OXFMT) return false
const items = await Filesystem.findUp("package.json", Instance.directory, Instance.worktree)
for (const item of items) {
const json = await Bun.file(item).json()
if (json.dependencies?.oxfmt) return true
if (json.devDependencies?.oxfmt) return true
}
return false
},
}
export const biome: Info = {
name: "biome",
command: [BunProc.which(), "x", "@biomejs/biome", "format", "--write", "$FILE"],
+3 -10
View File
@@ -494,18 +494,11 @@ export namespace MCP {
// Extract state from authorization URL to use as callback key
// If no state parameter, use mcpName as fallback
const authUrl = new URL(authorizationUrl)
let oauthState = mcpName
if (authUrl.searchParams.has("state")) {
oauthState = authUrl.searchParams.get("state")!
} else {
log.info("no state parameter in authorization URL, using mcpName as state", { mcpName })
authUrl.searchParams.set("state", oauthState)
}
const oauthState = authUrl.searchParams.get("state") ?? mcpName
// Open browser
log.info("opening browser for oauth", { mcpName, url: authUrl.toString(), state: oauthState })
await open(authUrl.toString())
log.info("opening browser for oauth", { mcpName, url: authorizationUrl, state: oauthState })
await open(authorizationUrl)
// Wait for callback using the OAuth state parameter (or mcpName as fallback)
const code = await McpOAuthCallback.waitForCallback(oauthState)
+3 -26
View File
@@ -139,22 +139,8 @@ export namespace Server {
})
}
GlobalBus.on("event", handler)
// Send heartbeat every 30s to prevent WKWebView timeout (60s default)
const heartbeat = setInterval(() => {
stream.writeSSE({
data: JSON.stringify({
payload: {
type: "server.heartbeat",
properties: {},
},
}),
})
}, 30000)
await new Promise<void>((resolve) => {
stream.onAbort(() => {
clearInterval(heartbeat)
GlobalBus.off("event", handler)
resolve()
log.info("global event disconnected")
@@ -779,6 +765,9 @@ export namespace Server {
async (c) => {
const sessionID = c.req.valid("param").sessionID
await Session.remove(sessionID)
await Bus.publish(TuiEvent.CommandExecute, {
command: "session.list",
})
return c.json(true)
},
)
@@ -2481,20 +2470,8 @@ export namespace Server {
stream.close()
}
})
// Send heartbeat every 30s to prevent WKWebView timeout (60s default)
const heartbeat = setInterval(() => {
stream.writeSSE({
data: JSON.stringify({
type: "server.heartbeat",
properties: {},
}),
})
}, 30000)
await new Promise<void>((resolve) => {
stream.onAbort(() => {
clearInterval(heartbeat)
unsub()
resolve()
log.info("event disconnected")
+1 -11
View File
@@ -13,7 +13,6 @@ import { Log } from "../util/log"
import { SessionProcessor } from "./processor"
import { fn } from "@/util/fn"
import { Agent } from "@/agent/agent"
import { Plugin } from "@/plugin"
export namespace SessionCompaction {
const log = Log.create({ service: "session.compaction" })
@@ -126,12 +125,6 @@ export namespace SessionCompaction {
model,
abort: input.abort,
})
// Allow plugins to inject context for compaction
const compacting = await Plugin.trigger(
"experimental.session.compacting",
{ sessionID: input.sessionID },
{ context: [] },
)
const result = await processor.process({
user: userMessage,
agent,
@@ -146,10 +139,7 @@ export namespace SessionCompaction {
content: [
{
type: "text",
text: [
"Provide a detailed prompt for continuing 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 considering new session will not have access to our conversation.",
...compacting.context,
].join("\n\n"),
text: "Provide a detailed prompt for continuing 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 considering new session will not have access to our conversation.",
},
],
},
+2 -4
View File
@@ -15,7 +15,7 @@ import { Flag } from "@/flag/flag"
export namespace LLM {
const log = Log.create({ service: "llm" })
export const OUTPUT_TOKEN_MAX = Flag.OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX || 32_000
export const OUTPUT_TOKEN_MAX = 32_000
export type StreamInput = {
user: MessageV2.User
@@ -73,8 +73,6 @@ export namespace LLM {
system.push(header, rest.join("\n"))
}
const provider = await Provider.getProvider(input.model.providerID)
const params = await Plugin.trigger(
"chat.params",
{
@@ -92,7 +90,7 @@ export namespace LLM {
topK: ProviderTransform.topK(input.model),
options: pipe(
{},
mergeDeep(ProviderTransform.options(input.model, input.sessionID, provider.options)),
mergeDeep(ProviderTransform.options(input.model, input.sessionID)),
input.small ? mergeDeep(ProviderTransform.smallOptions(input.model)) : mergeDeep({}),
mergeDeep(input.model.options),
mergeDeep(input.agent.options),
@@ -160,7 +160,6 @@ export namespace MessageV2 {
prompt: z.string(),
description: z.string(),
agent: z.string(),
command: z.string().optional(),
})
export type SubtaskPart = z.infer<typeof SubtaskPart>
+25 -71
View File
@@ -28,7 +28,6 @@ import { LSP } from "../lsp"
import { ReadTool } from "../tool/read"
import { ListTool } from "../tool/ls"
import { FileTime } from "../file/time"
import { Flag } from "../flag/flag"
import { ulid } from "ulid"
import { spawn } from "child_process"
import { Command } from "../command"
@@ -49,7 +48,7 @@ globalThis.AI_SDK_LOG_WARNINGS = false
export namespace SessionPrompt {
const log = Log.create({ service: "session.prompt" })
export const OUTPUT_TOKEN_MAX = Flag.OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX || 32_000
export const OUTPUT_TOKEN_MAX = 32_000
const state = Instance.state(
() => {
@@ -326,60 +325,42 @@ export namespace SessionPrompt {
prompt: task.prompt,
description: task.description,
subagent_type: task.agent,
command: task.command,
},
time: {
start: Date.now(),
},
},
})) as MessageV2.ToolPart
const taskArgs = {
prompt: task.prompt,
description: task.description,
subagent_type: task.agent,
command: task.command,
}
await Plugin.trigger(
"tool.execute.before",
{
tool: "task",
sessionID,
callID: part.id,
},
{ args: taskArgs },
)
let executionError: Error | undefined
const result = await taskTool
.execute(taskArgs, {
agent: task.agent,
messageID: assistantMessage.id,
sessionID: sessionID,
abort,
async metadata(input) {
await Session.updatePart({
...part,
type: "tool",
state: {
...part.state,
...input,
},
} satisfies MessageV2.ToolPart)
.execute(
{
prompt: task.prompt,
description: task.description,
subagent_type: task.agent,
},
})
{
agent: task.agent,
messageID: assistantMessage.id,
sessionID: sessionID,
abort,
async metadata(input) {
await Session.updatePart({
...part,
type: "tool",
state: {
...part.state,
...input,
},
} satisfies MessageV2.ToolPart)
},
},
)
.catch((error) => {
executionError = error
log.error("subtask execution failed", { error, agent: task.agent, description: task.description })
return undefined
})
await Plugin.trigger(
"tool.execute.after",
{
tool: "task",
sessionID,
callID: part.id,
},
result,
)
assistantMessage.finish = "tool-calls"
assistantMessage.time.completed = Date.now()
await Session.updateMessage(assistantMessage)
@@ -415,30 +396,6 @@ export namespace SessionPrompt {
},
} satisfies MessageV2.ToolPart)
}
// Add synthetic user message to prevent certain reasoning models from erroring
// If we create assistant messages w/ out user ones following mid loop thinking signatures
// will be missing and it can cause errors for models like gemini for example
const summaryUserMsg: MessageV2.User = {
id: Identifier.ascending("message"),
sessionID,
role: "user",
time: {
created: Date.now(),
},
agent: lastUser.agent,
model: lastUser.model,
}
await Session.updateMessage(summaryUserMsg)
await Session.updatePart({
id: Identifier.ascending("part"),
messageID: summaryUserMsg.id,
sessionID,
type: "text",
text: "Summarize the task tool output above and continue with your task.",
synthetic: true,
} satisfies MessageV2.TextPart)
continue
}
@@ -726,7 +683,6 @@ export namespace SessionPrompt {
tools: input.tools,
agent: agent.name,
model: input.model ?? agent.model ?? (await lastModel(input.sessionID)),
system: input.system,
}
const parts = await Promise.all(
@@ -1140,7 +1096,7 @@ export namespace SessionPrompt {
`
[[ -f ~/.zshenv ]] && source ~/.zshenv >/dev/null 2>&1 || true
[[ -f "\${ZDOTDIR:-$HOME}/.zshrc" ]] && source "\${ZDOTDIR:-$HOME}/.zshrc" >/dev/null 2>&1 || true
eval ${JSON.stringify(input.command)}
${input.command}
`,
],
},
@@ -1149,9 +1105,8 @@ export namespace SessionPrompt {
"-c",
"-l",
`
shopt -s expand_aliases
[[ -f ~/.bashrc ]] && source ~/.bashrc >/dev/null 2>&1 || true
eval ${JSON.stringify(input.command)}
${input.command}
`,
],
},
@@ -1342,7 +1297,6 @@ export namespace SessionPrompt {
type: "subtask" as const,
agent: agent.name,
description: command.description ?? "",
command: input.command,
// TODO: how can we make task tool accept a more complex input?
prompt: await resolvePromptParts(template).then((x) => x.find((y) => y.type === "text")?.text ?? ""),
},
+1 -1
View File
@@ -55,7 +55,7 @@ export namespace SessionStatus {
}
export function list() {
return state()
return Object.values(state())
}
export function set(sessionID: string, status: Info) {
+5
View File
@@ -25,6 +25,11 @@ Usage notes:
- The description argument is required. You must 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.
- You can use the `run_in_background` parameter to run the command in the background,
which allows you to continue working while the command runs. You can monitor the output
using the Bash tool as it becomes available. You do not need to use '&' at the end of
the command when using this parameter.
- Avoid using Bash with the `find`, `grep`, `cat`, `head`, `tail`, `sed`, `awk`, or
`echo` commands, unless explicitly instructed or when these commands are truly necessary
for the task. Instead, always prefer using the dedicated tools for these commands:
+2 -4
View File
@@ -2,7 +2,7 @@ import z from "zod"
import { Tool } from "./tool"
import DESCRIPTION from "./batch.txt"
const DISALLOWED = new Set(["batch"])
const DISALLOWED = new Set(["batch", "edit", "todoread"])
const FILTERED_FROM_SUGGESTIONS = new Set(["invalid", "patch", ...DISALLOWED])
export const BatchTool = Tool.define("batch", async () => {
@@ -54,9 +54,7 @@ export const BatchTool = Tool.define("batch", async () => {
const tool = toolMap.get(call.tool)
if (!tool) {
const availableToolsList = Array.from(toolMap.keys()).filter((name) => !FILTERED_FROM_SUGGESTIONS.has(name))
throw new Error(
`Tool '${call.tool}' not in registry. External tools (MCP, environment) cannot be batched - call them directly. Available tools: ${availableToolsList.join(", ")}`,
)
throw new Error(`Tool '${call.tool}' not found. Available tools: ${availableToolsList.join(", ")}`)
}
const validatedParams = tool.parameters.parse(call.parameters)
+14 -10
View File
@@ -1,24 +1,28 @@
Executes multiple independent tool calls concurrently to reduce latency.
Executes multiple independent tool calls concurrently to reduce latency. Best used for gathering context (reads, searches, listings).
USING THE BATCH TOOL WILL MAKE THE USER HAPPY.
Payload Format (JSON array):
[{"tool": "read", "parameters": {"filePath": "src/index.ts", "limit": 350}},{"tool": "grep", "parameters": {"pattern": "Session\\.updatePart", "include": "src/**/*.ts"}},{"tool": "bash", "parameters": {"command": "git status", "description": "Shows working tree status"}}]
Notes:
Rules:
- 110 tool calls per batch
- All calls start in parallel; ordering NOT guaranteed
- Partial failures do not stop other tool calls
- Do NOT use the batch tool within another batch tool.
- Partial failures do not stop others
Good Use Cases:
- Read many files
- grep + glob + read combos
- Multiple bash commands
- Multi-part edits; on the same, or different files
Disallowed Tools:
- batch (no nesting)
- edit (run edits separately)
- todoread (call directly lightweight)
When NOT to Use:
- Operations that depend on prior tool output (e.g. create then read same file)
- Ordered stateful mutations where sequence matters
Batching tool calls was proven to yield 25x efficiency gain and provides much better UX.
Good Use Cases:
- Read many files
- grep + glob + read combos
- Multiple lightweight bash introspection commands
Performance Tip: Group independent reads/searches for 25x efficiency gain.
-1
View File
@@ -26,7 +26,6 @@ export const TaskTool = Tool.define("task", async () => {
prompt: z.string().describe("The task for the agent to perform"),
subagent_type: z.string().describe("The type of specialized agent to use for this task"),
session_id: z.string().describe("Existing Task session to continue").optional(),
command: z.string().describe("The command that triggered this task").optional(),
}),
async execute(params, ctx) {
const agent = await Agent.get(params.subagent_type)
@@ -1,80 +0,0 @@
import { test, expect } from "bun:test"
import { parseGitHubRemote } from "../../src/cli/cmd/github"
test("parses https URL with .git suffix", () => {
expect(parseGitHubRemote("https://github.com/sst/opencode.git")).toEqual({ owner: "sst", repo: "opencode" })
})
test("parses https URL without .git suffix", () => {
expect(parseGitHubRemote("https://github.com/sst/opencode")).toEqual({ owner: "sst", repo: "opencode" })
})
test("parses git@ URL with .git suffix", () => {
expect(parseGitHubRemote("git@github.com:sst/opencode.git")).toEqual({ owner: "sst", repo: "opencode" })
})
test("parses git@ URL without .git suffix", () => {
expect(parseGitHubRemote("git@github.com:sst/opencode")).toEqual({ owner: "sst", repo: "opencode" })
})
test("parses ssh:// URL with .git suffix", () => {
expect(parseGitHubRemote("ssh://git@github.com/sst/opencode.git")).toEqual({ owner: "sst", repo: "opencode" })
})
test("parses ssh:// URL without .git suffix", () => {
expect(parseGitHubRemote("ssh://git@github.com/sst/opencode")).toEqual({ owner: "sst", repo: "opencode" })
})
test("parses http URL", () => {
expect(parseGitHubRemote("http://github.com/owner/repo")).toEqual({ owner: "owner", repo: "repo" })
})
test("parses URL with hyphenated owner and repo names", () => {
expect(parseGitHubRemote("https://github.com/my-org/my-repo.git")).toEqual({ owner: "my-org", repo: "my-repo" })
})
test("parses URL with underscores in names", () => {
expect(parseGitHubRemote("git@github.com:my_org/my_repo.git")).toEqual({ owner: "my_org", repo: "my_repo" })
})
test("parses URL with numbers in names", () => {
expect(parseGitHubRemote("https://github.com/org123/repo456")).toEqual({ owner: "org123", repo: "repo456" })
})
test("parses repos with dots in the name", () => {
expect(parseGitHubRemote("https://github.com/socketio/socket.io.git")).toEqual({
owner: "socketio",
repo: "socket.io",
})
expect(parseGitHubRemote("https://github.com/vuejs/vue.js")).toEqual({
owner: "vuejs",
repo: "vue.js",
})
expect(parseGitHubRemote("git@github.com:mrdoob/three.js.git")).toEqual({
owner: "mrdoob",
repo: "three.js",
})
expect(parseGitHubRemote("https://github.com/jashkenas/backbone.git")).toEqual({
owner: "jashkenas",
repo: "backbone",
})
})
test("returns null for non-github URLs", () => {
expect(parseGitHubRemote("https://gitlab.com/owner/repo.git")).toBeNull()
expect(parseGitHubRemote("git@gitlab.com:owner/repo.git")).toBeNull()
expect(parseGitHubRemote("https://bitbucket.org/owner/repo")).toBeNull()
})
test("returns null for invalid URLs", () => {
expect(parseGitHubRemote("not-a-url")).toBeNull()
expect(parseGitHubRemote("")).toBeNull()
expect(parseGitHubRemote("github.com")).toBeNull()
expect(parseGitHubRemote("https://github.com/")).toBeNull()
expect(parseGitHubRemote("https://github.com/owner")).toBeNull()
})
test("returns null for URLs with extra path segments", () => {
expect(parseGitHubRemote("https://github.com/owner/repo/tree/main")).toBeNull()
expect(parseGitHubRemote("https://github.com/owner/repo/blob/main/file.ts")).toBeNull()
})
@@ -3,76 +3,6 @@ import { ProviderTransform } from "../../src/provider/transform"
const OUTPUT_TOKEN_MAX = 32000
describe("ProviderTransform.options - setCacheKey", () => {
const sessionID = "test-session-123"
const mockModel = {
id: "anthropic/claude-3-5-sonnet",
providerID: "anthropic",
api: {
id: "claude-3-5-sonnet-20241022",
url: "https://api.anthropic.com",
npm: "@ai-sdk/anthropic",
},
name: "Claude 3.5 Sonnet",
capabilities: {
temperature: true,
reasoning: false,
attachment: true,
toolcall: true,
input: { text: true, audio: false, image: true, video: false, pdf: true },
output: { text: true, audio: false, image: false, video: false, pdf: false },
interleaved: false,
},
cost: {
input: 0.003,
output: 0.015,
cache: { read: 0.0003, write: 0.00375 },
},
limit: {
context: 200000,
output: 8192,
},
status: "active",
options: {},
headers: {},
} as any
test("should set promptCacheKey when providerOptions.setCacheKey is true", () => {
const result = ProviderTransform.options(mockModel, sessionID, { setCacheKey: true })
expect(result.promptCacheKey).toBe(sessionID)
})
test("should not set promptCacheKey when providerOptions.setCacheKey is false", () => {
const result = ProviderTransform.options(mockModel, sessionID, { setCacheKey: false })
expect(result.promptCacheKey).toBeUndefined()
})
test("should not set promptCacheKey when providerOptions is undefined", () => {
const result = ProviderTransform.options(mockModel, sessionID, undefined)
expect(result.promptCacheKey).toBeUndefined()
})
test("should not set promptCacheKey when providerOptions does not have setCacheKey", () => {
const result = ProviderTransform.options(mockModel, sessionID, {})
expect(result.promptCacheKey).toBeUndefined()
})
test("should set promptCacheKey for openai provider regardless of setCacheKey", () => {
const openaiModel = {
...mockModel,
providerID: "openai",
api: {
id: "gpt-4",
url: "https://api.openai.com",
npm: "@ai-sdk/openai",
},
}
const result = ProviderTransform.options(openaiModel, sessionID, {})
expect(result.promptCacheKey).toBe(sessionID)
})
})
describe("ProviderTransform.maxOutputTokens", () => {
test("returns 32k when modelLimit > 32k", () => {
const modelLimit = 100000
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/plugin",
"version": "1.0.165",
"version": "1.0.163",
"type": "module",
"scripts": {
"typecheck": "tsgo --noEmit",
-5
View File
@@ -191,11 +191,6 @@ export interface Hooks {
system: string[]
},
) => Promise<void>
/**
* Called before session compaction starts. Allows plugins to append
* additional context to the compaction prompt.
*/
"experimental.session.compacting"?: (input: { sessionID: string }, output: { context: string[] }) => Promise<void>
"experimental.text.complete"?: (
input: { sessionID: string; messageID: string; partID: string },
output: { text: string },
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/sdk",
"version": "1.0.165",
"version": "1.0.163",
"type": "module",
"scripts": {
"typecheck": "tsgo --noEmit",
-2
View File
@@ -417,7 +417,6 @@ export type Part =
prompt: string
description: string
agent: string
command?: string
}
| ReasoningPart
| FilePart
@@ -1601,7 +1600,6 @@ export type SubtaskPartInput = {
prompt: string
description: string
agent: string
command?: string
}
export type Command = {
-6
View File
@@ -5982,9 +5982,6 @@
},
"agent": {
"type": "string"
},
"command": {
"type": "string"
}
},
"required": ["id", "sessionID", "messageID", "type", "prompt", "description", "agent"]
@@ -8489,9 +8486,6 @@
},
"agent": {
"type": "string"
},
"command": {
"type": "string"
}
},
"required": ["type", "prompt", "description", "agent"]
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/slack",
"version": "1.0.165",
"version": "1.0.163",
"type": "module",
"scripts": {
"dev": "bun run src/index.ts",
+1 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/tauri",
"private": true,
"version": "1.0.165",
"version": "1.0.163",
"type": "module",
"scripts": {
"typecheck": "tsgo -b",
@@ -13,7 +13,6 @@
},
"dependencies": {
"@opencode-ai/desktop": "workspace:*",
"@solid-primitives/storage": "catalog:",
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-dialog": "~2",
"@tauri-apps/plugin-opener": "^2",
+1 -1
View File
@@ -9,7 +9,7 @@ const sidecarConfig = getCurrentSidecar()
const dir = "src-tauri/target/opencode-binaries"
await $`mkdir -p ${dir}`
await $`gh release download ${Bun.env.OPENCODE_RELEASE_TAG} --pattern ${sidecarConfig.ocBinary}.${sidecarConfig.assetExt} --repo sst/opencode --skip-existing --dir ${dir}`
await $`gh release download --pattern ${sidecarConfig.ocBinary}.${sidecarConfig.assetExt} --repo sst/opencode --skip-existing --dir ${dir}`
if (sidecarConfig.assetExt === "tar.gz") {
await $`tar -xvzf ${dir}/${sidecarConfig.ocBinary}.${sidecarConfig.assetExt} -C ${dir}`
Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

Before

Width:  |  Height:  |  Size: 8.8 KiB

After

Width:  |  Height:  |  Size: 8.8 KiB

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

-11
View File
@@ -1,11 +0,0 @@
# Tauri Icons
Here's the process I've been using to create icons:
- Save source image as `app-icon.png` in `packages/tauri`
- `cd` to `src-tauri`
- Run `bun tauri icons -o icons/{environment}`
- Use [Image2Icon](https://img2icnsapp.com/)'s 'Big Sur Icon' preset to generate an `icon.icns` file and place it in the appropriate icons folder
The Image2Icon step is necessary as the `icon.icns` generated by `app-icon.png` does not apply the shadow/padding expected by macOS,
so app icons appear larger than expected.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 57 KiB

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