Add OpenCode auto accept feature
This commit is contained in:
@@ -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),
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user