Compare commits

..

43 Commits

Author SHA1 Message Date
opencode 5199141369 release: v0.11.3 2025-09-24 03:43:46 +00:00
Dax Raad d86d3e7ea1 update copilot auth 2025-09-23 23:36:27 -04:00
GitHub Action fe8d29cb2b chore: format code 2025-09-23 21:59:09 +00:00
Frank edd6198999 zen: refund 2025-09-23 17:58:27 -04:00
GitHub Action c3b2c27997 chore: format code 2025-09-23 21:37:20 +00:00
Jay V 679aeb29f0 docs: add codex to zen 2025-09-23 17:36:40 -04:00
Jay V 190413580f docs: edit 2025-09-23 17:34:52 -04:00
Jay V 8c9fbc7717 docs: edits 2025-09-23 17:34:52 -04:00
Jay V 9d3fdda674 docs: edit 2025-09-23 17:34:52 -04:00
Siddhant Choudhary 449994f120 feat: output-format flag to stream json output (#2471)
Co-authored-by: Siddhant Choudhary <sid@treaps.com>
Co-authored-by: rekram1-node <aidenpcline@gmail.com>
2025-09-23 16:19:32 -05:00
opencode d772fff776 release: v0.11.2 2025-09-23 21:03:59 +00:00
Dax Raad 71b43fd02e fix codex errors 2025-09-23 16:56:40 -04:00
Dax Raad f40b91ab7a fix: remove file existence check from LSP debug and format storage code 2025-09-23 16:49:57 -04:00
Alain Schlesser 6404bd006d ignore: more reliable install script, handle non prettified json responses (#2745)
Co-authored-by: rekram1-node <aidenpcline@gmail.com>
2025-09-23 14:58:04 -05:00
Frank 75157e515c wip: remove 2025-09-23 14:21:39 -04:00
Frank 5a96ee8e1b zen: update logged endpoints 2025-09-23 14:21:39 -04:00
Adam ee6ceb4c64 fix: open text files 2025-09-23 12:43:24 -05:00
David Hill 9d53628e19 Merge branch 'dev' of https://github.com/sst/opencode into dev 2025-09-23 18:36:27 +01:00
David Hill 869b476145 ignore: Copy update 2025-09-23 18:35:42 +01:00
David Hill 223d487787 ignore: Footer update 2025-09-23 18:30:06 +01:00
Adam 5ead6d7dd5 fix: exclude generated css file 2025-09-23 12:08:49 -05:00
GitHub Action a98454217f chore: format code 2025-09-23 17:05:32 +00:00
Adam cbb75d8577 fix: theme css format 2025-09-23 12:04:50 -05:00
GitHub Action 4ab992a9a9 chore: format code 2025-09-23 16:41:57 +00:00
Adam 80b0a93d64 wip: desktop file updates 2025-09-23 11:41:15 -05:00
Grégoire Paris e749d48534 docs: fix grammar mistake in lsp docs(#2744) 2025-09-23 08:12:46 -05:00
GitHub Action d7e873f807 ignore: update download stats 2025-09-23 2025-09-23 12:04:24 +00:00
Aiden Cline c23510346b ignore: lsp debug file check (#2743) 2025-09-22 22:16:03 -05:00
Aiden Cline f9c5df05a1 docs: github copilot model enable note (#2741) 2025-09-22 21:29:16 -05:00
Aiden Cline 02b4d1e2fc fix: lsp extension undefined handle (#2739) 2025-09-22 21:14:55 -05:00
Joseph Hanson 0b36eb8760 docs: fix permissions link (#2738) 2025-09-22 20:28:54 -05:00
Adam Hosker 36bec9948c docs: Add editor setup tip (#2638) 2025-09-22 16:01:27 -04:00
GitHub Action 2db73c39df chore: format code 2025-09-22 19:59:29 +00:00
Jay V 6107666d04 docs: edit tools doc 2025-09-22 15:58:48 -04:00
Aiden Cline cc2bd7141f fix: enforce extensions requirement for custom lsp servers (#2734) 2025-09-22 11:45:47 -05:00
GitHub Action ee442975df ignore: update download stats 2025-09-22 2025-09-22 12:04:25 +00:00
Dax Raad 9b1a508657 ci: bump 2025-09-22 01:47:28 -04:00
Dax Raad 288c977596 ci: snapshot builds 2025-09-22 01:45:23 -04:00
iwauo 6b799b304c feat: add Java LSP server support (#2547)
Co-authored-by: rekram1-node <aidenpcline@gmail.com>
2025-09-21 23:55:15 -05:00
Aiden Cline 92c126d875 fix: lsp spawn logic (#2723) 2025-09-21 11:25:47 -05:00
GitHub Action 7123fbeb47 ignore: update download stats 2025-09-21 2025-09-21 12:04:06 +00:00
opencode 84bb692193 release: v0.11.1 2025-09-21 08:51:11 +00:00
Dax Raad 079095d7a9 core: filter models without keys in opencode provider 2025-09-21 04:43:32 -04:00
58 changed files with 2167 additions and 4105 deletions
+50
View File
@@ -0,0 +1,50 @@
name: snapshot
on:
push:
branches:
- dev
- opentui
concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- run: git fetch --force --tags
- uses: actions/setup-go@v5
with:
go-version: ">=1.24.0"
cache: true
cache-dependency-path: go.sum
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.2.21
- name: Cache ~/.bun
id: cache-bun
uses: actions/cache@v3
with:
path: ~/.bun
key: ${{ runner.os }}-bun-1-2-21-${{ hashFiles('bun.lock') }}
restore-keys: |
${{ runner.os }}-bun-1-2-21-
- name: Install dependencies
run: bun install
- name: Publish
run: |
./packages/opencode/script/publish.ts
env:
OPENCODE_SNAPSHOT: true
OPENCODE_TAG: ${{ github.ref_name }}
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }}
-11
View File
@@ -1,11 +0,0 @@
import { tool } from "@opencode-ai/plugin"
export default tool({
description: "call this tool when you want to give up",
args: {
message: tool.schema.string().describe("give up message"),
},
async execute(args) {
return "Hey fuck you!"
},
})
+3
View File
@@ -85,3 +85,6 @@
| 2025-09-18 | 358,717 (+7,600) | 266,922 (+5,952) | 625,639 (+13,552) |
| 2025-09-19 | 365,401 (+6,684) | 271,859 (+4,937) | 637,260 (+11,621) |
| 2025-09-20 | 372,092 (+6,691) | 276,917 (+5,058) | 649,009 (+11,749) |
| 2025-09-21 | 377,079 (+4,987) | 280,261 (+3,344) | 657,340 (+8,331) |
| 2025-09-22 | 382,492 (+5,413) | 284,009 (+3,748) | 666,501 (+9,161) |
| 2025-09-23 | 387,008 (+4,516) | 289,129 (+5,120) | 676,137 (+9,636) |
+12 -9
View File
@@ -14,11 +14,12 @@
},
"packages/app": {
"name": "@opencode/app",
"version": "0.11.0",
"version": "0.11.3",
"dependencies": {
"@kobalte/core": "0.13.11",
"@opencode-ai/sdk": "workspace:*",
"@shikijs/transformers": "3.9.2",
"@solid-primitives/event-bus": "1.1.2",
"@solid-primitives/resize-observer": "2.1.3",
"@solid-primitives/scroll": "2.1.3",
"@solidjs/router": "0.15.3",
@@ -61,7 +62,7 @@
},
"packages/console/core": {
"name": "@opencode/console-core",
"version": "0.11.0",
"version": "0.11.3",
"dependencies": {
"@aws-sdk/client-sts": "3.782.0",
"@opencode/console-resource": "workspace:*",
@@ -78,7 +79,7 @@
},
"packages/console/function": {
"name": "@opencode/console-function",
"version": "0.11.0",
"version": "0.11.3",
"dependencies": {
"@ai-sdk/anthropic": "2.0.0",
"@ai-sdk/openai": "2.0.2",
@@ -104,7 +105,7 @@
},
"packages/console/scripts": {
"name": "@opencode/console-scripts",
"version": "0.11.0",
"version": "0.11.3",
"dependencies": {
"@opencode/console-core": "workspace:*",
"tsx": "4.20.5",
@@ -116,7 +117,7 @@
},
"packages/function": {
"name": "@opencode/function",
"version": "0.11.0",
"version": "0.11.3",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "22.0.0",
@@ -131,7 +132,7 @@
},
"packages/opencode": {
"name": "opencode",
"version": "0.11.0",
"version": "0.11.3",
"bin": {
"opencode": "./bin/opencode",
},
@@ -182,7 +183,7 @@
},
"packages/plugin": {
"name": "@opencode-ai/plugin",
"version": "0.11.0",
"version": "0.11.3",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"zod": "catalog:",
@@ -194,7 +195,7 @@
},
"packages/sdk/js": {
"name": "@opencode-ai/sdk",
"version": "0.11.0",
"version": "0.11.3",
"dependencies": {
"@hey-api/openapi-ts": "0.81.0",
},
@@ -206,7 +207,7 @@
},
"packages/web": {
"name": "@opencode/web",
"version": "0.11.0",
"version": "0.11.3",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",
@@ -996,6 +997,8 @@
"@smithy/util-utf8": ["@smithy/util-utf8@4.1.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-mEu1/UIXAdNYuBcyEPbjScKi/+MQVXNIuY/7Cm5XLIWe319kDrT5SizBE95jqtmEXoDbGoZxKLCMttdZdqTZKQ=="],
"@solid-primitives/event-bus": ["@solid-primitives/event-bus@1.1.2", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-l+n10/51neGcMaP3ypYt21bXfoeWh8IaC8k7fYuY3ww2a8S1Zv2N2a7FF5Qn+waTu86l0V8/nRHjkyqVIZBYwA=="],
"@solid-primitives/event-listener": ["@solid-primitives/event-listener@2.4.3", "", { "dependencies": { "@solid-primitives/utils": "^6.3.2" }, "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-h4VqkYFv6Gf+L7SQj+Y6puigL/5DIi7x5q07VZET7AWcS+9/G3WfIE9WheniHWJs51OEkRB43w6lDys5YeFceg=="],
"@solid-primitives/keyed": ["@solid-primitives/keyed@1.5.2", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-BgoEdqPw48URnI+L5sZIHdF4ua4Las1eWEBBPaoSFs42kkhnHue+rwCBPL2Z9ebOyQ75sUhUfOETdJfmv0D6Kg=="],
+1 -3
View File
@@ -75,6 +75,7 @@ export const stripeWebhook = new WebhookEndpoint("StripeWebhookEndpoint", {
"checkout.session.async_payment_succeeded",
"checkout.session.completed",
"checkout.session.expired",
"charge.refunded",
"customer.created",
"customer.deleted",
"customer.updated",
@@ -93,9 +94,6 @@ export const stripeWebhook = new WebhookEndpoint("StripeWebhookEndpoint", {
"customer.subscription.resumed",
"customer.subscription.trial_will_end",
"customer.subscription.updated",
"customer.tax_id.created",
"customer.tax_id.deleted",
"customer.tax_id.updated",
],
})
+1 -1
View File
@@ -46,7 +46,7 @@ mkdir -p "$INSTALL_DIR"
if [ -z "$requested_version" ]; then
url="https://github.com/sst/opencode/releases/latest/download/$filename"
specific_version=$(curl -s https://api.github.com/repos/sst/opencode/releases/latest | awk -F'"' '/"tag_name": "/ {gsub(/^v/, "", $4); print $4}')
specific_version=$(curl -s https://api.github.com/repos/sst/opencode/releases/latest | sed -n 's/.*"tag_name": *"v\([^"]*\)".*/\1/p')
if [[ $? -ne 0 || -z "$specific_version" ]]; then
echo -e "${RED}Failed to fetch version information${NC}"
+1
View File
@@ -0,0 +1 @@
src/assets/theme.css
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode/app",
"version": "0.11.0",
"version": "0.11.3",
"description": "",
"type": "module",
"scripts": {
@@ -24,6 +24,7 @@
"@kobalte/core": "0.13.11",
"@opencode-ai/sdk": "workspace:*",
"@shikijs/transformers": "3.9.2",
"@solid-primitives/event-bus": "1.1.2",
"@solid-primitives/resize-observer": "2.1.3",
"@solid-primitives/scroll": "2.1.3",
"@solidjs/router": "0.15.3",
+3 -3
View File
@@ -37,7 +37,7 @@ class ColorResolver {
if (typeof value === "string") {
if (value === "none") return { dark: value, light: value }
if (value.startsWith("#")) {
return { dark: value.toUpperCase(), light: value.toUpperCase() }
return { dark: value.toLowerCase(), light: value.toLowerCase() }
}
const resolved = this.resolveReference(value)
return { dark: resolved, light: resolved }
@@ -57,7 +57,7 @@ class ColorResolver {
if (typeof value === "string") {
if (value === "none") return value
if (value.startsWith("#")) {
return value.toUpperCase()
return value.toLowerCase()
}
return this.resolveReference(value)
}
@@ -72,7 +72,7 @@ class ColorResolver {
if (typeof colorValue === "string") {
if (colorValue === "none") return colorValue
if (colorValue.startsWith("#")) {
return colorValue.toUpperCase()
return colorValue.toLowerCase()
}
return this.resolveReference(colorValue)
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+3 -580
View File
@@ -1,584 +1,6 @@
import { transformerNotationDiff } from "@shikijs/transformers"
import { marked } from "marked"
import markedShiki from "marked-shiki"
import { codeToHtml } from "shiki"
import { useMarked } from "@/context"
import { createResource } from "solid-js"
const markedWithShiki = marked.use(
markedShiki({
highlight(code, lang) {
return codeToHtml(code, {
// structure: "inline",
lang: lang || "text",
tabindex: false,
theme: {
colors: {
"actionBar.toggledBackground": "var(--theme-background-element)",
"activityBarBadge.background": "var(--theme-accent)",
"checkbox.border": "var(--theme-border)",
"editor.background": "transparent",
"editor.foreground": "var(--theme-text)",
"editor.inactiveSelectionBackground": "var(--theme-background-element)",
"editor.selectionHighlightBackground": "var(--theme-border-active)",
"editorIndentGuide.activeBackground1": "var(--theme-border-subtle)",
"editorIndentGuide.background1": "var(--theme-border-subtle)",
"input.placeholderForeground": "var(--theme-text-muted)",
"list.activeSelectionIconForeground": "var(--theme-text)",
"list.dropBackground": "var(--theme-background-element)",
"menu.background": "var(--theme-background-panel)",
"menu.border": "var(--theme-border)",
"menu.foreground": "var(--theme-text)",
"menu.selectionBackground": "var(--theme-primary)",
"menu.separatorBackground": "var(--theme-border)",
"ports.iconRunningProcessForeground": "var(--theme-success)",
"sideBarSectionHeader.background": "transparent",
"sideBarSectionHeader.border": "var(--theme-border-subtle)",
"sideBarTitle.foreground": "var(--theme-text-muted)",
"statusBarItem.remoteBackground": "var(--theme-success)",
"statusBarItem.remoteForeground": "var(--theme-text)",
"tab.lastPinnedBorder": "var(--theme-border-subtle)",
"tab.selectedBackground": "var(--theme-background-element)",
"tab.selectedForeground": "var(--theme-text-muted)",
"terminal.inactiveSelectionBackground": "var(--theme-background-element)",
"widget.border": "var(--theme-border)",
},
displayName: "opencode",
name: "opencode",
semanticHighlighting: true,
semanticTokenColors: {
customLiteral: "var(--theme-syntax-function)",
newOperator: "var(--theme-syntax-operator)",
numberLiteral: "var(--theme-syntax-number)",
stringLiteral: "var(--theme-syntax-string)",
},
tokenColors: [
{
scope: [
"meta.embedded",
"source.groovy.embedded",
"string meta.image.inline.markdown",
"variable.legacy.builtin.python",
],
settings: {
foreground: "var(--theme-text)",
},
},
{
scope: "emphasis",
settings: {
fontStyle: "italic",
},
},
{
scope: "strong",
settings: {
fontStyle: "bold",
},
},
{
scope: "header",
settings: {
foreground: "var(--theme-markdown-heading)",
},
},
{
scope: "comment",
settings: {
foreground: "var(--theme-syntax-comment)",
},
},
{
scope: "constant.language",
settings: {
foreground: "var(--theme-syntax-keyword)",
},
},
{
scope: [
"constant.numeric",
"variable.other.enummember",
"keyword.operator.plus.exponent",
"keyword.operator.minus.exponent",
],
settings: {
foreground: "var(--theme-syntax-number)",
},
},
{
scope: "constant.regexp",
settings: {
foreground: "var(--theme-syntax-operator)",
},
},
{
scope: "entity.name.tag",
settings: {
foreground: "var(--theme-syntax-keyword)",
},
},
{
scope: ["entity.name.tag.css", "entity.name.tag.less"],
settings: {
foreground: "var(--theme-syntax-operator)",
},
},
{
scope: "entity.other.attribute-name",
settings: {
foreground: "var(--theme-syntax-variable)",
},
},
{
scope: [
"entity.other.attribute-name.class.css",
"source.css entity.other.attribute-name.class",
"entity.other.attribute-name.id.css",
"entity.other.attribute-name.parent-selector.css",
"entity.other.attribute-name.parent.less",
"source.css entity.other.attribute-name.pseudo-class",
"entity.other.attribute-name.pseudo-element.css",
"source.css.less entity.other.attribute-name.id",
"entity.other.attribute-name.scss",
],
settings: {
foreground: "var(--theme-syntax-operator)",
},
},
{
scope: "invalid",
settings: {
foreground: "var(--theme-error)",
},
},
{
scope: "markup.underline",
settings: {
fontStyle: "underline",
},
},
{
scope: "markup.bold",
settings: {
fontStyle: "bold",
foreground: "var(--theme-markdown-strong)",
},
},
{
scope: "markup.heading",
settings: {
fontStyle: "bold",
foreground: "var(--theme-markdown-heading)",
},
},
{
scope: "markup.italic",
settings: {
fontStyle: "italic",
},
},
{
scope: "markup.strikethrough",
settings: {
fontStyle: "strikethrough",
},
},
{
scope: "markup.inserted",
settings: {
foreground: "var(--theme-diff-added)",
},
},
{
scope: "markup.deleted",
settings: {
foreground: "var(--theme-diff-removed)",
},
},
{
scope: "markup.changed",
settings: {
foreground: "var(--theme-diff-context)",
},
},
{
scope: "punctuation.definition.quote.begin.markdown",
settings: {
foreground: "var(--theme-markdown-block-quote)",
},
},
{
scope: "punctuation.definition.list.begin.markdown",
settings: {
foreground: "var(--theme-markdown-list-enumeration)",
},
},
{
scope: "markup.inline.raw",
settings: {
foreground: "var(--theme-markdown-code)",
},
},
{
scope: "punctuation.definition.tag",
settings: {
foreground: "var(--theme-syntax-punctuation)",
},
},
{
scope: ["meta.preprocessor", "entity.name.function.preprocessor"],
settings: {
foreground: "var(--theme-syntax-keyword)",
},
},
{
scope: "meta.preprocessor.string",
settings: {
foreground: "var(--theme-syntax-string)",
},
},
{
scope: "meta.preprocessor.numeric",
settings: {
foreground: "var(--theme-syntax-number)",
},
},
{
scope: "meta.structure.dictionary.key.python",
settings: {
foreground: "var(--theme-syntax-variable)",
},
},
{
scope: "meta.diff.header",
settings: {
foreground: "var(--theme-diff-hunk-header)",
},
},
{
scope: "storage",
settings: {
foreground: "var(--theme-syntax-keyword)",
},
},
{
scope: "storage.type",
settings: {
foreground: "var(--theme-syntax-keyword)",
},
},
{
scope: ["storage.modifier", "keyword.operator.noexcept"],
settings: {
foreground: "var(--theme-syntax-keyword)",
},
},
{
scope: ["string", "meta.embedded.assembly"],
settings: {
foreground: "var(--theme-syntax-string)",
},
},
{
scope: "string.tag",
settings: {
foreground: "var(--theme-syntax-string)",
},
},
{
scope: "string.value",
settings: {
foreground: "var(--theme-syntax-string)",
},
},
{
scope: "string.regexp",
settings: {
foreground: "var(--theme-syntax-operator)",
},
},
{
scope: [
"punctuation.definition.template-expression.begin",
"punctuation.definition.template-expression.end",
"punctuation.section.embedded",
],
settings: {
foreground: "var(--theme-syntax-keyword)",
},
},
{
scope: ["meta.template.expression"],
settings: {
foreground: "var(--theme-text)",
},
},
{
scope: [
"support.type.vendored.property-name",
"support.type.property-name",
"source.css variable",
"source.coffee.embedded",
],
settings: {
foreground: "var(--theme-syntax-variable)",
},
},
{
scope: "keyword",
settings: {
foreground: "var(--theme-syntax-keyword)",
},
},
{
scope: "keyword.control",
settings: {
foreground: "var(--theme-syntax-keyword)",
},
},
{
scope: "keyword.operator",
settings: {
foreground: "var(--theme-syntax-operator)",
},
},
{
scope: [
"keyword.operator.new",
"keyword.operator.expression",
"keyword.operator.cast",
"keyword.operator.sizeof",
"keyword.operator.alignof",
"keyword.operator.typeid",
"keyword.operator.alignas",
"keyword.operator.instanceof",
"keyword.operator.logical.python",
"keyword.operator.wordlike",
],
settings: {
foreground: "var(--theme-syntax-keyword)",
},
},
{
scope: "keyword.other.unit",
settings: {
foreground: "var(--theme-syntax-number)",
},
},
{
scope: ["punctuation.section.embedded.begin.php", "punctuation.section.embedded.end.php"],
settings: {
foreground: "var(--theme-syntax-keyword)",
},
},
{
scope: "support.function.git-rebase",
settings: {
foreground: "var(--theme-syntax-variable)",
},
},
{
scope: "constant.sha.git-rebase",
settings: {
foreground: "var(--theme-syntax-number)",
},
},
{
scope: [
"storage.modifier.import.java",
"variable.language.wildcard.java",
"storage.modifier.package.java",
],
settings: {
foreground: "var(--theme-text)",
},
},
{
scope: "variable.language",
settings: {
foreground: "var(--theme-syntax-keyword)",
},
},
{
scope: [
"entity.name.function",
"support.function",
"support.constant.handlebars",
"source.powershell variable.other.member",
"entity.name.operator.custom-literal",
],
settings: {
foreground: "var(--theme-syntax-function)",
},
},
{
scope: [
"support.class",
"support.type",
"entity.name.type",
"entity.name.namespace",
"entity.other.attribute",
"entity.name.scope-resolution",
"entity.name.class",
"storage.type.numeric.go",
"storage.type.byte.go",
"storage.type.boolean.go",
"storage.type.string.go",
"storage.type.uintptr.go",
"storage.type.error.go",
"storage.type.rune.go",
"storage.type.cs",
"storage.type.generic.cs",
"storage.type.modifier.cs",
"storage.type.variable.cs",
"storage.type.annotation.java",
"storage.type.generic.java",
"storage.type.java",
"storage.type.object.array.java",
"storage.type.primitive.array.java",
"storage.type.primitive.java",
"storage.type.token.java",
"storage.type.groovy",
"storage.type.annotation.groovy",
"storage.type.parameters.groovy",
"storage.type.generic.groovy",
"storage.type.object.array.groovy",
"storage.type.primitive.array.groovy",
"storage.type.primitive.groovy",
],
settings: {
foreground: "var(--theme-syntax-type)",
},
},
{
scope: [
"meta.type.cast.expr",
"meta.type.new.expr",
"support.constant.math",
"support.constant.dom",
"support.constant.json",
"entity.other.inherited-class",
"punctuation.separator.namespace.ruby",
],
settings: {
foreground: "var(--theme-syntax-type)",
},
},
{
scope: [
"keyword.control",
"source.cpp keyword.operator.new",
"keyword.operator.delete",
"keyword.other.using",
"keyword.other.directive.using",
"keyword.other.operator",
"entity.name.operator",
],
settings: {
foreground: "var(--theme-syntax-operator)",
},
},
{
scope: [
"variable",
"meta.definition.variable.name",
"support.variable",
"entity.name.variable",
"constant.other.placeholder",
],
settings: {
foreground: "var(--theme-syntax-variable)",
},
},
{
scope: ["variable.other.constant", "variable.other.enummember"],
settings: {
foreground: "var(--theme-syntax-variable)",
},
},
{
scope: ["meta.object-literal.key"],
settings: {
foreground: "var(--theme-syntax-variable)",
},
},
{
scope: [
"support.constant.property-value",
"support.constant.font-name",
"support.constant.media-type",
"support.constant.media",
"constant.other.color.rgb-value",
"constant.other.rgb-value",
"support.constant.color",
],
settings: {
foreground: "var(--theme-syntax-string)",
},
},
{
scope: [
"punctuation.definition.group.regexp",
"punctuation.definition.group.assertion.regexp",
"punctuation.definition.character-class.regexp",
"punctuation.character.set.begin.regexp",
"punctuation.character.set.end.regexp",
"keyword.operator.negation.regexp",
"support.other.parenthesis.regexp",
],
settings: {
foreground: "var(--theme-syntax-string)",
},
},
{
scope: [
"constant.character.character-class.regexp",
"constant.other.character-class.set.regexp",
"constant.other.character-class.regexp",
"constant.character.set.regexp",
],
settings: {
foreground: "var(--theme-syntax-operator)",
},
},
{
scope: ["keyword.operator.or.regexp", "keyword.control.anchor.regexp"],
settings: {
foreground: "var(--theme-syntax-operator)",
},
},
{
scope: "keyword.operator.quantifier.regexp",
settings: {
foreground: "var(--theme-syntax-operator)",
},
},
{
scope: ["constant.character", "constant.other.option"],
settings: {
foreground: "var(--theme-syntax-keyword)",
},
},
{
scope: "constant.character.escape",
settings: {
foreground: "var(--theme-syntax-operator)",
},
},
{
scope: "entity.name.label",
settings: {
foreground: "var(--theme-text-muted)",
},
},
],
type: "dark",
},
transformers: [transformerNotationDiff()],
})
},
}),
)
function strip(text: string): string {
const wrappedRe = /^\s*<([A-Za-z]\w*)>\s*([\s\S]*?)\s*<\/\1>\s*$/
const match = text.match(wrappedRe)
@@ -586,10 +8,11 @@ function strip(text: string): string {
}
export default function Markdown(props: { text: string; class?: string }) {
const marked = useMarked()
const [html] = createResource(
() => strip(props.text),
async (markdown) => {
return markedWithShiki.parse(markdown)
return marked.parse(markdown)
},
)
return (
+34
View File
@@ -0,0 +1,34 @@
import { createContext, useContext, type ParentProps } from "solid-js"
import { createEventBus } from "@solid-primitives/event-bus"
import type { Event as SDKEvent } from "@opencode-ai/sdk"
import { useSDK } from "@/context"
export type Event = SDKEvent // can extend with custom events later
function init() {
const sdk = useSDK()
const bus = createEventBus<Event>()
sdk.event.subscribe().then(async (events) => {
for await (const event of events.stream) {
bus.emit(event)
}
})
return bus
}
type EventContext = ReturnType<typeof init>
const ctx = createContext<EventContext>()
export function EventProvider(props: ParentProps) {
const value = init()
return <ctx.Provider value={value}>{props.children}</ctx.Provider>
}
export function useEvent() {
const value = useContext(ctx)
if (!value) {
throw new Error("useEvent must be used within a EventProvider")
}
return value
}
+3
View File
@@ -1,4 +1,7 @@
export { EventProvider, useEvent } from "./event"
export { LocalProvider, useLocal } from "./local"
export { MarkedProvider, useMarked } from "./marked"
export { SDKProvider, useSDK } from "./sdk"
export { ShikiProvider, useShiki } from "./shiki"
export { SyncProvider, useSync } from "./sync"
export { ThemeProvider, useTheme } from "./theme"
+26 -25
View File
@@ -1,9 +1,8 @@
import { createStore, produce, reconcile } from "solid-js/store"
import { batch, createContext, createEffect, createMemo, useContext, type ParentProps } from "solid-js"
import { useSync } from "./sync"
import { uniqueBy } from "remeda"
import type { FileContent, FileNode } from "@opencode-ai/sdk"
import { useSDK } from "./sdk"
import { useSDK, useEvent, useSync } from "@/context"
export type LocalFile = FileNode &
Partial<{
@@ -165,17 +164,19 @@ function init() {
})
}
const load = async (path: string) =>
sdk.file.read({ query: { path } }).then((x) => {
const load = async (path: string) => {
const relative = path.replace(sync.data.path.directory + "/", "")
sdk.file.read({ query: { path: relative } }).then((x) => {
setStore(
"node",
path,
relative,
produce((draft) => {
draft.loaded = true
draft.content = x.data
}),
)
})
}
const open = async (path: string) => {
const relative = path.replace(sync.data.path.directory + "/", "")
@@ -213,27 +214,27 @@ function init() {
})
}
sdk.event.subscribe().then(async (events) => {
for await (const event of events.stream) {
switch (event.type) {
case "message.part.updated":
const part = event.properties.part
if (part.type === "tool" && part.state.status === "completed") {
switch (part.tool) {
case "read":
console.log("read", part.state.input)
break
case "edit":
const absolute = part.state.input["filePath"] as string
const path = absolute.replace(sync.data.path.directory + "/", "")
load(path)
break
default:
break
}
const bus = useEvent()
bus.listen((event) => {
switch (event.type) {
case "message.part.updated":
const part = event.properties.part
if (part.type === "tool" && part.state.status === "completed") {
switch (part.tool) {
case "read":
console.log("read", part.state.input)
break
case "edit":
load(part.state.input["filePath"] as string)
break
default:
break
}
break
}
}
break
case "file.watcher.updated":
load(event.properties.file)
break
}
})
+43
View File
@@ -0,0 +1,43 @@
import { createContext, useContext, type ParentProps } from "solid-js"
import { useShiki } from "@/context"
import { marked } from "marked"
import markedShiki from "marked-shiki"
import { bundledLanguages, type BundledLanguage } from "shiki"
function init(highlighter: ReturnType<typeof useShiki>) {
return marked.use(
markedShiki({
async highlight(code, lang) {
if (!(lang in bundledLanguages)) {
lang = "text"
}
if (!highlighter.getLoadedLanguages().includes(lang)) {
await highlighter.loadLanguage(lang as BundledLanguage)
}
return highlighter.codeToHtml(code, {
lang: lang || "text",
theme: "opencode",
tabindex: false,
})
},
}),
)
}
type MarkedContext = ReturnType<typeof init>
const ctx = createContext<MarkedContext>()
export function MarkedProvider(props: ParentProps) {
const highlighter = useShiki()
const value = init(highlighter)
return <ctx.Provider value={value}>{props.children}</ctx.Provider>
}
export function useMarked() {
const value = useContext(ctx)
if (!value) {
throw new Error("useMarked must be used within a MarkedProvider")
}
return value
}
+582
View File
@@ -0,0 +1,582 @@
import { createHighlighter, type ThemeInput } from "shiki"
import { createContext, useContext, type ParentProps } from "solid-js"
const theme: ThemeInput = {
colors: {
"actionBar.toggledBackground": "var(--theme-background-element)",
"activityBarBadge.background": "var(--theme-accent)",
"checkbox.border": "var(--theme-border)",
"editor.background": "transparent",
"editor.foreground": "var(--theme-text)",
"editor.inactiveSelectionBackground": "var(--theme-background-element)",
"editor.selectionHighlightBackground": "var(--theme-border-active)",
"editorIndentGuide.activeBackground1": "var(--theme-border-subtle)",
"editorIndentGuide.background1": "var(--theme-border-subtle)",
"input.placeholderForeground": "var(--theme-text-muted)",
"list.activeSelectionIconForeground": "var(--theme-text)",
"list.dropBackground": "var(--theme-background-element)",
"menu.background": "var(--theme-background-panel)",
"menu.border": "var(--theme-border)",
"menu.foreground": "var(--theme-text)",
"menu.selectionBackground": "var(--theme-primary)",
"menu.separatorBackground": "var(--theme-border)",
"ports.iconRunningProcessForeground": "var(--theme-success)",
"sideBarSectionHeader.background": "transparent",
"sideBarSectionHeader.border": "var(--theme-border-subtle)",
"sideBarTitle.foreground": "var(--theme-text-muted)",
"statusBarItem.remoteBackground": "var(--theme-success)",
"statusBarItem.remoteForeground": "var(--theme-text)",
"tab.lastPinnedBorder": "var(--theme-border-subtle)",
"tab.selectedBackground": "var(--theme-background-element)",
"tab.selectedForeground": "var(--theme-text-muted)",
"terminal.inactiveSelectionBackground": "var(--theme-background-element)",
"widget.border": "var(--theme-border)",
},
displayName: "opencode",
name: "opencode",
semanticHighlighting: true,
semanticTokenColors: {
customLiteral: "var(--theme-syntax-function)",
newOperator: "var(--theme-syntax-operator)",
numberLiteral: "var(--theme-syntax-number)",
stringLiteral: "var(--theme-syntax-string)",
},
tokenColors: [
{
scope: [
"meta.embedded",
"source.groovy.embedded",
"string meta.image.inline.markdown",
"variable.legacy.builtin.python",
],
settings: {
foreground: "var(--theme-text)",
},
},
{
scope: "emphasis",
settings: {
fontStyle: "italic",
},
},
{
scope: "strong",
settings: {
fontStyle: "bold",
},
},
{
scope: "header",
settings: {
foreground: "var(--theme-markdown-heading)",
},
},
{
scope: "comment",
settings: {
foreground: "var(--theme-syntax-comment)",
},
},
{
scope: "constant.language",
settings: {
foreground: "var(--theme-syntax-keyword)",
},
},
{
scope: [
"constant.numeric",
"variable.other.enummember",
"keyword.operator.plus.exponent",
"keyword.operator.minus.exponent",
],
settings: {
foreground: "var(--theme-syntax-number)",
},
},
{
scope: "constant.regexp",
settings: {
foreground: "var(--theme-syntax-operator)",
},
},
{
scope: "entity.name.tag",
settings: {
foreground: "var(--theme-syntax-keyword)",
},
},
{
scope: ["entity.name.tag.css", "entity.name.tag.less"],
settings: {
foreground: "var(--theme-syntax-operator)",
},
},
{
scope: "entity.other.attribute-name",
settings: {
foreground: "var(--theme-syntax-variable)",
},
},
{
scope: [
"entity.other.attribute-name.class.css",
"source.css entity.other.attribute-name.class",
"entity.other.attribute-name.id.css",
"entity.other.attribute-name.parent-selector.css",
"entity.other.attribute-name.parent.less",
"source.css entity.other.attribute-name.pseudo-class",
"entity.other.attribute-name.pseudo-element.css",
"source.css.less entity.other.attribute-name.id",
"entity.other.attribute-name.scss",
],
settings: {
foreground: "var(--theme-syntax-operator)",
},
},
{
scope: "invalid",
settings: {
foreground: "var(--theme-error)",
},
},
{
scope: "markup.underline",
settings: {
fontStyle: "underline",
},
},
{
scope: "markup.bold",
settings: {
fontStyle: "bold",
foreground: "var(--theme-markdown-strong)",
},
},
{
scope: "markup.heading",
settings: {
fontStyle: "bold",
foreground: "var(--theme-markdown-heading)",
},
},
{
scope: "markup.italic",
settings: {
fontStyle: "italic",
},
},
{
scope: "markup.strikethrough",
settings: {
fontStyle: "strikethrough",
},
},
{
scope: "markup.inserted",
settings: {
foreground: "var(--theme-diff-added)",
},
},
{
scope: "markup.deleted",
settings: {
foreground: "var(--theme-diff-removed)",
},
},
{
scope: "markup.changed",
settings: {
foreground: "var(--theme-diff-context)",
},
},
{
scope: "punctuation.definition.quote.begin.markdown",
settings: {
foreground: "var(--theme-markdown-block-quote)",
},
},
{
scope: "punctuation.definition.list.begin.markdown",
settings: {
foreground: "var(--theme-markdown-list-enumeration)",
},
},
{
scope: "markup.inline.raw",
settings: {
foreground: "var(--theme-markdown-code)",
},
},
{
scope: "punctuation.definition.tag",
settings: {
foreground: "var(--theme-syntax-punctuation)",
},
},
{
scope: ["meta.preprocessor", "entity.name.function.preprocessor"],
settings: {
foreground: "var(--theme-syntax-keyword)",
},
},
{
scope: "meta.preprocessor.string",
settings: {
foreground: "var(--theme-syntax-string)",
},
},
{
scope: "meta.preprocessor.numeric",
settings: {
foreground: "var(--theme-syntax-number)",
},
},
{
scope: "meta.structure.dictionary.key.python",
settings: {
foreground: "var(--theme-syntax-variable)",
},
},
{
scope: "meta.diff.header",
settings: {
foreground: "var(--theme-diff-hunk-header)",
},
},
{
scope: "storage",
settings: {
foreground: "var(--theme-syntax-keyword)",
},
},
{
scope: "storage.type",
settings: {
foreground: "var(--theme-syntax-keyword)",
},
},
{
scope: ["storage.modifier", "keyword.operator.noexcept"],
settings: {
foreground: "var(--theme-syntax-keyword)",
},
},
{
scope: ["string", "meta.embedded.assembly"],
settings: {
foreground: "var(--theme-syntax-string)",
},
},
{
scope: "string.tag",
settings: {
foreground: "var(--theme-syntax-string)",
},
},
{
scope: "string.value",
settings: {
foreground: "var(--theme-syntax-string)",
},
},
{
scope: "string.regexp",
settings: {
foreground: "var(--theme-syntax-operator)",
},
},
{
scope: [
"punctuation.definition.template-expression.begin",
"punctuation.definition.template-expression.end",
"punctuation.section.embedded",
],
settings: {
foreground: "var(--theme-syntax-keyword)",
},
},
{
scope: ["meta.template.expression"],
settings: {
foreground: "var(--theme-text)",
},
},
{
scope: [
"support.type.vendored.property-name",
"support.type.property-name",
"source.css variable",
"source.coffee.embedded",
],
settings: {
foreground: "var(--theme-syntax-variable)",
},
},
{
scope: "keyword",
settings: {
foreground: "var(--theme-syntax-keyword)",
},
},
{
scope: "keyword.control",
settings: {
foreground: "var(--theme-syntax-keyword)",
},
},
{
scope: "keyword.operator",
settings: {
foreground: "var(--theme-syntax-operator)",
},
},
{
scope: [
"keyword.operator.new",
"keyword.operator.expression",
"keyword.operator.cast",
"keyword.operator.sizeof",
"keyword.operator.alignof",
"keyword.operator.typeid",
"keyword.operator.alignas",
"keyword.operator.instanceof",
"keyword.operator.logical.python",
"keyword.operator.wordlike",
],
settings: {
foreground: "var(--theme-syntax-keyword)",
},
},
{
scope: "keyword.other.unit",
settings: {
foreground: "var(--theme-syntax-number)",
},
},
{
scope: ["punctuation.section.embedded.begin.php", "punctuation.section.embedded.end.php"],
settings: {
foreground: "var(--theme-syntax-keyword)",
},
},
{
scope: "support.function.git-rebase",
settings: {
foreground: "var(--theme-syntax-variable)",
},
},
{
scope: "constant.sha.git-rebase",
settings: {
foreground: "var(--theme-syntax-number)",
},
},
{
scope: ["storage.modifier.import.java", "variable.language.wildcard.java", "storage.modifier.package.java"],
settings: {
foreground: "var(--theme-text)",
},
},
{
scope: "variable.language",
settings: {
foreground: "var(--theme-syntax-keyword)",
},
},
{
scope: [
"entity.name.function",
"support.function",
"support.constant.handlebars",
"source.powershell variable.other.member",
"entity.name.operator.custom-literal",
],
settings: {
foreground: "var(--theme-syntax-function)",
},
},
{
scope: [
"support.class",
"support.type",
"entity.name.type",
"entity.name.namespace",
"entity.other.attribute",
"entity.name.scope-resolution",
"entity.name.class",
"storage.type.numeric.go",
"storage.type.byte.go",
"storage.type.boolean.go",
"storage.type.string.go",
"storage.type.uintptr.go",
"storage.type.error.go",
"storage.type.rune.go",
"storage.type.cs",
"storage.type.generic.cs",
"storage.type.modifier.cs",
"storage.type.variable.cs",
"storage.type.annotation.java",
"storage.type.generic.java",
"storage.type.java",
"storage.type.object.array.java",
"storage.type.primitive.array.java",
"storage.type.primitive.java",
"storage.type.token.java",
"storage.type.groovy",
"storage.type.annotation.groovy",
"storage.type.parameters.groovy",
"storage.type.generic.groovy",
"storage.type.object.array.groovy",
"storage.type.primitive.array.groovy",
"storage.type.primitive.groovy",
],
settings: {
foreground: "var(--theme-syntax-type)",
},
},
{
scope: [
"meta.type.cast.expr",
"meta.type.new.expr",
"support.constant.math",
"support.constant.dom",
"support.constant.json",
"entity.other.inherited-class",
"punctuation.separator.namespace.ruby",
],
settings: {
foreground: "var(--theme-syntax-type)",
},
},
{
scope: [
"keyword.control",
"source.cpp keyword.operator.new",
"keyword.operator.delete",
"keyword.other.using",
"keyword.other.directive.using",
"keyword.other.operator",
"entity.name.operator",
],
settings: {
foreground: "var(--theme-syntax-operator)",
},
},
{
scope: [
"variable",
"meta.definition.variable.name",
"support.variable",
"entity.name.variable",
"constant.other.placeholder",
],
settings: {
foreground: "var(--theme-syntax-variable)",
},
},
{
scope: ["variable.other.constant", "variable.other.enummember"],
settings: {
foreground: "var(--theme-syntax-variable)",
},
},
{
scope: ["meta.object-literal.key"],
settings: {
foreground: "var(--theme-syntax-variable)",
},
},
{
scope: [
"support.constant.property-value",
"support.constant.font-name",
"support.constant.media-type",
"support.constant.media",
"constant.other.color.rgb-value",
"constant.other.rgb-value",
"support.constant.color",
],
settings: {
foreground: "var(--theme-syntax-string)",
},
},
{
scope: [
"punctuation.definition.group.regexp",
"punctuation.definition.group.assertion.regexp",
"punctuation.definition.character-class.regexp",
"punctuation.character.set.begin.regexp",
"punctuation.character.set.end.regexp",
"keyword.operator.negation.regexp",
"support.other.parenthesis.regexp",
],
settings: {
foreground: "var(--theme-syntax-string)",
},
},
{
scope: [
"constant.character.character-class.regexp",
"constant.other.character-class.set.regexp",
"constant.other.character-class.regexp",
"constant.character.set.regexp",
],
settings: {
foreground: "var(--theme-syntax-operator)",
},
},
{
scope: ["keyword.operator.or.regexp", "keyword.control.anchor.regexp"],
settings: {
foreground: "var(--theme-syntax-operator)",
},
},
{
scope: "keyword.operator.quantifier.regexp",
settings: {
foreground: "var(--theme-syntax-operator)",
},
},
{
scope: ["constant.character", "constant.other.option"],
settings: {
foreground: "var(--theme-syntax-keyword)",
},
},
{
scope: "constant.character.escape",
settings: {
foreground: "var(--theme-syntax-operator)",
},
},
{
scope: "entity.name.label",
settings: {
foreground: "var(--theme-text-muted)",
},
},
],
type: "dark",
}
const highlighter = await createHighlighter({
themes: [theme],
langs: [],
})
type ShikiContext = typeof highlighter
const ctx = createContext<ShikiContext>()
export function ShikiProvider(props: ParentProps) {
return <ctx.Provider value={highlighter}>{props.children}</ctx.Provider>
}
export function useShiki() {
const value = useContext(ctx)
if (!value) {
throw new Error("useShiki must be used within a ShikiProvider")
}
return value
}
+52 -54
View File
@@ -1,7 +1,7 @@
import type { Message, Agent, Provider, Session, Part, Config, Path, File, FileNode } from "@opencode-ai/sdk"
import { createStore, produce, reconcile } from "solid-js/store"
import { useSDK } from "./sdk"
import { createContext, Show, useContext, type ParentProps } from "solid-js"
import { useSDK, useEvent } from "@/context"
import { Binary } from "@/utils/binary"
function init() {
@@ -33,69 +33,67 @@ function init() {
changes: [],
})
const sdk = useSDK()
sdk.event.subscribe().then(async (events) => {
for await (const event of events.stream) {
switch (event.type) {
case "session.updated": {
const result = Binary.search(store.session, event.properties.info.id, (s) => s.id)
if (result.found) {
setStore("session", result.index, reconcile(event.properties.info))
break
}
setStore(
"session",
produce((draft) => {
draft.splice(result.index, 0, event.properties.info)
}),
)
const bus = useEvent()
bus.listen((event) => {
switch (event.type) {
case "session.updated": {
const result = Binary.search(store.session, event.properties.info.id, (s) => s.id)
if (result.found) {
setStore("session", result.index, reconcile(event.properties.info))
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)
}),
)
setStore(
"session",
produce((draft) => {
draft.splice(result.index, 0, event.properties.info)
}),
)
break
}
case "message.updated": {
const messages = store.message[event.properties.info.sessionID]
if (!messages) {
setStore("message", event.properties.info.sessionID, [event.properties.info])
break
}
case "message.part.updated": {
const parts = store.part[event.properties.part.messageID]
if (!parts) {
setStore("part", event.properties.part.messageID, [event.properties.part])
break
}
const result = Binary.search(parts, event.properties.part.id, (p) => p.id)
if (result.found) {
setStore("part", event.properties.part.messageID, result.index, reconcile(event.properties.part))
break
}
setStore(
"part",
event.properties.part.messageID,
produce((draft) => {
draft.splice(result.index, 0, event.properties.part)
}),
)
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.part.updated": {
const parts = store.part[event.properties.part.messageID]
if (!parts) {
setStore("part", event.properties.part.messageID, [event.properties.part])
break
}
const result = Binary.search(parts, event.properties.part.id, (p) => p.id)
if (result.found) {
setStore("part", event.properties.part.messageID, result.index, reconcile(event.properties.part))
break
}
setStore(
"part",
event.properties.part.messageID,
produce((draft) => {
draft.splice(result.index, 0, event.properties.part)
}),
)
break
}
}
})
const sdk = useSDK()
Promise.all([
sdk.config.providers().then((x) => setStore("provider", x.data!.providers)),
sdk.path.get().then((x) => setStore("path", x.data!)),
+24 -10
View File
@@ -4,7 +4,15 @@ import { Router, Route } from "@solidjs/router"
import "@/index.css"
import Layout from "@/pages/layout"
import Home from "@/pages"
import { SDKProvider, SyncProvider, LocalProvider, ThemeProvider } from "@/context"
import {
EventProvider,
SDKProvider,
SyncProvider,
LocalProvider,
ThemeProvider,
ShikiProvider,
MarkedProvider,
} from "@/context"
const root = document.getElementById("root")
@@ -18,15 +26,21 @@ render(
() => (
<div class="h-full bg-background text-text-muted">
<ThemeProvider defaultTheme="opencode" defaultDarkMode={true}>
<SDKProvider>
<SyncProvider>
<LocalProvider>
<Router root={Layout}>
<Route path="/" component={Home} />
</Router>
</LocalProvider>
</SyncProvider>
</SDKProvider>
<ShikiProvider>
<MarkedProvider>
<SDKProvider>
<EventProvider>
<SyncProvider>
<LocalProvider>
<Router root={Layout}>
<Route path="/" component={Home} />
</Router>
</LocalProvider>
</SyncProvider>
</EventProvider>
</SDKProvider>
</MarkedProvider>
</ShikiProvider>
</ThemeProvider>
</div>
),
+1 -1
View File
@@ -7,7 +7,7 @@
"dev:remote": "VITE_AUTH_URL=https://auth.dev.opencode.ai bun sst shell --stage=dev bun dev",
"build": "vinxi build && ../../opencode/script/schema.ts ./.output/public/config.json",
"start": "vinxi start",
"version": "0.11.0"
"version": "0.11.3"
},
"dependencies": {
"@ibm/plex": "6.4.1",
@@ -40,6 +40,10 @@
&[data-slot="payment-amount"] {
color: var(--color-text);
&[data-refunded="true"] {
text-decoration: line-through;
}
}
}
@@ -85,7 +85,9 @@ export function PaymentSection() {
{formatDateForTable(date)}
</td>
<td data-slot="payment-id">{payment.id}</td>
<td data-slot="payment-amount">${((payment.amount ?? 0) / 100000000).toFixed(2)}</td>
<td data-slot="payment-amount" data-refunded={!!payment.timeRefunded}>
${((payment.amount ?? 0) / 100000000).toFixed(2)}
</td>
<td data-slot="payment-receipt">
<button
onClick={async () => {
+1 -1
View File
@@ -175,7 +175,7 @@ export default function Home() {
<div data-component="legal">
<span>
©2025 <a href="https://anoma.ly">Anomaly Innovations</a>
©2025 <a href="https://anoma.ly">Anomaly</a>
</span>
</div>
</main>
@@ -1,6 +1,6 @@
import { Billing } from "@opencode/console-core/billing.js"
import type { APIEvent } from "@solidjs/start/server"
import { Database, eq, sql } from "@opencode/console-core/drizzle/index.js"
import { and, Database, eq, sql } from "@opencode/console-core/drizzle/index.js"
import { BillingTable, PaymentTable } from "@opencode/console-core/schema/billing.sql.js"
import { Identifier } from "@opencode/console-core/identifier.js"
import { centsToMicroCents } from "@opencode/console-core/util/price.js"
@@ -91,6 +91,39 @@ export async function POST(input: APIEvent) {
})
})
}
if (body.type === "charge.refunded") {
const customerID = body.data.object.customer as string
const paymentIntentID = body.data.object.payment_intent as string
if (!customerID) throw new Error("Customer ID not found")
if (!paymentIntentID) throw new Error("Payment ID not found")
const workspaceID = await Database.use((tx) =>
tx
.select({
workspaceID: BillingTable.workspaceID,
})
.from(BillingTable)
.where(eq(BillingTable.customerID, customerID))
.then((rows) => rows[0]?.workspaceID),
)
if (!workspaceID) throw new Error("Workspace ID not found")
await Database.transaction(async (tx) => {
await tx
.update(PaymentTable)
.set({
timeRefunded: new Date(body.created * 1000),
})
.where(and(eq(PaymentTable.paymentID, paymentIntentID), eq(PaymentTable.workspaceID, workspaceID)))
await tx
.update(BillingTable)
.set({
balance: sql`${BillingTable.balance} - ${centsToMicroCents(Billing.CHARGE_AMOUNT)}`,
})
.where(eq(BillingTable.workspaceID, workspaceID))
})
}
console.log("finished handling")
@@ -0,0 +1 @@
ALTER TABLE `payment` ADD `time_refunded` timestamp(3);
@@ -0,0 +1,650 @@
{
"version": "5",
"dialect": "mysql",
"id": "45b67fb4-77ce-4aa2-b883-1971429c69f5",
"prevId": "7f3989cb-3e8b-430e-a0f5-f87051d1d824",
"tables": {
"account": {
"name": "account",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"time_created": {
"name": "time_created",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"email": {
"name": "email",
"columns": ["email"],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraint": {}
},
"billing": {
"name": "billing",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"workspace_id": {
"name": "workspace_id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"time_created": {
"name": "time_created",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"customer_id": {
"name": "customer_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"payment_method_id": {
"name": "payment_method_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"payment_method_last4": {
"name": "payment_method_last4",
"type": "varchar(4)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"balance": {
"name": "balance",
"type": "bigint",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"monthly_limit": {
"name": "monthly_limit",
"type": "int",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"monthly_usage": {
"name": "monthly_usage",
"type": "bigint",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"time_monthly_usage_updated": {
"name": "time_monthly_usage_updated",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"reload": {
"name": "reload",
"type": "boolean",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"reload_error": {
"name": "reload_error",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"time_reload_error": {
"name": "time_reload_error",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"time_reload_locked_till": {
"name": "time_reload_locked_till",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {
"global_customer_id": {
"name": "global_customer_id",
"columns": ["customer_id"],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {
"billing_workspace_id_id_pk": {
"name": "billing_workspace_id_id_pk",
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
"checkConstraint": {}
},
"payment": {
"name": "payment",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"workspace_id": {
"name": "workspace_id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"time_created": {
"name": "time_created",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"customer_id": {
"name": "customer_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"payment_id": {
"name": "payment_id",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"amount": {
"name": "amount",
"type": "bigint",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"time_refunded": {
"name": "time_refunded",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"payment_workspace_id_id_pk": {
"name": "payment_workspace_id_id_pk",
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
"checkConstraint": {}
},
"usage": {
"name": "usage",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"workspace_id": {
"name": "workspace_id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"time_created": {
"name": "time_created",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"model": {
"name": "model",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"provider": {
"name": "provider",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"input_tokens": {
"name": "input_tokens",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"output_tokens": {
"name": "output_tokens",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"reasoning_tokens": {
"name": "reasoning_tokens",
"type": "int",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"cache_read_tokens": {
"name": "cache_read_tokens",
"type": "int",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"cache_write_5m_tokens": {
"name": "cache_write_5m_tokens",
"type": "int",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"cache_write_1h_tokens": {
"name": "cache_write_1h_tokens",
"type": "int",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"cost": {
"name": "cost",
"type": "bigint",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"usage_workspace_id_id_pk": {
"name": "usage_workspace_id_id_pk",
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
"checkConstraint": {}
},
"key": {
"name": "key",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"workspace_id": {
"name": "workspace_id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"time_created": {
"name": "time_created",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"actor": {
"name": "actor",
"type": "json",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"old_name": {
"name": "old_name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"key": {
"name": "key",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"time_used": {
"name": "time_used",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {
"global_key": {
"name": "global_key",
"columns": ["key"],
"isUnique": true
},
"name": {
"name": "name",
"columns": ["workspace_id", "name"],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {
"key_workspace_id_id_pk": {
"name": "key_workspace_id_id_pk",
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
"checkConstraint": {}
},
"user": {
"name": "user",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"workspace_id": {
"name": "workspace_id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"time_created": {
"name": "time_created",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"time_seen": {
"name": "time_seen",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"color": {
"name": "color",
"type": "int",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {
"user_email": {
"name": "user_email",
"columns": ["workspace_id", "email"],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {
"user_workspace_id_id_pk": {
"name": "user_workspace_id_id_pk",
"columns": ["workspace_id", "id"]
}
},
"uniqueConstraints": {},
"checkConstraint": {}
},
"workspace": {
"name": "workspace",
"columns": {
"id": {
"name": "id",
"type": "varchar(30)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"slug": {
"name": "slug",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"time_created": {
"name": "time_created",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
},
"time_updated": {
"name": "time_updated",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)"
},
"time_deleted": {
"name": "time_deleted",
"type": "timestamp(3)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {
"slug": {
"name": "slug",
"columns": ["slug"],
"isUnique": true
}
},
"foreignKeys": {},
"compositePrimaryKeys": {
"workspace_id": {
"name": "workspace_id",
"columns": ["id"]
}
},
"uniqueConstraints": {},
"checkConstraint": {}
}
},
"views": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"tables": {},
"indexes": {}
}
}
@@ -113,6 +113,13 @@
"when": 1758428484500,
"tag": "0015_cloudy_revanche",
"breakpoints": true
},
{
"idx": 16,
"version": "5",
"when": 1758663086739,
"tag": "0016_cold_la_nuit",
"breakpoints": true
}
]
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode/console-core",
"version": "0.11.0",
"version": "0.11.3",
"private": true,
"type": "module",
"dependencies": {
@@ -30,6 +30,7 @@ export const PaymentTable = mysqlTable(
customerID: varchar("customer_id", { length: 255 }),
paymentID: varchar("payment_id", { length: 255 }),
amount: bigint("amount", { mode: "number" }).notNull(),
timeRefunded: utc("time_refunded"),
},
(table) => [...workspaceIndexes(table)],
)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode/console-function",
"version": "0.11.0",
"version": "0.11.3",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
@@ -9,7 +9,12 @@ export default {
if (event.event.request.method !== "POST") continue
const url = new URL(event.event.request.url)
if (url.pathname !== "/zen/v1/chat/completions") return
if (
url.pathname !== "/zen/v1/chat/completions" &&
url.pathname !== "/zen/v1/messages" &&
url.pathname !== "/zen/v1/responses"
)
return
let metrics = {
event_type: "completions",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode/console-scripts",
"version": "0.11.0",
"version": "0.11.3",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode/function",
"version": "0.11.0",
"version": "0.11.3",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "0.11.0",
"version": "0.11.3",
"name": "opencode",
"type": "module",
"private": true,
+3 -3
View File
@@ -29,9 +29,9 @@ for (const [os, arch] of targets) {
console.log(`building ${os}-${arch}`)
const name = `${pkg.name}-${os}-${arch}`
await $`mkdir -p dist/${name}/bin`
await $`CGO_ENABLED=0 GOOS=${os} GOARCH=${GOARCH[arch]} go build -ldflags="-s -w -X main.Version=${version}" -o ../opencode/dist/${name}/bin/tui ../tui/cmd/opencode/main.go`.cwd(
"../tui",
)
await $`CGO_ENABLED=0 GOOS=${os} GOARCH=${GOARCH[arch]} go build -ldflags="-s -w -X main.Version=${version}" -o ../opencode/dist/${name}/bin/tui ../tui/cmd/opencode/main.go`
.cwd("../tui")
.quiet()
await Bun.build({
compile: {
target: `bun-${os}-${arch}` as any,
+12 -10
View File
@@ -5,10 +5,14 @@ import { $ } from "bun"
import pkg from "../package.json"
const dry = process.env["OPENCODE_DRY"] === "true"
const version = process.env["OPENCODE_VERSION"]!
const snapshot = process.env["OPENCODE_SNAPSHOT"] === "true"
const npmTag = snapshot ? "snapshot" : "latest"
let version = process.env["OPENCODE_VERSION"]
const tag = process.env["OPENCODE_TAG"] ?? (snapshot ? "snapshot" : "latest")
if (!version && snapshot) {
version = `0.0.0-${tag}-${new Date().toISOString().slice(0, 16).replace(/[-:T]/g, "")}`
process.env["OPENCODE_VERSION"] = version
}
if (!version) throw new Error("OPENCODE_VERSION is required")
console.log(`publishing ${version}`)
@@ -41,12 +45,10 @@ await Bun.file(`./dist/${pkg.name}/package.json`).write(
2,
),
)
if (!dry) {
for (const [name] of Object.entries(binaries)) {
await $`cd dist/${name} && chmod 777 -R . && bun publish --access public --tag ${npmTag}`
}
await $`cd ./dist/${pkg.name} && bun publish --access public --tag ${npmTag}`
for (const [name] of Object.entries(binaries)) {
await $`cd dist/${name} && chmod 777 -R . && bun publish --access public --tag ${tag}`
}
await $`cd ./dist/${pkg.name} && bun publish --access public --tag ${tag}`
if (!snapshot) {
for (const key of Object.keys(binaries)) {
@@ -142,7 +144,7 @@ if (!snapshot) {
await $`cd ./dist/aur-${pkg} && makepkg --printsrcinfo > .SRCINFO`
await $`cd ./dist/aur-${pkg} && git add PKGBUILD .SRCINFO`
await $`cd ./dist/aur-${pkg} && git commit -m "Update to v${version}"`
if (!dry) await $`cd ./dist/aur-${pkg} && git push`
await $`cd ./dist/aur-${pkg} && git push`
}
// Homebrew formula
@@ -201,5 +203,5 @@ if (!snapshot) {
await Bun.file("./dist/homebrew-tap/opencode.rb").write(homebrewFormula)
await $`cd ./dist/homebrew-tap && git add opencode.rb`
await $`cd ./dist/homebrew-tap && git commit -m "Update to v${version}"`
if (!dry) await $`cd ./dist/homebrew-tap && git push`
await $`cd ./dist/homebrew-tap && git push`
}
+24
View File
@@ -64,6 +64,12 @@ export const RunCommand = cmd({
type: "string",
describe: "agent to use",
})
.option("format", {
type: "string",
choices: ["default", "json"],
default: "default",
describe: "format: default (formatted) or json (raw JSON events)",
})
},
handler: async (args) => {
let message = args.message.join(" ")
@@ -144,6 +150,20 @@ export const RunCommand = cmd({
)
}
function outputJsonEvent(type: string, data: any) {
if (args.format === "json") {
const jsonEvent = {
type,
timestamp: Date.now(),
sessionID: session?.id,
...data,
}
process.stdout.write(JSON.stringify(jsonEvent) + "\n")
return true
}
return false
}
let text = ""
Bus.subscribe(MessageV2.Event.PartUpdated, async (evt) => {
@@ -152,6 +172,7 @@ export const RunCommand = cmd({
const part = evt.properties.part
if (part.type === "tool" && part.state.status === "completed") {
if (outputJsonEvent("tool_use", { part })) return
const [tool, color] = TOOL[part.tool] ?? [part.tool, UI.Style.TEXT_INFO_BOLD]
const title =
part.state.title ||
@@ -169,6 +190,7 @@ export const RunCommand = cmd({
text = part.text
if (part.time?.end) {
if (outputJsonEvent("text", { part })) return
UI.empty()
UI.println(UI.markdown(text))
UI.empty()
@@ -189,6 +211,7 @@ export const RunCommand = cmd({
}
errorMsg = errorMsg ? errorMsg + "\n" + err : err
if (outputJsonEvent("error", { error })) return
UI.error(err)
})
@@ -225,6 +248,7 @@ export const RunCommand = cmd({
const isPiped = !process.stdout.isTTY
if (isPiped) {
const match = result.parts.findLast((x: any) => x.type === "text") as any
if (outputJsonEvent("text", { text: match })) return
if (match) process.stdout.write(UI.markdown(match.text))
if (errorMsg) process.stdout.write(errorMsg)
}
+17 -1
View File
@@ -14,6 +14,7 @@ import { Flag } from "../flag/flag"
import { Auth } from "../auth"
import { type ParseError as JsoncParseError, parse as parseJsonc, printParseErrorCode } from "jsonc-parser"
import { Instance } from "../project/instance"
import { LSPServer } from "../lsp/server"
export namespace Config {
const log = Log.create({ service: "config" })
@@ -482,7 +483,22 @@ export namespace Config {
}),
]),
)
.optional(),
.optional()
.refine(
(data) => {
if (!data) return true
const serverIds = new Set(Object.values(LSPServer).map((s) => s.id))
return Object.entries(data).every(([id, config]) => {
if (config.disabled) return true
if (serverIds.has(id)) return true
return Boolean(config.extensions)
})
},
{
error: "For custom LSP servers, 'extensions' array is required.",
},
),
instructions: z.array(z.string()).optional().describe("Additional instruction files or patterns to include"),
layout: Layout.optional().describe("@deprecated Always uses stretch layout."),
permission: z
-1
View File
@@ -28,7 +28,6 @@ export namespace FileWatcher {
const ignore = (cfg.watcher?.ignore ?? []).map((v) => new Bun.Glob(v))
const watcher = chokidar.watch(Instance.directory, {
ignoreInitial: true,
awaitWriteFinish: true,
ignored: (filepath) => {
return FileIgnore.match(filepath, {
extra: ignore,
+16 -7
View File
@@ -72,7 +72,7 @@ export namespace LSP {
...existing,
id: name,
root: existing?.root ?? (async () => Instance.directory),
extensions: item.extensions ?? existing.extensions,
extensions: item.extensions ?? existing?.extensions ?? [],
spawn: async (root) => {
return {
process: spawn(item.command[0], item.command.slice(1), {
@@ -126,13 +126,22 @@ export namespace LSP {
result.push(match)
continue
}
log.info("spawning lsp server", { serverID: server.id })
const handle = await server.spawn(root).catch((err) => {
s.broken.add(root + server.id)
log.error(`Failed to spawn LSP server ${server.id}`, { error: err })
return undefined
})
const handle = await server
.spawn(root)
.then((h) => {
if (h === undefined) {
s.broken.add(root + server.id)
}
return h
})
.catch((err) => {
s.broken.add(root + server.id)
log.error(`Failed to spawn LSP server ${server.id}`, { error: err })
return undefined
})
if (!handle) continue
log.info("spawned lsp server", { serverID: server.id })
const client = await LSPClient.create({
serverID: server.id,
server: handle,
+88
View File
@@ -1,5 +1,6 @@
import { spawn, type ChildProcessWithoutNullStreams } from "child_process"
import path from "path"
import os from "os"
import { Global } from "../global"
import { Log } from "../util/log"
import { BunProc } from "../bun"
@@ -706,4 +707,91 @@ export namespace LSPServer {
}
},
}
export const JDTLS: Info = {
id: "jdtls",
root: NearestRoot(["pom.xml", "build.gradle", "build.gradle.kts", ".project", ".classpath"]),
extensions: [".java"],
async spawn(root) {
const java = Bun.which("java")
if (!java) {
log.error("Java 21 or newer is required to run the JDTLS. Please install it first.")
return
}
const javaMajorVersion = await $`java -version`
.quiet()
.nothrow()
.then(({ stderr }) => {
const m = /"(\d+)\.\d+\.\d+"/.exec(stderr.toString())
return !m ? undefined : parseInt(m[1])
})
if (javaMajorVersion == null || javaMajorVersion < 21) {
log.error("JDTLS requires at least Java 21.")
return
}
const distPath = path.join(Global.Path.bin, "jdtls")
const launcherDir = path.join(distPath, "plugins")
const installed = await fs.exists(launcherDir)
if (!installed) {
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
log.info("Downloading JDTLS LSP server.")
await fs.mkdir(distPath, { recursive: true })
const releaseURL =
"https://www.eclipse.org/downloads/download.php?file=/jdtls/snapshots/jdt-language-server-latest.tar.gz"
const archivePath = path.join(distPath, "release.tar.gz")
await $`curl -L -o '${archivePath}' '${releaseURL}'`.quiet().nothrow()
await $`tar -xzf ${archivePath}`.cwd(distPath).quiet().nothrow()
await fs.rm(archivePath, { force: true })
}
const jarFileName = await $`ls org.eclipse.equinox.launcher_*.jar`
.cwd(launcherDir)
.quiet()
.nothrow()
.then(({ stdout }) => stdout.toString().trim())
const launcherJar = path.join(launcherDir, jarFileName)
if (!(await fs.exists(launcherJar))) {
log.error(`Failed to locate the JDTLS launcher module in the installed directory: ${distPath}.`)
return
}
const configFile = path.join(
distPath,
(() => {
switch (process.platform) {
case "darwin":
return "config_mac"
case "linux":
return "config_linux"
case "win32":
return "config_windows"
default:
return "config_linux"
}
})(),
)
const dataDir = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-jdtls-data"))
return {
process: spawn(
java,
[
"-jar",
launcherJar,
"-configuration",
configFile,
"-data",
dataDir,
"-Declipse.application=org.eclipse.jdt.ls.core.id1",
"-Dosgi.bundles.defaultStartLevel=4",
"-Declipse.product=org.eclipse.jdt.ls.core.product",
"-Dlog.level=ALL",
"--add-modules=ALL-SYSTEM",
"--add-opens java.base/java.util=ALL-UNNAMED",
"--add-opens java.base/java.lang=ALL-UNNAMED",
],
{
cwd: root,
},
),
}
},
}
}
+1 -1
View File
@@ -27,7 +27,7 @@ export namespace Plugin {
}
const plugins = [...(config.plugin ?? [])]
if (!Flag.OPENCODE_DISABLE_DEFAULT_PLUGINS) {
plugins.push("opencode-copilot-auth@0.0.2")
plugins.push("opencode-copilot-auth@0.0.3")
plugins.push("opencode-anthropic-auth@0.0.2")
}
for (let plugin of plugins) {
+14 -4
View File
@@ -16,10 +16,7 @@ import { Flag } from "../flag/flag"
export namespace Provider {
const log = Log.create({ service: "provider" })
type CustomLoader = (
provider: ModelsDev.Provider,
api?: string,
) => Promise<{
type CustomLoader = (provider: ModelsDev.Provider) => Promise<{
autoload: boolean
getModel?: (sdk: any, modelID: string) => Promise<any>
options?: Record<string, any>
@@ -40,6 +37,19 @@ export namespace Provider {
}
},
async opencode(input) {
const hasKey = await (async () => {
if (input.env.some((item) => process.env[item])) return true
if (await Auth.get(input.id)) return true
return false
})()
if (!hasKey) {
for (const [key, value] of Object.entries(input.models)) {
if (value.cost.input === 0) continue
delete input.models[key]
}
}
return {
autoload: Object.keys(input.models).length > 0,
options: {},
+3 -3
View File
@@ -92,14 +92,14 @@ export namespace ProviderTransform {
}
if (modelID.includes("gpt-5") && !modelID.includes("gpt-5-chat")) {
result["reasoningEffort"] = "high"
result["reasoningEffort"] = "medium"
if (providerID !== "azure") {
result["textVerbosity"] = "low"
result["textVerbosity"] = modelID.includes("codex") ? "medium" : "low"
}
if (providerID === "opencode") {
result["promptCacheKey"] = sessionID
// result["include"] = ["reasoning.encrypted_content"]
// result["reasoningSummary"] = "detailed"
// result["reasoningSummary"] = "auto"
}
}
return result
+4 -1
View File
@@ -14,7 +14,10 @@ export namespace Storage {
const MIGRATIONS: Migration[] = [
async (dir) => {
const project = path.resolve(dir, "../project")
for await (const projectDir of new Bun.Glob("*").scan({ cwd: project, onlyFiles: false })) {
for await (const projectDir of new Bun.Glob("*").scan({
cwd: project,
onlyFiles: false,
})) {
log.info(`migrating project ${projectDir}`)
let projectID = projectDir
const fullProjectDir = path.join(project, projectDir)
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/plugin",
"version": "0.11.0",
"version": "0.11.3",
"type": "module",
"scripts": {
"typecheck": "tsc --noEmit",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/sdk",
"version": "0.11.0",
"version": "0.11.3",
"type": "module",
"scripts": {
"typecheck": "tsc --noEmit",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@opencode/web",
"type": "module",
"version": "0.11.0",
"version": "0.11.3",
"scripts": {
"dev": "astro dev",
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
+1 -3
View File
@@ -495,9 +495,7 @@ For quick reference, here are common setups.
}
```
See the full permissions guide for more patterns.
- /docs/permissions
See the full [permissions guide](/docs/permissions) for more patterns.
---
+2
View File
@@ -144,6 +144,8 @@ opencode models
This command displays all models available across your configured providers in the format `provider/model`.
This is useful for figuring out the exact model name to use in [your config](/docs/config/).
---
### run
+54 -52
View File
@@ -1,20 +1,32 @@
---
title: Custom tools
description: Create custom tools to extend opencode capabilities.
title: Custom Tools
description: Create tools the LLM can call in opencode.
---
Custom tools are functions you create that the LLM can call during conversations. They work alongside opencode's built-in tools like `read`, `write`, and `bash`.
---
## Tool structure
## Creating a tool
Tools are defined as `.ts/.js` files in the `.opencode/tool/` directory. They
can also be defined globally in `~/.config/opencode/tool/`.
Tools are defined as **TypeScript** or **JavaScript** files.
The easiest way to create tools is using the `tool()` helper which provides type safety and validation. Use `tool.schema` (which is just [Zod](https://zod.dev)) to define argument types:
---
```ts title=".opencode/tool/database.ts"
### Location
They can defined:
- Locally by placing them in the `.opencode/tool/` directory of your project.
- Or globally, by placing them in `~/.config/opencode/tool/`.
---
### Structure
The easiest way to create tools is using the `tool()` helper which provides type-safety and validation.
```ts title=".opencode/tool/database.ts" {1}
import { tool } from "@opencode-ai/plugin"
export default tool({
@@ -29,9 +41,23 @@ export default tool({
})
```
The **filename** becomes the **tool name**. The above creates a `database` tool.
---
### Arguments
You can use `tool.schema`, which is just [Zod](https://zod.dev), to define argument types.
```ts "tool.schema"
args: {
query: tool.schema.string().describe("SQL query to execute")
}
```
You can also import [Zod](https://zod.dev) directly and return a plain object:
```ts
```ts {6}
import { z } from "zod"
export default {
@@ -46,13 +72,31 @@ export default {
}
```
The filename becomes the tool name. This creates a `database` tool.
---
## Context
Tools receive context about the current session:
```ts title=".opencode/tool/project.ts" {8}
import { tool } from "@opencode-ai/plugin"
export default tool({
description: "Get project information",
args: {},
async execute(args, context) {
// Access context information
const { agent, sessionID, messageID } = context
return `Agent: ${agent}, Session: ${sessionID}, Message: ${messageID}`
},
})
```
---
## Multiple tools per file
You can export multiple tools from a single file. Each export becomes a separate tool with the name `<filename>_<exportname>`:
You can also export multiple tools from a single file. Each export becomes **a separate tool** with the name **`<filename>_<exportname>`**:
```ts title=".opencode/tool/math.ts"
import { tool } from "@opencode-ai/plugin"
@@ -81,45 +125,3 @@ export const multiply = tool({
```
This creates two tools: `math_add` and `math_multiply`.
---
## Arguments
Use `tool.schema` (which is just [Zod](https://zod.dev)) to define tool arguments with validation and descriptions:
```ts title=".opencode/tool/calculator.ts"
import { tool } from "@opencode-ai/plugin"
export default tool({
description: "Perform mathematical calculations",
args: {
expression: tool.schema.string().describe("Mathematical expression to evaluate"),
precision: tool.schema.number().optional().describe("Decimal precision"),
},
async execute(args) {
// Your calculation logic here
return `Result: ${eval(args.expression).toFixed(args.precision || 2)}`
},
})
```
---
## Context
Tools receive context about the current session:
```ts title=".opencode/tool/project.ts"
import { tool } from "@opencode-ai/plugin"
export default tool({
description: "Get project information",
args: {},
async execute(args, context) {
// Access context information
const { agent, sessionID, messageID } = context
return `Agent: ${agent}, Session: ${sessionID}, Message: ${messageID}`
},
})
```
+2
View File
@@ -24,6 +24,8 @@ To install opencode on VS Code and popular forks like Cursor, Windsurf, VSCodium
2. Open the integrated terminal
3. Run `opencode` - the extension installs automatically
If on the other hand you want to use your own IDE when you run `/editor` or `/export` from the TUI, you'll need to set `export EDITOR="code --wait"`. [Learn more](/docs/tui/#editor-setup).
---
### Manual Install
+1 -1
View File
@@ -37,7 +37,7 @@ The easiest way to install opencode is through the install script.
curl -fsSL https://opencode.ai/install | bash
```
You can also install it with the following:
You can also install it with the following commands:
- **Using Node.js**
+1 -1
View File
@@ -3,7 +3,7 @@ title: LSP Servers
description: opencode integrates with your LSP servers.
---
opencode integrates with your Language Server Protocol (LSP) to help the LLM interacts with your codebase. It uses diagnostics to provide feedback to the LLM.
opencode integrates with your Language Server Protocol (LSP) to help the LLM interact with your codebase. It uses diagnostics to provide feedback to the LLM.
---
@@ -356,6 +356,8 @@ To use your GitHub Copilot subscription with opencode:
:::note
Some models might need a [Pro+
subscription](https://github.com/features/copilot/plans) to use.
Some models need to be manually enabled in your [GitHub Copilot settings](https://docs.github.com/en/copilot/how-tos/use-ai-models/configure-access-to-ai-models#setup-for-individual-use).
:::
1. Run `opencode auth login` and select GitHub Copilot.
+12 -3
View File
@@ -185,6 +185,9 @@ Redo a previously undone message. Only available after using `/undo`.
Any file changes will also be restored.
:::
Internally, this uses Git to manage the file changes. So your project **needs to
be a Git repository**.
```bash frame="none"
/redo
```
@@ -237,6 +240,9 @@ Undo last message in the conversation. Removes the most recent user message, all
Any file changes made will also be reverted.
:::
Internally, this uses Git to manage the file changes. So your project **needs to
be a Git repository**.
```bash frame="none"
/undo
```
@@ -266,7 +272,8 @@ Both the `/editor` and `/export` commands use the editor specified in your `EDIT
export EDITOR=nano
export EDITOR=vim
# For GUI editors (VS Code, Cursor, VSCodium, Windsurf, Zed, etc.) include --wait
# For GUI editors, VS Code, Cursor, VSCodium, Windsurf, Zed, etc.
# include --wait
export EDITOR="code --wait"
```
@@ -279,7 +286,8 @@ Both the `/editor` and `/export` commands use the editor specified in your `EDIT
```bash
set EDITOR=notepad
# For GUI editors (VS Code, Cursor, VSCodium, Windsurf, Zed, etc.) include --wait
# For GUI editors, VS Code, Cursor, VSCodium, Windsurf, Zed, etc.
# include --wait
set EDITOR=code --wait
```
@@ -292,7 +300,8 @@ Both the `/editor` and `/export` commands use the editor specified in your `EDIT
```powershell
$env:EDITOR = "notepad"
# For GUI editors (VS Code, Cursor, VSCodium, Windsurf, Zed, etc.) include --wait
# For GUI editors, VS Code, Cursor, VSCodium, Windsurf, Zed, etc.
# include --wait
$env:EDITOR = "code --wait"
```
+3 -1
View File
@@ -64,7 +64,8 @@ You can also access our models through the following API endpoints.
| Model | Mode ID | Endpoint | AI SDK Package |
| ---------------- | ---------------- | --------------------------------------------- | --------------------------- |
| GPT-5 | gpt-5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5 | gpt-5 | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| GPT 5 Codex | gpt-5-codex | `https://opencode.ai/zen/v1/responses` | `@ai-sdk/openai` |
| Claude Sonnet 4 | claude-sonnet-4 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| Claude Haiku 3.5 | claude-3-5-haiku | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
| Claude Opus 4.1 | claude-opus-4-1 | `https://opencode.ai/zen/v1/messages` | `@ai-sdk/anthropic` |
@@ -88,6 +89,7 @@ We support a pay-as-you-go model. Below are the prices **per 1M tokens**.
| Claude Haiku 3.5 | $0.80 | $4.00 | $0.08 | $1.00 |
| Claude Opus 4.1 | $15.00 | $75.00 | $1.50 | $18.75 |
| GPT 5 | $1.25 | $10.00 | $0.125 | - |
| GPT 5 Codex | $1.25 | $10.00 | $0.125 | - |
| Kimi K2 | $0.60 | $2.50 | - | - |
:::note
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "opencode",
"displayName": "opencode",
"description": "opencode for VS Code",
"version": "0.11.0",
"version": "0.11.3",
"publisher": "sst-dev",
"repository": {
"type": "git",