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;
}