Merge branch 'dev' into llm-centralization

This commit is contained in:
Dax Raad 2025-12-14 16:48:51 -05:00
commit a5914f4d7c
56 changed files with 772 additions and 279 deletions

View file

@ -64,6 +64,12 @@ jobs:
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: "24" node-version: "24"

View file

@ -168,3 +168,5 @@
| 2025-12-10 | 1,025,891 (+14,403) | 991,708 (+17,786) | 2,017,599 (+32,189) | | 2025-12-10 | 1,025,891 (+14,403) | 991,708 (+17,786) | 2,017,599 (+32,189) |
| 2025-12-11 | 1,045,110 (+19,219) | 1,010,559 (+18,851) | 2,055,669 (+38,070) | | 2025-12-11 | 1,045,110 (+19,219) | 1,010,559 (+18,851) | 2,055,669 (+38,070) |
| 2025-12-12 | 1,061,340 (+16,230) | 1,030,838 (+20,279) | 2,092,178 (+36,509) | | 2025-12-12 | 1,061,340 (+16,230) | 1,030,838 (+20,279) | 2,092,178 (+36,509) |
| 2025-12-13 | 1,073,561 (+12,221) | 1,044,608 (+13,770) | 2,118,169 (+25,991) |
| 2025-12-14 | 1,082,042 (+8,481) | 1,052,425 (+7,817) | 2,134,467 (+16,298) |

View file

@ -20,7 +20,7 @@
}, },
"packages/console/app": { "packages/console/app": {
"name": "@opencode-ai/console-app", "name": "@opencode-ai/console-app",
"version": "1.0.152", "version": "1.0.153",
"dependencies": { "dependencies": {
"@cloudflare/vite-plugin": "1.15.2", "@cloudflare/vite-plugin": "1.15.2",
"@ibm/plex": "6.4.1", "@ibm/plex": "6.4.1",
@ -48,7 +48,7 @@
}, },
"packages/console/core": { "packages/console/core": {
"name": "@opencode-ai/console-core", "name": "@opencode-ai/console-core",
"version": "1.0.152", "version": "1.0.153",
"dependencies": { "dependencies": {
"@aws-sdk/client-sts": "3.782.0", "@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1", "@jsx-email/render": "1.1.1",
@ -75,7 +75,7 @@
}, },
"packages/console/function": { "packages/console/function": {
"name": "@opencode-ai/console-function", "name": "@opencode-ai/console-function",
"version": "1.0.152", "version": "1.0.153",
"dependencies": { "dependencies": {
"@ai-sdk/anthropic": "2.0.0", "@ai-sdk/anthropic": "2.0.0",
"@ai-sdk/openai": "2.0.2", "@ai-sdk/openai": "2.0.2",
@ -99,7 +99,7 @@
}, },
"packages/console/mail": { "packages/console/mail": {
"name": "@opencode-ai/console-mail", "name": "@opencode-ai/console-mail",
"version": "1.0.152", "version": "1.0.153",
"dependencies": { "dependencies": {
"@jsx-email/all": "2.2.3", "@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3", "@jsx-email/cli": "1.4.3",
@ -123,7 +123,7 @@
}, },
"packages/desktop": { "packages/desktop": {
"name": "@opencode-ai/desktop", "name": "@opencode-ai/desktop",
"version": "1.0.152", "version": "1.0.153",
"dependencies": { "dependencies": {
"@kobalte/core": "catalog:", "@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
@ -133,6 +133,7 @@
"@solid-primitives/active-element": "2.1.3", "@solid-primitives/active-element": "2.1.3",
"@solid-primitives/audio": "1.4.2", "@solid-primitives/audio": "1.4.2",
"@solid-primitives/event-bus": "1.1.2", "@solid-primitives/event-bus": "1.1.2",
"@solid-primitives/media": "2.3.3",
"@solid-primitives/resize-observer": "2.1.3", "@solid-primitives/resize-observer": "2.1.3",
"@solid-primitives/scroll": "2.1.3", "@solid-primitives/scroll": "2.1.3",
"@solid-primitives/storage": "4.3.3", "@solid-primitives/storage": "4.3.3",
@ -169,7 +170,7 @@
}, },
"packages/enterprise": { "packages/enterprise": {
"name": "@opencode-ai/enterprise", "name": "@opencode-ai/enterprise",
"version": "1.0.152", "version": "1.0.153",
"dependencies": { "dependencies": {
"@opencode-ai/ui": "workspace:*", "@opencode-ai/ui": "workspace:*",
"@opencode-ai/util": "workspace:*", "@opencode-ai/util": "workspace:*",
@ -198,7 +199,7 @@
}, },
"packages/function": { "packages/function": {
"name": "@opencode-ai/function", "name": "@opencode-ai/function",
"version": "1.0.152", "version": "1.0.153",
"dependencies": { "dependencies": {
"@octokit/auth-app": "8.0.1", "@octokit/auth-app": "8.0.1",
"@octokit/rest": "22.0.0", "@octokit/rest": "22.0.0",
@ -214,7 +215,7 @@
}, },
"packages/opencode": { "packages/opencode": {
"name": "opencode", "name": "opencode",
"version": "1.0.152", "version": "1.0.153",
"bin": { "bin": {
"opencode": "./bin/opencode", "opencode": "./bin/opencode",
}, },
@ -306,7 +307,7 @@
}, },
"packages/plugin": { "packages/plugin": {
"name": "@opencode-ai/plugin", "name": "@opencode-ai/plugin",
"version": "1.0.152", "version": "1.0.153",
"dependencies": { "dependencies": {
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
"zod": "catalog:", "zod": "catalog:",
@ -326,7 +327,7 @@
}, },
"packages/sdk/js": { "packages/sdk/js": {
"name": "@opencode-ai/sdk", "name": "@opencode-ai/sdk",
"version": "1.0.152", "version": "1.0.153",
"devDependencies": { "devDependencies": {
"@hey-api/openapi-ts": "0.88.1", "@hey-api/openapi-ts": "0.88.1",
"@tsconfig/node22": "catalog:", "@tsconfig/node22": "catalog:",
@ -337,7 +338,7 @@
}, },
"packages/slack": { "packages/slack": {
"name": "@opencode-ai/slack", "name": "@opencode-ai/slack",
"version": "1.0.152", "version": "1.0.153",
"dependencies": { "dependencies": {
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1", "@slack/bolt": "^3.17.1",
@ -350,7 +351,7 @@
}, },
"packages/tauri": { "packages/tauri": {
"name": "@opencode-ai/tauri", "name": "@opencode-ai/tauri",
"version": "1.0.152", "version": "1.0.153",
"dependencies": { "dependencies": {
"@opencode-ai/desktop": "workspace:*", "@opencode-ai/desktop": "workspace:*",
"@tauri-apps/api": "^2", "@tauri-apps/api": "^2",
@ -375,7 +376,7 @@
}, },
"packages/ui": { "packages/ui": {
"name": "@opencode-ai/ui", "name": "@opencode-ai/ui",
"version": "1.0.152", "version": "1.0.153",
"dependencies": { "dependencies": {
"@kobalte/core": "catalog:", "@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*", "@opencode-ai/sdk": "workspace:*",
@ -410,7 +411,7 @@
}, },
"packages/util": { "packages/util": {
"name": "@opencode-ai/util", "name": "@opencode-ai/util",
"version": "1.0.152", "version": "1.0.153",
"dependencies": { "dependencies": {
"zod": "catalog:", "zod": "catalog:",
}, },
@ -421,7 +422,7 @@
}, },
"packages/web": { "packages/web": {
"name": "@opencode-ai/web", "name": "@opencode-ai/web",
"version": "1.0.152", "version": "1.0.153",
"dependencies": { "dependencies": {
"@astrojs/cloudflare": "12.6.3", "@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1", "@astrojs/markdown-remark": "6.3.1",
@ -477,7 +478,7 @@
"@tailwindcss/vite": "4.1.11", "@tailwindcss/vite": "4.1.11",
"@tsconfig/bun": "1.0.9", "@tsconfig/bun": "1.0.9",
"@tsconfig/node22": "22.0.2", "@tsconfig/node22": "22.0.2",
"@types/bun": "1.3.3", "@types/bun": "1.3.4",
"@types/luxon": "3.7.1", "@types/luxon": "3.7.1",
"@types/node": "22.13.9", "@types/node": "22.13.9",
"@typescript/native-preview": "7.0.0-dev.20251207.1", "@typescript/native-preview": "7.0.0-dev.20251207.1",
@ -1703,7 +1704,7 @@
"@types/braces": ["@types/braces@3.0.5", "", {}, "sha512-SQFof9H+LXeWNz8wDe7oN5zu7ket0qwMu5vZubW4GCJ8Kkeh6nBWUz87+KTz/G3Kqsrp0j/W253XJb3KMEeg3w=="], "@types/braces": ["@types/braces@3.0.5", "", {}, "sha512-SQFof9H+LXeWNz8wDe7oN5zu7ket0qwMu5vZubW4GCJ8Kkeh6nBWUz87+KTz/G3Kqsrp0j/W253XJb3KMEeg3w=="],
"@types/bun": ["@types/bun@1.3.3", "", { "dependencies": { "bun-types": "1.3.3" } }, "sha512-ogrKbJ2X5N0kWLLFKeytG0eHDleBYtngtlbu9cyBKFtNL3cnpDZkNdQj8flVf6WTZUX5ulI9AY1oa7ljhSrp+g=="], "@types/bun": ["@types/bun@1.3.4", "", { "dependencies": { "bun-types": "1.3.4" } }, "sha512-EEPTKXHP+zKGPkhRLv+HI0UEX8/o+65hqARxLy8Ov5rIxMBPNTjeZww00CIihrIQGEQBYg+0roO5qOnS/7boGA=="],
"@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
@ -2009,7 +2010,7 @@
"bun-pty": ["bun-pty@0.4.2", "", {}, "sha512-sHImDz6pJDsHAroYpC9ouKVgOyqZ7FP3N+stX5IdMddHve3rf9LIZBDomQcXrACQ7sQDNuwZQHG8BKR7w8krkQ=="], "bun-pty": ["bun-pty@0.4.2", "", {}, "sha512-sHImDz6pJDsHAroYpC9ouKVgOyqZ7FP3N+stX5IdMddHve3rf9LIZBDomQcXrACQ7sQDNuwZQHG8BKR7w8krkQ=="],
"bun-types": ["bun-types@1.3.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-z3Xwlg7j2l9JY27x5Qn3Wlyos8YAp0kKRlrePAOjgjMGS5IG6E7Jnlx736vH9UVI4wUICwwhC9anYL++XeOgTQ=="], "bun-types": ["bun-types@1.3.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-5ua817+BZPZOlNaRgGBpZJOSAQ9RQ17pkwPD0yR7CfJg+r8DgIILByFifDTa+IPDDxzf5VNhtNlcKqFzDgJvlQ=="],
"bun-webgpu": ["bun-webgpu@0.1.4", "", { "dependencies": { "@webgpu/types": "^0.1.60" }, "optionalDependencies": { "bun-webgpu-darwin-arm64": "^0.1.4", "bun-webgpu-darwin-x64": "^0.1.4", "bun-webgpu-linux-x64": "^0.1.4", "bun-webgpu-win32-x64": "^0.1.4" } }, "sha512-Kw+HoXl1PMWJTh9wvh63SSRofTA8vYBFCw0XEP1V1fFdQEDhI8Sgf73sdndE/oDpN/7CMx0Yv/q8FCvO39ROMQ=="], "bun-webgpu": ["bun-webgpu@0.1.4", "", { "dependencies": { "@webgpu/types": "^0.1.60" }, "optionalDependencies": { "bun-webgpu-darwin-arm64": "^0.1.4", "bun-webgpu-darwin-x64": "^0.1.4", "bun-webgpu-linux-x64": "^0.1.4", "bun-webgpu-win32-x64": "^0.1.4" } }, "sha512-Kw+HoXl1PMWJTh9wvh63SSRofTA8vYBFCw0XEP1V1fFdQEDhI8Sgf73sdndE/oDpN/7CMx0Yv/q8FCvO39ROMQ=="],

View file

@ -17,6 +17,11 @@ inputs:
description: "Custom prompt to override the default prompt" description: "Custom prompt to override the default prompt"
required: false required: false
use_github_token:
description: "Use GITHUB_TOKEN directly instead of OpenCode App token exchange. When true, skips OIDC and uses the GITHUB_TOKEN env var."
required: false
default: "false"
runs: runs:
using: "composite" using: "composite"
steps: steps:
@ -51,3 +56,4 @@ runs:
MODEL: ${{ inputs.model }} MODEL: ${{ inputs.model }}
SHARE: ${{ inputs.share }} SHARE: ${{ inputs.share }}
PROMPT: ${{ inputs.prompt }} PROMPT: ${{ inputs.prompt }}
USE_GITHUB_TOKEN: ${{ inputs.use_github_token }}

View file

@ -1,3 +1,3 @@
{ {
"nodeModules": "sha256-nWSAnQEm/t1ESZe23dr4JnIOJQ0JLN0w4NVoMJajbVQ=" "nodeModules": "sha256-lgPsYtNJT7a+mDk5cTiEJLlBnTMTjxZCl8bw5WxcuaM="
} }

View file

@ -4,7 +4,7 @@
"description": "AI-powered development tool", "description": "AI-powered development tool",
"private": true, "private": true,
"type": "module", "type": "module",
"packageManager": "bun@1.3.3", "packageManager": "bun@1.3.4",
"scripts": { "scripts": {
"dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts", "dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
"typecheck": "bun turbo typecheck", "typecheck": "bun turbo typecheck",
@ -20,7 +20,7 @@
"packages/slack" "packages/slack"
], ],
"catalog": { "catalog": {
"@types/bun": "1.3.3", "@types/bun": "1.3.4",
"@hono/zod-validator": "0.4.2", "@hono/zod-validator": "0.4.2",
"ulid": "3.0.1", "ulid": "3.0.1",
"@kobalte/core": "0.13.11", "@kobalte/core": "0.13.11",

View file

@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/console-app", "name": "@opencode-ai/console-app",
"version": "1.0.152", "version": "1.0.153",
"type": "module", "type": "module",
"scripts": { "scripts": {
"typecheck": "tsgo --noEmit", "typecheck": "tsgo --noEmit",

View file

@ -1,7 +1,7 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core", "name": "@opencode-ai/console-core",
"version": "1.0.152", "version": "1.0.153",
"private": true, "private": true,
"type": "module", "type": "module",
"dependencies": { "dependencies": {

View file

@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/console-function", "name": "@opencode-ai/console-function",
"version": "1.0.152", "version": "1.0.153",
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"private": true, "private": true,
"type": "module", "type": "module",

View file

@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/console-mail", "name": "@opencode-ai/console-mail",
"version": "1.0.152", "version": "1.0.153",
"dependencies": { "dependencies": {
"@jsx-email/all": "2.2.3", "@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3", "@jsx-email/cli": "1.4.3",

View file

@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/desktop", "name": "@opencode-ai/desktop",
"version": "1.0.152", "version": "1.0.153",
"description": "", "description": "",
"type": "module", "type": "module",
"exports": { "exports": {
@ -37,6 +37,7 @@
"@solid-primitives/active-element": "2.1.3", "@solid-primitives/active-element": "2.1.3",
"@solid-primitives/audio": "1.4.2", "@solid-primitives/audio": "1.4.2",
"@solid-primitives/event-bus": "1.1.2", "@solid-primitives/event-bus": "1.1.2",
"@solid-primitives/media": "2.3.3",
"@solid-primitives/resize-observer": "2.1.3", "@solid-primitives/resize-observer": "2.1.3",
"@solid-primitives/scroll": "2.1.3", "@solid-primitives/scroll": "2.1.3",
"@solid-primitives/storage": "4.3.3", "@solid-primitives/storage": "4.3.3",

View file

@ -1,18 +1,7 @@
import { useFilteredList } from "@opencode-ai/ui/hooks" import { useFilteredList } from "@opencode-ai/ui/hooks"
import { import { createEffect, on, Component, Show, For, onMount, onCleanup, Switch, Match, createMemo } from "solid-js"
createEffect,
on,
Component,
Show,
For,
onMount,
onCleanup,
Switch,
Match,
createSignal,
createMemo,
} from "solid-js"
import { createStore } from "solid-js/store" import { createStore } from "solid-js/store"
import { makePersisted } from "@solid-primitives/storage"
import { createFocusSignal } from "@solid-primitives/active-element" import { createFocusSignal } from "@solid-primitives/active-element"
import { useLocal } from "@/context/local" import { useLocal } from "@/context/local"
import { ContentPart, DEFAULT_PROMPT, isPromptEqual, Prompt, useSession } from "@/context/session" import { ContentPart, DEFAULT_PROMPT, isPromptEqual, Prompt, useSession } from "@/context/session"
@ -81,22 +70,85 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const [store, setStore] = createStore<{ const [store, setStore] = createStore<{
popoverIsOpen: boolean popoverIsOpen: boolean
historyIndex: number
savedPrompt: Prompt | null
placeholder: number
}>({ }>({
popoverIsOpen: false, popoverIsOpen: false,
historyIndex: -1,
savedPrompt: null,
placeholder: Math.floor(Math.random() * PLACEHOLDERS.length),
}) })
const [placeholder, setPlaceholder] = createSignal(Math.floor(Math.random() * PLACEHOLDERS.length)) const MAX_HISTORY = 100
const [history, setHistory] = makePersisted(
createStore<{
entries: Prompt[]
}>({
entries: [],
}),
{
name: "prompt-history.v1",
},
)
onMount(() => { const clonePromptParts = (prompt: Prompt): Prompt =>
const interval = setInterval(() => { prompt.map((part) =>
setPlaceholder((prev) => (prev + 1) % PLACEHOLDERS.length) part.type === "text"
}, 6500) ? { ...part }
onCleanup(() => clearInterval(interval)) : {
}) ...part,
selection: part.selection ? { ...part.selection } : undefined,
},
)
const promptLength = (prompt: Prompt) => prompt.reduce((len, part) => len + part.content.length, 0)
const applyHistoryPrompt = (prompt: Prompt, position: "start" | "end") => {
const length = position === "start" ? 0 : promptLength(prompt)
session.prompt.set(prompt, length)
requestAnimationFrame(() => {
editorRef.focus()
setCursorPosition(editorRef, length)
})
}
const getCaretLineState = () => {
const selection = window.getSelection()
if (!selection || selection.rangeCount === 0) return { collapsed: false, onFirstLine: false, onLastLine: false }
const range = selection.getRangeAt(0)
const rect = range.getBoundingClientRect()
const editorRect = editorRef.getBoundingClientRect()
const style = window.getComputedStyle(editorRef)
const paddingTop = parseFloat(style.paddingTop) || 0
const paddingBottom = parseFloat(style.paddingBottom) || 0
let lineHeight = parseFloat(style.lineHeight)
if (!Number.isFinite(lineHeight)) lineHeight = parseFloat(style.fontSize) || 16
const scrollTop = editorRef.scrollTop
let relativeTop = rect.top - editorRect.top - paddingTop + scrollTop
if (!Number.isFinite(relativeTop)) relativeTop = scrollTop
relativeTop = Math.max(0, relativeTop)
let caretHeight = rect.height
if (!caretHeight || !Number.isFinite(caretHeight)) caretHeight = lineHeight
const relativeBottom = relativeTop + caretHeight
const contentHeight = Math.max(caretHeight, editorRef.scrollHeight - paddingTop - paddingBottom)
const threshold = Math.max(2, lineHeight / 2)
return {
collapsed: selection.isCollapsed,
onFirstLine: relativeTop <= threshold,
onLastLine: contentHeight - relativeBottom <= threshold,
}
}
createEffect(() => { createEffect(() => {
session.id session.id
editorRef.focus() editorRef.focus()
if (session.id) return
const interval = setInterval(() => {
setStore("placeholder", (prev) => (prev + 1) % PLACEHOLDERS.length)
}, 6500)
onCleanup(() => clearInterval(interval))
}) })
const isFocused = createFocusSignal(() => editorRef) const isFocused = createFocusSignal(() => editorRef)
@ -129,17 +181,12 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
addPart({ type: "file", path, content: "@" + path, start: 0, end: 0 }) addPart({ type: "file", path, content: "@" + path, start: 0, end: 0 })
} }
const { flat, active, onInput, onKeyDown, refetch } = useFilteredList<string>({ const { flat, active, onInput, onKeyDown } = useFilteredList<string>({
items: local.file.searchFilesAndDirectories, items: local.file.searchFilesAndDirectories,
key: (x) => x, key: (x) => x,
onSelect: handleFileSelect, onSelect: handleFileSelect,
}) })
createEffect(() => {
local.model.recent()
refetch()
})
createEffect( createEffect(
on( on(
() => session.prompt.current(), () => session.prompt.current(),
@ -221,6 +268,11 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
setStore("popoverIsOpen", false) setStore("popoverIsOpen", false)
} }
if (store.historyIndex >= 0) {
setStore("historyIndex", -1)
setStore("savedPrompt", null)
}
session.prompt.set(rawParts, cursorPosition) session.prompt.set(rawParts, cursorPosition)
} }
@ -296,12 +348,100 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
sessionID: session.id!, sessionID: session.id!,
}) })
const addToHistory = (prompt: Prompt) => {
const text = prompt
.map((p) => p.content)
.join("")
.trim()
if (!text) return
const entry = clonePromptParts(prompt)
const lastEntry = history.entries[0]
if (lastEntry) {
const lastText = lastEntry.map((p) => p.content).join("")
if (lastText === text) return
}
setHistory("entries", (entries) => [entry, ...entries].slice(0, MAX_HISTORY))
}
const navigateHistory = (direction: "up" | "down") => {
const entries = history.entries
const current = store.historyIndex
if (direction === "up") {
if (entries.length === 0) return false
if (current === -1) {
setStore("savedPrompt", clonePromptParts(session.prompt.current()))
setStore("historyIndex", 0)
applyHistoryPrompt(entries[0], "start")
return true
}
if (current < entries.length - 1) {
const next = current + 1
setStore("historyIndex", next)
applyHistoryPrompt(entries[next], "start")
return true
}
return false
}
if (current > 0) {
const next = current - 1
setStore("historyIndex", next)
applyHistoryPrompt(entries[next], "end")
return true
}
if (current === 0) {
setStore("historyIndex", -1)
const saved = store.savedPrompt
if (saved) {
applyHistoryPrompt(saved, "end")
setStore("savedPrompt", null)
return true
}
applyHistoryPrompt(DEFAULT_PROMPT, "end")
return true
}
return false
}
const handleKeyDown = (event: KeyboardEvent) => { const handleKeyDown = (event: KeyboardEvent) => {
if (store.popoverIsOpen && (event.key === "ArrowUp" || event.key === "ArrowDown" || event.key === "Enter")) { if (store.popoverIsOpen && (event.key === "ArrowUp" || event.key === "ArrowDown" || event.key === "Enter")) {
onKeyDown(event) onKeyDown(event)
event.preventDefault() event.preventDefault()
return return
} }
if (event.key === "ArrowUp" || event.key === "ArrowDown") {
const { collapsed, onFirstLine, onLastLine } = getCaretLineState()
if (!collapsed) return
const cursorPos = getCursorPosition(editorRef)
const textLength = promptLength(session.prompt.current())
const inHistory = store.historyIndex >= 0
const isStart = cursorPos === 0
const isEnd = cursorPos === textLength
const atAbsoluteStart = onFirstLine && isStart
const atAbsoluteEnd = onLastLine && isEnd
const allowUp = (inHistory && isEnd) || atAbsoluteStart
const allowDown = (inHistory && isStart) || atAbsoluteEnd
if (event.key === "ArrowUp") {
if (!allowUp) return
if (navigateHistory("up")) {
event.preventDefault()
}
return
}
if (!allowDown) return
if (navigateHistory("down")) {
event.preventDefault()
}
return
}
if (event.key === "Enter" && !event.shiftKey) { if (event.key === "Enter" && !event.shiftKey) {
handleSubmit(event) handleSubmit(event)
} }
@ -323,6 +463,10 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
return return
} }
addToHistory(prompt)
setStore("historyIndex", -1)
setStore("savedPrompt", null)
let existing = session.info() let existing = session.info()
if (!existing) { if (!existing) {
const created = await sdk.client.session.create() const created = await sdk.client.session.create()
@ -461,7 +605,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
/> />
<Show when={!session.prompt.dirty()}> <Show when={!session.prompt.dirty()}>
<div class="absolute top-0 left-0 px-5 py-3 text-14-regular text-text-weak pointer-events-none"> <div class="absolute top-0 left-0 px-5 py-3 text-14-regular text-text-weak pointer-events-none">
Ask anything... "{PLACEHOLDERS[placeholder()]}" Ask anything... "{PLACEHOLDERS[store.placeholder]}"
</div> </div>
</Show> </Show>
</div> </div>
@ -507,6 +651,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
items={models} items={models}
current={local.model.current()} current={local.model.current()}
filterKeys={["provider.name", "name", "id"]} filterKeys={["provider.name", "name", "id"]}
sortBy={(a, b) => a.name.localeCompare(b.name)}
// groupBy={(x) => (local.model.recent().includes(x) ? "Recent" : x.provider.name)} // groupBy={(x) => (local.model.recent().includes(x) ? "Recent" : x.provider.name)}
groupBy={(x) => x.provider.name} groupBy={(x) => x.provider.name}
sortGroupsBy={(a, b) => { sortGroupsBy={(a, b) => {

View file

@ -1,8 +1,9 @@
import { Ghostty, Terminal as Term, FitAddon } from "ghostty-web" 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 { useSDK } from "@/context/sdk"
import { SerializeAddon } from "@/addons/serialize" import { SerializeAddon } from "@/addons/serialize"
import { LocalPTY } from "@/context/session" import { LocalPTY } from "@/context/session"
import { usePrefersDark } from "@solid-primitives/media"
export interface TerminalProps extends ComponentProps<"div"> { export interface TerminalProps extends ComponentProps<"div"> {
pty: LocalPTY pty: LocalPTY
@ -21,6 +22,7 @@ export const Terminal = (props: TerminalProps) => {
let serializeAddon: SerializeAddon let serializeAddon: SerializeAddon
let fitAddon: FitAddon let fitAddon: FitAddon
let handleResize: () => void let handleResize: () => void
const prefersDark = usePrefersDark()
onMount(async () => { onMount(async () => {
ghostty = await Ghostty.load() ghostty = await Ghostty.load()
@ -31,10 +33,17 @@ export const Terminal = (props: TerminalProps) => {
fontSize: 14, fontSize: 14,
fontFamily: "TX-02, monospace", fontFamily: "TX-02, monospace",
allowTransparency: true, allowTransparency: true,
theme: { theme: prefersDark()
background: "#191515", ? {
foreground: "#d4d4d4", background: "#191515",
}, foreground: "#d4d4d4",
cursor: "#d4d4d4",
}
: {
background: "#fcfcfc",
foreground: "#211e1e",
cursor: "#211e1e",
},
scrollback: 10_000, scrollback: 10_000,
ghostty, ghostty,
}) })

View file

@ -189,11 +189,13 @@ export default function Layout(props: ParentProps) {
const hasError = createMemo(() => notifications().some((n) => n.type === "error")) const hasError = createMemo(() => notifications().some((n) => n.type === "error"))
const name = createMemo(() => getFilename(props.project.worktree)) const name = createMemo(() => getFilename(props.project.worktree))
const mask = "radial-gradient(circle 5px at calc(100% - 2px) 2px, transparent 5px, black 5.5px)" const mask = "radial-gradient(circle 5px at calc(100% - 2px) 2px, transparent 5px, black 5.5px)"
const opencode = "4b0ea68d7af9a6031a7ffda7ad66e0cb83315750"
return ( return (
<div class="relative size-6 shrink-0"> <div class="relative size-6 shrink-0">
<Avatar <Avatar
fallback={name()} fallback={name()}
src={props.project.icon?.url} src={props.project.id === opencode ? "https://opencode.ai/favicon.svg" : props.project.icon?.url}
{...getAvatarColors(props.project.icon?.color)} {...getAvatarColors(props.project.icon?.color)}
class={`size-full ${props.class ?? ""}`} class={`size-full ${props.class ?? ""}`}
style={ style={
@ -318,22 +320,20 @@ export default function Layout(props: ParentProps) {
) )
} }
return ( return (
<A <div
href={`${slug()}/session/${session.id}`} class="group/session relative w-full pl-4 pr-1 py-1 rounded-md cursor-default transition-colors
class="group/session focus:outline-none cursor-default" hover:bg-surface-raised-base-hover focus-within:bg-surface-raised-base-hover has-[.active]:bg-surface-raised-base-hover"
> >
<Tooltip placement="right" value={session.title}> <Tooltip placement="right" value={session.title} gutter={10}>
<div <A
class="relative w-full pl-4 pr-1 py-1 rounded-md href={`${slug()}/session/${session.id}`}
group-[.active]/session:bg-surface-raised-base-hover class="flex flex-col min-w-0 text-left w-full focus:outline-none"
group-hover/session:bg-surface-raised-base-hover
group-focus/session:bg-surface-raised-base-hover"
> >
<div class="flex items-center self-stretch gap-6 justify-between"> <div class="flex items-center self-stretch gap-6 justify-between transition-[padding] group-hover/session:pr-7 group-focus-within/session:pr-7 group-active/session:pr-7">
<span class="text-14-regular text-text-strong overflow-hidden text-ellipsis truncate"> <span class="text-14-regular text-text-strong overflow-hidden text-ellipsis truncate">
{session.title} {session.title}
</span> </span>
<div class="shrink-0 group-hover/session:hidden mr-1"> <div class="shrink-0 group-hover/session:hidden group-active/session:hidden group-focus-within/session:hidden">
<Switch> <Switch>
<Match when={hasError()}> <Match when={hasError()}>
<div class="size-1.5 mr-1.5 rounded-full bg-text-diff-delete-base" /> <div class="size-1.5 mr-1.5 rounded-full bg-text-diff-delete-base" />
@ -358,12 +358,6 @@ export default function Layout(props: ParentProps) {
</Match> </Match>
</Switch> </Switch>
</div> </div>
<div class="hidden group-hover/session:flex group-active/session:flex text-text-base gap-1">
{/* <IconButton icon="dot-grid" variant="ghost" /> */}
<Tooltip placement="right" value="Archive session">
<IconButton icon="archive" variant="ghost" onClick={() => archive(session)} />
</Tooltip>
</div>
</div> </div>
<Show when={session.summary?.files}> <Show when={session.summary?.files}>
<div class="flex justify-between items-center self-stretch"> <div class="flex justify-between items-center self-stretch">
@ -371,29 +365,40 @@ export default function Layout(props: ParentProps) {
<Show when={session.summary}>{(summary) => <DiffChanges changes={summary()} />}</Show> <Show when={session.summary}>{(summary) => <DiffChanges changes={summary()} />}</Show>
</div> </div>
</Show> </Show>
</div> </A>
</Tooltip> </Tooltip>
</A> <div class="hidden group-hover/session:flex group-active/session:flex group-focus-within/session:flex text-text-base gap-1 items-center absolute top-1 right-1">
{/* <IconButton icon="dot-grid" variant="ghost" /> */}
<Tooltip placement="right" value="Archive session">
<IconButton icon="archive" variant="ghost" onClick={() => archive(session)} />
</Tooltip>
</div>
</div>
) )
}} }}
</For> </For>
<Show when={sessions().length === 0}> <Show when={sessions().length === 0}>
<A href={`${slug()}/session`} class="group/session focus:outline-none cursor-default"> <div
<Tooltip placement="right" value="New session"> class="group/session relative w-full pl-4 pr-1 py-1 rounded-md cursor-default transition-colors
<div hover:bg-surface-raised-base-hover focus-within:bg-surface-raised-base-hover has-[.active]:bg-surface-raised-base-hover"
class="relative w-full pl-4 pr-1 py-1 rounded-md >
group-[.active]/session:bg-surface-raised-base-hover <div class="flex items-center self-stretch w-full">
group-hover/session:bg-surface-raised-base-hover <div class="flex-1 min-w-0">
group-focus/session:bg-surface-raised-base-hover" <Tooltip placement="right" value="New session">
> <A
<div class="flex items-center self-stretch gap-6 justify-between"> href={`${slug()}/session`}
<span class="text-14-regular text-text-strong overflow-hidden text-ellipsis truncate"> class="flex flex-col gap-1 min-w-0 text-left w-full focus:outline-none"
New session >
</span> <div class="flex items-center self-stretch gap-6 justify-between">
</div> <span class="text-14-regular text-text-strong overflow-hidden text-ellipsis truncate">
New session
</span>
</div>
</A>
</Tooltip>
</div> </div>
</Tooltip> </div>
</A> </div>
</Show> </Show>
</nav> </nav>
</Collapsible.Content> </Collapsible.Content>

View file

@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/enterprise", "name": "@opencode-ai/enterprise",
"version": "1.0.152", "version": "1.0.153",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {

View file

@ -1,7 +1,7 @@
id = "opencode" id = "opencode"
name = "OpenCode" name = "OpenCode"
description = "The open source coding agent." description = "The open source coding agent."
version = "1.0.152" version = "1.0.153"
schema_version = 1 schema_version = 1
authors = ["Anomaly"] authors = ["Anomaly"]
repository = "https://github.com/sst/opencode" repository = "https://github.com/sst/opencode"
@ -11,26 +11,26 @@ name = "OpenCode"
icon = "./icons/opencode.svg" icon = "./icons/opencode.svg"
[agent_servers.opencode.targets.darwin-aarch64] [agent_servers.opencode.targets.darwin-aarch64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.152/opencode-darwin-arm64.zip" archive = "https://github.com/sst/opencode/releases/download/v1.0.153/opencode-darwin-arm64.zip"
cmd = "./opencode" cmd = "./opencode"
args = ["acp"] args = ["acp"]
[agent_servers.opencode.targets.darwin-x86_64] [agent_servers.opencode.targets.darwin-x86_64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.152/opencode-darwin-x64.zip" archive = "https://github.com/sst/opencode/releases/download/v1.0.153/opencode-darwin-x64.zip"
cmd = "./opencode" cmd = "./opencode"
args = ["acp"] args = ["acp"]
[agent_servers.opencode.targets.linux-aarch64] [agent_servers.opencode.targets.linux-aarch64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.152/opencode-linux-arm64.tar.gz" archive = "https://github.com/sst/opencode/releases/download/v1.0.153/opencode-linux-arm64.tar.gz"
cmd = "./opencode" cmd = "./opencode"
args = ["acp"] args = ["acp"]
[agent_servers.opencode.targets.linux-x86_64] [agent_servers.opencode.targets.linux-x86_64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.152/opencode-linux-x64.tar.gz" archive = "https://github.com/sst/opencode/releases/download/v1.0.153/opencode-linux-x64.tar.gz"
cmd = "./opencode" cmd = "./opencode"
args = ["acp"] args = ["acp"]
[agent_servers.opencode.targets.windows-x86_64] [agent_servers.opencode.targets.windows-x86_64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.152/opencode-windows-x64.zip" archive = "https://github.com/sst/opencode/releases/download/v1.0.153/opencode-windows-x64.zip"
cmd = "./opencode.exe" cmd = "./opencode.exe"
args = ["acp"] args = ["acp"]

View file

@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/function", "name": "@opencode-ai/function",
"version": "1.0.152", "version": "1.0.153",
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"private": true, "private": true,
"type": "module", "type": "module",

View file

@ -1,10 +1,18 @@
FROM alpine FROM alpine AS base
# Disable the runtime transpiler cache by default inside Docker containers. # Disable the runtime transpiler cache by default inside Docker containers.
# On ephemeral containers, the cache is not useful # On ephemeral containers, the cache is not useful
ARG BUN_RUNTIME_TRANSPILER_CACHE_PATH=0 ARG BUN_RUNTIME_TRANSPILER_CACHE_PATH=0
ENV BUN_RUNTIME_TRANSPILER_CACHE_PATH=${BUN_RUNTIME_TRANSPILER_CACHE_PATH} ENV BUN_RUNTIME_TRANSPILER_CACHE_PATH=${BUN_RUNTIME_TRANSPILER_CACHE_PATH}
RUN apk add libgcc libstdc++ ripgrep RUN apk add libgcc libstdc++ ripgrep
ADD ./dist/opencode-linux-x64-baseline-musl/bin/opencode /usr/local/bin/opencode
FROM base AS build-amd64
COPY dist/opencode-linux-x64-baseline-musl/bin/opencode /usr/local/bin/opencode
FROM base AS build-arm64
COPY dist/opencode-linux-arm64-musl/bin/opencode /usr/local/bin/opencode
ARG TARGETARCH
FROM build-${TARGETARCH}
RUN opencode --version RUN opencode --version
ENTRYPOINT ["opencode"] ENTRYPOINT ["opencode"]

View file

@ -1,6 +1,6 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"version": "1.0.152", "version": "1.0.153",
"name": "opencode", "name": "opencode",
"type": "module", "type": "module",
"private": true, "private": true,

View file

@ -117,6 +117,9 @@ for (const item of targets) {
compile: { compile: {
autoloadBunfig: false, autoloadBunfig: false,
autoloadDotenv: false, autoloadDotenv: false,
//@ts-ignore (bun types aren't up to date)
autoloadTsconfig: true,
autoloadPackageJson: true,
target: name.replace(pkg.name, "bun") as any, target: name.replace(pkg.name, "bun") as any,
outfile: `dist/${name}/bin/opencode`, outfile: `dist/${name}/bin/opencode`,
execArgv: [`--user-agent=opencode/${Script.version}`, "--"], execArgv: [`--user-agent=opencode/${Script.version}`, "--"],

View file

@ -244,8 +244,8 @@ if (!Script.preview) {
await $`cd ./dist/homebrew-tap && git push` await $`cd ./dist/homebrew-tap && git push`
const image = "ghcr.io/sst/opencode" const image = "ghcr.io/sst/opencode"
await $`docker build -t ${image}:${Script.version} .` const platforms = "linux/amd64,linux/arm64"
await $`docker push ${image}:${Script.version}` const tags = [`${image}:${Script.version}`, `${image}:latest`]
await $`docker tag ${image}:${Script.version} ${image}:latest` const tagFlags = tags.flatMap((t) => ["-t", t])
await $`docker push ${image}:latest` await $`docker buildx build --platform ${platforms} ${tagFlags} --push .`
} }

View file

@ -85,47 +85,16 @@ export namespace BunProc {
version, version,
}) })
const total = 3 await BunProc.run(args, {
const wait = 500 cwd: Global.Path.cache,
}).catch((e) => {
const runInstall = async (count: number = 1): Promise<void> => { throw new InstallFailedError(
log.info("bun install attempt", { { pkg, version },
pkg, {
version, cause: e,
attempt: count, },
total, )
}) })
await BunProc.run(args, {
cwd: Global.Path.cache,
}).catch(async (error) => {
log.warn("bun install failed", {
pkg,
version,
attempt: count,
total,
error,
})
if (count >= total) {
throw new InstallFailedError(
{ pkg, version },
{
cause: error,
},
)
}
const delay = wait * count
log.info("bun install retrying", {
pkg,
version,
next: count + 1,
delay,
})
await Bun.sleep(delay)
return runInstall(count + 1)
})
}
await runInstall()
// Resolve actual version from installed package when using "latest" // Resolve actual version from installed package when using "latest"
// This ensures subsequent starts use the cached version until explicitly updated // This ensures subsequent starts use the cached version until explicitly updated

View file

@ -411,17 +411,30 @@ export const GithubRunCommand = cmd({
let exitCode = 0 let exitCode = 0
type PromptFiles = Awaited<ReturnType<typeof getUserPrompt>>["promptFiles"] type PromptFiles = Awaited<ReturnType<typeof getUserPrompt>>["promptFiles"]
const triggerCommentId = payload.comment.id const triggerCommentId = payload.comment.id
const useGithubToken = normalizeUseGithubToken()
try { try {
const actionToken = isMock ? args.token! : await getOidcToken() if (useGithubToken) {
appToken = await exchangeForAppToken(actionToken) const githubToken = process.env["GITHUB_TOKEN"]
if (!githubToken) {
throw new Error(
"GITHUB_TOKEN environment variable is not set. When using use_github_token, you must provide GITHUB_TOKEN.",
)
}
appToken = githubToken
} else {
const actionToken = isMock ? args.token! : await getOidcToken()
appToken = await exchangeForAppToken(actionToken)
}
octoRest = new Octokit({ auth: appToken }) octoRest = new Octokit({ auth: appToken })
octoGraph = graphql.defaults({ octoGraph = graphql.defaults({
headers: { authorization: `token ${appToken}` }, headers: { authorization: `token ${appToken}` },
}) })
const { userPrompt, promptFiles } = await getUserPrompt() const { userPrompt, promptFiles } = await getUserPrompt()
await configureGit(appToken) if (!useGithubToken) {
await configureGit(appToken)
}
await assertPermissions() await assertPermissions()
await addReaction() await addReaction()
@ -514,8 +527,10 @@ export const GithubRunCommand = cmd({
// Also output the clean error message for the action to capture // Also output the clean error message for the action to capture
//core.setOutput("prepare_error", e.message); //core.setOutput("prepare_error", e.message);
} finally { } finally {
await restoreGitConfig() if (!useGithubToken) {
await revokeAppToken() await restoreGitConfig()
await revokeAppToken()
}
} }
process.exit(exitCode) process.exit(exitCode)
@ -544,6 +559,14 @@ export const GithubRunCommand = cmd({
throw new Error(`Invalid share value: ${value}. Share must be a boolean.`) throw new Error(`Invalid share value: ${value}. Share must be a boolean.`)
} }
function normalizeUseGithubToken() {
const value = process.env["USE_GITHUB_TOKEN"]
if (!value) return false
if (value === "true") return true
if (value === "false") return false
throw new Error(`Invalid use_github_token value: ${value}. Must be a boolean.`)
}
function isIssueCommentEvent( function isIssueCommentEvent(
event: IssueCommentEvent | PullRequestReviewCommentEvent, event: IssueCommentEvent | PullRequestReviewCommentEvent,
): event is IssueCommentEvent { ): event is IssueCommentEvent {

View file

@ -218,7 +218,7 @@ function App() {
let continued = false let continued = false
createEffect(() => { createEffect(() => {
if (continued || sync.status !== "complete" || !args.continue) return if (continued || sync.status !== "complete" || !args.continue) return
const match = sync.data.session.at(0)?.id const match = sync.data.session.find((x) => x.parentID === undefined)?.id
if (match) { if (match) {
continued = true continued = true
route.navigate({ type: "session", sessionID: match }) route.navigate({ type: "session", sessionID: match })

View file

@ -14,12 +14,17 @@ export const AttachCommand = cmd({
.option("dir", { .option("dir", {
type: "string", type: "string",
description: "directory to run in", description: "directory to run in",
})
.option("session", {
alias: ["s"],
type: "string",
describe: "session id to continue",
}), }),
handler: async (args) => { handler: async (args) => {
if (args.dir) process.chdir(args.dir) if (args.dir) process.chdir(args.dir)
await tui({ await tui({
url: args.url, url: args.url,
args: {}, args: { sessionID: args.session },
}) })
}, },
}) })

View file

@ -705,8 +705,8 @@ export function Prompt(props: PromptProps) {
> >
<textarea <textarea
placeholder={props.sessionID ? undefined : `Ask anything... "${PLACEHOLDERS[store.placeholder]}"`} placeholder={props.sessionID ? undefined : `Ask anything... "${PLACEHOLDERS[store.placeholder]}"`}
textColor={theme.text} textColor={keybind.leader ? theme.textMuted : theme.text}
focusedTextColor={theme.text} focusedTextColor={keybind.leader ? theme.textMuted : theme.text}
minHeight={1} minHeight={1}
maxHeight={6} maxHeight={6}
onContentChange={() => { onContentChange={() => {
@ -854,7 +854,7 @@ export function Prompt(props: PromptProps) {
</text> </text>
<Show when={store.mode === "normal"}> <Show when={store.mode === "normal"}>
<box flexDirection="row" gap={1}> <box flexDirection="row" gap={1}>
<text flexShrink={0} fg={theme.text}> <text flexShrink={0} fg={keybind.leader ? theme.textMuted : theme.text}>
{local.model.parsed().model} {local.model.parsed().model}
</text> </text>
<text fg={theme.textMuted}>{local.model.parsed().provider}</text> <text fg={theme.textMuted}>{local.model.parsed().provider}</text>
@ -869,25 +869,17 @@ export function Prompt(props: PromptProps) {
borderColor={highlight()} borderColor={highlight()}
customBorderChars={{ customBorderChars={{
...EmptyBorder, ...EmptyBorder,
// when the background is transparent, don't draw the vertical line vertical: "╹",
vertical: theme.background.a != 0 ? "╹" : " ",
}} }}
> >
<box <box
height={1} height={1}
border={["bottom"]} border={["bottom"]}
borderColor={theme.backgroundElement} borderColor={theme.backgroundElement}
customBorderChars={ customBorderChars={{
theme.background.a != 0 ...EmptyBorder,
? { horizontal: "▀",
...EmptyBorder, }}
horizontal: "▀",
}
: {
...EmptyBorder,
horizontal: " ",
}
}
/> />
</box> </box>
<box flexDirection="row" justifyContent="space-between"> <box flexDirection="row" justifyContent="space-between">

View file

@ -277,7 +277,10 @@ export function Sidebar(props: { sessionID: string }) {
</box> </box>
</box> </box>
</Show> </Show>
<text fg={theme.text}>{directory()}</text> <text>
<span style={{ fg: theme.textMuted }}>{directory().split("/").slice(0, -1).join("/")}/</span>
<span style={{ fg: theme.text }}>{directory().split("/").at(-1)}</span>
</text>
<text fg={theme.textMuted}> <text fg={theme.textMuted}>
<span style={{ fg: theme.success }}></span> <b>Open</b> <span style={{ fg: theme.success }}></span> <b>Open</b>
<span style={{ fg: theme.text }}> <span style={{ fg: theme.text }}>

View file

@ -275,3 +275,21 @@ export const terraform: Info = {
return Bun.which("terraform") !== null return Bun.which("terraform") !== null
}, },
} }
export const latexindent: Info = {
name: "latexindent",
command: ["latexindent", "-w", "-s", "$FILE"],
extensions: [".tex"],
async enabled() {
return Bun.which("latexindent") !== null
},
}
export const gleam: Info = {
name: "gleam",
command: ["gleam", "format", "$FILE"],
extensions: [".gleam"],
async enabled() {
return Bun.which("gleam") !== null
},
}

View file

@ -34,6 +34,7 @@ export const LANGUAGE_EXTENSIONS: Record<string, string> = {
".gitrebase": "git-rebase", ".gitrebase": "git-rebase",
".go": "go", ".go": "go",
".groovy": "groovy", ".groovy": "groovy",
".gleam": "gleam",
".hbs": "handlebars", ".hbs": "handlebars",
".handlebars": "handlebars", ".handlebars": "handlebars",
".hs": "haskell", ".hs": "haskell",

View file

@ -1386,4 +1386,145 @@ export namespace LSPServer {
} }
}, },
} }
export const TexLab: Info = {
id: "texlab",
extensions: [".tex", ".bib"],
root: NearestRoot([".latexmkrc", "latexmkrc", ".texlabroot", "texlabroot"]),
async spawn(root) {
let bin = Bun.which("texlab", {
PATH: process.env["PATH"] + ":" + Global.Path.bin,
})
if (!bin) {
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
log.info("downloading texlab from GitHub releases")
const response = await fetch("https://api.github.com/repos/latex-lsp/texlab/releases/latest")
if (!response.ok) {
log.error("Failed to fetch texlab release info")
return
}
const release = (await response.json()) as {
tag_name?: string
assets?: { name?: string; browser_download_url?: string }[]
}
const version = release.tag_name?.replace("v", "")
if (!version) {
log.error("texlab release did not include a version tag")
return
}
const platform = process.platform
const arch = process.arch
const texArch = arch === "arm64" ? "aarch64" : "x86_64"
const texPlatform = platform === "darwin" ? "macos" : platform === "win32" ? "windows" : "linux"
const ext = platform === "win32" ? "zip" : "tar.gz"
const assetName = `texlab-${texArch}-${texPlatform}.${ext}`
const assets = release.assets ?? []
const asset = assets.find((a) => a.name === assetName)
if (!asset?.browser_download_url) {
log.error(`Could not find asset ${assetName} in texlab release`)
return
}
const downloadResponse = await fetch(asset.browser_download_url)
if (!downloadResponse.ok) {
log.error("Failed to download texlab")
return
}
const tempPath = path.join(Global.Path.bin, assetName)
await Bun.file(tempPath).write(downloadResponse)
if (ext === "zip") {
await $`unzip -o -q ${tempPath}`.cwd(Global.Path.bin).nothrow()
}
if (ext === "tar.gz") {
await $`tar -xzf ${tempPath}`.cwd(Global.Path.bin).nothrow()
}
await fs.rm(tempPath, { force: true })
bin = path.join(Global.Path.bin, "texlab" + (platform === "win32" ? ".exe" : ""))
if (!(await Bun.file(bin).exists())) {
log.error("Failed to extract texlab binary")
return
}
if (platform !== "win32") {
await $`chmod +x ${bin}`.nothrow()
}
log.info("installed texlab", { bin })
}
return {
process: spawn(bin, {
cwd: root,
}),
}
},
}
export const DockerfileLS: Info = {
id: "dockerfile",
extensions: [".dockerfile", "Dockerfile"],
root: async () => Instance.directory,
async spawn(root) {
let binary = Bun.which("docker-langserver")
const args: string[] = []
if (!binary) {
const js = path.join(Global.Path.bin, "node_modules", "dockerfile-language-server-nodejs", "lib", "server.js")
if (!(await Bun.file(js).exists())) {
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
await Bun.spawn([BunProc.which(), "install", "dockerfile-language-server-nodejs"], {
cwd: Global.Path.bin,
env: {
...process.env,
BUN_BE_BUN: "1",
},
stdout: "pipe",
stderr: "pipe",
stdin: "pipe",
}).exited
}
binary = BunProc.which()
args.push("run", js)
}
args.push("--stdio")
const proc = spawn(binary, args, {
cwd: root,
env: {
...process.env,
BUN_BE_BUN: "1",
},
})
return {
process: proc,
}
},
}
export const Gleam: Info = {
id: "gleam",
extensions: [".gleam"],
root: NearestRoot(["gleam.toml"]),
async spawn(root) {
const gleam = Bun.which("gleam")
if (!gleam) {
log.info("gleam not found, please install gleam first")
return
}
return {
process: spawn(gleam, ["lsp"], {
cwd: root,
}),
}
},
}
} }

View file

@ -199,14 +199,29 @@ export namespace ProviderTransform {
} }
export function temperature(model: Provider.Model) { export function temperature(model: Provider.Model) {
if (model.api.id.toLowerCase().includes("qwen")) return 0.55 const id = model.id.toLowerCase()
if (model.api.id.toLowerCase().includes("claude")) return undefined if (id.includes("qwen")) return 0.55
if (model.api.id.toLowerCase().includes("gemini-3-pro")) return 1.0 if (id.includes("claude")) return undefined
return 0 if (id.includes("gemini-3-pro")) return 1.0
if (id.includes("glm-4.6")) return 1.0
if (id.includes("minimax-m2")) return 1.0
// if (id.includes("kimi-k2")) {
// if (id.includes("thinking")) return 1.0
// return 0.6
// }
return undefined
} }
export function topP(model: Provider.Model) { export function topP(model: Provider.Model) {
if (model.api.id.toLowerCase().includes("qwen")) return 1 const id = model.id.toLowerCase()
if (id.includes("qwen")) return 1
if (id.includes("minimax-m2")) return 0.95
return undefined
}
export function topK(model: Provider.Model) {
const id = model.id.toLowerCase()
if (id.includes("minimax-m2")) return 40
return undefined return undefined
} }

View file

@ -10,6 +10,7 @@ import type { MessageV2 } from "./message-v2"
import { Plugin } from "@/plugin" import { Plugin } from "@/plugin"
import { SystemPrompt } from "./system" import { SystemPrompt } from "./system"
import { ToolRegistry } from "@/tool/registry" import { ToolRegistry } from "@/tool/registry"
import { Flag } from "@/flag/flag"
export namespace LLM { export namespace LLM {
const log = Log.create({ service: "llm" }) const log = Log.create({ service: "llm" })
@ -133,6 +134,7 @@ export namespace LLM {
"x-opencode-project": Instance.project.id, "x-opencode-project": Instance.project.id,
"x-opencode-session": input.sessionID, "x-opencode-session": input.sessionID,
"x-opencode-request": input.user.id, "x-opencode-request": input.user.id,
"x-opencode-client": Flag.OPENCODE_CLIENT,
} }
: undefined), : undefined),
...input.model.headers, ...input.model.headers,

View file

@ -68,6 +68,15 @@ export namespace SessionRetry {
if (json.code === "Some resource has been exhausted") { if (json.code === "Some resource has been exhausted") {
return "Provider is overloaded" return "Provider is overloaded"
} }
if (json.type === "error" && json.error?.code?.includes("rate_limit")) {
return "Rate Limited"
}
if (
json.error?.message?.includes("no_kv_space") ||
(json.type === "error" && json.error?.type === "server_error")
) {
return "Provider Server Error"
}
} catch {} } catch {}
} }

View file

@ -18,6 +18,8 @@ import { Instance } from "../project/instance"
import { Agent } from "../agent/agent" import { Agent } from "../agent/agent"
import { Snapshot } from "@/snapshot" import { Snapshot } from "@/snapshot"
const MAX_DIAGNOSTICS_PER_FILE = 20
function normalizeLineEndings(text: string): string { function normalizeLineEndings(text: string): string {
return text.replaceAll("\r\n", "\n") return text.replaceAll("\r\n", "\n")
} }
@ -141,10 +143,11 @@ export const EditTool = Tool.define("edit", {
for (const [file, issues] of Object.entries(diagnostics)) { for (const [file, issues] of Object.entries(diagnostics)) {
if (issues.length === 0) continue if (issues.length === 0) continue
if (file === filePath) { if (file === filePath) {
output += `\nThis file has errors, please fix\n<file_diagnostics>\n${issues const errors = issues.filter((item) => item.severity === 1)
.filter((item) => item.severity === 1) const limited = errors.slice(0, MAX_DIAGNOSTICS_PER_FILE)
.map(LSP.Diagnostic.pretty) const suffix =
.join("\n")}\n</file_diagnostics>\n` errors.length > MAX_DIAGNOSTICS_PER_FILE ? `\n... and ${errors.length - MAX_DIAGNOSTICS_PER_FILE} more` : ""
output += `\nThis file has errors, please fix\n<file_diagnostics>\n${limited.map(LSP.Diagnostic.pretty).join("\n")}${suffix}\n</file_diagnostics>\n`
continue continue
} }
} }

View file

@ -11,6 +11,9 @@ import { Filesystem } from "../util/filesystem"
import { Instance } from "../project/instance" import { Instance } from "../project/instance"
import { Agent } from "../agent/agent" import { Agent } from "../agent/agent"
const MAX_DIAGNOSTICS_PER_FILE = 20
const MAX_PROJECT_DIAGNOSTICS_FILES = 5
export const WriteTool = Tool.define("write", { export const WriteTool = Tool.define("write", {
description: DESCRIPTION, description: DESCRIPTION,
parameters: z.object({ parameters: z.object({
@ -77,13 +80,20 @@ export const WriteTool = Tool.define("write", {
let output = "" let output = ""
await LSP.touchFile(filepath, true) await LSP.touchFile(filepath, true)
const diagnostics = await LSP.diagnostics() const diagnostics = await LSP.diagnostics()
let projectDiagnosticsCount = 0
for (const [file, issues] of Object.entries(diagnostics)) { for (const [file, issues] of Object.entries(diagnostics)) {
if (issues.length === 0) continue if (issues.length === 0) continue
const sorted = issues.toSorted((a, b) => (a.severity ?? 4) - (b.severity ?? 4))
const limited = sorted.slice(0, MAX_DIAGNOSTICS_PER_FILE)
const suffix =
issues.length > MAX_DIAGNOSTICS_PER_FILE ? `\n... and ${issues.length - MAX_DIAGNOSTICS_PER_FILE} more` : ""
if (file === filepath) { if (file === filepath) {
output += `\nThis file has errors, please fix\n<file_diagnostics>\n${issues.map(LSP.Diagnostic.pretty).join("\n")}\n</file_diagnostics>\n` output += `\nThis file has errors, please fix\n<file_diagnostics>\n${limited.map(LSP.Diagnostic.pretty).join("\n")}${suffix}\n</file_diagnostics>\n`
continue continue
} }
output += `\n<project_diagnostics>\n${file}\n${issues.map(LSP.Diagnostic.pretty).join("\n")}\n</project_diagnostics>\n` if (projectDiagnosticsCount >= MAX_PROJECT_DIAGNOSTICS_FILES) continue
projectDiagnosticsCount++
output += `\n<project_diagnostics>\n${file}\n${limited.map(LSP.Diagnostic.pretty).join("\n")}${suffix}\n</project_diagnostics>\n`
} }
return { return {

View file

@ -1,7 +1,7 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/plugin", "name": "@opencode-ai/plugin",
"version": "1.0.152", "version": "1.0.153",
"type": "module", "type": "module",
"scripts": { "scripts": {
"typecheck": "tsgo --noEmit", "typecheck": "tsgo --noEmit",
@ -24,4 +24,4 @@
"typescript": "catalog:", "typescript": "catalog:",
"@typescript/native-preview": "catalog:" "@typescript/native-preview": "catalog:"
} }
} }

View file

@ -1,7 +1,7 @@
{ {
"$schema": "https://json.schemastore.org/package.json", "$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/sdk", "name": "@opencode-ai/sdk",
"version": "1.0.152", "version": "1.0.153",
"type": "module", "type": "module",
"scripts": { "scripts": {
"typecheck": "tsgo --noEmit", "typecheck": "tsgo --noEmit",
@ -29,4 +29,4 @@
"publishConfig": { "publishConfig": {
"directory": "dist" "directory": "dist"
} }
} }

View file

@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/slack", "name": "@opencode-ai/slack",
"version": "1.0.152", "version": "1.0.153",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "bun run src/index.ts", "dev": "bun run src/index.ts",

View file

@ -1,7 +1,7 @@
{ {
"name": "@opencode-ai/tauri", "name": "@opencode-ai/tauri",
"private": true, "private": true,
"version": "1.0.152", "version": "1.0.153",
"type": "module", "type": "module",
"scripts": { "scripts": {
"typecheck": "tsgo -b", "typecheck": "tsgo -b",

View file

@ -1,7 +1,7 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "OpenCode", "productName": "OpenCode",
"mainBinaryName": "OpenCode Desktop", "mainBinaryName": "OpenCode",
"version": "../package.json", "version": "../package.json",
"identifier": "ai.opencode.desktop", "identifier": "ai.opencode.desktop",
"build": { "build": {
@ -19,7 +19,7 @@
}, },
"bundle": { "bundle": {
"active": true, "active": true,
"targets": ["deb", "rpm", "dmg", "nsis"], "targets": ["deb", "rpm", "dmg", "nsis", "appimage"],
"icon": ["icons/32x32.png", "icons/128x128.png", "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico"], "icon": ["icons/32x32.png", "icons/128x128.png", "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico"],
"externalBin": ["sidecars/opencode"], "externalBin": ["sidecars/opencode"],
"createUpdaterArtifacts": true, "createUpdaterArtifacts": true,

View file

@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/ui", "name": "@opencode-ai/ui",
"version": "1.0.152", "version": "1.0.153",
"type": "module", "type": "module",
"exports": { "exports": {
"./*": "./src/components/*.tsx", "./*": "./src/components/*.tsx",

View file

@ -13,7 +13,9 @@ export type TriggerTitle = {
} }
const isTriggerTitle = (val: any): val is TriggerTitle => { const isTriggerTitle = (val: any): val is TriggerTitle => {
return typeof val === "object" && val !== null && "title" in val && !(val instanceof Node) return (
typeof val === "object" && val !== null && "title" in val && (typeof Node === "undefined" || !(val instanceof Node))
)
} }
export interface BasicToolProps { export interface BasicToolProps {

View file

@ -148,7 +148,7 @@
padding: 0 12px 0 8px; padding: 0 12px 0 8px;
} }
gap: 4px; gap: 8px;
/* text-14-medium */ /* text-14-medium */
font-family: var(--font-family-sans); font-family: var(--font-family-sans);

View file

@ -321,7 +321,6 @@ ToolRegistry.register({
render(props) { render(props) {
return ( return (
<BasicTool <BasicTool
defaultOpen
icon="console" icon="console"
trigger={{ trigger={{
title: "Shell", title: "Shell",

View file

@ -1,5 +1,6 @@
[data-component="session-turn"] { [data-component="session-turn"] {
/* flex: 1; */ /* flex: 1; */
--scroll-y: 0px;
height: 100%; height: 100%;
min-height: 0; min-height: 0;
min-width: 0; min-width: 0;
@ -26,18 +27,27 @@
align-items: flex-start; align-items: flex-start;
align-self: stretch; align-self: stretch;
min-width: 0; min-width: 0;
gap: 32px; gap: clamp(8px, calc(42px - var(--scroll-y) * 0.48), 42px);
overflow-anchor: none;
} }
[data-slot="session-turn-sticky-header"] { [data-slot="session-turn-sticky-title"] {
width: 100%; width: 100%;
position: sticky; position: sticky;
top: 0; top: 0;
background-color: var(--background-stronger); background-color: var(--background-stronger);
z-index: 21;
/* padding-bottom: clamp(0px, calc(8px - var(--scroll-y) * 0.16), 8px); */
}
[data-slot="session-turn-response-trigger"] {
position: sticky;
top: 32px;
background-color: var(--background-stronger);
z-index: 20; z-index: 20;
display: flex; width: calc(100% + 9px);
flex-direction: column; margin-left: -9px;
gap: 8px; padding-left: 9px;
padding-bottom: 8px; padding-bottom: 8px;
} }
@ -49,13 +59,8 @@
height: 32px; height: 32px;
} }
/* [data-slot="session-turn-message-content"] { */ [data-slot="session-turn-message-content"] {
/* } */ margin-top: -24px;
[data-slot="session-turn-response-trigger"] {
width: calc(100% + 9px);
margin-left: -9px;
padding-left: 9px;
} }
[data-slot="session-turn-message-title"] { [data-slot="session-turn-message-title"] {
@ -292,6 +297,7 @@
[data-slot="session-turn-collapsible"] { [data-slot="session-turn-collapsible"] {
gap: 32px; gap: 32px;
overflow: visible; overflow: visible;
/* margin-top: clamp(8px, calc(24px - var(--scroll-y) * 0.32), 24px); */
} }
[data-slot="session-turn-collapsible-trigger-content"] { [data-slot="session-turn-collapsible-trigger-content"] {

View file

@ -3,18 +3,7 @@ import { useData } from "../context"
import { useDiffComponent } from "../context/diff" import { useDiffComponent } from "../context/diff"
import { getDirectory, getFilename } from "@opencode-ai/util/path" import { getDirectory, getFilename } from "@opencode-ai/util/path"
import { checksum } from "@opencode-ai/util/encode" import { checksum } from "@opencode-ai/util/encode"
import { import { createEffect, createMemo, For, Match, onCleanup, ParentProps, Show, Switch } from "solid-js"
createEffect,
createMemo,
createSignal,
For,
Match,
onCleanup,
onMount,
ParentProps,
Show,
Switch,
} from "solid-js"
import { createResizeObserver } from "@solid-primitives/resize-observer" import { createResizeObserver } from "@solid-primitives/resize-observer"
import { DiffChanges } from "./diff-changes" import { DiffChanges } from "./diff-changes"
import { Typewriter } from "./typewriter" import { Typewriter } from "./typewriter"
@ -60,45 +49,77 @@ export function SessionTurn(
const working = createMemo(() => status()?.type !== "idle") const working = createMemo(() => status()?.type !== "idle")
let scrollRef: HTMLDivElement | undefined let scrollRef: HTMLDivElement | undefined
const [contentRef, setContentRef] = createSignal<HTMLDivElement>() const [state, setState] = createStore({
const [stickyHeaderRef, setStickyHeaderRef] = createSignal<HTMLDivElement>() contentRef: undefined as HTMLDivElement | undefined,
const [userScrolled, setUserScrolled] = createSignal(false) stickyTitleRef: undefined as HTMLDivElement | undefined,
const [stickyHeaderHeight, setStickyHeaderHeight] = createSignal(0) stickyTriggerRef: undefined as HTMLDivElement | undefined,
userScrolled: false,
stickyHeaderHeight: 0,
scrollY: 0,
autoScrolling: false,
})
function handleScroll() { function handleScroll() {
if (!scrollRef) return if (!scrollRef) return
setState("scrollY", scrollRef.scrollTop)
if (state.autoScrolling) return
const { scrollTop, scrollHeight, clientHeight } = scrollRef const { scrollTop, scrollHeight, clientHeight } = scrollRef
const atBottom = scrollHeight - scrollTop - clientHeight < 50 const atBottom = scrollHeight - scrollTop - clientHeight < 50
if (!atBottom && working()) { if (!atBottom && working()) {
setUserScrolled(true) setState("userScrolled", true)
} }
} }
function handleInteraction() { function handleInteraction() {
if (working()) { if (working()) {
setUserScrolled(true) setState("userScrolled", true)
} }
} }
function scrollToBottom() {
if (!scrollRef || state.userScrolled || !working() || state.autoScrolling) return
setState("autoScrolling", true)
requestAnimationFrame(() => {
scrollRef?.scrollTo({ top: scrollRef.scrollHeight, behavior: "auto" })
requestAnimationFrame(() => {
setState("autoScrolling", false)
})
})
}
createEffect(() => { createEffect(() => {
if (!working()) { if (!working()) {
setUserScrolled(false) setState("userScrolled", false)
} }
}) })
createResizeObserver(contentRef, () => { createResizeObserver(
if (!scrollRef || userScrolled() || !working()) return () => state.contentRef,
scrollRef.scrollTop = scrollRef.scrollHeight () => {
}) scrollToBottom()
},
)
createResizeObserver(stickyHeaderRef, ({ height }) => { createResizeObserver(
setStickyHeaderHeight(height + 8) () => state.stickyTitleRef,
}) ({ height }) => {
const triggerHeight = state.stickyTriggerRef?.offsetHeight ?? 0
setState("stickyHeaderHeight", height + triggerHeight + 8)
},
)
createResizeObserver(
() => state.stickyTriggerRef,
({ height }) => {
const titleHeight = state.stickyTitleRef?.offsetHeight ?? 0
setState("stickyHeaderHeight", titleHeight + height + 8)
},
)
return ( return (
<div data-component="session-turn" class={props.classes?.root}> <div data-component="session-turn" class={props.classes?.root} style={{ "--scroll-y": `${state.scrollY}px` }}>
<div ref={scrollRef} onScroll={handleScroll} data-slot="session-turn-content" class={props.classes?.content}> <div ref={scrollRef} onScroll={handleScroll} data-slot="session-turn-content" class={props.classes?.content}>
<div ref={setContentRef} onClick={handleInteraction}> <div ref={(el) => setState("contentRef", el)} onClick={handleInteraction}>
<Show when={message()}> <Show when={message()}>
{(message) => { {(message) => {
const assistantMessages = createMemo(() => { const assistantMessages = createMemo(() => {
@ -175,6 +196,9 @@ export function SessionTurn(
break break
} }
} else if (last.type === "reasoning") { } else if (last.type === "reasoning") {
const text = last.text ?? ""
const match = text.trimStart().match(/^\*\*(.+?)\*\*/)
if (match) return `Thinking · ${match[1].trim()}`
return "Thinking" return "Thinking"
} else if (last.type === "text") { } else if (last.type === "text") {
return "Gathering thoughts" return "Gathering thoughts"
@ -237,7 +261,7 @@ export function SessionTurn(
createEffect((prev) => { createEffect((prev) => {
const isWorking = working() const isWorking = working()
if (prev && !isWorking && !userScrolled()) { if (prev && !isWorking && !state.userScrolled) {
setStore("stepsExpanded", false) setStore("stepsExpanded", false)
} }
return isWorking return isWorking
@ -248,10 +272,10 @@ export function SessionTurn(
data-message={message().id} data-message={message().id}
data-slot="session-turn-message-container" data-slot="session-turn-message-container"
class={props.classes?.container} class={props.classes?.container}
style={{ "--sticky-header-height": `${stickyHeaderHeight()}px` }} style={{ "--sticky-header-height": `${state.stickyHeaderHeight}px` }}
> >
{/* Sticky Header */} {/* Title (sticky) */}
<div ref={setStickyHeaderRef} data-slot="session-turn-sticky-header"> <div ref={(el) => setState("stickyTitleRef", el)} data-slot="session-turn-sticky-title">
<div data-slot="session-turn-message-header"> <div data-slot="session-turn-message-header">
<div data-slot="session-turn-message-title"> <div data-slot="session-turn-message-title">
<Switch> <Switch>
@ -264,29 +288,31 @@ export function SessionTurn(
</Switch> </Switch>
</div> </div>
</div> </div>
<div data-slot="session-turn-message-content"> </div>
<Message message={message()} parts={parts()} /> {/* User Message */}
</div> <div data-slot="session-turn-message-content">
<div data-slot="session-turn-response-trigger"> <Message message={message()} parts={parts()} />
<Button </div>
data-slot="session-turn-collapsible-trigger-content" {/* Trigger (sticky) */}
variant="ghost" <div ref={(el) => setState("stickyTriggerRef", el)} data-slot="session-turn-response-trigger">
size="small" <Button
onClick={() => setStore("stepsExpanded", !store.stepsExpanded)} data-slot="session-turn-collapsible-trigger-content"
> variant="ghost"
<Show when={working()}> size="small"
<Spinner /> onClick={() => setStore("stepsExpanded", !store.stepsExpanded)}
</Show> >
<Switch> <Show when={working()}>
<Match when={working()}>{store.status ?? "Considering next steps..."}</Match> <Spinner />
<Match when={store.stepsExpanded}>Hide steps</Match> </Show>
<Match when={!store.stepsExpanded}>Show steps</Match> <Switch>
</Switch> <Match when={working()}>{store.status ?? "Considering next steps"}</Match>
<span>·</span> <Match when={store.stepsExpanded}>Hide steps</Match>
<span>{store.duration}</span> <Match when={!store.stepsExpanded}>Show steps</Match>
<Icon name="chevron-grabber-vertical" size="small" /> </Switch>
</Button> <span>·</span>
</div> <span>{store.duration}</span>
<Icon name="chevron-grabber-vertical" size="small" />
</Button>
</div> </div>
{/* Response */} {/* Response */}
<Show when={store.stepsExpanded}> <Show when={store.stepsExpanded}>

View file

@ -5,7 +5,7 @@
@keyframes pulse-opacity { @keyframes pulse-opacity {
0%, 0%,
100% { 100% {
opacity: 0; opacity: 0.4;
} }
50% { 50% {
opacity: 1; opacity: 1;
@ -18,7 +18,7 @@
opacity: 0; opacity: 0;
} }
50% { 50% {
opacity: 0.3; opacity: 0.2;
} }
} }

View file

@ -1,6 +1,6 @@
{ {
"name": "@opencode-ai/util", "name": "@opencode-ai/util",
"version": "1.0.152", "version": "1.0.153",
"private": true, "private": true,
"type": "module", "type": "module",
"exports": { "exports": {

View file

@ -54,6 +54,7 @@ export default defineConfig({
"", "",
"config", "config",
"providers", "providers",
"network",
"enterprise", "enterprise",
"troubleshooting", "troubleshooting",
"1-0", "1-0",

View file

@ -1,7 +1,7 @@
{ {
"name": "@opencode-ai/web", "name": "@opencode-ai/web",
"type": "module", "type": "module",
"version": "1.0.152", "version": "1.0.153",
"scripts": { "scripts": {
"dev": "astro dev", "dev": "astro dev",
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev", "dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",

View file

@ -67,6 +67,25 @@ You can also bind a keyboard shortcut by editing your `keymap.json`:
--- ---
### JetBrains IDEs
Add to your [JetBrains IDE](https://www.jetbrains.com/) acp.json according to the [documentation](https://www.jetbrains.com/help/ai-assistant/acp.html):
```json title="acp.json"
{
"agent_servers": {
"OpenCode": {
"command": "/absolute/path/bin/opencode",
"args": ["acp"]
}
}
}
```
To open it, use the new 'OpenCode' agent in the AI Chat agent selector.
---
### Avante.nvim ### Avante.nvim
Add to your [Avante.nvim](https://github.com/yetone/avante.nvim) configuration: Add to your [Avante.nvim](https://github.com/yetone/avante.nvim) configuration:

View file

@ -15,17 +15,21 @@ You can also check out [awesome-opencode](https://github.com/awesome-opencode/aw
## Plugins ## Plugins
| Name | Description | | Name | Description |
| ------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- | | ------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
| [opencode-helicone-session](https://github.com/H2Shami/opencode-helicone-session) | Automatically inject Helicone session headers for request grouping | | [opencode-helicone-session](https://github.com/H2Shami/opencode-helicone-session) | Automatically inject Helicone session headers for request grouping |
| [opencode-skills](https://github.com/malhashemi/opencode-skills) | Manage and organize OpenCode skills and capabilities | | [opencode-skills](https://github.com/malhashemi/opencode-skills) | Manage and organize OpenCode skills and capabilities |
| [opencode-type-inject](https://github.com/nick-vi/opencode-type-inject) | Auto-inject TypeScript/Svelte types into file reads with lookup tools | | [opencode-type-inject](https://github.com/nick-vi/opencode-type-inject) | Auto-inject TypeScript/Svelte types into file reads with lookup tools |
| [opencode-openai-codex-auth](https://github.com/numman-ali/opencode-openai-codex-auth) | Use your ChatGPT Plus/Pro subscription instead of API credits | | [opencode-openai-codex-auth](https://github.com/numman-ali/opencode-openai-codex-auth) | Use your ChatGPT Plus/Pro subscription instead of API credits |
| [opencode-gemini-auth](https://github.com/jenslys/opencode-gemini-auth) | Use your existing Gemini plan instead of API billing | | [opencode-gemini-auth](https://github.com/jenslys/opencode-gemini-auth) | Use your existing Gemini plan instead of API billing |
| [opencode-antigravity-auth](https://github.com/NoeFabris/opencode-antigravity-auth) | Use Antigravity's free models instead of API billing | | [opencode-antigravity-auth](https://github.com/NoeFabris/opencode-antigravity-auth) | Use Antigravity's free models instead of API billing |
| [opencode-dynamic-context-pruning](https://github.com/Tarquinen/opencode-dynamic-context-pruning) | Optimize token usage by pruning obsolete tool outputs | | [opencode-google-antigravity-auth](https://github.com/shekohex/opencode-goggle-antigravity-auth) | Google Antigravity OAuth Plugin, with support for Google Search, and more robust API handling |
| [opencode-wakatime](https://github.com/angristan/opencode-wakatime) | Track OpenCode usage with Wakatime | | [opencode-dynamic-context-pruning](https://github.com/Tarquinen/opencode-dynamic-context-pruning) | Optimize token usage by pruning obsolete tool outputs |
| [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main) | Clean up markdown tables produced by LLMs | | [opencode-websearch-cited](https://github.com/ghoulr/opencode-websearch-cited.git) | Add native websearch support for supported providers with Google grounded style |
| [opencode-pty](https://github.com/shekohex/opencode-pty.git) | Enables AI agents to run background processes in a PTY, send interactive input to them. |
| [opencode-wakatime](https://github.com/angrister/opencode-wakatime) | Track OpenCode usage with Wakatime |
| [opencode-md-table-formatter](https://github.com/franlol/opencode-md-table-formatter/tree/main) | Clean up markdown tables produced by LLMs |
| [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) | Background agents, pre-built LSP/AST/MCP tools, curated agents, Claude Code compatible |
--- ---

View file

@ -29,6 +29,7 @@ OpenCode comes with several built-in formatters for popular languages and framew
| dart | .dart | `dart` command available | | dart | .dart | `dart` command available |
| ocamlformat | .ml, .mli | `ocamlformat` command available and `.ocamlformat` config file | | ocamlformat | .ml, .mli | `ocamlformat` command available and `.ocamlformat` config file |
| terraform | .tf, .tfvars | `terraform` command available | | terraform | .tf, .tfvars | `terraform` command available |
| gleam | .gleam | `gleam` command available |
So if your project has `prettier` in your `package.json`, OpenCode will automatically use it. So if your project has `prettier` in your `package.json`, OpenCode will automatically use it.

View file

@ -36,6 +36,7 @@ OpenCode comes with several built-in LSP servers for popular languages:
| ocaml-lsp | .ml, .mli | `ocamllsp` command available | | ocaml-lsp | .ml, .mli | `ocamllsp` command available |
| terraform | .tf, .tfvars | Auto-installs from GitHub releases | | terraform | .tf, .tfvars | Auto-installs from GitHub releases |
| bash | .sh, .bash, .zsh, .ksh | Auto-installs bash-language-server | | bash | .sh, .bash, .zsh, .ksh | Auto-installs bash-language-server |
| gleam | .gleam | `gleam` command available |
LSP servers are automatically enabled when one of the above file extensions are detected and the requirements are met. LSP servers are automatically enabled when one of the above file extensions are detected and the requirements are met.

View file

@ -0,0 +1,57 @@
---
title: Network
description: Configure proxies and custom certificates.
---
OpenCode supports standard proxy environment variables and custom certificates for enterprise network environments.
---
## Proxy
OpenCode respects standard proxy environment variables.
```bash
# HTTPS proxy (recommended)
export HTTPS_PROXY=https://proxy.example.com:8080
# HTTP proxy (if HTTPS not available)
export HTTP_PROXY=http://proxy.example.com:8080
# Bypass proxy for local server (required)
export NO_PROXY=localhost,127.0.0.1
```
:::caution
The TUI communicates with a local HTTP server. You must bypass the proxy for this connection to prevent routing loops.
:::
You can configure the server's port and hostname using [CLI flags](/docs/cli#run).
---
### Authenticate
If your proxy requires basic authentication, include credentials in the URL.
```bash
export HTTPS_PROXY=http://username:password@proxy.example.com:8080
```
:::caution
Avoid hardcoding passwords. Use environment variables or secure credential storage.
:::
For proxies requiring advanced authentication like NTLM or Kerberos, consider using an LLM Gateway that supports your authentication method.
---
## Custom certificates
If your enterprise uses custom CAs for HTTPS connections, configure OpenCode to trust them.
```bash
export NODE_EXTRA_CA_CERTS=/path/to/ca-cert.pem
```
This works for both proxy connections and direct API access.

View file

@ -2,7 +2,7 @@
"name": "opencode", "name": "opencode",
"displayName": "opencode", "displayName": "opencode",
"description": "opencode for VS Code", "description": "opencode for VS Code",
"version": "1.0.152", "version": "1.0.153",
"publisher": "sst-dev", "publisher": "sst-dev",
"repository": { "repository": {
"type": "git", "type": "git",