File size: 5,487 Bytes
f0634fb | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 | /**
* Build the unified `SessionConfigOption[]` surface advertised on
* `session/new` + `session/load` + `session/resume` and refreshed by
* `config_option_update`.
*
* The surface has up to three options:
* - `id: 'model'` (`type: 'select'`, `category: 'model'`) β one row per
* {@link AcpModelEntry}. Thinking is an orthogonal axis (separate toggle).
* - `id: 'thinking'` (`type: 'select'`, `category: 'thought_level'`) β
* appears ONLY when the currently-selected model's catalog row has
* `thinkingSupported === true`; otherwise omitted so the client doesn't
* render a non-actionable toggle. Encoded as a `select` for Zed
* compatibility (its chip strip only renders `select` options; the spec's
* `boolean` arm shows as "Unknown"). The entries adapt to the model's
* declared capability: a model with `supportEfforts` offers `off` plus
* every declared effort level; a boolean model keeps the plain
* `off` / `on` pair. `always_thinking` models drop the `off` entry.
* - `id: 'mode'` (`type: 'select'`, `category: 'mode'`) β the locked
* 4-mode taxonomy ({@link ACP_MODES}).
*/
import type { SessionConfigOption, SessionConfigSelectOption } from '@agentclientprotocol/sdk';
import { ACP_MODES, type AcpModeId } from './modes';
import type { AcpModelEntry } from './model-catalog';
/**
* Project the catalog into the `SessionConfigOption` `model` arm. One option
* row per catalog entry. `currentValue` is the bare model id.
*/
export function buildModelOption(
models: readonly AcpModelEntry[],
currentBaseModelId: string,
): SessionConfigOption {
const options: SessionConfigSelectOption[] = models.map((model) => ({
value: model.id,
name: model.name,
...(model.description !== undefined ? { description: model.description } : {}),
}));
return {
type: 'select',
id: 'model',
name: 'Model',
category: 'model',
currentValue: currentBaseModelId,
options,
};
}
/**
* Build the `thinking` select. Entries adapt to the model's declared
* capability: with `supportEfforts` the select is `off` plus every declared
* effort level; without them it stays the boolean `off` / `on` pair.
* `alwaysThinking` models drop the `off` entry (thinking cannot be disabled β
* ACP has no "disabled entry" concept). A `currentLevel` not present in the
* entries (e.g. an engine-resolved effort the catalog didn't declare) is
* appended so `currentValue` always stays selectable.
*/
export function buildThinkingOption(
currentLevel: string,
alwaysThinking = false,
supportEfforts?: readonly string[],
): SessionConfigOption {
const values = supportEfforts !== undefined ? ['off', ...supportEfforts] : ['off', 'on'];
const options: SessionConfigSelectOption[] = values
.filter((value) => !(alwaysThinking && value === 'off'))
.map((value) => ({ value, name: thinkingOptionName(value) }));
if (!options.some((option) => option.value === currentLevel)) {
options.push({ value: currentLevel, name: thinkingOptionName(currentLevel) });
}
return {
type: 'select',
id: 'thinking',
name: 'Thinking',
category: 'thought_level',
currentValue: currentLevel,
options,
};
}
/** Display name for a thinking select entry (`off` β `Thinking Off`). */
function thinkingOptionName(value: string): string {
return `Thinking ${value.charAt(0).toUpperCase()}${value.slice(1)}`;
}
/**
* Project the locked 4-mode taxonomy ({@link ACP_MODES}) into the
* `SessionConfigOption` `mode` arm. Order is preserved (default β plan β auto β
* yolo).
*/
export function buildModeOption(currentModeId: AcpModeId): SessionConfigOption {
const options: SessionConfigSelectOption[] = ACP_MODES.map((mode) => ({
value: mode.id,
name: mode.name,
description: mode.description,
}));
return {
type: 'select',
id: 'mode',
name: 'Mode',
category: 'mode',
currentValue: currentModeId,
options,
};
}
/**
* Compose the `SessionConfigOption[]` surface β
* `[modelOption, β¦(thinkingOption?), modeOption]`. Order is part of the
* contract: ACP clients render options top-to-bottom, model on top of mode.
*
* The thinking toggle only appears when the currently-selected base model is
* `thinkingSupported`; otherwise the snapshot is just `[modelOption, modeOption]`.
*
* Returns a mutable `SessionConfigOption[]` (rather than `readonly`) so the
* value is assignable to the SDK's `NewSessionResponse.configOptions` field,
* which is typed `Array<SessionConfigOption>`.
*/
export function buildSessionConfigOptions(
models: readonly AcpModelEntry[],
currentBaseModelId: string,
currentThinkingLevel: string,
currentModeId: AcpModeId,
): SessionConfigOption[] {
const currentModelEntry = models.find((m) => m.id === currentBaseModelId);
const showThinking = currentModelEntry?.thinkingSupported === true;
const alwaysThinking = currentModelEntry?.alwaysThinking === true;
const out: SessionConfigOption[] = [buildModelOption(models, currentBaseModelId)];
if (showThinking) {
// Always-thinking models render locked-on regardless of the session's
// recorded level β the runtime clamps the same way.
const level = alwaysThinking && currentThinkingLevel === 'off' ? 'on' : currentThinkingLevel;
out.push(buildThinkingOption(level, alwaysThinking, currentModelEntry?.supportEfforts));
}
out.push(buildModeOption(currentModeId));
return out;
}
|