diff --git a/apps/mobile/src/features/threads/ThreadComposer.tsx b/apps/mobile/src/features/threads/ThreadComposer.tsx index 6873926fd5dd..726dadb9a334 100644 --- a/apps/mobile/src/features/threads/ThreadComposer.tsx +++ b/apps/mobile/src/features/threads/ThreadComposer.tsx @@ -651,8 +651,14 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer ? undefined : currentModelSelection.instanceId; const modelOptions = useMemo( - () => buildModelOptions(props.serverConfig, currentModelSelection, lockedProviderInstanceId), - [props.serverConfig, currentModelSelection, lockedProviderInstanceId], + () => + buildModelOptions( + props.serverConfig, + currentModelSelection, + lockedProviderInstanceId, + props.projectCwd, + ), + [props.serverConfig, currentModelSelection, lockedProviderInstanceId, props.projectCwd], ); const threadProviderGroups = useMemo(() => groupByProvider(modelOptions), [modelOptions]); const currentModelOption = diff --git a/apps/mobile/src/features/threads/new-task-flow-provider.tsx b/apps/mobile/src/features/threads/new-task-flow-provider.tsx index 9a523c6bb0b0..a4a6036b2953 100644 --- a/apps/mobile/src/features/threads/new-task-flow-provider.tsx +++ b/apps/mobile/src/features/threads/new-task-flow-provider.tsx @@ -578,14 +578,21 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) { selectedEnvironmentServerConfig, storedStickyModelSelection, ); + const modelCwd = + (workspaceMode === "worktree" + ? selectedProject?.workspaceRoot + : (selectedWorktreePath ?? selectedProject?.workspaceRoot)) || null; const modelOptions = useMemo( () => buildModelOptions( selectedEnvironmentServerConfig, draftModelSelection ?? projectDefaultModelSelection ?? stickyModelSelection, + undefined, + modelCwd, ), [ selectedEnvironmentServerConfig, + modelCwd, draftModelSelection, projectDefaultModelSelection, stickyModelSelection, diff --git a/apps/mobile/src/lib/modelOptions.ts b/apps/mobile/src/lib/modelOptions.ts index 436d6b9996fb..592ecf0b7ef6 100644 --- a/apps/mobile/src/lib/modelOptions.ts +++ b/apps/mobile/src/lib/modelOptions.ts @@ -1,3 +1,4 @@ +import { resolveProviderForCwd } from "@t3tools/client-runtime/providerSkills"; import type { MenuAction } from "@react-native-menu/menu"; import type { ModelCapabilities, @@ -167,10 +168,12 @@ export function buildModelOptions( config: T3ServerConfig | null | undefined, fallbackModelSelection: ModelSelection | null, providerInstanceId?: ModelSelection["instanceId"], + cwd?: string | null, ): ReadonlyArray { const options = new Map(); - for (const provider of config?.providers ?? []) { + for (const snapshot of config?.providers ?? []) { + const provider = resolveProviderForCwd(snapshot, cwd); if ( (providerInstanceId !== undefined && provider.instanceId !== providerInstanceId) || !provider.enabled || diff --git a/apps/server/src/provider/ProviderRegistry.test.ts b/apps/server/src/provider/ProviderRegistry.test.ts index d0c1f348a736..28ff63148c4b 100644 --- a/apps/server/src/provider/ProviderRegistry.test.ts +++ b/apps/server/src/provider/ProviderRegistry.test.ts @@ -620,7 +620,7 @@ it.layer( }); describe("ProviderRegistry.layer", () => { - it("stores workspace skills and commands without changing machine metadata", () => { + it("stores workspace skills, commands and options without changing machine metadata", () => { const provider = { instanceId: ProviderInstanceId.make("codex"), driver: ProviderDriverKind.make("codex"), @@ -639,7 +639,15 @@ it.layer( checkedAt: "2026-03-25T00:01:00.000Z", slashCommands: [{ name: "project" }], skills: [{ name: "project", path: "/project/SKILL.md", enabled: true }], - } satisfies ServerProvider; + optionDescriptors: [ + { + id: "agent", + label: "Agent", + type: "select", + options: [{ id: "readonly", label: "Readonly" }], + }, + ], + } satisfies ProviderWorkspaceSnapshot; const result = ProviderRegistry.upsertProviderWorkspaceSnapshot( provider, @@ -649,12 +657,14 @@ it.layer( assert.deepStrictEqual(result.slashCommands, provider.slashCommands); assert.deepStrictEqual(result.skills, provider.skills); + assert.strictEqual(result.models, provider.models); assert.deepStrictEqual(result.workspaceSnapshots, [ { cwd: "/project", checkedAt: scopedSnapshot.checkedAt, slashCommands: scopedSnapshot.slashCommands, skills: scopedSnapshot.skills, + optionDescriptors: scopedSnapshot.optionDescriptors, }, ]); diff --git a/apps/server/src/provider/ProviderRegistry.ts b/apps/server/src/provider/ProviderRegistry.ts index 0e2c50e6670e..61e3f32021f2 100644 --- a/apps/server/src/provider/ProviderRegistry.ts +++ b/apps/server/src/provider/ProviderRegistry.ts @@ -198,6 +198,9 @@ export function upsertProviderWorkspaceSnapshot( : scopedSnapshot.slashCommands, ...(scopedSnapshot.slashCommandsPending ? { slashCommandsPending: true } : {}), skills: scopedSnapshot.skills, + ...(scopedSnapshot.optionDescriptors === undefined + ? {} + : { optionDescriptors: scopedSnapshot.optionDescriptors }), } satisfies NonNullable[number]; return { ...provider, diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index c8c202f5cadc..247f521b16a9 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -413,7 +413,10 @@ import { import { terminalEnvironment } from "../state/terminal"; import { threadEnvironment } from "../state/threads"; import { workspacePreparationRetryRunIds } from "@t3tools/client-runtime/state/turn-item-presentation"; -import { resolveProviderSkillsForCwd } from "@t3tools/client-runtime/providerSkills"; +import { + resolveProviderForCwd, + resolveProviderSkillsForCwd, +} from "@t3tools/client-runtime/providerSkills"; import { vcsEnvironment } from "../state/vcs"; import { sourceControlEnvironment } from "../state/sourceControl"; import { useProjectClone } from "../state/projectClones"; @@ -3245,12 +3248,23 @@ export default function ChatView(props: ChatViewProps) { versionMismatchThreadContinuation, versionMismatchServerLabel, ]); + const gitCwd = activeProject + ? projectScriptCwd({ + project: { cwd: activeProject.workspaceRoot }, + worktreePath: activeThread?.worktreePath ?? null, + }) + : null; const providerInstanceEntries = useMemo( () => sortProviderInstanceEntries( - applyProviderInstanceSettings(deriveProviderInstanceEntries(providerStatuses), settings), + applyProviderInstanceSettings( + deriveProviderInstanceEntries( + providerStatuses.map((provider) => resolveProviderForCwd(provider, gitCwd)), + ), + settings, + ), ), - [providerStatuses, settings], + [providerStatuses, settings, gitCwd], ); const { selectedProviderEntry, requestedDriverKind } = useMemo( () => @@ -4117,12 +4131,6 @@ export default function ChatView(props: ChatViewProps) { : JSON.stringify([itemId, latestCheckpointCompletedAt]); }, [serverVisibleTurnItems, turnDiffSummaries]); - const gitCwd = activeProject - ? projectScriptCwd({ - project: { cwd: activeProject.workspaceRoot }, - worktreePath: activeThread?.worktreePath ?? null, - }) - : null; const gitStatusCwd = activeThread?.worktreePath ?? gitCwd; const gitStatusQuery = useEnvironmentQuery( gitStatusCwd === null diff --git a/apps/web/src/components/chat/ChatComposer.tsx b/apps/web/src/components/chat/ChatComposer.tsx index cc4031710158..dd5d67da2391 100644 --- a/apps/web/src/components/chat/ChatComposer.tsx +++ b/apps/web/src/components/chat/ChatComposer.tsx @@ -1130,6 +1130,7 @@ import { getProviderSkillsForSlashMenu, hasCompleteProviderWorkspaceSnapshot, hasCurrentProviderWorkspaceSnapshot, + resolveProviderForCwd, resolveProviderSkillsForCwd, resolveProviderSlashCommandsForCwd, } from "@t3tools/client-runtime/providerSkills"; @@ -2122,12 +2123,16 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) // Instance-aware projection of the wire provider list. One entry per // configured instance (default built-in + any custom `providerInstances.*`), // sorted default-first per driver kind for a stable picker order. + const workspaceProviders = useMemo( + () => providerStatuses.map((provider) => resolveProviderForCwd(provider, gitCwd)), + [providerStatuses, gitCwd], + ); const providerInstanceEntries = useMemo>( () => sortProviderInstanceEntries( - applyProviderInstanceSettings(deriveProviderInstanceEntries(providerStatuses), settings), + applyProviderInstanceSettings(deriveProviderInstanceEntries(workspaceProviders), settings), ), - [providerStatuses, settings], + [workspaceProviders, settings], ); const selectedProviderByThreadId = composerDraft.activeProvider ?? null; const { @@ -2197,7 +2202,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) const { modelOptions: composerModelOptions, selectedModel } = useEffectiveComposerModelState({ threadRef: composerDraftTarget, - providers: providerStatuses, + providers: workspaceProviders, selectedProvider, selectedInstanceId, threadModelSelection: activeThreadModelSelection, diff --git a/packages/client-runtime/src/providerSkills.test.ts b/packages/client-runtime/src/providerSkills.test.ts index 1b9ca671d10c..1e53707d47f3 100644 --- a/packages/client-runtime/src/providerSkills.test.ts +++ b/packages/client-runtime/src/providerSkills.test.ts @@ -13,6 +13,7 @@ import { getProviderSkillsForSlashMenu, hasCompleteProviderWorkspaceSnapshot, hasCurrentProviderWorkspaceSnapshot, + resolveProviderForCwd, resolveProviderSkillsForCwd, resolveProviderSlashCommandsForCwd, resolveProviderSkillSourceKind, @@ -304,3 +305,64 @@ describe("workspace provider snapshots", () => { ); }); }); + +describe("resolveProviderForCwd", () => { + it("replaces only workspace option ids and leaves the global catalog untouched", () => { + const variant = { + id: "variant", + label: "Reasoning", + type: "select" as const, + options: [{ id: "high", label: "High" }], + }; + const globalAgent = { + id: "agent", + label: "Agent", + type: "select" as const, + options: [{ id: "build", label: "Build" }], + }; + const catalog: ServerProvider = { + ...provider, + models: [ + { + slug: "openai/gpt-5.4", + name: "GPT-5.4", + isCustom: false, + capabilities: { optionDescriptors: [variant, globalAgent] }, + }, + { slug: "custom/model", name: "Custom", isCustom: true, capabilities: null }, + ], + workspaceSnapshots: ["readonly", "audit"].map((name) => ({ + ...provider.workspaceSnapshots[0]!, + cwd: "/workspace/" + name, + optionDescriptors: [{ ...globalAgent, options: [{ id: name, label: name }] }], + })), + }; + for (const name of ["readonly", "audit"]) { + const scoped = resolveProviderForCwd(catalog, "/workspace/" + name); + expect(scoped.models[0]?.capabilities?.optionDescriptors).toEqual([ + variant, + { ...globalAgent, options: [{ id: name, label: name }] }, + ]); + expect(scoped.models[1]?.capabilities?.optionDescriptors).toEqual([ + { ...globalAgent, options: [{ id: name, label: name }] }, + ]); + } + expect(catalog.models[0]?.capabilities?.optionDescriptors).toEqual([variant, globalAgent]); + expect(resolveProviderForCwd(catalog, "/workspace/unknown")).toBe(catalog); + expect(resolveProviderForCwd(catalog, null)).toBe(catalog); + expect(resolveProviderForCwd(provider, "/workspace/project-a")).toBe(provider); + const emptyAgents: ServerProvider = { + ...catalog, + workspaceSnapshots: [ + { + ...provider.workspaceSnapshots[0]!, + optionDescriptors: [{ ...globalAgent, options: [] }], + }, + ], + }; + expect( + resolveProviderForCwd(emptyAgents, "/workspace/project-a").models[0]?.capabilities + ?.optionDescriptors, + ).toEqual([variant, { ...globalAgent, options: [] }]); + }); +}); diff --git a/packages/client-runtime/src/providerSkills.ts b/packages/client-runtime/src/providerSkills.ts index fe69c371fc6c..197dd8ee27cd 100644 --- a/packages/client-runtime/src/providerSkills.ts +++ b/packages/client-runtime/src/providerSkills.ts @@ -115,6 +115,31 @@ export function hasCurrentProviderWorkspaceSnapshot( ); } +/** Applies workspace options without changing the provider's machine-wide catalog. */ +export function resolveProviderForCwd( + provider: ServerProvider, + cwd: string | null | undefined, +): ServerProvider { + const descriptors = resolveProviderWorkspaceSnapshot(provider, cwd)?.optionDescriptors; + if (descriptors === undefined) return provider; + const overridden = new Set(descriptors.map((descriptor) => descriptor.id)); + return { + ...provider, + models: provider.models.map((model) => ({ + ...model, + capabilities: { + ...model.capabilities, + optionDescriptors: [ + ...(model.capabilities?.optionDescriptors ?? []).filter( + (descriptor) => !overridden.has(descriptor.id), + ), + ...descriptors, + ], + }, + })), + }; +} + export function resolveProviderSkillsForCwd( provider: ServerProvider, cwd: string | null | undefined, diff --git a/packages/contracts/src/server.ts b/packages/contracts/src/server.ts index 5855477855d1..af2d9025ff7f 100644 --- a/packages/contracts/src/server.ts +++ b/packages/contracts/src/server.ts @@ -23,7 +23,7 @@ import { ResolvedKeybindingsConfig, } from "./keybindings.ts"; import { EditorId, FileManagerRevealKind, RemoteOpenTarget } from "./editor.ts"; -import { ModelCapabilities } from "./model.ts"; +import { ModelCapabilities, ProviderOptionDescriptor } from "./model.ts"; import { RuntimeMode } from "./providerPolicy.ts"; import { ProviderDriverKind, ProviderInstanceId } from "./providerInstance.ts"; import { ServerProviderUsageLimits, UsageLimitSourceSnapshots } from "./providerUsageLimits.ts"; @@ -141,6 +141,8 @@ export const ServerProviderWorkspaceSnapshot = Schema.Struct({ /** Skills are available, but command discovery still needs a retry. */ slashCommandsPending: Schema.optional(Schema.Boolean), skills: Schema.Array(ServerProviderSkill), + /** Workspace options replace descriptors with the same id in the model catalog. */ + optionDescriptors: Schema.optional(Schema.Array(ProviderOptionDescriptor)), }); export type ServerProviderWorkspaceSnapshot = typeof ServerProviderWorkspaceSnapshot.Type; diff --git a/packages/provider-core/src/server/driver.ts b/packages/provider-core/src/server/driver.ts index 1801fa316901..234c9cca4694 100644 --- a/packages/provider-core/src/server/driver.ts +++ b/packages/provider-core/src/server/driver.ts @@ -68,7 +68,7 @@ export interface ProviderDriverMetadata { } export type ProviderWorkspaceSnapshot = ServerProvider & - Pick; + Pick; /** * One materialized provider instance. Held by the registry, looked up by diff --git a/packages/provider-opencode/src/server/OpenCodeRuntime.ts b/packages/provider-opencode/src/server/OpenCodeRuntime.ts index c91da0326228..3d2200880adb 100644 --- a/packages/provider-opencode/src/server/OpenCodeRuntime.ts +++ b/packages/provider-opencode/src/server/OpenCodeRuntime.ts @@ -208,6 +208,12 @@ export const loadOpenCodeCommands = (client: OpencodeClient) => ), ); +/** Lists agents in the SDK client's directory, including project-local definitions. */ +export const loadOpenCodeAgents = (client: OpencodeClient) => + runOpenCodeSdk("app.agents", (signal) => client.app.agents(undefined, { signal })).pipe( + Effect.map((result) => result.data ?? []), + ); + export interface ParsedOpenCodeModelSlug { readonly providerID: string; readonly modelID: string; @@ -939,12 +945,6 @@ const makeOpenCodeRuntime = Effect.gen(function* () { ), ); - const loadAgents = (client: OpencodeClient) => - runOpenCodeSdk("app.agents", (signal) => client.app.agents(undefined, { signal })).pipe( - Effect.map((result) => result.data ?? []), - Effect.orElseSucceed((): ReadonlyArray => []), - ); - const loadOpenCodeSkills: OpenCodeRuntime["Service"]["loadOpenCodeSkills"] = (client) => runOpenCodeSdk("app.skills", (signal) => client.app.skills(undefined, { signal })).pipe( Effect.map((result) => @@ -962,7 +962,7 @@ const makeOpenCodeRuntime = Effect.gen(function* () { Effect.all( [ loadProviders(client), - loadAgents(client), + loadOpenCodeAgents(client).pipe(Effect.orElseSucceed((): ReadonlyArray => [])), loadSkills(client), loadOpenCodeCommands(client).pipe(Effect.orElseSucceed(() => [])), ], diff --git a/packages/provider-opencode/src/server/driver.ts b/packages/provider-opencode/src/server/driver.ts index f3340a62e9e0..267edac8ec67 100644 --- a/packages/provider-opencode/src/server/driver.ts +++ b/packages/provider-opencode/src/server/driver.ts @@ -38,6 +38,7 @@ import { openCodeUsageReader, type OpenCodeUsageReaderEnv } from "./usage.ts"; import { readOpenCodeGoUsageLimits } from "./usageLimits.ts"; import { checkOpenCodeProviderStatus, + openCodeAgentOptionDescriptor, loadOpenCode2Workspace, makeOpenCode2ModelLoader, makePendingOpenCodeProvider, @@ -346,6 +347,10 @@ export const OpenCodeDriver: ProviderDriver< return yield* loadOpenCode2Workspace( Effect.all( { + agents: client.agent.list({ location }).pipe( + Effect.map((list) => list.data), + Effect.orElseSucceed(() => []), + ), skills: client.skill.list({ location }).pipe(Effect.map((list) => list.data)), commands: client.command.list({ location }).pipe(Effect.map((list) => list.data)), }, @@ -411,6 +416,7 @@ export const OpenCodeDriver: ProviderDriver< ) => Effect.all( { + agents: OpenCodeRuntime.loadOpenCodeAgents(client).pipe(Effect.orElseSucceed(() => [])), skills: openCodeRuntime.loadOpenCodeSkills(client), commands: OpenCodeRuntime.loadOpenCodeCommands(client).pipe( Effect.timeout("10 seconds"), @@ -517,8 +523,18 @@ export const OpenCodeDriver: ProviderDriver< ), ), ]).pipe( - Effect.map(([machineSnapshot, { skills, commands }]) => ({ + Effect.map(([machineSnapshot, { agents, skills, commands }]) => ({ ...machineSnapshot, + // A failed agent listing keeps the machine-wide agents. + ...(agents.length > 0 + ? { + optionDescriptors: [ + openCodeAgentOptionDescriptor( + agents.map((agent) => ({ ...agent, name: agent.id })), + ), + ], + } + : {}), skills: openCode2SkillsToServerProviderSkills(skills), slashCommands: openCode2CommandsToServerProviderSlashCommands(commands), })), @@ -527,8 +543,11 @@ export const OpenCodeDriver: ProviderDriver< snapshot.getSnapshot, loadWorkspaceForCwd(cwd).pipe(Effect.timeout("20 seconds")), ]).pipe( - Effect.map(([machineSnapshot, { skills, commands }]) => ({ + Effect.map(([machineSnapshot, { agents, skills, commands }]) => ({ ...machineSnapshot, + ...(agents.length > 0 + ? { optionDescriptors: [openCodeAgentOptionDescriptor(agents)] } + : {}), skills: openCodeSkillsToServerProviderSkills(skills), slashCommands: openCodeCommandsToServerProviderSlashCommands(commands), })), diff --git a/packages/provider-opencode/src/server/status.test.ts b/packages/provider-opencode/src/server/status.test.ts index c84dbdf3f894..3a773c8474e3 100644 --- a/packages/provider-opencode/src/server/status.test.ts +++ b/packages/provider-opencode/src/server/status.test.ts @@ -814,6 +814,7 @@ const builtinSkills = [ { id: "report", name: "Report", path: "/builtin/report.md" }, ]; const scanned: OpenCode2Workspace = { + agents: [{ id: "build", mode: "primary", hidden: false }], commands: [...builtinCommands, { name: "hello", description: "Say hello to the workspace" }], skills: [ ...builtinSkills, @@ -829,7 +830,7 @@ const scanned: OpenCode2Workspace = { it.effect("reads a fresh OpenCode 2 directory again once its scan has ended", () => Effect.gen(function* () { // A directory the server has not served yet lists no commands until its scan ends. - const reads: Array = [{ commands: [], skills: [] }, scanned]; + const reads: Array = [{ agents: [], commands: [], skills: [] }, scanned]; const scanEnded = yield* Deferred.make(); const load = yield* loadOpenCode2Workspace( Effect.sync(() => reads.shift()!), diff --git a/packages/provider-opencode/src/server/status.ts b/packages/provider-opencode/src/server/status.ts index f89298af01ae..29e824c6ba84 100644 --- a/packages/provider-opencode/src/server/status.ts +++ b/packages/provider-opencode/src/server/status.ts @@ -1,5 +1,6 @@ import { type ModelCapabilities, + type SelectProviderOptionDescriptor, type ServerProviderModel, type ServerProviderSkill, type ServerProviderSlashCommand, @@ -163,10 +164,30 @@ function inferDefaultVariant( return undefined; } -function inferDefaultAgent(agents: ReadonlyArray): string | undefined { +function inferDefaultAgent(agents: ReadonlyArray<{ readonly name: string }>): string | undefined { return agents.find((agent) => agent.name === "build")?.name ?? agents[0]?.name ?? undefined; } +export function openCodeAgentOptionDescriptor( + agents: ReadonlyArray>, +): SelectProviderOptionDescriptor { + const visible = agents.filter( + (agent) => !agent.hidden && (agent.mode === "primary" || agent.mode === "all"), + ); + const defaultAgent = inferDefaultAgent(visible); + return { + id: "agent", + label: "Agent", + type: "select", + options: visible.map((agent) => ({ + id: agent.name, + label: titleCaseSlug(agent.name), + ...(agent.name === defaultAgent ? { isDefault: true } : {}), + })), + ...(defaultAgent ? { currentValue: defaultAgent } : {}), + }; +} + const DEFAULT_OPENCODE_MODEL_CAPABILITIES: ModelCapabilities = createModelCapabilities({ optionDescriptors: [ { @@ -213,15 +234,7 @@ function openCodeCapabilitiesForModel(input: { ? { id: value, label: titleCaseSlug(value), isDefault: true as const } : { id: value, label: titleCaseSlug(value) }, ); - const primaryAgents = input.agents.filter( - (agent) => !agent.hidden && (agent.mode === "primary" || agent.mode === "all"), - ); - const defaultAgent = inferDefaultAgent(primaryAgents); - const agentOptions = primaryAgents.map((agent) => - defaultAgent === agent.name - ? { id: agent.name, label: titleCaseSlug(agent.name), isDefault: true as const } - : { id: agent.name, label: titleCaseSlug(agent.name) }, - ); + const agentDescriptor = openCodeAgentOptionDescriptor(input.agents); return createModelCapabilities({ optionDescriptors: [ ...(variantOptions.length > 0 @@ -235,17 +248,7 @@ function openCodeCapabilitiesForModel(input: { }, ] : []), - ...(agentOptions.length > 0 - ? [ - { - id: "agent", - label: "Agent", - type: "select" as const, - options: agentOptions, - ...(defaultAgent ? { currentValue: defaultAgent } : {}), - }, - ] - : []), + ...(agentDescriptor.options.length > 0 ? [agentDescriptor] : []), ], }); } @@ -410,8 +413,13 @@ export const makeOpenCode2ModelLoader = ( ); }); -/** A workspace's skills and commands, as an OpenCode 2 server lists them for its directory. */ +/** A workspace's agents, skills and commands as OpenCode 2 lists them for its directory. */ export interface OpenCode2Workspace { + readonly agents: ReadonlyArray<{ + readonly id: string; + readonly mode: Agent["mode"]; + readonly hidden: boolean; + }>; readonly skills: ReadonlyArray<{ readonly id: string; readonly name: string; diff --git a/packages/provider-opencode/src/server/v2/adapter.ts b/packages/provider-opencode/src/server/v2/adapter.ts index 306836614d55..92448beaa37b 100644 --- a/packages/provider-opencode/src/server/v2/adapter.ts +++ b/packages/provider-opencode/src/server/v2/adapter.ts @@ -779,9 +779,11 @@ const sameModel = (left: ModelRef, right: ModelRef | undefined) => left.id === right?.id && (left.variant ?? "default") === (right?.variant ?? "default"); -/** OpenCode's own agents for T3's interaction modes; plan mode is its read-only `plan` agent. */ +/** Plan mode takes precedence over the selected workspace agent. */ const agentFor = (input: ProviderAdapter.ProviderAdapterV2TurnInput) => - input.runtimePolicy.interactionMode === "plan" ? "plan" : "build"; + input.runtimePolicy.interactionMode === "plan" + ? "plan" + : (getModelSelectionStringOptionValue(input.modelSelection, "agent") ?? "build"); /** `/name args` naming one of the workspace's commands, which OpenCode expands itself. */ const commandOf = (text: string) => {