Merge branch 'dev' into perm-dialog

This commit is contained in:
Aiden Cline 2025-11-20 14:12:22 -06:00
commit 207e13b80d
40 changed files with 292 additions and 112 deletions

View file

@ -37,6 +37,11 @@ jobs:
git config --global user.email "action@github.com"
git config --global user.name "Github Action"
- name: Update flake.lock
run: |
set -euo pipefail
nix flake update
- name: Update node_modules hash
run: |
set -euo pipefail
@ -62,7 +67,7 @@ jobs:
echo "" >> "$GITHUB_STEP_SUMMARY"
}
FILES=(flake.nix nix/node-modules.nix nix/hashes.json)
FILES=(flake.lock flake.nix nix/node-modules.nix nix/hashes.json)
STATUS="$(git status --short -- "${FILES[@]}" || true)"
if [ -z "$STATUS" ]; then
summarize "no changes"
@ -71,7 +76,7 @@ jobs:
fi
git add "${FILES[@]}"
git commit -m "Update Nix hashes"
git commit -m "Update Nix flake.lock and hashes"
BRANCH="${TARGET_BRANCH:-${GITHUB_REF_NAME}}"
git push origin HEAD:"$BRANCH"

1
.gitignore vendored
View file

@ -17,3 +17,4 @@ dist
refs
Session.vim
opencode.json
a.out

View file

@ -145,3 +145,4 @@
| 2025-11-17 | 780,161 (+9,092) | 723,339 (+6,743) | 1,503,500 (+15,835) |
| 2025-11-18 | 791,563 (+11,402) | 732,544 (+9,205) | 1,524,107 (+20,607) |
| 2025-11-19 | 804,409 (+12,846) | 747,624 (+15,080) | 1,552,033 (+27,926) |
| 2025-11-20 | 814,620 (+10,211) | 757,907 (+10,283) | 1,572,527 (+20,494) |

0
a.out
View file

View file

@ -41,7 +41,7 @@
},
"packages/console/core": {
"name": "@opencode-ai/console-core",
"version": "1.0.80",
"version": "1.0.81",
"dependencies": {
"@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1",
@ -68,7 +68,7 @@
},
"packages/console/function": {
"name": "@opencode-ai/console-function",
"version": "1.0.80",
"version": "1.0.81",
"dependencies": {
"@ai-sdk/anthropic": "2.0.0",
"@ai-sdk/openai": "2.0.2",
@ -92,7 +92,7 @@
},
"packages/console/mail": {
"name": "@opencode-ai/console-mail",
"version": "1.0.80",
"version": "1.0.81",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
@ -116,7 +116,7 @@
},
"packages/desktop": {
"name": "@opencode-ai/desktop",
"version": "1.0.80",
"version": "1.0.81",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@ -156,7 +156,7 @@
},
"packages/function": {
"name": "@opencode-ai/function",
"version": "1.0.80",
"version": "1.0.81",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "22.0.0",
@ -172,7 +172,7 @@
},
"packages/opencode": {
"name": "opencode",
"version": "1.0.80",
"version": "1.0.81",
"bin": {
"opencode": "./bin/opencode",
},
@ -251,7 +251,7 @@
},
"packages/plugin": {
"name": "@opencode-ai/plugin",
"version": "1.0.80",
"version": "1.0.81",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"zod": "catalog:",
@ -271,7 +271,7 @@
},
"packages/sdk/js": {
"name": "@opencode-ai/sdk",
"version": "1.0.80",
"version": "1.0.81",
"devDependencies": {
"@hey-api/openapi-ts": "0.81.0",
"@tsconfig/node22": "catalog:",
@ -282,7 +282,7 @@
},
"packages/slack": {
"name": "@opencode-ai/slack",
"version": "1.0.80",
"version": "1.0.81",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1",
@ -295,7 +295,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
"version": "1.0.80",
"version": "1.0.81",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@ -325,7 +325,7 @@
},
"packages/util": {
"name": "@opencode-ai/util",
"version": "1.0.80",
"version": "1.0.81",
"dependencies": {
"zod": "catalog:",
},
@ -335,7 +335,7 @@
},
"packages/web": {
"name": "@opencode-ai/web",
"version": "1.0.80",
"version": "1.0.81",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",

6
flake.lock generated
View file

@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1762156382,
"narHash": "sha256-Yg7Ag7ov5+36jEFC1DaZh/12SEXo6OO3/8rqADRxiqs=",
"lastModified": 1763464769,
"narHash": "sha256-AJHrsT7VoeQzErpBRlLJM1SODcaayp0joAoEA35yiwM=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "7241bcbb4f099a66aafca120d37c65e8dda32717",
"rev": "6f374686605df381de8541c072038472a5ea2e2d",
"type": "github"
},
"original": {

View file

@ -1,3 +1,3 @@
{
"nodeModules": "sha256-xqiDrKpODha+cfU6UpXLEUcApZ1xEkjRpqzFVJmq1uA="
"nodeModules": "sha256-bPiUpHGtgwVxHQHXBprpc6fFeJqW6/x7dwtQZBq29oU="
}

View file

@ -7,7 +7,7 @@
"dev:remote": "VITE_AUTH_URL=https://auth.dev.opencode.ai bun sst shell --stage=dev bun dev",
"build": "./script/generate-sitemap.ts && vinxi build && ../../opencode/script/schema.ts ./.output/public/config.json",
"start": "vinxi start",
"version": "1.0.80"
"version": "1.0.81"
},
"dependencies": {
"@ibm/plex": "6.4.1",

View file

@ -236,3 +236,14 @@ export function IconChevronRight(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
</svg>
)
}
export function IconBreakdown(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
return (
<svg {...props} width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M2 12L2 8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
<path d="M6 12L6 4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
<path d="M10 12L10 6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
<path d="M14 12L14 9" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
</svg>
)
}

View file

@ -56,6 +56,53 @@
color: var(--color-text);
font-weight: 500;
}
[data-slot="tokens-with-breakdown"] {
position: relative;
display: flex;
align-items: center;
gap: var(--space-2);
}
[data-slot="breakdown-button"] {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0;
background: transparent;
border: none;
color: var(--color-text-muted);
cursor: pointer;
transition: color 0.15s ease;
&:hover {
color: var(--color-text);
}
svg {
width: 16px;
height: 16px;
}
}
[data-slot="breakdown-popup"] {
position: absolute;
left: 0;
top: 100%;
margin-top: var(--space-2);
background: var(--color-bg);
border: 1px solid var(--color-border);
border-radius: var(--border-radius-sm);
padding: var(--space-2);
z-index: 10;
min-width: 180px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
font-size: var(--font-size-xs);
@media (prefers-color-scheme: dark) {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
}
}
tbody tr:last-child td {
@ -116,4 +163,24 @@
}
}
}
/* Breakdown popup content */
[data-slot="breakdown-row"] {
display: flex;
justify-content: space-between;
align-items: center;
gap: var(--space-4);
padding: var(--space-1) 0;
}
[data-slot="breakdown-label"] {
color: var(--color-text-muted);
font-size: var(--font-size-xs);
}
[data-slot="breakdown-value"] {
color: var(--color-text);
font-weight: 500;
font-size: var(--font-size-xs);
}
}

View file

@ -1,9 +1,9 @@
import { Billing } from "@opencode-ai/console-core/billing.js"
import { createAsync, query, useParams } from "@solidjs/router"
import { createMemo, For, Show, createEffect } from "solid-js"
import { createMemo, For, Show, createEffect, createSignal } from "solid-js"
import { formatDateUTC, formatDateForTable } from "../common"
import { withActor } from "~/context/auth.withActor"
import { IconChevronLeft, IconChevronRight } from "~/component/icon"
import { IconChevronLeft, IconChevronRight, IconBreakdown } from "~/component/icon"
import styles from "./usage-section.module.css"
import { createStore } from "solid-js/store"
@ -22,15 +22,34 @@ export function UsageSection() {
const params = useParams()
const usage = createAsync(() => queryUsageInfo(params.id!, 0))
const [store, setStore] = createStore({ page: 0, usage: [] as Awaited<ReturnType<typeof getUsageInfo>> })
const [openBreakdownId, setOpenBreakdownId] = createSignal<string | null>(null)
createEffect(() => {
setStore({ usage: usage() })
}, [usage])
createEffect(() => {
if (!openBreakdownId()) return
const handleClickOutside = (e: MouseEvent) => {
const target = e.target as HTMLElement
if (!target.closest('[data-slot="tokens-with-breakdown"]')) {
setOpenBreakdownId(null)
}
}
document.addEventListener("click", handleClickOutside)
return () => document.removeEventListener("click", handleClickOutside)
})
const hasResults = createMemo(() => store.usage && store.usage.length > 0)
const canGoPrev = createMemo(() => store.page > 0)
const canGoNext = createMemo(() => store.usage && store.usage.length === PAGE_SIZE)
const calculateTotalInputTokens = (u: Awaited<ReturnType<typeof getUsageInfo>>[0]) => {
return u.inputTokens + (u.cacheReadTokens ?? 0) + (u.cacheWrite5mTokens ?? 0) + (u.cacheWrite1hTokens ?? 0)
}
const goPrev = async () => {
const usage = await getUsageInfo(params.id!, store.page - 1)
setStore({
@ -73,15 +92,50 @@ export function UsageSection() {
</thead>
<tbody>
<For each={store.usage}>
{(usage) => {
{(usage, index) => {
const date = createMemo(() => new Date(usage.timeCreated))
const totalInputTokens = createMemo(() => calculateTotalInputTokens(usage))
const breakdownId = `breakdown-${index()}`
const isOpen = createMemo(() => openBreakdownId() === breakdownId)
const isClaude = usage.model.toLowerCase().includes("claude")
return (
<tr>
<td data-slot="usage-date" title={formatDateUTC(date())}>
{formatDateForTable(date())}
</td>
<td data-slot="usage-model">{usage.model}</td>
<td data-slot="usage-tokens">{usage.inputTokens}</td>
<td data-slot="usage-tokens">
<div data-slot="tokens-with-breakdown" onClick={(e) => e.stopPropagation()}>
<button
data-slot="breakdown-button"
onClick={(e) => {
e.stopPropagation()
setOpenBreakdownId(isOpen() ? null : breakdownId)
}}
>
<IconBreakdown />
</button>
<span onClick={() => setOpenBreakdownId(null)}>{totalInputTokens()}</span>
<Show when={isOpen()}>
<div data-slot="breakdown-popup" onClick={(e) => e.stopPropagation()}>
<div data-slot="breakdown-row">
<span data-slot="breakdown-label">Input</span>
<span data-slot="breakdown-value">{usage.inputTokens}</span>
</div>
<div data-slot="breakdown-row">
<span data-slot="breakdown-label">Cache Read</span>
<span data-slot="breakdown-value">{usage.cacheReadTokens ?? 0}</span>
</div>
<Show when={isClaude}>
<div data-slot="breakdown-row">
<span data-slot="breakdown-label">Cache Write</span>
<span data-slot="breakdown-value">{usage.cacheWrite5mTokens ?? 0}</span>
</div>
</Show>
</div>
</Show>
</div>
</td>
<td data-slot="usage-tokens">{usage.outputTokens}</td>
<td data-slot="usage-cost">${((usage.cost ?? 0) / 100000000).toFixed(4)}</td>
</tr>

View file

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

View file

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

View file

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

View file

@ -1,6 +1,6 @@
{
"name": "@opencode-ai/desktop",
"version": "1.0.80",
"version": "1.0.81",
"description": "",
"type": "module",
"scripts": {

View file

@ -1,7 +1,7 @@
id = "opencode"
name = "OpenCode"
description = "The AI coding agent built for the terminal"
version = "1.0.80"
version = "1.0.81"
schema_version = 1
authors = ["Anomaly"]
repository = "https://github.com/sst/opencode"
@ -11,26 +11,26 @@ name = "OpenCode"
icon = "./icons/opencode.svg"
[agent_servers.opencode.targets.darwin-aarch64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.80/opencode-darwin-arm64.zip"
archive = "https://github.com/sst/opencode/releases/download/v1.0.81/opencode-darwin-arm64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.darwin-x86_64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.80/opencode-darwin-x64.zip"
archive = "https://github.com/sst/opencode/releases/download/v1.0.81/opencode-darwin-x64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-aarch64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.80/opencode-linux-arm64.zip"
archive = "https://github.com/sst/opencode/releases/download/v1.0.81/opencode-linux-arm64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-x86_64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.80/opencode-linux-x64.zip"
archive = "https://github.com/sst/opencode/releases/download/v1.0.81/opencode-linux-x64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.windows-x86_64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.80/opencode-windows-x64.zip"
archive = "https://github.com/sst/opencode/releases/download/v1.0.81/opencode-windows-x64.zip"
cmd = "./opencode.exe"
args = ["acp"]

View file

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

View file

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

View file

@ -132,10 +132,10 @@ if (!Script.preview) {
"package() {",
` cd "opencode-\${pkgver}/packages/opencode"`,
' mkdir -p "${pkgdir}/usr/bin"',
' arch="x64"',
' target_arch="x64"',
' case "$CARCH" in',
' x86_64) arch="x64" ;;',
' aarch64) arch="arm64" ;;',
' x86_64) target_arch="x64" ;;',
' aarch64) target_arch="arm64" ;;',
' *) printf "unsupported architecture: %s\\n" "$CARCH" >&2 ; return 1 ;;',
" esac",
' libc=""',
@ -148,14 +148,14 @@ if (!Script.preview) {
' libc="-musl"',
" fi",
' base=""',
' if [ "$arch" = "x64" ]; then',
' if [ "$target_arch" = "x64" ]; then',
" if ! grep -qi avx2 /proc/cpuinfo 2>/dev/null; then",
' base="-baseline"',
" fi",
" fi",
' bin="dist/opencode-linux-${arch}${base}${libc}/bin/opencode"',
' bin="dist/opencode-linux-${target_arch}${base}${libc}/bin/opencode"',
' if [ ! -f "$bin" ]; then',
' printf "unable to find binary for %s%s%s\\n" "$arch" "$base" "$libc" >&2',
' printf "unable to find binary for %s%s%s\\n" "$target_arch" "$base" "$libc" >&2',
" return 1",
" fi",
' install -Dm755 "$bin" "${pkgdir}/usr/bin/opencode"',

View file

@ -53,15 +53,7 @@ export function Autocomplete(props: {
// Track props.value to make memo reactive to text changes
props.value // <- there surely is a better way to do this, like making .input() reactive
const val = props.input().getTextRange(store.index + 1, props.input().cursorOffset + 1)
// If the filter contains a space, hide the autocomplete
if (val.includes(" ")) {
hide()
return undefined
}
return val
return props.input().getTextRange(store.index + 1, props.input().cursorOffset)
})
function insertPart(text: string, part: PromptInfo["parts"][number]) {
@ -132,9 +124,10 @@ export function Autocomplete(props: {
(item): AutocompleteOption => ({
display: Locale.truncateMiddle(item, width),
onSelect: () => {
const mime = Bun.file(item).type || "text/plain"
insertPart(item, {
type: "file",
mime: "text/plain",
mime,
filename: item,
url: `file://${process.cwd()}/${item}`,
source: {
@ -387,17 +380,19 @@ export function Autocomplete(props: {
get visible() {
return store.visible
},
onInput() {
onInput(value) {
if (store.visible) {
if (props.input().cursorOffset <= store.index) {
if (
// Typed text before the trigger
props.input().cursorOffset <= store.index ||
// There is a space between the trigger and the cursor
props.input().getTextRange(store.index, props.input().cursorOffset).match(/\s/) ||
// "/<command>" is not the sole content
(store.visible === "/" && value.match(/^\S+\s+\S+\s*$/))
) {
hide()
return
}
// Check if a space was typed after the trigger character
const currentText = props.input().getTextRange(store.index + 1, props.input().cursorOffset + 1)
if (currentText.includes(" ")) {
hide()
}
}
},
onKeyDown(e: KeyEvent) {

View file

@ -6,7 +6,7 @@ import { CliRenderer } from "@opentui/core"
export namespace Editor {
export async function open(opts: { value: string; renderer: CliRenderer }): Promise<string | undefined> {
const editor = process.env["EDITOR"]
const editor = process.env["VISUAL"] || process.env["EDITOR"]
if (!editor) return
const filepath = join(tmpdir(), `${Date.now()}.md`)

View file

@ -428,8 +428,8 @@ export namespace Config {
input_newline: z.string().optional().default("shift+return,ctrl+j").describe("Insert newline in input"),
history_previous: z.string().optional().default("up").describe("Previous history item"),
history_next: z.string().optional().default("down").describe("Next history item"),
session_child_cycle: z.string().optional().default("ctrl+right").describe("Next child session"),
session_child_cycle_reverse: z.string().optional().default("ctrl+left").describe("Previous child session"),
session_child_cycle: z.string().optional().default("<leader>right").describe("Next child session"),
session_child_cycle_reverse: z.string().optional().default("<leader>left").describe("Previous child session"),
})
.strict()
.meta({

View file

@ -11,6 +11,8 @@ export namespace Flag {
export const OPENCODE_ENABLE_EXPERIMENTAL_MODELS = truthy("OPENCODE_ENABLE_EXPERIMENTAL_MODELS")
export const OPENCODE_DISABLE_AUTOCOMPACT = truthy("OPENCODE_DISABLE_AUTOCOMPACT")
export const OPENCODE_FAKE_VCS = process.env["OPENCODE_FAKE_VCS"]
export const OPENCODE_EXPERIMENTAL_BASH_MAX_OUTPUT_LENGTH =
process.env["OPENCODE_EXPERIMENTAL_BASH_MAX_OUTPUT_LENGTH"]
// Experimental
export const OPENCODE_EXPERIMENTAL = truthy("OPENCODE_EXPERIMENTAL")

View file

@ -250,12 +250,12 @@ export namespace LSPServer {
},
}
export const RubyLsp: Info = {
export const Rubocop: Info = {
id: "ruby-lsp",
root: NearestRoot(["Gemfile"]),
extensions: [".rb", ".rake", ".gemspec", ".ru"],
async spawn(root) {
let bin = Bun.which("ruby-lsp", {
let bin = Bun.which("rubocop", {
PATH: process.env["PATH"] + ":" + Global.Path.bin,
})
if (!bin) {
@ -266,25 +266,25 @@ export namespace LSPServer {
return
}
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
log.info("installing ruby-lsp")
log.info("installing rubocop")
const proc = Bun.spawn({
cmd: ["gem", "install", "ruby-lsp", "--bindir", Global.Path.bin],
cmd: ["gem", "install", "rubocop", "--bindir", Global.Path.bin],
stdout: "pipe",
stderr: "pipe",
stdin: "pipe",
})
const exit = await proc.exited
if (exit !== 0) {
log.error("Failed to install ruby-lsp")
log.error("Failed to install rubocop")
return
}
bin = path.join(Global.Path.bin, "ruby-lsp" + (process.platform === "win32" ? ".exe" : ""))
log.info(`installed ruby-lsp`, {
bin = path.join(Global.Path.bin, "rubocop" + (process.platform === "win32" ? ".exe" : ""))
log.info(`installed rubocop`, {
bin,
})
}
return {
process: spawn(bin!, ["--stdio"], {
process: spawn(bin!, ["--lsp"], {
cwd: root,
}),
}

View file

@ -42,6 +42,9 @@ import { SessionSummary } from "@/session/summary"
import { GlobalBus } from "@/bus/global"
import { SessionStatus } from "@/session/status"
// @ts-ignore This global is needed to prevent ai-sdk from logging warnings to stdout https://github.com/vercel/ai/blob/2dc67e0ef538307f21368db32d5a12345d98831b/packages/ai/src/logger/log-warnings.ts#L85
globalThis.AI_SDK_LOG_WARNINGS = false
const ERRORS = {
400: {
description: "Bad request",

View file

@ -638,6 +638,7 @@ export namespace MessageV2 {
state: "output-available",
toolCallId: part.callID,
input: part.state.input,
// TODO: prolly need something better here when dealing with synthetic user messages + attachments
output: part.state.time.compacted ? "[Old tool result content cleared]" : part.state.output,
callProviderMetadata: part.metadata,
})

View file

@ -12,9 +12,14 @@ import { Filesystem } from "@/util/filesystem"
import { Wildcard } from "@/util/wildcard"
import { Permission } from "@/permission"
import { fileURLToPath } from "url"
import { Flag } from "@/flag/flag.ts"
import path from "path"
const MAX_OUTPUT_LENGTH = 30_000
const DEFAULT_MAX_OUTPUT_LENGTH = 30_000
const MAX_OUTPUT_LENGTH = (() => {
const parsed = Number(Flag.OPENCODE_EXPERIMENTAL_BASH_MAX_OUTPUT_LENGTH)
return Number.isInteger(parsed) && parsed > 0 ? parsed : DEFAULT_MAX_OUTPUT_LENGTH
})()
const DEFAULT_TIMEOUT = 1 * 60 * 1000
const MAX_TIMEOUT = 10 * 60 * 1000
const SIGKILL_TIMEOUT_MS = 200

View file

@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/plugin",
"version": "1.0.80",
"version": "1.0.81",
"type": "module",
"scripts": {
"typecheck": "tsgo --noEmit",

View file

@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/sdk",
"version": "1.0.80",
"version": "1.0.81",
"type": "module",
"scripts": {
"typecheck": "tsgo --noEmit",

View file

@ -81,8 +81,8 @@ class KeybindsConfig:
session_unshare: Union[Unset, str] = "none"
session_interrupt: Union[Unset, str] = "esc"
session_compact: Union[Unset, str] = "<leader>c"
session_child_cycle: Union[Unset, str] = "ctrl+right"
session_child_cycle_reverse: Union[Unset, str] = "ctrl+left"
session_child_cycle: Union[Unset, str] = "<leader>right"
session_child_cycle_reverse: Union[Unset, str] = "<leader>left"
messages_page_up: Union[Unset, str] = "pgup"
messages_page_down: Union[Unset, str] = "pgdown"
messages_half_page_up: Union[Unset, str] = "ctrl+alt+u"

View file

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

View file

@ -1,6 +1,6 @@
{
"name": "@opencode-ai/ui",
"version": "1.0.80",
"version": "1.0.81",
"type": "module",
"exports": {
".": "./src/components/index.ts",

View file

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

View file

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

View file

@ -89,8 +89,8 @@ A general-purpose agent for researching complex questions, searching for code, a
```
3. **Navigation between sessions**: When subagents create their own child sessions, you can navigate between the parent session and all child sessions using:
- **Ctrl+Right** (or your configured `session_child_cycle` keybind) to cycle forward through parent → child1 → child2 → ... → parent
- **Ctrl+Left** (or your configured `session_child_cycle_reverse` keybind) to cycle backward through parent ← child1 ← child2 ← ... ← parent
- **\<Leader>+Right** (or your configured `session_child_cycle` keybind) to cycle forward through parent → child1 → child2 → ... → parent
- **\<Leader>+Left** (or your configured `session_child_cycle_reverse` keybind) to cycle backward through parent ← child1 ← child2 ← ... ← parent
This allows you to seamlessly switch between the main conversation and specialized subagent work.

View file

@ -23,8 +23,8 @@ OpenCode has a list of keybinds that you can customize through the OpenCode conf
"session_unshare": "none",
"session_interrupt": "escape",
"session_compact": "<leader>c",
"session_child_cycle": "ctrl+right",
"session_child_cycle_reverse": "ctrl+left",
"session_child_cycle": "<leader>+right",
"session_child_cycle_reverse": "<leader>+left",
"messages_page_up": "pageup",
"messages_page_down": "pagedown",
"messages_half_page_up": "ctrl+alt+u",

View file

@ -11,27 +11,27 @@ OpenCode integrates with your Language Server Protocol (LSP) to help the LLM int
OpenCode comes with several built-in LSP servers for popular languages:
| LSP Server | Extensions | Requirements |
| ---------------- | ---------------------------------------------------- | ------------------------------------------------------------ |
| typescript | .ts, .tsx, .js, .jsx, .mjs, .cjs, .mts, .cts | `typescript` dependency in project |
| deno | .ts, .tsx, .js, .jsx, .mjs | `deno` command available (auto-detects deno.json/deno.jsonc) |
| eslint | .ts, .tsx, .js, .jsx, .mjs, .cjs, .mts, .cts, .vue | `eslint` dependency in project |
| gopls | .go | `go` command available |
| ruby-lsp | .rb, .rake, .gemspec, .ru | `ruby` and `gem` commands available |
| pyright | .py, .pyi | `pyright` dependency installed |
| elixir-ls | .ex, .exs | `elixir` command available |
| zls | .zig, .zon | `zig` command available |
| csharp | .cs | `.NET SDK` installed |
| vue | .vue | Auto-installs for Vue projects |
| rust | .rs | `rust-analyzer` command available |
| clangd | .c, .cpp, .cc, .cxx, .c++, .h, .hpp, .hh, .hxx, .h++ | Auto-installs for C/C++ projects |
| svelte | .svelte | Auto-installs for Svelte projects |
| astro | .astro | Auto-installs for Astro projects |
| yaml-ls | .yaml, .yml | Auto-installs Red Hat yaml-language-server |
| jdtls | .java | `Java SDK (version 21+)` installed |
| lua-ls | .lua | Auto-installs for Lua projects |
| sourcekit-lsp | .swift, .objc, .objcpp | `swift` installed (`xcode` on macOS) |
| php intelephense | .php | Auto-installs for PHP projects |
| LSP Server | Extensions | Requirements |
| ------------------ | ---------------------------------------------------- | ------------------------------------------------------------ |
| typescript | .ts, .tsx, .js, .jsx, .mjs, .cjs, .mts, .cts | `typescript` dependency in project |
| deno | .ts, .tsx, .js, .jsx, .mjs | `deno` command available (auto-detects deno.json/deno.jsonc) |
| eslint | .ts, .tsx, .js, .jsx, .mjs, .cjs, .mts, .cts, .vue | `eslint` dependency in project |
| gopls | .go | `go` command available |
| ruby-lsp (rubocop) | .rb, .rake, .gemspec, .ru | `ruby` and `gem` commands available |
| pyright | .py, .pyi | `pyright` dependency installed |
| elixir-ls | .ex, .exs | `elixir` command available |
| zls | .zig, .zon | `zig` command available |
| csharp | .cs | `.NET SDK` installed |
| vue | .vue | Auto-installs for Vue projects |
| rust | .rs | `rust-analyzer` command available |
| clangd | .c, .cpp, .cc, .cxx, .c++, .h, .hpp, .hh, .hxx, .h++ | Auto-installs for C/C++ projects |
| svelte | .svelte | Auto-installs for Svelte projects |
| astro | .astro | Auto-installs for Astro projects |
| yaml-ls | .yaml, .yml | Auto-installs Red Hat yaml-language-server |
| jdtls | .java | `Java SDK (version 21+)` installed |
| lua-ls | .lua | Auto-installs for Lua projects |
| sourcekit-lsp | .swift, .objc, .objcpp | `swift` installed (`xcode` on macOS) |
| php intelephense | .php | Auto-installs for PHP projects |
LSP servers are automatically enabled when one of the above file extensions are detected and the requirements are met.
@ -114,7 +114,7 @@ You can add custom LSP servers by specifying the command and file extensions:
### PHP Intelephense
PHP Intelephense offers premium features through a license key. Uou can provide a license key by placing (only) the key in a text file at:
PHP Intelephense offers premium features through a license key. You can provide a license key by placing (only) the key in a text file at:
- On macOS/Linux: `$HOME/intelephense/licence.txt`
- On Windows: `%USERPROFILE%/intelephense/licence.txt`

View file

@ -35,17 +35,16 @@ Consider using one of the models we recommend.
However, there are only a few of them that are good at both generating code and tool calling.
Here are several models, in no particular order, that work well with OpenCode (to name a few):
Here are several models that work well with OpenCode, in no particular order. (This is not an exhaustive list):
- GPT 5
- GPT 5 Codex
- GPT 5.1
- GPT 5.1 Codex
- Claude Sonnet 4.5
- Claude Sonnet 4
- Claude Opus 4.1
- Claude Haiku 4.5
- Kimi K2
- GLM 4.6
- Qwen3 Coder
- GPT 4.1
- Gemini 2.5 Pro
- Gemini 3 Pro
---

View file

@ -357,6 +357,42 @@ Or if you already have an API key, you can select **Manually enter API Key** and
---
### Cortecs
1. Head over to the [Cortecs console](https://cortecs.ai/), create an account, and generate an API key.
2. Run `opencode auth login` and select **Cortecs**.
```bash
$ opencode auth login
┌ Add credential
◆ Select provider
│ ● Cortecs
│ ...
```
3. Enter your Cortecs API key.
```bash
$ opencode auth login
┌ Add credential
◇ Select provider
│ Cortecs
◇ Enter your API key
│ _
```
4. Run the `/models` command to select a model like _Kimi K2 Instruct_.
---
### DeepSeek
1. Head over to the [DeepSeek console](https://platform.deepseek.com/), create an account, and click **Create new API key**.

View file

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