From 4a57013cf8cb163f58638273fd9da8538cd33cb7 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" <219766164+opencode-agent[bot]@users.noreply.github.com> Date: Wed, 5 Aug 2026 08:25:06 +0000 Subject: [PATCH 01/16] fix(app): show pending tool details (#40603) Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com> --- packages/session-ui/src/components/basic-tool.tsx | 2 +- packages/session-ui/src/components/message-part.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/session-ui/src/components/basic-tool.tsx b/packages/session-ui/src/components/basic-tool.tsx index 2b73db24ed1..efc15c173f9 100644 --- a/packages/session-ui/src/components/basic-tool.tsx +++ b/packages/session-ui/src/components/basic-tool.tsx @@ -204,7 +204,7 @@ export function BasicTool(props: BasicToolProps) { > - + - + {trigger().subtitle} - + {(arg) => {arg}} From f929f8f100581a94f3484b6992d02d57d75fab7f Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Wed, 5 Aug 2026 13:05:19 -0500 Subject: [PATCH 02/16] refactor(opencode): simplify retry error matching (#40694) --- packages/opencode/src/session/retry.ts | 37 +++++++------------- packages/opencode/test/session/retry.test.ts | 9 +++-- 2 files changed, 19 insertions(+), 27 deletions(-) diff --git a/packages/opencode/src/session/retry.ts b/packages/opencode/src/session/retry.ts index 4139665bd2b..d1864cb7a8a 100644 --- a/packages/opencode/src/session/retry.ts +++ b/packages/opencode/src/session/retry.ts @@ -122,32 +122,19 @@ export function retryable(error: Err, provider: string) { return { message: error.data.message.includes("Overloaded") ? "Provider is overloaded" : error.data.message } } - // Check for rate limit patterns in plain text error messages - const msg = isRecord(error.data) ? error.data.message : undefined - if (typeof msg === "string") { - const lower = msg.toLowerCase() - if ( - lower.includes("rate increased too quickly") || - lower.includes("rate limit") || - lower.includes("too many requests") - ) { - return { message: msg } - } - } - - const json = parseJSON(msg) - if (!json || typeof json !== "object") return undefined - const code = typeof json.code === "string" ? json.code : "" - - if (json.type === "error" && json.error?.type === "too_many_requests") { - return { message: "Too Many Requests" } - } - if (code.includes("exhausted") || code.includes("unavailable")) { - return { message: "Provider is overloaded" } - } - if (json.type === "error" && typeof json.error?.code === "string" && json.error.code.includes("rate_limit")) { - return { message: "Rate Limited" } + const message = isRecord(error.data) ? error.data.message : undefined + if (typeof message !== "string") return undefined + const lower = message.toLowerCase() + if ( + lower.includes("rate increased too quickly") || + lower.includes("rate limit") || + lower.includes("rate_limit") || + lower.includes("too many requests") + ) { + return { message } } + if (lower.includes("too_many_requests")) return { message: "Too Many Requests" } + if (lower.includes("exhausted") || lower.includes("unavailable")) return { message: "Provider is overloaded" } return undefined } diff --git a/packages/opencode/test/session/retry.test.ts b/packages/opencode/test/session/retry.test.ts index 30ac879a6a9..0e30a5473a2 100644 --- a/packages/opencode/test/session/retry.test.ts +++ b/packages/opencode/test/session/retry.test.ts @@ -118,16 +118,21 @@ describe("session.retry.delay", () => { }) describe("session.retry.retryable", () => { - test("maps too_many_requests json messages", () => { + test("retries serialized too_many_requests messages", () => { const error = wrap(JSON.stringify({ type: "error", error: { type: "too_many_requests" } })) expect(SessionRetry.retryable(error, retryProvider)).toEqual({ message: "Too Many Requests" }) }) - test("maps overloaded provider codes", () => { + test("retries serialized overloaded provider codes", () => { const error = wrap(JSON.stringify({ code: "resource_exhausted" })) expect(SessionRetry.retryable(error, retryProvider)).toEqual({ message: "Provider is overloaded" }) }) + test("retries serialized rate_limit messages", () => { + const message = JSON.stringify({ type: "error", error: { code: "rate_limit_exceeded" } }) + expect(SessionRetry.retryable(wrap(message), retryProvider)).toEqual({ message }) + }) + test("does not retry unknown json messages", () => { const error = wrap(JSON.stringify({ error: { message: "no_kv_space" } })) expect(SessionRetry.retryable(error, retryProvider)).toBeUndefined() From 61aefc07593043a2cef6cc870f7267b09483f5c0 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Wed, 5 Aug 2026 14:39:49 -0500 Subject: [PATCH 03/16] fix(opencode): expand retryable error patterns (#40707) --- packages/opencode/src/session/retry.ts | 29 ++++++++++----- packages/opencode/test/session/retry.test.ts | 39 ++++++++++++++++++++ 2 files changed, 59 insertions(+), 9 deletions(-) diff --git a/packages/opencode/src/session/retry.ts b/packages/opencode/src/session/retry.ts index d1864cb7a8a..22399e8703a 100644 --- a/packages/opencode/src/session/retry.ts +++ b/packages/opencode/src/session/retry.ts @@ -28,6 +28,15 @@ export const RETRY_BACKOFF_FACTOR = 2 export const RETRY_MAX_DELAY_NO_HEADERS = 30_000 // 30 seconds export const RETRY_MAX_DELAY = 2_147_483_647 // max 32-bit signed integer for setTimeout +const RETRYABLE_MESSAGE_PATTERNS = [ + /429|500|502|503|504|524/i, + /rate increased too quickly|rate limit|rate-limit|rate_limit|too many requests/i, + /overloaded|service unavailable|service_unavailable|service-unavailable|internal error|internal_error|internal server error|server error|server_error|server-error|provider returned error|provider_returned_error|provider-returned-error/i, + /terminated|fetch failed|failed to fetch|network error|upstream connect|connection error|connection refused|connection lost|socket connection was closed|socket hang up|reset before headers|getaddrinfo|enotfound|eai_again|econnrefused|econnreset|etimedout/i, + /^timeout$|\b(?:request|response|connection|network|stream|read) (?:timeout|timed out|time out)\b/i, + /try your request again|retry your request|resource exhausted|resource_exhausted/i, +] + function cap(ms: number) { return Math.min(ms, RETRY_MAX_DELAY) } @@ -72,7 +81,12 @@ export function retryable(error: Err, provider: string) { const status = error.data.statusCode // 5xx errors are transient server failures and should always be retried, // even when the provider SDK doesn't explicitly mark them as retryable. - if (!error.data.isRetryable && !(status !== undefined && status >= 500)) return undefined + if ( + !error.data.isRetryable && + !(status !== undefined && status >= 500) && + !matchesRetryableMessage(error.data.message) && + !matchesRetryableMessage(error.data.responseBody) + ) return undefined if (error.data.responseBody?.includes("FreeUsageLimitError")) { return { message: GO_UPSELL_MESSAGE, @@ -125,19 +139,16 @@ export function retryable(error: Err, provider: string) { const message = isRecord(error.data) ? error.data.message : undefined if (typeof message !== "string") return undefined const lower = message.toLowerCase() - if ( - lower.includes("rate increased too quickly") || - lower.includes("rate limit") || - lower.includes("rate_limit") || - lower.includes("too many requests") - ) { - return { message } - } if (lower.includes("too_many_requests")) return { message: "Too Many Requests" } if (lower.includes("exhausted") || lower.includes("unavailable")) return { message: "Provider is overloaded" } + if (matchesRetryableMessage(message)) return { message } return undefined } +function matchesRetryableMessage(value: unknown) { + return typeof value === "string" && RETRYABLE_MESSAGE_PATTERNS.some((pattern) => pattern.test(value)) +} + function str(value: unknown) { if (value === undefined || value === null) return "" return String(value) diff --git a/packages/opencode/test/session/retry.test.ts b/packages/opencode/test/session/retry.test.ts index 0e30a5473a2..018f76fc3ea 100644 --- a/packages/opencode/test/session/retry.test.ts +++ b/packages/opencode/test/session/retry.test.ts @@ -168,6 +168,45 @@ describe("session.retry.retryable", () => { expect(SessionRetry.retryable(error, retryProvider)).toEqual({ message: msg }) }) + test.each([ + "Internal server error", + "internal error", + "server-error", + "Provider returned error", + "provider-returned-error", + "terminated", + "fetch failed", + "connection refused", + "connect ECONNREFUSED", + "request ETIMEDOUT", + "failed to fetch", + "EAI_AGAIN", + "response timed out", + "Please retry your request", + "try your request again", + "upstream returned status 524", + ])("retries matching API error text: %s", (message) => { + expect(SessionRetry.retryable(wrap(message), retryProvider)).toEqual({ message }) + }) + + test("retries hyphenated service-unavailable errors", () => { + expect(SessionRetry.retryable(wrap("service-unavailable"), retryProvider)).toEqual({ + message: "Provider is overloaded", + }) + }) + + test("matches retryable API response bodies", () => { + const error = Schema.decodeUnknownSync(SessionV1.APIError.Schema)( + new SessionV1.APIError({ + message: "Request failed", + isRetryable: false, + statusCode: 400, + responseBody: JSON.stringify({ error: { message: "upstream connection refused" } }), + }).toObject(), + ) + expect(SessionRetry.retryable(error, retryProvider)).toEqual({ message: "Request failed" }) + }) + test("retries transport timeout errors", () => { const request = MessageV2.fromError(new ProviderError.HeaderTimeoutError(10000), { providerID }) expect(SessionV1.APIError.isInstance(request)).toBe(true) From 057b5a9dee0b151b18ff5a1164a3cf709389497a Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Wed, 5 Aug 2026 19:41:32 +0000 Subject: [PATCH 04/16] chore: generate --- packages/opencode/src/session/retry.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/session/retry.ts b/packages/opencode/src/session/retry.ts index 22399e8703a..cab48dda633 100644 --- a/packages/opencode/src/session/retry.ts +++ b/packages/opencode/src/session/retry.ts @@ -86,7 +86,8 @@ export function retryable(error: Err, provider: string) { !(status !== undefined && status >= 500) && !matchesRetryableMessage(error.data.message) && !matchesRetryableMessage(error.data.responseBody) - ) return undefined + ) + return undefined if (error.data.responseBody?.includes("FreeUsageLimitError")) { return { message: GO_UPSELL_MESSAGE, From b84c63d034c7acbe897bd6515b0042fab58770fc Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Wed, 5 Aug 2026 15:02:39 -0500 Subject: [PATCH 05/16] fix(stats): reduce html payloads --- .../stats/app/src/routes/[lab]/[model].tsx | 193 +++----- packages/stats/app/src/routes/[lab]/index.tsx | 79 ++-- .../stats/app/src/routes/compare-cards.tsx | 2 +- packages/stats/app/src/routes/geo-map.ts | 120 +++++ packages/stats/app/src/routes/index.tsx | 447 +++--------------- 5 files changed, 314 insertions(+), 527 deletions(-) create mode 100644 packages/stats/app/src/routes/geo-map.ts diff --git a/packages/stats/app/src/routes/[lab]/[model].tsx b/packages/stats/app/src/routes/[lab]/[model].tsx index 3b2bf341fdb..0b079e91513 100644 --- a/packages/stats/app/src/routes/[lab]/[model].tsx +++ b/packages/stats/app/src/routes/[lab]/[model].tsx @@ -1,23 +1,17 @@ import { Meta, Title } from "@solidjs/meta" import { ProviderIcon } from "@opencode-ai/ui/provider-icon" -import { geoEquirectangular, geoPath } from "d3-geo" import { scaleSqrt } from "d3-scale" import countryCodesSource from "i18n-iso-countries/codes.json?raw" -import { feature, mesh } from "topojson-client" -import countriesTopologySource from "world-atlas/countries-50m.json?raw" import { getStatsModelData, type CountryEntry, type ModelPeerEntry, type ModelUsagePoint, type StatsModelData, - type UsageRange, } from "@opencode-ai/stats-core/domain/home" import { createAsync, query, useParams } from "@solidjs/router" import { createMemo, createSignal, createUniqueId, For, onMount, Show, type JSX } from "solid-js" import { getRequestEvent } from "solid-js/web" -import type { FeatureCollection, GeometryObject, GeoJsonProperties } from "geojson" -import type { GeometryCollection, Topology } from "topojson-specification" import { LocaleLinks } from "../../component/locale-links" import { useI18n } from "../../context/i18n" import { useLanguage } from "../../context/language" @@ -25,10 +19,10 @@ import { localizedUrl } from "../../lib/language" import { findModelCatalogEntry, formatCatalogLabName, - getModelCatalog, - type ModelCatalog, + loadModelCatalog, type ModelCatalogEntry, } from "../model-catalog" +import { geoMapHeight, geoMapWidth, worldBorderPath, worldCountryMarkers, worldCountryPaths } from "../geo-map" import { SectionHeading } from "../section-heading" import { runStatsEffect } from "../../stats-runtime" import { setStatsPageCacheHeaders } from "../stats-cache" @@ -51,45 +45,41 @@ import { } from "../stats-shell" const statsUnfurlPath = "banner.png" -const geoMapWidth = 960 -const geoMapHeight = 430 const shortMonths = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"] as const type IsoCountryCode = readonly [string, string, string] -type WorldCountryProperties = GeoJsonProperties & { name?: string } -type WorldTopology = Topology<{ countries: GeometryCollection }> +type ModelCatalogOption = Pick +type ModelPageCatalog = { + entry: ModelCatalogEntry | null + labs: { id: string; name: string }[] + labModels: ModelCatalogOption[] +} +type StatsModelPageData = Omit & { country: CountryEntry[] } +type ModelPageData = { catalog: ModelPageCatalog; stats: StatsModelPageData | null } const countryNumericIds = new Map( (JSON.parse(countryCodesSource) as IsoCountryCode[]).map((country) => [country[0], country[2]] as const), ) -const worldTopology = JSON.parse(countriesTopologySource) as WorldTopology -const worldCountryGeometries: GeometryCollection = { - ...worldTopology.objects.countries, - geometries: worldTopology.objects.countries.geometries.filter((country) => String(country.id ?? "") !== "010"), -} -const worldCountries = feature(worldTopology, worldCountryGeometries) as FeatureCollection< - GeometryObject, - WorldCountryProperties -> -const worldProjection = geoEquirectangular().fitExtent( - [ - [10, 12], - [geoMapWidth - 10, geoMapHeight - 12], - ], - worldCountries, -) -const worldPath = geoPath(worldProjection) -const worldCountryPaths = worldCountries.features.map((country) => ({ - id: String(country.id ?? "").padStart(3, "0"), - path: worldPath(country) ?? "", - marker: geoCountryMarker(country), -})) -const worldBorderPath = worldPath(mesh(worldTopology, worldCountryGeometries, (a, b) => a !== b)) ?? "" -const getModelData = query(async (lab: string, model: string) => { +const getModelPageData = query(async (labParam: string, modelParam: string) => { "use server" - return runStatsEffect(getStatsModelData(model, lab)) -}, "getStatsModelData") + const catalog = await loadModelCatalog() + const entry = findModelCatalogEntry(catalog, modelParam, labParam) ?? null + const lab = entry?.lab ?? labParam + const model = entry?.slug ?? modelParam + const stats = lab && model ? await runStatsEffect(getStatsModelData(model, lab)) : null + return { + catalog: { + entry, + labs: catalog.labs.map((item) => ({ id: item.id, name: item.name })), + labModels: + catalog.labs + .find((item) => item.id === (entry?.lab ?? providerSlug(labParam))) + ?.models.map((item) => ({ id: item.id, lab: item.lab, slug: item.slug, name: item.name })) ?? [], + }, + stats: stats ? { ...stats, country: stats.country["2M"] } : null, + } satisfies ModelPageData +}, "getStatsModelPageData") export default function StatsModel() { const i18n = useI18n() @@ -99,18 +89,9 @@ export default function StatsModel() { const params = useParams() const labParam = createMemo(() => params.lab ?? "") const modelParam = createMemo(() => params.model ?? "") - const catalog = createAsync(() => getModelCatalog()) - const catalogEntry = createMemo(() => { - const data = catalog() - if (!data) return undefined - return findModelCatalogEntry(data, modelParam(), labParam()) ?? null - }) - const stats = createAsync(() => { - const entry = catalogEntry() - if (catalog() === undefined || entry === undefined) return Promise.resolve(undefined) - if (!entry && (!labParam() || !modelParam())) return Promise.resolve(null) - return getModelData(labParam(), entry?.slug ?? modelParam()) - }) + const page = createAsync(() => getModelPageData(labParam(), modelParam())) + const catalogEntry = createMemo(() => page()?.catalog.entry) + const stats = createMemo(() => page()?.stats) const githubStars = createAsync(() => getGitHubStars()) const [themePreference, setThemePreference] = createSignal("system") const modelName = createMemo(() => catalogEntry()?.name ?? stats()?.model ?? modelParam() ?? i18n.t("model.fallback")) @@ -179,13 +160,13 @@ export default function StatsModel() {
- }> + }> }> <> @@ -193,10 +174,10 @@ export default function StatsModel() { - + props.catalog?.name ?? props.data?.model ?? i18n.t("model.fallback") const weights = () => props.catalog?.weights[0] const labs = () => props.catalogData?.labs ?? [] - const labModels = () => - props.catalogData?.labs.find((lab) => lab.id === providerSlug(labId()))?.models ?? - (props.catalog ? [props.catalog] : []) + const labModels = () => props.catalogData?.labModels ?? (props.catalog ? [props.catalog] : []) return (
) } @@ -1853,12 +1576,10 @@ function MetricBar(props: { value: number; max: number; active: boolean }) { ) } -function SessionCostSection(props: { data: StatsHomeData["sessionCost"] }) { +function SessionCostSection(props: { data: SessionCostEntry[] }) { const i18n = useI18n() - const [product, setProduct] = createSignal("Go") const [activeIndex, setActiveIndex] = createSignal(2) - const data = createMemo(() => props.data[product()]) - const visible = createMemo(() => data().slice(0, 16)) + const visible = createMemo(() => props.data.slice(0, 16)) const selectedIndex = createMemo(() => Math.min(activeIndex(), Math.max(visible().length - 1, 0))) return ( @@ -1877,17 +1598,6 @@ function SessionCostSection(props: { data: StatsHomeData["sessionCost"] }) { >
- ) } @@ -1949,11 +1659,6 @@ function SessionCostChart(props: { ) } -function LiveIndicator() { - const i18n = useI18n() - return {i18n.t("chart.live")} -} - function formatTokenCount(value: number) { if (value >= 1_000_000) return `${Number((value / 1_000_000).toFixed(1))}M` return `${Math.round(value / 1_000)}K` From 3355b78d91876104085995ea5e9dc27ede28d21b Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Wed, 5 Aug 2026 20:06:05 +0000 Subject: [PATCH 06/16] chore: generate --- packages/stats/app/src/routes/[lab]/[model].tsx | 7 +------ packages/stats/app/src/routes/[lab]/index.tsx | 6 +----- packages/stats/app/src/routes/index.tsx | 7 +------ 3 files changed, 3 insertions(+), 17 deletions(-) diff --git a/packages/stats/app/src/routes/[lab]/[model].tsx b/packages/stats/app/src/routes/[lab]/[model].tsx index 0b079e91513..c9906a975e9 100644 --- a/packages/stats/app/src/routes/[lab]/[model].tsx +++ b/packages/stats/app/src/routes/[lab]/[model].tsx @@ -16,12 +16,7 @@ import { LocaleLinks } from "../../component/locale-links" import { useI18n } from "../../context/i18n" import { useLanguage } from "../../context/language" import { localizedUrl } from "../../lib/language" -import { - findModelCatalogEntry, - formatCatalogLabName, - loadModelCatalog, - type ModelCatalogEntry, -} from "../model-catalog" +import { findModelCatalogEntry, formatCatalogLabName, loadModelCatalog, type ModelCatalogEntry } from "../model-catalog" import { geoMapHeight, geoMapWidth, worldBorderPath, worldCountryMarkers, worldCountryPaths } from "../geo-map" import { SectionHeading } from "../section-heading" import { runStatsEffect } from "../../stats-runtime" diff --git a/packages/stats/app/src/routes/[lab]/index.tsx b/packages/stats/app/src/routes/[lab]/index.tsx index 8931c2af0cf..3d4a1a663c3 100644 --- a/packages/stats/app/src/routes/[lab]/index.tsx +++ b/packages/stats/app/src/routes/[lab]/index.tsx @@ -152,11 +152,7 @@ export default function StatsLab() { - + usageTotal(item) > 0)} fallback={} > - +
0} From 709c195905c260299facd5b566d9ab537c48c3bc Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Wed, 5 Aug 2026 15:14:03 -0500 Subject: [PATCH 07/16] fix(opencode): preserve compatible stream errors (#40718) --- bun.lock | 1 + package.json | 3 +- .../test/session/processor-effect.test.ts | 49 +++++++++++++++++++ .../@ai-sdk%2Fopenai-compatible@2.0.41.patch | 39 +++++++++++++++ 4 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 patches/@ai-sdk%2Fopenai-compatible@2.0.41.patch diff --git a/bun.lock b/bun.lock index 0d08cb8a95a..3ba15ef800b 100644 --- a/bun.lock +++ b/bun.lock @@ -1078,6 +1078,7 @@ "@ai-sdk/google@3.0.73": "patches/@ai-sdk%2Fgoogle@3.0.73.patch", "pacote@21.5.0": "patches/pacote@21.5.0.patch", "@dnd-kit/dom@0.5.0": "patches/@dnd-kit%2Fdom@0.5.0.patch", + "@ai-sdk/openai-compatible@2.0.41": "patches/@ai-sdk%2Fopenai-compatible@2.0.41.patch", }, "overrides": { "@opentui/core": "catalog:", diff --git a/package.json b/package.json index 15725c865fc..58712547b4b 100644 --- a/package.json +++ b/package.json @@ -158,6 +158,7 @@ "@pierre/trees@1.0.0-beta.4": "patches/@pierre%2Ftrees@1.0.0-beta.4.patch", "@modelcontextprotocol/sdk@1.29.0": "patches/@modelcontextprotocol%2Fsdk@1.29.0.patch", "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" + "@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" } } diff --git a/packages/opencode/test/session/processor-effect.test.ts b/packages/opencode/test/session/processor-effect.test.ts index 52876054365..d9466c0c2c9 100644 --- a/packages/opencode/test/session/processor-effect.test.ts +++ b/packages/opencode/test/session/processor-effect.test.ts @@ -604,6 +604,55 @@ it.live("session.processor effect tests retry recognized structured json errors" ), ) +it.live("session.processor effect tests retry OpenAI-compatible midstream server errors", () => + provideTmpdirServer( + ({ dir, llm }) => + 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.text("after") + + const chat = yield* session.create({}) + const parent = yield* user(chat.id, "retry midstream server error") + const msg = yield* assistant(chat.id, parent.id, path.resolve(dir)) + const mdl = yield* provider.getModel(ref.providerID, ref.modelID) + const handle = yield* processors.create({ + assistantMessage: msg, + sessionID: chat.id, + model: mdl, + }) + + const value = yield* handle.process({ + user: { + id: parent.id, + sessionID: chat.id, + role: "user", + time: parent.time, + agent: parent.agent, + model: { providerID: ref.providerID, modelID: ref.modelID }, + } satisfies SessionV1.User, + sessionID: chat.id, + model: mdl, + agent: agent(), + system: [], + messages: [{ role: "user", content: "retry midstream server error" }], + tools: {}, + }) + + const parts = yield* MessageV2.parts(msg.id) + + expect(value).toBe("continue") + expect(yield* llm.calls).toBe(2) + expect(parts.some((part) => part.type === "text" && part.text === "after")).toBe(true) + expect(handle.message.error).toBeUndefined() + }), + { config: (url) => providerCfg(url) }, + ), +) + it.live("session.processor effect tests publish retry status updates", () => provideTmpdirServer( ({ dir, llm }) => diff --git a/patches/@ai-sdk%2Fopenai-compatible@2.0.41.patch b/patches/@ai-sdk%2Fopenai-compatible@2.0.41.patch new file mode 100644 index 00000000000..9f03ec95732 --- /dev/null +++ b/patches/@ai-sdk%2Fopenai-compatible@2.0.41.patch @@ -0,0 +1,39 @@ +diff --git a/dist/index.js b/dist/index.js +index dca128d3a790378c51a24a16d92585178343b278..da75f9d64acd2b607abd15079ce2d03b7a8d675a 100644 +--- a/dist/index.js ++++ b/dist/index.js +@@ -696,7 +696,7 @@ var OpenAICompatibleChatLanguageModel = class { + finishReason = { unified: "error", raw: void 0 }; + controller.enqueue({ + type: "error", +- error: chunk.value.error.message ++ error: chunk.value.error + }); + return; + } +diff --git a/dist/index.mjs b/dist/index.mjs +index 3b1e1b6bdec5032e3b4fa5ffbcc8cdf3dfe1cc40..eaffc446f80552ea0b26573b89daa4dfc7776e6e 100644 +--- a/dist/index.mjs ++++ b/dist/index.mjs +@@ -683,7 +683,7 @@ var OpenAICompatibleChatLanguageModel = class { + finishReason = { unified: "error", raw: void 0 }; + controller.enqueue({ + type: "error", +- error: chunk.value.error.message ++ error: chunk.value.error + }); + return; + } +diff --git a/src/chat/openai-compatible-chat-language-model.ts b/src/chat/openai-compatible-chat-language-model.ts +index 8c622db23c2d9a7373701f5a1b0c2ba109e24602..643c3db68a6043e097edc1122e0eb53fd13495c5 100644 +--- a/src/chat/openai-compatible-chat-language-model.ts ++++ b/src/chat/openai-compatible-chat-language-model.ts +@@ -442,7 +442,7 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV3 { + finishReason = { unified: 'error', raw: undefined }; + controller.enqueue({ + type: 'error', +- error: chunk.value.error.message, ++ error: chunk.value.error, + }); + return; + } From 082fe93e160c042332b3686a006d0f35ce4a6d6e Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Wed, 5 Aug 2026 20:16:40 +0000 Subject: [PATCH 08/16] 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 d9466c0c2c9..052477d0a2e 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 09/16] 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 83bc08a630e..6f321d88bf2 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 10/16] 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 3cec2cba04d..1ea92a46e93 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 47cc07b9295..10a10558008 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 c378688490c..39877378a43 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 28e5cf59a33..398f53f7186 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 c37c9c21161..26720c8ac0d 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 7d22ce5e166..e69623179fa 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 12dd96a5e66..cfc302e7753 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 00000000000..ff18a16b7fa --- /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 00000000000..6eb9f9ab6a7 --- /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 16/16] 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 c08c202fa2e..2f37c33fb37 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")}
-