Compare commits

...

1 Commits

Author SHA1 Message Date
nikita-ashihmin
6b7473b1af refactor!: remove agent config option (#1112)
## Why

The main-thread agent option is a rarely used, provider-specific
parameter. It is not used by ACP clients or their settings, and
maintaining a public picker for it adds config state, discovery calls,
restart propagation, and live-switch handling for functionality that
clients do not need.

Removing it keeps the session configuration contract focused on settings
that ACP clients actually use. This does not remove ACP subagent
sessions or the Agent/Task tool behavior; it only removes the unused
main-thread agent selector and parameter plumbing.

## Summary

- remove the main-thread Agent session config picker and custom-agent
discovery
- remove live agent switching and session state used only by that picker
- ignore claudeCode.options.agent and stop restoring it during context
resets
- document agent as ignored in the exported NewSessionMeta contract
- preserve ACP subagent sessions and Agent/Task tool behavior

## Breaking change

BREAKING CHANGE: claudeCode.options.agent is no longer forwarded to
Claude Code, and the agent-picker exports DEFAULT_AGENT_ID,
AGENT_CONFIG_ID, BUILTIN_AGENT_NAMES, and discoverCustomAgents are
removed. Consumers relying on main-thread agent selection must stop
passing that option.

## Testing

- npm run check
- npm run build
- npx vitest run src/tests/create-session-options.test.ts (62 passed)
2026-09-11 01:23:11 +04:00
10 changed files with 79 additions and 439 deletions

View File

@@ -56,7 +56,6 @@ import {
} from "@agentclientprotocol/sdk";
import {
AccountInfo,
AgentInfo,
CanUseTool,
deleteSession,
FastModeDisabledReason,
@@ -243,11 +242,8 @@ import {
exitPlanModeRawOutput,
observeExitPlanToolResults,
} from "./exit-plan.js";
import { DEFAULT_AGENT_ID } from "./session-config-ids.js";
import { parseToolResultMeta } from "./tool-result-meta.js";
import { formatUsageResponse, isUsageCommandText, parseUsageResponse } from "./usage-markdown.js";
export { DEFAULT_AGENT_ID } from "./session-config-ids.js";
import { MODE_CONFIG_ID, SessionModeManager } from "./session-mode.js";
import {
applyAvailableModelsAllowlist,
@@ -814,14 +810,6 @@ export type Session = {
/** Initial mode fallback is reported after session/new, on the first prompt. */
autoModeFallbackWarningPending?: boolean;
configOptions: SessionConfigOption[];
/** Custom main-thread agent personas the user (or a plugin/project) has
* configured, discovered via `supportedAgents()` with Claude Code's built-in
* subagents filtered out. Empty when none are configured, in which case the
* "agent" config option is omitted entirely. */
agents: AgentInfo[];
/** The currently selected main-thread agent name, or "default" for the
* standard Claude Code agent (no `agent` flag applied). */
currentAgent: string;
/** Whether Fast mode is currently enabled for this session. Tracked as the
* user's intent so it persists across model switches; the Fast mode config
* option is only surfaced while the selected model supports it. */
@@ -1172,13 +1160,15 @@ export type NewSessionMeta = {
claudeCode?: {
/**
* Options forwarded to Claude Code when starting a new session.
* Those parameters will be ignored and managed by ACP:
* Those parameters will not be forwarded because they are managed by ACP:
* - cwd
* - includePartialMessages
* - allowDangerouslySkipPermissions
* - permissionMode
* - canUseTool
* - executable
* The `agent` parameter is also ignored: main-thread agent selection is not
* part of this adapter's ACP contract.
* Those parameters will be used and updated to work with ACP:
* - hooks (merged with ACP's hooks)
* - mcpServers (merged with ACP's mcpServers)
@@ -7434,8 +7424,6 @@ export class ClaudeAcpAgent {
session.models,
session.modelInfos,
seedEffort,
session.agents,
session.currentAgent,
{
// The toggle follows the newly selected model: it disappears when the
// model lacks fast support and reappears (with the retained user
@@ -7522,19 +7510,6 @@ export class ClaudeAcpAgent {
if (modeDowngraded) {
await this.sessionModes.publishFallbackState(sessionId, session);
}
} else if (configId === AGENT_CONFIG_ID) {
// Live agent switch — no subprocess restart needed. Apply the SDK flag
// first so a rejected control request leaves both `currentAgent` and the
// config option untouched (no UI/SDK desync). Passing `null` clears the
// flag layer back to the standard Claude Code agent; the change takes
// effect on the next turn (SDK >= 0.3.161).
await session.query.applyFlagSettings({
agent: value === DEFAULT_AGENT_ID ? null : value,
});
session.currentAgent = value;
session.configOptions = session.configOptions.map((o) =>
o.id === configId && typeof o.currentValue === "string" ? { ...o, currentValue: value } : o,
);
} else if (configId === EFFORT_CONFIG_ID) {
// Apply first so a rejected control request cannot leave the displayed
// value ahead of the SDK flag layer.
@@ -7898,7 +7873,13 @@ export class ClaudeAcpAgent {
// Extract options from _meta if provided
const sessionMeta = params._meta as NewSessionMeta | undefined;
const userProvidedOptions = sessionMeta?.claudeCode?.options;
const userProvidedOptions = sessionMeta?.claudeCode?.options
? { ...sessionMeta.claudeCode.options }
: undefined;
// Main-thread agent selection is intentionally not part of this adapter's
// ACP contract. Ignore the provider-specific option instead of forwarding
// hidden state that the Client cannot inspect or change.
if (userProvidedOptions) delete userProvidedOptions.agent;
const forwardSubagentText =
clientSupportsSubagents(this.clientCapabilities) ||
supportsSubagentTranscript(this.clientCapabilities) ||
@@ -8304,19 +8285,6 @@ export class ClaudeAcpAgent {
});
timing.phase("modes");
const agents = await discoverCustomAgents(q);
timing.phase("agents");
// Only adopt the requested agent as the selected value if it's one we
// actually surface in the picker. A built-in (filtered out above) or
// otherwise-unknown name would leave the config option's `currentValue`
// pointing at an entry not in its own `options` list, which clients render
// as a blank/invalid selection.
const requestedAgent = userProvidedOptions?.agent;
const currentAgent =
requestedAgent && agents.some((a) => a.name === requestedAgent)
? requestedAgent
: DEFAULT_AGENT_ID;
// Seed Fast mode from the SDK's reported state so the UI reflects reality
// (the CLI may start a session with fast mode already on, or force it off
// when `fastModePerSessionOptIn` is set). The toggle is only surfaced while
@@ -8352,8 +8320,6 @@ export class ClaudeAcpAgent {
mergeEffortSettings(settingsManager.getSettings(), configuredSettingsObject),
currentModelInfo,
),
agents,
currentAgent,
fastMode,
{
useRecommendedValue,
@@ -8426,8 +8392,6 @@ export class ClaudeAcpAgent {
initialEffort?.currentValue === userProvidedOptions.effort
? userProvidedOptions.effort
: undefined,
agents,
currentAgent,
fastModeEnabled,
fastModeDisabledReason,
abortController,
@@ -8876,44 +8840,11 @@ function isValidBaseUrl(baseUrl: string | undefined): baseUrl is string {
return parsed.protocol === "http:" || parsed.protocol === "https:";
}
// `supportedAgents()` always returns Claude Code's built-in subagents — the
// ones used for Task-tool delegation (Explore, Plan, etc.) — even when the user
// has configured none of their own. Those aren't meaningful *main-thread*
// personas, so we filter them out and only surface the Agent picker when the
// user (or a plugin/project) has configured custom agents. Update this set if
// the SDK's built-in roster changes.
export const BUILTIN_AGENT_NAMES = new Set([
"claude",
"general-purpose",
"Explore",
"Plan",
"statusline-setup",
]);
// Value of the synthetic "Default" entry in the agent picker, which maps to the
// standard Claude Code agent (`applyFlagSettings({ agent: null })`). It is a
// reserved sentinel: a custom agent named exactly this would collide with it
// (two options sharing the value, selection silently routing to `null`), so we
// exclude that name from discovery.
/** Discover user/plugin/project-configured main-thread agents, excluding the
* built-in subagents and the reserved "default" sentinel. Returns an empty
* list if discovery fails so a flaky control request never blocks session
* creation. */
export async function discoverCustomAgents(q: Query): Promise<AgentInfo[]> {
try {
const agents = await q.supportedAgents();
return agents.filter((a) => !BUILTIN_AGENT_NAMES.has(a.name) && a.name !== DEFAULT_AGENT_ID);
} catch {
return [];
}
}
/** Stable ids for the session config options surfaced via `configOptions`.
* Centralized so the option declarations in `buildConfigOptions` and the
* handlers in `setSessionConfigOption`/`applyConfigOptionValue` reference the
* same identifiers and can't drift apart. */
export { MODE_CONFIG_ID };
export const AGENT_CONFIG_ID = "agent";
export const FAST_MODE_CONFIG_ID = "fast";
/** Select-fallback values used when the client has not opted into boolean
@@ -9059,8 +8990,6 @@ export function buildConfigOptions(
models: SessionModelState,
modelInfos: ModelInfo[],
currentEffortLevel?: string,
agents: AgentInfo[] = [],
currentAgent: string = DEFAULT_AGENT_ID,
fastMode?: FastModeOptionState,
presentation?: ConfigOptionPresentation,
): SessionConfigOption[] {
@@ -9089,28 +9018,6 @@ export function buildConfigOptions(
);
}
// Only surface the Agent picker when there's a real choice — i.e. the user
// has configured at least one custom agent (built-ins are filtered out in
// discoverCustomAgents). With none configured, "Default" would be the only
// entry, so we omit the option entirely.
if (agents.length > 0) {
options.push({
id: AGENT_CONFIG_ID,
name: "Agent",
description: "Main-thread agent persona",
type: "select",
currentValue: currentAgent,
options: [
{ value: DEFAULT_AGENT_ID, name: "Default", description: "Standard Claude Code agent" },
...agents.map((a) => ({
value: a.name,
name: a.name,
description: a.description || undefined,
})),
],
});
}
return options;
}

View File

@@ -5,7 +5,7 @@ import type {
PermissionMode,
SDKUserMessage,
} from "@anthropic-ai/claude-agent-sdk";
import { DEFAULT_AGENT_ID, DEFAULT_MODEL_ID } from "./session-config-ids.js";
import { DEFAULT_MODEL_ID } from "./session-config-ids.js";
export type ClearContextReset = {
toolUseId: string;
@@ -37,7 +37,6 @@ export type ClearContextSession<Turn extends ClearContextTurn = ClearContextTurn
accumulatedModelUsage?: Record<string, Usage>;
models: { currentModelId: string };
configOptions: SessionConfigOption[];
currentAgent: string;
fastModeEnabled: boolean;
effortPinnedLevel?: string;
activeTurn?: Turn | null;
@@ -89,7 +88,6 @@ function restartParams<Session extends ClearContextSession>(session: Session): N
...(session.models.currentModelId !== DEFAULT_MODEL_ID
? { model: session.models.currentModelId }
: {}),
...(session.currentAgent !== DEFAULT_AGENT_ID ? { agent: session.currentAgent } : {}),
...(session.effortPinnedLevel !== undefined
? { effort: session.effortPinnedLevel as EffortLevel }
: {}),

View File

@@ -1,4 +1,3 @@
/** Shared sentinels and ids for session preferences managed by the adapter. */
export const DEFAULT_MODEL_ID = "default";
export const DEFAULT_AGENT_ID = "default";
export const EFFORT_CONFIG_ID = "effort";

View File

@@ -41,7 +41,6 @@ import {
messageIdForGrouping,
buildConfigOptions,
createFastModeConfigOption,
discoverCustomAgents,
runPromptWithCancellation,
type AcpClient,
type SDKMessageFilter,
@@ -3221,8 +3220,6 @@ describe("permission request cancellation", () => {
cachedWriteTokens: 0,
},
configOptions: [],
agents: [],
currentAgent: "default",
fastModeEnabled: false,
abortController: new AbortController(),
emitRawSDKMessages: false,
@@ -8645,8 +8642,6 @@ describe("session/close", () => {
cachedWriteTokens: 0,
},
configOptions: [],
agents: [],
currentAgent: "default",
fastModeEnabled: false,
abortController: new AbortController(),
emitRawSDKMessages: false,
@@ -8741,8 +8736,6 @@ describe("session/delete", () => {
cachedWriteTokens: 0,
},
configOptions: [],
agents: [],
currentAgent: "default",
fastModeEnabled: false,
abortController: new AbortController(),
emitRawSDKMessages: false,
@@ -8854,8 +8847,6 @@ describe("getOrCreateSession param change detection", () => {
cachedWriteTokens: 0,
},
configOptions: [],
agents: [],
currentAgent: "default",
fastModeEnabled: false,
abortController: new AbortController(),
emitRawSDKMessages: false,
@@ -12082,8 +12073,6 @@ describe("post-error recovery", () => {
cachedWriteTokens: 0,
},
configOptions: [],
agents: [],
currentAgent: "default",
fastModeEnabled: false,
abortController: new AbortController(),
emitRawSDKMessages: false,
@@ -16655,8 +16644,6 @@ describe("session/cancel wedge recovery (issue #680)", () => {
cachedWriteTokens: 0,
},
configOptions: [],
agents: [],
currentAgent: "default",
fastModeEnabled: false,
abortController: new AbortController(),
emitRawSDKMessages: false,
@@ -18172,265 +18159,59 @@ describe("messageIdForGrouping", () => {
});
});
describe("agent selection config option", () => {
const baseModes = { currentModeId: "default", availableModes: [] };
const baseModels = { currentModelId: "default", availableModels: [] };
describe("buildConfigOptions model option resolved description", () => {
const modes = { currentModeId: "default", availableModes: [] };
const models = {
currentModelId: "default",
availableModels: [{ modelId: "default", name: "Default", description: "" }],
};
describe("discoverCustomAgents", () => {
it("filters out Claude Code's built-in subagents", async () => {
const q = {
supportedAgents: async () => [
{ name: "claude", description: "catch-all" },
{ name: "Explore", description: "search" },
{ name: "general-purpose", description: "gp" },
{ name: "Plan", description: "architect" },
{ name: "statusline-setup", description: "status" },
{ name: "my-reviewer", description: "Reviews code" },
{ name: "my-writer", description: "Writes docs" },
],
} as any;
const agents = await discoverCustomAgents(q);
expect(agents.map((a) => a.name)).toEqual(["my-reviewer", "my-writer"]);
});
it("excludes a custom agent named 'default' (reserved sentinel)", async () => {
const q = {
supportedAgents: async () => [
{ name: "default", description: "collides with the synthetic Default entry" },
{ name: "my-reviewer", description: "Reviews code" },
],
} as any;
const agents = await discoverCustomAgents(q);
expect(agents.map((a) => a.name)).toEqual(["my-reviewer"]);
});
it("returns an empty list when discovery throws", async () => {
const q = {
supportedAgents: async () => {
throw new Error("control request failed");
},
} as any;
expect(await discoverCustomAgents(q)).toEqual([]);
});
});
describe("buildConfigOptions agent option", () => {
it("omits the agent option when no custom agents are configured", () => {
const options = buildConfigOptions(baseModes, baseModels, [], undefined, [], "default");
expect(options.find((o) => o.id === "agent")).toBeUndefined();
});
it("adds an agent option with a synthetic Default entry when custom agents exist", () => {
const agents = [
{ name: "my-reviewer", description: "Reviews code" },
// empty description should normalize to undefined, not ""
{ name: "my-writer", description: "" },
];
const options = buildConfigOptions(
baseModes,
baseModels,
[],
undefined,
agents,
"my-reviewer",
);
const agentOption = options.find((o) => o.id === "agent");
expect(agentOption).toBeDefined();
expect(agentOption!.currentValue).toBe("my-reviewer");
expect(agentOption!.type).toBe("select");
const entries = (agentOption as any).options;
expect(entries.map((o: any) => o.value)).toEqual(["default", "my-reviewer", "my-writer"]);
expect(entries[2].description).toBeUndefined();
});
});
describe("buildConfigOptions model option resolved description", () => {
const modes = { currentModeId: "default", availableModes: [] };
const models = {
currentModelId: "default",
availableModels: [{ modelId: "default", name: "Default", description: "" }],
};
it("sets description to the named model's displayName when resolvedModel matches", () => {
const modelInfos = [
{
value: "default",
displayName: "Default",
description: "",
resolvedModel: "claude-sonnet-5",
},
{
value: "sonnet",
displayName: "Claude Sonnet 5",
description: "Balanced",
resolvedModel: "claude-sonnet-5",
},
];
const options = buildConfigOptions(
modes,
models,
modelInfos as any,
undefined,
[],
"default",
);
const modelOption = options.find((o) => o.id === "model");
const defaultEntry = (modelOption as any).options.find((o: any) => o.value === "default");
expect(defaultEntry.description).toBe("Claude Sonnet 5");
});
it("falls back to resolvedModel itself when no named model shares it", () => {
const modelInfos = [
{
value: "default",
displayName: "Default",
description: "",
resolvedModel: "claude-opus-5-20251201",
},
];
const options = buildConfigOptions(
modes,
models,
modelInfos as any,
undefined,
[],
"default",
);
const modelOption = options.find((o) => o.id === "model");
const defaultEntry = (modelOption as any).options.find((o: any) => o.value === "default");
expect(defaultEntry.description).toBe("claude-opus-5-20251201");
});
it("leaves description undefined when default model has no resolvedModel", () => {
const modelsNoDesc = {
currentModelId: "default",
availableModels: [{ modelId: "default", name: "Default" }],
};
const modelInfos = [{ value: "default", displayName: "Default", description: "" }];
const options = buildConfigOptions(
modes,
modelsNoDesc,
modelInfos as any,
undefined,
[],
"default",
);
const modelOption = options.find((o) => o.id === "model");
const defaultEntry = (modelOption as any).options.find((o: any) => o.value === "default");
expect(defaultEntry.description).toBeUndefined();
});
});
describe("switching the agent", () => {
function createMockAgent() {
const mockClient = { sessionUpdate: async () => {} } as unknown as AcpClient;
return new ClaudeAcpAgent(mockClient, { log: () => {}, error: () => {} });
}
const agents = [{ name: "my-reviewer", description: "Reviews code" }];
function injectSession(agent: ClaudeAcpAgent, sessionId: string) {
function* empty() {}
const applyFlagSettings = vi.fn(async () => {});
const gen = Object.assign(empty(), {
interrupt: vi.fn(),
close: vi.fn(),
applyFlagSettings,
});
agent.sessions[sessionId] = {
query: gen as any,
input: new Pushable(),
cancelled: false,
titles: new SessionTitles(agent, sessionId),
cwd: "/test",
sessionFingerprint: JSON.stringify({ cwd: "/test", mcpServers: [] }),
modes: { currentModeId: "default", availableModes: [] },
models: { currentModelId: "default", availableModels: [] },
modelInfos: [],
settingsManager: { dispose: vi.fn() } as any,
accumulatedUsage: {
inputTokens: 0,
outputTokens: 0,
cachedReadTokens: 0,
cachedWriteTokens: 0,
},
configOptions: buildConfigOptions(baseModes, baseModels, [], undefined, agents, "default"),
agents,
currentAgent: "default",
fastModeEnabled: false,
abortController: new AbortController(),
emitRawSDKMessages: false,
forwardSubagentText: false,
contextWindowSize: 200000,
contextWindowAuthoritative: false,
providerCacheKey: "default",
taskState: new Map(),
toolUseCache: {},
emittedToolCalls: new Set(),
liveBackgroundTasks: new Map(),
emittedAssistantText: false,
owedTrailingIdles: 0,
messageIdToUuid: new Map(),
sessionFailureState: { epoch: randomUUID(), revisions: new Map(), active: new Map() },
fileChangeReportRequestIds: new Set(),
};
return { session: agent.sessions[sessionId]!, applyFlagSettings };
}
it("applies the agent flag live without restarting the subprocess", async () => {
const agent = createMockAgent();
const { session, applyFlagSettings } = injectSession(agent, "s1");
const result = await agent.setSessionConfigOption({
sessionId: "s1",
configId: "agent",
value: "my-reviewer",
});
expect(applyFlagSettings).toHaveBeenCalledWith({ agent: "my-reviewer" });
expect(session.currentAgent).toBe("my-reviewer");
// The whole point of the SDK >= 0.3.161 approach: no process teardown.
expect(session.query.interrupt).not.toHaveBeenCalled();
expect(session.abortController.signal.aborted).toBe(false);
expect(agent.sessions["s1"]).toBe(session);
const agentOption = result.configOptions.find((o) => o.id === "agent");
expect(agentOption?.currentValue).toBe("my-reviewer");
});
it("clears the flag (agent: null) when switching back to default", async () => {
const agent = createMockAgent();
const { session, applyFlagSettings } = injectSession(agent, "s2");
session.currentAgent = "my-reviewer";
await agent.setSessionConfigOption({
sessionId: "s2",
configId: "agent",
it("sets description to the named model's displayName when resolvedModel matches", () => {
const modelInfos = [
{
value: "default",
});
displayName: "Default",
description: "",
resolvedModel: "claude-sonnet-5",
},
{
value: "sonnet",
displayName: "Claude Sonnet 5",
description: "Balanced",
resolvedModel: "claude-sonnet-5",
},
];
const options = buildConfigOptions(modes, models, modelInfos as any, undefined);
const modelOption = options.find((o) => o.id === "model");
const defaultEntry = (modelOption as any).options.find((o: any) => o.value === "default");
expect(defaultEntry.description).toBe("Claude Sonnet 5");
});
expect(applyFlagSettings).toHaveBeenCalledWith({ agent: null });
expect(session.currentAgent).toBe("default");
});
it("falls back to resolvedModel itself when no named model shares it", () => {
const modelInfos = [
{
value: "default",
displayName: "Default",
description: "",
resolvedModel: "claude-opus-5-20251201",
},
];
const options = buildConfigOptions(modes, models, modelInfos as any, undefined);
const modelOption = options.find((o) => o.id === "model");
const defaultEntry = (modelOption as any).options.find((o: any) => o.value === "default");
expect(defaultEntry.description).toBe("claude-opus-5-20251201");
});
it("leaves tracked state untouched when the live switch is rejected", async () => {
const agent = createMockAgent();
const { session, applyFlagSettings } = injectSession(agent, "s3");
applyFlagSettings.mockRejectedValueOnce(new Error("control channel closed"));
await expect(
agent.setSessionConfigOption({
sessionId: "s3",
configId: "agent",
value: "my-reviewer",
}),
).rejects.toThrow("control channel closed");
// The flag never applied, so neither currentAgent nor the config option
// moves — no desync with the agent the SDK is actually running.
expect(session.currentAgent).toBe("default");
const agentOption = session.configOptions.find((o) => o.id === "agent");
expect(agentOption?.currentValue).toBe("default");
});
it("leaves description undefined when default model has no resolvedModel", () => {
const modelsNoDesc = {
currentModelId: "default",
availableModels: [{ modelId: "default", name: "Default" }],
};
const modelInfos = [{ value: "default", displayName: "Default", description: "" }];
const options = buildConfigOptions(modes, modelsNoDesc, modelInfos as any, undefined);
const modelOption = options.find((o) => o.id === "model");
const defaultEntry = (modelOption as any).options.find((o: any) => o.value === "default");
expect(defaultEntry.description).toBeUndefined();
});
});

View File

@@ -1,42 +0,0 @@
import { describe, it, expect } from "vitest";
import { randomUUID } from "crypto";
import { query } from "@anthropic-ai/claude-agent-sdk";
import type { SDKUserMessage } from "@anthropic-ai/claude-agent-sdk";
import { Pushable } from "../utils.js";
import { discoverCustomAgents, BUILTIN_AGENT_NAMES } from "../acp-agent.js";
// `discoverCustomAgents` distinguishes user/plugin-configured agents from
// Claude Code's built-in subagents by name (BUILTIN_AGENT_NAMES), since the
// SDK's `AgentInfo` carries no built-in/source flag. That hardcoded set drifts
// silently if the SDK changes its default roster: a newly added built-in would
// leak into the ACP "Agent" picker as if the user had configured it. This
// integration test runs the real SDK so the set is flagged the moment it does.
describe.skipIf(!process.env.RUN_INTEGRATION_TESTS)("agent discovery (SDK)", () => {
it("BUILTIN_AGENT_NAMES exactly covers the SDK's default agent roster", async () => {
const input = new Pushable<SDKUserMessage>();
const q = query({
prompt: input,
options: {
systemPrompt: { type: "preset", preset: "claude_code" },
sessionId: randomUUID(),
// No setting sources → no user/project/local custom agents are loaded,
// so `supportedAgents()` reports exactly the intrinsic built-ins
// regardless of what the developer has in ~/.claude/agents.
settingSources: [],
includePartialMessages: true,
},
});
try {
const reported = (await q.supportedAgents()).map((a) => a.name).sort();
expect(reported).toEqual([...BUILTIN_AGENT_NAMES].sort());
// With the built-ins being the only agents present, discovery surfaces
// nothing — the ACP "Agent" picker is correctly omitted.
expect(await discoverCustomAgents(q)).toEqual([]);
} finally {
input.end();
q.return(undefined);
}
}, 30000);
});

View File

@@ -28,7 +28,6 @@ function testSession(overrides: Partial<TestSession> = {}): TestSession {
},
models: { currentModelId: "default" },
configOptions: [],
currentAgent: "default",
fastModeEnabled: false,
input: { push: vi.fn() },
...overrides,
@@ -77,7 +76,6 @@ describe("continuePlanInFreshContext", () => {
turnQueue: [turn],
pendingExitPlanContextReset: reset,
models: { currentModelId: "claude-sonnet" },
currentAgent: "reviewer",
fastModeEnabled: true,
effortPinnedLevel: "high",
configOptions: [
@@ -109,7 +107,6 @@ describe("continuePlanInFreshContext", () => {
options: expect.objectContaining({
env: { PRESERVED: "yes" },
model: "claude-sonnet",
agent: "reviewer",
effort: "high",
}),
},
@@ -189,7 +186,6 @@ describe("continuePlanInFreshContext", () => {
const oldSession = testSession({
activeTurn: turn,
models: { currentModelId: "default" },
currentAgent: "default",
configOptions: [
{
id: "effort",

View File

@@ -125,6 +125,21 @@ describe("createSession options merging", () => {
expect(capturedOptions!.disallowedTools).toContain("AskUserQuestion");
});
it("ignores the provider-specific main-thread agent option", async () => {
const response = await agent.newSession({
cwd: process.cwd(),
mcpServers: [],
_meta: {
claudeCode: {
options: { agent: "reviewer" },
},
},
});
expect(capturedOptions).not.toHaveProperty("agent");
expect(response.configOptions?.some((option) => option.id === "agent")).toBe(false);
});
it("works when user provides empty disallowedTools", async () => {
await agent.newSession({
cwd: process.cwd(),

View File

@@ -151,7 +151,7 @@ describe("normalizeFastModeDisabledReason", () => {
describe("buildConfigOptions Fast mode", () => {
it("omits the Fast mode option when the model does not support it", () => {
const options = buildConfigOptions(MODES, MODELS, MODEL_INFOS, undefined, [], "default", {
const options = buildConfigOptions(MODES, MODELS, MODEL_INFOS, undefined, {
supported: false,
enabled: false,
useBooleanOption: true,
@@ -160,12 +160,12 @@ describe("buildConfigOptions Fast mode", () => {
});
it("omits the Fast mode option when no fast mode state is provided", () => {
const options = buildConfigOptions(MODES, MODELS, MODEL_INFOS, undefined, [], "default");
const options = buildConfigOptions(MODES, MODELS, MODEL_INFOS, undefined);
expect(options.find((o) => o.id === FAST_MODE_CONFIG_ID)).toBeUndefined();
});
it("surfaces a boolean toggle when supported and the client opted in", () => {
const options = buildConfigOptions(MODES, MODELS, MODEL_INFOS, undefined, [], "default", {
const options = buildConfigOptions(MODES, MODELS, MODEL_INFOS, undefined, {
supported: true,
enabled: true,
useBooleanOption: true,
@@ -174,7 +174,7 @@ describe("buildConfigOptions Fast mode", () => {
});
it("surfaces a select fallback when supported but the client did not opt in", () => {
const options = buildConfigOptions(MODES, MODELS, MODEL_INFOS, undefined, [], "default", {
const options = buildConfigOptions(MODES, MODELS, MODEL_INFOS, undefined, {
supported: true,
enabled: false,
useBooleanOption: false,

View File

@@ -136,8 +136,6 @@ describe("buildConfigOptions recommended values presentation", () => {
MODELS,
MODEL_INFOS,
undefined,
[],
"default",
undefined,
RECOMMENDED_PRESENTATION,
);
@@ -176,8 +174,6 @@ describe("buildConfigOptions recommended values presentation", () => {
{ ...MODELS, currentModelId: "opus" },
MODEL_INFOS,
"high",
[],
"default",
undefined,
RECOMMENDED_PRESENTATION,
);
@@ -198,8 +194,6 @@ describe("buildConfigOptions recommended values presentation", () => {
MODELS,
MODEL_INFOS.filter((model) => model.value === "default"),
undefined,
[],
"default",
undefined,
RECOMMENDED_PRESENTATION,
);
@@ -223,8 +217,6 @@ describe("buildConfigOptions recommended values presentation", () => {
MODELS,
infos,
undefined,
[],
"default",
undefined,
RECOMMENDED_PRESENTATION,
);
@@ -248,8 +240,6 @@ describe("buildConfigOptions recommended values presentation", () => {
MODELS,
infos,
undefined,
[],
"default",
undefined,
RECOMMENDED_PRESENTATION,
);
@@ -269,8 +259,6 @@ describe("buildConfigOptions recommended values presentation", () => {
MODELS,
infos,
undefined,
[],
"default",
undefined,
RECOMMENDED_PRESENTATION,
);

View File

@@ -74,8 +74,6 @@ export function mockSessionState(
accumulatedModelUsage: {},
lastModelUsageReading: {},
configOptions: [],
agents: [],
currentAgent: "default",
abortController: new AbortController(),
emitRawSDKMessages: false,
forwardSubagentText: false,