mirror of
https://github.com/agentclientprotocol/claude-agent-acp.git
synced 2026-10-27 04:30:59 +00:00
feat: add ultracode session config option (#1438)
Adds an **Ultracode** session config option. It turns on Claude Code's
dynamic workflows for every task, at any effort level. Clients that
support boolean config options get a native switch; the rest get an
On/Off select. This follows the Fast mode pattern.
## How the CLI handles ultracode (2.1.310, checked against the bundled
binary and a live session)
- **Which models:** ultracode works on exactly the models that support
`xhigh` effort. The CLI uses the same check for both. So Opus 4.7+,
Sonnet 5+, Haiku 5.5 and Fable qualify; Opus 4.6 and Haiku 4.5 don't.
Effort level no longer matters.
- **Workflows:** dynamic workflows must be enabled. They are off by
default on Pro plans, and org or managed policy can turn them off.
- **State reporting:** `query.getSettings()` returns
`applied.ultracodeAvailable` and `applied.ultracodeRequested`. The CLI's
schema says hosts should "offer an Ultracode control only while this is
true". This method exists at runtime but is missing from the SDK's
`.d.ts`, so the adapter reads it with a cast and falls back to settings
and model data when it is absent.
- **Unsupported requests:** `applyFlagSettings({ultracode})` is never
validated. The request is stored and stays in place across model
switches; it takes effect whenever the model supports it.
- **Effort changes:** an `effortLevel` change sent without an
`ultracode` key turns ultracode off.
## Behavior
- **Visibility:** at session start, one `getSettings()` call decides
availability and the starting value. Model switches combine the
workflows result learned at start with the new model's
`supportedEffortLevels`, so the switch path makes no extra control
request.
- **Value:** the session tracks the user's choice, like
`fastModeEnabled`. The option hides on unsupported models and comes back
with the same value.
- **Effort applies:** `effortFlagSettings` now carries the session's
toggle instead of `settings.ultracode`. This applies at the initial
apply, on model switch, and from the effort picker, so effort changes no
longer turn ultracode off.
- **Clear context:** the clear-context restart restores the toggle, as
it does Fast mode.
## Known limitation
If a user types `/effort ultracode on` as a prompt, the toggle does not
update. The adapter already doesn't pick up effort changes typed as
`/effort` commands.
## Testing
- New `src/tests/ultracode-config.test.ts`. It covers the helpers and
session behavior: seeding from the CLI's report, hiding while workflows
are off, following the model's support, keeping the value across effort
and model switches, and rejected applies.
- New clear-context restore test. `effortFlagSettings` tests updated.
- Live probe against CLI 2.1.310 confirmed the `getSettings` fields, the
`xhigh` proxy (`claude-opus-4-6` reports unavailable while the request
stays in place), and the effort/key behavior.
- `npm run check`, `npm run build`, `npm run test:run` all pass.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -17,6 +17,7 @@ This tool implements an ACP agent by using the official [Claude Agent SDK](https
|
|||||||
- Interactive (and background) terminals
|
- Interactive (and background) terminals
|
||||||
- Custom [Slash commands](https://docs.anthropic.com/en/docs/claude-code/slash-commands)
|
- Custom [Slash commands](https://docs.anthropic.com/en/docs/claude-code/slash-commands)
|
||||||
- Client MCP servers
|
- Client MCP servers
|
||||||
|
- An Ultracode session config option, which turns on Claude Code's dynamic workflows for every task. It appears when dynamic workflows are enabled and the model supports ultracode (any model with `xhigh` effort)
|
||||||
- `/mcp` in the chat: the MCP server status as a list. The adapter runs `/mcp reconnect`, `/mcp enable`, and `/mcp disable` through the SDK control API, because Claude Code refuses them in SDK mode. A reconnect of an ACP server that needs authentication starts MCP OAuth through URL elicitation
|
- `/mcp` in the chat: the MCP server status as a list. The adapter runs `/mcp reconnect`, `/mcp enable`, and `/mcp disable` through the SDK control API, because Claude Code refuses them in SDK mode. A reconnect of an ACP server that needs authentication starts MCP OAuth through URL elicitation
|
||||||
- Session-scoped long-running goals for AIR through the [goal extension](docs/air-extensions.md#goal) under `_meta.jetbrains.air.goal`
|
- Session-scoped long-running goals for AIR through the [goal extension](docs/air-extensions.md#goal) under `_meta.jetbrains.air.goal`
|
||||||
- Structured errors, recovery, and warnings through the opt-in [session failure extension](docs/air-extensions.md#session-failure)
|
- Structured errors, recovery, and warnings through the opt-in [session failure extension](docs/air-extensions.md#session-failure)
|
||||||
|
|||||||
@@ -707,7 +707,7 @@ A non-empty plan also gets one clear-context choice with the same priority.
|
|||||||
After that choice, the adapter stops the old private Claude query and hides that internal rejection.
|
After that choice, the adapter stops the old private Claude query and hides that internal rejection.
|
||||||
It starts a new private query under the same ACP session.
|
It starts a new private query under the same ACP session.
|
||||||
It continues the turn with `Implement the following plan:` and the complete plan.
|
It continues the turn with `Implement the following plan:` and the complete plan.
|
||||||
The mode, model, agent, effort, Fast mode, and turn usage stay the same.
|
The mode, model, agent, effort, Fast mode, ultracode, and turn usage stay the same.
|
||||||
The SDK cannot apply `allowedPrompts` again, so those hints do not survive the handoff.
|
The SDK cannot apply `allowedPrompts` again, so those hints do not survive the handoff.
|
||||||
|
|
||||||
The adapter does not allow a callback only because the session mode is `bypassPermissions`.
|
The adapter does not allow a callback only because the session mode is `bypassPermissions`.
|
||||||
|
|||||||
@@ -365,6 +365,15 @@ import {
|
|||||||
mergeEffortSettings,
|
mergeEffortSettings,
|
||||||
settingsEffortForModel,
|
settingsEffortForModel,
|
||||||
} from "./session-effort.js";
|
} from "./session-effort.js";
|
||||||
|
import {
|
||||||
|
createUltracodeConfigOption,
|
||||||
|
readUltracodeSettings,
|
||||||
|
resolveUltracodeEnabled,
|
||||||
|
ULTRACODE_CONFIG_ID,
|
||||||
|
ultracodeSupported,
|
||||||
|
ultracodeWorkflowsEnabled,
|
||||||
|
type UltracodeOptionState,
|
||||||
|
} from "./session-ultracode.js";
|
||||||
|
|
||||||
export { EFFORT_CONFIG_ID, settingsEffortForModel } from "./session-effort.js";
|
export { EFFORT_CONFIG_ID, settingsEffortForModel } from "./session-effort.js";
|
||||||
export {
|
export {
|
||||||
@@ -1191,6 +1200,15 @@ export type Session = {
|
|||||||
* user's intent so it persists across model switches; the Fast mode config
|
* user's intent so it persists across model switches; the Fast mode config
|
||||||
* option is only surfaced while the selected model supports it. */
|
* option is only surfaced while the selected model supports it. */
|
||||||
fastModeEnabled: boolean;
|
fastModeEnabled: boolean;
|
||||||
|
/** Whether ultracode is turned on for this session. Tracked as the user's
|
||||||
|
* intent, like `fastModeEnabled`: the CLI keeps the request across model
|
||||||
|
* switches and applies it whenever the model supports ultracode, while the
|
||||||
|
* config option is only surfaced on supporting models. */
|
||||||
|
ultracodeEnabled: boolean;
|
||||||
|
/** Whether dynamic workflows are enabled, learned from the CLI's
|
||||||
|
* `get_settings` at session start; undefined when unknown. Combined with
|
||||||
|
* the model's support to decide whether to show the ultracode option. */
|
||||||
|
ultracodeWorkflowsEnabled?: boolean;
|
||||||
/** The non-default effort the user picked through the ACP picker this
|
/** The non-default effort the user picked through the ACP picker this
|
||||||
* session. A pin lives at the SDK's flag layer, which overrides the CLI's
|
* session. A pin lives at the SDK's flag layer, which overrides the CLI's
|
||||||
* persisted effort (including the per-model `modelSettings` entries), so it
|
* persisted effort (including the per-model `modelSettings` entries), so it
|
||||||
@@ -2801,6 +2819,7 @@ export class ClaudeAcpAgent {
|
|||||||
return session;
|
return session;
|
||||||
},
|
},
|
||||||
applyFastMode: (session, enabled) => this.applyFastMode(session, enabled),
|
applyFastMode: (session, enabled) => this.applyFastMode(session, enabled),
|
||||||
|
applyUltracode: (session, enabled) => this.applyUltracode(session, enabled),
|
||||||
sessionUpdate: (notification) => this.client.sessionUpdate(notification),
|
sessionUpdate: (notification) => this.client.sessionUpdate(notification),
|
||||||
supportsPlanOperations: () => this.clientCapabilities?.plan != null,
|
supportsPlanOperations: () => this.clientCapabilities?.plan != null,
|
||||||
ensureConsumer: (session, id) => this.ensureConsumer(session, id),
|
ensureConsumer: (session, id) => this.ensureConsumer(session, id),
|
||||||
@@ -9203,13 +9222,18 @@ export class ClaudeAcpAgent {
|
|||||||
throw new Error(`Unknown config option: ${params.configId}`);
|
throw new Error(`Unknown config option: ${params.configId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fast mode carries a boolean value (for Clients that opted into boolean
|
// Fast mode and ultracode carry a boolean value (for Clients that opted
|
||||||
// config options) or the "on"/"off" select fallback, so it bypasses the
|
// into boolean config options) or the "on"/"off" select fallback, so they
|
||||||
|
// bypass the
|
||||||
// string-only validation the select-style options below rely on.
|
// string-only validation the select-style options below rely on.
|
||||||
if (params.configId === FAST_MODE_CONFIG_ID) {
|
if (params.configId === FAST_MODE_CONFIG_ID) {
|
||||||
await this.applyFastMode(session, resolveFastModeEnabled(params));
|
await this.applyFastMode(session, resolveFastModeEnabled(params));
|
||||||
return { configOptions: session.configOptions };
|
return { configOptions: session.configOptions };
|
||||||
}
|
}
|
||||||
|
if (params.configId === ULTRACODE_CONFIG_ID) {
|
||||||
|
await this.applyUltracode(session, resolveUltracodeEnabled(params));
|
||||||
|
return { configOptions: session.configOptions };
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof params.value !== "string") {
|
if (typeof params.value !== "string") {
|
||||||
throw new Error(`Invalid value for config option ${params.configId}: ${params.value}`);
|
throw new Error(`Invalid value for config option ${params.configId}: ${params.value}`);
|
||||||
@@ -11038,6 +11062,13 @@ export class ClaudeAcpAgent {
|
|||||||
{
|
{
|
||||||
useRecommendedValue,
|
useRecommendedValue,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// Like Fast mode, the toggle hides on models without ultracode
|
||||||
|
// support and comes back with the retained intent on one that has it.
|
||||||
|
supported: ultracodeSupported(session.ultracodeWorkflowsEnabled, newModelInfo),
|
||||||
|
enabled: session.ultracodeEnabled,
|
||||||
|
useBooleanOption: clientSupportsBooleanConfigOptions(this.clientCapabilities),
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Opted-in clients apply the concrete displayed effort on every switch,
|
// Opted-in clients apply the concrete displayed effort on every switch,
|
||||||
@@ -11064,10 +11095,7 @@ export class ClaudeAcpAgent {
|
|||||||
// deliberately apply their concrete displayed effort.
|
// deliberately apply their concrete displayed effort.
|
||||||
effortFlagSettings(
|
effortFlagSettings(
|
||||||
useRecommendedValue ? newEffort : undefined,
|
useRecommendedValue ? newEffort : undefined,
|
||||||
mergeEffortSettings(
|
session.ultracodeEnabled,
|
||||||
session.settingsManager.getSettings(),
|
|
||||||
session.effortSettingsOverride,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
session.effortPinnedLevel = effortPinnedForNewModel ? pinnedEffort : undefined;
|
session.effortPinnedLevel = effortPinnedForNewModel ? pinnedEffort : undefined;
|
||||||
@@ -11136,15 +11164,7 @@ export class ClaudeAcpAgent {
|
|||||||
} else if (configId === EFFORT_CONFIG_ID) {
|
} else if (configId === EFFORT_CONFIG_ID) {
|
||||||
// Apply first so a rejected control request cannot leave the displayed
|
// Apply first so a rejected control request cannot leave the displayed
|
||||||
// value ahead of the SDK flag layer.
|
// value ahead of the SDK flag layer.
|
||||||
await session.query.applyFlagSettings(
|
await session.query.applyFlagSettings(effortFlagSettings(value, session.ultracodeEnabled));
|
||||||
effortFlagSettings(
|
|
||||||
value,
|
|
||||||
mergeEffortSettings(
|
|
||||||
session.settingsManager.getSettings(),
|
|
||||||
session.effortSettingsOverride,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
session.configOptions = session.configOptions.map((o) =>
|
session.configOptions = session.configOptions.map((o) =>
|
||||||
o.id === configId && typeof o.currentValue === "string" ? { ...o, currentValue: value } : o,
|
o.id === configId && typeof o.currentValue === "string" ? { ...o, currentValue: value } : o,
|
||||||
);
|
);
|
||||||
@@ -11226,6 +11246,22 @@ export class ClaudeAcpAgent {
|
|||||||
this.refreshFastModeOption(session, enabled);
|
this.refreshFastModeOption(session, enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Turn ultracode on or off for a session. Sends an explicit boolean, which
|
||||||
|
* leaves the effort level alone (`null` would reset the flag layer instead).
|
||||||
|
* The SDK flag is applied first so a rejected request leaves the session
|
||||||
|
* state and option untouched. */
|
||||||
|
private async applyUltracode(session: Session, enabled: boolean): Promise<void> {
|
||||||
|
await session.query.applyFlagSettings({ ultracode: enabled });
|
||||||
|
session.ultracodeEnabled = enabled;
|
||||||
|
const refreshed = createUltracodeConfigOption(
|
||||||
|
enabled,
|
||||||
|
clientSupportsBooleanConfigOptions(this.clientCapabilities),
|
||||||
|
);
|
||||||
|
session.configOptions = session.configOptions.map((o) =>
|
||||||
|
o.id === ULTRACODE_CONFIG_ID ? refreshed : o,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/** Reconcile the session's Fast mode toggle with an SDK-reported
|
/** Reconcile the session's Fast mode toggle with an SDK-reported
|
||||||
* `fast_mode_state` (delivered on `system`/init and on user-turn `result`s).
|
* `fast_mode_state` (delivered on `system`/init and on user-turn `result`s).
|
||||||
* The SDK can flip fast mode independently of the user — e.g. back to `on`
|
* The SDK can flip fast mode independently of the user — e.g. back to `on`
|
||||||
@@ -12064,6 +12100,21 @@ export class ClaudeAcpAgent {
|
|||||||
settingsManager.getSettings(),
|
settingsManager.getSettings(),
|
||||||
configuredSettingsObject,
|
configuredSettingsObject,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// The CLI reports whether ultracode can run (workflows enabled and a
|
||||||
|
// supporting model) and whether the session already requests it (an
|
||||||
|
// `ultracode: true` setting). Without a report, fall back to settings.
|
||||||
|
const ultracodeReport = await readUltracodeSettings(q);
|
||||||
|
timing.phase("ultracode");
|
||||||
|
const ultracodeEnabled = ultracodeReport?.requested ?? effortSettings.ultracode === true;
|
||||||
|
const workflowsEnabled = ultracodeWorkflowsEnabled(ultracodeReport, currentModelInfo);
|
||||||
|
const ultracode: UltracodeOptionState = {
|
||||||
|
supported:
|
||||||
|
ultracodeReport?.available ?? ultracodeSupported(workflowsEnabled, currentModelInfo),
|
||||||
|
enabled: ultracodeEnabled,
|
||||||
|
useBooleanOption: clientSupportsBooleanConfigOptions(this.clientCapabilities),
|
||||||
|
};
|
||||||
|
|
||||||
const configOptions = buildConfigOptions(
|
const configOptions = buildConfigOptions(
|
||||||
modes,
|
modes,
|
||||||
models,
|
models,
|
||||||
@@ -12075,10 +12126,11 @@ export class ClaudeAcpAgent {
|
|||||||
{
|
{
|
||||||
useRecommendedValue,
|
useRecommendedValue,
|
||||||
},
|
},
|
||||||
|
ultracode,
|
||||||
);
|
);
|
||||||
const initialEffort = configOptions.find((option) => option.id === EFFORT_CONFIG_ID);
|
const initialEffort = configOptions.find((option) => option.id === EFFORT_CONFIG_ID);
|
||||||
if (useRecommendedValue && typeof initialEffort?.currentValue === "string") {
|
if (useRecommendedValue && typeof initialEffort?.currentValue === "string") {
|
||||||
await q.applyFlagSettings(effortFlagSettings(initialEffort.currentValue, effortSettings));
|
await q.applyFlagSettings(effortFlagSettings(initialEffort.currentValue, ultracodeEnabled));
|
||||||
}
|
}
|
||||||
// Seed the context window without awaited IPC. The cached authoritative
|
// Seed the context window without awaited IPC. The cached authoritative
|
||||||
// window from a prior turn wins (`result.modelUsage`, cross-session),
|
// window from a prior turn wins (`result.modelUsage`, cross-session),
|
||||||
@@ -12146,6 +12198,8 @@ export class ClaudeAcpAgent {
|
|||||||
currentAgent,
|
currentAgent,
|
||||||
fastModeEnabled,
|
fastModeEnabled,
|
||||||
fastModeDisabledReason,
|
fastModeDisabledReason,
|
||||||
|
ultracodeEnabled,
|
||||||
|
ultracodeWorkflowsEnabled: workflowsEnabled,
|
||||||
abortController,
|
abortController,
|
||||||
emitRawSDKMessages: sessionMeta?.claudeCode?.emitRawSDKMessages ?? false,
|
emitRawSDKMessages: sessionMeta?.claudeCode?.emitRawSDKMessages ?? false,
|
||||||
forwardSubagentText,
|
forwardSubagentText,
|
||||||
@@ -12975,6 +13029,7 @@ export function buildConfigOptions(
|
|||||||
currentAgent: string = DEFAULT_AGENT_ID,
|
currentAgent: string = DEFAULT_AGENT_ID,
|
||||||
fastMode?: FastModeOptionState,
|
fastMode?: FastModeOptionState,
|
||||||
presentation?: ConfigOptionPresentation,
|
presentation?: ConfigOptionPresentation,
|
||||||
|
ultracode?: UltracodeOptionState,
|
||||||
): SessionConfigOption[] {
|
): SessionConfigOption[] {
|
||||||
const options: SessionConfigOption[] = [SessionModeManager.configOption(modes)];
|
const options: SessionConfigOption[] = [SessionModeManager.configOption(modes)];
|
||||||
// A gateway that lists none of the SDK's models leaves no model to offer, so
|
// A gateway that lists none of the SDK's models leaves no model to offer, so
|
||||||
@@ -13005,6 +13060,12 @@ export function buildConfigOptions(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Surface the ultracode toggle only while the CLI can run it: dynamic
|
||||||
|
// workflows are enabled and the current model supports it.
|
||||||
|
if (ultracode?.supported) {
|
||||||
|
options.push(createUltracodeConfigOption(ultracode.enabled, ultracode.useBooleanOption));
|
||||||
|
}
|
||||||
|
|
||||||
// Only surface the Agent picker when there's a real choice — i.e. the user
|
// 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
|
// has configured at least one custom agent (built-ins are filtered out in
|
||||||
// discoverCustomAgents). With none configured, "Default" would be the only
|
// discoverCustomAgents). With none configured, "Default" would be the only
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ export type ClearContextSession<Turn extends ClearContextTurn = ClearContextTurn
|
|||||||
configOptions: SessionConfigOption[];
|
configOptions: SessionConfigOption[];
|
||||||
currentAgent: string;
|
currentAgent: string;
|
||||||
fastModeEnabled: boolean;
|
fastModeEnabled: boolean;
|
||||||
|
ultracodeEnabled: boolean;
|
||||||
effortPinnedLevel?: string;
|
effortPinnedLevel?: string;
|
||||||
activeTurn?: Turn | null;
|
activeTurn?: Turn | null;
|
||||||
turnQueue?: Turn[];
|
turnQueue?: Turn[];
|
||||||
@@ -59,6 +60,7 @@ export type ClearContextCoordinatorHost<
|
|||||||
options: { publicSessionId: string; permissionMode: PermissionMode },
|
options: { publicSessionId: string; permissionMode: PermissionMode },
|
||||||
): Promise<Session>;
|
): Promise<Session>;
|
||||||
applyFastMode(session: Session, enabled: boolean): Promise<void>;
|
applyFastMode(session: Session, enabled: boolean): Promise<void>;
|
||||||
|
applyUltracode(session: Session, enabled: boolean): Promise<void>;
|
||||||
publishSessionState(
|
publishSessionState(
|
||||||
sessionId: string,
|
sessionId: string,
|
||||||
mode: PermissionMode,
|
mode: PermissionMode,
|
||||||
@@ -146,6 +148,16 @@ export async function continuePlanInFreshContext<
|
|||||||
}
|
}
|
||||||
assertRestartActive(freshSession);
|
assertRestartActive(freshSession);
|
||||||
}
|
}
|
||||||
|
// The fresh query seeds ultracode from settings only, so a toggle made
|
||||||
|
// during the old session would otherwise be lost.
|
||||||
|
if (oldSession.ultracodeEnabled !== freshSession.ultracodeEnabled) {
|
||||||
|
try {
|
||||||
|
await host.applyUltracode(freshSession, oldSession.ultracodeEnabled);
|
||||||
|
} catch (error) {
|
||||||
|
host.logError("Failed to restore ultracode after clearing context:", error);
|
||||||
|
}
|
||||||
|
assertRestartActive(freshSession);
|
||||||
|
}
|
||||||
|
|
||||||
oldSession.activeTurn = null;
|
oldSession.activeTurn = null;
|
||||||
oldSession.turnQueue = [];
|
oldSession.turnQueue = [];
|
||||||
|
|||||||
@@ -2,3 +2,4 @@
|
|||||||
export const DEFAULT_MODEL_ID = "default";
|
export const DEFAULT_MODEL_ID = "default";
|
||||||
export const DEFAULT_AGENT_ID = "default";
|
export const DEFAULT_AGENT_ID = "default";
|
||||||
export const EFFORT_CONFIG_ID = "effort";
|
export const EFFORT_CONFIG_ID = "effort";
|
||||||
|
export const ULTRACODE_CONFIG_ID = "ultracode";
|
||||||
|
|||||||
@@ -14,14 +14,15 @@ export function toSdkEffortLevel(value: string | undefined): EffortLevel | null
|
|||||||
|
|
||||||
// Since CLI 2.1.284 an `effortLevel` that changes the level, sent without an
|
// Since CLI 2.1.284 an `effortLevel` that changes the level, sent without an
|
||||||
// `ultracode` key, turns ultracode off. The CLI's own effort control keeps
|
// `ultracode` key, turns ultracode off. The CLI's own effort control keeps
|
||||||
// ultracode on at any level, so carry a settings-requested ultracode along
|
// ultracode on at any level, so send `ultracode: true` with every effort apply
|
||||||
// with every effort apply.
|
// while the session has ultracode on. When it is off, the key is omitted: an
|
||||||
|
// effort change then leaves ultracode off, as it already was.
|
||||||
export function effortFlagSettings(
|
export function effortFlagSettings(
|
||||||
value: string | undefined,
|
value: string | undefined,
|
||||||
settings: Settings,
|
ultracode: boolean,
|
||||||
): { effortLevel: EffortLevel | null; ultracode?: true } {
|
): { effortLevel: EffortLevel | null; ultracode?: true } {
|
||||||
const effortLevel = toSdkEffortLevel(value);
|
const effortLevel = toSdkEffortLevel(value);
|
||||||
return settings.ultracode === true ? { effortLevel, ultracode: true } : { effortLevel };
|
return ultracode ? { effortLevel, ultracode: true } : { effortLevel };
|
||||||
}
|
}
|
||||||
|
|
||||||
function canonicalizeModelSettingsKey(value: string): string {
|
function canonicalizeModelSettingsKey(value: string): string {
|
||||||
|
|||||||
136
src/session-ultracode.ts
Normal file
136
src/session-ultracode.ts
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
import type {
|
||||||
|
SessionConfigOption,
|
||||||
|
SetSessionConfigOptionRequest,
|
||||||
|
} from "@agentclientprotocol/sdk/unstable";
|
||||||
|
import type { ModelInfo, Query } from "@anthropic-ai/claude-agent-sdk";
|
||||||
|
import { ULTRACODE_CONFIG_ID } from "./session-config-ids.js";
|
||||||
|
|
||||||
|
export { ULTRACODE_CONFIG_ID } from "./session-config-ids.js";
|
||||||
|
|
||||||
|
/** Select-fallback values for clients without boolean config options. */
|
||||||
|
export const ULTRACODE_ON = "on";
|
||||||
|
export const ULTRACODE_OFF = "off";
|
||||||
|
const ULTRACODE_DESCRIPTION = "Dynamic workflows on every task, at any effort level";
|
||||||
|
|
||||||
|
/** Ultracode state from the CLI's `get_settings` control response
|
||||||
|
* (`applied.ultracodeAvailable` / `applied.ultracodeRequested`). Each field is
|
||||||
|
* undefined when the CLI does not report it. */
|
||||||
|
export type UltracodeSettingsReport = {
|
||||||
|
/** Workflows are enabled and the current model supports ultracode. */
|
||||||
|
available?: boolean;
|
||||||
|
/** Ultracode is turned on for the session, whether or not it can take
|
||||||
|
* effect on the current model. */
|
||||||
|
requested?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
// `Query.getSettings()` exists at runtime (sdk.mjs) but is missing from the
|
||||||
|
// SDK's type declarations as of 0.3.310.
|
||||||
|
type GetSettingsQuery = Query & {
|
||||||
|
getSettings?: () => Promise<{
|
||||||
|
applied?: { ultracodeAvailable?: unknown; ultracodeRequested?: unknown };
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Ask the CLI whether ultracode is available and requested for the session.
|
||||||
|
* Returns undefined when the SDK has no `getSettings` or the request fails;
|
||||||
|
* callers then fall back to {@link modelSupportsUltracode}. */
|
||||||
|
export async function readUltracodeSettings(
|
||||||
|
query: Query,
|
||||||
|
): Promise<UltracodeSettingsReport | undefined> {
|
||||||
|
const getSettings = (query as GetSettingsQuery).getSettings;
|
||||||
|
if (typeof getSettings !== "function") return undefined;
|
||||||
|
try {
|
||||||
|
const applied = (await getSettings.call(query))?.applied;
|
||||||
|
return {
|
||||||
|
available:
|
||||||
|
typeof applied?.ultracodeAvailable === "boolean" ? applied.ultracodeAvailable : undefined,
|
||||||
|
requested:
|
||||||
|
typeof applied?.ultracodeRequested === "boolean" ? applied.ultracodeRequested : undefined,
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The CLI supports ultracode on exactly the models that support `xhigh`
|
||||||
|
* effort (both use the same capability check), and `supportedEffortLevels`
|
||||||
|
* lists `xhigh` only for those models. E.g. Opus 4.7+, Sonnet 5+ and Haiku
|
||||||
|
* 5.5 qualify; Opus 4.6 and Haiku 4.5 do not. */
|
||||||
|
export function modelSupportsUltracode(modelInfo: ModelInfo | undefined): boolean {
|
||||||
|
return (
|
||||||
|
modelInfo?.supportsEffort === true &&
|
||||||
|
modelInfo.supportedEffortLevels?.includes("xhigh") === true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Whether dynamic workflows are enabled for the session, inferred from the
|
||||||
|
* CLI's `ultracodeAvailable` for the model the session started on. This is
|
||||||
|
* the part of availability that does not change with the model: the plan
|
||||||
|
* default (off on Pro), `enableWorkflows`/`disableWorkflows` and org policy.
|
||||||
|
* Undefined when the report can't tell, e.g. `ultracodeAvailable: false` on a
|
||||||
|
* model that lacks ultracode support anyway. */
|
||||||
|
export function ultracodeWorkflowsEnabled(
|
||||||
|
report: UltracodeSettingsReport | undefined,
|
||||||
|
modelInfo: ModelInfo | undefined,
|
||||||
|
): boolean | undefined {
|
||||||
|
if (report?.available === true) return true;
|
||||||
|
if (report?.available === false && modelSupportsUltracode(modelInfo)) return false;
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Whether to offer the ultracode toggle for `modelInfo`. An unknown workflows
|
||||||
|
* state shows the toggle: the CLI keeps an unavailable request without
|
||||||
|
* error, so the toggle is merely inert there. */
|
||||||
|
export function ultracodeSupported(
|
||||||
|
workflowsEnabled: boolean | undefined,
|
||||||
|
modelInfo: ModelInfo | undefined,
|
||||||
|
): boolean {
|
||||||
|
return workflowsEnabled !== false && modelSupportsUltracode(modelInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Ultracode state threaded into `buildConfigOptions`. The option is only
|
||||||
|
* surfaced while `supported`. */
|
||||||
|
export type UltracodeOptionState = {
|
||||||
|
supported: boolean;
|
||||||
|
enabled: boolean;
|
||||||
|
/** Whether the client opted into boolean config options. */
|
||||||
|
useBooleanOption: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Build the ultracode toggle: a native boolean for clients that opted into
|
||||||
|
* boolean config options, otherwise an "on"/"off" select. */
|
||||||
|
export function createUltracodeConfigOption(
|
||||||
|
enabled: boolean,
|
||||||
|
useBooleanOption: boolean,
|
||||||
|
): SessionConfigOption {
|
||||||
|
const base = {
|
||||||
|
id: ULTRACODE_CONFIG_ID,
|
||||||
|
name: "Ultracode",
|
||||||
|
description: ULTRACODE_DESCRIPTION,
|
||||||
|
category: "model_config",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
if (useBooleanOption) {
|
||||||
|
return { ...base, type: "boolean", currentValue: enabled };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
type: "select",
|
||||||
|
currentValue: enabled ? ULTRACODE_ON : ULTRACODE_OFF,
|
||||||
|
options: [
|
||||||
|
{ value: ULTRACODE_ON, name: "On" },
|
||||||
|
{ value: ULTRACODE_OFF, name: "Off" },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Read the requested ultracode value from a `session/set_config_option`
|
||||||
|
* request: a boolean, or the "on"/"off" select-fallback strings. */
|
||||||
|
export function resolveUltracodeEnabled(params: SetSessionConfigOptionRequest): boolean {
|
||||||
|
const value = params.value;
|
||||||
|
if (typeof value === "boolean") return value;
|
||||||
|
if (value === ULTRACODE_ON) return true;
|
||||||
|
if (value === ULTRACODE_OFF) return false;
|
||||||
|
throw new Error(`Invalid value for config option ${ULTRACODE_CONFIG_ID}: ${value}`);
|
||||||
|
}
|
||||||
@@ -13978,6 +13978,7 @@ describe("session/close", () => {
|
|||||||
agents: [],
|
agents: [],
|
||||||
currentAgent: "default",
|
currentAgent: "default",
|
||||||
fastModeEnabled: false,
|
fastModeEnabled: false,
|
||||||
|
ultracodeEnabled: false,
|
||||||
abortController: new AbortController(),
|
abortController: new AbortController(),
|
||||||
emitRawSDKMessages: false,
|
emitRawSDKMessages: false,
|
||||||
forwardSubagentText: false,
|
forwardSubagentText: false,
|
||||||
@@ -14099,6 +14100,7 @@ describe("session/delete", () => {
|
|||||||
agents: [],
|
agents: [],
|
||||||
currentAgent: "default",
|
currentAgent: "default",
|
||||||
fastModeEnabled: false,
|
fastModeEnabled: false,
|
||||||
|
ultracodeEnabled: false,
|
||||||
abortController: new AbortController(),
|
abortController: new AbortController(),
|
||||||
emitRawSDKMessages: false,
|
emitRawSDKMessages: false,
|
||||||
forwardSubagentText: false,
|
forwardSubagentText: false,
|
||||||
@@ -14258,6 +14260,7 @@ describe("getOrCreateSession param change detection", () => {
|
|||||||
agents: [],
|
agents: [],
|
||||||
currentAgent: "default",
|
currentAgent: "default",
|
||||||
fastModeEnabled: false,
|
fastModeEnabled: false,
|
||||||
|
ultracodeEnabled: false,
|
||||||
abortController: new AbortController(),
|
abortController: new AbortController(),
|
||||||
emitRawSDKMessages: false,
|
emitRawSDKMessages: false,
|
||||||
forwardSubagentText: false,
|
forwardSubagentText: false,
|
||||||
@@ -18712,6 +18715,7 @@ describe("post-error recovery", () => {
|
|||||||
agents: [],
|
agents: [],
|
||||||
currentAgent: "default",
|
currentAgent: "default",
|
||||||
fastModeEnabled: false,
|
fastModeEnabled: false,
|
||||||
|
ultracodeEnabled: false,
|
||||||
abortController: new AbortController(),
|
abortController: new AbortController(),
|
||||||
emitRawSDKMessages: false,
|
emitRawSDKMessages: false,
|
||||||
forwardSubagentText: false,
|
forwardSubagentText: false,
|
||||||
@@ -27068,6 +27072,7 @@ describe("session/cancel wedge recovery (issue #680)", () => {
|
|||||||
agents: [],
|
agents: [],
|
||||||
currentAgent: "default",
|
currentAgent: "default",
|
||||||
fastModeEnabled: false,
|
fastModeEnabled: false,
|
||||||
|
ultracodeEnabled: false,
|
||||||
abortController: new AbortController(),
|
abortController: new AbortController(),
|
||||||
emitRawSDKMessages: false,
|
emitRawSDKMessages: false,
|
||||||
forwardSubagentText: false,
|
forwardSubagentText: false,
|
||||||
@@ -28975,6 +28980,7 @@ describe("agent selection config option", () => {
|
|||||||
agents,
|
agents,
|
||||||
currentAgent: "default",
|
currentAgent: "default",
|
||||||
fastModeEnabled: false,
|
fastModeEnabled: false,
|
||||||
|
ultracodeEnabled: false,
|
||||||
abortController: new AbortController(),
|
abortController: new AbortController(),
|
||||||
emitRawSDKMessages: false,
|
emitRawSDKMessages: false,
|
||||||
forwardSubagentText: false,
|
forwardSubagentText: false,
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ function testSession(overrides: Partial<TestSession> = {}): TestSession {
|
|||||||
configOptions: [],
|
configOptions: [],
|
||||||
currentAgent: "default",
|
currentAgent: "default",
|
||||||
fastModeEnabled: false,
|
fastModeEnabled: false,
|
||||||
|
ultracodeEnabled: false,
|
||||||
input: { push: vi.fn() },
|
input: { push: vi.fn() },
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
@@ -49,6 +50,7 @@ function testHost(
|
|||||||
return freshSession;
|
return freshSession;
|
||||||
}),
|
}),
|
||||||
applyFastMode: vi.fn(async () => {}),
|
applyFastMode: vi.fn(async () => {}),
|
||||||
|
applyUltracode: vi.fn(async () => {}),
|
||||||
publishSessionState: vi.fn(async () => {}),
|
publishSessionState: vi.fn(async () => {}),
|
||||||
continuationMessage: vi.fn((sessionId, plan, promptUuid) => ({
|
continuationMessage: vi.fn((sessionId, plan, promptUuid) => ({
|
||||||
type: "user" as const,
|
type: "user" as const,
|
||||||
@@ -252,6 +254,25 @@ describe("continuePlanInFreshContext", () => {
|
|||||||
expect(host.applyFastMode).toHaveBeenCalledWith(freshSession, false);
|
expect(host.applyFastMode).toHaveBeenCalledWith(freshSession, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("restores ultracode turned on before clearing context", async () => {
|
||||||
|
const turn: ClearContextTurn = {
|
||||||
|
settled: false,
|
||||||
|
promptUuid: "00000000-0000-4000-8000-000000000000",
|
||||||
|
};
|
||||||
|
const oldSession = testSession({ activeTurn: turn, ultracodeEnabled: true });
|
||||||
|
const freshSession = testSession({ ultracodeEnabled: false });
|
||||||
|
const host = testHost(oldSession, freshSession);
|
||||||
|
|
||||||
|
await continuePlanInFreshContext(
|
||||||
|
"public-session",
|
||||||
|
oldSession,
|
||||||
|
{ toolUseId: "tool-plan", plan: "Ship it", mode: "auto" },
|
||||||
|
host,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(host.applyUltracode).toHaveBeenCalledWith(freshSession, true);
|
||||||
|
});
|
||||||
|
|
||||||
it("does not publish or continue a restart cancelled while creating the fresh session", async () => {
|
it("does not publish or continue a restart cancelled while creating the fresh session", async () => {
|
||||||
const turn: ClearContextTurn = {
|
const turn: ClearContextTurn = {
|
||||||
settled: false,
|
settled: false,
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ export function makeMockQuery(overrides: Record<string, unknown> = {}) {
|
|||||||
supportedCommands: async () => [],
|
supportedCommands: async () => [],
|
||||||
mcpServerStatus: async () => [],
|
mcpServerStatus: async () => [],
|
||||||
getContextUsage: async () => DEFAULT_CONTEXT_USAGE,
|
getContextUsage: async () => DEFAULT_CONTEXT_USAGE,
|
||||||
|
// No ultracode fields: session creation falls back to settings and the
|
||||||
|
// model's effort levels, as with a CLI that does not report them.
|
||||||
|
getSettings: async () => ({ applied: {} }),
|
||||||
close: () => {},
|
close: () => {},
|
||||||
interrupt: async () => undefined,
|
interrupt: async () => undefined,
|
||||||
[Symbol.asyncIterator]: async function* () {},
|
[Symbol.asyncIterator]: async function* () {},
|
||||||
|
|||||||
@@ -381,17 +381,17 @@ describe("settingsEffortForModel", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("effortFlagSettings", () => {
|
describe("effortFlagSettings", () => {
|
||||||
it("carries ultracode only when settings request it", () => {
|
it("carries ultracode only while the session has it on", () => {
|
||||||
expect(effortFlagSettings("high", { ultracode: true })).toEqual({
|
expect(effortFlagSettings("high", true)).toEqual({
|
||||||
effortLevel: "high",
|
effortLevel: "high",
|
||||||
ultracode: true,
|
ultracode: true,
|
||||||
});
|
});
|
||||||
expect(effortFlagSettings("default", { ultracode: true })).toEqual({
|
expect(effortFlagSettings("default", true)).toEqual({
|
||||||
effortLevel: null,
|
effortLevel: null,
|
||||||
ultracode: true,
|
ultracode: true,
|
||||||
});
|
});
|
||||||
expect(effortFlagSettings("high", { ultracode: false })).toEqual({ effortLevel: "high" });
|
expect(effortFlagSettings("high", false)).toEqual({ effortLevel: "high" });
|
||||||
expect(effortFlagSettings(undefined, {})).toEqual({ effortLevel: null });
|
expect(effortFlagSettings(undefined, false)).toEqual({ effortLevel: null });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
292
src/tests/ultracode-config.test.ts
Normal file
292
src/tests/ultracode-config.test.ts
Normal file
@@ -0,0 +1,292 @@
|
|||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import * as fs from "node:fs";
|
||||||
|
import * as os from "node:os";
|
||||||
|
import * as path from "node:path";
|
||||||
|
import type { ClientCapabilities } from "@agentclientprotocol/sdk/unstable";
|
||||||
|
import type { ModelInfo, Query } from "@anthropic-ai/claude-agent-sdk";
|
||||||
|
import type { AcpClient, ClaudeAcpAgent as ClaudeAcpAgentType } from "../acp-agent.js";
|
||||||
|
import {
|
||||||
|
createUltracodeConfigOption,
|
||||||
|
modelSupportsUltracode,
|
||||||
|
readUltracodeSettings,
|
||||||
|
resolveUltracodeEnabled,
|
||||||
|
ULTRACODE_CONFIG_ID,
|
||||||
|
ULTRACODE_OFF,
|
||||||
|
ULTRACODE_ON,
|
||||||
|
ultracodeSupported,
|
||||||
|
ultracodeWorkflowsEnabled,
|
||||||
|
} from "../session-ultracode.js";
|
||||||
|
import { makeMockQuery } from "./helpers.js";
|
||||||
|
|
||||||
|
const { querySpy } = vi.hoisted(() => ({ querySpy: vi.fn() }));
|
||||||
|
|
||||||
|
vi.mock("@anthropic-ai/claude-agent-sdk", async () => {
|
||||||
|
const actual = await vi.importActual<any>("@anthropic-ai/claude-agent-sdk");
|
||||||
|
return { ...actual, query: querySpy };
|
||||||
|
});
|
||||||
|
|
||||||
|
const OPUS: ModelInfo = {
|
||||||
|
value: "opus",
|
||||||
|
displayName: "Opus",
|
||||||
|
description: "",
|
||||||
|
supportsEffort: true,
|
||||||
|
supportedEffortLevels: ["low", "medium", "high", "xhigh", "max"],
|
||||||
|
};
|
||||||
|
const FABLE: ModelInfo = {
|
||||||
|
value: "fable",
|
||||||
|
displayName: "Fable",
|
||||||
|
description: "",
|
||||||
|
supportsEffort: true,
|
||||||
|
supportedEffortLevels: ["low", "medium", "high", "xhigh", "max"],
|
||||||
|
};
|
||||||
|
// Opus 4.6 and Haiku 4.5 have no `xhigh` effort, so the CLI has no ultracode
|
||||||
|
// for them either.
|
||||||
|
const OLD_OPUS: ModelInfo = {
|
||||||
|
value: "claude-opus-4-6",
|
||||||
|
displayName: "Opus 4.6",
|
||||||
|
description: "",
|
||||||
|
supportsEffort: true,
|
||||||
|
supportedEffortLevels: ["low", "medium", "high", "max"],
|
||||||
|
};
|
||||||
|
|
||||||
|
function getSettingsQuery(getSettings: unknown): Query {
|
||||||
|
return { getSettings } as unknown as Query;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("ultracode helpers", () => {
|
||||||
|
it("builds a boolean option or an on/off select", () => {
|
||||||
|
expect(createUltracodeConfigOption(true, true)).toEqual({
|
||||||
|
id: ULTRACODE_CONFIG_ID,
|
||||||
|
name: "Ultracode",
|
||||||
|
description: "Dynamic workflows on every task, at any effort level",
|
||||||
|
category: "model_config",
|
||||||
|
type: "boolean",
|
||||||
|
currentValue: true,
|
||||||
|
});
|
||||||
|
expect(createUltracodeConfigOption(false, false)).toMatchObject({
|
||||||
|
type: "select",
|
||||||
|
currentValue: ULTRACODE_OFF,
|
||||||
|
options: [
|
||||||
|
{ value: ULTRACODE_ON, name: "On" },
|
||||||
|
{ value: ULTRACODE_OFF, name: "Off" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("accepts booleans and the select strings", () => {
|
||||||
|
const request = (value: unknown) =>
|
||||||
|
({ sessionId: "s", configId: ULTRACODE_CONFIG_ID, value }) as never;
|
||||||
|
expect(resolveUltracodeEnabled(request(true))).toBe(true);
|
||||||
|
expect(resolveUltracodeEnabled(request(ULTRACODE_OFF))).toBe(false);
|
||||||
|
expect(() => resolveUltracodeEnabled(request("maybe"))).toThrow(
|
||||||
|
"Invalid value for config option ultracode: maybe",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("treats a model as supporting ultracode when it supports xhigh effort", () => {
|
||||||
|
expect(modelSupportsUltracode(OPUS)).toBe(true);
|
||||||
|
expect(modelSupportsUltracode(OLD_OPUS)).toBe(false);
|
||||||
|
expect(modelSupportsUltracode({ ...OPUS, supportsEffort: false })).toBe(false);
|
||||||
|
expect(modelSupportsUltracode(undefined)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("infers the workflows gate only when the start model can tell", () => {
|
||||||
|
expect(ultracodeWorkflowsEnabled({ available: true }, OLD_OPUS)).toBe(true);
|
||||||
|
expect(ultracodeWorkflowsEnabled({ available: false }, OPUS)).toBe(false);
|
||||||
|
// Opus 4.6 is unavailable regardless of workflows.
|
||||||
|
expect(ultracodeWorkflowsEnabled({ available: false }, OLD_OPUS)).toBeUndefined();
|
||||||
|
expect(ultracodeWorkflowsEnabled(undefined, OPUS)).toBeUndefined();
|
||||||
|
|
||||||
|
expect(ultracodeSupported(undefined, OPUS)).toBe(true);
|
||||||
|
expect(ultracodeSupported(false, OPUS)).toBe(false);
|
||||||
|
expect(ultracodeSupported(true, OLD_OPUS)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reads the CLI's ultracode report and tolerates CLIs without one", async () => {
|
||||||
|
await expect(
|
||||||
|
readUltracodeSettings(
|
||||||
|
getSettingsQuery(async () => ({
|
||||||
|
applied: { ultracodeAvailable: true, ultracodeRequested: false },
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
).resolves.toEqual({ available: true, requested: false });
|
||||||
|
await expect(
|
||||||
|
readUltracodeSettings(getSettingsQuery(async () => ({ applied: {} }))),
|
||||||
|
).resolves.toEqual({ available: undefined, requested: undefined });
|
||||||
|
await expect(readUltracodeSettings(getSettingsQuery(undefined))).resolves.toBeUndefined();
|
||||||
|
await expect(
|
||||||
|
readUltracodeSettings(
|
||||||
|
getSettingsQuery(async () => {
|
||||||
|
throw new Error("unsupported control request");
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
).resolves.toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("ultracode session config option", () => {
|
||||||
|
let tempDir: string;
|
||||||
|
let originalClaudeConfigDir: string | undefined;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "acp-ultracode-"));
|
||||||
|
originalClaudeConfigDir = process.env.CLAUDE_CONFIG_DIR;
|
||||||
|
process.env.CLAUDE_CONFIG_DIR = tempDir;
|
||||||
|
querySpy.mockReset();
|
||||||
|
vi.resetModules();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
if (originalClaudeConfigDir) {
|
||||||
|
process.env.CLAUDE_CONFIG_DIR = originalClaudeConfigDir;
|
||||||
|
} else {
|
||||||
|
delete process.env.CLAUDE_CONFIG_DIR;
|
||||||
|
}
|
||||||
|
await fs.promises.rm(tempDir, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
async function createSession(opts: {
|
||||||
|
models?: ModelInfo[];
|
||||||
|
getSettings?: () => Promise<unknown>;
|
||||||
|
settings?: Record<string, unknown>;
|
||||||
|
clientCapabilities?: ClientCapabilities;
|
||||||
|
}) {
|
||||||
|
if (opts.settings) {
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
path.join(tempDir, "settings.json"),
|
||||||
|
JSON.stringify(opts.settings),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const applyFlagSettings = vi.fn();
|
||||||
|
querySpy.mockReturnValue(
|
||||||
|
makeMockQuery({
|
||||||
|
initializationResult: async () => ({ models: opts.models ?? [OPUS, FABLE, OLD_OPUS] }),
|
||||||
|
applyFlagSettings,
|
||||||
|
...(opts.getSettings ? { getSettings: opts.getSettings } : {}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const { ClaudeAcpAgent } = await import("../acp-agent.js");
|
||||||
|
const agent: ClaudeAcpAgentType = new ClaudeAcpAgent({
|
||||||
|
sessionUpdate: async () => {},
|
||||||
|
requestPermission: async () => ({ outcome: { outcome: "cancelled" } }),
|
||||||
|
readTextFile: async () => ({ content: "" }),
|
||||||
|
writeTextFile: async () => ({}),
|
||||||
|
} as unknown as AcpClient);
|
||||||
|
(agent as any).clientCapabilities = opts.clientCapabilities ?? {
|
||||||
|
session: { configOptions: { boolean: {} } },
|
||||||
|
};
|
||||||
|
const response = await (agent as any).createSession({
|
||||||
|
cwd: tempDir,
|
||||||
|
mcpServers: [],
|
||||||
|
_meta: { disableBuiltInTools: true },
|
||||||
|
});
|
||||||
|
const sessionId: string = response.sessionId;
|
||||||
|
const ultracodeOption = () =>
|
||||||
|
agent.sessions[sessionId].configOptions.find((o) => o.id === ULTRACODE_CONFIG_ID);
|
||||||
|
return { agent, sessionId, applyFlagSettings, ultracodeOption };
|
||||||
|
}
|
||||||
|
|
||||||
|
it("seeds the option from the CLI's report", async () => {
|
||||||
|
const { ultracodeOption, agent, sessionId } = await createSession({
|
||||||
|
getSettings: async () => ({
|
||||||
|
applied: { ultracodeAvailable: true, ultracodeRequested: true },
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
expect(ultracodeOption()).toEqual(createUltracodeConfigOption(true, true));
|
||||||
|
expect(agent.sessions[sessionId].ultracodeEnabled).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("hides the option while dynamic workflows are off, across model switches", async () => {
|
||||||
|
const { agent, sessionId, ultracodeOption } = await createSession({
|
||||||
|
getSettings: async () => ({
|
||||||
|
applied: { ultracodeAvailable: false, ultracodeRequested: false },
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
expect(ultracodeOption()).toBeUndefined();
|
||||||
|
|
||||||
|
await agent.setSessionConfigOption({ sessionId, configId: "model", value: "fable" });
|
||||||
|
expect(ultracodeOption()).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("follows the model's support and keeps the toggle across switches", async () => {
|
||||||
|
const { agent, sessionId, applyFlagSettings, ultracodeOption } = await createSession({
|
||||||
|
clientCapabilities: {},
|
||||||
|
});
|
||||||
|
expect(ultracodeOption()).toEqual(createUltracodeConfigOption(false, false));
|
||||||
|
|
||||||
|
await agent.setSessionConfigOption({
|
||||||
|
sessionId,
|
||||||
|
configId: ULTRACODE_CONFIG_ID,
|
||||||
|
value: ULTRACODE_ON,
|
||||||
|
});
|
||||||
|
expect(applyFlagSettings).toHaveBeenLastCalledWith({ ultracode: true });
|
||||||
|
|
||||||
|
await agent.setSessionConfigOption({ sessionId, configId: "model", value: "claude-opus-4-6" });
|
||||||
|
expect(ultracodeOption()).toBeUndefined();
|
||||||
|
|
||||||
|
await agent.setSessionConfigOption({ sessionId, configId: "model", value: "fable" });
|
||||||
|
expect(ultracodeOption()).toEqual(createUltracodeConfigOption(true, false));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps a toggled-on ultracode when effort changes, and not a toggled-off one", async () => {
|
||||||
|
// An effortLevel apply without an ultracode key turns ultracode off, so
|
||||||
|
// the effort picker must carry the session's toggle.
|
||||||
|
const { agent, sessionId, applyFlagSettings } = await createSession({
|
||||||
|
settings: { ultracode: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
await agent.setSessionConfigOption({
|
||||||
|
sessionId,
|
||||||
|
configId: ULTRACODE_CONFIG_ID,
|
||||||
|
type: "boolean",
|
||||||
|
value: false,
|
||||||
|
});
|
||||||
|
expect(applyFlagSettings).toHaveBeenLastCalledWith({ ultracode: false });
|
||||||
|
await agent.setSessionConfigOption({ sessionId, configId: "effort", value: "high" });
|
||||||
|
expect(applyFlagSettings).toHaveBeenLastCalledWith({ effortLevel: "high" });
|
||||||
|
|
||||||
|
await agent.setSessionConfigOption({
|
||||||
|
sessionId,
|
||||||
|
configId: ULTRACODE_CONFIG_ID,
|
||||||
|
type: "boolean",
|
||||||
|
value: true,
|
||||||
|
});
|
||||||
|
await agent.setSessionConfigOption({ sessionId, configId: "effort", value: "low" });
|
||||||
|
expect(applyFlagSettings).toHaveBeenLastCalledWith({ effortLevel: "low", ultracode: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps ultracode on through the effort apply that follows a model switch", async () => {
|
||||||
|
// recommendedValue clients apply the new model's effort on every switch.
|
||||||
|
const { agent, sessionId, applyFlagSettings } = await createSession({
|
||||||
|
clientCapabilities: {
|
||||||
|
session: { configOptions: { boolean: {} } },
|
||||||
|
_meta: { jetbrains: { air: { version: 1, capabilities: ["recommendedValue"] } } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await agent.setSessionConfigOption({
|
||||||
|
sessionId,
|
||||||
|
configId: ULTRACODE_CONFIG_ID,
|
||||||
|
type: "boolean",
|
||||||
|
value: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
await agent.setSessionConfigOption({ sessionId, configId: "model", value: "fable" });
|
||||||
|
expect(applyFlagSettings).toHaveBeenLastCalledWith({ effortLevel: "medium", ultracode: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not change session state when the SDK rejects the flag", async () => {
|
||||||
|
const { agent, sessionId, applyFlagSettings, ultracodeOption } = await createSession({});
|
||||||
|
applyFlagSettings.mockRejectedValueOnce(new Error("nope"));
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
agent.setSessionConfigOption({
|
||||||
|
sessionId,
|
||||||
|
configId: ULTRACODE_CONFIG_ID,
|
||||||
|
type: "boolean",
|
||||||
|
value: true,
|
||||||
|
}),
|
||||||
|
).rejects.toThrow("nope");
|
||||||
|
expect(agent.sessions[sessionId].ultracodeEnabled).toBe(false);
|
||||||
|
expect(ultracodeOption()).toEqual(createUltracodeConfigOption(false, true));
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user