Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
c2e39bb
test(opencode): use native config path in permission assertion (#45849)
opencode-agent[bot] Aug 28, 2026
755ebdb
sync release versions for v1.18.25
Aug 28, 2026
1be9fd5
docs(go): add Hy4 preview (#45904)
MrMushrooooom Aug 28, 2026
df35e84
docs(zen): add Ling 3.0 Flash Fin Free (#45923)
MrMushrooooom Aug 28, 2026
62a2f0e
feat(console): animate Go usage allowances and bonuses (#46055)
kitlangton Aug 29, 2026
dc4449d
chore: update nix node_modules hashes
opencode-agent[bot] Aug 29, 2026
be53e17
docs(go): end Hy3 usage promotion (#46213)
MrMushrooooom Aug 30, 2026
10765ff
fix: remove Hy3 Free docs and correct Go chart rendering (#46221)
MrMushrooooom Aug 30, 2026
9f69463
fix(app): backport session rename and tab menu fixes to v1 (#46116)
opencode-agent[bot] Aug 31, 2026
26ff3ed
fix(tui): keep home shortcuts right-aligned (#36906)
opencode-agent[bot] Aug 31, 2026
b639de0
fix(stats): merge deepseek flash variants (#46446)
adamdotdevin Aug 31, 2026
0428492
chore: generate
opencode-agent[bot] Aug 31, 2026
ba79057
docs on proper usage of OpenCode Go
thdxr Sep 1, 2026
2386fce
chore: generate
opencode-agent[bot] Sep 1, 2026
5c5c709
fix(tui): pin diff highlights query (#46519)
opencode-agent[bot] Sep 1, 2026
f7da00f
fix(opencode): omit empty apply patch move path (#45329)
altendky Sep 1, 2026
be3b703
fix(web): restore documentation list markers
thdxr Sep 1, 2026
1ead9e3
fix(web): number Go usage requirements
thdxr Sep 1, 2026
ebece6e
docs(web): update Qwen3.7 Max Go usage (#46555)
MrMushrooooom Sep 1, 2026
5341a5e
feat(console): add workspace migration timestamp (#46627)
vimtor Sep 1, 2026
1ce281b
fix(stats): prevent comparison legend collapse
adamdotdevin Sep 1, 2026
df6aecd
chore: generate
opencode-agent[bot] Sep 1, 2026
216ba8f
fix(opencode): stop Azure model discovery from logging to stdout (#46…
rekram1-node Sep 1, 2026
2da5a4b
refactor(tui): use OpenTUI Dynamic in session view (#46649)
rekram1-node Sep 1, 2026
55c54d1
chore: use native runtime conditions in development (#46644)
rekram1-node Sep 1, 2026
4502ee5
fix(core): bump @ai-sdk/amazon-bedrock to 4.0.166 for reasoning and r…
pengzh1 Sep 1, 2026
dc8753f
chore: update nix node_modules hashes
opencode-agent[bot] Sep 1, 2026
3f39a32
feat(opencode): tolerate Anthropic thinking block binding (#46653)
rekram1-node Sep 1, 2026
2961956
chore: update nix node_modules hashes
opencode-agent[bot] Sep 1, 2026
af1f9e6
remove azure discovery stuff (#46666)
neriousy Sep 1, 2026
1542195
fix(opencode): allow none reasoning effort in Bedrock SDK (#46671)
rekram1-node Sep 1, 2026
c066339
test(opencode): guard patched dependency versions (#46673)
rekram1-node Sep 1, 2026
765ae64
fix(core): Fix for incorrect time.start reset in tool call logging (#…
bartlettroscoe Sep 1, 2026
c10d676
chore: update nix node_modules hashes
opencode-agent[bot] Sep 1, 2026
86387e9
test(opencode): fix session tools test typecheck and runtime (#46677)
rekram1-node Sep 1, 2026
8100c68
fix(app): bump happy-dom to fix GC-dependent MutationObserver flake (…
rekram1-node Sep 1, 2026
774cc7c
release: v1.18.26
Sep 1, 2026
e820b29
chore(termux): seed release automation
github-actions[bot] Sep 2, 2026
514ee91
Termux v1.18.26 (#66)
unemployabot[bot] Sep 2, 2026
fe32eb3
Merge commit '514ee91b1d593f3460d0d66f0f28a15a1825400b' into checkpoi…
github-actions[bot] Sep 2, 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
2 changes: 1 addition & 1 deletion .termux-release-automation
Submodule .termux-release-automation updated from e85e24 to 514ee9
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ Caveats:
- If `spawn` does not work for you, you can debug the server separately:
- Debug server: `bun run --inspect=ws://localhost:6499/ --cwd packages/opencode ./src/index.ts serve --port 4096`,
then attach TUI with `opencode attach http://localhost:4096`
- Debug TUI: `bun run --inspect=ws://localhost:6499/ --cwd packages/opencode --conditions=browser ./src/index.ts`
- Debug TUI: `bun run --inspect=ws://localhost:6499/ --cwd packages/opencode ./src/index.ts`

Other tips and tricks:

Expand Down
110 changes: 54 additions & 56 deletions bun.lock

Large diffs are not rendered by default.

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-fJ72uEK9rSoFL6eJk0Lwkc2TIMLZyQ7Iz83WrZE2duA=",
"aarch64-linux": "sha256-ElEwz5spFa8XFYSBiGjKlTKRFQCju/ZYDlb6h1FaKoI=",
"aarch64-darwin": "sha256-RmbrAlggOqxNFdhW+qj2tjRCpRf2NDLe68TikbGtCeA=",
"x86_64-darwin": "sha256-ZgYE0J+Dkz/kALK3kZ1jdFIZ5/BEkEaw0mXCqPon0iY="
"x86_64-linux": "sha256-xxfesqajP1GI/XdzCIM8hHHOdhWBrjfKkQxDpC2qRKE=",
"aarch64-linux": "sha256-T9HxvCNwt7St6VQsEqLMCSVu7jgTnQjdo824LRzi+t4=",
"aarch64-darwin": "sha256-vo0ALiy0tGnp8YVqNwGS8lsXKUmvkRiKPUacbQhZMHs=",
"x86_64-darwin": "sha256-cToGEEFCm4HX4xLfVNYEwugoUhCKVXAbH+uSI2cVo4w="
}
}
8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"type": "module",
"packageManager": "bun@1.3.14",
"scripts": {
"dev": "bun run --cwd packages/opencode --conditions=browser src/index.ts",
"dev": "bun run --cwd packages/opencode src/index.ts",
"dev:desktop": "bun --cwd packages/desktop dev",
"dev:web": "bun --cwd packages/app dev",
"dev:console": "ulimit -n 10240 2>/dev/null; bun run --cwd packages/console/app dev",
Expand Down Expand Up @@ -137,6 +137,7 @@
"electron"
],
"overrides": {
"@ai-sdk/anthropic": "3.0.111",
"@opentui/core": "catalog:",
"@opentui/keymap": "catalog:",
"@opentui/solid": "catalog:",
Expand All @@ -145,7 +146,6 @@
},
"patchedDependencies": {
"@dnd-kit/dom@0.5.0": "patches/@dnd-kit%2Fdom@0.5.0.patch",
"@ff-labs/fff-bun@0.9.3": "patches/@ff-labs%2Ffff-bun@0.9.3.patch",
"@npmcli/agent@4.0.2": "patches/@npmcli%2Fagent@4.0.2.patch",
"@silvia-odwyer/photon-node@0.3.4": "patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch",
"@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch",
Expand All @@ -160,6 +160,8 @@
"effect@4.0.0-beta.83": "patches/effect@4.0.0-beta.83.patch",
"@tanstack/virtual-core@3.17.3": "patches/@tanstack%2Fvirtual-core@3.17.3.patch",
"@ai-sdk/openai-compatible@2.0.41": "patches/@ai-sdk%2Fopenai-compatible@2.0.41.patch",
"@ai-sdk/groq@3.0.31": "patches/@ai-sdk%2Fgroq@3.0.31.patch"
"@ai-sdk/groq@3.0.31": "patches/@ai-sdk%2Fgroq@3.0.31.patch",
"@ai-sdk/anthropic@3.0.111": "patches/@ai-sdk%2Fanthropic@3.0.111.patch",
"@ai-sdk/amazon-bedrock@4.0.166": "patches/@ai-sdk%2Famazon-bedrock@4.0.166.patch"
}
}
2 changes: 1 addition & 1 deletion packages/app/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

- `opencode dev web` proxies `https://app.opencode.ai`, so local UI/CSS changes will not show there.
- For local UI changes, run the backend and app dev servers separately.
- Backend (from `packages/opencode`): `bun run --conditions=browser ./src/index.ts serve --port 4096`
- Backend (from `packages/opencode`): `bun run ./src/index.ts serve --port 4096`
- App (from `packages/app`): `bun dev -- --port 4444`
- Open `http://localhost:4444` to verify UI changes (it targets the backend at `http://localhost:4096`).

Expand Down
141 changes: 141 additions & 0 deletions packages/app/e2e/regression/session-rename.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { expect, test } from "@playwright/test"
import { fixture, pageMessages } from "../smoke/session-timeline.fixture"
import { mockOpenCodeServer } from "../utils/mock-server"

test.beforeEach(async ({ page }) => {
const sessions = fixture.sessions.map((session) => ({ ...session }))
await mockOpenCodeServer(page, {
protocol: "v1",
sessions,
provider: fixture.provider,
directory: fixture.directory,
project: fixture.project,
pageMessages,
})
await page.route(/\/session\/[^/]+(?:\?.*)?$/, async (route) => {
if (route.request().method() !== "PATCH") return route.fallback()
const id = new URL(route.request().url()).pathname.split("/").at(-1)
const session = sessions.find((item) => item.id === id)
const payload: unknown = route.request().postDataJSON()
if (
!session ||
!payload ||
typeof payload !== "object" ||
!("title" in payload) ||
typeof payload.title !== "string"
)
throw new Error("Invalid rename request")
session.title = payload.title
await route.fulfill({ json: session, headers: { "access-control-allow-origin": "*" } })
})
await page.addInitScript((directory) => {
localStorage.setItem(
"opencode.global.dat:server",
JSON.stringify({
projects: { local: [{ worktree: directory, expanded: true }] },
lastProject: { local: directory },
}),
)
}, fixture.directory)
await page.goto("/")
await page.locator('[data-component="home-session-row"]').filter({ hasText: fixture.expected.targetTitle }).click()
await expect(page.getByRole("heading", { name: fixture.expected.targetTitle, exact: true })).toBeVisible()
})

for (const commit of ["Enter", "blur", "click outside"]) {
test(`saves the session heading on ${commit}`, async ({ page }) => {
await page.getByRole("heading", { name: fixture.expected.targetTitle, exact: true }).click()
const input = page.locator('input[data-slot="session-title-child"]')
await expect(input).toBeFocused()
await input.fill("Renamed session")
if (commit === "Enter") await input.press("Enter")
if (commit === "blur") await input.press("Tab")
if (commit === "click outside") await page.getByRole("textbox", { name: "Prompt", exact: true }).click()
await expect(page.getByRole("heading", { name: "Renamed session", exact: true })).toBeVisible()
await expect(page.locator('[data-slot="titlebar-tabs"] a').filter({ hasText: "Renamed session" })).toBeVisible()
await page.reload()
await expect(page.getByRole("heading", { name: "Renamed session", exact: true })).toBeVisible()
})
}

test("cancels the session heading with Escape", async ({ page }) => {
await page.getByRole("heading", { name: fixture.expected.targetTitle, exact: true }).click()
const input = page.locator('input[data-slot="session-title-child"]')
await input.fill("Discard this title")
await input.press("Escape")
await expect(page.getByRole("heading", { name: fixture.expected.targetTitle, exact: true })).toBeVisible()
await page.reload()
await expect(page.getByRole("heading", { name: fixture.expected.targetTitle, exact: true })).toBeVisible()
})

test("keeps the draft when saving the session heading fails", async ({ page }) => {
await page.route(/\/session\/[^/]+(?:\?.*)?$/, (route) => {
if (route.request().method() !== "PATCH") return route.fallback()
return route.fulfill({ status: 500, headers: { "access-control-allow-origin": "*" } })
})
await page.getByRole("heading", { name: fixture.expected.targetTitle, exact: true }).click()
const input = page.locator('input[data-slot="session-title-child"]')
await input.fill("Retry this title")
await input.press("Tab")
await expect(page.getByText("Request failed", { exact: true })).toBeVisible()
await expect(input).toBeEnabled()
await expect(input).toHaveValue("Retry this title")
await expect(
page.locator('[data-slot="titlebar-tabs"] a').filter({ hasText: fixture.expected.targetTitle }),
).toBeVisible()
})

test("does not save an empty session heading", async ({ page }) => {
await page.getByRole("heading", { name: fixture.expected.targetTitle, exact: true }).click()
const input = page.locator('input[data-slot="session-title-child"]')
await input.fill(" ")
await input.press("Tab")
await expect(page.getByRole("heading", { name: fixture.expected.targetTitle, exact: true })).toBeVisible()
await page.reload()
await expect(page.getByRole("heading", { name: fixture.expected.targetTitle, exact: true })).toBeVisible()
})

test("renames and closes the session tab from its context menu", async ({ page }) => {
const tab = page.locator('[data-slot="titlebar-tabs"] a').filter({ hasText: fixture.expected.targetTitle })
await tab.click({ button: "right" })
await expect(page.getByRole("menuitem", { name: "Rename", exact: true })).toBeVisible()
await page.keyboard.press("Escape")
await expect(page.getByRole("menuitem", { name: "Rename", exact: true })).toBeHidden()
await expect(tab).toBeFocused()
await tab.press("Shift+F10")
await page.getByRole("menuitem", { name: "Rename", exact: true }).click()
const input = page.locator('[data-slot="tab-title"][contenteditable="true"]')
await expect(input).toBeFocused()
await input.fill("Renamed from tab")
await input.press("Enter")
await expect(page.getByRole("heading", { name: "Renamed from tab", exact: true })).toBeVisible()
await page.reload()
await expect(page.getByRole("heading", { name: "Renamed from tab", exact: true })).toBeVisible()
const renamed = page.locator('[data-slot="titlebar-tabs"] a').filter({ hasText: "Renamed from tab" })
await renamed.click({ button: "right" })
await page.getByRole("menuitem", { name: "Close tab", exact: true }).click()
await expect(renamed).toBeHidden()
await page.getByRole("button", { name: "Home", exact: true }).click()
await expect(
page.locator('[data-component="home-session-row"]').filter({ hasText: "Renamed from tab" }),
).toBeVisible()
})

test("renames an inactive tab without switching sessions", async ({ page }) => {
await page.getByRole("button", { name: "Home", exact: true }).click()
await page.locator('[data-component="home-session-row"]').filter({ hasText: fixture.expected.sourceTitle }).click()
await expect(page.getByRole("heading", { name: fixture.expected.sourceTitle, exact: true })).toBeVisible()
const tab = page.locator('[data-slot="titlebar-tabs"] a').filter({ hasText: fixture.expected.targetTitle })
await tab.click({ button: "right" })
await page.getByRole("menuitem", { name: "Rename", exact: true }).click()
const input = page.locator('[data-slot="tab-title"][contenteditable="true"]')
await expect(input).toBeFocused()
await input.fill("Inactive tab renamed")
await input.press("Tab")
await expect(page.getByRole("heading", { name: fixture.expected.sourceTitle, exact: true })).toBeVisible()
await expect(page).toHaveURL(new RegExp(`/session/${fixture.sourceID}$`))
await page.locator('[data-slot="titlebar-tabs"] a').filter({ hasText: "Inactive tab renamed" }).click()
await expect(page.getByRole("heading", { name: "Inactive tab renamed", exact: true })).toBeVisible()
await page.reload()
await expect(page.getByRole("heading", { name: "Inactive tab renamed", exact: true })).toBeVisible()
})
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ test("shows the not found fallback when the viewed session is deleted", async ({
})

await expect(page.getByText("This session cannot be found")).toBeVisible()
await expect(page.getByRole("button", { name: "Close Tab" })).toBeVisible()
await expect(page.getByRole("button", { name: "Close Tab", exact: true })).toBeVisible()
await expect(page.getByRole("heading", { name: taskDescription })).toHaveCount(0)
})

Expand Down
4 changes: 2 additions & 2 deletions packages/app/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@opencode-ai/app",
"version": "1.18.25",
"version": "1.18.26",
"description": "",
"type": "module",
"exports": {
Expand Down Expand Up @@ -32,7 +32,7 @@
},
"license": "MIT",
"devDependencies": {
"@happy-dom/global-registrator": "20.0.11",
"@happy-dom/global-registrator": "20.12.0",
"@playwright/test": "catalog:",
"@sentry/vite-plugin": "catalog:",
"@tailwindcss/vite": "catalog:",
Expand Down
71 changes: 51 additions & 20 deletions packages/app/src/components/titlebar-tab-nav.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { createEffect, createMemo, createSignal, onCleanup, Show, type Ref } from "solid-js"
import { createStore } from "solid-js/store"
import { makeEventListener } from "@solid-primitives/event-listener"
import { createResizeObserver } from "@solid-primitives/resize-observer"
import { createMutation } from "@tanstack/solid-query"
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
import { MenuV2 } from "@opencode-ai/ui/v2/menu-v2"
import { useGlobal } from "@/context/global"
import { useLanguage } from "@/context/language"
import { ServerConnection, serverName } from "@/context/server"
Expand Down Expand Up @@ -33,6 +35,8 @@ export function TabNavItem(props: {
pressed?: boolean
hidden?: boolean
}) {
const language = useLanguage()
const [menu, setMenu] = createStore({ open: false, rename: false })
const [editing, setEditing] = createSignal(false)
const [titleOverflowing, setTitleOverflowing] = createSignal(false)
let tabRoot!: HTMLDivElement
Expand Down Expand Up @@ -76,7 +80,7 @@ export function TabNavItem(props: {
})

const [popoverOpen, setPopoverOpen] = createSignal(false)
const previewBlocked = () => !!props.dragging || editing() || !!props.pressed || !props.session()
const previewBlocked = () => !!props.dragging || editing() || menu.open || !!props.pressed || !props.session()

const measureTitleOverflow = () => {
if (!titleEl || editing()) {
Expand Down Expand Up @@ -138,9 +142,9 @@ export function TabNavItem(props: {
titleEl.textContent = value
})

const openRename = (event: MouseEvent) => {
event.preventDefault()
event.stopPropagation()
const openRename = (event?: MouseEvent) => {
event?.preventDefault()
event?.stopPropagation()
if (!canOpenTabRename(props.dragging, editing(), rename.isPending)) return
const session = props.session()
if (!session) return
Expand Down Expand Up @@ -171,7 +175,7 @@ export function TabNavItem(props: {
onCleanup(cleanup)
})

const tab = (
const tab = () => (
<div
ref={(el) => {
tabRoot = el
Expand All @@ -196,7 +200,11 @@ export function TabNavItem(props: {
closeTab(event)
}}
>
<a
<MenuV2.Context.Trigger
as="a"
disabled={editing() || props.dragging}
aria-haspopup="menu"
aria-expanded={menu.open}
data-slot="tab-link"
data-titlebar-tab-link
href={props.href}
Expand Down Expand Up @@ -276,7 +284,7 @@ export function TabNavItem(props: {
event.preventDefault()
}}
/>
</a>
</MenuV2.Context.Trigger>

<div data-slot="tab-close">
<IconButtonV2
Expand All @@ -289,26 +297,49 @@ export function TabNavItem(props: {
}}
onClick={closeTab}
icon={<IconV2 name="xmark-small" />}
aria-label={language.t("common.closeTab")}
/>
</div>
</div>
)

return (
<TabPreviewPopover
trigger={tab}
open={popoverOpen() && !previewBlocked()}
onOpenChange={(value) => {
if (value && previewBlocked()) return
setPopoverOpen(value)
}}
data={{
projectName: projectName(),
title: props.session()?.title,
path: previewPath(),
serverName: serverLabel(),
<MenuV2.Context
onOpenChange={(open) => {
setMenu("open", open)
if (open) setPopoverOpen(false)
}}
/>
>
<TabPreviewPopover
trigger={tab()}
open={popoverOpen() && !previewBlocked()}
onOpenChange={(value) => {
if (value && previewBlocked()) return
setPopoverOpen(value)
}}
data={{
projectName: projectName(),
title: props.session()?.title,
path: previewPath(),
serverName: serverLabel(),
}}
/>
<MenuV2.Context.Portal>
<MenuV2.Context.Content
onCloseAutoFocus={(event) => {
if (!menu.rename) return
event.preventDefault()
setMenu("rename", false)
openRename()
}}
>
<MenuV2.Item disabled={!props.session() || rename.isPending} onSelect={() => setMenu("rename", true)}>
{language.t("common.rename")}
</MenuV2.Item>
<MenuV2.Item onSelect={props.onClose}>{language.t("common.closeTab")}</MenuV2.Item>
</MenuV2.Context.Content>
</MenuV2.Context.Portal>
</MenuV2.Context>
)
}

Expand Down
4 changes: 3 additions & 1 deletion packages/app/src/pages/session/timeline/message-timeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -777,6 +777,7 @@ export function MessageTimeline(props: {
}

const saveTitleEditor = () => {
if (!title.editing) return
const id = sessionID()
if (!id) return
if (titleMutation.isPending) return
Expand Down Expand Up @@ -1447,6 +1448,7 @@ export function MessageTimeline(props: {
onInput={(event) => setTitle("draft", event.currentTarget.value)}
onKeyDown={(event) => {
event.stopPropagation()
if (event.isComposing || event.keyCode === 229) return
if (event.key === "Enter") {
event.preventDefault()
void saveTitleEditor()
Expand All @@ -1457,7 +1459,7 @@ export function MessageTimeline(props: {
closeTitleEditor()
}
}}
onBlur={closeTitleEditor}
onBlur={saveTitleEditor}
/>
</Show>
</Show>
Expand Down
Loading