Add OpenCode auto accept feature

This commit is contained in:
Mohamed Boudra
2026-05-26 21:51:04 +07:00
parent 0ab41fbd9a
commit 6d205f8853
11 changed files with 265 additions and 65 deletions
+7 -1
View File
@@ -27,6 +27,7 @@ interface ProviderLaunchConfig {
model?: string;
thinkingOptionId?: string;
modeId?: string;
featureValues?: Record<string, unknown>;
}
const SEND_TIMEOUT_MS = 240_000;
@@ -52,7 +53,12 @@ function fullAccessConfig(provider: RewindFlowProvider): ProviderLaunchConfig {
modeId: "full-access",
};
case "opencode":
return { provider, model: "opencode/big-pickle", modeId: "full-access" };
return {
provider,
model: "opencode/big-pickle",
modeId: "build",
featureValues: { auto_accept: true },
};
case "pi":
return {
provider,
@@ -19,7 +19,7 @@ import {
} from "react-native";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { useShallow } from "zustand/shallow";
import { Brain, ChevronDown, ListTodo, Settings2, Zap } from "lucide-react-native";
import { Brain, ChevronDown, ListTodo, Settings2, ShieldCheck, Zap } from "lucide-react-native";
import { getProviderIcon } from "@/components/provider-icons";
import { CombinedModelSelector } from "@/components/combined-model-selector";
import {
@@ -139,6 +139,7 @@ function findOptionLabel(
const FEATURE_ICONS: Record<string, typeof Zap> = {
"list-todo": ListTodo,
"shield-check": ShieldCheck,
zap: Zap,
};
@@ -151,6 +152,7 @@ function getFeatureIconColor(
enabled: boolean,
palette: {
blue: { 400: string };
green: { 400: string };
yellow: { 400: string };
},
foregroundMuted: string,
@@ -162,6 +164,8 @@ function getFeatureIconColor(
switch (getFeatureHighlightColor(featureId)) {
case "blue":
return palette.blue[400];
case "green":
return palette.green[400];
case "yellow":
return palette.yellow[400];
default:
@@ -4,6 +4,7 @@ import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { useShallow } from "zustand/shallow";
import { useStoreWithEqualityFn } from "zustand/traditional";
import {
Bot,
ChevronDown,
ShieldAlert,
ShieldCheck,
@@ -33,6 +34,7 @@ function shouldRenderForPlacement(placement: AgentModeControlPlacement, isCompac
}
const MODE_ICONS = {
Bot,
ShieldCheck,
ShieldAlert,
ShieldOff,
@@ -1,7 +1,7 @@
import type { AgentFeature, AgentModelDefinition } from "@server/server/agent/agent-sdk-types";
export type ExplainedAgentControl = "mode" | "model" | "thinking";
export type FeatureHighlightColor = "blue" | "default" | "yellow";
export type FeatureHighlightColor = "blue" | "default" | "green" | "yellow";
export function getAgentControlHint(selector: ExplainedAgentControl): string {
switch (selector) {
@@ -32,6 +32,8 @@ export function getFeatureHighlightColor(featureId: string): FeatureHighlightCol
switch (featureId) {
case "fast_mode":
return "yellow";
case "auto_accept":
return "green";
case "plan_mode":
return "blue";
default:
@@ -1156,11 +1156,12 @@ export class AgentManager {
async setAgentMode(agentId: string, modeId: string): Promise<void> {
const agent = this.requireSessionAgent(agentId);
await agent.session.setMode(modeId);
agent.config.modeId = modeId;
agent.currentModeId = modeId;
const currentMode = await agent.session.getCurrentMode();
agent.config.modeId = currentMode ?? undefined;
agent.currentModeId = currentMode;
// Update runtimeInfo to reflect the new mode
if (agent.runtimeInfo) {
agent.runtimeInfo = { ...agent.runtimeInfo, modeId };
agent.runtimeInfo = { ...agent.runtimeInfo, modeId: currentMode };
}
this.touchUpdatedAt(agent);
this.emitState(agent);
@@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest";
import { resolveAndValidateCreateAgentMode } from "./create-agent-mode.js";
const CLAUDE_MODES = ["default", "acceptEdits", "plan", "bypassPermissions"];
const OPENCODE_MODES = ["build", "full-access", "plan"];
const OPENCODE_MODES = ["build", "plan"];
const CODEX_MODES = ["auto", "full-access"];
describe("resolveAndValidateCreateAgentMode", () => {
@@ -25,7 +25,7 @@ describe("resolveAndValidateCreateAgentMode", () => {
availableModes: OPENCODE_MODES,
}),
).toThrow(
"Invalid mode 'bypassPermissions' for provider 'opencode'. Available modes: build, full-access, plan",
"Invalid mode 'bypassPermissions' for provider 'opencode'. Available modes: build, plan",
);
});
@@ -68,7 +68,7 @@ describe("resolveAndValidateCreateAgentMode", () => {
availableModes: OPENCODE_MODES,
}),
).toThrow(
"cannot inherit mode 'bypassPermissions' from caller (provider 'claude') for new agent (provider 'opencode'). Pass an explicit mode. Available modes for 'opencode': build, full-access, plan",
"cannot inherit mode 'bypassPermissions' from caller (provider 'claude') for new agent (provider 'opencode'). Pass an explicit mode. Available modes for 'opencode': build, plan",
);
});
@@ -1447,11 +1447,39 @@ describe("create_agent MCP tool", () => {
initialPrompt: "Do work",
}),
).rejects.toThrow(
"Invalid mode 'bypassPermissions' for provider 'opencode'. Available modes: build, full-access, plan",
"Invalid mode 'bypassPermissions' for provider 'opencode'. Available modes: build, plan",
);
expect(spies.agentManager.createAgent).not.toHaveBeenCalled();
});
it("accepts legacy OpenCode full-access as build plus auto accept", async () => {
const { agentManager, agentStorage, spies } = createTestDeps();
spies.agentManager.createAgent.mockResolvedValue({
id: "child-agent",
cwd: existingCwd,
lifecycle: "idle",
currentModeId: "build",
availableModes: [],
config: { title: "Child", featureValues: { auto_accept: true } },
} as ManagedAgent);
const server = await createAgentMcpServer({ agentManager, agentStorage, logger });
const tool = registeredTool(server, "create_agent");
await tool.handler({
cwd: existingCwd,
title: "Legacy mode",
provider: "opencode/gpt-5.4",
settings: { modeId: "full-access" },
initialPrompt: "Do work",
});
expect(spies.agentManager.createAgent).toHaveBeenCalledWith(
expect.objectContaining({ modeId: "build", featureValues: { auto_accept: true } }),
undefined,
undefined,
);
});
it("inherits the caller mode when the new agent uses the same provider", async () => {
const { agentManager, agentStorage, spies } = createTestDeps();
spies.agentManager.getAgent.mockReturnValue({
@@ -1513,12 +1541,12 @@ describe("create_agent MCP tool", () => {
initialPrompt: "Do work",
}),
).rejects.toThrow(
"cannot inherit mode 'default' from caller (provider 'claude') for new agent (provider 'opencode'). Pass an explicit mode. Available modes for 'opencode': build, full-access, plan",
"cannot inherit mode 'default' from caller (provider 'claude') for new agent (provider 'opencode'). Pass an explicit mode. Available modes for 'opencode': build, plan",
);
expect(spies.agentManager.createAgent).not.toHaveBeenCalled();
});
it("inherits the target provider's unattended mode when caller is unattended cross-provider", async () => {
it("maps unattended callers to OpenCode auto accept", async () => {
const { agentManager, agentStorage, spies } = createTestDeps();
spies.agentManager.getAgent.mockReturnValue({
id: "parent-agent",
@@ -1530,9 +1558,9 @@ describe("create_agent MCP tool", () => {
id: "child-agent",
cwd: existingCwd,
lifecycle: "idle",
currentModeId: "full-access",
currentModeId: "build",
availableModes: [],
config: { title: "Child" },
config: { title: "Child", featureValues: { auto_accept: true } },
} as ManagedAgent);
const server = await createAgentMcpServer({
@@ -1549,7 +1577,7 @@ describe("create_agent MCP tool", () => {
});
expect(spies.agentManager.createAgent).toHaveBeenCalledWith(
expect.objectContaining({ modeId: "full-access" }),
expect.objectContaining({ modeId: "build", featureValues: { auto_accept: true } }),
undefined,
expect.any(Object),
);
+93 -14
View File
@@ -10,7 +10,7 @@ import type {
} from "@modelcontextprotocol/sdk/types.js";
import type { AgentProvider } from "./agent-sdk-types.js";
import type { AgentManager, WaitForAgentResult } from "./agent-manager.js";
import type { AgentManager, ManagedAgent, WaitForAgentResult } from "./agent-manager.js";
import {
AgentFeatureSchema,
AgentPermissionRequestPayloadSchema,
@@ -134,6 +134,42 @@ const CODEX_TO_CLAUDE_MODE: Record<string, string> = {
"full-access": "bypassPermissions",
};
const OPENCODE_PROVIDER_ID = "opencode";
const OPENCODE_BUILD_MODE_ID = "build";
const OPENCODE_LEGACY_FULL_ACCESS_MODE_ID = "full-access";
const OPENCODE_AUTO_ACCEPT_FEATURE_ID = "auto_accept";
function isOpenCodeLegacyFullAccessMode(
provider: AgentProvider,
modeId: string | undefined,
): boolean {
return provider === OPENCODE_PROVIDER_ID && modeId === OPENCODE_LEGACY_FULL_ACCESS_MODE_ID;
}
function withOpenCodeAutoAcceptFeature(
features: Record<string, unknown> | undefined,
enabled: boolean,
): Record<string, unknown> {
return {
...features,
[OPENCODE_AUTO_ACCEPT_FEATURE_ID]: enabled,
};
}
function hasOpenCodeAutoAcceptFeature(agent: ManagedAgent): boolean {
if (agent.provider !== OPENCODE_PROVIDER_ID) {
return false;
}
return (
agent.features?.some(
(feature) =>
feature.id === OPENCODE_AUTO_ACCEPT_FEATURE_ID &&
feature.type === "toggle" &&
feature.value === true,
) === true || agent.config.featureValues?.[OPENCODE_AUTO_ACCEPT_FEATURE_ID] === true
);
}
function mapModeAcrossProviders(
sourceMode: string,
sourceProvider: AgentProvider,
@@ -583,6 +619,9 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
: {}),
...(callerAgent.config.title ? { title: callerAgent.config.title } : {}),
...(callerAgent.config.extra ? { extra: callerAgent.config.extra } : {}),
...(callerAgent.config.featureValues
? { featureValues: callerAgent.config.featureValues }
: {}),
...(callerAgent.config.systemPrompt ? { systemPrompt: callerAgent.config.systemPrompt } : {}),
...(callerAgent.config.mcpServers ? { mcpServers: callerAgent.config.mcpServers } : {}),
};
@@ -910,6 +949,48 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
return modes.some((mode) => mode.id === modeId && mode.isUnattended === true);
};
const isAgentInUnattendedState = (agent: ManagedAgent): boolean => {
return (
isParentInUnattendedMode(agent.provider, agent.currentModeId) ||
hasOpenCodeAutoAcceptFeature(agent)
);
};
const resolveCreateModeAndFeatures = (input: {
provider: AgentProvider;
requestedMode: string | undefined;
parent: { provider: AgentProvider; modeId: string | null; isUnattended: boolean } | null;
features: Record<string, unknown> | undefined;
}): { mode: string | undefined; features: Record<string, unknown> | undefined } => {
const legacyOpenCodeFullAccess = isOpenCodeLegacyFullAccessMode(
input.provider,
input.requestedMode,
);
const inheritsOpenCodeUnattended =
input.provider === OPENCODE_PROVIDER_ID &&
input.requestedMode === undefined &&
input.parent?.isUnattended === true;
const inheritsOpenCodeAutoAccept =
inheritsOpenCodeUnattended && input.features?.[OPENCODE_AUTO_ACCEPT_FEATURE_ID] === undefined;
const requestedMode = legacyOpenCodeFullAccess ? OPENCODE_BUILD_MODE_ID : input.requestedMode;
const features =
legacyOpenCodeFullAccess || inheritsOpenCodeAutoAccept
? withOpenCodeAutoAcceptFeature(input.features, true)
: input.features;
const mode =
inheritsOpenCodeUnattended && requestedMode === undefined
? OPENCODE_BUILD_MODE_ID
: resolveAndValidateCreateAgentMode({
requestedMode,
targetProvider: input.provider,
parent: input.parent,
availableModes: getAvailableModeIds(input.provider),
targetUnattendedMode: getUnattendedModeId(input.provider),
});
return { mode, features };
};
const resolveCallerCreateAgentArgs = (
args: unknown,
parentAgentId: string,
@@ -928,16 +1009,15 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
lockedCwd: callerContext?.lockedCwd,
allowCustomCwd: callerContext?.allowCustomCwd ?? true,
});
const resolvedMode = resolveAndValidateCreateAgentMode({
const resolvedRuntime = resolveCreateModeAndFeatures({
provider,
requestedMode: settings?.modeId,
targetProvider: provider,
parent: {
provider: parentAgent.provider,
modeId: parentAgent.currentModeId,
isUnattended: isParentInUnattendedMode(parentAgent.provider, parentAgent.currentModeId),
isUnattended: isAgentInUnattendedState(parentAgent),
},
availableModes: getAvailableModeIds(provider),
targetUnattendedMode: getUnattendedModeId(provider),
features: settings?.features,
});
return {
provider,
@@ -946,11 +1026,11 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
normalizedTitle: callerArgs.title.trim(),
model: resolvedProviderModel.model,
thinkingOptionId: settings?.thinkingOptionId,
features: settings?.features,
features: resolvedRuntime.features,
labels: callerArgs.labels,
notifyOnFinish: callerArgs.notifyOnFinish ?? false,
resolvedCwd,
resolvedMode,
resolvedMode: resolvedRuntime.mode,
setupContinuation: undefined,
};
};
@@ -962,12 +1042,11 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
const resolvedProviderModel = resolveRequiredProviderModel(topLevelArgs.provider);
const { cwd, settings, worktreeName, baseBranch, refName, action, githubPrNumber } =
topLevelArgs;
const resolvedMode = resolveAndValidateCreateAgentMode({
const resolvedRuntime = resolveCreateModeAndFeatures({
provider: resolvedProviderModel.provider,
requestedMode: settings?.modeId,
targetProvider: resolvedProviderModel.provider,
parent: null,
availableModes: getAvailableModeIds(resolvedProviderModel.provider),
targetUnattendedMode: getUnattendedModeId(resolvedProviderModel.provider),
features: settings?.features,
});
let resolvedCwd = expandUserPath(cwd);
let setupContinuation: AgentWorktreeSetupContinuation | undefined;
@@ -1021,11 +1100,11 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
normalizedTitle: topLevelArgs.title.trim(),
model: resolvedProviderModel.model,
thinkingOptionId: settings?.thinkingOptionId,
features: settings?.features,
features: resolvedRuntime.features,
labels: topLevelArgs.labels,
notifyOnFinish: topLevelArgs.notifyOnFinish ?? false,
resolvedCwd,
resolvedMode,
resolvedMode: resolvedRuntime.mode,
setupContinuation,
};
};
@@ -2,7 +2,12 @@ import { z } from "zod";
import type { AgentMode } from "./agent-sdk-types.js";
export type AgentModeColorTier = "safe" | "moderate" | "dangerous" | "planning" | `#${string}`;
export type AgentModeIcon = "ShieldCheck" | "ShieldAlert" | "ShieldOff" | "ShieldQuestionMark";
export type AgentModeIcon =
| "Bot"
| "ShieldCheck"
| "ShieldAlert"
| "ShieldOff"
| "ShieldQuestionMark";
export interface AgentModeVisuals {
icon: AgentModeIcon;
@@ -126,22 +131,14 @@ const OPENCODE_MODES: AgentProviderModeDefinition[] = [
id: "build",
label: "Build",
description: "Allows edits and tool execution for implementation work",
icon: "ShieldCheck",
icon: "Bot",
colorTier: "moderate",
},
{
id: "full-access",
label: "Full Access",
description: "Automatically approves all tool permission prompts for the session",
icon: "ShieldAlert",
colorTier: "dangerous",
isUnattended: true,
},
{
id: "plan",
label: "Plan",
description: "Read-only planning mode that avoids file edits",
icon: "ShieldCheck",
icon: "Bot",
colorTier: "planning",
},
];
@@ -61,8 +61,8 @@ function questionEvent(): unknown {
};
}
describe("OpenCode full-access mode", () => {
test("includes virtual full-access mode with dynamic OpenCode agents", async () => {
describe("OpenCode auto_accept feature", () => {
test("lists OpenCode modes without the legacy virtual full-access mode", async () => {
const { runtime } = mockOpenCodeClient({
agents: [
{ name: "build", mode: "primary", hidden: false, description: "Build agent" },
@@ -73,14 +73,36 @@ describe("OpenCode full-access mode", () => {
const client = new OpenCodeAgentClient(createTestLogger(), undefined, { runtime });
const modes = await client.listModes({ cwd: "/tmp/project", force: false });
expect(modes.map((mode) => mode.id)).toEqual(["build", "plan", "full-access", "paseo-custom"]);
expect(modes.find((mode) => mode.id === "full-access")).toMatchObject({
label: "Full Access",
description: "Automatically approves all tool permission prompts for the session",
});
expect(modes.map((mode) => mode.id)).toEqual(["build", "plan", "paseo-custom"]);
});
test("reports full-access but sends prompts through OpenCode build agent", async () => {
test("lists auto accept as a provider feature", async () => {
const { runtime } = mockOpenCodeClient();
const client = new OpenCodeAgentClient(createTestLogger(), undefined, { runtime });
const enabledFeatures = await client.listFeatures({
provider: "opencode",
cwd: "/tmp/project",
featureValues: { auto_accept: true },
});
const legacyFeatures = await client.listFeatures({
provider: "opencode",
cwd: "/tmp/project",
modeId: "full-access",
});
expect(enabledFeatures).toEqual([
expect.objectContaining({
type: "toggle",
id: "auto_accept",
label: "Auto Accept",
value: true,
}),
]);
expect(legacyFeatures).toEqual([expect.objectContaining({ id: "auto_accept", value: true })]);
});
test("keeps legacy full-access as an alias for build plus auto accept", async () => {
const { openCodeClient, runtime } = mockOpenCodeClient();
const client = new OpenCodeAgentClient(createTestLogger(), undefined, { runtime });
@@ -90,7 +112,8 @@ describe("OpenCode full-access mode", () => {
modeId: "full-access",
});
expect(await session.getCurrentMode()).toBe("full-access");
expect(await session.getCurrentMode()).toBe("build");
expect(session.features).toEqual([expect.objectContaining({ id: "auto_accept", value: true })]);
await session.run("Implement the change");
@@ -102,7 +125,7 @@ describe("OpenCode full-access mode", () => {
await session.close();
});
test("auto-approves tool permissions in full-access without surfacing them", async () => {
test("auto-approves tool permissions when auto accept is enabled", async () => {
const { openCodeClient, runtime } = mockOpenCodeClient({
events: [toolPermissionEvent(), idleEvent()],
});
@@ -112,7 +135,7 @@ describe("OpenCode full-access mode", () => {
const session = await client.createSession({
provider: "opencode",
cwd: "/tmp/project",
modeId: "full-access",
featureValues: { auto_accept: true },
});
session.subscribe((event) => receivedEvents.push(event));
@@ -130,7 +153,7 @@ describe("OpenCode full-access mode", () => {
await session.close();
});
test("keeps questions separate from full-access tool auto-approval", async () => {
test("keeps questions separate from auto accept tool approval", async () => {
const { openCodeClient, runtime } = mockOpenCodeClient({
events: [questionEvent(), idleEvent()],
});
@@ -140,7 +163,7 @@ describe("OpenCode full-access mode", () => {
const session = await client.createSession({
provider: "opencode",
cwd: "/tmp/project",
modeId: "full-access",
featureValues: { auto_accept: true },
});
session.subscribe((event) => receivedEvents.push(event));
@@ -83,7 +83,8 @@ const OPENCODE_CAPABILITIES: AgentCapabilityFlags = {
};
const OPENCODE_BUILD_MODE_ID = "build";
const OPENCODE_FULL_ACCESS_MODE_ID = "full-access";
const OPENCODE_LEGACY_FULL_ACCESS_MODE_ID = "full-access";
const OPENCODE_AUTO_ACCEPT_FEATURE_ID = "auto_accept";
const OPENCODE_PERSISTED_SESSION_LIMIT = 200;
const OPENCODE_PENDING_ABORT_START_TIMEOUT_MS = 10_000;
const OPENCODE_PERMISSION_ACTION_ALLOW_ONCE = "allow_once";
@@ -100,13 +101,24 @@ const DEFAULT_MODES: AgentMode[] = [
label: "Plan",
description: "Read-only planning mode that avoids file edits",
},
{
id: OPENCODE_FULL_ACCESS_MODE_ID,
label: "Full Access",
description: "Automatically approves all tool permission prompts for the session",
},
];
function isOpenCodeAutoAcceptEnabled(config: AgentSessionConfig): boolean {
return config.featureValues?.[OPENCODE_AUTO_ACCEPT_FEATURE_ID] === true;
}
function buildOpenCodeAutoAcceptFeature(config: AgentSessionConfig): AgentFeature {
return {
type: "toggle",
id: OPENCODE_AUTO_ACCEPT_FEATURE_ID,
label: "Auto Accept",
description: "Automatically approves OpenCode tool permission prompts.",
tooltip: "Auto accept permission prompts",
icon: "shield-check",
value: isOpenCodeAutoAcceptEnabled(config),
};
}
function buildOpenCodePermissionActions(): AgentPermissionAction[] {
return [
{
@@ -436,11 +448,26 @@ function normalizeOpenCodeModeId(modeId: string | null | undefined): string {
function resolveOpenCodeRuntimeAgentId(modeId: string | null | undefined): string {
const normalizedModeId = normalizeOpenCodeModeId(modeId);
return normalizedModeId === OPENCODE_FULL_ACCESS_MODE_ID
return normalizedModeId === OPENCODE_LEGACY_FULL_ACCESS_MODE_ID
? OPENCODE_BUILD_MODE_ID
: normalizedModeId;
}
function normalizeOpenCodeConfig(config: OpenCodeAgentConfig): OpenCodeAgentConfig {
if (normalizeOpenCodeModeId(config.modeId) !== OPENCODE_LEGACY_FULL_ACCESS_MODE_ID) {
return { ...config };
}
return {
...config,
modeId: OPENCODE_BUILD_MODE_ID,
featureValues: {
...config.featureValues,
[OPENCODE_AUTO_ACCEPT_FEATURE_ID]: true,
},
};
}
function isSelectableOpenCodeAgent(agent: { mode?: string; hidden?: boolean }): boolean {
return (agent.mode === "primary" || agent.mode === "all") && agent.hidden !== true;
}
@@ -462,6 +489,7 @@ function mapOpenCodeAgentToMode(agent: {
return {
id: agent.name,
label: agent.name.charAt(0).toUpperCase() + agent.name.slice(1),
icon: "Bot",
description:
typeof agent.description === "string" && agent.description.trim().length > 0
? agent.description.trim()
@@ -473,6 +501,9 @@ function mapOpenCodeAgentToMode(agent: {
function mergeOpenCodeModes(discoveredModes: AgentMode[]): AgentMode[] {
const modesById = new Map(DEFAULT_MODES.map((mode) => [mode.id, mode]));
for (const mode of discoveredModes) {
if (mode.id === OPENCODE_LEGACY_FULL_ACCESS_MODE_ID) {
continue;
}
modesById.set(mode.id, mode);
}
return sortOpenCodeModes(Array.from(modesById.values()));
@@ -1329,8 +1360,8 @@ export class OpenCodeAgentClient implements AgentClient {
}
}
async listFeatures(_config: AgentSessionConfig): Promise<AgentFeature[]> {
return [];
async listFeatures(config: AgentSessionConfig): Promise<AgentFeature[]> {
return [buildOpenCodeAutoAcceptFeature(this.assertConfig(config))];
}
async listPersistedAgents(
@@ -1437,7 +1468,7 @@ export class OpenCodeAgentClient implements AgentClient {
if (config.provider !== "opencode") {
throw new Error(`OpenCodeAgentClient received config for provider '${config.provider}'`);
}
return { ...config, provider: "opencode" };
return normalizeOpenCodeConfig({ ...config, provider: "opencode" });
}
private async populateModelContextWindowCache(
@@ -2541,6 +2572,7 @@ class OpenCodeAgentSession implements AgentSession {
private readonly logger: Logger;
private readonly modelContextWindowsByModelKey: ReadonlyMap<string, number>;
private currentMode: string = "default";
private autoAcceptEnabled = false;
private pendingPermissions = new Map<string, AgentPermissionRequest>();
private abortController: AbortController | null = null;
private pendingAbortPromise: Promise<void> | null = null;
@@ -2590,6 +2622,7 @@ class OpenCodeAgentSession implements AgentSession {
this.logger = logger.child({ agentId: this.agentId });
this.modelContextWindowsByModelKey = modelContextWindowsByModelKey;
this.currentMode = normalizeOpenCodeModeId(config.modeId);
this.autoAcceptEnabled = isOpenCodeAutoAcceptEnabled(config);
this.releaseServer = releaseServer ?? null;
this.persistSession = persistSession;
this.selectedModelContextWindowMaxTokens = this.resolveConfiguredModelContextWindowMaxTokens(
@@ -2602,6 +2635,10 @@ class OpenCodeAgentSession implements AgentSession {
return this.sessionId;
}
get features(): AgentFeature[] {
return [buildOpenCodeAutoAcceptFeature(this.config)];
}
async getRuntimeInfo(): Promise<AgentRuntimeInfo> {
return {
provider: "opencode",
@@ -3238,7 +3275,28 @@ class OpenCodeAgentSession implements AgentSession {
}
async setMode(modeId: string): Promise<void> {
this.currentMode = normalizeOpenCodeModeId(modeId);
const normalizedModeId = normalizeOpenCodeModeId(modeId);
if (normalizedModeId === OPENCODE_LEGACY_FULL_ACCESS_MODE_ID) {
this.currentMode = OPENCODE_BUILD_MODE_ID;
await this.setFeature(OPENCODE_AUTO_ACCEPT_FEATURE_ID, true);
return;
}
this.currentMode = normalizedModeId;
this.config.modeId = normalizedModeId;
}
async setFeature(featureId: string, value: unknown): Promise<void> {
if (featureId !== OPENCODE_AUTO_ACCEPT_FEATURE_ID) {
throw new Error(`Unsupported OpenCode feature '${featureId}'`);
}
const enabled = value === true;
this.autoAcceptEnabled = enabled;
this.config.featureValues = {
...this.config.featureValues,
[OPENCODE_AUTO_ACCEPT_FEATURE_ID]: enabled,
};
}
getPendingPermissions(): AgentPermissionRequest[] {
@@ -3521,7 +3579,7 @@ class OpenCodeAgentSession implements AgentSession {
}
private async tryAutoApproveToolPermission(request: AgentPermissionRequest): Promise<boolean> {
if (this.currentMode !== OPENCODE_FULL_ACCESS_MODE_ID || request.kind !== "tool") {
if (!this.autoAcceptEnabled || request.kind !== "tool") {
return false;
}