@adecore/agents-react 0.31.10 → 0.31.11
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/dist/chat/logic/picker-entries.d.ts +7 -0
- package/dist/chat/logic/picker-entries.js +22 -4
- package/dist/chat/ui/ModelMarks.d.ts +4 -0
- package/dist/chat/ui/ModelMarks.js +9 -0
- package/dist/chat/ui/Pickers.js +3 -2
- package/dist/chat/ui/RunSettings.js +71 -4
- package/dist/locales/en/agent-chat.d.ts +1 -0
- package/dist/locales/en/agent-chat.js +1 -0
- package/dist/locales/nl/agent-chat.d.ts +1 -0
- package/dist/locales/nl/agent-chat.js +1 -0
- package/package.json +8 -8
- package/src/chat/logic/picker-entries.ts +26 -4
- package/src/chat/ui/ModelMarks.tsx +25 -0
- package/src/chat/ui/Pickers.tsx +3 -6
- package/src/chat/ui/RunSettings.tsx +195 -66
- package/src/locales/en/agent-chat.ts +1 -0
- package/src/locales/nl/agent-chat.ts +1 -0
|
@@ -17,4 +17,11 @@ export interface PickerCurrent {
|
|
|
17
17
|
provider: AgentKind;
|
|
18
18
|
model: string;
|
|
19
19
|
}
|
|
20
|
+
export declare function modelsByVendor(models: ModelInfo[]): ModelInfo[];
|
|
21
|
+
export declare function shortlistOf(kind: AgentKind, models: ModelInfo[], current: PickerCurrent | null): ModelInfo[] | null;
|
|
22
|
+
export declare function wantsModelSearch(providers: readonly ProviderInfo[]): boolean;
|
|
23
|
+
export declare function searchModels(providers: readonly ProviderInfo[], query: string): Array<{
|
|
24
|
+
entry: ProviderInfo;
|
|
25
|
+
models: ModelInfo[];
|
|
26
|
+
}>;
|
|
20
27
|
export declare function pickerEntriesOf(providers: readonly ProviderInfo[], query: string, legacyOpen: boolean, expanded?: ReadonlySet<AgentKind>, current?: PickerCurrent | null): PickerEntry[];
|
|
@@ -10,7 +10,7 @@ function matches(model, query) {
|
|
|
10
10
|
* By maker, then by name, when the provider says who made its models; a list with no makers keeps the order
|
|
11
11
|
* it came in. A model with no maker goes last, so the ones that have one read as groups.
|
|
12
12
|
*/
|
|
13
|
-
function
|
|
13
|
+
export function modelsByVendor(models) {
|
|
14
14
|
if (!models.some((model) => model.vendor !== undefined)) {
|
|
15
15
|
return models;
|
|
16
16
|
}
|
|
@@ -22,6 +22,24 @@ function byVendor(models) {
|
|
|
22
22
|
a.name.localeCompare(b.name, undefined, { sensitivity: 'base', numeric: true }));
|
|
23
23
|
});
|
|
24
24
|
}
|
|
25
|
+
/* The models a long list shows before it is opened: the default and the one that is picked. Null when the list is short enough to show whole. */
|
|
26
|
+
export function shortlistOf(kind, models, current) {
|
|
27
|
+
if (models.length <= COMPACT_MODEL_LIMIT) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
return modelsByVendor(models.filter((model) => model.isDefault || (current?.provider === kind && current.model === model.slug)));
|
|
31
|
+
}
|
|
32
|
+
/* Whether the providers offer enough models together that a search is quicker than reading the list. */
|
|
33
|
+
export function wantsModelSearch(providers) {
|
|
34
|
+
return providers.reduce((count, entry) => count + entry.models.length, 0) > COMPACT_MODEL_LIMIT;
|
|
35
|
+
}
|
|
36
|
+
/* Every model of every provider that a search finds, legacy ones included, by maker; a provider with none is left out. */
|
|
37
|
+
export function searchModels(providers, query) {
|
|
38
|
+
const needle = query.trim().toLowerCase();
|
|
39
|
+
return providers
|
|
40
|
+
.map((entry) => ({ entry, models: modelsByVendor(entry.models.filter((model) => matches(model, needle))) }))
|
|
41
|
+
.filter((group) => group.models.length > 0);
|
|
42
|
+
}
|
|
25
43
|
const NONE = new Set();
|
|
26
44
|
/*
|
|
27
45
|
* The rows of the model list. A model an old CLI cannot run is a row like any other, so a search for
|
|
@@ -30,20 +48,20 @@ const NONE = new Set();
|
|
|
30
48
|
* that opens the rest, grouped by maker.
|
|
31
49
|
*/
|
|
32
50
|
export function pickerEntriesOf(providers, query, legacyOpen, expanded = NONE, current = null) {
|
|
33
|
-
const rowsOf = (entry, models) =>
|
|
51
|
+
const rowsOf = (entry, models) => modelsByVendor(models).map((model) => ({ kind: 'model', provider: entry, model }));
|
|
34
52
|
if (query !== '') {
|
|
35
53
|
return providers.flatMap((entry) => rowsOf(entry, entry.models.filter((model) => matches(model, query))));
|
|
36
54
|
}
|
|
37
55
|
const legacy = providers.flatMap((entry) => entry.models.filter((model) => model.legacy).map((model) => ({ kind: 'model', provider: entry, model })));
|
|
38
56
|
const live = providers.flatMap((entry) => {
|
|
39
57
|
const models = entry.models.filter((model) => !model.legacy);
|
|
40
|
-
|
|
58
|
+
const shortlist = shortlistOf(entry.kind, models, current);
|
|
59
|
+
if (shortlist === null) {
|
|
41
60
|
return rowsOf(entry, models);
|
|
42
61
|
}
|
|
43
62
|
if (expanded.has(entry.kind)) {
|
|
44
63
|
return [{ kind: 'all', provider: entry, count: models.length, open: true }, ...rowsOf(entry, models)];
|
|
45
64
|
}
|
|
46
|
-
const shortlist = models.filter((model) => model.isDefault || (current?.provider === entry.kind && current.model === model.slug));
|
|
47
65
|
return [...rowsOf(entry, shortlist), { kind: 'all', provider: entry, count: models.length, open: false }];
|
|
48
66
|
});
|
|
49
67
|
if (legacy.length === 0) {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { Wrench } from 'lucide-react';
|
|
4
|
+
import { Icon, Pill, Tooltip } from '@adecore/ui';
|
|
5
|
+
/* What a model row says beside the name: its badge, and a mark when the provider says the model can call tools. */
|
|
6
|
+
export function ModelMarks({ model }) {
|
|
7
|
+
const { t } = useTranslation('agent-chat');
|
|
8
|
+
return (_jsxs(_Fragment, { children: [model.badge && (_jsx(Pill, { tone: "accent", shape: "tag", size: "sm", children: model.badge.charAt(0).toLocaleUpperCase() + model.badge.slice(1) })), model.tools === true && (_jsx(Tooltip, { label: t('pickers.model.tools'), children: _jsx("span", { className: "flex shrink-0 text-text-faint", "aria-label": t('pickers.model.tools'), children: _jsx(Icon, { icon: Wrench, size: 12 }) }) }))] }));
|
|
9
|
+
}
|
package/dist/chat/ui/Pickers.js
CHANGED
|
@@ -9,9 +9,10 @@ import { modelRequirementOf } from '../../agents/model-requirement.js';
|
|
|
9
9
|
import { useChatHost } from '../../host.js';
|
|
10
10
|
import { ChatScopeContext } from '../../scope.js';
|
|
11
11
|
import { useReportedModels } from '../../state/chats.js';
|
|
12
|
+
import { ModelMarks } from './ModelMarks.js';
|
|
12
13
|
import { pickerEntriesOf } from '../logic/picker-entries.js';
|
|
13
14
|
import { forgetStashed, STASH_SHORTCUT, useStash } from '../stash.js';
|
|
14
|
-
import { Icon, IconButton, Menu,
|
|
15
|
+
import { Icon, IconButton, Menu, Popover, Tooltip } from '@adecore/ui';
|
|
15
16
|
const triggerClass = 'flex h-7 shrink-0 items-center gap-1 whitespace-nowrap rounded-md px-2 text-xs text-text-muted hover:bg-surface-hover hover:text-text data-[popup-open]:bg-surface-active data-[popup-open]:text-text';
|
|
16
17
|
const chipClass = 'flex h-6 min-w-0 shrink items-center gap-1.5 whitespace-nowrap rounded-md bg-surface-hover px-2 text-xs text-text hover:bg-surface-active data-[popup-open]:bg-surface-active';
|
|
17
18
|
/*
|
|
@@ -122,7 +123,7 @@ export function ModelPicker({ providers, provider, selection, open, onOpenChange
|
|
|
122
123
|
const next = entries[i + 1];
|
|
123
124
|
const lastOfGroup = next === undefined || next.kind !== 'model' || next.provider.kind !== entry.provider.kind;
|
|
124
125
|
const chosen = entry.provider.kind === provider && entry.model.slug === selection.model;
|
|
125
|
-
return (_jsxs("div", { children: [first && (_jsxs(Menu.Label, { className: "flex items-center gap-1.5", children: [_jsx(AgentIcon, { kind: entry.provider.kind, size: 12 }), entry.provider.name] })), vendorFirst && _jsx(Menu.Label, { children: entry.model.vendor }), _jsxs("button", { role: "option", "aria-selected": chosen, "aria-disabled": requirement !== null || undefined, "data-active": i === index, className: `cursor-row flex w-full flex-col items-stretch gap-0.5 rounded-md px-2.5 py-1.5 text-left text-sm ${requirement === null ? 'text-text-muted' : 'text-text-faint'}`, onMouseEnter: () => setIndex(i), onClick: () => choose(entry), children: [_jsxs("span", { className: "flex w-full items-center gap-2", children: [_jsx(Menu.Check, { kind: "radio", checked: chosen }), _jsx("span", { className: "min-w-0 truncate", children: entry.model.name }),
|
|
126
|
+
return (_jsxs("div", { children: [first && (_jsxs(Menu.Label, { className: "flex items-center gap-1.5", children: [_jsx(AgentIcon, { kind: entry.provider.kind, size: 12 }), entry.provider.name] })), vendorFirst && _jsx(Menu.Label, { children: entry.model.vendor }), _jsxs("button", { role: "option", "aria-selected": chosen, "aria-disabled": requirement !== null || undefined, "data-active": i === index, className: `cursor-row flex w-full flex-col items-stretch gap-0.5 rounded-md px-2.5 py-1.5 text-left text-sm ${requirement === null ? 'text-text-muted' : 'text-text-faint'}`, onMouseEnter: () => setIndex(i), onClick: () => choose(entry), children: [_jsxs("span", { className: "flex w-full items-center gap-2", children: [_jsx(Menu.Check, { kind: "radio", checked: chosen }), _jsx("span", { className: "min-w-0 truncate", children: entry.model.name }), _jsx(ModelMarks, { model: entry.model }), _jsx("span", { className: "grow" }), entry.model.legacy && _jsx("span", { className: "text-xs text-text-faint", children: t('pickers.model.legacy') })] }), requirement !== null && (_jsx("span", { className: "pl-6 text-xs text-text-faint", children: machine === null
|
|
126
127
|
? t('pickers.model.requiresHere', { cli: requirement.cli, version: requirement.minVersion })
|
|
127
128
|
: t('pickers.model.requires', { cli: requirement.cli, version: requirement.minVersion, machine }) }))] }), lastOfGroup && blockedKinds.has(entry.provider.kind) && (_jsx("button", { type: "button", className: "cursor-row flex w-full items-center rounded-md px-2.5 py-1.5 text-left text-xs text-text-muted", onClick: () => {
|
|
128
129
|
setOpen(false);
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Fragment, useEffect, useRef, useState } from 'react';
|
|
2
3
|
import { useTranslation } from 'react-i18next';
|
|
3
4
|
import clsx from 'clsx';
|
|
4
|
-
import { Activity, TriangleAlert, ChevronDown, ChevronRight, ChevronUp, FilePen, Gauge, Hand, Settings, Shield, ShieldAlert, SlidersHorizontal } from 'lucide-react';
|
|
5
|
+
import { Activity, TriangleAlert, ChevronDown, ChevronRight, ChevronUp, FilePen, Gauge, Hand, Search, Settings, Shield, ShieldAlert, SlidersHorizontal } from 'lucide-react';
|
|
5
6
|
import { AccountDot } from '../../agents/AccountDot.js';
|
|
6
7
|
import { limitsOfAccount, sessionWindow } from '../../agents/account-limits.js';
|
|
7
8
|
import { canContinueOn } from '../../agents/accounts.js';
|
|
8
9
|
import { AgentIcon } from '../../agents/AgentIcon.js';
|
|
9
10
|
import { modelName, shortModelName } from '../../agents/model-name.js';
|
|
11
|
+
import { ModelMarks } from './ModelMarks.js';
|
|
12
|
+
import { modelsByVendor, searchModels, shortlistOf, wantsModelSearch } from '../logic/picker-entries.js';
|
|
10
13
|
import { CONTEXT_OPTION, contextFraction, contextSegments, orderOptions } from '../logic/context-usage.js';
|
|
11
14
|
import { RUNTIME_MODES } from '../runtime-modes.js';
|
|
12
15
|
import { formatClock, formatWeekdayClock, isSameDay, formatPercent, formatTokens } from '@adecore/ui/format';
|
|
13
|
-
import { Icon, Menu,
|
|
16
|
+
import { Icon, Menu, Tooltip, useNow } from '@adecore/ui';
|
|
14
17
|
import { useChatHost } from '../../host.js';
|
|
15
18
|
import { useUsageLimits } from '../../usage/limits.js';
|
|
16
19
|
const MINUTE_MS = 60_000;
|
|
@@ -59,6 +62,8 @@ function modelGroupsOf(providers, legacy) {
|
|
|
59
62
|
*/
|
|
60
63
|
export function RunSettings({ disabled = false, providers, provider, selection, model, runtimeMode, effectiveRuntimeMode, permissionMode, open, onOpenChange, onModel, onOption, onMode, usage, compactDisabled, onCompact, account }) {
|
|
61
64
|
const { t } = useTranslation('agent-chat');
|
|
65
|
+
const [query, setQuery] = useState('');
|
|
66
|
+
const searchRef = useRef(null);
|
|
62
67
|
const owner = providers.find((entry) => entry.kind === provider);
|
|
63
68
|
const current = modelName(selection.model, owner?.models);
|
|
64
69
|
const short = shortModelName(current, owner?.models);
|
|
@@ -87,8 +92,70 @@ export function RunSettings({ disabled = false, providers, provider, selection,
|
|
|
87
92
|
const slash = value.indexOf('/');
|
|
88
93
|
onModel(value.slice(0, slash), value.slice(slash + 1));
|
|
89
94
|
};
|
|
90
|
-
const
|
|
91
|
-
|
|
95
|
+
const searchable = wantsModelSearch(providers);
|
|
96
|
+
const found = query.trim() === '' ? null : searchModels(providers, query);
|
|
97
|
+
// The menu moves the focus to its popup as it opens, so the search field takes it a frame later.
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
if (!open || !searchable) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const frame = requestAnimationFrame(() => searchRef.current?.focus());
|
|
103
|
+
return () => cancelAnimationFrame(frame);
|
|
104
|
+
}, [open, searchable]);
|
|
105
|
+
const changeOpen = (next) => {
|
|
106
|
+
if (!next) {
|
|
107
|
+
setQuery('');
|
|
108
|
+
}
|
|
109
|
+
onOpenChange(next);
|
|
110
|
+
};
|
|
111
|
+
// An item the arrow keys moved to has the focus; typing there still goes to the search.
|
|
112
|
+
const onPopupKey = (event) => {
|
|
113
|
+
if (!searchable || event.target === searchRef.current || event.metaKey || event.ctrlKey || event.altKey) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const typed = event.key.length === 1 && event.key !== ' ';
|
|
117
|
+
if (!typed && event.key !== 'Backspace') {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
event.preventDefault();
|
|
121
|
+
event.stopPropagation();
|
|
122
|
+
setQuery((now) => (typed ? now + event.key : now.slice(0, -1)));
|
|
123
|
+
searchRef.current?.focus();
|
|
124
|
+
};
|
|
125
|
+
// The menu reads letters as a jump to an item and arrows as moving through them; the field keeps what it types.
|
|
126
|
+
const onSearchKey = (event) => {
|
|
127
|
+
if (event.key === 'Escape' && query !== '') {
|
|
128
|
+
event.preventDefault();
|
|
129
|
+
event.stopPropagation();
|
|
130
|
+
setQuery('');
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (event.key === 'Enter') {
|
|
134
|
+
event.preventDefault();
|
|
135
|
+
const first = found?.[0];
|
|
136
|
+
if (first !== undefined) {
|
|
137
|
+
onModel(first.entry.kind, first.models[0].slug);
|
|
138
|
+
changeOpen(false);
|
|
139
|
+
}
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (event.key !== 'Escape' && event.key !== 'ArrowDown' && event.key !== 'ArrowUp' && event.key !== 'Tab') {
|
|
143
|
+
event.stopPropagation();
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
const modelRow = (entry, row) => (_jsxs(Menu.RadioItem, { value: modelValue(entry.kind, row.slug), closeOnClick: true, children: [_jsx(AgentIcon, { kind: entry.kind, size: 14, className: "shrink-0" }), _jsx("span", { className: "min-w-0 truncate", children: row.name }), _jsx(ModelMarks, { model: row })] }, row.slug));
|
|
147
|
+
// Every model of one provider, under a label per maker where the provider names them.
|
|
148
|
+
const byMaker = (entry, models) => modelsByVendor(models).map((row, i, sorted) => (_jsxs(Fragment, { children: [row.vendor !== undefined && sorted[i - 1]?.vendor !== row.vendor && _jsx(Menu.GroupLabel, { children: row.vendor }), modelRow(entry, row)] }, row.slug)));
|
|
149
|
+
// A provider with many models shows the default and the chosen one, with all of them one submenu away.
|
|
150
|
+
const providerModels = (entry, models) => {
|
|
151
|
+
const shortlist = shortlistOf(entry.kind, models, { provider, model: selection.model });
|
|
152
|
+
if (shortlist === null) {
|
|
153
|
+
return models.map((row) => modelRow(entry, row));
|
|
154
|
+
}
|
|
155
|
+
return (_jsxs(_Fragment, { children: [shortlist.map((row) => modelRow(entry, row)), _jsxs(Menu.SubmenuRoot, { children: [_jsx(Menu.SubmenuTrigger, { className: "text-text-muted", children: t('pickers.model.allGroup', { count: models.length }) }), _jsx(Menu.Popup, { className: "w-80", children: _jsx(Menu.RadioGroup, { value: chosenModel, onValueChange: chooseModel, children: byMaker(entry, models) }) })] })] }));
|
|
156
|
+
};
|
|
157
|
+
const modelGroups = (groups) => groups.map(({ entry, models }) => (_jsxs(Menu.Group, { children: [grouped && (_jsxs(Menu.GroupLabel, { className: "flex items-center gap-1.5", children: [_jsx(AgentIcon, { kind: entry.kind, size: 12 }), entry.name] })), providerModels(entry, models)] }, entry.kind)));
|
|
158
|
+
return (_jsxs(Menu.Root, { open: open, onOpenChange: changeOpen, highlightItemOnHover: !searchable, children: [_jsx(Tooltip, { label: permissionHint ?? t('pickers.settings.title'), kbd: "/model", children: _jsxs(Menu.Trigger, { disabled: disabled, className: clsx('flex h-8 min-w-0 items-center gap-2 rounded-full border pr-3 pl-2.5 text-xs whitespace-nowrap text-text-muted @max-xl/composer:gap-1.5 @max-sm/composer:pr-2.5', open ? 'border-accent bg-accent-soft' : 'border-border hover:bg-surface-hover'), children: [_jsx(AgentIcon, { kind: provider, size: 14, className: "shrink-0" }), _jsxs("span", { className: "flex min-w-0 items-center gap-2 overflow-hidden @max-xl/composer:gap-1.5", children: [_jsx("span", { className: "truncate font-medium text-text @max-xl/composer:hidden", children: current }), _jsx("span", { className: "hidden truncate font-medium text-text @max-xl/composer:inline", children: short }), account !== null && (_jsxs("span", { className: "flex min-w-0 items-center gap-2 @max-xl/composer:gap-1.5", children: [_jsx("span", { className: "shrink-0 text-text-faint", children: "\u00B7" }), _jsx(AccountDot, { color: account.choice.current?.account.color }), _jsx("span", { className: "truncate @max-xl/composer:hidden", children: account.choice.current === null ? account.choice.currentId : account.choice.nameOf(account.choice.current) })] })), summary.map((entry) => (_jsxs("span", { className: clsx('flex shrink-0 items-center gap-2 @max-xl/composer:gap-1.5', !entry.ring && '@max-xl/composer:hidden'), children: [_jsx("span", { className: "text-text-faint", children: "\u00B7" }), entry.ring && _jsx(ContextRing, { usage: usage }), _jsx("span", { className: clsx(entry.ring && '@max-sm/composer:hidden'), children: entry.label })] }, entry.id))), _jsx("span", { className: "shrink-0 text-text-faint", children: "\u00B7" }), _jsxs("span", { className: clsx('flex shrink-0 items-center gap-1', fullAccess && 'text-status-needs-you'), children: [_jsx(Icon, { icon: MODE_ICONS[shownMode], size: 12, className: "shrink-0" }), _jsx("span", { children: effectiveRuntimeMode === undefined && permissionMode !== undefined ? permissionMode : t(`modes.${shownMode}.short`) }), permissionFallback && _jsx(Icon, { icon: TriangleAlert, size: 12, className: "shrink-0 text-status-needs-you" })] })] }), _jsx(Icon, { icon: open ? ChevronUp : ChevronDown, size: 12, className: "shrink-0 @max-sm/composer:hidden" })] }) }), _jsxs(Menu.Popup, { side: "top", sideOffset: 8, align: "start", className: "w-80", onKeyDownCapture: onPopupKey, children: [providers.length === 0 && _jsx("div", { className: "px-2.5 py-3 text-xs text-text-faint", children: t('pickers.model.noProvider') }), searchable && (_jsxs("div", { className: "flex items-center gap-2 border-b border-border px-2.5", children: [_jsx(Icon, { icon: Search, size: 14, className: "shrink-0 text-text-faint" }), _jsx("input", { ref: searchRef, className: "h-8 w-full bg-transparent text-sm text-text outline-none placeholder:text-text-faint", placeholder: t('pickers.model.search'), "aria-label": t('pickers.model.search'), spellCheck: false, value: query, onChange: (event) => setQuery(event.target.value), onKeyDown: onSearchKey })] })), found !== null && found.length === 0 && _jsx("div", { className: "px-2.5 py-3 text-xs text-text-faint", children: t('pickers.model.noMatch') }), found !== null && found.length > 0 && (_jsx(Menu.RadioGroup, { value: chosenModel, onValueChange: chooseModel, children: found.map(({ entry, models }) => (_jsxs(Menu.Group, { children: [grouped && (_jsxs(Menu.GroupLabel, { className: "flex items-center gap-1.5", children: [_jsx(AgentIcon, { kind: entry.kind, size: 12 }), entry.name] })), byMaker(entry, models)] }, entry.kind))) })), found === null && (_jsxs(_Fragment, { children: [_jsx(Menu.RadioGroup, { value: chosenModel, onValueChange: chooseModel, children: modelGroups(live) }), legacy.length > 0 && (_jsxs(Menu.SubmenuRoot, { children: [_jsxs(Menu.SubmenuTrigger, { className: "text-text-muted", children: [_jsx("span", { className: "min-w-0 grow truncate", children: t('pickers.model.legacyGroup', { count: legacy.reduce((count, group) => count + group.models.length, 0) }) }), chosenLegacy && _jsx(Menu.Hint, { className: "truncate", children: chosenLegacy.name })] }), _jsx(Menu.Popup, { className: "min-w-56", children: _jsx(Menu.RadioGroup, { value: chosenModel, onValueChange: chooseModel, children: modelGroups(legacy) }) })] })), _jsx(Menu.Separator, {}), account !== null && _jsx(AccountSubmenu, { provider: provider, account: account }), _jsxs(Menu.SubmenuRoot, { children: [_jsxs(Menu.SubmenuTrigger, { className: "text-text-muted", children: [_jsx(Icon, { icon: MODE_ICONS[runtimeMode], size: 14, className: clsx('shrink-0', fullAccess ? 'text-status-needs-you' : 'text-text-faint') }), _jsx("span", { className: "min-w-0 grow truncate", children: t('pickers.mode.label') }), _jsx(Menu.Hint, { className: "truncate", children: t(`modes.${runtimeMode}.label`) })] }), _jsxs(Menu.Popup, { className: "w-72", children: [permissionHint !== null && _jsx("p", { className: "px-2.5 py-2 text-xs text-text-muted", children: permissionHint }), _jsx(Menu.RadioGroup, { value: runtimeMode, onValueChange: (mode) => onMode(mode), children: RUNTIME_MODES.map((mode) => (_jsxs(Menu.RadioItem, { value: mode, className: "items-start", children: [_jsx("span", { className: "grid h-lh w-4 shrink-0 place-items-center", children: _jsx(Icon, { icon: MODE_ICONS[mode], size: 14, className: mode === 'full-access' ? 'text-status-needs-you' : 'text-text-faint' }) }), _jsxs("span", { className: "flex min-w-0 grow flex-col", children: [t(`modes.${mode}.label`), _jsx("span", { className: "text-xs text-text-faint", children: t(`modes.${mode}.hint`) })] })] }, mode))) })] })] }), options.map((option) => (_jsx(OptionRow, { option: option, selection: selection, onChange: (value) => onOption(option.id, value) }, option.id))), (hasContextOption || standaloneContext) && (_jsxs(_Fragment, { children: [_jsx(Menu.Separator, {}), _jsx(ContextUsage, { usage: usage, disabled: compactDisabled, onCompact: onCompact })] }))] }))] })] }));
|
|
92
159
|
}
|
|
93
160
|
/* Red where a window is nearly spent, amber where it is worth knowing, as the usage bars have it. */
|
|
94
161
|
function sessionTone(used) {
|
|
@@ -439,6 +439,7 @@ export default {
|
|
|
439
439
|
legacyGroup_one: 'Legacy models ({{count}})',
|
|
440
440
|
legacyGroup_other: 'Legacy models ({{count}})',
|
|
441
441
|
legacy: 'Legacy',
|
|
442
|
+
tools: 'Can use tools',
|
|
442
443
|
requires: 'Requires {{cli}} {{version}} or later on {{machine}}',
|
|
443
444
|
requiresHere: 'Requires {{cli}} {{version}} or later',
|
|
444
445
|
providerSettings: 'Provider settings'
|
|
@@ -439,6 +439,7 @@ export default {
|
|
|
439
439
|
legacyGroup_one: 'Legacy-modellen ({{count}})',
|
|
440
440
|
legacyGroup_other: 'Legacy-modellen ({{count}})',
|
|
441
441
|
legacy: 'Legacy',
|
|
442
|
+
tools: 'Kan tools gebruiken',
|
|
442
443
|
requires: 'Vereist {{cli}} {{version}} of nieuwer op {{machine}}',
|
|
443
444
|
requiresHere: 'Vereist {{cli}} {{version}} of nieuwer',
|
|
444
445
|
providerSettings: 'Providerinstellingen'
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adecore/agents-react",
|
|
3
|
-
"version": "0.31.
|
|
3
|
+
"version": "0.31.11",
|
|
4
4
|
"description": "React agent chat threads, composers, approvals, provider accounts and usage views.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -67,11 +67,11 @@
|
|
|
67
67
|
"test:visuals": "bun --config=integration.bunfig.toml --conditions=source test ./src/chat/visual-viewport.integration.test.ts"
|
|
68
68
|
},
|
|
69
69
|
"dependencies": {
|
|
70
|
-
"@adecore/agent-contracts": "0.31.
|
|
71
|
-
"@adecore/editor": "0.31.
|
|
72
|
-
"@adecore/editor-react": "0.31.
|
|
73
|
-
"@adecore/intelligent-ui": "0.31.
|
|
74
|
-
"@adecore/merge": "0.31.
|
|
70
|
+
"@adecore/agent-contracts": "0.31.11",
|
|
71
|
+
"@adecore/editor": "0.31.11",
|
|
72
|
+
"@adecore/editor-react": "0.31.11",
|
|
73
|
+
"@adecore/intelligent-ui": "0.31.11",
|
|
74
|
+
"@adecore/merge": "0.31.11",
|
|
75
75
|
"@codemirror/commands": "^6.11.1",
|
|
76
76
|
"@codemirror/language": "^6.12.4",
|
|
77
77
|
"@codemirror/state": "^6.7.6",
|
|
@@ -94,14 +94,14 @@
|
|
|
94
94
|
"zustand": "^5.0.15"
|
|
95
95
|
},
|
|
96
96
|
"peerDependencies": {
|
|
97
|
-
"@adecore/ui": "^0.31.
|
|
97
|
+
"@adecore/ui": "^0.31.11",
|
|
98
98
|
"i18next": "^26.4.2",
|
|
99
99
|
"react": "^19.3.0",
|
|
100
100
|
"react-dom": "^19.3.0",
|
|
101
101
|
"react-i18next": "^17.0.15"
|
|
102
102
|
},
|
|
103
103
|
"devDependencies": {
|
|
104
|
-
"@adecore/ui": "0.31.
|
|
104
|
+
"@adecore/ui": "0.31.11",
|
|
105
105
|
"@types/bun": "^1.4.2",
|
|
106
106
|
"@types/react": "^19.3.0",
|
|
107
107
|
"@types/react-dom": "^19.3.0",
|
|
@@ -25,7 +25,7 @@ function matches(model: ModelInfo, query: string): boolean {
|
|
|
25
25
|
* By maker, then by name, when the provider says who made its models; a list with no makers keeps the order
|
|
26
26
|
* it came in. A model with no maker goes last, so the ones that have one read as groups.
|
|
27
27
|
*/
|
|
28
|
-
function
|
|
28
|
+
export function modelsByVendor(models: ModelInfo[]): ModelInfo[] {
|
|
29
29
|
if (!models.some((model) => model.vendor !== undefined)) {
|
|
30
30
|
return models;
|
|
31
31
|
}
|
|
@@ -40,6 +40,27 @@ function byVendor(models: ModelInfo[]): ModelInfo[] {
|
|
|
40
40
|
});
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/* The models a long list shows before it is opened: the default and the one that is picked. Null when the list is short enough to show whole. */
|
|
44
|
+
export function shortlistOf(kind: AgentKind, models: ModelInfo[], current: PickerCurrent | null): ModelInfo[] | null {
|
|
45
|
+
if (models.length <= COMPACT_MODEL_LIMIT) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
return modelsByVendor(models.filter((model) => model.isDefault || (current?.provider === kind && current.model === model.slug)));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/* Whether the providers offer enough models together that a search is quicker than reading the list. */
|
|
52
|
+
export function wantsModelSearch(providers: readonly ProviderInfo[]): boolean {
|
|
53
|
+
return providers.reduce((count, entry) => count + entry.models.length, 0) > COMPACT_MODEL_LIMIT;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* Every model of every provider that a search finds, legacy ones included, by maker; a provider with none is left out. */
|
|
57
|
+
export function searchModels(providers: readonly ProviderInfo[], query: string): Array<{ entry: ProviderInfo; models: ModelInfo[] }> {
|
|
58
|
+
const needle = query.trim().toLowerCase();
|
|
59
|
+
return providers
|
|
60
|
+
.map((entry) => ({ entry, models: modelsByVendor(entry.models.filter((model) => matches(model, needle))) }))
|
|
61
|
+
.filter((group) => group.models.length > 0);
|
|
62
|
+
}
|
|
63
|
+
|
|
43
64
|
const NONE: ReadonlySet<AgentKind> = new Set();
|
|
44
65
|
|
|
45
66
|
/*
|
|
@@ -55,7 +76,8 @@ export function pickerEntriesOf(
|
|
|
55
76
|
expanded: ReadonlySet<AgentKind> = NONE,
|
|
56
77
|
current: PickerCurrent | null = null
|
|
57
78
|
): PickerEntry[] {
|
|
58
|
-
const rowsOf = (entry: ProviderInfo, models: ModelInfo[]): PickerEntry[] =>
|
|
79
|
+
const rowsOf = (entry: ProviderInfo, models: ModelInfo[]): PickerEntry[] =>
|
|
80
|
+
modelsByVendor(models).map((model) => ({ kind: 'model', provider: entry, model }));
|
|
59
81
|
if (query !== '') {
|
|
60
82
|
return providers.flatMap((entry) =>
|
|
61
83
|
rowsOf(
|
|
@@ -69,13 +91,13 @@ export function pickerEntriesOf(
|
|
|
69
91
|
);
|
|
70
92
|
const live = providers.flatMap((entry): PickerEntry[] => {
|
|
71
93
|
const models = entry.models.filter((model) => !model.legacy);
|
|
72
|
-
|
|
94
|
+
const shortlist = shortlistOf(entry.kind, models, current);
|
|
95
|
+
if (shortlist === null) {
|
|
73
96
|
return rowsOf(entry, models);
|
|
74
97
|
}
|
|
75
98
|
if (expanded.has(entry.kind)) {
|
|
76
99
|
return [{ kind: 'all', provider: entry, count: models.length, open: true }, ...rowsOf(entry, models)];
|
|
77
100
|
}
|
|
78
|
-
const shortlist = models.filter((model) => model.isDefault || (current?.provider === entry.kind && current.model === model.slug));
|
|
79
101
|
return [...rowsOf(entry, shortlist), { kind: 'all', provider: entry, count: models.length, open: false }];
|
|
80
102
|
});
|
|
81
103
|
if (legacy.length === 0) {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { useTranslation } from 'react-i18next';
|
|
2
|
+
import { Wrench } from 'lucide-react';
|
|
3
|
+
import type { ModelInfo } from '@adecore/agent-contracts';
|
|
4
|
+
import { Icon, Pill, Tooltip } from '@adecore/ui';
|
|
5
|
+
|
|
6
|
+
/* What a model row says beside the name: its badge, and a mark when the provider says the model can call tools. */
|
|
7
|
+
export function ModelMarks({ model }: { model: ModelInfo }) {
|
|
8
|
+
const { t } = useTranslation('agent-chat');
|
|
9
|
+
return (
|
|
10
|
+
<>
|
|
11
|
+
{model.badge && (
|
|
12
|
+
<Pill tone="accent" shape="tag" size="sm">
|
|
13
|
+
{model.badge.charAt(0).toLocaleUpperCase() + model.badge.slice(1)}
|
|
14
|
+
</Pill>
|
|
15
|
+
)}
|
|
16
|
+
{model.tools === true && (
|
|
17
|
+
<Tooltip label={t('pickers.model.tools')}>
|
|
18
|
+
<span className="flex shrink-0 text-text-faint" aria-label={t('pickers.model.tools')}>
|
|
19
|
+
<Icon icon={Wrench} size={12} />
|
|
20
|
+
</span>
|
|
21
|
+
</Tooltip>
|
|
22
|
+
)}
|
|
23
|
+
</>
|
|
24
|
+
);
|
|
25
|
+
}
|
package/src/chat/ui/Pickers.tsx
CHANGED
|
@@ -9,9 +9,10 @@ import { modelRequirementOf } from '../../agents/model-requirement';
|
|
|
9
9
|
import { useChatHost } from '../../host';
|
|
10
10
|
import { ChatScopeContext } from '../../scope';
|
|
11
11
|
import { useReportedModels } from '../../state/chats';
|
|
12
|
+
import { ModelMarks } from './ModelMarks';
|
|
12
13
|
import { pickerEntriesOf, type PickerEntry } from '../logic/picker-entries';
|
|
13
14
|
import { forgetStashed, STASH_SHORTCUT, useStash, type StashedPrompt } from '../stash';
|
|
14
|
-
import { Icon, IconButton, Menu,
|
|
15
|
+
import { Icon, IconButton, Menu, Popover, Tooltip } from '@adecore/ui';
|
|
15
16
|
|
|
16
17
|
const triggerClass =
|
|
17
18
|
'flex h-7 shrink-0 items-center gap-1 whitespace-nowrap rounded-md px-2 text-xs text-text-muted hover:bg-surface-hover hover:text-text data-[popup-open]:bg-surface-active data-[popup-open]:text-text';
|
|
@@ -257,11 +258,7 @@ export function ModelPicker({
|
|
|
257
258
|
<span className="flex w-full items-center gap-2">
|
|
258
259
|
<Menu.Check kind="radio" checked={chosen} />
|
|
259
260
|
<span className="min-w-0 truncate">{entry.model.name}</span>
|
|
260
|
-
{entry.model
|
|
261
|
-
<Pill tone="accent" shape="tag" size="sm">
|
|
262
|
-
{entry.model.badge.charAt(0).toLocaleUpperCase() + entry.model.badge.slice(1)}
|
|
263
|
-
</Pill>
|
|
264
|
-
)}
|
|
261
|
+
<ModelMarks model={entry.model} />
|
|
265
262
|
<span className="grow" />
|
|
266
263
|
{entry.model.legacy && <span className="text-xs text-text-faint">{t('pickers.model.legacy')}</span>}
|
|
267
264
|
</span>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Fragment, useEffect, useRef, useState, type KeyboardEvent } from 'react';
|
|
1
2
|
import { useTranslation } from 'react-i18next';
|
|
2
3
|
import clsx from 'clsx';
|
|
3
4
|
import {
|
|
@@ -9,6 +10,7 @@ import {
|
|
|
9
10
|
FilePen,
|
|
10
11
|
Gauge,
|
|
11
12
|
Hand,
|
|
13
|
+
Search,
|
|
12
14
|
Settings,
|
|
13
15
|
Shield,
|
|
14
16
|
ShieldAlert,
|
|
@@ -22,10 +24,12 @@ import { canContinueOn } from '../../agents/accounts';
|
|
|
22
24
|
import { AgentIcon } from '../../agents/AgentIcon';
|
|
23
25
|
import type { AccountChoice } from '../account-choice';
|
|
24
26
|
import { modelName, shortModelName } from '../../agents/model-name';
|
|
27
|
+
import { ModelMarks } from './ModelMarks';
|
|
28
|
+
import { modelsByVendor, searchModels, shortlistOf, wantsModelSearch } from '../logic/picker-entries';
|
|
25
29
|
import { CONTEXT_OPTION, contextFraction, contextSegments, orderOptions, type ContextPart } from '../logic/context-usage';
|
|
26
30
|
import { RUNTIME_MODES } from '../runtime-modes';
|
|
27
31
|
import { formatClock, formatWeekdayClock, isSameDay, formatPercent, formatTokens } from '@adecore/ui/format';
|
|
28
|
-
import { Icon, Menu,
|
|
32
|
+
import { Icon, Menu, Tooltip, useNow } from '@adecore/ui';
|
|
29
33
|
import { useChatHost } from '../../host';
|
|
30
34
|
import { useUsageLimits } from '../../usage/limits';
|
|
31
35
|
|
|
@@ -134,6 +138,8 @@ export function RunSettings({
|
|
|
134
138
|
account
|
|
135
139
|
}: RunSettingsProps) {
|
|
136
140
|
const { t } = useTranslation('agent-chat');
|
|
141
|
+
const [query, setQuery] = useState('');
|
|
142
|
+
const searchRef = useRef<HTMLInputElement>(null);
|
|
137
143
|
const owner = providers.find((entry) => entry.kind === provider);
|
|
138
144
|
const current = modelName(selection.model, owner?.models);
|
|
139
145
|
const short = shortModelName(current, owner?.models);
|
|
@@ -164,6 +170,100 @@ export function RunSettings({
|
|
|
164
170
|
onModel(value.slice(0, slash) as AgentKind, value.slice(slash + 1));
|
|
165
171
|
};
|
|
166
172
|
|
|
173
|
+
const searchable = wantsModelSearch(providers);
|
|
174
|
+
const found = query.trim() === '' ? null : searchModels(providers, query);
|
|
175
|
+
|
|
176
|
+
// The menu moves the focus to its popup as it opens, so the search field takes it a frame later.
|
|
177
|
+
useEffect(() => {
|
|
178
|
+
if (!open || !searchable) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const frame = requestAnimationFrame(() => searchRef.current?.focus());
|
|
182
|
+
return () => cancelAnimationFrame(frame);
|
|
183
|
+
}, [open, searchable]);
|
|
184
|
+
|
|
185
|
+
const changeOpen = (next: boolean): void => {
|
|
186
|
+
if (!next) {
|
|
187
|
+
setQuery('');
|
|
188
|
+
}
|
|
189
|
+
onOpenChange(next);
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// An item the arrow keys moved to has the focus; typing there still goes to the search.
|
|
193
|
+
const onPopupKey = (event: KeyboardEvent<HTMLDivElement>): void => {
|
|
194
|
+
if (!searchable || event.target === searchRef.current || event.metaKey || event.ctrlKey || event.altKey) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const typed = event.key.length === 1 && event.key !== ' ';
|
|
198
|
+
if (!typed && event.key !== 'Backspace') {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
event.preventDefault();
|
|
202
|
+
event.stopPropagation();
|
|
203
|
+
setQuery((now) => (typed ? now + event.key : now.slice(0, -1)));
|
|
204
|
+
searchRef.current?.focus();
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
// The menu reads letters as a jump to an item and arrows as moving through them; the field keeps what it types.
|
|
208
|
+
const onSearchKey = (event: KeyboardEvent<HTMLInputElement>): void => {
|
|
209
|
+
if (event.key === 'Escape' && query !== '') {
|
|
210
|
+
event.preventDefault();
|
|
211
|
+
event.stopPropagation();
|
|
212
|
+
setQuery('');
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (event.key === 'Enter') {
|
|
216
|
+
event.preventDefault();
|
|
217
|
+
const first = found?.[0];
|
|
218
|
+
if (first !== undefined) {
|
|
219
|
+
onModel(first.entry.kind, first.models[0]!.slug);
|
|
220
|
+
changeOpen(false);
|
|
221
|
+
}
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (event.key !== 'Escape' && event.key !== 'ArrowDown' && event.key !== 'ArrowUp' && event.key !== 'Tab') {
|
|
225
|
+
event.stopPropagation();
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const modelRow = (entry: ProviderInfo, row: ModelInfo) => (
|
|
230
|
+
<Menu.RadioItem key={row.slug} value={modelValue(entry.kind, row.slug)} closeOnClick>
|
|
231
|
+
<AgentIcon kind={entry.kind} size={14} className="shrink-0" />
|
|
232
|
+
<span className="min-w-0 truncate">{row.name}</span>
|
|
233
|
+
<ModelMarks model={row} />
|
|
234
|
+
</Menu.RadioItem>
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
// Every model of one provider, under a label per maker where the provider names them.
|
|
238
|
+
const byMaker = (entry: ProviderInfo, models: ModelInfo[]) =>
|
|
239
|
+
modelsByVendor(models).map((row, i, sorted) => (
|
|
240
|
+
<Fragment key={row.slug}>
|
|
241
|
+
{row.vendor !== undefined && sorted[i - 1]?.vendor !== row.vendor && <Menu.GroupLabel>{row.vendor}</Menu.GroupLabel>}
|
|
242
|
+
{modelRow(entry, row)}
|
|
243
|
+
</Fragment>
|
|
244
|
+
));
|
|
245
|
+
|
|
246
|
+
// A provider with many models shows the default and the chosen one, with all of them one submenu away.
|
|
247
|
+
const providerModels = (entry: ProviderInfo, models: ModelInfo[]) => {
|
|
248
|
+
const shortlist = shortlistOf(entry.kind, models, { provider, model: selection.model });
|
|
249
|
+
if (shortlist === null) {
|
|
250
|
+
return models.map((row) => modelRow(entry, row));
|
|
251
|
+
}
|
|
252
|
+
return (
|
|
253
|
+
<>
|
|
254
|
+
{shortlist.map((row) => modelRow(entry, row))}
|
|
255
|
+
<Menu.SubmenuRoot>
|
|
256
|
+
<Menu.SubmenuTrigger className="text-text-muted">{t('pickers.model.allGroup', { count: models.length })}</Menu.SubmenuTrigger>
|
|
257
|
+
<Menu.Popup className="w-80">
|
|
258
|
+
<Menu.RadioGroup value={chosenModel} onValueChange={chooseModel}>
|
|
259
|
+
{byMaker(entry, models)}
|
|
260
|
+
</Menu.RadioGroup>
|
|
261
|
+
</Menu.Popup>
|
|
262
|
+
</Menu.SubmenuRoot>
|
|
263
|
+
</>
|
|
264
|
+
);
|
|
265
|
+
};
|
|
266
|
+
|
|
167
267
|
const modelGroups = (groups: typeof live) =>
|
|
168
268
|
groups.map(({ entry, models }) => (
|
|
169
269
|
<Menu.Group key={entry.kind}>
|
|
@@ -173,22 +273,12 @@ export function RunSettings({
|
|
|
173
273
|
{entry.name}
|
|
174
274
|
</Menu.GroupLabel>
|
|
175
275
|
)}
|
|
176
|
-
{models
|
|
177
|
-
<Menu.RadioItem key={row.slug} value={modelValue(entry.kind, row.slug)} closeOnClick>
|
|
178
|
-
<AgentIcon kind={entry.kind} size={14} className="shrink-0" />
|
|
179
|
-
<span className="min-w-0 truncate">{row.name}</span>
|
|
180
|
-
{row.badge && (
|
|
181
|
-
<Pill tone="accent" shape="tag" size="sm">
|
|
182
|
-
{row.badge.charAt(0).toLocaleUpperCase() + row.badge.slice(1)}
|
|
183
|
-
</Pill>
|
|
184
|
-
)}
|
|
185
|
-
</Menu.RadioItem>
|
|
186
|
-
))}
|
|
276
|
+
{providerModels(entry, models)}
|
|
187
277
|
</Menu.Group>
|
|
188
278
|
));
|
|
189
279
|
|
|
190
280
|
return (
|
|
191
|
-
<Menu.Root open={open} onOpenChange={
|
|
281
|
+
<Menu.Root open={open} onOpenChange={changeOpen} highlightItemOnHover={!searchable}>
|
|
192
282
|
<Tooltip label={permissionHint ?? t('pickers.settings.title')} kbd="/model">
|
|
193
283
|
{/* Styled from `open`: the tooltip puts `data-popup-open` on the same trigger. */}
|
|
194
284
|
<Menu.Trigger
|
|
@@ -233,63 +323,102 @@ export function RunSettings({
|
|
|
233
323
|
<Icon icon={open ? ChevronUp : ChevronDown} size={12} className="shrink-0 @max-sm/composer:hidden" />
|
|
234
324
|
</Menu.Trigger>
|
|
235
325
|
</Tooltip>
|
|
236
|
-
<Menu.Popup side="top" sideOffset={8} align="start" className="w-80">
|
|
326
|
+
<Menu.Popup side="top" sideOffset={8} align="start" className="w-80" onKeyDownCapture={onPopupKey}>
|
|
237
327
|
{providers.length === 0 && <div className="px-2.5 py-3 text-xs text-text-faint">{t('pickers.model.noProvider')}</div>}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
</Menu.RadioGroup>
|
|
253
|
-
</Menu.Popup>
|
|
254
|
-
</Menu.SubmenuRoot>
|
|
328
|
+
{searchable && (
|
|
329
|
+
<div className="flex items-center gap-2 border-b border-border px-2.5">
|
|
330
|
+
<Icon icon={Search} size={14} className="shrink-0 text-text-faint" />
|
|
331
|
+
<input
|
|
332
|
+
ref={searchRef}
|
|
333
|
+
className="h-8 w-full bg-transparent text-sm text-text outline-none placeholder:text-text-faint"
|
|
334
|
+
placeholder={t('pickers.model.search')}
|
|
335
|
+
aria-label={t('pickers.model.search')}
|
|
336
|
+
spellCheck={false}
|
|
337
|
+
value={query}
|
|
338
|
+
onChange={(event) => setQuery(event.target.value)}
|
|
339
|
+
onKeyDown={onSearchKey}
|
|
340
|
+
/>
|
|
341
|
+
</div>
|
|
255
342
|
)}
|
|
256
|
-
<
|
|
257
|
-
{
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
size={14}
|
|
274
|
-
className={mode === 'full-access' ? 'text-status-needs-you' : 'text-text-faint'}
|
|
275
|
-
/>
|
|
276
|
-
</span>
|
|
277
|
-
<span className="flex min-w-0 grow flex-col">
|
|
278
|
-
{t(`modes.${mode}.label`)}
|
|
279
|
-
<span className="text-xs text-text-faint">{t(`modes.${mode}.hint`)}</span>
|
|
280
|
-
</span>
|
|
281
|
-
</Menu.RadioItem>
|
|
282
|
-
))}
|
|
283
|
-
</Menu.RadioGroup>
|
|
284
|
-
</Menu.Popup>
|
|
285
|
-
</Menu.SubmenuRoot>
|
|
286
|
-
{options.map((option) => (
|
|
287
|
-
<OptionRow key={option.id} option={option} selection={selection} onChange={(value) => onOption(option.id, value)} />
|
|
288
|
-
))}
|
|
289
|
-
{(hasContextOption || standaloneContext) && (
|
|
343
|
+
{found !== null && found.length === 0 && <div className="px-2.5 py-3 text-xs text-text-faint">{t('pickers.model.noMatch')}</div>}
|
|
344
|
+
{found !== null && found.length > 0 && (
|
|
345
|
+
<Menu.RadioGroup value={chosenModel} onValueChange={chooseModel}>
|
|
346
|
+
{found.map(({ entry, models }) => (
|
|
347
|
+
<Menu.Group key={entry.kind}>
|
|
348
|
+
{grouped && (
|
|
349
|
+
<Menu.GroupLabel className="flex items-center gap-1.5">
|
|
350
|
+
<AgentIcon kind={entry.kind} size={12} />
|
|
351
|
+
{entry.name}
|
|
352
|
+
</Menu.GroupLabel>
|
|
353
|
+
)}
|
|
354
|
+
{byMaker(entry, models)}
|
|
355
|
+
</Menu.Group>
|
|
356
|
+
))}
|
|
357
|
+
</Menu.RadioGroup>
|
|
358
|
+
)}
|
|
359
|
+
{found === null && (
|
|
290
360
|
<>
|
|
361
|
+
<Menu.RadioGroup value={chosenModel} onValueChange={chooseModel}>
|
|
362
|
+
{modelGroups(live)}
|
|
363
|
+
</Menu.RadioGroup>
|
|
364
|
+
{legacy.length > 0 && (
|
|
365
|
+
<Menu.SubmenuRoot>
|
|
366
|
+
<Menu.SubmenuTrigger className="text-text-muted">
|
|
367
|
+
<span className="min-w-0 grow truncate">
|
|
368
|
+
{t('pickers.model.legacyGroup', { count: legacy.reduce((count, group) => count + group.models.length, 0) })}
|
|
369
|
+
</span>
|
|
370
|
+
{chosenLegacy && <Menu.Hint className="truncate">{chosenLegacy.name}</Menu.Hint>}
|
|
371
|
+
</Menu.SubmenuTrigger>
|
|
372
|
+
<Menu.Popup className="min-w-56">
|
|
373
|
+
<Menu.RadioGroup value={chosenModel} onValueChange={chooseModel}>
|
|
374
|
+
{modelGroups(legacy)}
|
|
375
|
+
</Menu.RadioGroup>
|
|
376
|
+
</Menu.Popup>
|
|
377
|
+
</Menu.SubmenuRoot>
|
|
378
|
+
)}
|
|
291
379
|
<Menu.Separator />
|
|
292
|
-
<
|
|
380
|
+
{account !== null && <AccountSubmenu provider={provider} account={account} />}
|
|
381
|
+
<Menu.SubmenuRoot>
|
|
382
|
+
<Menu.SubmenuTrigger className="text-text-muted">
|
|
383
|
+
<Icon
|
|
384
|
+
icon={MODE_ICONS[runtimeMode]}
|
|
385
|
+
size={14}
|
|
386
|
+
className={clsx('shrink-0', fullAccess ? 'text-status-needs-you' : 'text-text-faint')}
|
|
387
|
+
/>
|
|
388
|
+
<span className="min-w-0 grow truncate">{t('pickers.mode.label')}</span>
|
|
389
|
+
<Menu.Hint className="truncate">{t(`modes.${runtimeMode}.label`)}</Menu.Hint>
|
|
390
|
+
</Menu.SubmenuTrigger>
|
|
391
|
+
<Menu.Popup className="w-72">
|
|
392
|
+
{permissionHint !== null && <p className="px-2.5 py-2 text-xs text-text-muted">{permissionHint}</p>}
|
|
393
|
+
<Menu.RadioGroup value={runtimeMode} onValueChange={(mode: RuntimeMode) => onMode(mode)}>
|
|
394
|
+
{RUNTIME_MODES.map((mode) => (
|
|
395
|
+
<Menu.RadioItem key={mode} value={mode} className="items-start">
|
|
396
|
+
{/* The hint makes the row two lines high; the box keeps the icon on the label's line. */}
|
|
397
|
+
<span className="grid h-lh w-4 shrink-0 place-items-center">
|
|
398
|
+
<Icon
|
|
399
|
+
icon={MODE_ICONS[mode]}
|
|
400
|
+
size={14}
|
|
401
|
+
className={mode === 'full-access' ? 'text-status-needs-you' : 'text-text-faint'}
|
|
402
|
+
/>
|
|
403
|
+
</span>
|
|
404
|
+
<span className="flex min-w-0 grow flex-col">
|
|
405
|
+
{t(`modes.${mode}.label`)}
|
|
406
|
+
<span className="text-xs text-text-faint">{t(`modes.${mode}.hint`)}</span>
|
|
407
|
+
</span>
|
|
408
|
+
</Menu.RadioItem>
|
|
409
|
+
))}
|
|
410
|
+
</Menu.RadioGroup>
|
|
411
|
+
</Menu.Popup>
|
|
412
|
+
</Menu.SubmenuRoot>
|
|
413
|
+
{options.map((option) => (
|
|
414
|
+
<OptionRow key={option.id} option={option} selection={selection} onChange={(value) => onOption(option.id, value)} />
|
|
415
|
+
))}
|
|
416
|
+
{(hasContextOption || standaloneContext) && (
|
|
417
|
+
<>
|
|
418
|
+
<Menu.Separator />
|
|
419
|
+
<ContextUsage usage={usage} disabled={compactDisabled} onCompact={onCompact} />
|
|
420
|
+
</>
|
|
421
|
+
)}
|
|
293
422
|
</>
|
|
294
423
|
)}
|
|
295
424
|
</Menu.Popup>
|
|
@@ -439,6 +439,7 @@ export default {
|
|
|
439
439
|
legacyGroup_one: 'Legacy models ({{count}})',
|
|
440
440
|
legacyGroup_other: 'Legacy models ({{count}})',
|
|
441
441
|
legacy: 'Legacy',
|
|
442
|
+
tools: 'Can use tools',
|
|
442
443
|
requires: 'Requires {{cli}} {{version}} or later on {{machine}}',
|
|
443
444
|
requiresHere: 'Requires {{cli}} {{version}} or later',
|
|
444
445
|
providerSettings: 'Provider settings'
|
|
@@ -439,6 +439,7 @@ export default {
|
|
|
439
439
|
legacyGroup_one: 'Legacy-modellen ({{count}})',
|
|
440
440
|
legacyGroup_other: 'Legacy-modellen ({{count}})',
|
|
441
441
|
legacy: 'Legacy',
|
|
442
|
+
tools: 'Kan tools gebruiken',
|
|
442
443
|
requires: 'Vereist {{cli}} {{version}} of nieuwer op {{machine}}',
|
|
443
444
|
requiresHere: 'Vereist {{cli}} {{version}} of nieuwer',
|
|
444
445
|
providerSettings: 'Providerinstellingen'
|