From 3e931152d129669530ea97b80dba64eeb310f0bc Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Thu, 21 May 2026 22:22:51 -0500 Subject: [PATCH 0001/1395] tweak(tui): remove italics from thinking labels (#28737) --- packages/opencode/src/cli/cmd/tui/context/theme.tsx | 5 ++++- .../src/cli/cmd/tui/routes/session/index.tsx | 12 ++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/context/theme.tsx b/packages/opencode/src/cli/cmd/tui/context/theme.tsx index 3829f4488c3a..33dfd3056db6 100644 --- a/packages/opencode/src/cli/cmd/tui/context/theme.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/theme.tsx @@ -49,6 +49,7 @@ type Theme = TuiThemeCurrent & { _hasSelectedListItemText: boolean } type ThemeColor = Exclude +type SyntaxStyleOverrides = Record export function selectedForeground(theme: Theme, bg?: RGBA): RGBA { // If theme explicitly defines selectedListItemText, use it @@ -718,16 +719,18 @@ export function generateSyntax(theme: Theme) { return SyntaxStyle.fromTheme(getSyntaxRules(theme)) } -export function generateSubtleSyntax(theme: Theme) { +export function generateSubtleSyntax(theme: Theme, overrides?: SyntaxStyleOverrides) { const rules = getSyntaxRules(theme) return SyntaxStyle.fromTheme( rules.map((rule) => { + const override = rule.scope.reduce((acc, scope) => ({ ...acc, ...overrides?.[scope] }), {}) if (rule.style.foreground) { const fg = rule.style.foreground return { ...rule, style: { ...rule.style, + ...override, foreground: RGBA.fromInts( Math.round(fg.r * 255), Math.round(fg.g * 255), diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index d10b01670ce5..ae23d58bcc95 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -21,7 +21,7 @@ import { useSync } from "@tui/context/sync" import { useEvent } from "@tui/context/event" import { SplitBorder } from "@tui/component/border" import { Spinner } from "@tui/component/spinner" -import { selectedForeground, useTheme } from "@tui/context/theme" +import { generateSubtleSyntax, selectedForeground, useTheme } from "@tui/context/theme" import { BoxRenderable, ScrollBoxRenderable, addDefaultParsers, TextAttributes, RGBA } from "@opentui/core" import { Prompt, type PromptRef } from "@tui/component/prompt" import type { @@ -1497,7 +1497,7 @@ const PART_MAPPING = { } function ReasoningPart(props: { last: boolean; part: ReasoningPart; message: AssistantMessage }) { - const { theme, subtleSyntax } = useTheme() + const { theme } = useTheme() const ctx = use() // Collapsed by default in hide mode: a single line throughout, so the // layout never shifts. Click to open the full markdown block, click to close. @@ -1519,6 +1519,8 @@ function ReasoningPart(props: { last: boolean; part: ReasoningPart; message: Ass // blocks. Surface the title both while streaming and after settling so the // collapsed line carries real signal, not just a duration. const title = createMemo(() => reasoningTitle(content())) + // Keep markdown emphasis for the existing thinking color/concealment, but render it without italics. + const syntax = createMemo(() => generateSubtleSyntax(theme, { "markup.italic": { italic: false } })) const toggle = () => { if (!inMinimal()) return @@ -1535,7 +1537,9 @@ function ReasoningPart(props: { last: boolean; part: ReasoningPart; message: Ass filetype="markdown" drawUnstyledText={false} streaming={true} - syntaxStyle={subtleSyntax()} + syntaxStyle={syntax()} + // `_Thinking:_`/`_Thought:_` still drives markdown emphasis color and conceals the underscores; + // the syntax override above removes only the italic attribute from that emphasis token. content={(inMinimal() ? "- " : "") + (isDone() ? "_Thought:_ " : "_Thinking:_ ") + content()} conceal={ctx.conceal()} fg={theme.textMuted} @@ -1563,7 +1567,7 @@ function CollapsedReasoningText(props: { title: string | null; duration: number return ( - + {props.title ? "+ Thought: " + props.title + " · " + duration() : "+ Thought: " + duration()} From 7a9724496b0f255fa8da13db58cb4ebb14841860 Mon Sep 17 00:00:00 2001 From: JPFrancoia Date: Fri, 22 May 2026 05:03:24 +0100 Subject: [PATCH 0002/1395] fix(vertex): Vertex (Antropic) provider: use .rep.googleapis.com for continental multi-region endpoints (us, eu) (#28347) Co-authored-by: Aiden Cline --- .../core/src/plugin/provider/google-vertex.ts | 26 ++++--- .../provider-google-vertex-anthropic.test.ts | 69 ++++++++++++++++++- .../plugin/provider-google-vertex.test.ts | 26 +++++++ packages/opencode/src/provider/provider.ts | 21 ++++++ .../opencode/test/provider/provider.test.ts | 50 ++++++++++++++ 5 files changed, 182 insertions(+), 10 deletions(-) diff --git a/packages/core/src/plugin/provider/google-vertex.ts b/packages/core/src/plugin/provider/google-vertex.ts index eaf24e00b602..299315274802 100644 --- a/packages/core/src/plugin/provider/google-vertex.ts +++ b/packages/core/src/plugin/provider/google-vertex.ts @@ -25,7 +25,8 @@ function resolveLocation(options: Record) { } function vertexEndpoint(location: string) { - return location === "global" ? "aiplatform.googleapis.com" : `${location}-aiplatform.googleapis.com` + if (location === "global") return "aiplatform.googleapis.com" + return `${location}-aiplatform.googleapis.com` } function replaceVertexVars(value: string, project: string | undefined, location: string) { @@ -131,16 +132,23 @@ export const GoogleVertexAnthropicPlugin = PluginV2.define({ "aisdk.sdk": Effect.fn(function* (evt) { if (evt.package !== "@ai-sdk/google-vertex/anthropic") return const mod = yield* Effect.promise(() => import("@ai-sdk/google-vertex/anthropic")) + const project = + typeof evt.options.project === "string" + ? evt.options.project + : (process.env.GOOGLE_CLOUD_PROJECT ?? process.env.GCP_PROJECT ?? process.env.GCLOUD_PROJECT) + const location = + typeof evt.options.location === "string" + ? evt.options.location + : (process.env.GOOGLE_CLOUD_LOCATION ?? process.env.VERTEX_LOCATION ?? "global") evt.sdk = mod.createVertexAnthropic({ ...evt.options, - project: - typeof evt.options.project === "string" - ? evt.options.project - : (process.env.GOOGLE_CLOUD_PROJECT ?? process.env.GCP_PROJECT ?? process.env.GCLOUD_PROJECT), - location: - typeof evt.options.location === "string" - ? evt.options.location - : (process.env.GOOGLE_CLOUD_LOCATION ?? process.env.VERTEX_LOCATION ?? "global"), + project, + location, + // Continental multi-regions (eu, us) require Regional Endpoint Platform + // domains; the default {region}-aiplatform.googleapis.com does not resolve. + ...((location === "eu" || location === "us") && project && !evt.options.baseURL + ? { baseURL: `https://aiplatform.${location}.rep.googleapis.com/v1/projects/${project}/locations/${location}/publishers/anthropic/models` } + : {}), }) }), "aisdk.language": Effect.fn(function* (evt) { diff --git a/packages/core/test/plugin/provider-google-vertex-anthropic.test.ts b/packages/core/test/plugin/provider-google-vertex-anthropic.test.ts index 85a11baf82ac..7cb2b24ffd68 100644 --- a/packages/core/test/plugin/provider-google-vertex-anthropic.test.ts +++ b/packages/core/test/plugin/provider-google-vertex-anthropic.test.ts @@ -2,7 +2,7 @@ import { describe, expect } from "bun:test" import { Effect } from "effect" import { Catalog } from "@opencode-ai/core/catalog" import { PluginV2 } from "@opencode-ai/core/plugin" -import { GoogleVertexAnthropicPlugin } from "@opencode-ai/core/plugin/provider/google-vertex" +import { GoogleVertexAnthropicPlugin, GoogleVertexPlugin } from "@opencode-ai/core/plugin/provider/google-vertex" import { ProviderV2 } from "@opencode-ai/core/provider" import { fakeSelectorSdk, it, model, withEnv } from "./provider-helper" @@ -109,6 +109,73 @@ describe("GoogleVertexAnthropicPlugin", () => { ), ) + it.effect("creates SDKs for google-vertex Anthropic models with multi-region endpoints", () => + Effect.gen(function* () { + const plugin = yield* PluginV2.Service + yield* plugin.add(GoogleVertexAnthropicPlugin) + const result = yield* plugin.trigger( + "aisdk.sdk", + { + model: model("google-vertex", "claude-sonnet-4-5"), + package: "@ai-sdk/google-vertex/anthropic", + options: { name: "google-vertex", project: "project", location: "eu" }, + }, + {}, + ) + expect(result.sdk.languageModel("claude-sonnet-4-5").config.baseURL).toBe( + "https://aiplatform.eu.rep.googleapis.com/v1/projects/project/locations/eu/publishers/anthropic/models", + ) + }), + ) + + it.effect("keeps configured baseURL for google-vertex Anthropic models", () => + Effect.gen(function* () { + const plugin = yield* PluginV2.Service + yield* plugin.add(GoogleVertexAnthropicPlugin) + const result = yield* plugin.trigger( + "aisdk.sdk", + { + model: model("google-vertex", "claude-sonnet-4-5"), + package: "@ai-sdk/google-vertex/anthropic", + options: { name: "google-vertex", project: "project", location: "eu", baseURL: "https://proxy.example/v1" }, + }, + {}, + ) + expect(result.sdk.languageModel("claude-sonnet-4-5").config.baseURL).toBe("https://proxy.example/v1") + }), + ) + + it.effect("selects google-vertex Anthropic language models through V2 plugins", () => + Effect.gen(function* () { + const plugin = yield* PluginV2.Service + yield* plugin.add(GoogleVertexPlugin) + yield* plugin.add(GoogleVertexAnthropicPlugin) + const sdkResult = yield* plugin.trigger( + "aisdk.sdk", + { + model: model("google-vertex", " claude-sonnet-4-5 "), + package: "@ai-sdk/google-vertex/anthropic", + options: { name: "google-vertex", project: "project", location: "us" }, + }, + {}, + ) + const languageResult = yield* plugin.trigger( + "aisdk.language", + { + model: model("google-vertex", " claude-sonnet-4-5 "), + sdk: sdkResult.sdk, + options: {}, + }, + {}, + ) + const language = languageResult.language as unknown as { config: { baseURL: string }; modelId: string } + expect(language.config.baseURL).toBe( + "https://aiplatform.us.rep.googleapis.com/v1/projects/project/locations/us/publishers/anthropic/models", + ) + expect(language.modelId).toBe("claude-sonnet-4-5") + }), + ) + it.effect("trims model IDs before selecting language models", () => Effect.gen(function* () { const plugin = yield* PluginV2.Service diff --git a/packages/core/test/plugin/provider-google-vertex.test.ts b/packages/core/test/plugin/provider-google-vertex.test.ts index 4c4e19ee8562..2a9a18875c28 100644 --- a/packages/core/test/plugin/provider-google-vertex.test.ts +++ b/packages/core/test/plugin/provider-google-vertex.test.ts @@ -160,6 +160,32 @@ describe("GoogleVertexPlugin", () => { ), ) + it.effect("keeps OpenAI-compatible Vertex endpoint templates regional for eu", () => + Effect.gen(function* () { + const plugin = yield* PluginV2.Service + const catalog = yield* Catalog.Service + yield* plugin.add(GoogleVertexPlugin) + const load = yield* catalog.loader() + yield* load((catalog) => + catalog.provider.update(ProviderV2.ID.make("google-vertex"), (provider) => { + provider.endpoint = { + type: "aisdk", + package: "@ai-sdk/openai-compatible", + url: "https://${GOOGLE_VERTEX_ENDPOINT}/v1/projects/${GOOGLE_VERTEX_PROJECT}/locations/${GOOGLE_VERTEX_LOCATION}", + } + provider.options.aisdk.provider.project = "config-project" + provider.options.aisdk.provider.location = "eu" + }), + ) + const provider = yield* catalog.provider.get(ProviderV2.ID.make("google-vertex")) + expect(provider.endpoint).toEqual({ + type: "aisdk", + package: "@ai-sdk/openai-compatible", + url: "https://eu-aiplatform.googleapis.com/v1/projects/config-project/locations/eu", + }) + }), + ) + it.effect("defaults location to us-central1 when only project is configured", () => withEnv( { diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 25180215291c..2fe2b7a4a549 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -85,6 +85,13 @@ function wrapSSE(res: Response, ms: number, ctl: AbortController) { }) } +function googleVertexAnthropicBaseURL(project: string | undefined, location: string | undefined) { + if (!project) return + if (location !== "eu" && location !== "us") return + // Continental multi-regions require Regional Endpoint Platform domains. + return `https://aiplatform.${location}.rep.googleapis.com/v1/projects/${project}/locations/${location}/publishers/anthropic/models` +} + type BundledSDK = { languageModel(modelId: string): LanguageModelV3 } @@ -507,11 +514,13 @@ function custom(dep: CustomDep): Record { const location = env["GOOGLE_CLOUD_LOCATION"] ?? env["VERTEX_LOCATION"] ?? "global" const autoload = Boolean(project) if (!autoload) return { autoload: false } + const baseURL = googleVertexAnthropicBaseURL(project, location) return { autoload: true, options: { project, location, + ...(baseURL && { baseURL }), }, async getModel(sdk: any, modelID) { const id = String(modelID).trim() @@ -1516,6 +1525,18 @@ export const layer = Layer.effect( const provider = s.providers[model.providerID] const options = { ...provider.options } + if ( + model.providerID === "google-vertex" && + model.api.npm === "@ai-sdk/google-vertex/anthropic" && + !options.baseURL + ) { + const baseURL = googleVertexAnthropicBaseURL( + typeof options.project === "string" ? options.project : undefined, + typeof options.location === "string" ? options.location : undefined, + ) + if (baseURL) options.baseURL = baseURL + } + if (model.providerID === "google-vertex" && !model.api.npm.includes("@ai-sdk/openai-compatible")) { delete options.fetch } diff --git a/packages/opencode/test/provider/provider.test.ts b/packages/opencode/test/provider/provider.test.ts index 069c2f5ca381..3a2d34f560f5 100644 --- a/packages/opencode/test/provider/provider.test.ts +++ b/packages/opencode/test/provider/provider.test.ts @@ -73,6 +73,8 @@ const paid = (providers: Record model.cost.input > 0).length } +const languageBaseURL = (language: unknown) => (language as { config: { baseURL: string } }).config.baseURL + const it = testEffect(Layer.mergeAll(Provider.defaultLayer, Env.defaultLayer, Plugin.defaultLayer)) const experimentalModels = testEffect(providerLayer({ enableExperimentalModels: true })) @@ -1546,6 +1548,54 @@ it.instance( }, ) +it.instance("Google Vertex: uses REP endpoint for Claude continental multi-regions", () => + Effect.gen(function* () { + yield* set("GOOGLE_CLOUD_PROJECT", "test-project") + yield* set("VERTEX_LOCATION", "eu") + const provider = yield* Provider.Service + const model = yield* provider.getModel( + ProviderID.make("google-vertex"), + ModelID.make("claude-sonnet-4-6@default"), + ) + const language = yield* provider.getLanguage(model) + expect(languageBaseURL(language)).toBe( + "https://aiplatform.eu.rep.googleapis.com/v1/projects/test-project/locations/eu/publishers/anthropic/models", + ) + }), +) + +it.instance("Google Vertex Anthropic: uses REP endpoint for continental multi-regions", () => + Effect.gen(function* () { + yield* set("GOOGLE_CLOUD_PROJECT", "test-project") + yield* set("VERTEX_LOCATION", "us") + const provider = yield* Provider.Service + const model = yield* provider.getModel( + ProviderID.make("google-vertex-anthropic"), + ModelID.make("claude-sonnet-4-6@default"), + ) + const language = yield* provider.getLanguage(model) + expect(languageBaseURL(language)).toBe( + "https://aiplatform.us.rep.googleapis.com/v1/projects/test-project/locations/us/publishers/anthropic/models", + ) + }), +) + +it.instance("Google Vertex: keeps regional Claude endpoints unchanged", () => + Effect.gen(function* () { + yield* set("GOOGLE_CLOUD_PROJECT", "test-project") + yield* set("VERTEX_LOCATION", "europe-west1") + const provider = yield* Provider.Service + const model = yield* provider.getModel( + ProviderID.make("google-vertex"), + ModelID.make("claude-sonnet-4-6@default"), + ) + const language = yield* provider.getLanguage(model) + expect(languageBaseURL(language)).toBe( + "https://europe-west1-aiplatform.googleapis.com/v1/projects/test-project/locations/europe-west1/publishers/anthropic/models", + ) + }), +) + it.instance("cloudflare-ai-gateway loads with env variables", () => Effect.gen(function* () { yield* set("CLOUDFLARE_ACCOUNT_ID", "test-account") From 9f06accfb4a33638fb05f36cd2597a916040a1d2 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Fri, 22 May 2026 04:04:41 +0000 Subject: [PATCH 0003/1395] chore: generate --- packages/core/src/plugin/provider/google-vertex.ts | 4 +++- packages/opencode/test/provider/provider.test.ts | 10 ++-------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/packages/core/src/plugin/provider/google-vertex.ts b/packages/core/src/plugin/provider/google-vertex.ts index 299315274802..3da7caf4e744 100644 --- a/packages/core/src/plugin/provider/google-vertex.ts +++ b/packages/core/src/plugin/provider/google-vertex.ts @@ -147,7 +147,9 @@ export const GoogleVertexAnthropicPlugin = PluginV2.define({ // Continental multi-regions (eu, us) require Regional Endpoint Platform // domains; the default {region}-aiplatform.googleapis.com does not resolve. ...((location === "eu" || location === "us") && project && !evt.options.baseURL - ? { baseURL: `https://aiplatform.${location}.rep.googleapis.com/v1/projects/${project}/locations/${location}/publishers/anthropic/models` } + ? { + baseURL: `https://aiplatform.${location}.rep.googleapis.com/v1/projects/${project}/locations/${location}/publishers/anthropic/models`, + } : {}), }) }), diff --git a/packages/opencode/test/provider/provider.test.ts b/packages/opencode/test/provider/provider.test.ts index 3a2d34f560f5..8e3276dbabf3 100644 --- a/packages/opencode/test/provider/provider.test.ts +++ b/packages/opencode/test/provider/provider.test.ts @@ -1553,10 +1553,7 @@ it.instance("Google Vertex: uses REP endpoint for Claude continental multi-regio yield* set("GOOGLE_CLOUD_PROJECT", "test-project") yield* set("VERTEX_LOCATION", "eu") const provider = yield* Provider.Service - const model = yield* provider.getModel( - ProviderID.make("google-vertex"), - ModelID.make("claude-sonnet-4-6@default"), - ) + const model = yield* provider.getModel(ProviderID.make("google-vertex"), ModelID.make("claude-sonnet-4-6@default")) const language = yield* provider.getLanguage(model) expect(languageBaseURL(language)).toBe( "https://aiplatform.eu.rep.googleapis.com/v1/projects/test-project/locations/eu/publishers/anthropic/models", @@ -1585,10 +1582,7 @@ it.instance("Google Vertex: keeps regional Claude endpoints unchanged", () => yield* set("GOOGLE_CLOUD_PROJECT", "test-project") yield* set("VERTEX_LOCATION", "europe-west1") const provider = yield* Provider.Service - const model = yield* provider.getModel( - ProviderID.make("google-vertex"), - ModelID.make("claude-sonnet-4-6@default"), - ) + const model = yield* provider.getModel(ProviderID.make("google-vertex"), ModelID.make("claude-sonnet-4-6@default")) const language = yield* provider.getLanguage(model) expect(languageBaseURL(language)).toBe( "https://europe-west1-aiplatform.googleapis.com/v1/projects/test-project/locations/europe-west1/publishers/anthropic/models", From 1f0390cfbbd931297faa4dbc6d297ef2665a4a84 Mon Sep 17 00:00:00 2001 From: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Date: Fri, 22 May 2026 12:23:16 +0800 Subject: [PATCH 0004/1395] app: wrap provider data in Map to avoid store (#28765) --- .../components/dialog-connect-provider.tsx | 4 +-- .../src/components/dialog-custom-provider.tsx | 2 +- .../components/dialog-select-model-unpaid.tsx | 2 +- .../src/components/dialog-select-provider.tsx | 2 +- .../src/components/session-context-usage.tsx | 2 +- .../session/session-context-tab.tsx | 2 +- packages/app/src/context/global-sync.tsx | 15 +++-------- .../app/src/context/global-sync/bootstrap.ts | 9 +++---- .../src/context/global-sync/child-store.ts | 12 ++++----- packages/app/src/context/global-sync/types.ts | 4 +-- packages/app/src/context/global-sync/utils.ts | 21 +++++++++++---- packages/app/src/context/local.tsx | 2 +- packages/app/src/hooks/use-providers.ts | 27 +++++++++++++++---- packages/app/src/pages/layout.tsx | 2 +- packages/app/src/pages/session.tsx | 9 ++++--- .../src/pages/session/session-side-panel.tsx | 4 +-- packages/ui/src/components/message-part.tsx | 4 +-- packages/ui/src/context/data.tsx | 12 +++++++-- 18 files changed, 80 insertions(+), 55 deletions(-) diff --git a/packages/app/src/components/dialog-connect-provider.tsx b/packages/app/src/components/dialog-connect-provider.tsx index e305743799af..9086c108260b 100644 --- a/packages/app/src/components/dialog-connect-provider.tsx +++ b/packages/app/src/components/dialog-connect-provider.tsx @@ -41,9 +41,7 @@ export function DialogConnectProvider(props: { provider: string }) { }) const provider = createMemo( - () => - providers.all().find((x) => x.id === props.provider) ?? - globalSync.data.provider.all.find((x) => x.id === props.provider)!, + () => providers.all().get(props.provider) ?? globalSync.data.provider.all.get(props.provider)!, ) const fallback = createMemo(() => [ { diff --git a/packages/app/src/components/dialog-custom-provider.tsx b/packages/app/src/components/dialog-custom-provider.tsx index 53b66fb451d3..7d449849ebf2 100644 --- a/packages/app/src/components/dialog-custom-provider.tsx +++ b/packages/app/src/components/dialog-custom-provider.tsx @@ -106,7 +106,7 @@ export function DialogCustomProvider(props: Props) { form, t: language.t, disabledProviders: globalSync.data.config.disabled_providers ?? [], - existingProviderIDs: new Set(globalSync.data.provider.all.map((p) => p.id)), + existingProviderIDs: new Set(globalSync.data.provider.all.keys()), }) batch(() => { setForm("err", output.err) diff --git a/packages/app/src/components/dialog-select-model-unpaid.tsx b/packages/app/src/components/dialog-select-model-unpaid.tsx index e25e8f0c17de..f916ef62308d 100644 --- a/packages/app/src/components/dialog-select-model-unpaid.tsx +++ b/packages/app/src/components/dialog-select-model-unpaid.tsx @@ -91,7 +91,7 @@ export const DialogSelectModelUnpaid: Component<{ model?: ModelState }> = (props
x?.id} + key={(p) => p.id} items={providers.popular} activeIcon="plus-small" sortBy={(a, b) => { diff --git a/packages/app/src/components/dialog-select-provider.tsx b/packages/app/src/components/dialog-select-provider.tsx index e53738399aba..1273db596fcd 100644 --- a/packages/app/src/components/dialog-select-provider.tsx +++ b/packages/app/src/components/dialog-select-provider.tsx @@ -35,7 +35,7 @@ export const DialogSelectProvider: Component = () => { key={(x) => x?.id} items={() => { language.locale() - return [{ id: CUSTOM_ID, name: customLabel() }, ...providers.all()] + return [{ id: CUSTOM_ID, name: customLabel() }, ...providers.all().values()] }} filterKeys={["id", "name"]} groupBy={(x) => (popularProviders.includes(x.id) ? popularGroup() : otherGroup())} diff --git a/packages/app/src/components/session-context-usage.tsx b/packages/app/src/components/session-context-usage.tsx index 6b7fe4ef7de3..1f65e9adb3c7 100644 --- a/packages/app/src/components/session-context-usage.tsx +++ b/packages/app/src/components/session-context-usage.tsx @@ -52,7 +52,7 @@ export function SessionContextUsage(props: SessionContextUsageProps) { }), ) - const metrics = createMemo(() => getSessionContextMetrics(messages(), providers.all())) + const metrics = createMemo(() => getSessionContextMetrics(messages(), [...providers.all().values()])) const context = createMemo(() => metrics().context) const cost = createMemo(() => { return usd().format(metrics().totalCost) diff --git a/packages/app/src/components/session/session-context-tab.tsx b/packages/app/src/components/session/session-context-tab.tsx index 43741bd3fc0d..88c3889858c8 100644 --- a/packages/app/src/components/session/session-context-tab.tsx +++ b/packages/app/src/components/session/session-context-tab.tsx @@ -132,7 +132,7 @@ export function SessionContextTab() { }), ) - const metrics = createMemo(() => getSessionContextMetrics(messages(), providers.all())) + const metrics = createMemo(() => getSessionContextMetrics(messages(), [...providers.all().values()])) const ctx = createMemo(() => metrics().context) const formatter = createMemo(() => createSessionContextFormatter(language.intl())) diff --git a/packages/app/src/context/global-sync.tsx b/packages/app/src/context/global-sync.tsx index c418420da525..2e9ac170713e 100644 --- a/packages/app/src/context/global-sync.tsx +++ b/packages/app/src/context/global-sync.tsx @@ -1,12 +1,4 @@ -import type { - Config, - OpencodeClient, - Path, - Project, - ProviderAuthResponse, - ProviderListResponse, - Todo, -} from "@opencode-ai/sdk/v2/client" +import type { Config, OpencodeClient, Path, Project, ProviderAuthResponse, Todo } from "@opencode-ai/sdk/v2/client" import { showToast } from "@opencode-ai/ui/toast" import { getFilename } from "@opencode-ai/core/util/path" import { batch, createContext, getOwner, onCleanup, onMount, type ParentProps, untrack, useContext } from "solid-js" @@ -37,6 +29,7 @@ import { createRefreshQueue } from "./global-sync/queue" import { directoryKey } from "./global-sync/utils" import { PathKey } from "@/utils/path-key" import { createDirSyncContext } from "./directory-sync" +import { NormalizedProviderListResponse } from "@opencode-ai/ui/context" type GlobalStore = { ready: boolean @@ -46,7 +39,7 @@ type GlobalStore = { session_todo: { [sessionID: string]: Todo[] } - provider: ProviderListResponse + provider: NormalizedProviderListResponse provider_auth: ProviderAuthResponse config: Config reload: undefined | "pending" | "complete" @@ -121,7 +114,7 @@ function createGlobalSync() { return pathQuery.data ?? EMPTY }, get provider() { - const EMPTY = { all: [], connected: [], default: {} } + const EMPTY = { all: new Map(), connected: [], default: {} } if (providerQuery.isLoading) return EMPTY return providerQuery.data ?? EMPTY }, diff --git a/packages/app/src/context/global-sync/bootstrap.ts b/packages/app/src/context/global-sync/bootstrap.ts index 531917bde6ae..655f65a6768c 100644 --- a/packages/app/src/context/global-sync/bootstrap.ts +++ b/packages/app/src/context/global-sync/bootstrap.ts @@ -5,7 +5,6 @@ import type { PermissionRequest, Project, ProviderAuthResponse, - ProviderListResponse, QuestionRequest, Session, Todo, @@ -20,6 +19,7 @@ import { cmp, normalizeAgentList, normalizeProviderList } from "./utils" import { formatServerError } from "@/utils/server-errors" import { QueryClient, queryOptions } from "@tanstack/solid-query" import { loadMcpQuery } from "../global-sync" +import { NormalizedProviderListResponse } from "@opencode-ai/ui/context" type GlobalStore = { ready: boolean @@ -28,7 +28,7 @@ type GlobalStore = { session_todo: { [sessionID: string]: Todo[] } - provider: ProviderListResponse + provider: NormalizedProviderListResponse provider_auth: ProviderAuthResponse config: Config reload: undefined | "pending" | "complete" @@ -208,7 +208,7 @@ export async function bootstrapDirectory(input: { config: Config path: Path project: Project[] - provider: ProviderListResponse + provider: NormalizedProviderListResponse } queryClient: QueryClient }) { @@ -220,7 +220,6 @@ export async function bootstrapDirectory(input: { if (Object.keys(input.store.config).length === 0 && Object.keys(input.global.config).length > 0) { input.setStore("config", reconcile(input.global.config, { merge: false })) } - if (loading) input.setStore("status", "partial") const rev = (providerRev.get(input.directory) ?? 0) + 1 providerRev.set(input.directory, rev) @@ -327,7 +326,5 @@ export async function bootstrapDirectory(input: { description: formatServerError(slowErrs[0], input.translate), }) } - - if (loading && slowErrs.length === 0) input.setStore("status", "complete") })() } diff --git a/packages/app/src/context/global-sync/child-store.ts b/packages/app/src/context/global-sync/child-store.ts index 08299b3017e4..56935ccc996c 100644 --- a/packages/app/src/context/global-sync/child-store.ts +++ b/packages/app/src/context/global-sync/child-store.ts @@ -1,7 +1,7 @@ import { createRoot, getOwner, onCleanup, runWithOwner, type Owner } from "solid-js" import { createStore, type SetStoreFunction, type Store } from "solid-js/store" import { Persist, persisted } from "@/utils/persist" -import type { ProviderListResponse, VcsInfo } from "@opencode-ai/sdk/v2/client" +import type { VcsInfo } from "@opencode-ai/sdk/v2/client" import { DIR_IDLE_TTL_MS, MAX_DIR_STORES, @@ -17,6 +17,7 @@ import { canDisposeDirectory, pickDirectoriesToEvict } from "./eviction" import { useQueries } from "@tanstack/solid-query" import { QueryOptionsApi } from "../global-sync" import { directoryKey, type DirectoryKey } from "./utils" +import { NormalizedProviderListResponse } from "@opencode-ai/ui/context" export function createChildStoreManager(input: { owner: Owner @@ -27,7 +28,7 @@ export function createChildStoreManager(input: { translate: (key: string, vars?: Record) => string queryOptions: QueryOptionsApi global: { - provider: ProviderListResponse + provider: NormalizedProviderListResponse } }) { const children: Record, SetStoreFunction]> = {} @@ -190,10 +191,9 @@ export function createChildStoreManager(input: { return !providerQuery.isLoading }, get provider() { - const EMPTY = { all: [], connected: [], default: {} } + const EMPTY = { all: new Map(), connected: [], default: {} } if (providerQuery.isLoading) return EMPTY - if (providerQuery.data?.all.length === 0 && input.global.provider.all.length > 0) - return input.global.provider + if (providerQuery.data?.all.size === 0 && input.global.provider.all.size > 0) return input.global.provider return providerQuery.data ?? EMPTY }, config: {}, @@ -202,7 +202,7 @@ export function createChildStoreManager(input: { return { state: "", config: "", worktree: "", directory: "", home: "" } return pathQuery.data }, - status: "loading" as const, + status: "complete" as const, agent: [], command: [], session: [], diff --git a/packages/app/src/context/global-sync/types.ts b/packages/app/src/context/global-sync/types.ts index 43837ac97f3a..77b5b0c78fd1 100644 --- a/packages/app/src/context/global-sync/types.ts +++ b/packages/app/src/context/global-sync/types.ts @@ -8,7 +8,6 @@ import type { Part, Path, PermissionRequest, - ProviderListResponse, QuestionRequest, Session, SessionStatus, @@ -16,6 +15,7 @@ import type { Todo, VcsInfo, } from "@opencode-ai/sdk/v2/client" +import { NormalizedProviderListResponse } from "@opencode-ai/ui/context" import type { Accessor } from "solid-js" import type { SetStoreFunction, Store } from "solid-js/store" @@ -38,7 +38,7 @@ export type State = { projectMeta: ProjectMeta | undefined icon: string | undefined provider_ready: boolean - provider: ProviderListResponse + provider: NormalizedProviderListResponse config: Config path: Path session: Session[] diff --git a/packages/app/src/context/global-sync/utils.ts b/packages/app/src/context/global-sync/utils.ts index b982990884bf..2515a593fb36 100644 --- a/packages/app/src/context/global-sync/utils.ts +++ b/packages/app/src/context/global-sync/utils.ts @@ -1,4 +1,5 @@ import type { Agent, Project, ProviderListResponse } from "@opencode-ai/sdk/v2/client" +import { NormalizedProviderListResponse } from "@opencode-ai/ui/context" export { pathKey as directoryKey, type PathKey as DirectoryKey } from "@/utils/path-key" export const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0) @@ -17,13 +18,23 @@ export function normalizeAgentList(input: unknown): Agent[] { return Object.values(input).filter(isAgent) } -export function normalizeProviderList(input: ProviderListResponse): ProviderListResponse { +export function normalizeProviderList(input: ProviderListResponse): NormalizedProviderListResponse { return { ...input, - all: input.all.map((provider) => ({ - ...provider, - models: Object.fromEntries(Object.entries(provider.models).filter(([, info]) => info.status !== "deprecated")), - })), + all: new Map( + input.all.map( + (provider) => + [ + provider.id, + { + ...provider, + models: Object.fromEntries( + Object.entries(provider.models).filter(([, info]) => info.status !== "deprecated"), + ), + }, + ] as const, + ), + ), } } diff --git a/packages/app/src/context/local.tsx b/packages/app/src/context/local.tsx index 4465a0261dd0..c0ae79d69579 100644 --- a/packages/app/src/context/local.tsx +++ b/packages/app/src/context/local.tsx @@ -90,7 +90,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ }) const validModel = (model: ModelKey) => { - const provider = providers.all().find((item) => item.id === model.providerID) + const provider = providers.all().get(model.providerID) return !!provider?.models[model.modelID] && connected().has(model.providerID) } diff --git a/packages/app/src/hooks/use-providers.ts b/packages/app/src/hooks/use-providers.ts index f4ed359de300..45ad8b07d701 100644 --- a/packages/app/src/hooks/use-providers.ts +++ b/packages/app/src/hooks/use-providers.ts @@ -1,6 +1,7 @@ import { useGlobalSync } from "@/context/global-sync" import { decode64 } from "@/utils/base64" import { useParams } from "@solidjs/router" +import { Iterable, pipe } from "effect" import { createMemo } from "solid-js" export const popularProviders = [ @@ -29,16 +30,32 @@ export function useProviders() { return { all: () => providers().all, default: () => providers().default, - popular: () => providers().all.filter((p) => popularProviderSet.has(p.id)), + popular: () => + pipe( + providers().all, + Iterable.map(([, p]) => p), + Iterable.filter((p) => popularProviderSet.has(p.id)), + (v) => Array.from(v), + ), connected: () => { const connected = new Set(providers().connected) - return providers().all.filter((p) => connected.has(p.id)) + return pipe( + providers().all, + Iterable.map(([, p]) => p), + Iterable.filter((p) => connected.has(p.id)), + (v) => Array.from(v), + ) }, paid: () => { const connected = new Set(providers().connected) - return providers().all.filter( - (p) => connected.has(p.id) && (p.id !== "opencode" || Object.values(p.models).some((m) => m.cost?.input)), - ) + return [ + ...Iterable.filter( + providers().all, + ([id]) => + connected.has(id) && + (id !== "opencode" || Object.values(providers().all.get(id)?.models ?? {}).some((m) => m.cost?.input)), + ), + ] }, } } diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 44ab97ed6a89..3166cc11f06c 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -2303,7 +2303,7 @@ export default function Layout(props: ParentProps) {
0 && providers.paid().length === 0), + hidden: store.gettingStartedDismissed || !(providers.all().size > 0 && providers.paid().length === 0), }} >
diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 52b6c5d66ca6..406ece23ce4b 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -262,8 +262,9 @@ export default function Page() { const isDesktop = createMediaQuery("(min-width: 768px)") const size = createSizing() - const desktopReviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened()) - const desktopFileTreeOpen = createMemo(() => isDesktop() && layout.fileTree.opened()) + const isV2NewSessionPage = () => import.meta.env.VITE_OPENCODE_CHANNEL === "prod" || !params.id + const desktopReviewOpen = createMemo(() => isDesktop() && view().reviewPanel.opened() && !isV2NewSessionPage()) + const desktopFileTreeOpen = createMemo(() => isDesktop() && layout.fileTree.opened() && !isV2NewSessionPage()) const desktopSidePanelOpen = createMemo(() => desktopReviewOpen() || desktopFileTreeOpen()) const sessionPanelWidth = createMemo(() => { if (!desktopSidePanelOpen()) return "100%" @@ -1733,12 +1734,12 @@ export default function Page() { - {/* Session panel */}
+