From 082fe93e160c042332b3686a006d0f35ce4a6d6e Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Wed, 5 Aug 2026 20:16:40 +0000 Subject: [PATCH 01/34] chore: generate --- packages/opencode/test/session/processor-effect.test.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/opencode/test/session/processor-effect.test.ts b/packages/opencode/test/session/processor-effect.test.ts index d9466c0c2c98..052477d0a2e7 100644 --- a/packages/opencode/test/session/processor-effect.test.ts +++ b/packages/opencode/test/session/processor-effect.test.ts @@ -610,9 +610,7 @@ it.live("session.processor effect tests retry OpenAI-compatible midstream server Effect.gen(function* () { const { processors, session, provider } = yield* boot() - yield* llm.push( - raw({ chunks: [{ error: { type: "server_error", code: "server_error", message: "xxx" } }] }), - ) + yield* llm.push(raw({ chunks: [{ error: { type: "server_error", code: "server_error", message: "xxx" } }] })) yield* llm.text("after") const chat = yield* session.create({}) From b1f8cc04af936c3d3b5de8cb0645c6d249968eb4 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Wed, 5 Aug 2026 20:33:25 +0000 Subject: [PATCH 02/34] chore: update nix node_modules hashes --- nix/hashes.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nix/hashes.json b/nix/hashes.json index 83bc08a630ed..6f321d88bf2e 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-GRjnvvyj37H36RqiCB7dz5ALAEwvw16izwuk1wsHEpU=", - "aarch64-linux": "sha256-0OIn1o6dpqIQ5XgIMzpenMCMqsYzraaYVJk+te5eINU=", - "aarch64-darwin": "sha256-sQSQcuox78d8wT1lsKYHqVBl11NusiszQ+gu2XYXZi8=", - "x86_64-darwin": "sha256-SINkhMRd4oM1zABSs1Uf3OAzxJ1lgcycl1U4fmi+baY=" + "x86_64-linux": "sha256-uduwrM143NDSc+tXsi4lVVfoMll2a3BDHRUjuO7GB68=", + "aarch64-linux": "sha256-6DUda78XdXY6DP86lIUkweSjys3iG4Y4mo1PiaNuXbg=", + "aarch64-darwin": "sha256-AkJwfLULLZVwwz+XU1QcFUZoIS7oVPCn+n/MXEaxrqE=", + "x86_64-darwin": "sha256-hAxKGdiITTxQ2uujQt6prNjo3NxGAMMeo+9HlMWK6GU=" } } From 146720e197866afc2e799462374c42b51e12857b Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Wed, 5 Aug 2026 15:54:35 -0500 Subject: [PATCH 03/34] fix(stats): improve page speed --- packages/stats/app/src/entry-server.tsx | 12 ++++++- .../stats/app/src/routes/compare-cards.tsx | 1 - packages/stats/app/src/routes/index.css | 36 +++++++++++++------ packages/stats/app/src/routes/index.tsx | 30 ++++------------ packages/stats/app/vite.config.ts | 3 +- 5 files changed, 45 insertions(+), 37 deletions(-) diff --git a/packages/stats/app/src/entry-server.tsx b/packages/stats/app/src/entry-server.tsx index 3cec2cba04dd..1ea92a46e938 100644 --- a/packages/stats/app/src/entry-server.tsx +++ b/packages/stats/app/src/entry-server.tsx @@ -1,7 +1,10 @@ // @refresh reload +import type { Asset, PageEvent } from "@solidjs/start" import { createHandler, StartServer } from "@solidjs/start/server" +import ibmPlexMonoMediumLatin1 from "@ibm/plex/IBM-Plex-Mono/fonts/split/woff2/IBMPlexMono-Medium-Latin1.woff2?url" import { getRequestEvent } from "solid-js/web" import { dir, localeFromRequest, tag } from "./lib/language" +import statsStylesheetUrl from "./routes/index.css?url" const statsThemePreloadScript = `;(function () { var preference = "system" @@ -18,8 +21,13 @@ export default createHandler( () => ( { - const event = getRequestEvent() + const event = getRequestEvent() as PageEvent | undefined const locale = event ? localeFromRequest(event.request) : "en" + const stylesheet = (event?.assets as Asset[] | undefined)?.find( + (asset): asset is Extract => + asset.tag === "link" && asset.attrs.rel === "stylesheet", + ) + const stylesheetHref = import.meta.env.DEV ? statsStylesheetUrl : stylesheet?.attrs.href return ( @@ -27,6 +35,8 @@ export default createHandler( + + {stylesheetHref ? : null} {assets} diff --git a/packages/stats/app/src/routes/compare-cards.tsx b/packages/stats/app/src/routes/compare-cards.tsx index 47cc07b9295d..10a105580085 100644 --- a/packages/stats/app/src/routes/compare-cards.tsx +++ b/packages/stats/app/src/routes/compare-cards.tsx @@ -86,7 +86,6 @@ function FeaturedComparisonCard(props: { pair: ComparisonPair }) { diff --git a/packages/stats/app/src/routes/index.css b/packages/stats/app/src/routes/index.css index c378688490cd..39877378a435 100644 --- a/packages/stats/app/src/routes/index.css +++ b/packages/stats/app/src/routes/index.css @@ -55,7 +55,7 @@ body { --color-background-strong: #161616; --color-background-strong-hover: #242424; --color-text: #5c5c5c; - --color-text-weak: #808080; + --color-text-weak: #707070; --color-text-strong: #161616; --color-text-inverted: #ffffff; --color-border-weak: #0000001a; @@ -66,10 +66,10 @@ body { --stats-line-strong: #00000033; --stats-text: #161616; --stats-muted: #5c5c5c; - --stats-faint: #808080; + --stats-faint: #707070; --stats-theme-icon-active: #3a3a3a; --stats-accent: #3b5cf6; - --stats-accent-text: #6c7dff; + --stats-accent-text: #3b5cf6; --stats-bar-idle: #d4d4d4; --stats-dot: #d4d4d4; --stats-hero-muted: #5c5c5c; @@ -92,6 +92,18 @@ body { display: none !important; } +[data-page="stats"] [data-slot="visually-hidden"] { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0 0 0 0); + white-space: nowrap; + border: 0; +} + [data-page="stats"][data-layout="compare-detail"] { /* The table contains its own wide rows; keep the page itself out of the horizontal scroll chain. */ overflow-x: visible; @@ -7079,7 +7091,7 @@ body { --color-background-strong: #ffffff; --color-background-strong-hover: #eeeeee; --color-text: #d4d4d4; - --color-text-weak: #808080; + --color-text-weak: #a3a3a3; --color-text-strong: #ffffff; --color-text-inverted: #161616; --color-border-weak: #ffffff1a; @@ -7090,11 +7102,12 @@ body { --stats-line-strong: #ffffff33; --stats-text: #ffffff; --stats-muted: #d4d4d4; - --stats-faint: #808080; + --stats-faint: #a3a3a3; + --stats-accent-text: #8190ff; --stats-theme-icon-active: #fafafa; --stats-bar-idle: #303030; --stats-dot: #303030; - --stats-hero-muted: #808080; + --stats-hero-muted: #a3a3a3; --stats-hero-pattern: #303030; --stats-logo-bg: #f1ecec; --stats-logo-fill: #b7b1b1; @@ -7254,7 +7267,7 @@ body { --color-background-strong: #ffffff; --color-background-strong-hover: #eeeeee; --color-text: #d4d4d4; - --color-text-weak: #808080; + --color-text-weak: #a3a3a3; --color-text-strong: #ffffff; --color-text-inverted: #161616; --color-border-weak: #ffffff1a; @@ -7265,11 +7278,12 @@ body { --stats-line-strong: #ffffff33; --stats-text: #ffffff; --stats-muted: #d4d4d4; - --stats-faint: #808080; + --stats-faint: #a3a3a3; + --stats-accent-text: #8190ff; --stats-theme-icon-active: #fafafa; --stats-bar-idle: #303030; --stats-dot: #303030; - --stats-hero-muted: #808080; + --stats-hero-muted: #a3a3a3; --stats-hero-pattern: #303030; --stats-logo-bg: #f1ecec; --stats-logo-fill: #b7b1b1; @@ -8393,7 +8407,7 @@ body { } [data-page="stats"] [data-component="top-models-chart"][data-dense-labels="true"] { - --top-models-mobile-bar-width: 12px; + --top-models-mobile-bar-width: 18px; } [data-page="stats"] [data-component="top-models-chart"][data-dense-labels="true"] [data-slot="top-models-axis"], @@ -8402,7 +8416,7 @@ body { } [data-page="stats"] [data-component="market-share"][data-dense-labels="true"] { - --market-mobile-bar-width: 12px; + --market-mobile-bar-width: 18px; } [data-page="stats"] [data-component="market-share"][data-dense-labels="true"] [data-slot="market-labels"], diff --git a/packages/stats/app/src/routes/index.tsx b/packages/stats/app/src/routes/index.tsx index 28e5cf59a336..398f53f71861 100644 --- a/packages/stats/app/src/routes/index.tsx +++ b/packages/stats/app/src/routes/index.tsx @@ -1,11 +1,7 @@ -import { Link, Meta, Title } from "@solidjs/meta" +import { Meta, Title } from "@solidjs/meta" import { ProviderIcon } from "@opencode-ai/ui/provider-icon" import { scaleSqrt } from "d3-scale" import countryCodesSource from "i18n-iso-countries/codes.json?raw" -import ibmPlexMonoRegularLatin1 from "@ibm/plex/IBM-Plex-Mono/fonts/split/woff2/IBMPlexMono-Regular-Latin1.woff2?url" -import ibmPlexMonoMediumLatin1 from "@ibm/plex/IBM-Plex-Mono/fonts/split/woff2/IBMPlexMono-Medium-Latin1.woff2?url" -import ibmPlexMonoSemiBoldLatin1 from "@ibm/plex/IBM-Plex-Mono/fonts/split/woff2/IBMPlexMono-SemiBold-Latin1.woff2?url" -import ibmPlexMonoBoldLatin1 from "@ibm/plex/IBM-Plex-Mono/fonts/split/woff2/IBMPlexMono-Bold-Latin1.woff2?url" import { getStatsHomeData, type CacheRatioEntry, @@ -142,10 +138,6 @@ export default function StatsHome() { - - - -
@@ -227,7 +219,8 @@ function Hero(props: { updatedAt: string | null }) { return (
-

+

+ {currentUpdatedLabel()}

+
{(entry) => ( @@ -766,7 +758,7 @@ function Leaderboard(props: { )}
-
+
{(entry) => ( props.onActiveModelChange(props.entry.model)} onPointerLeave={(event) => { if (event.pointerType === "touch") return @@ -956,6 +945,7 @@ function MarketShare(props: { {(day, index) => ( +
{(message) => ( diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index c37c9c21161a..26720c8ac0d4 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -95,6 +95,8 @@ export const dict = { "command.session.share.description": "Share this session and copy the URL to clipboard", "command.session.unshare": "Unshare session", "command.session.unshare.description": "Stop sharing this session", + "command.session.export": "Export session", + "command.session.export.description": "Export the full session transcript as JSON", "palette.search.placeholder": "Search files, commands, and sessions", "palette.search.placeholder.home": "Search commands and sessions", @@ -489,6 +491,7 @@ export const dict = { "context.systemPrompt.title": "System Prompt", "context.rawMessages.title": "Raw messages", + "context.export.session": "Export session", "context.stats.session": "Session", "context.stats.messages": "Messages", @@ -568,6 +571,11 @@ export const dict = { "toast.session.unshare.failed.title": "Failed to unshare session", "toast.session.unshare.failed.description": "An error occurred while unsharing the session", + "toast.session.export.success.title": "Session exported", + "toast.session.export.success.description": "Saved session to {{filename}}", + "toast.session.export.failed.title": "Failed to export session", + "toast.session.export.failed.description": "An error occurred while exporting the session", + "toast.session.listFailed.title": "Failed to load sessions for {{project}}", "toast.project.reloadFailed.title": "Failed to reload {{project}}", @@ -802,6 +810,7 @@ export const dict = { "common.moreOptions": "More options", "common.learnMore": "Learn more", "common.rename": "Rename", + "common.export": "Export", "common.reset": "Reset", "common.archive": "Archive", "common.delete": "Delete", diff --git a/packages/app/src/pages/session/timeline/message-timeline.tsx b/packages/app/src/pages/session/timeline/message-timeline.tsx index 7d22ce5e1668..e69623179faa 100644 --- a/packages/app/src/pages/session/timeline/message-timeline.tsx +++ b/packages/app/src/pages/session/timeline/message-timeline.tsx @@ -53,6 +53,7 @@ import type { UserMessage, } from "@opencode-ai/sdk/v2" import { showToast } from "@/utils/toast" +import { downloadSessionExport, fetchSessionExport, sessionExportFilename } from "@/utils/session-export" import { getDirectory, getFilename } from "@opencode-ai/core/util/path" import { Popover as KobaltePopover } from "@kobalte/core/popover" import { normalize } from "@opencode-ai/session-ui/session-diff" @@ -806,6 +807,29 @@ export function MessageTimeline(props: { navigate(`/${params.dir}/session`) } + const exportSession = async (sessionID: string) => { + try { + const data = await fetchSessionExport({ + sessionID, + client: sdk().client, + }) + const filename = sessionExportFilename(data.info) + downloadSessionExport(filename, data) + showToast({ + variant: "success", + icon: "circle-check", + title: language.t("toast.session.export.success.title"), + description: language.t("toast.session.export.success.description", { filename }), + }) + } catch (err) { + showToast({ + variant: "error", + title: language.t("toast.session.export.failed.title"), + description: err instanceof Error ? err.message : language.t("toast.session.export.failed.description"), + }) + } + } + const archiveSession = async (sessionID: string) => { const session = sync().session.get(sessionID) if (!session) return @@ -1564,6 +1588,9 @@ export function MessageTimeline(props: { + exportSession(id)}> + {language.t("common.export")} + void archiveSession(id)}> {language.t("common.archive")} @@ -1635,6 +1662,9 @@ export function MessageTimeline(props: { {language.t("session.share.action.share")}... + exportSession(id)}> + {language.t("common.export")}... + void archiveSession(id)}> {language.t("common.archive")} diff --git a/packages/app/src/pages/session/use-session-commands.tsx b/packages/app/src/pages/session/use-session-commands.tsx index 12dd96a5e66b..cfc302e77533 100644 --- a/packages/app/src/pages/session/use-session-commands.tsx +++ b/packages/app/src/pages/session/use-session-commands.tsx @@ -12,10 +12,11 @@ import { useSettings } from "@/context/settings" import { useSync } from "@/context/sync" import { useTerminal } from "@/context/terminal" import { showToast } from "@/utils/toast" +import { downloadSessionExport, fetchSessionExport, sessionExportFilename } from "@/utils/session-export" import { findLast } from "@opencode-ai/core/util/array" import { createSessionTabs } from "@/pages/session/helpers" import { extractPromptFromParts } from "@/utils/prompt" -import { UserMessage } from "@opencode-ai/sdk/v2" +import { Message, Part, UserMessage } from "@opencode-ai/sdk/v2" import { useSessionLayout } from "@/pages/session/session-layout" import { createSessionOwnership } from "./session-ownership" import { useLocal } from "@/context/local" @@ -231,6 +232,31 @@ export const useSessionCommands = (actions: SessionCommandContext) => { ) } + const exportSession = async () => { + const sessionID = params.id + if (!sessionID) return + try { + const data = await fetchSessionExport({ + sessionID, + client: sdk().client, + }) + const filename = sessionExportFilename(data.info) + downloadSessionExport(filename, data) + showToast({ + variant: "success", + icon: "circle-check", + title: language.t("toast.session.export.success.title"), + description: language.t("toast.session.export.success.description", { filename }), + }) + } catch (err) { + showToast({ + variant: "error", + title: language.t("toast.session.export.failed.title"), + description: err instanceof Error ? err.message : language.t("toast.session.export.failed.description"), + }) + } + } + const openFile = () => { void openDialog( () => import("@/components/dialog-select-file"), @@ -458,6 +484,14 @@ export const useSessionCommands = (actions: SessionCommandContext) => { disabled: !params.id || visibleUserMessages().length === 0, onSelect: fork, }), + sessionCommand({ + id: "session.export", + title: language.t("command.session.export"), + description: language.t("command.session.export.description"), + slash: "export", + disabled: !params.id, + onSelect: exportSession, + }), ] const fileCmds = () => { diff --git a/packages/app/src/utils/session-export.test.ts b/packages/app/src/utils/session-export.test.ts new file mode 100644 index 000000000000..ff18a16b7fa3 --- /dev/null +++ b/packages/app/src/utils/session-export.test.ts @@ -0,0 +1,61 @@ +import { describe, expect, test } from "bun:test" +import { fetchSessionExport, sessionExportFilename } from "./session-export" +import type { Message, Part, Session } from "@opencode-ai/sdk/v2/client" + +describe("sessionExportFilename", () => { + test("generates filename from title", () => { + expect(sessionExportFilename({ id: "ses_123", title: "Clone PR in worktree from fork" })).toBe( + "clone-pr-in-worktree-from-fork.json", + ) + }) + + test("generates filename from slug when title missing", () => { + expect(sessionExportFilename({ id: "ses_123", slug: "my-session-slug" })).toBe("my-session-slug.json") + }) + + test("falls back to id when title and slug are empty", () => { + expect(sessionExportFilename({ id: "ses_123" })).toBe("ses_123.json") + }) +}) + +describe("fetchSessionExport", () => { + test("fetches full transcript from client", async () => { + const session = { id: "ses_1", title: "Test Session" } as Session + const msg = { id: "msg_1", role: "user" } as Message + const part = { id: "prt_1", type: "text", text: "hello" } as Part + const messages = [{ info: msg, parts: [part] }] + + const client = { + session: { + get: async () => ({ data: session }), + messages: async () => ({ data: messages }), + }, + } + + const result = await fetchSessionExport({ + sessionID: "ses_1", + client, + }) + + expect(result).toEqual({ + info: session, + messages, + }) + }) + + test("throws when session not found", async () => { + const client = { + session: { + get: async () => ({ data: null }), + messages: async () => ({ data: [] }), + }, + } + + expect( + fetchSessionExport({ + sessionID: "ses_missing", + client, + }), + ).rejects.toThrow("Session not found: ses_missing") + }) +}) diff --git a/packages/app/src/utils/session-export.ts b/packages/app/src/utils/session-export.ts new file mode 100644 index 000000000000..6eb9f9ab6a7c --- /dev/null +++ b/packages/app/src/utils/session-export.ts @@ -0,0 +1,61 @@ +import type { Message, Part, Session } from "@opencode-ai/sdk/v2/client" + +// Matches the exact `{ info, messages: [{ info, parts }] }` structure produced by `opencode export` CLI +export type SessionExportData = { + info: Session + messages: { + info: Message + parts: Part[] + }[] +} + +export type SessionExportClient = { + session: { + get: (input: { sessionID: string }) => Promise<{ data?: Session | null }> + messages: (input: { sessionID: string }) => Promise<{ data?: SessionExportData["messages"] | null }> + } +} + +export async function fetchSessionExport(input: { + sessionID: string + client: SessionExportClient +}): Promise { + const [sessionRes, messagesRes] = await Promise.all([ + input.client.session.get({ sessionID: input.sessionID }), + input.client.session.messages({ sessionID: input.sessionID }), + ]) + + if (!sessionRes?.data) { + throw new Error(`Session not found: ${input.sessionID}`) + } + if (!messagesRes?.data) { + throw new Error(`Failed to load messages for session: ${input.sessionID}`) + } + + return { + info: sessionRes.data, + messages: messagesRes.data, + } +} + +export function sessionExportFilename(session: { id: string; title?: string; slug?: string }) { + const name = session.title || session.slug || session.id + const clean = name + .toLowerCase() + .replace(/[^a-z0-9_-]+/gi, "-") + .replace(/^-+|-+$/g, "") + return `${clean || session.id}.json` +} + +export function downloadSessionExport(filename: string, data: unknown) { + const json = JSON.stringify(data, null, 2) + const blob = new Blob([json], { type: "application/json" }) + const url = URL.createObjectURL(blob) + const a = document.createElement("a") + a.href = url + a.download = filename + document.body.appendChild(a) + a.click() + document.body.removeChild(a) + URL.revokeObjectURL(url) +} From b8bd88901a4870ef3a5752840f4e23e11d54e24e Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Thu, 6 Aug 2026 01:52:15 +0000 Subject: [PATCH 09/34] chore: generate --- .../app/src/components/session/session-context-tab.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/app/src/components/session/session-context-tab.tsx b/packages/app/src/components/session/session-context-tab.tsx index c08c202fa2e9..2f37c33fb37e 100644 --- a/packages/app/src/components/session/session-context-tab.tsx +++ b/packages/app/src/components/session/session-context-tab.tsx @@ -358,7 +358,12 @@ export function SessionContextTab() {
{language.t("context.rawMessages.title")}
- From 847771fe06d89eb41aa5f5ba9ebe6cee1ec6067c Mon Sep 17 00:00:00 2001 From: Frank Date: Thu, 6 Aug 2026 03:21:16 -0400 Subject: [PATCH 10/34] fix log processor --- .../console/function/src/log-processor.ts | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/console/function/src/log-processor.ts b/packages/console/function/src/log-processor.ts index ee8743ef1544..75daba2b5046 100644 --- a/packages/console/function/src/log-processor.ts +++ b/packages/console/function/src/log-processor.ts @@ -54,15 +54,15 @@ export default { console.log(JSON.stringify(data, null, 2)) const lakeIngest = getLakeIngest() - const [honeycomb, lake] = await Promise.all([ - fetch("https://api.honeycomb.io/1/batch/zen", { - method: "POST", - headers: { - "Content-Type": "application/json", - "X-Honeycomb-Team": Resource.HONEYCOMB_API_KEY.value, - }, - body: JSON.stringify(events), - }), + const [lake] = await Promise.all([ + // fetch("https://api.honeycomb.io/1/batch/zen", { + // method: "POST", + // headers: { + // "Content-Type": "application/json", + // "X-Honeycomb-Team": Resource.HONEYCOMB_API_KEY.value, + // }, + // body: JSON.stringify(events), + // }), ...(lakeIngest ? [ fetch(lakeIngest.url, { @@ -76,8 +76,8 @@ export default { ] : []), ]) - console.log(honeycomb.status) - console.log(await honeycomb.text()) + // console.log(honeycomb.status) + // console.log(await honeycomb.text()) if (lake) { console.log(lake.status) console.log(await lake.text()) From f6c5afe5a960a252177c1c7ddc280706fd85b7b0 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" <219766164+opencode-agent[bot]@users.noreply.github.com> Date: Thu, 6 Aug 2026 17:25:41 +1000 Subject: [PATCH 11/34] fix(desktop): disable packaged console logging (#40794) Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com> --- packages/desktop/src/main/logging.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/desktop/src/main/logging.ts b/packages/desktop/src/main/logging.ts index 8866fd78ef77..b8fc260cb874 100644 --- a/packages/desktop/src/main/logging.ts +++ b/packages/desktop/src/main/logging.ts @@ -189,6 +189,11 @@ async function writeZip(output: string, entries: Entry[]) { } function initConsoleTransport() { + if (app.isPackaged) { + log.transports.console.level = false + return + } + const write = log.transports.console.writeFn.bind(log.transports.console) log.transports.console.writeFn = (options) => { try { From def7220bfc65b84046e597e9be772eae81f663ff Mon Sep 17 00:00:00 2001 From: ayu Date: Thu, 6 Aug 2026 01:23:32 -0700 Subject: [PATCH 12/34] fix(tui): support copying over ssh with `set-clipboard on` tmux config (#30472) Co-authored-by: Simon Klee --- packages/tui/src/clipboard.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/tui/src/clipboard.ts b/packages/tui/src/clipboard.ts index 08f86f9f7a97..2ae29da88894 100644 --- a/packages/tui/src/clipboard.ts +++ b/packages/tui/src/clipboard.ts @@ -23,7 +23,8 @@ function command(command: string, args: string[] = [], input?: string) { function writeOsc52(text: string) { if (!process.stdout.isTTY) return const sequence = `\x1b]52;c;${Buffer.from(text).toString("base64")}\x07` - process.stdout.write(process.env.TMUX || process.env.STY ? `\x1bPtmux;\x1b${sequence}\x1b\\` : sequence) + const passthrough = `\x1bPtmux;\x1b${sequence}\x1b\\` + process.stdout.write(process.env.TMUX ? sequence + passthrough : process.env.STY ? passthrough : sequence) } export async function read() { From b379d71d194704ed42d08e9b736cf2966e3d1a02 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 6 Aug 2026 10:58:58 -0500 Subject: [PATCH 13/34] fix(stats): update github stars --- packages/stats/app/src/component/model-compare-detail.tsx | 7 ++++++- packages/stats/app/src/routes/[lab]/[model].tsx | 7 ++++++- packages/stats/app/src/routes/[lab]/index.tsx | 7 ++++++- packages/stats/app/src/routes/compare/index.tsx | 7 ++++++- packages/stats/app/src/routes/stats-shell.tsx | 5 +++-- 5 files changed, 27 insertions(+), 6 deletions(-) diff --git a/packages/stats/app/src/component/model-compare-detail.tsx b/packages/stats/app/src/component/model-compare-detail.tsx index d02723e4614f..3790789b58b3 100644 --- a/packages/stats/app/src/component/model-compare-detail.tsx +++ b/packages/stats/app/src/component/model-compare-detail.tsx @@ -31,6 +31,7 @@ import { applyThemePreference, Footer, getGitHubStars, + githubLink, Header, isThemePreference, themeStorageKey, @@ -240,7 +241,11 @@ export default function ModelCompareDetailPage(props: ModelCompareDetailPageProp -
+
-
+
}> diff --git a/packages/stats/app/src/routes/[lab]/index.tsx b/packages/stats/app/src/routes/[lab]/index.tsx index 3d4a1a663c37..ab202fc89f7d 100644 --- a/packages/stats/app/src/routes/[lab]/index.tsx +++ b/packages/stats/app/src/routes/[lab]/index.tsx @@ -32,6 +32,7 @@ import { applyThemePreference, Footer, getGitHubStars, + githubLink, Header, isThemePreference, themeStorageKey, @@ -141,7 +142,11 @@ export default function StatsLab() { -
+
}> diff --git a/packages/stats/app/src/routes/compare/index.tsx b/packages/stats/app/src/routes/compare/index.tsx index 7aa5cbe4c708..861f69493473 100644 --- a/packages/stats/app/src/routes/compare/index.tsx +++ b/packages/stats/app/src/routes/compare/index.tsx @@ -20,6 +20,7 @@ import { applyThemePreference, Footer, getGitHubStars, + githubLink, Header, isThemePreference, themeStorageKey, @@ -125,7 +126,11 @@ export default function ModelCompareIndex() { -
+
diff --git a/packages/stats/app/src/routes/stats-shell.tsx b/packages/stats/app/src/routes/stats-shell.tsx index 8906dc9d5a7a..1aa6e7a26fa8 100644 --- a/packages/stats/app/src/routes/stats-shell.tsx +++ b/packages/stats/app/src/routes/stats-shell.tsx @@ -10,7 +10,7 @@ export type HeaderLink = { href: string; label: string } export const githubLink = { href: "https://github.com/anomalyco/opencode", apiHref: "https://api.github.com/repos/anomalyco/opencode", - fallbackStars: "150K", + fallbackStars: "195K", } export const themePreferences = ["dark", "light", "system"] as const export const themeStorageKey = "opencode:stats-theme" @@ -18,7 +18,8 @@ export type ThemePreference = (typeof themePreferences)[number] const compactNumberFormatter = new Intl.NumberFormat("en", { notation: "compact", - maximumFractionDigits: 1, + maximumFractionDigits: 0, + roundingIncrement: 5, }) export const getGitHubStars = query(async () => { From 03bff6500abd09fc469d59e5bd4143d3eb053a94 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 6 Aug 2026 11:07:33 -0500 Subject: [PATCH 14/34] fix(app): update homepage stats --- packages/console/app/src/config.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/console/app/src/config.ts b/packages/console/app/src/config.ts index 2cd039fef7ef..78da7eba3120 100644 --- a/packages/console/app/src/config.ts +++ b/packages/console/app/src/config.ts @@ -9,8 +9,8 @@ export const config = { github: { repoUrl: "https://github.com/anomalyco/opencode", starsFormatted: { - compact: "160K", - full: "160,000", + compact: "195K", + full: "195,000", }, }, @@ -22,8 +22,8 @@ export const config = { // Static stats (used on landing page) stats: { - contributors: "900", + contributors: "950", commits: "13,000", - monthlyUsers: "7.5M", + monthlyUsers: "16M", }, } as const From dc898ca2c3e64f95fca7e2f737c3cd1eb14ae5ec Mon Sep 17 00:00:00 2001 From: Frank Date: Thu, 6 Aug 2026 19:34:26 -0400 Subject: [PATCH 15/34] sync --- packages/console/app/src/routes/zen/util/handler.ts | 3 ++- packages/console/core/src/schema/billing.sql.ts | 1 - packages/console/core/src/schema/referral.sql.ts | 7 ++----- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/console/app/src/routes/zen/util/handler.ts b/packages/console/app/src/routes/zen/util/handler.ts index 4d31756161d8..a92aaaea35c1 100644 --- a/packages/console/app/src/routes/zen/util/handler.ts +++ b/packages/console/app/src/routes/zen/util/handler.ts @@ -197,7 +197,8 @@ export async function handler( if (Array.isArray(v)) return [[k, v]] if (typeof v === "object") return [[k, replacer(v)]] if (typeof v === "string") { - if (v === "$workspace") return authInfo?.workspaceID ? [[k, authInfo?.workspaceID]] : [] + if (v === "$workspace") return authInfo?.workspaceID ? [[k, authInfo.workspaceID]] : [] + if (v === "$org") return authInfo?.workspaceID ? [[k, authInfo.workspaceID.replace("wrk_", "org_")]] : [] if (v === "$user") return stickyId ? [[k, stickyId]] : [] if (v.startsWith("$header.")) { const headerValue = input.request.headers.get(v.slice(8)) diff --git a/packages/console/core/src/schema/billing.sql.ts b/packages/console/core/src/schema/billing.sql.ts index b177858f363f..915646cf3da0 100644 --- a/packages/console/core/src/schema/billing.sql.ts +++ b/packages/console/core/src/schema/billing.sql.ts @@ -53,7 +53,6 @@ export const BillingTable = mysqlTable( ...workspaceIndexes(table), uniqueIndex("global_customer_id").on(table.customerID), uniqueIndex("global_subscription_id").on(table.subscriptionID), - uniqueIndex("global_lite_subscription_id").on(table.liteSubscriptionID), ], ) diff --git a/packages/console/core/src/schema/referral.sql.ts b/packages/console/core/src/schema/referral.sql.ts index be2499ee1158..9850c92457db 100644 --- a/packages/console/core/src/schema/referral.sql.ts +++ b/packages/console/core/src/schema/referral.sql.ts @@ -1,4 +1,4 @@ -import { bigint, index, mysqlTable, primaryKey, uniqueIndex, varchar } from "drizzle-orm/mysql-core" +import { bigint, mysqlTable, primaryKey, uniqueIndex, varchar } from "drizzle-orm/mysql-core" import { timestamps, ulid, utc, workspaceColumns } from "../drizzle/types" import { workspaceIndexes } from "./workspace.sql" @@ -31,8 +31,5 @@ export const ReferralRewardTable = mysqlTable( amount: bigint("amount", { mode: "number" }).notNull(), timeApplied: utc("time_applied"), }, - (table) => [ - primaryKey({ columns: [table.workspaceID, table.referralID] }), - index("referral_id").on(table.referralID), - ], + (table) => [primaryKey({ columns: [table.workspaceID, table.referralID] })], ) From 69f2cbaa3ab875bd1a1cf4392ea68f207b7966d8 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Thu, 6 Aug 2026 23:37:25 +0000 Subject: [PATCH 16/34] chore: generate --- packages/console/app/src/routes/zen/util/handler.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/console/app/src/routes/zen/util/handler.ts b/packages/console/app/src/routes/zen/util/handler.ts index a92aaaea35c1..7319295a67ba 100644 --- a/packages/console/app/src/routes/zen/util/handler.ts +++ b/packages/console/app/src/routes/zen/util/handler.ts @@ -198,7 +198,8 @@ export async function handler( if (typeof v === "object") return [[k, replacer(v)]] if (typeof v === "string") { if (v === "$workspace") return authInfo?.workspaceID ? [[k, authInfo.workspaceID]] : [] - if (v === "$org") return authInfo?.workspaceID ? [[k, authInfo.workspaceID.replace("wrk_", "org_")]] : [] + if (v === "$org") + return authInfo?.workspaceID ? [[k, authInfo.workspaceID.replace("wrk_", "org_")]] : [] if (v === "$user") return stickyId ? [[k, stickyId]] : [] if (v.startsWith("$header.")) { const headerValue = input.request.headers.get(v.slice(8)) From b7f9363393c4caefb413372d9a49111ed8289666 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" <219766164+opencode-agent[bot]@users.noreply.github.com> Date: Thu, 6 Aug 2026 21:11:14 -0500 Subject: [PATCH 17/34] fix(opencode): serialize orphaned compaction history (#40800) Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> --- packages/opencode/src/session/compaction.ts | 51 ++++++++++-- .../opencode/test/session/compaction.test.ts | 77 ++++++++++++++++++- 2 files changed, 119 insertions(+), 9 deletions(-) diff --git a/packages/opencode/src/session/compaction.ts b/packages/opencode/src/session/compaction.ts index fa439e4efffa..7693f5ccfdcc 100644 --- a/packages/opencode/src/session/compaction.ts +++ b/packages/opencode/src/session/compaction.ts @@ -49,6 +49,42 @@ type CompletedCompaction = { summary: string | undefined } +const truncate = (value: string) => + value.length <= TOOL_OUTPUT_MAX_CHARS ? value : `${value.slice(0, TOOL_OUTPUT_MAX_CHARS)}\n[truncated]` + +const serialize = (message: SessionV1.WithParts) => { + if (message.info.role === "user") { + const text = message.parts + .filter((part): part is SessionV1.TextPart => part.type === "text" && !part.ignored) + .map((part) => part.text) + .filter(Boolean) + .join("\n") + const files = message.parts.flatMap((part) => + part.type === "file" ? [`[Attached ${part.mime}: ${part.filename ?? "file"}]`] : [], + ) + return [...(text ? [`[User]: ${text}`] : []), ...files].join("\n") + } + return message.parts + .flatMap((part) => { + if (part.type === "text") return part.text ? [`[Assistant]: ${part.text}`] : [] + if (part.type === "reasoning") return part.text ? [`[Assistant reasoning]: ${part.text}`] : [] + if (part.type !== "tool") return [] + const call = `[Assistant tool call]: ${part.tool}(${JSON.stringify(part.state.input)})` + if (part.state.status === "completed") { + const attachments = (part.state.attachments ?? []).map( + (item) => `[Attached ${item.mime}: ${item.filename ?? "file"}]`, + ) + const output = part.state.time.compacted + ? "[Old tool result content cleared]" + : truncate([part.state.output, ...attachments].join("\n")) + return [call, `[Tool result]: ${output}`] + } + if (part.state.status === "error") return [call, `[Tool error]: ${part.state.error}`] + return [call] + }) + .join("\n") +} + function summaryText(message: SessionV1.WithParts) { const text = message.parts .filter((part): part is SessionV1.TextPart => part.type === "text") @@ -348,10 +384,7 @@ const layer = Layer.effect( const nextPrompt = compacting.prompt ?? buildPrompt({ previousSummary, context: compacting.context }) const msgs = structuredClone(selected.head) yield* plugin.trigger("experimental.chat.messages.transform", {}, { messages: msgs }) - const modelMessages = yield* MessageV2.toModelMessagesEffect(msgs, model, { - stripMedia: true, - toolOutputMaxChars: TOOL_OUTPUT_MAX_CHARS, - }) + const conversation = msgs.map(serialize).filter(Boolean).join("\n\n") const ctx = yield* InstanceState.context const msg: SessionV1.Assistant = { id: MessageID.ascending(), @@ -392,10 +425,16 @@ const layer = Layer.effect( tools: {}, system: [], messages: [ - ...modelMessages, { role: "user", - content: [{ type: "text", text: nextPrompt }], + content: [ + { + type: "text", + text: [nextPrompt, "The following is the conversation history:", conversation] + .filter(Boolean) + .join("\n\n"), + }, + ], }, ], model, diff --git a/packages/opencode/test/session/compaction.test.ts b/packages/opencode/test/session/compaction.test.ts index 4a4210cf08bc..0dff7354b5b6 100644 --- a/packages/opencode/test/session/compaction.test.ts +++ b/packages/opencode/test/session/compaction.test.ts @@ -1362,10 +1362,10 @@ describe("session.compaction.process", () => { "summarizes only the head while keeping recent tail out of summary input", () => { const stub = llm() - let captured = "" + let messages: LLM.StreamInput["messages"] = [] stub.push( reply("summary", (input) => { - captured = JSON.stringify(input.messages) + messages = input.messages }), ) return Effect.gen(function* () { @@ -1386,7 +1386,10 @@ describe("session.compaction.process", () => { auto: false, }) - expect(captured).toContain("older context") + const captured = JSON.stringify(messages) + expect(messages).toHaveLength(1) + expect(messages[0]?.role).toBe("user") + expect(captured).toContain("[User]: older context") expect(captured).not.toContain("keep this turn") expect(captured).not.toContain("and this one too") expect(captured).not.toContain("What did we do so far?") @@ -1437,6 +1440,74 @@ describe("session.compaction.process", () => { { git: true }, ) + itCompaction.instance( + "serializes repeated compaction history as one user message", + () => { + const stub = llm() + let captured: LLM.StreamInput["messages"] = [] + stub.push( + reply("summary two", (input) => { + captured = input.messages + }), + ) + + return Effect.gen(function* () { + const ssn = yield* SessionNs.Service + const test = yield* TestInstance + const session = yield* ssn.create({}) + const turn = yield* createUserMessage(session.id, "original request") + const kept = yield* createAssistantMessage(session.id, turn.id, test.directory) + yield* ssn.updatePart({ + id: PartID.ascending(), + messageID: kept.id, + sessionID: session.id, + type: "tool", + callID: "read-call", + tool: "read", + state: { + status: "completed", + input: { filePath: "src/index.ts" }, + output: "file contents", + title: "src/index.ts", + metadata: {}, + time: { start: Date.now(), end: Date.now() }, + }, + }) + + const previous = yield* ssn.updateMessage({ + id: MessageID.ascending(), + role: "user", + model: ref, + sessionID: session.id, + agent: "build", + time: { created: Date.now() }, + }) + yield* ssn.updatePart({ + id: PartID.ascending(), + messageID: previous.id, + sessionID: session.id, + type: "compaction", + auto: false, + tail_start_id: kept.id, + }) + yield* createSummaryAssistantMessage(session.id, previous.id, test.directory, "summary one") + yield* createCompactionMarker(session.id) + + const msgs = MessageV2.filterCompacted(yield* MessageV2.stream(session.id)) + const parent = msgs.at(-1)?.info.id + expect(parent).toBeTruthy() + yield* SessionCompaction.use.process({ parentID: parent!, messages: msgs, sessionID: session.id, auto: false }) + + expect(captured).toHaveLength(1) + expect(captured[0]?.role).toBe("user") + expect(JSON.stringify(captured)).toContain('[Assistant tool call]: read({\\"filePath\\":\\"src/index.ts\\"})') + expect(JSON.stringify(captured)).toContain("[Tool result]: file contents") + expect(JSON.stringify(captured)).not.toContain('\\"role\\":\\"assistant\\"') + }).pipe(withCompaction({ llm: stub.llmLayer, config: cfg({ tail_turns: 0 }) })) + }, + { git: true }, + ) + itCompaction.instance("keeps recent pre-compaction turns across repeated compactions", () => { const stub = llm() stub.push(reply("summary one")) From 8d65dbdd04b39f6d2830a708bfb843b1d006a44e Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" <219766164+opencode-agent[bot]@users.noreply.github.com> Date: Fri, 7 Aug 2026 03:06:23 +0000 Subject: [PATCH 18/34] fix(app): complete translation coverage (#40981) Co-authored-by: Hona <10430890+Hona@users.noreply.github.com> --- packages/app/AGENTS.md | 3 +++ .../app/src/components/dialog-usage-exceeded.tsx | 4 +++- packages/app/src/components/titlebar.tsx | 2 +- packages/app/src/i18n/ar.ts | 13 +++++++++++++ packages/app/src/i18n/az.ts | 13 +++++++++++++ packages/app/src/i18n/br.ts | 13 +++++++++++++ packages/app/src/i18n/bs.ts | 13 +++++++++++++ packages/app/src/i18n/da.ts | 13 +++++++++++++ packages/app/src/i18n/de.ts | 13 +++++++++++++ packages/app/src/i18n/en.ts | 1 + packages/app/src/i18n/es.ts | 13 +++++++++++++ packages/app/src/i18n/fi.ts | 13 +++++++++++++ packages/app/src/i18n/fr.ts | 13 +++++++++++++ packages/app/src/i18n/hi.ts | 13 +++++++++++++ packages/app/src/i18n/id.ts | 13 +++++++++++++ packages/app/src/i18n/it.ts | 13 +++++++++++++ packages/app/src/i18n/ja.ts | 13 +++++++++++++ packages/app/src/i18n/ko.ts | 13 +++++++++++++ packages/app/src/i18n/nl.ts | 13 +++++++++++++ packages/app/src/i18n/no.ts | 13 +++++++++++++ packages/app/src/i18n/pa.ts | 13 +++++++++++++ packages/app/src/i18n/parity.test.ts | 2 +- packages/app/src/i18n/pl.ts | 13 +++++++++++++ packages/app/src/i18n/ru.ts | 13 +++++++++++++ packages/app/src/i18n/sv.ts | 13 +++++++++++++ packages/app/src/i18n/th.ts | 13 +++++++++++++ packages/app/src/i18n/tr.ts | 13 +++++++++++++ packages/app/src/i18n/uk.ts | 13 +++++++++++++ packages/app/src/i18n/ur.ts | 13 +++++++++++++ packages/app/src/i18n/vi.ts | 13 +++++++++++++ packages/app/src/i18n/zh.ts | 13 +++++++++++++ packages/app/src/i18n/zht.ts | 13 +++++++++++++ packages/desktop/AGENTS.md | 2 ++ 33 files changed, 362 insertions(+), 3 deletions(-) diff --git a/packages/app/AGENTS.md b/packages/app/AGENTS.md index 2f587ea73d53..4972f2e3a6cc 100644 --- a/packages/app/AGENTS.md +++ b/packages/app/AGENTS.md @@ -24,6 +24,9 @@ - NEVER hardcode user-visible English strings in production code. ALWAYS use an i18n key for visible copy, placeholders, accessible labels, tooltips, menus, dialogs, toasts, empty states, and displayed errors. - When migrating existing copy to i18n, preserve the English text byte-for-byte unless the task explicitly requests a copy change. - NEVER change existing English text or English keys to facilitate translation. English is intentional, designer-written source copy; adapt locale-specific translations and i18n mechanics around it. +- Keep locale complexity behind the shared typed i18n APIs. Feature and component code should use `language.t(...)` for ordinary copy and `language.plural(baseKey, count, params)` for count-sensitive copy. It must not inspect the locale, call `Intl.PluralRules`, construct or select plural-category keys such as `.one` or `.other`, or branch on locale-specific grammar. +- Prefer complete translated phrases. Do not concatenate grammatical fragments or make call sites assemble sentences. Keep placeholders to irreducible dynamic values such as names, paths, and counts. +- If a translation cannot be expressed by the current API, deepen the shared language/UI i18n module so one typed call owns locale selection, plural resolution, fallback, and interpolation. Do not leak that machinery into product code. - Do not translate from model knowledge alone. Verify terminology and grammar with Unicode CLDR locale/plural data, Microsoft Localization Style Guides and terminology, Apple localization/style guidance and localized platform UI, Mozilla localization style guides, Mozilla Pontoon, and the Firefox localization corpus at `github.com/mozilla-l10n/firefox-l10n`. - Also use the relevant language authority or official dictionary for the locale (for example RAE/Fundéu, FranceTerme, Duden, TDK, Kotus/Kielitoimiston sanakirja, Språkrådet/Bokmålsordboka, Rada Języka Polskiego/PWN, the Russian and Arabic language academies, the Ukrainian Orthography, Taiwan MOE dictionaries, or the Royal Society of Thailand). Treat the English dictionary as the semantic source of truth and preserve placeholders, code identifiers, product names, and keyboard labels. diff --git a/packages/app/src/components/dialog-usage-exceeded.tsx b/packages/app/src/components/dialog-usage-exceeded.tsx index bf5da751e225..de6451a63b6a 100644 --- a/packages/app/src/components/dialog-usage-exceeded.tsx +++ b/packages/app/src/components/dialog-usage-exceeded.tsx @@ -1,4 +1,5 @@ import { usePlatform } from "@/context/platform" +import { useLanguage } from "@/context/language" import { Button } from "@opencode-ai/ui/button" import { useDialog } from "@opencode-ai/ui/context/dialog" import { Dialog } from "@opencode-ai/ui/dialog" @@ -14,6 +15,7 @@ export type DialogGoUpsellProps = { export function DialogUsageExceeded(props: DialogGoUpsellProps) { const dialog = useDialog() + const language = useLanguage() const platform = usePlatform() const runAction = () => { @@ -32,7 +34,7 @@ export function DialogUsageExceeded(props: DialogGoUpsellProps) {