@theia/ai-core-ui 1.75.0-next.8 → 1.75.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/browser/ai-configuration/ai-configuration-category-registry.d.ts +29 -0
- package/lib/browser/ai-configuration/ai-configuration-category-registry.d.ts.map +1 -0
- package/lib/browser/ai-configuration/ai-configuration-category-registry.js +83 -0
- package/lib/browser/ai-configuration/ai-configuration-category-registry.js.map +1 -0
- package/lib/browser/ai-configuration/ai-configuration-category-registry.spec.d.ts +2 -0
- package/lib/browser/ai-configuration/ai-configuration-category-registry.spec.d.ts.map +1 -0
- package/lib/browser/ai-configuration/ai-configuration-category-registry.spec.js +122 -0
- package/lib/browser/ai-configuration/ai-configuration-category-registry.spec.js.map +1 -0
- package/lib/browser/ai-configuration/ai-configuration-category.d.ts +204 -0
- package/lib/browser/ai-configuration/ai-configuration-category.d.ts.map +1 -0
- package/lib/browser/ai-configuration/ai-configuration-category.js +86 -0
- package/lib/browser/ai-configuration/ai-configuration-category.js.map +1 -0
- package/lib/browser/ai-configuration/ai-configuration-selection-model.d.ts +15 -0
- package/lib/browser/ai-configuration/ai-configuration-selection-model.d.ts.map +1 -0
- package/lib/browser/ai-configuration/ai-configuration-selection-model.js +44 -0
- package/lib/browser/ai-configuration/ai-configuration-selection-model.js.map +1 -0
- package/lib/browser/ai-configuration/ai-configuration-setting-commands.d.ts +48 -0
- package/lib/browser/ai-configuration/ai-configuration-setting-commands.d.ts.map +1 -0
- package/lib/browser/ai-configuration/ai-configuration-setting-commands.js +98 -0
- package/lib/browser/ai-configuration/ai-configuration-setting-commands.js.map +1 -0
- package/lib/browser/ai-configuration/ai-configuration-setting-commands.spec.d.ts +2 -0
- package/lib/browser/ai-configuration/ai-configuration-setting-commands.spec.d.ts.map +1 -0
- package/lib/browser/ai-configuration/ai-configuration-setting-commands.spec.js +70 -0
- package/lib/browser/ai-configuration/ai-configuration-setting-commands.spec.js.map +1 -0
- package/lib/browser/ai-configuration/components/ai-configuration-components.spec.d.ts +2 -0
- package/lib/browser/ai-configuration/components/ai-configuration-components.spec.d.ts.map +1 -0
- package/lib/browser/ai-configuration/components/ai-configuration-components.spec.js +596 -0
- package/lib/browser/ai-configuration/components/ai-configuration-components.spec.js.map +1 -0
- package/lib/browser/ai-configuration/components/ai-configuration-controls.d.ts +118 -0
- package/lib/browser/ai-configuration/components/ai-configuration-controls.d.ts.map +1 -0
- package/lib/browser/ai-configuration/components/ai-configuration-controls.js +191 -0
- package/lib/browser/ai-configuration/components/ai-configuration-controls.js.map +1 -0
- package/lib/browser/ai-configuration/components/ai-configuration-item-row.d.ts +57 -0
- package/lib/browser/ai-configuration/components/ai-configuration-item-row.d.ts.map +1 -0
- package/lib/browser/ai-configuration/components/ai-configuration-item-row.js +48 -0
- package/lib/browser/ai-configuration/components/ai-configuration-item-row.js.map +1 -0
- package/lib/browser/ai-configuration/components/ai-configuration-origin-badge.d.ts +49 -0
- package/lib/browser/ai-configuration/components/ai-configuration-origin-badge.d.ts.map +1 -0
- package/lib/browser/ai-configuration/components/ai-configuration-origin-badge.js +79 -0
- package/lib/browser/ai-configuration/components/ai-configuration-origin-badge.js.map +1 -0
- package/lib/browser/ai-configuration/components/ai-configuration-primitives.d.ts +161 -0
- package/lib/browser/ai-configuration/components/ai-configuration-primitives.d.ts.map +1 -0
- package/lib/browser/ai-configuration/components/ai-configuration-primitives.js +131 -0
- package/lib/browser/ai-configuration/components/ai-configuration-primitives.js.map +1 -0
- package/lib/browser/ai-configuration/components/ai-configuration-setting-row.d.ts +54 -0
- package/lib/browser/ai-configuration/components/ai-configuration-setting-row.d.ts.map +1 -0
- package/lib/browser/ai-configuration/components/ai-configuration-setting-row.js +77 -0
- package/lib/browser/ai-configuration/components/ai-configuration-setting-row.js.map +1 -0
- package/lib/browser/ai-configuration/components/ai-settings-row-service.d.ts +194 -0
- package/lib/browser/ai-configuration/components/ai-settings-row-service.d.ts.map +1 -0
- package/lib/browser/ai-configuration/components/ai-settings-row-service.js +352 -0
- package/lib/browser/ai-configuration/components/ai-settings-row-service.js.map +1 -0
- package/lib/browser/ai-configuration/components/ai-settings-row-service.spec.d.ts +2 -0
- package/lib/browser/ai-configuration/components/ai-settings-row-service.spec.d.ts.map +1 -0
- package/lib/browser/ai-configuration/components/ai-settings-row-service.spec.js +233 -0
- package/lib/browser/ai-configuration/components/ai-settings-row-service.spec.js.map +1 -0
- package/lib/browser/ai-configuration/components/ai-settings-row.d.ts +50 -0
- package/lib/browser/ai-configuration/components/ai-settings-row.d.ts.map +1 -0
- package/lib/browser/ai-configuration/components/ai-settings-row.js +85 -0
- package/lib/browser/ai-configuration/components/ai-settings-row.js.map +1 -0
- package/lib/browser/ai-configuration/components/collapsible-list.d.ts +65 -0
- package/lib/browser/ai-configuration/components/collapsible-list.d.ts.map +1 -0
- package/lib/browser/ai-configuration/components/collapsible-list.js +82 -0
- package/lib/browser/ai-configuration/components/collapsible-list.js.map +1 -0
- package/lib/browser/ai-configuration/components/prompt-customization-dialogs.d.ts +35 -0
- package/lib/browser/ai-configuration/components/prompt-customization-dialogs.d.ts.map +1 -0
- package/lib/browser/ai-configuration/components/prompt-customization-dialogs.js +83 -0
- package/lib/browser/ai-configuration/components/prompt-customization-dialogs.js.map +1 -0
- package/lib/browser/ai-configuration/components/variant-set-card.d.ts +25 -0
- package/lib/browser/ai-configuration/components/variant-set-card.d.ts.map +1 -0
- package/lib/browser/ai-configuration/components/variant-set-card.js +133 -0
- package/lib/browser/ai-configuration/components/variant-set-card.js.map +1 -0
- package/lib/browser/ai-configuration/renderers/collection-category-renderer.d.ts +44 -0
- package/lib/browser/ai-configuration/renderers/collection-category-renderer.d.ts.map +1 -0
- package/lib/browser/ai-configuration/renderers/collection-category-renderer.js +106 -0
- package/lib/browser/ai-configuration/renderers/collection-category-renderer.js.map +1 -0
- package/lib/browser/ai-configuration/renderers/collection-category-renderer.spec.d.ts +2 -0
- package/lib/browser/ai-configuration/renderers/collection-category-renderer.spec.d.ts.map +1 -0
- package/lib/browser/ai-configuration/renderers/collection-category-renderer.spec.js +120 -0
- package/lib/browser/ai-configuration/renderers/collection-category-renderer.spec.js.map +1 -0
- package/lib/browser/ai-configuration/renderers/single-page-category-renderer.d.ts +15 -0
- package/lib/browser/ai-configuration/renderers/single-page-category-renderer.d.ts.map +1 -0
- package/lib/browser/ai-configuration/renderers/single-page-category-renderer.js +20 -0
- package/lib/browser/ai-configuration/renderers/single-page-category-renderer.js.map +1 -0
- package/lib/browser/ai-core-ui-frontend-module.d.ts +1 -0
- package/lib/browser/ai-core-ui-frontend-module.d.ts.map +1 -1
- package/lib/browser/ai-core-ui-frontend-module.js +11 -0
- package/lib/browser/ai-core-ui-frontend-module.js.map +1 -1
- package/package.json +6 -5
- package/src/browser/ai-configuration/ai-configuration-category-registry.spec.ts +137 -0
- package/src/browser/ai-configuration/ai-configuration-category-registry.ts +79 -0
- package/src/browser/ai-configuration/ai-configuration-category.ts +246 -0
- package/src/browser/ai-configuration/ai-configuration-selection-model.ts +42 -0
- package/src/browser/ai-configuration/ai-configuration-setting-commands.spec.ts +82 -0
- package/src/browser/ai-configuration/ai-configuration-setting-commands.ts +104 -0
- package/src/browser/ai-configuration/components/ai-configuration-components.spec.ts +655 -0
- package/src/browser/ai-configuration/components/ai-configuration-controls.tsx +382 -0
- package/src/browser/ai-configuration/components/ai-configuration-item-row.tsx +127 -0
- package/src/browser/ai-configuration/components/ai-configuration-origin-badge.tsx +114 -0
- package/src/browser/ai-configuration/components/ai-configuration-primitives.tsx +359 -0
- package/src/browser/ai-configuration/components/ai-configuration-setting-row.tsx +154 -0
- package/src/browser/ai-configuration/components/ai-settings-row-service.spec.ts +275 -0
- package/src/browser/ai-configuration/components/ai-settings-row-service.ts +401 -0
- package/src/browser/ai-configuration/components/ai-settings-row.tsx +176 -0
- package/src/browser/ai-configuration/components/collapsible-list.tsx +205 -0
- package/src/browser/ai-configuration/components/prompt-customization-dialogs.ts +89 -0
- package/src/browser/ai-configuration/components/variant-set-card.tsx +200 -0
- package/src/browser/ai-configuration/renderers/collection-category-renderer.spec.ts +126 -0
- package/src/browser/ai-configuration/renderers/collection-category-renderer.tsx +164 -0
- package/src/browser/ai-configuration/renderers/single-page-category-renderer.tsx +41 -0
- package/src/browser/ai-core-ui-frontend-module.ts +14 -1
- package/src/browser/style/ai-configuration-components.css +1410 -0
- package/src/browser/style/index.css +17 -0
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 EclipseSource and others.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Presentational, DI-free form controls shared across the AI configuration pages so that a given
|
|
19
|
+
* logical control (a toggle, a text input, a select, a numeric stepper, a chip/tag editor) looks
|
|
20
|
+
* and behaves the same regardless of which category renders it. Owners pass in the current value
|
|
21
|
+
* and an `onChange`/`onCommit` callback; controls that edit free text commit on blur/Enter so that
|
|
22
|
+
* typing does not write a preference per keystroke.
|
|
23
|
+
*
|
|
24
|
+
* All classes are prefixed `ai-config-` and are styled in `ai-configuration-components.css`.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { nls } from '@theia/core';
|
|
28
|
+
import { codicon } from '@theia/core/lib/browser';
|
|
29
|
+
import { SelectComponent, SelectOption } from '@theia/core/lib/browser/widgets/select-component';
|
|
30
|
+
import * as React from '@theia/core/shared/react';
|
|
31
|
+
|
|
32
|
+
/** Accessible toggle switch backed by a real checkbox; `large` renders the hero-sized variant. */
|
|
33
|
+
export const AiToggleSwitch: React.FC<{
|
|
34
|
+
checked: boolean;
|
|
35
|
+
ariaLabel: string;
|
|
36
|
+
disabled?: boolean;
|
|
37
|
+
large?: boolean;
|
|
38
|
+
onChange: (checked: boolean) => void;
|
|
39
|
+
}> = ({ checked, ariaLabel, disabled, large, onChange }) => (
|
|
40
|
+
<label className={`ai-config-switch${large ? ' ai-config-switch-large' : ''}`}>
|
|
41
|
+
<input
|
|
42
|
+
type='checkbox'
|
|
43
|
+
checked={checked}
|
|
44
|
+
disabled={disabled}
|
|
45
|
+
aria-label={ariaLabel}
|
|
46
|
+
onChange={e => onChange(e.target.checked)}
|
|
47
|
+
/>
|
|
48
|
+
<span className='ai-config-switch-track'><span className='ai-config-switch-thumb'></span></span>
|
|
49
|
+
</label>
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
/** Single-line text input committing on blur or Enter (so typing does not write a preference per keystroke). */
|
|
53
|
+
export const AiTextInput: React.FC<{
|
|
54
|
+
value: string;
|
|
55
|
+
ariaLabel: string;
|
|
56
|
+
placeholder?: string;
|
|
57
|
+
disabled?: boolean;
|
|
58
|
+
monospace?: boolean;
|
|
59
|
+
/** Masks the value (`type=password`) and disables autocomplete/spellcheck; used for API keys and secrets. */
|
|
60
|
+
password?: boolean;
|
|
61
|
+
/** Marks the input as invalid (error border) and, with {@link errorMessage}, shows the message beneath it. */
|
|
62
|
+
invalid?: boolean;
|
|
63
|
+
/** Validation message shown under the input when {@link invalid} is set. */
|
|
64
|
+
errorMessage?: string;
|
|
65
|
+
onCommit: (value: string) => void;
|
|
66
|
+
}> = ({ value, ariaLabel, placeholder, disabled, monospace, password, invalid, errorMessage, onCommit }) => {
|
|
67
|
+
const [draft, setDraft] = React.useState(value);
|
|
68
|
+
React.useEffect(() => setDraft(value), [value]);
|
|
69
|
+
const commitIfChanged = (): void => {
|
|
70
|
+
if (draft !== value) {
|
|
71
|
+
onCommit(draft);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
return <span className='ai-config-input-wrap'>
|
|
75
|
+
<input
|
|
76
|
+
className={`theia-input ai-config-input${monospace ? ' ai-config-input-mono' : ''}${invalid ? ' error' : ''}`}
|
|
77
|
+
type={password ? 'password' : 'text'}
|
|
78
|
+
value={draft}
|
|
79
|
+
aria-label={ariaLabel}
|
|
80
|
+
placeholder={placeholder}
|
|
81
|
+
disabled={disabled}
|
|
82
|
+
autoComplete={password ? 'off' : undefined}
|
|
83
|
+
spellCheck={false}
|
|
84
|
+
onChange={e => setDraft(e.target.value)}
|
|
85
|
+
onBlur={commitIfChanged}
|
|
86
|
+
onKeyDown={e => { if (e.key === 'Enter') { commitIfChanged(); } }}
|
|
87
|
+
/>
|
|
88
|
+
{invalid && errorMessage && <span className='ai-config-input-error'>{errorMessage}</span>}
|
|
89
|
+
</span>;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/** Keeps a value inside the preference's `minimum`/`maximum`, either of which may be absent. */
|
|
93
|
+
export function clampNumber(value: number, min: number | undefined, max: number | undefined): number {
|
|
94
|
+
let result = value;
|
|
95
|
+
if (min !== undefined) {
|
|
96
|
+
result = Math.max(min, result);
|
|
97
|
+
}
|
|
98
|
+
if (max !== undefined) {
|
|
99
|
+
result = Math.min(max, result);
|
|
100
|
+
}
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** A `− value +` numeric stepper. Commits on blur/Enter (typing) and immediately on the buttons. */
|
|
105
|
+
export const AiNumberStepper: React.FC<{
|
|
106
|
+
value: number;
|
|
107
|
+
ariaLabel: string;
|
|
108
|
+
/**
|
|
109
|
+
* Whole numbers only, i.e. the preference is declared `integer`. A `number` preference keeps what the
|
|
110
|
+
* user typed: rounding it would turn a delay of `1.5` seconds into `2` on commit.
|
|
111
|
+
*/
|
|
112
|
+
integer?: boolean;
|
|
113
|
+
min?: number;
|
|
114
|
+
max?: number;
|
|
115
|
+
disabled?: boolean;
|
|
116
|
+
onCommit: (value: number) => void;
|
|
117
|
+
}> = ({ value, ariaLabel, integer, min, max, disabled, onCommit }) => {
|
|
118
|
+
const [draft, setDraft] = React.useState(String(value));
|
|
119
|
+
React.useEffect(() => setDraft(String(value)), [value]);
|
|
120
|
+
const commitDraft = (): void => {
|
|
121
|
+
const parsed = Number(draft);
|
|
122
|
+
const rounded = integer ? Math.round(parsed) : parsed;
|
|
123
|
+
const next = clampNumber(Number.isNaN(parsed) ? value : rounded, min, max);
|
|
124
|
+
setDraft(String(next));
|
|
125
|
+
if (next !== value) {
|
|
126
|
+
onCommit(next);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
const step = (delta: number): void => {
|
|
130
|
+
const next = clampNumber(value + delta, min, max);
|
|
131
|
+
if (next !== value) {
|
|
132
|
+
onCommit(next);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
return <span className='ai-config-stepper-wrap'>
|
|
136
|
+
<span className='ai-config-stepper'>
|
|
137
|
+
<button
|
|
138
|
+
type='button'
|
|
139
|
+
className='ai-config-stepper-button'
|
|
140
|
+
aria-label={nls.localize('theia/ai/core/aiConfiguration/decrease', 'Decrease')}
|
|
141
|
+
disabled={disabled || (min !== undefined && value <= min)}
|
|
142
|
+
onClick={() => step(-1)}
|
|
143
|
+
>−</button>
|
|
144
|
+
<input
|
|
145
|
+
className='ai-config-stepper-input'
|
|
146
|
+
inputMode='numeric'
|
|
147
|
+
aria-label={ariaLabel}
|
|
148
|
+
value={draft}
|
|
149
|
+
disabled={disabled}
|
|
150
|
+
onChange={e => setDraft(e.target.value)}
|
|
151
|
+
onBlur={commitDraft}
|
|
152
|
+
onKeyDown={e => { if (e.key === 'Enter') { commitDraft(); } }}
|
|
153
|
+
/>
|
|
154
|
+
<button
|
|
155
|
+
type='button'
|
|
156
|
+
className='ai-config-stepper-button'
|
|
157
|
+
aria-label={nls.localize('theia/ai/core/aiConfiguration/increase', 'Increase')}
|
|
158
|
+
disabled={disabled || (max !== undefined && value >= max)}
|
|
159
|
+
onClick={() => step(1)}
|
|
160
|
+
>+</button>
|
|
161
|
+
</span>
|
|
162
|
+
</span>;
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* A `string[]` editor mirroring the Settings UI array input: each value is a list row with a hover-revealed
|
|
167
|
+
* remove button, and a final "Add Value…" input (Enter or the + button appends). It reuses the Settings UI's
|
|
168
|
+
* DOM shape and class names (styled by our own `.ai-config-array` rules) so it looks identical, without
|
|
169
|
+
* depending on the preferences package's DI-bound renderer. Values commit immediately on add/remove.
|
|
170
|
+
*/
|
|
171
|
+
export const AiArrayInput: React.FC<{
|
|
172
|
+
values: string[];
|
|
173
|
+
ariaLabel: string;
|
|
174
|
+
addPlaceholder: string;
|
|
175
|
+
disabled?: boolean;
|
|
176
|
+
/** Returns why an entry is not acceptable, or `undefined` to accept it. Rejected entries are not added. */
|
|
177
|
+
validate?: (value: string) => string | undefined;
|
|
178
|
+
onChange: (values: string[]) => void;
|
|
179
|
+
}> = ({ values, ariaLabel, addPlaceholder, disabled, validate, onChange }) => {
|
|
180
|
+
const [draft, setDraft] = React.useState('');
|
|
181
|
+
const [error, setError] = React.useState<string | undefined>(undefined);
|
|
182
|
+
const addItem = (): void => {
|
|
183
|
+
const entry = draft.trim();
|
|
184
|
+
if (!entry) {
|
|
185
|
+
setDraft('');
|
|
186
|
+
setError(undefined);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const rejection = validate?.(entry);
|
|
190
|
+
if (rejection) {
|
|
191
|
+
// Keep the text so the user can correct it rather than retype it.
|
|
192
|
+
setError(rejection);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
setError(undefined);
|
|
196
|
+
if (!values.includes(entry)) {
|
|
197
|
+
onChange([...values, entry]);
|
|
198
|
+
}
|
|
199
|
+
setDraft('');
|
|
200
|
+
};
|
|
201
|
+
const removeAt = (index: number): void => onChange(values.filter((_, itemIndex) => itemIndex !== index));
|
|
202
|
+
return <ul className='preference-array ai-config-array'>
|
|
203
|
+
{values.map((value, index) => (
|
|
204
|
+
<li className='preference-array-element' key={`${value}-${index}`}>
|
|
205
|
+
<span className='preference-array-element-val'>{value}</span>
|
|
206
|
+
<span
|
|
207
|
+
className='preference-array-element-btn remove-btn'
|
|
208
|
+
role='button'
|
|
209
|
+
tabIndex={0}
|
|
210
|
+
title={nls.localizeByDefault('Remove')}
|
|
211
|
+
onClick={() => { if (!disabled) { removeAt(index); } }}
|
|
212
|
+
>
|
|
213
|
+
<i className={codicon('close')}></i>
|
|
214
|
+
</span>
|
|
215
|
+
</li>
|
|
216
|
+
))}
|
|
217
|
+
<li className='ai-config-array-input-row'>
|
|
218
|
+
<input
|
|
219
|
+
className='preference-array-input theia-input'
|
|
220
|
+
type='text'
|
|
221
|
+
aria-label={ariaLabel}
|
|
222
|
+
placeholder={addPlaceholder}
|
|
223
|
+
spellCheck={false}
|
|
224
|
+
value={draft}
|
|
225
|
+
disabled={disabled}
|
|
226
|
+
onChange={e => { setDraft(e.target.value); setError(undefined); }}
|
|
227
|
+
onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); addItem(); } }}
|
|
228
|
+
/>
|
|
229
|
+
<span
|
|
230
|
+
className={`preference-array-element-btn ${codicon('add')}`}
|
|
231
|
+
role='button'
|
|
232
|
+
tabIndex={0}
|
|
233
|
+
title={nls.localizeByDefault('Add')}
|
|
234
|
+
onClick={() => { if (!disabled) { addItem(); } }}
|
|
235
|
+
></span>
|
|
236
|
+
</li>
|
|
237
|
+
{error && <li className='ai-config-array-error'><span className='ai-config-input-error'>{error}</span></li>}
|
|
238
|
+
</ul>;
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
/** A monospace path input plus a "Browse…" button that delegates folder selection to `onBrowse`. */
|
|
242
|
+
export const AiPathInput: React.FC<{
|
|
243
|
+
value: string;
|
|
244
|
+
placeholder: string;
|
|
245
|
+
browseLabel: string;
|
|
246
|
+
disabled?: boolean;
|
|
247
|
+
onCommit: (value: string) => void;
|
|
248
|
+
onBrowse: () => Promise<string | undefined>;
|
|
249
|
+
}> = ({ value, placeholder, browseLabel, disabled, onCommit, onBrowse }) => {
|
|
250
|
+
const [draft, setDraft] = React.useState(value);
|
|
251
|
+
React.useEffect(() => setDraft(value), [value]);
|
|
252
|
+
const commitDraft = (): void => {
|
|
253
|
+
if (draft !== value) {
|
|
254
|
+
onCommit(draft);
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
const browse = (): void => {
|
|
258
|
+
onBrowse().then(picked => {
|
|
259
|
+
if (picked !== undefined) {
|
|
260
|
+
setDraft(picked);
|
|
261
|
+
onCommit(picked);
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
};
|
|
265
|
+
return <div className='ai-config-path-row'>
|
|
266
|
+
<input
|
|
267
|
+
className='theia-input ai-config-input ai-config-input-mono'
|
|
268
|
+
type='text'
|
|
269
|
+
value={draft}
|
|
270
|
+
placeholder={placeholder}
|
|
271
|
+
disabled={disabled}
|
|
272
|
+
onChange={e => setDraft(e.target.value)}
|
|
273
|
+
onBlur={commitDraft}
|
|
274
|
+
onKeyDown={e => { if (e.key === 'Enter') { commitDraft(); } }}
|
|
275
|
+
/>
|
|
276
|
+
<button type='button' className='theia-button secondary' disabled={disabled} onClick={browse}>{browseLabel}</button>
|
|
277
|
+
</div>;
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* A button that hands editing off to the `settings.json` file. Used for preferences whose value is a
|
|
282
|
+
* complex object (or array of objects) that cannot be edited meaningfully through an inline control;
|
|
283
|
+
* clicking it opens `settings.json` focused on the preference, mirroring the Settings view's
|
|
284
|
+
* "Edit in settings.json" link.
|
|
285
|
+
*/
|
|
286
|
+
export const AiEditInSettingsButton: React.FC<{
|
|
287
|
+
label: string;
|
|
288
|
+
ariaLabel: string;
|
|
289
|
+
disabled?: boolean;
|
|
290
|
+
onClick: () => void;
|
|
291
|
+
}> = ({ label, ariaLabel, disabled, onClick }) => (
|
|
292
|
+
<button
|
|
293
|
+
type='button'
|
|
294
|
+
className='theia-button secondary ai-config-edit-in-settings'
|
|
295
|
+
aria-label={ariaLabel}
|
|
296
|
+
disabled={disabled}
|
|
297
|
+
onClick={onClick}
|
|
298
|
+
>
|
|
299
|
+
<span className={codicon('edit')}></span>
|
|
300
|
+
{label}
|
|
301
|
+
</button>
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
/** A single option for {@link AiEnumSelect}. */
|
|
305
|
+
export interface AiEnumOption {
|
|
306
|
+
readonly value: string;
|
|
307
|
+
readonly label: string;
|
|
308
|
+
/** Optional native tooltip, e.g. to surface an option's secondary description. */
|
|
309
|
+
readonly title?: string;
|
|
310
|
+
readonly disabled?: boolean;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* A theme-styled select over a fixed set of options, backed by the shared {@link SelectComponent} so
|
|
315
|
+
* that each option can surface its own description (which native `<option>` cannot) and the control
|
|
316
|
+
* matches the Settings UI. Commits the selected value via `onCommit`.
|
|
317
|
+
*/
|
|
318
|
+
export const AiEnumSelect: React.FC<{
|
|
319
|
+
value: string | undefined;
|
|
320
|
+
options: AiEnumOption[];
|
|
321
|
+
ariaLabel: string;
|
|
322
|
+
className?: string;
|
|
323
|
+
disabled?: boolean;
|
|
324
|
+
invalid?: boolean;
|
|
325
|
+
onCommit: (value: string) => void;
|
|
326
|
+
// `ariaLabel` is intentionally not destructured: SelectComponent exposes no accessible-name prop yet.
|
|
327
|
+
}> = ({ value, options, className, disabled, invalid, onCommit }) => {
|
|
328
|
+
// `title` becomes the per-option `description` shown by SelectComponent's dropdown.
|
|
329
|
+
const selectOptions = React.useMemo<SelectOption[]>(() => options.map(option => ({
|
|
330
|
+
value: option.value,
|
|
331
|
+
label: option.label,
|
|
332
|
+
description: option.title,
|
|
333
|
+
disabled: option.disabled
|
|
334
|
+
})), [options]);
|
|
335
|
+
const handleChange = React.useCallback((option: SelectOption): void => {
|
|
336
|
+
// SelectComponent has no whole-control `disabled`; guard the commit so a disabled select never writes.
|
|
337
|
+
if (!disabled && option.value !== undefined) {
|
|
338
|
+
onCommit(option.value);
|
|
339
|
+
}
|
|
340
|
+
}, [disabled, onCommit]);
|
|
341
|
+
const componentClassName = `ai-config-select${invalid ? ' error' : ''}${disabled ? ' disabled' : ''}${className ? ' ' + className : ''}`;
|
|
342
|
+
return <SelectComponent
|
|
343
|
+
options={selectOptions}
|
|
344
|
+
defaultValue={value}
|
|
345
|
+
onChange={handleChange}
|
|
346
|
+
className={componentClassName}
|
|
347
|
+
/>;
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
/** A single non-"Limited" choice for {@link AiSessionLimitControl}, mapping a label to its magic value. */
|
|
351
|
+
export interface SessionLimitSpecialOption {
|
|
352
|
+
readonly value: number;
|
|
353
|
+
readonly label: string;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* A plain numeric input for a session-limit preference. Any special sentinel values (e.g. -1 = unlimited,
|
|
358
|
+
* 0 = don't persist) can be entered directly, instead of being hidden behind a separate
|
|
359
|
+
* "Limited / Unlimited / …" dropdown; the preference description explains what they mean.
|
|
360
|
+
*/
|
|
361
|
+
export const AiSessionLimitControl: React.FC<{
|
|
362
|
+
value: number;
|
|
363
|
+
limitedMin: number;
|
|
364
|
+
limitedMax?: number;
|
|
365
|
+
specials: SessionLimitSpecialOption[];
|
|
366
|
+
ariaLabel: string;
|
|
367
|
+
disabled?: boolean;
|
|
368
|
+
onCommit: (value: number) => void;
|
|
369
|
+
}> = ({ value, limitedMin, limitedMax, specials, ariaLabel, disabled, onCommit }) => {
|
|
370
|
+
// Special sentinel values sit below the "limited" minimum, so the input has to allow entering them.
|
|
371
|
+
const min = specials.reduce((smallest, special) => Math.min(smallest, special.value), limitedMin);
|
|
372
|
+
return <AiNumberStepper
|
|
373
|
+
value={value}
|
|
374
|
+
ariaLabel={ariaLabel}
|
|
375
|
+
// A session limit is a count, so it stays a whole number even though the stepper allows fractions.
|
|
376
|
+
integer={true}
|
|
377
|
+
min={min}
|
|
378
|
+
max={limitedMax}
|
|
379
|
+
disabled={disabled}
|
|
380
|
+
onCommit={onCommit}
|
|
381
|
+
/>;
|
|
382
|
+
};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 EclipseSource and others.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
import { codicon } from '@theia/core/lib/browser';
|
|
18
|
+
import * as React from '@theia/core/shared/react';
|
|
19
|
+
import { AiConfigurationItemStatus } from '../ai-configuration-category';
|
|
20
|
+
import { AiConfigurationKindBadge, AiConfigurationStatusBadge } from './ai-configuration-primitives';
|
|
21
|
+
import { AiConfigurationOrigin, AiConfigurationOriginBadges } from './ai-configuration-origin-badge';
|
|
22
|
+
import { AiSettingGearButton } from './ai-configuration-setting-row';
|
|
23
|
+
|
|
24
|
+
export interface AiConfigurationItemRowProps {
|
|
25
|
+
readonly label: string;
|
|
26
|
+
/**
|
|
27
|
+
* Anchors the row for deep links: rendered as `data-ai-config-row-id`, which is what a
|
|
28
|
+
* {@link AiConfigurationSelection.highlight} scrolls to and flashes.
|
|
29
|
+
*/
|
|
30
|
+
readonly rowId?: string;
|
|
31
|
+
/** Renders the label in the code/monospace font (e.g. for shell command patterns), like the Variables list. */
|
|
32
|
+
readonly monospaceLabel?: boolean;
|
|
33
|
+
/** `codicon(...)` class shown before the label. */
|
|
34
|
+
readonly iconClass?: string;
|
|
35
|
+
readonly description?: string;
|
|
36
|
+
/** Short badges shown after the label, for what the item *is*. Use {@link origins} for where it came from. */
|
|
37
|
+
readonly tags?: string[];
|
|
38
|
+
/** Where the item came from, shown after the tags as {@link AiConfigurationOriginBadge}s. */
|
|
39
|
+
readonly origins?: AiConfigurationOrigin[];
|
|
40
|
+
/** Status badge shown on the right (e.g. an alias' resolved model). */
|
|
41
|
+
readonly status?: AiConfigurationItemStatus;
|
|
42
|
+
/** `true` reveals the modified edge (left indicator bar), matching {@link AiConfigurationSettingRow}. */
|
|
43
|
+
readonly modified?: boolean;
|
|
44
|
+
/** Optional trailing content (extra stats, or an inline control such as a select) shown on the right. */
|
|
45
|
+
readonly trailing?: React.ReactNode;
|
|
46
|
+
/**
|
|
47
|
+
* Opens the row's gear context menu (e.g. "Reset Setting") anchored at the cog, which appears on hover in
|
|
48
|
+
* the row's left gutter (mirroring {@link AiConfigurationSettingRow}). Wired the same way as
|
|
49
|
+
* {@link AiConfigurationSettingRow.onOpenMenu} (typically to {@link AiSettingsRowService.openResetMenu}).
|
|
50
|
+
* Omit to hide the gear.
|
|
51
|
+
*/
|
|
52
|
+
readonly onOpenMenu?: (gear: HTMLElement) => void;
|
|
53
|
+
/**
|
|
54
|
+
* Invoked when the row is clicked or activated via keyboard; typically `ctx.navigate(...)`. When provided
|
|
55
|
+
* the whole row is a navigable control ending in a chevron. Omit it for rows that only host an inline
|
|
56
|
+
* control (e.g. the Tools page's per-tool confirmation select): the row is then not clickable and shows
|
|
57
|
+
* no chevron.
|
|
58
|
+
*/
|
|
59
|
+
readonly onSelect?: () => void;
|
|
60
|
+
/**
|
|
61
|
+
* Turns the label into a link that navigates somewhere related to the row without being the row's own
|
|
62
|
+
* detail, e.g. an agent capability pointing at the (globally shared) snippet it pulls in. Distinct from
|
|
63
|
+
* {@link onSelect}: the row itself stays non-navigable, so a row can keep hosting its own control.
|
|
64
|
+
*/
|
|
65
|
+
readonly onActivateLabel?: () => void;
|
|
66
|
+
/** Tooltip for an {@link onActivateLabel} label, explaining where the link goes. */
|
|
67
|
+
readonly labelTooltip?: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Overview row for a collection item: an icon, a label with an optional description, and a right-hand slot
|
|
72
|
+
* for a status badge / stats / an inline control. When {@link AiConfigurationItemRowProps.onSelect} is given
|
|
73
|
+
* the whole row is a single navigable control (click or Enter/Space) that opens the item's detail page,
|
|
74
|
+
* ending in a chevron; without it the row simply presents its trailing control. A flat, list-style
|
|
75
|
+
* alternative to a card grid, matching the Settings and extensions lists; every non-label slot is optional.
|
|
76
|
+
*/
|
|
77
|
+
export const AiConfigurationItemRow: React.FC<AiConfigurationItemRowProps> = ({
|
|
78
|
+
label, rowId, tags, origins, monospaceLabel, iconClass, description, status, modified, trailing, onOpenMenu, onSelect, onActivateLabel, labelTooltip
|
|
79
|
+
}) => {
|
|
80
|
+
const navigable = onSelect !== undefined;
|
|
81
|
+
const onKeyDown = (event: React.KeyboardEvent<HTMLDivElement>): void => {
|
|
82
|
+
if (onSelect && (event.key === 'Enter' || event.key === ' ')) {
|
|
83
|
+
event.preventDefault();
|
|
84
|
+
onSelect();
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
return <div
|
|
88
|
+
className={`ai-configuration-item-row${navigable ? ' navigable' : ''}${modified ? ' modified' : ''}`}
|
|
89
|
+
data-ai-config-row-id={rowId}
|
|
90
|
+
role={navigable ? 'button' : undefined}
|
|
91
|
+
tabIndex={navigable ? 0 : undefined}
|
|
92
|
+
onClick={onSelect}
|
|
93
|
+
onKeyDown={navigable ? onKeyDown : undefined}
|
|
94
|
+
>
|
|
95
|
+
{onOpenMenu && <div className='ai-configuration-item-row-actions'>
|
|
96
|
+
<AiSettingGearButton onOpenMenu={onOpenMenu} />
|
|
97
|
+
</div>}
|
|
98
|
+
{iconClass && <span className={`ai-configuration-item-row-icon ${iconClass}`}></span>}
|
|
99
|
+
<div className='ai-configuration-item-row-text'>
|
|
100
|
+
{/* Name and badges share a line, so a badge reads as part of the name rather than as a second row. */}
|
|
101
|
+
<div className='ai-configuration-item-row-heading'>
|
|
102
|
+
{onActivateLabel
|
|
103
|
+
? <button
|
|
104
|
+
type='button'
|
|
105
|
+
className={`ai-configuration-item-row-label link${monospaceLabel ? ' mono' : ''}`}
|
|
106
|
+
title={labelTooltip}
|
|
107
|
+
onClick={event => {
|
|
108
|
+
// A navigable row would otherwise also handle the click and open its own detail.
|
|
109
|
+
event.stopPropagation();
|
|
110
|
+
onActivateLabel();
|
|
111
|
+
}}
|
|
112
|
+
>{label}</button>
|
|
113
|
+
: <span className={`ai-configuration-item-row-label${monospaceLabel ? ' mono' : ''}`}>{label}</span>}
|
|
114
|
+
{tags && tags.length > 0 && <span className='ai-configuration-item-row-tags'>
|
|
115
|
+
{tags.map(tag => <AiConfigurationKindBadge key={tag} label={tag} variant='outline' />)}
|
|
116
|
+
</span>}
|
|
117
|
+
<AiConfigurationOriginBadges origins={origins} />
|
|
118
|
+
</div>
|
|
119
|
+
{description && <span className='ai-configuration-item-row-description'>{description}</span>}
|
|
120
|
+
</div>
|
|
121
|
+
<div className='ai-configuration-item-row-trailing'>
|
|
122
|
+
{status && <AiConfigurationStatusBadge status={status} />}
|
|
123
|
+
{trailing}
|
|
124
|
+
{navigable && <span className={`ai-configuration-item-row-chevron ${codicon('chevron-right')}`} aria-hidden={true}></span>}
|
|
125
|
+
</div>
|
|
126
|
+
</div>;
|
|
127
|
+
};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 EclipseSource and others.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
import { nls } from '@theia/core';
|
|
18
|
+
import { codicon } from '@theia/core/lib/browser';
|
|
19
|
+
import { InstalledAgentPluginInfo } from '@theia/ai-core/lib/browser/agent-plugin-ui-bridge';
|
|
20
|
+
import * as React from '@theia/core/shared/react';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Where a configuration entry came from, when that is not the user: an AI registry entry, or an Agent
|
|
24
|
+
* Plugin that contributed it. Built through {@link AiConfigurationOrigin.registry} or
|
|
25
|
+
* {@link AiConfigurationOrigin.agentPlugin} rather than by hand, so that the same origin reads the same
|
|
26
|
+
* on every page - an entry's provenance is surfaced in an overview list, on a detail header and in a
|
|
27
|
+
* collapsible row, and those diverged while each page phrased it for itself.
|
|
28
|
+
*/
|
|
29
|
+
export interface AiConfigurationOrigin {
|
|
30
|
+
/** Short, on one line next to the entry's name; the {@link tooltip} carries the detail. */
|
|
31
|
+
readonly label: string;
|
|
32
|
+
/** `codicon(...)` class shown before the label. */
|
|
33
|
+
readonly iconClass: string;
|
|
34
|
+
readonly tooltip: string;
|
|
35
|
+
/** Opens or reveals the origin. Omit for an origin with nothing to navigate to; the badge is then static. */
|
|
36
|
+
readonly activate?: () => void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export namespace AiConfigurationOrigin {
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* An entry installed from, or linked to, an AI registry approval.
|
|
43
|
+
*
|
|
44
|
+
* @param open reveals the entry in the registry UI. Omit it where no registry UI is bound: the entry
|
|
45
|
+
* still came from the registry, which is worth stating, so the badge is shown - just not as a link.
|
|
46
|
+
*/
|
|
47
|
+
export function registry(entryId: string, open?: () => void): AiConfigurationOrigin {
|
|
48
|
+
return {
|
|
49
|
+
label: nls.localize('theia/ai/core/aiConfiguration/origin/fromRegistry', 'From registry'),
|
|
50
|
+
iconClass: codicon('link-external'),
|
|
51
|
+
tooltip: open
|
|
52
|
+
? nls.localize('theia/ai/core/aiConfiguration/origin/openInRegistry', 'Open in AI registry: {0}', entryId)
|
|
53
|
+
: nls.localize('theia/ai/core/aiConfiguration/origin/registryEntry', 'AI registry entry: {0}', entryId),
|
|
54
|
+
activate: open
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* An entry contributed by an installed Agent Plugin. Takes the resolved plugin rather than its
|
|
60
|
+
* identifier: a bare identifier is not worth showing, so a caller that cannot resolve one renders no
|
|
61
|
+
* badge at all.
|
|
62
|
+
*/
|
|
63
|
+
export function agentPlugin(plugin: InstalledAgentPluginInfo, reveal: () => void): AiConfigurationOrigin {
|
|
64
|
+
return {
|
|
65
|
+
label: nls.localize('theia/ai/core/aiConfiguration/origin/viaAgentPlugin', 'via {0}', plugin.name),
|
|
66
|
+
// `package`, matching the Agent Plugin cards in the Extensions view; `extensions` is the icon
|
|
67
|
+
// that view's VS Code extension section owns.
|
|
68
|
+
iconClass: codicon('package'),
|
|
69
|
+
tooltip: nls.localize('theia/ai/core/aiConfiguration/origin/openAgentPlugin', 'Open the Agent Plugin that provides this: {0}', plugin.name),
|
|
70
|
+
activate: reveal
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface AiConfigurationOriginBadgeProps {
|
|
76
|
+
readonly origin: AiConfigurationOrigin;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* One {@link AiConfigurationOrigin} as a pill next to an entry's name: a `<button>` when the origin can
|
|
81
|
+
* be opened, a plain `<span>` when it cannot, so that only what is actually navigable is focusable.
|
|
82
|
+
* The click never bubbles - these sit inside rows and headers that are themselves clickable, and
|
|
83
|
+
* following the badge must not also open the row.
|
|
84
|
+
*/
|
|
85
|
+
export const AiConfigurationOriginBadge: React.FC<AiConfigurationOriginBadgeProps> = ({ origin }) => {
|
|
86
|
+
const content = <>
|
|
87
|
+
<span aria-hidden='true' className={`ai-configuration-origin-badge-icon ${origin.iconClass}`}></span>
|
|
88
|
+
<span className='ai-configuration-origin-badge-label'>{origin.label}</span>
|
|
89
|
+
</>;
|
|
90
|
+
return origin.activate
|
|
91
|
+
? <button
|
|
92
|
+
type='button'
|
|
93
|
+
className='ai-configuration-origin-badge activatable'
|
|
94
|
+
title={origin.tooltip}
|
|
95
|
+
aria-label={origin.tooltip}
|
|
96
|
+
onClick={event => {
|
|
97
|
+
event.stopPropagation();
|
|
98
|
+
origin.activate!();
|
|
99
|
+
}}
|
|
100
|
+
>{content}</button>
|
|
101
|
+
: <span className='ai-configuration-origin-badge' title={origin.tooltip}>{content}</span>;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export interface AiConfigurationOriginBadgesProps {
|
|
105
|
+
readonly origins?: AiConfigurationOrigin[];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** The origins of one entry, in the order given; nothing at all when it has none, which is the common case. */
|
|
109
|
+
export const AiConfigurationOriginBadges: React.FC<AiConfigurationOriginBadgesProps> = ({ origins }) =>
|
|
110
|
+
origins && origins.length > 0
|
|
111
|
+
? <span className='ai-configuration-origin-badges'>
|
|
112
|
+
{origins.map(origin => <AiConfigurationOriginBadge key={origin.label} origin={origin} />)}
|
|
113
|
+
</span>
|
|
114
|
+
: undefined;
|