Compare commits

..

41 Commits

Author SHA1 Message Date
opencode 930a1bf358 release: v1.0.55 2025-11-10 06:47:55 +00:00
GitHub Action 6cf7f18cc9 chore: format code 2025-11-10 06:41:40 +00:00
Aiden Cline 3f59570ee6 fix: add null check 2025-11-10 00:40:42 -06:00
GitHub Action 304e956b5d chore: format code 2025-11-10 06:00:38 +00:00
Wankong 538eaa42aa docs: format installation commands for madkrown code block (#4151)
Co-authored-by: GitHub Action <action@github.com>
2025-11-09 23:59:57 -06:00
opencode 67c41fd389 release: v1.0.54 2025-11-10 05:50:33 +00:00
GitHub Action 83ea19770a chore: format code 2025-11-10 05:44:26 +00:00
Dax Raad 3ace8543b2 tui: auto-scroll to bottom when switching between sessions 2025-11-10 00:43:48 -05:00
opencode eb855e1e31 release: v1.0.53 2025-11-10 05:42:55 +00:00
Dax Raad 5e53f054c6 make reasoning parts less ugly 2025-11-10 00:37:35 -05:00
Dax Raad 5d5e184329 tui: improve session UI with better sidebar toggle and message handling 2025-11-10 00:21:31 -05:00
Dax Raad 2fbb49ac30 tui: render reasoning parts with syntax highlighting instead of plain text 2025-11-10 00:08:05 -05:00
Dax Raad c56b407e1d tui: display 'Free' badge for zero-cost models in model selection dialog 2025-11-09 23:56:24 -05:00
lenstr bdaa0e8b8c fix: --model flag being ignored in TUI mode (#4147) 2025-11-09 20:21:52 -06:00
Aiden Cline 4e549b1c05 fix: allow user to configure doom loop & external dir perms (#4095) 2025-11-09 20:21:38 -06:00
Aiden Cline 7be8e16c33 ci: sync zed 2025-11-09 13:54:14 -06:00
Frank d1588f93a1 doc: add big pickle to doc 2025-11-09 14:12:54 -05:00
Aiden Cline 576696a370 ci: update description 2025-11-09 13:09:52 -06:00
Aiden Cline 2c6f9043e8 ci: fix 2025-11-09 13:06:32 -06:00
Aiden Cline 9f771ef0ae ci: fix 2025-11-09 13:05:51 -06:00
Aiden Cline 356715f67d ci: fix regex 2025-11-09 13:03:10 -06:00
Aiden Cline 540421267a fix 2025-11-09 12:56:17 -06:00
Aiden Cline e253398936 ci: ignore 2025-11-09 12:54:59 -06:00
Aiden Cline ee87e1f139 ci: ignore 2025-11-09 12:54:22 -06:00
Aiden Cline 8887616457 ci: sync zed 2025-11-09 12:47:36 -06:00
Aiden Cline 905c034885 ci: zed sync 2025-11-09 12:00:08 -06:00
Aiden Cline 92f7c4943f ci: zed sync 2025-11-09 11:59:00 -06:00
Aiden Cline 10bde356b1 chore: rm comment 2025-11-09 11:50:02 -06:00
Aiden Cline f7cc46cd9f set cap for max time to wait between retries (#4135)
Co-authored-by: GitHub Action <action@github.com>
2025-11-09 11:46:58 -06:00
Mathias Beugnon d9ffe07391 fix: messageID type in chat.message (#4128)
Co-authored-by: GitHub Action <action@github.com>
2025-11-09 09:39:50 -06:00
GitHub Action c0702ed8bd ignore: update download stats 2025-11-09 2025-11-09 12:04:16 +00:00
opencode b927b9dca6 release: v1.0.51 2025-11-09 06:46:42 +00:00
Dax Raad 4b7231be68 fix race condition 2025-11-09 01:41:49 -05:00
GitHub Action 70a6fe96ea chore: format code 2025-11-09 06:01:58 +00:00
Aiden Cline 6e5971dff2 ci: update sync zed 2025-11-09 00:01:13 -06:00
opencode d48d6b3577 release: v1.0.50 2025-11-09 05:53:26 +00:00
Aiden Cline 4b1668c3ef Revert "tui: display 'Free' badge for zero-cost models in model selection dialog"
This reverts commit ce9b758d0a.
2025-11-08 23:48:18 -06:00
Mathias Beugnon d85eb1b880 feat: add input context to chat.params and chat.message (#4085) 2025-11-08 23:29:27 -06:00
Ivan Starkov 9637d70407 fix: UI Freezes for a few minutes if repo has binary files (#4109) 2025-11-08 23:28:09 -06:00
Aiden Cline cfbbdc2e14 ci: add job to sync zed extension 2025-11-08 23:23:43 -06:00
GitHub Action feb65201f6 chore: format code 2025-11-09 01:58:05 +00:00
38 changed files with 592 additions and 195 deletions
+34
View File
@@ -0,0 +1,34 @@
name: "sync-zed-extension"
on:
workflow_dispatch:
release:
types: [published]
jobs:
zed:
name: Release Zed Extension
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: ./.github/actions/setup-bun
- name: Get version tag
id: get_tag
run: |
if [ "${{ github.event_name }}" = "release" ]; then
TAG="${{ github.event.release.tag_name }}"
else
TAG=$(git tag --list 'v[0-9]*.*' --sort=-version:refname | head -n 1)
fi
echo "tag=${TAG}" >> $GITHUB_OUTPUT
echo "Using tag: ${TAG}"
- name: Sync Zed extension
run: |
./script/sync-zed.ts ${{ steps.get_tag.outputs.tag }}
env:
GITHUB_TOKEN: ${{ secrets.SST_GITHUB_TOKEN }}
+1
View File
@@ -134,3 +134,4 @@
| 2025-11-06 | 686,252 (+11,178) | 630,885 (+11,195) | 1,317,137 (+22,373) |
| 2025-11-07 | 696,646 (+10,394) | 642,146 (+11,261) | 1,338,792 (+21,655) |
| 2025-11-08 | 706,035 (+9,389) | 653,489 (+11,343) | 1,359,524 (+20,732) |
| 2025-11-09 | 713,462 (+7,427) | 660,459 (+6,970) | 1,373,921 (+14,397) |
+11 -11
View File
@@ -39,7 +39,7 @@
},
"packages/console/core": {
"name": "@opencode-ai/console-core",
"version": "1.0.49",
"version": "1.0.55",
"dependencies": {
"@aws-sdk/client-sts": "3.782.0",
"@jsx-email/render": "1.1.1",
@@ -66,7 +66,7 @@
},
"packages/console/function": {
"name": "@opencode-ai/console-function",
"version": "1.0.49",
"version": "1.0.55",
"dependencies": {
"@ai-sdk/anthropic": "2.0.0",
"@ai-sdk/openai": "2.0.2",
@@ -90,7 +90,7 @@
},
"packages/console/mail": {
"name": "@opencode-ai/console-mail",
"version": "1.0.49",
"version": "1.0.55",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
@@ -114,7 +114,7 @@
},
"packages/desktop": {
"name": "@opencode-ai/desktop",
"version": "1.0.49",
"version": "1.0.55",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -154,7 +154,7 @@
},
"packages/function": {
"name": "@opencode-ai/function",
"version": "1.0.49",
"version": "1.0.55",
"dependencies": {
"@octokit/auth-app": "8.0.1",
"@octokit/rest": "22.0.0",
@@ -170,7 +170,7 @@
},
"packages/opencode": {
"name": "opencode",
"version": "1.0.49",
"version": "1.0.55",
"bin": {
"opencode": "./bin/opencode",
},
@@ -248,7 +248,7 @@
},
"packages/plugin": {
"name": "@opencode-ai/plugin",
"version": "1.0.49",
"version": "1.0.55",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"zod": "catalog:",
@@ -268,7 +268,7 @@
},
"packages/sdk/js": {
"name": "@opencode-ai/sdk",
"version": "1.0.49",
"version": "1.0.55",
"devDependencies": {
"@hey-api/openapi-ts": "0.81.0",
"@tsconfig/node22": "catalog:",
@@ -279,7 +279,7 @@
},
"packages/slack": {
"name": "@opencode-ai/slack",
"version": "1.0.49",
"version": "1.0.55",
"dependencies": {
"@opencode-ai/sdk": "workspace:*",
"@slack/bolt": "^3.17.1",
@@ -292,7 +292,7 @@
},
"packages/ui": {
"name": "@opencode-ai/ui",
"version": "1.0.49",
"version": "1.0.55",
"dependencies": {
"@kobalte/core": "catalog:",
"@opencode-ai/sdk": "workspace:*",
@@ -322,7 +322,7 @@
},
"packages/web": {
"name": "@opencode-ai/web",
"version": "1.0.49",
"version": "1.0.55",
"dependencies": {
"@astrojs/cloudflare": "12.6.3",
"@astrojs/markdown-remark": "6.3.1",
+1 -1
View File
@@ -7,7 +7,7 @@
"dev:remote": "VITE_AUTH_URL=https://auth.dev.opencode.ai bun sst shell --stage=dev bun dev",
"build": "./script/generate-sitemap.ts && vinxi build && ../../opencode/script/schema.ts ./.output/public/config.json",
"start": "vinxi start",
"version": "1.0.49"
"version": "1.0.55"
},
"dependencies": {
"@ibm/plex": "6.4.1",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/console-core",
"version": "1.0.49",
"version": "1.0.55",
"private": true,
"type": "module",
"dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-function",
"version": "1.0.49",
"version": "1.0.55",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/console-mail",
"version": "1.0.49",
"version": "1.0.55",
"dependencies": {
"@jsx-email/all": "2.2.3",
"@jsx-email/cli": "1.4.3",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/desktop",
"version": "1.0.49",
"version": "1.0.55",
"description": "",
"type": "module",
"scripts": {
+6 -6
View File
@@ -1,7 +1,7 @@
id = "opencode"
name = "OpenCode"
description = "The AI coding agent built for the terminal"
version = "1.0.49"
version = "1.0.55"
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.49/opencode-darwin-arm64.zip"
archive = "https://github.com/sst/opencode/releases/download/v1.0.55/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.49/opencode-darwin-x64.zip"
archive = "https://github.com/sst/opencode/releases/download/v1.0.55/opencode-darwin-x64.zip"
cmd = "./opencode"
args = ["acp"]
[agent_servers.opencode.targets.linux-aarch64]
archive = "https://github.com/sst/opencode/releases/download/v1.0.49/opencode-linux-arm64.zip"
archive = "https://github.com/sst/opencode/releases/download/v1.0.55/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.49/opencode-linux-x64.zip"
archive = "https://github.com/sst/opencode/releases/download/v1.0.55/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.49/opencode-windows-x64.zip"
archive = "https://github.com/sst/opencode/releases/download/v1.0.55/opencode-windows-x64.zip"
cmd = "./opencode.exe"
args = ["acp"]
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/function",
"version": "1.0.49",
"version": "1.0.55",
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"type": "module",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json.schemastore.org/package.json",
"version": "1.0.49",
"version": "1.0.55",
"name": "opencode",
"type": "module",
"private": true,
+6
View File
@@ -20,6 +20,8 @@ export namespace Agent {
edit: Config.Permission,
bash: z.record(z.string(), Config.Permission),
webfetch: Config.Permission.optional(),
doom_loop: Config.Permission.optional(),
external_directory: Config.Permission.optional(),
}),
model: z
.object({
@@ -45,6 +47,8 @@ export namespace Agent {
"*": "allow",
},
webfetch: "allow",
doom_loop: "ask",
external_directory: "ask",
}
const agentPermission = mergeAgentPermissions(defaultPermission, cfg.permission ?? {})
@@ -244,6 +248,8 @@ function mergeAgentPermissions(basePermission: any, overridePermission: any): Ag
edit: merged.edit ?? "allow",
webfetch: merged.webfetch ?? "allow",
bash: mergedBash ?? { "*": "allow" },
doom_loop: merged.doom_loop,
external_directory: merged.external_directory,
}
return result
@@ -36,7 +36,7 @@ export function DialogModel() {
title: model.name ?? item.modelID,
description: provider.name,
category: "Recent",
footer: model.cost.input === 0 && provider.id === "opencode" ? <Free /> : undefined,
footer: model.cost?.input === 0 && provider.id === "opencode" ? <Free /> : undefined,
},
]
})
@@ -59,7 +59,7 @@ export function DialogModel() {
title: info.name ?? model,
description: provider.name,
category: provider.name,
footer: info.cost.input === 0 && provider.id === "opencode" ? <Free /> : undefined,
footer: info.cost?.input === 0 && provider.id === "opencode" ? <Free /> : undefined,
})),
filter((x) => Boolean(ref()?.filter) || !local.model.recent().find((y) => isDeepEqual(y, x.value))),
),
@@ -221,23 +221,22 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
sdk.client.app.agents().then((x) => setStore("agent", x.data ?? [])),
sdk.client.config.get().then((x) => setStore("config", x.data!)),
]).then(() => {
if (store.status === "loading") setStore("status", "partial")
})
// non-blocking
Promise.all([
sdk.client.session.list().then((x) =>
setStore(
"session",
(x.data ?? []).toSorted((a, b) => a.id.localeCompare(b.id)),
setStore("status", "partial")
// non-blocking
Promise.all([
sdk.client.session.list().then((x) =>
setStore(
"session",
(x.data ?? []).toSorted((a, b) => a.id.localeCompare(b.id)),
),
),
),
sdk.client.command.list().then((x) => setStore("command", x.data ?? [])),
sdk.client.lsp.status().then((x) => setStore("lsp", x.data!)),
sdk.client.mcp.status().then((x) => setStore("mcp", x.data!)),
sdk.client.formatter.status().then((x) => setStore("formatter", x.data!)),
]).then(() => {
setStore("status", "complete")
sdk.client.command.list().then((x) => setStore("command", x.data ?? [])),
sdk.client.lsp.status().then((x) => setStore("lsp", x.data!)),
sdk.client.mcp.status().then((x) => setStore("mcp", x.data!)),
sdk.client.formatter.status().then((x) => setStore("formatter", x.data!)),
]).then(() => {
setStore("status", "complete")
})
})
const result = {
@@ -149,15 +149,6 @@ export function Session() {
}, 50)
}
// snap to bottom when revert position changes
createEffect((old) => {
if (old !== session()?.revert?.messageID) toBottom()
return session()?.revert?.messageID
})
// snap to bottom when session changes
createEffect(on(() => route.sessionID, toBottom))
const local = useLocal()
function moveChild(direction: number) {
@@ -272,14 +263,18 @@ export function Session() {
const revert = session().revert?.messageID
const message = messages().findLast((x) => (!revert || x.id < revert) && x.role === "user")
if (!message) return
sdk.client.session.revert({
path: {
id: route.sessionID,
},
body: {
messageID: message.id,
},
})
sdk.client.session
.revert({
path: {
id: route.sessionID,
},
body: {
messageID: message.id,
},
})
.then(() => {
toBottom()
})
const parts = sync.data.part[message.id]
prompt.set(
parts.reduce(
@@ -327,7 +322,7 @@ export function Session() {
},
},
{
title: "Toggle sidebar",
title: sidebarVisible() ? "Hide sidebar" : "Show sidebar",
value: "session.sidebar.toggle",
keybind: "sidebar_toggle",
category: "Session",
@@ -631,6 +626,9 @@ export function Session() {
const dialog = useDialog()
const renderer = useRenderer()
// snap to bottom when session changes
createEffect(on(() => route.sessionID, toBottom))
return (
<context.Provider
value={{
@@ -879,11 +877,16 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las
return (
<>
<For each={props.parts}>
{(part) => {
{(part, index) => {
const component = createMemo(() => PART_MAPPING[part.type as keyof typeof PART_MAPPING])
return (
<Show when={component()}>
<Dynamic component={component()} part={part as any} message={props.message} />
<Dynamic
last={index() === props.parts.length - 1}
component={component()}
part={part as any}
message={props.message}
/>
</Show>
)
}}
@@ -944,27 +947,36 @@ const PART_MAPPING = {
reasoning: ReasoningPart,
}
function ReasoningPart(props: { part: ReasoningPart; message: AssistantMessage }) {
const { theme } = useTheme()
function ReasoningPart(props: { last: boolean; part: ReasoningPart; message: AssistantMessage }) {
const { theme, syntax } = useTheme()
const ctx = use()
return (
<Show when={props.part.text.trim()}>
<box
id={"text-" + props.part.id}
paddingLeft={2}
marginTop={1}
flexShrink={0}
flexDirection="row"
gap={1}
border={["left"]}
customBorderChars={SplitBorder.customBorderChars}
borderColor={theme.backgroundPanel}
borderColor={theme.backgroundElement}
>
<box paddingTop={1} paddingBottom={1} paddingLeft={2} backgroundColor={theme.backgroundPanel}>
<text fg={theme.text}>{props.part.text.trim()}</text>
</box>
<code
filetype="markdown"
drawUnstyledText={false}
streaming={true}
syntaxStyle={syntax()}
content={props.part.text.trim()}
conceal={ctx.conceal()}
fg={theme.text}
/>
</box>
</Show>
)
}
function TextPart(props: { part: TextPart; message: AssistantMessage }) {
function TextPart(props: { last: boolean; part: TextPart; message: AssistantMessage }) {
const ctx = use()
const { syntax } = useTheme()
return (
@@ -985,7 +997,7 @@ function TextPart(props: { part: TextPart; message: AssistantMessage }) {
// Pending messages moved to individual tool pending functions
function ToolPart(props: { part: ToolPart; message: AssistantMessage }) {
function ToolPart(props: { last: boolean; part: ToolPart; message: AssistantMessage }) {
const { theme } = useTheme()
const sync = useSync()
const [margin, setMargin] = createSignal(0)
@@ -97,6 +97,7 @@ export const TuiThreadCommand = cmd({
continue: args.continue,
sessionID: args.session,
agent: args.agent,
model: args.model,
prompt,
},
onExit: async () => {
+4
View File
@@ -360,6 +360,8 @@ export namespace Config {
edit: Permission.optional(),
bash: z.union([Permission, z.record(z.string(), Permission)]).optional(),
webfetch: Permission.optional(),
doom_loop: Permission.optional(),
external_directory: Permission.optional(),
})
.optional(),
})
@@ -574,6 +576,8 @@ export namespace Config {
edit: Permission.optional(),
bash: z.union([Permission, z.record(z.string(), Permission)]).optional(),
webfetch: Permission.optional(),
doom_loop: Permission.optional(),
external_directory: Permission.optional(),
})
.optional(),
tools: z.record(z.string(), z.boolean()).optional(),
+10 -1
View File
@@ -267,15 +267,24 @@ export namespace SessionCompaction {
max: maxRetries,
})
if (result.shouldRetry) {
const start = Date.now()
for (let retry = 1; retry < maxRetries; retry++) {
const lastRetryPart = result.parts.findLast((p): p is MessageV2.RetryPart => p.type === "retry")
if (lastRetryPart) {
const delayMs = SessionRetry.getRetryDelayInMs(lastRetryPart.error, retry)
const delayMs = SessionRetry.getBoundedDelay({
error: lastRetryPart.error,
attempt: retry,
startTime: start,
})
if (!delayMs) {
break
}
log.info("retrying with backoff", {
attempt: retry,
delayMs,
elapsed: Date.now() - start,
})
const stop = await SessionRetry.sleep(delayMs, signal)
+33 -14
View File
@@ -206,6 +206,8 @@ export namespace SessionPrompt {
const params = await Plugin.trigger(
"chat.params",
{
sessionID: input.sessionID,
agent: agent.name,
model: model.info,
provider: await Provider.getProvider(model.providerID),
message: userMsg,
@@ -353,15 +355,24 @@ export namespace SessionPrompt {
max: maxRetries,
})
if (result.shouldRetry) {
const start = Date.now()
for (let retry = 1; retry < maxRetries; retry++) {
const lastRetryPart = result.parts.findLast((p): p is MessageV2.RetryPart => p.type === "retry")
if (lastRetryPart) {
const delayMs = SessionRetry.getRetryDelayInMs(lastRetryPart.error, retry)
const delayMs = SessionRetry.getBoundedDelay({
error: lastRetryPart.error,
attempt: retry,
startTime: start,
})
if (!delayMs) {
break
}
log.info("retrying with backoff", {
attempt: retry,
delayMs,
elapsed: Date.now() - start,
})
const stop = await SessionRetry.sleep(delayMs, abort.signal)
@@ -882,7 +893,12 @@ export namespace SessionPrompt {
await Plugin.trigger(
"chat.message",
{},
{
sessionID: input.sessionID,
agent: input.agent,
model: input.model,
messageID: input.messageID,
},
{
message: info,
parts,
@@ -1099,18 +1115,21 @@ export namespace SessionPrompt {
JSON.stringify(p.state.input) === JSON.stringify(value.input),
)
) {
await Permission.ask({
type: "doom-loop",
pattern: value.toolName,
sessionID: assistantMsg.sessionID,
messageID: assistantMsg.id,
callID: value.toolCallId,
title: `Possible doom loop: "${value.toolName}" called ${DOOM_LOOP_THRESHOLD} times with identical arguments`,
metadata: {
tool: value.toolName,
input: value.input,
},
})
const permission = await Agent.get(input.agent).then((x) => x.permission)
if (permission.doom_loop === "ask") {
await Permission.ask({
type: "doom_loop",
pattern: value.toolName,
sessionID: assistantMsg.sessionID,
messageID: assistantMsg.id,
callID: value.toolCallId,
title: `Possible doom loop: "${value.toolName}" called ${DOOM_LOOP_THRESHOLD} times with identical arguments`,
metadata: {
tool: value.toolName,
input: value.input,
},
})
}
}
}
break
+48 -29
View File
@@ -1,8 +1,10 @@
import { iife } from "@/util/iife"
import { MessageV2 } from "./message-v2"
export namespace SessionRetry {
export const RETRY_INITIAL_DELAY = 2000
export const RETRY_BACKOFF_FACTOR = 2
export const RETRY_MAX_DELAY = 600_000 // 10 minutes
export async function sleep(ms: number, signal: AbortSignal): Promise<void> {
return new Promise((resolve, reject) => {
@@ -18,40 +20,57 @@ export namespace SessionRetry {
})
}
export function getRetryDelayInMs(error: MessageV2.APIError, attempt: number): number {
const base = RETRY_INITIAL_DELAY * Math.pow(RETRY_BACKOFF_FACTOR, attempt - 1)
const headers = error.data.responseHeaders
if (!headers) return base
export function getRetryDelayInMs(error: MessageV2.APIError, attempt: number) {
const delay = iife(() => {
const headers = error.data.responseHeaders
if (headers) {
const retryAfterMs = headers["retry-after-ms"]
if (retryAfterMs) {
const parsedMs = Number.parseFloat(retryAfterMs)
if (!Number.isNaN(parsedMs)) {
return parsedMs
}
}
const retryAfterMs = headers["retry-after-ms"]
if (retryAfterMs) {
const parsed = Number.parseFloat(retryAfterMs)
const normalized = normalizeDelay({ base, candidate: parsed })
if (normalized != null) return normalized
}
const retryAfter = headers["retry-after"]
if (retryAfter) {
const parsedSeconds = Number.parseFloat(retryAfter)
if (!Number.isNaN(parsedSeconds)) {
// convert seconds to milliseconds
return Math.ceil(parsedSeconds * 1000)
}
// Try parsing as HTTP date format
const parsed = Date.parse(retryAfter) - Date.now()
if (!Number.isNaN(parsed) && parsed > 0) {
return Math.ceil(parsed)
}
}
}
const retryAfter = headers["retry-after"]
if (!retryAfter) return base
return RETRY_INITIAL_DELAY * Math.pow(RETRY_BACKOFF_FACTOR, attempt - 1)
})
const seconds = Number.parseFloat(retryAfter)
if (!Number.isNaN(seconds)) {
const normalized = normalizeDelay({ base, candidate: seconds * 1000 })
if (normalized != null) return normalized
return base
}
// dont retry if wait is too far from now
if (delay > RETRY_MAX_DELAY) return undefined
const dateMs = Date.parse(retryAfter) - Date.now()
const normalized = normalizeDelay({ base, candidate: dateMs })
if (normalized != null) return normalized
return base
return delay
}
function normalizeDelay(input: { base: number; candidate: number }): number | undefined {
if (Number.isNaN(input.candidate)) return undefined
if (input.candidate < 0) return undefined
if (input.candidate < 60_000) return input.candidate
if (input.candidate < input.base) return input.candidate
return undefined
export function getBoundedDelay(input: {
error: MessageV2.APIError
attempt: number
startTime: number
maxDuration?: number
}) {
const elapsed = Date.now() - input.startTime
const maxDuration = input.maxDuration ?? RETRY_MAX_DELAY
const remaining = maxDuration - elapsed
if (remaining <= 0) return undefined
const delay = getRetryDelayInMs(input.error, input.attempt)
if (!delay) return undefined
return Math.min(delay, remaining)
}
}
+4 -3
View File
@@ -143,15 +143,16 @@ export namespace Snapshot {
export async function diffFull(from: string, to: string): Promise<FileDiff[]> {
const git = gitdir()
const result: FileDiff[] = []
for await (const line of $`git --git-dir=${git} diff --numstat ${from} ${to} -- .`
for await (const line of $`git --git-dir=${git} diff --no-renames --numstat ${from} ${to} -- .`
.quiet()
.cwd(Instance.directory)
.nothrow()
.lines()) {
if (!line) continue
const [additions, deletions, file] = line.split("\t")
const before = await $`git --git-dir=${git} show ${from}:${file}`.quiet().nothrow().text()
const after = await $`git --git-dir=${git} show ${to}:${file}`.quiet().nothrow().text()
const isBinaryFile = additions === "-" && deletions === "-"
const before = isBinaryFile ? "" : await $`git --git-dir=${git} show ${from}:${file}`.quiet().nothrow().text()
const after = isBinaryFile ? "" : await $`git --git-dir=${git} show ${to}:${file}`.quiet().nothrow().text()
result.push({
file,
before,
+16 -13
View File
@@ -35,24 +35,27 @@ export const EditTool = Tool.define("edit", {
throw new Error("oldString and newString must be different")
}
const agent = await Agent.get(ctx.agent)
const filePath = path.isAbsolute(params.filePath) ? params.filePath : path.join(Instance.directory, params.filePath)
if (!Filesystem.contains(Instance.directory, filePath)) {
const parentDir = path.dirname(filePath)
await Permission.ask({
type: "external-directory",
pattern: parentDir,
sessionID: ctx.sessionID,
messageID: ctx.messageID,
callID: ctx.callID,
title: `Edit file outside working directory: ${filePath}`,
metadata: {
filepath: filePath,
parentDir,
},
})
if (agent.permission.external_directory === "ask") {
await Permission.ask({
type: "external_directory",
pattern: parentDir,
sessionID: ctx.sessionID,
messageID: ctx.messageID,
callID: ctx.callID,
title: `Edit file outside working directory: ${filePath}`,
metadata: {
filepath: filePath,
parentDir,
},
})
}
}
const agent = await Agent.get(ctx.agent)
let diff = ""
let contentOld = ""
let contentNew = ""
+14 -12
View File
@@ -55,18 +55,20 @@ export const PatchTool = Tool.define("patch", {
if (!Filesystem.contains(Instance.directory, filePath)) {
const parentDir = path.dirname(filePath)
await Permission.ask({
type: "external-directory",
pattern: parentDir,
sessionID: ctx.sessionID,
messageID: ctx.messageID,
callID: ctx.callID,
title: `Patch file outside working directory: ${filePath}`,
metadata: {
filepath: filePath,
parentDir,
},
})
if (agent.permission.external_directory === "ask") {
await Permission.ask({
type: "external_directory",
pattern: parentDir,
sessionID: ctx.sessionID,
messageID: ctx.messageID,
callID: ctx.callID,
title: `Patch file outside working directory: ${filePath}`,
metadata: {
filepath: filePath,
parentDir,
},
})
}
}
switch (hunk.type) {
+16 -12
View File
@@ -10,6 +10,7 @@ import { Instance } from "../project/instance"
import { Provider } from "../provider/provider"
import { Identifier } from "../id/id"
import { Permission } from "../permission"
import { Agent } from "@/agent/agent"
const DEFAULT_READ_LIMIT = 2000
const MAX_LINE_LENGTH = 2000
@@ -27,21 +28,24 @@ export const ReadTool = Tool.define("read", {
filepath = path.join(process.cwd(), filepath)
}
const title = path.relative(Instance.worktree, filepath)
const agent = await Agent.get(ctx.agent)
if (!ctx.extra?.["bypassCwdCheck"] && !Filesystem.contains(Instance.directory, filepath)) {
const parentDir = path.dirname(filepath)
await Permission.ask({
type: "external-directory",
pattern: parentDir,
sessionID: ctx.sessionID,
messageID: ctx.messageID,
callID: ctx.callID,
title: `Access file outside working directory: ${filepath}`,
metadata: {
filepath,
parentDir,
},
})
if (agent.permission.external_directory === "ask") {
await Permission.ask({
type: "external_directory",
pattern: parentDir,
sessionID: ctx.sessionID,
messageID: ctx.messageID,
callID: ctx.callID,
title: `Access file outside working directory: ${filepath}`,
metadata: {
filepath,
parentDir,
},
})
}
}
const file = Bun.file(filepath)
+16 -13
View File
@@ -18,28 +18,31 @@ export const WriteTool = Tool.define("write", {
filePath: z.string().describe("The absolute path to the file to write (must be absolute, not relative)"),
}),
async execute(params, ctx) {
const agent = await Agent.get(ctx.agent)
const filepath = path.isAbsolute(params.filePath) ? params.filePath : path.join(Instance.directory, params.filePath)
if (!Filesystem.contains(Instance.directory, filepath)) {
const parentDir = path.dirname(filepath)
await Permission.ask({
type: "external-directory",
pattern: parentDir,
sessionID: ctx.sessionID,
messageID: ctx.messageID,
callID: ctx.callID,
title: `Write file outside working directory: ${filepath}`,
metadata: {
filepath,
parentDir,
},
})
if (agent.permission.external_directory === "ask") {
await Permission.ask({
type: "external_directory",
pattern: parentDir,
sessionID: ctx.sessionID,
messageID: ctx.messageID,
callID: ctx.callID,
title: `Write file outside working directory: ${filepath}`,
metadata: {
filepath,
parentDir,
},
})
}
}
const file = Bun.file(filepath)
const exists = await file.exists()
if (exists) await FileTime.assert(ctx.sessionID, filepath)
const agent = await Agent.get(ctx.agent)
if (agent.permission.edit === "ask")
await Permission.ask({
type: "write",
+132 -7
View File
@@ -13,8 +13,8 @@ function apiError(headers?: Record<string, string>): MessageV2.APIError {
describe("session.retry.getRetryDelayInMs", () => {
test("doubles delay on each attempt when headers missing", () => {
const error = apiError()
const delays = Array.from({ length: 7 }, (_, index) => SessionRetry.getRetryDelayInMs(error, index + 1))
expect(delays).toStrictEqual([2000, 4000, 8000, 16000, 32000, 64000, 128000])
const delays = Array.from({ length: 10 }, (_, index) => SessionRetry.getRetryDelayInMs(error, index + 1))
expect(delays).toStrictEqual([2000, 4000, 8000, 16000, 32000, 64000, 128000, 256000, 512000, undefined])
})
test("prefers retry-after-ms when shorter than exponential", () => {
@@ -27,11 +27,6 @@ describe("session.retry.getRetryDelayInMs", () => {
expect(SessionRetry.getRetryDelayInMs(error, 3)).toBe(30000)
})
test("falls back to exponential when server delay is long", () => {
const error = apiError({ "retry-after": "120" })
expect(SessionRetry.getRetryDelayInMs(error, 2)).toBe(4000)
})
test("accepts http-date retry-after values", () => {
const date = new Date(Date.now() + 20000).toUTCString()
const error = apiError({ "retry-after": date })
@@ -44,4 +39,134 @@ describe("session.retry.getRetryDelayInMs", () => {
const error = apiError({ "retry-after": "not-a-number" })
expect(SessionRetry.getRetryDelayInMs(error, 1)).toBe(2000)
})
test("ignores malformed date retry hints", () => {
const error = apiError({ "retry-after": "Invalid Date String" })
expect(SessionRetry.getRetryDelayInMs(error, 1)).toBe(2000)
})
test("ignores past date retry hints", () => {
const pastDate = new Date(Date.now() - 5000).toUTCString()
const error = apiError({ "retry-after": pastDate })
expect(SessionRetry.getRetryDelayInMs(error, 1)).toBe(2000)
})
test("returns undefined when delay exceeds 10 minutes", () => {
const error = apiError()
expect(SessionRetry.getRetryDelayInMs(error, 10)).toBeUndefined()
})
test("returns undefined when retry-after exceeds 10 minutes", () => {
const error = apiError({ "retry-after": "50" })
expect(SessionRetry.getRetryDelayInMs(error, 1)).toBe(50000)
const longError = apiError({ "retry-after-ms": "700000" })
expect(SessionRetry.getRetryDelayInMs(longError, 1)).toBeUndefined()
})
})
describe("session.retry.getBoundedDelay", () => {
test("returns full delay when under time budget", () => {
const error = apiError()
const startTime = Date.now()
const delay = SessionRetry.getBoundedDelay({
error,
attempt: 1,
startTime,
})
expect(delay).toBe(2000)
})
test("returns remaining time when delay exceeds budget", () => {
const error = apiError()
const startTime = Date.now() - 598_000 // 598 seconds elapsed, 2 seconds remaining
const delay = SessionRetry.getBoundedDelay({
error,
attempt: 1,
startTime,
})
expect(delay).toBeGreaterThanOrEqual(1900)
expect(delay).toBeLessThanOrEqual(2100)
})
test("returns undefined when time budget exhausted", () => {
const error = apiError()
const startTime = Date.now() - 600_000 // exactly 10 minutes elapsed
const delay = SessionRetry.getBoundedDelay({
error,
attempt: 1,
startTime,
})
expect(delay).toBeUndefined()
})
test("returns undefined when time budget exceeded", () => {
const error = apiError()
const startTime = Date.now() - 700_000 // 11+ minutes elapsed
const delay = SessionRetry.getBoundedDelay({
error,
attempt: 1,
startTime,
})
expect(delay).toBeUndefined()
})
test("respects custom maxDuration", () => {
const error = apiError()
const startTime = Date.now() - 58_000 // 58 seconds elapsed
const delay = SessionRetry.getBoundedDelay({
error,
attempt: 1,
startTime,
maxDuration: 60_000, // 1 minute max
})
expect(delay).toBeGreaterThanOrEqual(1900)
expect(delay).toBeLessThanOrEqual(2100)
})
test("caps exponential backoff to remaining time", () => {
const error = apiError()
const startTime = Date.now() - 595_000 // 595 seconds elapsed, 5 seconds remaining
const delay = SessionRetry.getBoundedDelay({
error,
attempt: 5, // would normally be 32 seconds
startTime,
})
expect(delay).toBeGreaterThanOrEqual(4900)
expect(delay).toBeLessThanOrEqual(5100)
})
test("respects server retry-after within budget", () => {
const error = apiError({ "retry-after": "30" })
const startTime = Date.now() - 550_000 // 550 seconds elapsed, 50 seconds remaining
const delay = SessionRetry.getBoundedDelay({
error,
attempt: 1,
startTime,
})
expect(delay).toBe(30000)
})
test("caps server retry-after to remaining time", () => {
const error = apiError({ "retry-after": "30" })
const startTime = Date.now() - 590_000 // 590 seconds elapsed, 10 seconds remaining
const delay = SessionRetry.getBoundedDelay({
error,
attempt: 1,
startTime,
})
expect(delay).toBeGreaterThanOrEqual(9900)
expect(delay).toBeLessThanOrEqual(10100)
})
test("returns undefined when getRetryDelayInMs returns undefined", () => {
const error = apiError()
const startTime = Date.now()
const delay = SessionRetry.getBoundedDelay({
error,
attempt: 10, // exceeds RETRY_MAX_DELAY
startTime,
})
expect(delay).toBeUndefined()
})
})
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/plugin",
"version": "1.0.49",
"version": "1.0.55",
"type": "module",
"scripts": {
"typecheck": "tsgo --noEmit",
+5 -2
View File
@@ -143,12 +143,15 @@ export interface Hooks {
/**
* Called when a new message is received
*/
"chat.message"?: (input: {}, output: { message: UserMessage; parts: Part[] }) => Promise<void>
"chat.message"?: (
input: { sessionID: string; agent?: string; model?: { providerID: string; modelID: string }; messageID?: string },
output: { message: UserMessage; parts: Part[] },
) => Promise<void>
/**
* Modify parameters sent to LLM
*/
"chat.params"?: (
input: { model: Model; provider: Provider; message: UserMessage },
input: { sessionID: string; agent: string; model: Model; provider: Provider; message: UserMessage },
output: { temperature: number; topP: number; options: Record<string, any> },
) => Promise<void>
"permission.ask"?: (input: Permission, output: { status: "ask" | "deny" | "allow" }) => Promise<void>
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@opencode-ai/sdk",
"version": "1.0.49",
"version": "1.0.55",
"type": "module",
"scripts": {
"typecheck": "tsgo --noEmit",
+8
View File
@@ -198,6 +198,8 @@ export type AgentConfig = {
[key: string]: "ask" | "allow" | "deny"
}
webfetch?: "ask" | "allow" | "deny"
doom_loop?: "ask" | "allow" | "deny"
external_directory?: "ask" | "allow" | "deny"
}
[key: string]:
| unknown
@@ -216,6 +218,8 @@ export type AgentConfig = {
[key: string]: "ask" | "allow" | "deny"
}
webfetch?: "ask" | "allow" | "deny"
doom_loop?: "ask" | "allow" | "deny"
external_directory?: "ask" | "allow" | "deny"
}
| undefined
}
@@ -463,6 +467,8 @@ export type Config = {
[key: string]: "ask" | "allow" | "deny"
}
webfetch?: "ask" | "allow" | "deny"
doom_loop?: "ask" | "allow" | "deny"
external_directory?: "ask" | "allow" | "deny"
}
tools?: {
[key: string]: boolean
@@ -1043,6 +1049,8 @@ export type Agent = {
[key: string]: "ask" | "allow" | "deny"
}
webfetch?: "ask" | "allow" | "deny"
doom_loop?: "ask" | "allow" | "deny"
external_directory?: "ask" | "allow" | "deny"
}
model?: {
modelID: string
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/slack",
"version": "1.0.49",
"version": "1.0.55",
"type": "module",
"scripts": {
"dev": "bun run src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/ui",
"version": "1.0.49",
"version": "1.0.55",
"type": "module",
"exports": {
".": "./src/components/index.ts",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@opencode-ai/web",
"type": "module",
"version": "1.0.49",
"version": "1.0.55",
"scripts": {
"dev": "astro dev",
"dev:remote": "VITE_API_URL=https://api.opencode.ai astro dev",
+30 -6
View File
@@ -41,12 +41,36 @@ You can also install it with the following commands:
- **Using Node.js**
<Tabs>
<TabItem label="npm">```bash npm install -g opencode-ai ```</TabItem>
<TabItem label="Bun">```bash bun install -g opencode-ai ```</TabItem>
<TabItem label="pnpm">```bash pnpm install -g opencode-ai ```</TabItem>
<TabItem label="Yarn">```bash yarn global add opencode-ai ```</TabItem>
</Tabs>
<Tabs>
<TabItem label="npm">
```bash
npm install -g opencode-ai
```
</TabItem>
<TabItem label="Bun">
```bash
bun install -g opencode-ai
```
</TabItem>
<TabItem label="pnpm">
```bash
pnpm install -g opencode-ai
```
</TabItem>
<TabItem label="Yarn">
```bash
yarn global add opencode-ai
```
</TabItem>
</Tabs>
- **Using Homebrew on macOS and Linux**
+5 -5
View File
@@ -75,6 +75,7 @@ You can also access our models through the following API endpoints.
| Kimi K2 | kimi-k2 | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Qwen3 Coder 480B | qwen3-coder | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Grok Code Fast 1 | grok-code | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
| Big Pickle | big-pickle | `https://opencode.ai/zen/v1/chat/completions` | `@ai-sdk/openai-compatible` |
The [model id](/docs/config/#models) in your OpenCode config
uses the format `opencode/<model-id>`. For example, for GPT 5 Codex, you would
@@ -117,8 +118,8 @@ We support a pay-as-you-go model. Below are the prices **per 1M tokens**.
| GLM 4.6 | $0.60 | $2.20 | $0.10 | - |
| Kimi K2 | $0.60 | $2.50 | $0.36 | - |
| Qwen3 Coder 480B | $0.45 | $1.50 | - | - |
| Grok Code Fast 1 | Free | Free | - | - |
| Code Supernova | Free | Free | - | - |
| Grok Code Fast 1 | Free | Free | Free | - |
| Big Pickle | Free | Free | Free | - |
| Claude Sonnet 4.5 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 |
| Claude Sonnet 4.5 (> 200K tokens) | $6.00 | $22.50 | $0.60 | $7.50 |
| Claude Sonnet 4 (≤ 200K tokens) | $3.00 | $15.00 | $0.30 | $3.75 |
@@ -138,7 +139,7 @@ Credit card fees are passed along at cost; we don't charge anything beyond that.
The free models:
- Grok Code Fast 1 is currently free on OpenCode for a limited time. The xAI team is using this time to collect feedback and improve Grok Code.
- Code Supernova is a stealth model that's free on OpenCode for a limited time. The team is using this time to collect feedback and improve the model.
- Big Pickle is a stealth model that's free on OpenCode for a limited time. The team is using this time to collect feedback and improve the model.
:::tip
Subscription plans and a free tier are coming soon.
@@ -153,8 +154,7 @@ Subscription plans and a free tier are coming soon.
All our models are hosted in the US. Our providers follow a zero-retention policy and do not use your data for model training, with the following exceptions:
- Grok Code Fast 1: During its free period, collected data may be used to improve Grok Code.
- Code Supernova: During its free period, collected data may be used to improve
the model.
- Big Pickle: During its free period, collected data may be used to improve the model.
- OpenAI APIs: Requests are retained for 30 days in accordance with [OpenAI's Data Policies](https://platform.openai.com/docs/guides/your-data).
- Anthropic APIs: Requests are retained for 30 days in accordance with [Anthropic's Data Policies](https://docs.anthropic.com/en/docs/claude-code/data-usage).
+1 -1
View File
@@ -21,7 +21,7 @@ if (!Script.preview) {
const commits = log
.split("\n")
.filter((line) => line && !line.match(/^\w+ (ignore:|test:|chore:)/i))
.filter((line) => line && !line.match(/^\w+ (ignore:|test:|chore:|ci:)/i))
.join("\n")
const opencode = await createOpencode()
+120
View File
@@ -0,0 +1,120 @@
#!/usr/bin/env bun
import { $ } from "bun"
import { tmpdir } from "os"
import { join } from "path"
const FORK_REPO = "sst/zed-extensions"
const UPSTREAM_REPO = "zed-industries/extensions"
const EXTENSION_NAME = "opencode"
const OPENCODE_REPO = "sst/opencode"
async function main() {
const version = process.argv[2]
if (!version) throw new Error("Version argument required: bun script/sync-zed.ts v1.0.52")
const token = process.env.GITHUB_TOKEN
if (!token) throw new Error("GITHUB_TOKEN environment variable required")
const cleanVersion = version.replace(/^v/, "")
console.log(`📦 Syncing Zed extension for version ${cleanVersion}`)
const commitSha = await $`git rev-parse ${version}`.text()
const sha = commitSha.trim()
console.log(`🔍 Found commit SHA: ${sha}`)
const extensionToml = await $`git show ${version}:packages/extensions/zed/extension.toml`.text()
const parsed = Bun.TOML.parse(extensionToml) as { version: string }
const extensionVersion = parsed.version
if (extensionVersion !== cleanVersion) {
throw new Error(`Version mismatch: extension.toml has ${extensionVersion} but tag is ${cleanVersion}`)
}
console.log(`✅ Version ${extensionVersion} matches tag`)
// Clone the fork to a temp directory
const workDir = join(tmpdir(), `zed-extensions-${Date.now()}`)
console.log(`📁 Working in ${workDir}`)
await $`git clone https://x-access-token:${token}@github.com/${FORK_REPO}.git ${workDir}`
process.chdir(workDir)
// Configure git identity
await $`git config user.name "github-actions[bot]"`
await $`git config user.email "github-actions[bot]@users.noreply.github.com"`
// Sync fork with upstream
console.log(`🔄 Syncing fork with upstream...`)
await $`git remote add upstream https://github.com/${UPSTREAM_REPO}.git`
await $`git fetch upstream`
await $`git checkout main`
await $`git merge upstream/main --ff-only`
await $`git push origin main`
console.log(`✅ Fork synced`)
// Create a new branch
const branchName = `update-${EXTENSION_NAME}-${cleanVersion}`
console.log(`🌿 Creating branch ${branchName}`)
await $`git checkout -b ${branchName}`
const submodulePath = `extensions/${EXTENSION_NAME}`
console.log(`📌 Updating submodule to commit ${sha}`)
await $`git submodule update --init ${submodulePath}`
process.chdir(submodulePath)
await $`git fetch`
await $`git checkout ${sha}`
process.chdir(workDir)
await $`git add ${submodulePath}`
console.log(`📝 Updating extensions.toml`)
const extensionsTomlPath = "extensions.toml"
const extensionsToml = await Bun.file(extensionsTomlPath).text()
const versionRegex = new RegExp(`(\\[${EXTENSION_NAME}\\][\\s\\S]*?)version = "[^"]+"`)
const updatedToml = extensionsToml.replace(versionRegex, `$1version = "${cleanVersion}"`)
if (updatedToml === extensionsToml) {
throw new Error(`Failed to update version in extensions.toml - pattern not found`)
}
await Bun.write(extensionsTomlPath, updatedToml)
await $`git add extensions.toml`
const commitMessage = `Update ${EXTENSION_NAME} to v${cleanVersion}`
await $`git commit -m ${commitMessage}`
console.log(`✅ Changes committed`)
// Delete any existing branches for opencode updates
console.log(`🔍 Checking for existing branches...`)
const branches = await $`git ls-remote --heads https://x-access-token:${token}@github.com/${FORK_REPO}.git`.text()
const branchPattern = `refs/heads/update-${EXTENSION_NAME}-`
const oldBranches = branches
.split("\n")
.filter((line) => line.includes(branchPattern))
.map((line) => line.split("refs/heads/")[1])
.filter(Boolean)
if (oldBranches.length > 0) {
console.log(`🗑️ Found ${oldBranches.length} old branch(es), deleting...`)
for (const branch of oldBranches) {
await $`git push https://x-access-token:${token}@github.com/${FORK_REPO}.git --delete ${branch}`
console.log(`✅ Deleted branch ${branch}`)
}
}
console.log(`🚀 Pushing to fork...`)
await $`git push https://x-access-token:${token}@github.com/${FORK_REPO}.git ${branchName}`
console.log(`📬 Creating pull request...`)
const prUrl =
await $`gh pr create --repo ${UPSTREAM_REPO} --base main --head ${FORK_REPO.split("/")[0]}:${branchName} --title "Update ${EXTENSION_NAME} to v${cleanVersion}" --body "Release notes:\n\nhttps://github.com/${OPENCODE_REPO}/releases/tag/v${cleanVersion}"`.text()
console.log(`✅ Pull request created: ${prUrl}`)
console.log(`🎉 Done!`)
}
main().catch((err) => {
console.error("❌ Error:", err.message)
process.exit(1)
})
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "opencode",
"displayName": "opencode",
"description": "opencode for VS Code",
"version": "1.0.49",
"version": "1.0.55",
"publisher": "sst-dev",
"repository": {
"type": "git",