Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
3995847
Apply PR #12022: feat: update tui model dialog to utilize model famil…
opencode-agent[bot] Mar 6, 2026
41de7a8
Apply PR #12633: feat(tui): add auto-accept mode for permission requests
opencode-agent[bot] Mar 6, 2026
6426e41
Apply PR #14307: fix: use parentID matching instead of ID ordering fo…
opencode-agent[bot] Mar 6, 2026
ab0025b
Apply PR #14471: [DO NOT MERGE]: beta badge for desktop app
opencode-agent[bot] Mar 6, 2026
50ee7ae
Apply PR #14677: feat: add experimental hashline edit mode with dual-…
opencode-agent[bot] Mar 6, 2026
2589831
Apply PR #15487: core: make account login upgrades safe while adding …
opencode-agent[bot] Mar 6, 2026
0c2ae82
Apply PR #15697: tweak(ui): make questions popup collapsible
opencode-agent[bot] Mar 6, 2026
f196fb4
Apply PR #15698: tweak(ui): add sidebar fade mask under new buttons
opencode-agent[bot] Mar 6, 2026
8bea7dd
Apply PR #15863: ANIMATION RETRIBUTION II: PROLAPSED OUBLIETTE
opencode-agent[bot] Mar 6, 2026
7727c29
Apply PR #15994: implement background agents
opencode-agent[bot] Mar 6, 2026
fc2f8bf
Apply PR #16069: feat(windows): add first-class pwsh/powershell support
opencode-agent[bot] Mar 6, 2026
b585ac5
Apply PR #16221: Fix review/filetree empty states
opencode-agent[bot] Mar 6, 2026
818ff4a
Apply PR #16230: feat(tui): add initial support for workspaces into t…
opencode-agent[bot] Mar 6, 2026
34ed50a
Apply PR #16286: refactor(opencode): replace Bun shell in core flows
opencode-agent[bot] Mar 6, 2026
66d26ed
Apply PR #16296: fix(app): align same() helper usage across app and util
opencode-agent[bot] Mar 6, 2026
4afd368
Apply PR #16299: feat(windows): add arm64 release targets for cli and…
opencode-agent[bot] Mar 6, 2026
e013966
chore: update nix node_modules hashes
opencode-agent[bot] Mar 6, 2026
7723159
fix(opencode): repair missing account.org_id column on startup
SergioChan Mar 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ jobs:
target: x86_64-apple-darwin
- host: macos-latest
target: aarch64-apple-darwin
- host: blacksmith-4vcpu-windows-2025
target: aarch64-pc-windows-msvc
- host: blacksmith-4vcpu-windows-2025
target: x86_64-pc-windows-msvc
- host: blacksmith-4vcpu-ubuntu-2404
Expand Down Expand Up @@ -212,6 +214,27 @@ jobs:
opencode-app-id: ${{ vars.OPENCODE_APP_ID }}
opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }}

- name: Setup Windows ARM64 clang
if: runner.os == 'Windows' && matrix.settings.target == 'aarch64-pc-windows-msvc'
shell: pwsh
run: |
$vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe"
if (!(Test-Path $vswhere)) { throw "vswhere.exe not found at $vswhere" }
$root = & $vswhere -latest -products * -property installationPath
if (!$root) { throw "Visual Studio installation not found" }
$llvm = Join-Path $root "VC\Tools\Llvm"
$bin = @(
(Join-Path $llvm "x64\bin"),
(Join-Path $llvm "bin")
) | Where-Object { Test-Path (Join-Path $_ "clang.exe") } | Select-Object -First 1
if (!$bin -and (Test-Path $llvm)) {
$bin = Get-ChildItem -Path $llvm -Filter clang.exe -Recurse -File | Select-Object -First 1 | ForEach-Object { $_.DirectoryName }
}
if (!$bin) { throw "clang.exe not found under $llvm" }
$env:PATH = "$bin;$env:PATH"
Add-Content -Path $env:GITHUB_PATH -Value $bin
clang --version

- name: Build and upload artifacts
uses: tauri-apps/tauri-action@390cbe447412ced1303d35abe75287949e43437a
timeout-minutes: 60
Expand Down Expand Up @@ -254,6 +277,9 @@ jobs:
- host: macos-latest
target: aarch64-apple-darwin
platform_flag: --mac --arm64
- host: "blacksmith-4vcpu-windows-2025"
target: aarch64-pc-windows-msvc
platform_flag: --win --arm64
- host: "blacksmith-4vcpu-windows-2025"
target: x86_64-pc-windows-msvc
platform_flag: --win
Expand Down
4 changes: 4 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,3 +122,7 @@ const table = sqliteTable("session", {
- Avoid mocks as much as possible
- Test actual implementation, do not duplicate logic into tests
- Tests cannot run from repo root (guard: `do-not-run-tests-from-root`); run from package dirs like `packages/opencode`.

## Type Checking

- Always run `bun typecheck` from package directories (e.g., `packages/opencode`), never `tsc` directly.
6 changes: 5 additions & 1 deletion bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions nix/hashes.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"nodeModules": {
"x86_64-linux": "sha256-pBTIT8Pgdm3272YhBjiAZsmj0SSpHTklh6lGc8YcMoE=",
"aarch64-linux": "sha256-prt039++d5UZgtldAN6+RVOR557ifIeusiy5XpzN8QU=",
"aarch64-darwin": "sha256-Y3f+cXcIGLqz6oyc5fG22t6CLD4wGkvwqO6RNXjFriQ=",
"x86_64-darwin": "sha256-BjbBBhQUgGhrlP56skABcrObvutNUZSWnrnPCg1OTKE="
"x86_64-linux": "sha256-CdgMDfqrB9R/mSzmpEFUIN6ZC4ePvwHtt+2gcblQ4PA=",
"aarch64-linux": "sha256-gy/nTLhvo15U+7xLtanj43FE6eDh9gMpBhaiqvroiwY=",
"aarch64-darwin": "sha256-DxXzKnYFVR4kYXwNKVsLztC0F/0lEhoCK7WQ55WTsZ0=",
"x86_64-darwin": "sha256-/b3O2eSjpbpq5HZRuYZveuJHbklf1tUtSecc+afWc6Y="
}
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@
"protobufjs",
"tree-sitter",
"tree-sitter-bash",
"tree-sitter-powershell",
"web-tree-sitter",
"electron"
],
Expand Down
9 changes: 5 additions & 4 deletions packages/app/e2e/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
sessionItemSelector,
dropdownMenuTriggerSelector,
dropdownMenuContentSelector,
sessionHeaderSelector,
projectMenuTriggerSelector,
projectWorkspacesToggleSelector,
titlebarRightSelector,
Expand Down Expand Up @@ -229,9 +230,9 @@ export async function hoverSessionItem(page: Page, sessionID: string) {
export async function openSessionMoreMenu(page: Page, sessionID: string) {
await expect(page).toHaveURL(new RegExp(`/session/${sessionID}(?:[/?#]|$)`))

const scroller = page.locator(".scroll-view__viewport").first()
await expect(scroller).toBeVisible()
await expect(scroller.getByRole("heading", { level: 1 }).first()).toBeVisible({ timeout: 30_000 })
const header = page.locator(sessionHeaderSelector).first()
await expect(header).toBeVisible()
await expect(header.getByRole("heading", { level: 1 }).first()).toBeVisible({ timeout: 30_000 })

const menu = page
.locator(dropdownMenuContentSelector)
Expand All @@ -247,7 +248,7 @@ export async function openSessionMoreMenu(page: Page, sessionID: string) {

if (opened) return menu

const menuTrigger = scroller.getByRole("button", { name: /more options/i }).first()
const menuTrigger = header.getByRole("button", { name: /more options/i }).first()
await expect(menuTrigger).toBeVisible()
await menuTrigger.click()

Expand Down
2 changes: 2 additions & 0 deletions packages/app/e2e/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ export const dropdownMenuContentSelector = '[data-component="dropdown-menu-conte

export const inlineInputSelector = '[data-component="inline-input"]'

export const sessionHeaderSelector = "[data-session-title]"

export const sessionItemSelector = (sessionID: string) => `${sidebarNavSelector} [data-session-id="${sessionID}"]`

export const workspaceItemSelector = (slug: string) =>
Expand Down
4 changes: 2 additions & 2 deletions packages/app/e2e/session/session.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
openSharePopover,
withSession,
} from "../actions"
import { sessionItemSelector, inlineInputSelector } from "../selectors"
import { sessionHeaderSelector, sessionItemSelector, inlineInputSelector } from "../selectors"

const shareDisabled = process.env.OPENCODE_DISABLE_SHARE === "true" || process.env.OPENCODE_DISABLE_SHARE === "1"

Expand Down Expand Up @@ -44,7 +44,7 @@ test("session can be renamed via header menu", async ({ page, sdk, gotoSession }
const menu = await openSessionMoreMenu(page, session.id)
await clickMenuItem(menu, /rename/i)

const input = page.locator(".scroll-view__viewport").locator(inlineInputSelector).first()
const input = page.locator(sessionHeaderSelector).locator(inlineInputSelector).first()
await expect(input).toBeVisible()
await expect(input).toBeFocused()
await input.fill(renamedTitle)
Expand Down
68 changes: 19 additions & 49 deletions packages/app/src/components/prompt-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,17 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
applyingHistory: false,
})

const buttonsSpring = useSpring(() => (store.mode === "normal" ? 1 : 0), { visualDuration: 0.2, bounce: 0 })
const buttonsSpring = useSpring(
() => (store.mode === "normal" ? 1 : 0),
{ visualDuration: 0.2, bounce: 0 },
)

const springFade = (t: number): Record<string, string> => ({
opacity: `${t}`,
transform: `scale(${0.95 + t * 0.05})`,
filter: `blur(${(1 - t) * 2}px)`,
"pointer-events": t > 0.5 ? "auto" : "none",
})

const commentCount = createMemo(() => {
if (store.mode === "shell") return 0
Expand Down Expand Up @@ -1246,9 +1256,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
<div
aria-hidden={store.mode !== "normal"}
class="flex items-center gap-1"
style={{
"pointer-events": buttonsSpring() > 0.5 ? "auto" : "none",
}}
style={{ "pointer-events": buttonsSpring() > 0.5 ? "auto" : "none" }}
>
<TooltipKeybind
placement="top"
Expand All @@ -1260,11 +1268,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
type="button"
variant="ghost"
class="size-8 p-0"
style={{
opacity: buttonsSpring(),
transform: `scale(${0.95 + buttonsSpring() * 0.05})`,
filter: `blur(${(1 - buttonsSpring()) * 2}px)`,
}}
style={springFade(buttonsSpring())}
onClick={pick}
disabled={store.mode !== "normal"}
tabIndex={store.mode === "normal" ? undefined : -1}
Expand Down Expand Up @@ -1302,11 +1306,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
icon={working() ? "stop" : "arrow-up"}
variant="primary"
class="size-8"
style={{
opacity: buttonsSpring(),
transform: `scale(${0.95 + buttonsSpring() * 0.05})`,
filter: `blur(${(1 - buttonsSpring()) * 2}px)`,
}}
style={springFade(buttonsSpring())}
aria-label={working() ? language.t("prompt.action.stop") : language.t("prompt.action.send")}
/>
</Tooltip>
Expand Down Expand Up @@ -1362,13 +1362,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
<div class="flex items-center gap-1.5 min-w-0 flex-1 relative">
<div
class="h-7 flex items-center gap-1.5 max-w-[160px] min-w-0 absolute inset-y-0 left-0"
style={{
padding: "0 4px 0 8px",
opacity: 1 - buttonsSpring(),
transform: `scale(${0.95 + (1 - buttonsSpring()) * 0.05})`,
filter: `blur(${buttonsSpring() * 2}px)`,
"pointer-events": buttonsSpring() < 0.5 ? "auto" : "none",
}}
style={{ padding: "0 4px 0 8px", ...springFade(1 - buttonsSpring()) }}
>
<span class="truncate text-13-medium text-text-strong">{language.t("prompt.mode.shell")}</span>
<div class="size-4 shrink-0" />
Expand All @@ -1387,13 +1381,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
onSelect={local.agent.set}
class="capitalize max-w-[160px]"
valueClass="truncate text-13-regular"
triggerStyle={{
height: "28px",
opacity: buttonsSpring(),
transform: `scale(${0.95 + buttonsSpring() * 0.05})`,
filter: `blur(${(1 - buttonsSpring()) * 2}px)`,
"pointer-events": buttonsSpring() > 0.5 ? "auto" : "none",
}}
triggerStyle={{ height: "28px", ...springFade(buttonsSpring()) }}
variant="ghost"
/>
</TooltipKeybind>
Expand All @@ -1411,13 +1399,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
variant="ghost"
size="normal"
class="min-w-0 max-w-[320px] text-13-regular group"
style={{
height: "28px",
opacity: buttonsSpring(),
transform: `scale(${0.95 + buttonsSpring() * 0.05})`,
filter: `blur(${(1 - buttonsSpring()) * 2}px)`,
"pointer-events": buttonsSpring() > 0.5 ? "auto" : "none",
}}
style={{ height: "28px", ...springFade(buttonsSpring()) }}
onClick={() => dialog.show(() => <DialogSelectModelUnpaid />)}
>
<Show when={local.model.current()?.provider?.id}>
Expand Down Expand Up @@ -1446,13 +1428,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
triggerProps={{
variant: "ghost",
size: "normal",
style: {
height: "28px",
opacity: buttonsSpring(),
transform: `scale(${0.95 + buttonsSpring() * 0.05})`,
filter: `blur(${(1 - buttonsSpring()) * 2}px)`,
"pointer-events": buttonsSpring() > 0.5 ? "auto" : "none",
},
style: { height: "28px", ...springFade(buttonsSpring()) },
class: "min-w-0 max-w-[320px] text-13-regular group",
}}
>
Expand Down Expand Up @@ -1484,13 +1460,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
onSelect={(x) => local.model.variant.set(x === "default" ? undefined : x)}
class="capitalize max-w-[160px]"
valueClass="truncate text-13-regular"
triggerStyle={{
height: "28px",
opacity: buttonsSpring(),
transform: `scale(${0.95 + buttonsSpring() * 0.05})`,
filter: `blur(${(1 - buttonsSpring()) * 2}px)`,
"pointer-events": buttonsSpring() > 0.5 ? "auto" : "none",
}}
triggerStyle={{ height: "28px", ...springFade(buttonsSpring()) }}
variant="ghost"
/>
</TooltipKeybind>
Expand Down
31 changes: 15 additions & 16 deletions packages/app/src/components/prompt-input/submit.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import type { Message } from "@opencode-ai/sdk/v2/client"
import { showToast } from "@opencode-ai/ui/toast"
import { base64Encode } from "@opencode-ai/util/encode"
import { errorMessage } from "@/pages/layout/helpers"
import { useNavigate, useParams } from "@solidjs/router"
import type { Accessor } from "solid-js"
import { batch, type Accessor } from "solid-js"
import type { FileSelection } from "@/context/file"
import { useGlobalSync } from "@/context/global-sync"
import { useLanguage } from "@/context/language"
Expand Down Expand Up @@ -65,14 +66,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
const language = useLanguage()
const params = useParams()

const errorMessage = (err: unknown) => {
if (err && typeof err === "object" && "data" in err) {
const data = (err as { data?: { message?: string } }).data
if (data?.message) return data.message
}
if (err instanceof Error) return err.message
return language.t("common.requestFailed")
}
const toastError = (err: unknown) => errorMessage(err, language.t("common.requestFailed"))

const abort = async () => {
const sessionID = params.id
Expand Down Expand Up @@ -158,7 +152,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
.catch((err) => {
showToast({
title: language.t("prompt.toast.worktreeCreateFailed.title"),
description: errorMessage(err),
description: toastError(err),
})
return undefined
})
Expand Down Expand Up @@ -197,7 +191,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
.catch((err) => {
showToast({
title: language.t("prompt.toast.sessionCreateFailed.title"),
description: errorMessage(err),
description: toastError(err),
})
return undefined
})
Expand Down Expand Up @@ -255,7 +249,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
.catch((err) => {
showToast({
title: language.t("prompt.toast.shellSendFailed.title"),
description: errorMessage(err),
description: toastError(err),
})
restoreInput()
})
Expand Down Expand Up @@ -333,9 +327,14 @@ export function createPromptSubmit(input: PromptSubmitInput) {
messageID,
})

removeCommentItems(commentItems)
clearInput()
addOptimisticMessage()
batch(() => {
removeCommentItems(commentItems)
clearInput()
if (sessionDirectory === projectDirectory) {
sync.set("session_status", session.id, { type: "busy" })
}
addOptimisticMessage()
})

const waitForWorktree = async () => {
const worktree = WorktreeState.get(sessionDirectory)
Expand Down Expand Up @@ -412,7 +411,7 @@ export function createPromptSubmit(input: PromptSubmitInput) {
}
showToast({
title: language.t("prompt.toast.promptSendFailed.title"),
description: errorMessage(err),
description: toastError(err),
})
removeOptimisticMessage()
restoreCommentItems(commentItems)
Expand Down
Loading
Loading