Replace the raw per-agent mode dropdown in the BooCoder composer with a curated three-option permission ladder mapped generically onto each provider's native modes: `plan` id -> Plan, default -> Ask, isUnattended -> Bypass (claude bypassPermissions, qwen yolo, opencode full-access). modeId stays the single wire field; the active unified mode is derived from it (no contracts change). Native BooCode gains its own mode set: Ask stages to the pending-changes queue (today's behavior), Bypass auto-applies the queue to disk after the turn (interactive messages path + task dispatcher path), Plan falls back to Ask. The shared apps/server inference engine is left untouched. Also preserve isUnattended on live-probed ACP modes so opencode's bypass mode stays detectable from the wire. Coder 373 tests green; coder + web typecheck clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
136 lines
4.2 KiB
TypeScript
136 lines
4.2 KiB
TypeScript
/**
|
|
* ACP model/mode derivation — adapted from Paseo acp-agent.ts.
|
|
*/
|
|
import type {
|
|
SessionConfigOption,
|
|
SessionModelState,
|
|
SessionModeState,
|
|
} from '@agentclientprotocol/sdk';
|
|
import type { ProviderMode, ProviderModel, ThinkingOption } from './provider-types.js';
|
|
|
|
type SelectConfigOption = Extract<SessionConfigOption, { type: 'select' }>;
|
|
|
|
interface SelectConfigChoice {
|
|
value: string;
|
|
name: string;
|
|
description?: string | null;
|
|
group?: string;
|
|
}
|
|
|
|
function findSelectConfigOption({
|
|
configOptions,
|
|
category,
|
|
id,
|
|
}: {
|
|
configOptions: SessionConfigOption[] | null | undefined;
|
|
category: string;
|
|
id?: string;
|
|
}): SelectConfigOption | null {
|
|
const option = configOptions?.find(
|
|
(entry): entry is SelectConfigOption =>
|
|
entry.type === 'select' && entry.category === category && (!id || entry.id === id),
|
|
);
|
|
return option ?? null;
|
|
}
|
|
|
|
function flattenSelectOptions(options: SelectConfigOption['options']): SelectConfigChoice[] {
|
|
const flattened: SelectConfigChoice[] = [];
|
|
for (const option of options) {
|
|
if ('value' in option) {
|
|
flattened.push(option);
|
|
continue;
|
|
}
|
|
for (const groupOption of option.options) {
|
|
flattened.push({ ...groupOption, group: option.group });
|
|
}
|
|
}
|
|
return flattened;
|
|
}
|
|
|
|
function deriveSelectorOptions(
|
|
configOptions: SessionConfigOption[] | null | undefined,
|
|
category: string,
|
|
): ThinkingOption[] {
|
|
const option = findSelectConfigOption({ configOptions, category });
|
|
if (!option) return [];
|
|
|
|
return flattenSelectOptions(option.options).map((value) => ({
|
|
id: value.value,
|
|
label: value.name,
|
|
isDefault: value.value === option.currentValue,
|
|
}));
|
|
}
|
|
|
|
export function deriveModesFromACP(
|
|
fallbackModes: ProviderMode[],
|
|
modeState?: SessionModeState | null,
|
|
configOptions?: SessionConfigOption[] | null,
|
|
): { modes: ProviderMode[]; currentModeId: string | null } {
|
|
if (modeState?.availableModes?.length) {
|
|
return {
|
|
// ACP omits the unattended flag; inherit it from the manifest fallback by
|
|
// id so the unified permission picker can still detect each agent's bypass
|
|
// mode (e.g. opencode `full-access`) from live-probed modes.
|
|
modes: modeState.availableModes.map((mode) => {
|
|
const fb = fallbackModes.find((f) => f.id === mode.id);
|
|
return {
|
|
id: mode.id,
|
|
label: mode.name,
|
|
description: mode.description ?? undefined,
|
|
...(fb?.isUnattended ? { isUnattended: true } : {}),
|
|
};
|
|
}),
|
|
currentModeId: modeState.currentModeId ?? null,
|
|
};
|
|
}
|
|
|
|
const modeOption = findSelectConfigOption({ configOptions, category: 'mode' });
|
|
if (modeOption) {
|
|
const flatOptions = flattenSelectOptions(modeOption.options);
|
|
return {
|
|
modes: flatOptions.map((option) => ({
|
|
id: option.value,
|
|
label: option.name,
|
|
description: option.description ?? undefined,
|
|
})),
|
|
currentModeId: modeOption.currentValue,
|
|
};
|
|
}
|
|
|
|
return { modes: fallbackModes, currentModeId: null };
|
|
}
|
|
|
|
export function deriveModelDefinitionsFromACP(
|
|
models: SessionModelState | null | undefined,
|
|
configOptions?: SessionConfigOption[] | null,
|
|
): ProviderModel[] {
|
|
const thinkingOptions = deriveSelectorOptions(configOptions, 'thought_level');
|
|
const defaultThinkingOptionId = thinkingOptions.find((o) => o.isDefault)?.id;
|
|
|
|
if (models?.availableModels?.length) {
|
|
return models.availableModels.map((model) => ({
|
|
id: model.modelId,
|
|
label: model.name,
|
|
description: model.description ?? undefined,
|
|
isDefault: model.modelId === models.currentModelId,
|
|
thinkingOptions: thinkingOptions.length > 0 ? thinkingOptions : undefined,
|
|
defaultThinkingOptionId: defaultThinkingOptionId ?? undefined,
|
|
}));
|
|
}
|
|
|
|
const modelOptions = deriveSelectorOptions(configOptions, 'model');
|
|
return modelOptions.map((option) => ({
|
|
id: option.id,
|
|
label: option.label,
|
|
isDefault: option.isDefault,
|
|
thinkingOptions: thinkingOptions.length > 0 ? thinkingOptions : undefined,
|
|
defaultThinkingOptionId: defaultThinkingOptionId ?? undefined,
|
|
}));
|
|
}
|
|
|
|
export function findThoughtLevelConfigId(
|
|
configOptions: SessionConfigOption[] | null | undefined,
|
|
): string | null {
|
|
return findSelectConfigOption({ configOptions, category: 'thought_level' })?.id ?? null;
|
|
}
|