diff --git a/.github/actions/setup-bun/action.yml b/.github/actions/setup-bun/action.yml
index 9d29724d86a4..ad3fd6daf873 100644
--- a/.github/actions/setup-bun/action.yml
+++ b/.github/actions/setup-bun/action.yml
@@ -58,10 +58,18 @@ runs:
# Workaround for patched peer variants
# e.g. ./patches/ for standard-openapi
# https://github.com/oven-sh/bun/issues/28147
- if [ "$RUNNER_OS" = "Windows" ]; then
- bun install --linker hoisted ${{ inputs.install-flags }}
- else
- bun install ${{ inputs.install-flags }}
+ install_dependencies() {
+ if [ "$RUNNER_OS" = "Windows" ]; then
+ bun install --linker hoisted ${{ inputs.install-flags }}
+ else
+ bun install ${{ inputs.install-flags }}
+ fi
+ }
+
+ if ! install_dependencies; then
+ echo "Bun install failed; clearing the runner-local Bun cache and retrying"
+ rm -rf "$(bun pm cache)"
+ install_dependencies
fi
shell: bash
diff --git a/.github/workflows/publish-fork-release.yml b/.github/workflows/publish-fork-release.yml
new file mode 100644
index 000000000000..553e006c0065
--- /dev/null
+++ b/.github/workflows/publish-fork-release.yml
@@ -0,0 +1,70 @@
+name: publish-fork-release
+
+on:
+ workflow_dispatch:
+ inputs:
+ version:
+ description: Release version without the v prefix
+ required: true
+ default: 1.18.23
+ source_run_id:
+ description: Actions run ID containing the completed desktop artifacts
+ required: true
+ default: "33138463284"
+
+permissions:
+ actions: read
+ contents: write
+
+jobs:
+ publish:
+ runs-on: ubuntu-24.04
+ env:
+ OPENCODE_VERSION: ${{ inputs.version }}
+ SOURCE_RUN_ID: ${{ inputs.source_run_id }}
+ steps:
+ - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
+ with:
+ pattern: desktop-*
+ path: /tmp/desktop
+ merge-multiple: true
+ run-id: ${{ inputs.source_run_id }}
+ github-token: ${{ github.token }}
+
+ - name: Upload desktop installers and publish release
+ env:
+ GH_TOKEN: ${{ github.token }}
+ run: |
+ set -euo pipefail
+ if [[ ! "$SOURCE_RUN_ID" =~ ^[0-9]+$ ]]; then
+ echo "source_run_id must be numeric"
+ exit 1
+ fi
+ if [[ "$(gh release view "v${OPENCODE_VERSION}" --json isDraft --jq .isDraft --repo "${GITHUB_REPOSITORY}")" != "true" ]]; then
+ echo "Release v${OPENCODE_VERSION} is missing or already published"
+ exit 1
+ fi
+ mapfile -d '' files < <(find /tmp/desktop -mindepth 1 -maxdepth 1 -type f \( \
+ -name 'opencode-desktop-*.AppImage' -o \
+ -name 'opencode-desktop-*.blockmap' -o \
+ -name 'opencode-desktop-*.deb' -o \
+ -name 'opencode-desktop-*.dmg' -o \
+ -name 'opencode-desktop-*.exe' -o \
+ -name 'opencode-desktop-*.rpm' -o \
+ -name 'opencode-desktop-*.tar.gz' -o \
+ -name 'opencode-desktop-*.zip' -o \
+ -name 'latest.json' -o \
+ -name 'latest*.yml' \
+ \) -print0)
+ if (( ${#files[@]} == 0 )); then
+ echo "No desktop release assets found"
+ exit 1
+ fi
+ printf 'Uploading %s\n' "${files[@]}"
+ gh release upload "v${OPENCODE_VERSION}" "${files[@]}" \
+ --clobber \
+ --repo "${GITHUB_REPOSITORY}"
+ gh release edit "v${OPENCODE_VERSION}" \
+ --draft=false \
+ --latest \
+ --repo "${GITHUB_REPOSITORY}"
diff --git a/.github/workflows/release-fork.yml b/.github/workflows/release-fork.yml
new file mode 100644
index 000000000000..9e566968170d
--- /dev/null
+++ b/.github/workflows/release-fork.yml
@@ -0,0 +1,180 @@
+name: release-fork
+
+on:
+ workflow_dispatch:
+ inputs:
+ version:
+ description: "Release version without the v prefix"
+ required: true
+ default: "1.18.23"
+ type: string
+
+permissions:
+ contents: write
+
+concurrency:
+ group: release-fork-${{ inputs.version }}
+ cancel-in-progress: false
+
+env:
+ OPENCODE_VERSION: ${{ inputs.version }}
+ OPENCODE_CHANNEL: prod
+
+jobs:
+ create-release:
+ runs-on: ubuntu-24.04
+ steps:
+ - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v4.3.0
+ with:
+ fetch-depth: 0
+
+ - name: Create draft release
+ env:
+ GH_TOKEN: ${{ github.token }}
+ run: |
+ set -euo pipefail
+ if gh release view "v${OPENCODE_VERSION}" --repo "${GITHUB_REPOSITORY}" >/dev/null 2>&1; then
+ if [ "$(gh release view "v${OPENCODE_VERSION}" --repo "${GITHUB_REPOSITORY}" --json isDraft --jq .isDraft)" != "true" ]; then
+ echo "Published release v${OPENCODE_VERSION} already exists"
+ exit 1
+ fi
+ echo "Reusing existing draft release v${OPENCODE_VERSION}"
+ exit 0
+ fi
+ gh release create "v${OPENCODE_VERSION}" \
+ --draft \
+ --generate-notes \
+ --target "${GITHUB_SHA}" \
+ --title "v${OPENCODE_VERSION}" \
+ --repo "${GITHUB_REPOSITORY}"
+
+ build-cli:
+ needs: create-release
+ runs-on: ubuntu-24.04
+ steps:
+ - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v4.3.0
+
+ - uses: ./.github/actions/setup-bun
+
+ - name: Build CLI archives
+ env:
+ GH_REPO: ${{ github.repository }}
+ GH_TOKEN: ${{ github.token }}
+ OPENCODE_RELEASE: "true"
+ run: |
+ ./packages/opencode/script/build.ts
+ ./packages/cli/script/build.ts
+
+ build-desktop:
+ needs: create-release
+ strategy:
+ fail-fast: false
+ matrix:
+ settings:
+ - host: macos-26-intel
+ target: x86_64-apple-darwin
+ platform_flag: --mac --x64
+ bun_install_flags: --os=darwin --cpu=x64
+ - host: macos-26
+ target: aarch64-apple-darwin
+ platform_flag: --mac --arm64
+ bun_install_flags: --os=darwin --cpu=arm64
+ - host: windows-2025
+ target: aarch64-pc-windows-msvc
+ platform_flag: --win --arm64
+ - host: windows-2025
+ target: x86_64-pc-windows-msvc
+ platform_flag: --win
+ bun_install_flags: --os=win32 --cpu=x64
+ - host: ubuntu-24.04
+ target: x86_64-unknown-linux-gnu
+ platform_flag: --linux
+ - host: ubuntu-24.04-arm
+ target: aarch64-unknown-linux-gnu
+ platform_flag: --linux --arm64
+ runs-on: ${{ matrix.settings.host }}
+ steps:
+ - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v4.3.0
+
+ - uses: ./.github/actions/setup-bun
+ with:
+ install-flags: ${{ matrix.settings.bun_install_flags }}
+
+ - name: Install Linux packaging tools
+ if: runner.os == 'Linux'
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y --no-install-recommends rpm
+
+ - name: Prepare desktop build
+ working-directory: packages/desktop
+ run: bun ./scripts/prepare.ts
+ env:
+ OPENCODE_VERSION: ${{ env.OPENCODE_VERSION }}
+ OPENCODE_CHANNEL: prod
+ RUST_TARGET: ${{ matrix.settings.target }}
+
+ - name: Build desktop application
+ working-directory: packages/desktop
+ run: bun run build
+ env:
+ NODE_OPTIONS: --max-old-space-size=4096
+ OPENCODE_VERSION: ${{ env.OPENCODE_VERSION }}
+ OPENCODE_CHANNEL: prod
+ RUST_TARGET: ${{ matrix.settings.target }}
+ OPENCODE_UNSIGNED_BUILD: "true"
+
+ - name: Package desktop installer
+ working-directory: packages/desktop
+ run: npx electron-builder ${{ matrix.settings.platform_flag }} --publish never --config electron-builder.config.ts
+ env:
+ GITHUB_ACTIONS: "false"
+ CSC_IDENTITY_AUTO_DISCOVERY: "false"
+ OPENCODE_CHANNEL: prod
+ OPENCODE_UNSIGNED_BUILD: "true"
+
+ - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
+ with:
+ name: desktop-${{ matrix.settings.target }}
+ path: packages/desktop/dist/*
+
+ publish-release:
+ needs:
+ - build-cli
+ - build-desktop
+ runs-on: ubuntu-24.04
+ steps:
+ - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
+ with:
+ pattern: desktop-*
+ path: /tmp/desktop
+ merge-multiple: true
+
+ - name: Upload desktop installers and publish release
+ env:
+ GH_TOKEN: ${{ github.token }}
+ run: |
+ set -euo pipefail
+ mapfile -d '' files < <(find /tmp/desktop -mindepth 1 -maxdepth 1 -type f \( \
+ -name 'opencode-desktop-*.AppImage' -o \
+ -name 'opencode-desktop-*.blockmap' -o \
+ -name 'opencode-desktop-*.deb' -o \
+ -name 'opencode-desktop-*.dmg' -o \
+ -name 'opencode-desktop-*.exe' -o \
+ -name 'opencode-desktop-*.rpm' -o \
+ -name 'opencode-desktop-*.tar.gz' -o \
+ -name 'opencode-desktop-*.zip' -o \
+ -name 'latest.json' -o \
+ -name 'latest*.yml' \
+ \) -print0)
+ if (( ${#files[@]} == 0 )); then
+ echo "No desktop release assets found"
+ exit 1
+ fi
+ gh release upload "v${OPENCODE_VERSION}" "${files[@]}" \
+ --clobber \
+ --repo "${GITHUB_REPOSITORY}"
+ gh release edit "v${OPENCODE_VERSION}" \
+ --draft=false \
+ --latest \
+ --repo "${GITHUB_REPOSITORY}"
diff --git a/packages/desktop/electron-builder.config.ts b/packages/desktop/electron-builder.config.ts
index 508c0df5e914..cac1ba2987f9 100644
--- a/packages/desktop/electron-builder.config.ts
+++ b/packages/desktop/electron-builder.config.ts
@@ -9,6 +9,7 @@ const execFileAsync = promisify(execFile)
const packageDir = path.dirname(fileURLToPath(import.meta.url))
const rootDir = path.resolve(packageDir, "../..")
const signScript = path.join(rootDir, "script", "sign-windows.ps1")
+const unsignedBuild = process.env.OPENCODE_UNSIGNED_BUILD === "true"
// The Electron 42 packaging update briefly installed Linux launchers/icons under
// "opencode-desktop". Keep that hidden desktop entry around so existing GNOME/KDE
// pins still resolve after the canonical app id changes back to ai.opencode.desktop.
@@ -19,6 +20,7 @@ const metainfoFpm = (appId: string) =>
`${path.join(packageDir, "resources", `${appId}.metainfo.xml`)}=/usr/share/metainfo/${appId}.metainfo.xml`
async function signWindows(configuration: { path: string }) {
+ if (unsignedBuild) return
if (process.platform !== "win32") return
if (process.env.GITHUB_ACTIONS !== "true") return
@@ -75,15 +77,15 @@ const getBase = (appId: string): Configuration => ({
mac: {
category: "public.app-category.developer-tools",
icon: `resources/icons/icon.icns`,
- hardenedRuntime: true,
+ hardenedRuntime: !unsignedBuild,
gatekeeperAssess: false,
entitlements: "resources/entitlements.plist",
entitlementsInherit: "resources/entitlements.plist",
- notarize: true,
+ notarize: !unsignedBuild,
target: ["dmg", "zip"],
},
dmg: {
- sign: true,
+ sign: !unsignedBuild,
},
protocols: {
name: "OpenCode",
diff --git a/packages/tui/src/component/prompt/index.tsx b/packages/tui/src/component/prompt/index.tsx
index fe7f4a22f75f..5443cd88c587 100644
--- a/packages/tui/src/component/prompt/index.tsx
+++ b/packages/tui/src/component/prompt/index.tsx
@@ -165,6 +165,7 @@ export function Prompt(props: PromptProps) {
const stash = usePromptStash()
const keymap = useOpencodeKeymap()
const agentShortcut = useCommandShortcut("agent.cycle")
+ const variantShortcut = useCommandShortcut("variant.cycle")
const paletteShortcut = useCommandShortcut("command.palette.show")
const renderer = useRenderer()
const exit = useExit()
@@ -1675,6 +1676,11 @@ export function Prompt(props: PromptProps) {
+ 0 && variantShortcut()}>
+
+ {variantShortcut()} thinking
+
+
{paletteShortcut()} commands