@adecore/agents-react 0.29.3 → 0.31.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.
Files changed (52) hide show
  1. package/dist/agents/AgentIcon.js +4 -3
  2. package/dist/agents/accounts.d.ts +2 -0
  3. package/dist/agents/accounts.js +6 -0
  4. package/dist/agents/agent-marks.d.ts +8 -0
  5. package/dist/agents/agent-marks.js +12 -0
  6. package/dist/agents/provider-paths.js +3 -2
  7. package/dist/chat/logic/picker-entries.d.ts +12 -2
  8. package/dist/chat/logic/picker-entries.js +41 -7
  9. package/dist/chat/ui/Pickers.js +31 -3
  10. package/dist/chat/ui/rows/row-rhythm.js +3 -2
  11. package/dist/index.d.ts +1 -0
  12. package/dist/index.js +1 -0
  13. package/dist/locales/en/agent-chat.d.ts +2 -0
  14. package/dist/locales/en/agent-chat.js +2 -0
  15. package/dist/locales/en/agent-providers.d.ts +3 -0
  16. package/dist/locales/en/agent-providers.js +4 -1
  17. package/dist/locales/nl/agent-chat.d.ts +2 -0
  18. package/dist/locales/nl/agent-chat.js +2 -0
  19. package/dist/locales/nl/agent-providers.d.ts +3 -0
  20. package/dist/locales/nl/agent-providers.js +4 -1
  21. package/dist/providers/AccountDetail.d.ts +4 -3
  22. package/dist/providers/AccountDetail.js +6 -5
  23. package/dist/providers/AddAccountForm.d.ts +3 -2
  24. package/dist/providers/AddAccountForm.js +2 -2
  25. package/dist/providers/CliDetail.d.ts +3 -3
  26. package/dist/providers/CliDetail.js +3 -3
  27. package/dist/providers/ProvidersPane.js +11 -10
  28. package/dist/providers/parts.js +5 -4
  29. package/dist/theme.css +3 -0
  30. package/dist/usage/format.js +6 -2
  31. package/dist/usage/limit-groups.js +2 -1
  32. package/package.json +8 -8
  33. package/src/agents/AgentIcon.tsx +12 -3
  34. package/src/agents/accounts.ts +13 -0
  35. package/src/agents/agent-marks.ts +26 -0
  36. package/src/agents/provider-paths.ts +3 -2
  37. package/src/chat/logic/picker-entries.ts +70 -10
  38. package/src/chat/ui/Pickers.tsx +54 -2
  39. package/src/chat/ui/rows/row-rhythm.ts +3 -2
  40. package/src/index.ts +1 -0
  41. package/src/locales/en/agent-chat.ts +2 -0
  42. package/src/locales/en/agent-providers.ts +4 -1
  43. package/src/locales/nl/agent-chat.ts +2 -0
  44. package/src/locales/nl/agent-providers.ts +4 -1
  45. package/src/providers/AccountDetail.tsx +34 -26
  46. package/src/providers/AddAccountForm.tsx +8 -3
  47. package/src/providers/CliDetail.tsx +5 -5
  48. package/src/providers/ProvidersPane.tsx +13 -10
  49. package/src/providers/parts.tsx +6 -5
  50. package/src/theme.css +3 -0
  51. package/src/usage/format.ts +6 -2
  52. package/src/usage/limit-groups.ts +2 -1
@@ -3,18 +3,19 @@ import clsx from 'clsx';
3
3
  import { Bot } from 'lucide-react';
4
4
  import { siClaude, siGithubcopilot, siGooglegemini } from 'simple-icons';
5
5
  import { Icon } from '@adecore/ui';
6
+ import { hostAgentMark } from './agent-marks.js';
6
7
  import { PROVIDER_PATHS } from './provider-paths.js';
7
8
  // simple-icons removed OpenAI's mark at its request, so Codex reuses the usage page's path.
8
- const MARKS = {
9
+ const BUILT_IN_MARKS = {
9
10
  claude: siClaude,
10
11
  codex: { title: 'Codex', path: PROVIDER_PATHS.codex },
11
12
  gemini: siGooglegemini,
12
13
  copilot: siGithubcopilot
13
14
  };
14
15
  export function AgentIcon({ kind, size = 14, className }) {
15
- const mark = MARKS[kind];
16
+ const mark = hostAgentMark(kind) ?? BUILT_IN_MARKS[kind];
16
17
  if (!mark) {
17
18
  return _jsx(Icon, { icon: Bot, size: size, className: className });
18
19
  }
19
- return (_jsx("svg", { role: "img", "aria-label": mark.title, viewBox: "0 0 24 24", width: size, height: size, fill: "currentColor", className: clsx('align-middle', className), children: _jsx("path", { d: mark.path }) }));
20
+ return (_jsx("svg", { role: "img", "aria-label": mark.title, viewBox: mark.viewBox ?? '0 0 24 24', width: size, height: size, fill: "currentColor", className: clsx('align-middle', className), children: _jsx("path", { d: mark.path }) }));
20
21
  }
@@ -10,6 +10,8 @@ export interface AccountEntry {
10
10
  }
11
11
  export declare function accountsOfKind(accounts: ProviderAccounts | null, kind: AgentKind): AccountEntry[];
12
12
  export declare function accountName(entry: Pick<AccountEntry, 'id' | 'account' | 'isDefault'>, providerName: string): string;
13
+ export type AccountLogin = 'terminal' | 'chat' | null;
14
+ export declare function accountLoginOf(accounts: ProviderAccounts | null | undefined, kind: AgentKind): AccountLogin;
13
15
  export type AccountTone = 'muted' | 'needs-you' | 'error';
14
16
  export declare const ACCOUNT_TONE_CLASSES: Record<AccountTone, string>;
15
17
  export declare function accountStatusLine(status: ProviderAccountStatus | null, canLogIn: boolean): {
@@ -26,6 +26,12 @@ export function accountsOfKind(accounts, kind) {
26
26
  export function accountName(entry, providerName) {
27
27
  return entry.account.label ?? (entry.isDefault ? providerName : entry.id);
28
28
  }
29
+ export function accountLoginOf(accounts, kind) {
30
+ if (accounts?.loginCommands?.[kind] !== undefined) {
31
+ return 'terminal';
32
+ }
33
+ return accounts?.chatLogins?.includes(kind) === true ? 'chat' : null;
34
+ }
29
35
  export const ACCOUNT_TONE_CLASSES = {
30
36
  muted: 'text-text-muted',
31
37
  'needs-you': 'text-status-needs-you',
@@ -0,0 +1,8 @@
1
+ import type { AgentKind } from '@adecore/agent-contracts';
2
+ export interface AgentMark {
3
+ title: string;
4
+ path: string;
5
+ viewBox?: string;
6
+ }
7
+ export declare function configureAgentMarks(marks: Readonly<Partial<Record<AgentKind, AgentMark>>>): void;
8
+ export declare function hostAgentMark(kind: AgentKind): AgentMark | undefined;
@@ -0,0 +1,12 @@
1
+ let hostMarks = {};
2
+ /*
3
+ * The marks of the kinds a host adds, and any it draws differently, set once before the first render. Every
4
+ * place the shared UI draws an agent's mark reads them; a kind with none keeps the built-in mark or the robot.
5
+ * A later call replaces the whole set.
6
+ */
7
+ export function configureAgentMarks(marks) {
8
+ hostMarks = { ...marks };
9
+ }
10
+ export function hostAgentMark(kind) {
11
+ return hostMarks[kind];
12
+ }
@@ -1,8 +1,9 @@
1
1
  /*
2
- * The marks of Anthropic and OpenAI. Lucide has no brand marks, so these are the icon rule's
2
+ * The marks of Anthropic and OpenAI, and a monogram for a provider with no mark of its own. Lucide has no brand marks, so these are the icon rule's
3
3
  * exception for provider marks. One path each, in `currentColor`, so a row decides how loud the logo is.
4
4
  */
5
5
  export const PROVIDER_PATHS = {
6
6
  claude: 'M4.709 15.955l4.72-2.647.079-.23-.08-.128H9.2l-.79-.048-2.698-.073-2.339-.097-2.266-.122-.571-.121L0 11.784l.055-.352.48-.321.686.06 1.52.103 2.278.158 1.652.097 2.449.255h.389l.055-.157-.134-.098-.103-.097-2.358-1.596-2.552-1.688-1.336-.972-.724-.491-.364-.462-.158-1.008.656-.722.881.06.225.061.893.686 1.908 1.476 2.491 1.833.365.304.145-.103.019-.073-.164-.274-1.355-2.446-1.446-2.49-.644-1.032-.17-.619a2.97 2.97 0 01-.104-.729L6.283.134 6.696 0l.996.134.42.364.62 1.414 1.002 2.229 1.555 3.03.456.898.243.832.091.255h.158V9.01l.128-1.706.237-2.095.23-2.695.08-.76.377-.91.747-.492.584.28.48.685-.067.444-.286 1.851-.559 2.903-.364 1.942h.212l.243-.242.985-1.306 1.652-2.064.73-.82.85-.904.547-.431h1.033l.76 1.129-.34 1.166-1.064 1.347-.881 1.142-1.264 1.7-.79 1.36.073.11.188-.02 2.856-.606 1.543-.28 1.841-.315.833.388.091.395-.328.807-1.969.486-2.309.462-3.439.813-.042.03.048.061 1.549.146.662.036h1.622l3.02.225.79.522.474.638-.079.485-1.215.62-1.64-.389-3.829-.91-1.312-.329h-.182v.11l1.093 1.068 2.006 1.81 2.509 2.33.127.578-.322.455-.34-.049-2.205-1.657-.851-.747-1.926-1.62h-.128v.17l.444.649 2.345 3.521.122 1.08-.17.353-.607.212-.668-.122-1.374-1.925-1.415-2.167-1.143-1.943-.14.08-.674 7.254-.316.37-.729.28-.607-.461-.322-.747.322-1.476.389-1.924.315-1.53.286-1.9.17-.632-.012-.042-.14.018-1.434 1.967-2.18 2.945-1.726 1.845-.414.164-.717-.37.067-.662.401-.589 2.388-3.036 1.44-1.882.93-1.086-.006-.158h-.055L4.132 18.56l-1.13.146-.487-.456.061-.746.231-.243 1.914-1.316-.012.01z',
7
- codex: 'M21.55 10.004a5.416 5.416 0 00-.478-4.501c-1.217-2.09-3.662-3.166-6.05-2.66A5.59 5.59 0 0010.831 1C8.39.995 6.224 2.546 5.473 4.838A5.553 5.553 0 001.76 7.496a5.487 5.487 0 00.691 6.5 5.416 5.416 0 00.477 4.502c1.217 2.09 3.662 3.165 6.05 2.66A5.586 5.586 0 0013.168 23c2.443.006 4.61-1.546 5.361-3.84a5.554 5.554 0 003.715-2.66 5.488 5.488 0 00-.693-6.497v.001zm-8.381 11.558a4.199 4.199 0 01-2.675-.954c.034-.018.093-.05.132-.074l4.44-2.53a.71.71 0 00.364-.623v-6.176l1.877 1.069c.02.01.033.029.036.05v5.115c-.003 2.274-1.87 4.118-4.174 4.123zM4.192 17.78a4.059 4.059 0 01-.498-2.763c.032.019.09.055.131.078l4.44 2.53c.225.13.504.13.73 0l5.42-3.088v2.138a.068.068 0 01-.027.057L9.9 19.288c-1.999 1.136-4.552.46-5.707-1.51h-.001zM3.023 8.216A4.15 4.15 0 015.198 6.41l-.002.151v5.06a.711.711 0 00.364.624l5.42 3.087-1.876 1.07a.067.067 0 01-.063.005l-4.489-2.559c-1.995-1.14-2.679-3.658-1.53-5.63h.001zm15.417 3.54l-5.42-3.088L14.896 7.6a.067.067 0 01.063-.006l4.489 2.557c1.998 1.14 2.683 3.662 1.529 5.633a4.163 4.163 0 01-2.174 1.807V12.38a.71.71 0 00-.363-.623zm1.867-2.773a6.04 6.04 0 00-.132-.078l-4.44-2.53a.731.731 0 00-.729 0l-5.42 3.088V7.325a.068.068 0 01.027-.057L14.1 4.713c2-1.137 4.555-.46 5.707 1.513.487.833.664 1.809.499 2.757h.001zm-11.741 3.81l-1.877-1.068a.065.065 0 01-.036-.051V6.559c.001-2.277 1.873-4.122 4.181-4.12.976 0 1.92.338 2.671.954-.034.018-.092.05-.131.073l-4.44 2.53a.71.71 0 00-.365.623l-.003 6.173v.001zm1.02-2.168L12 9.25l2.414 1.375v2.75L12 14.75l-2.415-1.375v-2.75z'
7
+ codex: 'M21.55 10.004a5.416 5.416 0 00-.478-4.501c-1.217-2.09-3.662-3.166-6.05-2.66A5.59 5.59 0 0010.831 1C8.39.995 6.224 2.546 5.473 4.838A5.553 5.553 0 001.76 7.496a5.487 5.487 0 00.691 6.5 5.416 5.416 0 00.477 4.502c1.217 2.09 3.662 3.165 6.05 2.66A5.586 5.586 0 0013.168 23c2.443.006 4.61-1.546 5.361-3.84a5.554 5.554 0 003.715-2.66 5.488 5.488 0 00-.693-6.497v.001zm-8.381 11.558a4.199 4.199 0 01-2.675-.954c.034-.018.093-.05.132-.074l4.44-2.53a.71.71 0 00.364-.623v-6.176l1.877 1.069c.02.01.033.029.036.05v5.115c-.003 2.274-1.87 4.118-4.174 4.123zM4.192 17.78a4.059 4.059 0 01-.498-2.763c.032.019.09.055.131.078l4.44 2.53c.225.13.504.13.73 0l5.42-3.088v2.138a.068.068 0 01-.027.057L9.9 19.288c-1.999 1.136-4.552.46-5.707-1.51h-.001zM3.023 8.216A4.15 4.15 0 015.198 6.41l-.002.151v5.06a.711.711 0 00.364.624l5.42 3.087-1.876 1.07a.067.067 0 01-.063.005l-4.489-2.559c-1.995-1.14-2.679-3.658-1.53-5.63h.001zm15.417 3.54l-5.42-3.088L14.896 7.6a.067.067 0 01.063-.006l4.489 2.557c1.998 1.14 2.683 3.662 1.529 5.633a4.163 4.163 0 01-2.174 1.807V12.38a.71.71 0 00-.363-.623zm1.867-2.773a6.04 6.04 0 00-.132-.078l-4.44-2.53a.731.731 0 00-.729 0l-5.42 3.088V7.325a.068.068 0 01.027-.057L14.1 4.713c2-1.137 4.555-.46 5.707 1.513.487.833.664 1.809.499 2.757h.001zm-11.741 3.81l-1.877-1.068a.065.065 0 01-.036-.051V6.559c.001-2.277 1.873-4.122 4.181-4.12.976 0 1.92.338 2.671.954-.034.018-.092.05-.131.073l-4.44 2.53a.71.71 0 00-.365.623l-.003 6.173v.001zm1.02-2.168L12 9.25l2.414 1.375v2.75L12 14.75l-2.415-1.375v-2.75z',
8
+ nanogpt: 'M5 20V4h2.6l8.8 11.2V4H19v16h-2.6L7.6 8.8V20z'
8
9
  };
@@ -1,4 +1,5 @@
1
- import type { ModelInfo, ProviderInfo } from '@adecore/agent-contracts';
1
+ import type { AgentKind, ModelInfo, ProviderInfo } from '@adecore/agent-contracts';
2
+ export declare const COMPACT_MODEL_LIMIT = 8;
2
3
  export type PickerEntry = {
3
4
  kind: 'model';
4
5
  provider: ProviderInfo;
@@ -6,5 +7,14 @@ export type PickerEntry = {
6
7
  } | {
7
8
  kind: 'legacy';
8
9
  count: number;
10
+ } | {
11
+ kind: 'all';
12
+ provider: ProviderInfo;
13
+ count: number;
14
+ open: boolean;
9
15
  };
10
- export declare function pickerEntriesOf(providers: readonly ProviderInfo[], query: string, legacyOpen: boolean): PickerEntry[];
16
+ export interface PickerCurrent {
17
+ provider: AgentKind;
18
+ model: string;
19
+ }
20
+ export declare function pickerEntriesOf(providers: readonly ProviderInfo[], query: string, legacyOpen: boolean, expanded?: ReadonlySet<AgentKind>, current?: PickerCurrent | null): PickerEntry[];
@@ -1,17 +1,51 @@
1
+ /*
2
+ * A provider with more live models than this keeps the list short: the model that is picked and the default,
3
+ * with the rest behind one row that opens all of them. A list of three hundred models is not one to scroll.
4
+ */
5
+ export const COMPACT_MODEL_LIMIT = 8;
1
6
  function matches(model, query) {
2
- return model.name.toLowerCase().includes(query) || model.slug.toLowerCase().includes(query);
7
+ return model.name.toLowerCase().includes(query) || model.slug.toLowerCase().includes(query) || (model.vendor?.toLowerCase().includes(query) ?? false);
8
+ }
9
+ /*
10
+ * By maker, then by name, when the provider says who made its models; a list with no makers keeps the order
11
+ * it came in. A model with no maker goes last, so the ones that have one read as groups.
12
+ */
13
+ function byVendor(models) {
14
+ if (!models.some((model) => model.vendor !== undefined)) {
15
+ return models;
16
+ }
17
+ return [...models].sort((a, b) => {
18
+ if (a.vendor === undefined || b.vendor === undefined) {
19
+ return a.vendor === b.vendor ? 0 : a.vendor === undefined ? 1 : -1;
20
+ }
21
+ return (a.vendor.localeCompare(b.vendor, undefined, { sensitivity: 'base' }) ||
22
+ a.name.localeCompare(b.name, undefined, { sensitivity: 'base', numeric: true }));
23
+ });
3
24
  }
25
+ const NONE = new Set();
4
26
  /*
5
27
  * The rows of the model list. A model an old CLI cannot run is a row like any other, so a search for
6
- * only that model still finds the row that explains why it cannot be picked.
28
+ * only that model still finds the row that explains why it cannot be picked. A search looks through
29
+ * every model of every provider; without one, a provider with many models shows a short list and a row
30
+ * that opens the rest, grouped by maker.
7
31
  */
8
- export function pickerEntriesOf(providers, query, legacyOpen) {
9
- const rows = providers.flatMap((entry) => entry.models.map((model) => ({ kind: 'model', provider: entry, model })));
32
+ export function pickerEntriesOf(providers, query, legacyOpen, expanded = NONE, current = null) {
33
+ const rowsOf = (entry, models) => byVendor(models).map((model) => ({ kind: 'model', provider: entry, model }));
10
34
  if (query !== '') {
11
- return rows.filter((row) => matches(row.model, query));
35
+ return providers.flatMap((entry) => rowsOf(entry, entry.models.filter((model) => matches(model, query))));
12
36
  }
13
- const live = rows.filter((row) => !row.model.legacy);
14
- const legacy = rows.filter((row) => row.model.legacy);
37
+ const legacy = providers.flatMap((entry) => entry.models.filter((model) => model.legacy).map((model) => ({ kind: 'model', provider: entry, model })));
38
+ const live = providers.flatMap((entry) => {
39
+ const models = entry.models.filter((model) => !model.legacy);
40
+ if (models.length <= COMPACT_MODEL_LIMIT) {
41
+ return rowsOf(entry, models);
42
+ }
43
+ if (expanded.has(entry.kind)) {
44
+ return [{ kind: 'all', provider: entry, count: models.length, open: true }, ...rowsOf(entry, models)];
45
+ }
46
+ const shortlist = models.filter((model) => model.isDefault || (current?.provider === entry.kind && current.model === model.slug));
47
+ return [...rowsOf(entry, shortlist), { kind: 'all', provider: entry, count: models.length, open: false }];
48
+ });
15
49
  if (legacy.length === 0) {
16
50
  return live;
17
51
  }
@@ -24,6 +24,7 @@ export function ModelPicker({ providers, provider, selection, open, onOpenChange
24
24
  const { t } = useTranslation('agent-chat');
25
25
  const [query, setQuery] = useState('');
26
26
  const [legacyOpen, setLegacyOpen] = useState(false);
27
+ const [expanded, setExpanded] = useState(new Set());
27
28
  const [index, setIndex] = useState(0);
28
29
  const inputRef = useRef(null);
29
30
  const listRef = useRef(null);
@@ -32,7 +33,7 @@ export function ModelPicker({ providers, provider, selection, open, onOpenChange
32
33
  const chip = agentChipOf(providers, provider, selection.model);
33
34
  const grouped = providers.length > 1;
34
35
  const trimmed = query.trim().toLowerCase();
35
- const entries = useMemo(() => pickerEntriesOf(providers, trimmed, legacyOpen), [providers, trimmed, legacyOpen]);
36
+ const entries = useMemo(() => pickerEntriesOf(providers, trimmed, legacyOpen, expanded, { provider, model: selection.model }), [providers, trimmed, legacyOpen, expanded, provider, selection.model]);
36
37
  const scope = useContext(ChatScopeContext);
37
38
  const machine = host.useMachineLabel(scope?.id ?? '');
38
39
  const reported = useReportedModels(provider);
@@ -44,6 +45,7 @@ export function ModelPicker({ providers, provider, selection, open, onOpenChange
44
45
  if (!next) {
45
46
  setQuery('');
46
47
  setLegacyOpen(false);
48
+ setExpanded(new Set());
47
49
  }
48
50
  setIndex(0);
49
51
  onOpenChange(next);
@@ -61,6 +63,18 @@ export function ModelPicker({ providers, provider, selection, open, onOpenChange
61
63
  inputRef.current?.focus();
62
64
  return;
63
65
  }
66
+ if (entry.kind === 'all') {
67
+ const kind = entry.provider.kind;
68
+ setExpanded((now) => {
69
+ const next = new Set(now);
70
+ if (!next.delete(kind)) {
71
+ next.add(kind);
72
+ }
73
+ return next;
74
+ });
75
+ inputRef.current?.focus();
76
+ return;
77
+ }
64
78
  if (requirementOf(entry) !== null) {
65
79
  return;
66
80
  }
@@ -85,16 +99,30 @@ export function ModelPicker({ providers, provider, selection, open, onOpenChange
85
99
  setQuery(e.target.value);
86
100
  setIndex(0);
87
101
  }, onKeyDown: onKeyDown })] }), _jsxs("div", { ref: listRef, className: "max-h-72 overflow-auto p-1", role: "listbox", children: [entries.length === 0 && (_jsx("div", { className: "px-3 py-6 text-center text-xs text-text-faint", children: providers.length === 0 ? t('pickers.model.noProvider') : t('pickers.model.noMatch') })), entries.map((entry, i) => {
102
+ if (entry.kind === 'all') {
103
+ const before = entries[i - 1];
104
+ const header = grouped && (before === undefined || before.kind === 'legacy' || before.provider.kind !== entry.provider.kind);
105
+ return (_jsxs("div", { children: [header && (_jsxs(Menu.Label, { className: "flex items-center gap-1.5", children: [_jsx(AgentIcon, { kind: entry.provider.kind, size: 12 }), entry.provider.name] })), _jsxs("button", { "data-active": i === index, "aria-expanded": entry.open, className: "cursor-row flex w-full items-center gap-2 rounded-md px-2.5 py-1.5 text-left text-xs text-text-muted", onMouseEnter: () => setIndex(i), onClick: () => choose(entry), children: [_jsx(Icon, { icon: entry.open ? ChevronDown : ChevronRight, size: 12 }), t('pickers.model.allGroup', { count: entry.count })] })] }, `all-${entry.provider.kind}`));
106
+ }
88
107
  if (entry.kind === 'legacy') {
89
108
  return (_jsxs("button", { "data-active": i === index, className: "cursor-row flex w-full items-center gap-2 rounded-md px-2.5 py-1.5 text-left text-xs text-text-faint", onMouseEnter: () => setIndex(i), onClick: () => choose(entry), children: [_jsx(Icon, { icon: legacyOpen ? ChevronDown : ChevronRight, size: 12 }), t('pickers.model.legacyGroup', { count: entry.count })] }, "legacy"));
90
109
  }
91
110
  const previous = entries[i - 1];
92
- const first = grouped && (previous === undefined || previous.kind !== 'model' || previous.provider.kind !== entry.provider.kind);
111
+ // A provider's rows start where the one before it ended; its row that opens the rest belongs to it.
112
+ const first = grouped && (previous === undefined || previous.kind === 'legacy' || previous.provider.kind !== entry.provider.kind);
113
+ // The makers read as groups only where the whole list is shown, so a short list is not cut by labels.
114
+ const vendorOpen = trimmed !== '' || expanded.has(entry.provider.kind);
115
+ const vendorFirst = vendorOpen &&
116
+ entry.model.vendor !== undefined &&
117
+ (previous === undefined ||
118
+ previous.kind !== 'model' ||
119
+ previous.provider.kind !== entry.provider.kind ||
120
+ previous.model.vendor !== entry.model.vendor);
93
121
  const requirement = requirementOf(entry);
94
122
  const next = entries[i + 1];
95
123
  const lastOfGroup = next === undefined || next.kind !== 'model' || next.provider.kind !== entry.provider.kind;
96
124
  const chosen = entry.provider.kind === provider && entry.model.slug === selection.model;
97
- 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] })), _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 }), entry.model.badge && (_jsx(Pill, { tone: "accent", shape: "tag", size: "sm", children: entry.model.badge.charAt(0).toLocaleUpperCase() + entry.model.badge.slice(1) })), _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
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 }), entry.model.badge && (_jsx(Pill, { tone: "accent", shape: "tag", size: "sm", children: entry.model.badge.charAt(0).toLocaleUpperCase() + entry.model.badge.slice(1) })), _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
98
126
  ? t('pickers.model.requiresHere', { cli: requirement.cli, version: requirement.minVersion })
99
127
  : 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: () => {
100
128
  setOpen(false);
@@ -4,12 +4,13 @@ import { isBlock } from '../../logic/timeline.js';
4
4
  export const FOLLOW_THRESHOLD_PX = 40;
5
5
  /*
6
6
  * The gaps a row wears for where it sits: a question is followed by its answer gap, and a seam where
7
- * the list rhythm of tool lines meets the block rhythm of prose gets the block gap. A question
7
+ * the list rhythm of tool lines meets the block rhythm of prose gets the block gap. A visual has no
8
+ * margin of its own like prose does, so the row under one takes the block gap too. A question
8
9
  * already carries the gap before it, and the row after one is the answer, so neither takes a seam.
9
10
  */
10
11
  export function rowRhythm(row, previous) {
11
12
  const question = row.kind === 'user';
12
- const seam = !question && previous !== null && previous.kind !== 'user' && isBlock(row) !== isBlock(previous);
13
+ const seam = !question && previous !== null && previous.kind !== 'user' && (isBlock(row) !== isBlock(previous) || previous.kind === 'visual');
13
14
  return clsx(question && 'pb-(--chat-answer-gap)', seam && 'pt-(--chat-block-gap)');
14
15
  }
15
16
  /*
package/dist/index.d.ts CHANGED
@@ -2,6 +2,7 @@ export { type ChatActions, type FileRef, type ShellCodeBlockContext, type ChatPl
2
2
  export { type ChatScope, ChatScopeContext, useChatScope } from './scope.js';
3
3
  export { type ChatRequestMap, type ChatEventMap, type ChatTransportStatus, type ChatTransport, ChatTransportError, errorCode, isConnectionError } from './transport.js';
4
4
  export { portTransport, type PortTransport } from './port-transport.js';
5
+ export { configureAgentMarks, type AgentMark } from './agents/agent-marks.js';
5
6
  export { configureChatStorage, type ChatStorageRecord, type ChatStorageAdapter, type ChatStorageOptions } from './storage.js';
6
7
  export { setLazyPrefetch, onLazyOpenError } from './lazy.js';
7
8
  export { type MountedEntry, MountedRegistry } from './mounted-registry.js';
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@ export { ChatHostProvider, useChatHost } from './host.js';
3
3
  export { ChatScopeContext, useChatScope } from './scope.js';
4
4
  export { ChatTransportError, errorCode, isConnectionError } from './transport.js';
5
5
  export { portTransport } from './port-transport.js';
6
+ export { configureAgentMarks } from './agents/agent-marks.js';
6
7
  export { configureChatStorage } from './storage.js';
7
8
  export { setLazyPrefetch, onLazyOpenError } from './lazy.js';
8
9
  export { MountedRegistry } from './mounted-registry.js';
@@ -434,6 +434,8 @@ declare const _default: {
434
434
  search: string;
435
435
  noProvider: string;
436
436
  noMatch: string;
437
+ allGroup_one: string;
438
+ allGroup_other: string;
437
439
  legacyGroup_one: string;
438
440
  legacyGroup_other: string;
439
441
  legacy: string;
@@ -434,6 +434,8 @@ export default {
434
434
  search: 'Search models',
435
435
  noProvider: 'No chat provider installed',
436
436
  noMatch: 'No models match',
437
+ allGroup_one: 'Models ({{count}})',
438
+ allGroup_other: 'Models ({{count}})',
437
439
  legacyGroup_one: 'Legacy models ({{count}})',
438
440
  legacyGroup_other: 'Legacy models ({{count}})',
439
441
  legacy: 'Legacy',
@@ -66,6 +66,8 @@ declare const _default: {
66
66
  signedOutDetail: string;
67
67
  logIn: string;
68
68
  logInAgain: string;
69
+ inChat: string;
70
+ inChatDetail: string;
69
71
  };
70
72
  folder: {
71
73
  label: string;
@@ -106,6 +108,7 @@ declare const _default: {
106
108
  add: {
107
109
  title: string;
108
110
  description: string;
111
+ chatDescription: string;
109
112
  name: string;
110
113
  namePlaceholder: string;
111
114
  color: string;
@@ -65,7 +65,9 @@ export default {
65
65
  signedOut: 'Not logged in',
66
66
  signedOutDetail: "Opens a terminal that runs the CLI's login with this folder.",
67
67
  logIn: 'Log in',
68
- logInAgain: 'Log in again'
68
+ logInAgain: 'Log in again',
69
+ inChat: 'Logs in from a chat',
70
+ inChatDetail: 'The first chat on this account asks you to log in, and the CLI keeps that login in this folder.'
69
71
  },
70
72
  folder: {
71
73
  label: 'Folder',
@@ -106,6 +108,7 @@ export default {
106
108
  add: {
107
109
  title: 'Add a {{provider}} account',
108
110
  description: 'Ruimte makes a folder for it on the machine, then opens a terminal where you log in.',
111
+ chatDescription: 'A folder is made for it on the machine. The first chat on it asks you to log in.',
109
112
  name: 'Name',
110
113
  namePlaceholder: 'Work',
111
114
  color: 'Color',
@@ -434,6 +434,8 @@ declare const _default: {
434
434
  search: string;
435
435
  noProvider: string;
436
436
  noMatch: string;
437
+ allGroup_one: string;
438
+ allGroup_other: string;
437
439
  legacyGroup_one: string;
438
440
  legacyGroup_other: string;
439
441
  legacy: string;
@@ -434,6 +434,8 @@ export default {
434
434
  search: 'Modellen zoeken',
435
435
  noProvider: 'Geen chat-CLI geïnstalleerd',
436
436
  noMatch: 'Geen modellen gevonden',
437
+ allGroup_one: 'Modellen ({{count}})',
438
+ allGroup_other: 'Modellen ({{count}})',
437
439
  legacyGroup_one: 'Legacy-modellen ({{count}})',
438
440
  legacyGroup_other: 'Legacy-modellen ({{count}})',
439
441
  legacy: 'Legacy',
@@ -66,6 +66,8 @@ declare const _default: {
66
66
  signedOutDetail: string;
67
67
  logIn: string;
68
68
  logInAgain: string;
69
+ inChat: string;
70
+ inChatDetail: string;
69
71
  };
70
72
  folder: {
71
73
  label: string;
@@ -106,6 +108,7 @@ declare const _default: {
106
108
  add: {
107
109
  title: string;
108
110
  description: string;
111
+ chatDescription: string;
109
112
  name: string;
110
113
  namePlaceholder: string;
111
114
  color: string;
@@ -65,7 +65,9 @@ export default {
65
65
  signedOut: 'Niet ingelogd',
66
66
  signedOutDetail: 'Opent een terminal die de login van de CLI met deze map draait.',
67
67
  logIn: 'Inloggen',
68
- logInAgain: 'Opnieuw inloggen'
68
+ logInAgain: 'Opnieuw inloggen',
69
+ inChat: 'Logt in vanuit een chat',
70
+ inChatDetail: 'De eerste chat op dit account vraagt je in te loggen, en de CLI bewaart die login in deze map.'
69
71
  },
70
72
  folder: {
71
73
  label: 'Map',
@@ -106,6 +108,7 @@ export default {
106
108
  add: {
107
109
  title: 'Een {{provider}}-account toevoegen',
108
110
  description: 'Ruimte maakt er een map voor op de machine en opent dan een terminal waarin je inlogt.',
111
+ chatDescription: 'Er komt een map voor op de machine. De eerste chat erop vraagt je in te loggen.',
109
112
  name: 'Naam',
110
113
  namePlaceholder: 'Werk',
111
114
  color: 'Kleur',
@@ -1,12 +1,13 @@
1
1
  import type { ProviderInfo } from '@adecore/agent-contracts';
2
- import { type AccountEntry } from '../agents/accounts.js';
2
+ import { type AccountEntry, type AccountLogin } from '../agents/accounts.js';
3
3
  interface AccountDetailProps {
4
4
  provider: ProviderInfo;
5
5
  entry: AccountEntry;
6
- canLogIn: boolean;
6
+ login: AccountLogin;
7
+ canOpenLogin: boolean;
7
8
  loginBlocked: string | null;
8
9
  secretsAvailable: boolean;
9
10
  onRemoved(): void;
10
11
  }
11
- export declare function AccountDetail({ provider, entry, canLogIn, loginBlocked, secretsAvailable, onRemoved }: AccountDetailProps): import("react").JSX.Element;
12
+ export declare function AccountDetail({ provider, entry, login, canOpenLogin, loginBlocked, secretsAvailable, onRemoved }: AccountDetailProps): import("react").JSX.Element;
12
13
  export {};
@@ -32,7 +32,7 @@ function NameField({ value, label, onSave }) {
32
32
  } }));
33
33
  }
34
34
  /* One account: how it looks in a picker, who is signed in, where its folder is, its variables. */
35
- export function AccountDetail({ provider, entry, canLogIn, loginBlocked, secretsAvailable, onRemoved }) {
35
+ export function AccountDetail({ provider, entry, login, canOpenLogin, loginBlocked, secretsAvailable, onRemoved }) {
36
36
  const { t } = useTranslation('agent-providers');
37
37
  const scope = useChatScope();
38
38
  const host = useChatHost();
@@ -46,9 +46,10 @@ export function AccountDetail({ provider, entry, canLogIn, loginBlocked, secrets
46
46
  copyText(folder);
47
47
  host.notify({ kind: 'success', title: t('account.folderCopied'), description: folder });
48
48
  };
49
+ const trouble = status !== null && TROUBLE.has(status.state) && status.message ? status.message : null;
49
50
  const loginDetail = () => {
50
- if (status !== null && TROUBLE.has(status.state) && status.message) {
51
- return status.message;
51
+ if (trouble !== null) {
52
+ return trouble;
52
53
  }
53
54
  if (!signedIn) {
54
55
  return t('account.login.signedOutDetail');
@@ -59,11 +60,11 @@ export function AccountDetail({ provider, entry, canLogIn, loginBlocked, secrets
59
60
  return (_jsxs(_Fragment, { children: [_jsx(DetailHeader, { mark: _jsx("span", { className: "grid h-6 shrink-0 place-items-center", children: _jsx(AccountDot, { color: account.color, className: "size-3" }) }), title: name, subtitle: _jsxs(_Fragment, { children: [_jsx(CliMark, { kind: provider.kind, size: 12, className: "text-text-muted" }), t('account.kind', { provider: provider.name })] }), actions: folder !== '' && (_jsx(Button, { variant: "secondary", onClick: showFolder, children: t('account.showFolder') })) }), _jsxs(SettingsSection, { children: [_jsx(SettingsRow, { label: t('account.enabled.label'), description: t('account.enabled.description'), control: _jsx(Switch, { checked: account.enabled !== false, label: t('account.enabled.label'), onCheckedChange: (checked) => {
60
61
  const { enabled: _enabled, ...rest } = account;
61
62
  void saveAccount(scope, host, id, checked ? rest : { ...rest, enabled: false });
62
- } }) }), _jsx(SettingsRow, { label: t('account.name'), control: _jsx(NameField, { value: name, label: t('account.name'), onSave: (label) => void saveAccount(scope, host, id, { ...account, label }) }, name) }), _jsx(SettingsRow, { label: t('account.color'), control: _jsx(AccountColors, { value: account.color, label: t('account.color'), onChange: (color) => void saveAccount(scope, host, id, { ...account, color }) }) })] }), canLogIn && (_jsxs(SettingsSection, { title: t('account.login.title'), children: [_jsx(SettingsRow, { label: signedIn
63
+ } }) }), _jsx(SettingsRow, { label: t('account.name'), control: _jsx(NameField, { value: name, label: t('account.name'), onSave: (label) => void saveAccount(scope, host, id, { ...account, label }) }, name) }), _jsx(SettingsRow, { label: t('account.color'), control: _jsx(AccountColors, { value: account.color, label: t('account.color'), onChange: (color) => void saveAccount(scope, host, id, { ...account, color }) }) })] }), (login === 'chat' || (login === 'terminal' && canOpenLogin)) && (_jsxs(SettingsSection, { title: t('account.login.title'), children: [login === 'chat' ? (_jsx(SettingsRow, { label: t('account.login.inChat'), description: trouble ?? t('account.login.inChatDetail') })) : (_jsx(SettingsRow, { label: signedIn
63
64
  ? status?.email
64
65
  ? t('account.login.signedInAs', { email: status.email })
65
66
  : t('account.login.signedIn')
66
- : t('account.login.signedOut'), description: loginBlocked === null ? loginDetail() : `${loginDetail()} ${loginBlocked}`, control: loginBlocked === null ? (loginButton) : (_jsx(Tooltip, { label: loginBlocked, children: _jsx("span", { className: "inline-flex", children: loginButton }) })) }), _jsxs(SettingsRow, { label: t('account.folder.label'), children: [_jsx(Input, { mono: true, className: "h-8.5", value: folder, readOnly: true, "aria-label": t('account.folder.label') }), _jsx("p", { className: "-mt-1 text-xs text-text-muted", children: variable ? (_jsx(Trans, { t: t, i18nKey: "account.folder.detail", values: { variable }, components: { code: _jsx("code", { className: "font-mono text-code" }) } })) : (t('account.folder.detailPlain')) })] })] })), _jsx(AccountVariables, { id: id, account: account, secretsAvailable: secretsAvailable }, `${id}:${JSON.stringify(account.env ?? [])}`), _jsx(SettingsSection, { children: _jsx(SettingsRow, { label: t('account.remove.label'), description: isDefault ? t('account.remove.defaultNote') : t('account.remove.description'), control: _jsx(Button, { variant: "danger-outline", disabled: isDefault, onClick: () => setConfirming(true), children: t('account.remove.button') }) }) }), _jsx(ConfirmDialog, { open: confirming, onOpenChange: setConfirming, title: t('account.remove.confirmTitle', { account: name }), description: t('account.remove.confirmDescription', { folder }), confirmLabel: t('account.remove.confirm'), onConfirm: async () => {
67
+ : t('account.login.signedOut'), description: loginBlocked === null ? loginDetail() : `${loginDetail()} ${loginBlocked}`, control: loginBlocked === null ? (loginButton) : (_jsx(Tooltip, { label: loginBlocked, children: _jsx("span", { className: "inline-flex", children: loginButton }) })) })), _jsxs(SettingsRow, { label: t('account.folder.label'), children: [_jsx(Input, { mono: true, className: "h-8.5", value: folder, readOnly: true, "aria-label": t('account.folder.label') }), _jsx("p", { className: "-mt-1 text-xs text-text-muted", children: variable ? (_jsx(Trans, { t: t, i18nKey: "account.folder.detail", values: { variable }, components: { code: _jsx("code", { className: "font-mono text-code" }) } })) : (t('account.folder.detailPlain')) })] })] })), _jsx(AccountVariables, { id: id, account: account, secretsAvailable: secretsAvailable }, `${id}:${JSON.stringify(account.env ?? [])}`), _jsx(SettingsSection, { children: _jsx(SettingsRow, { label: t('account.remove.label'), description: isDefault ? t('account.remove.defaultNote') : t('account.remove.description'), control: _jsx(Button, { variant: "danger-outline", disabled: isDefault, onClick: () => setConfirming(true), children: t('account.remove.button') }) }) }), _jsx(ConfirmDialog, { open: confirming, onOpenChange: setConfirming, title: t('account.remove.confirmTitle', { account: name }), description: t('account.remove.confirmDescription', { folder }), confirmLabel: t('account.remove.confirm'), onConfirm: async () => {
67
68
  await removeAccount(scope, host, id, provider.kind);
68
69
  onRemoved();
69
70
  } })] }));
@@ -1,10 +1,11 @@
1
1
  import type { ProviderInfo } from '@adecore/agent-contracts';
2
- import { type AccountEntry } from '../agents/accounts.js';
2
+ import { type AccountEntry, type AccountLogin } from '../agents/accounts.js';
3
3
  interface AddAccountFormProps {
4
4
  provider: ProviderInfo;
5
5
  entries: AccountEntry[];
6
+ login: AccountLogin;
6
7
  onCancel(): void;
7
8
  onAdded(id: string, name: string, made: boolean): void;
8
9
  }
9
- export declare function AddAccountForm({ provider, entries, onCancel, onAdded }: AddAccountFormProps): import("react").JSX.Element;
10
+ export declare function AddAccountForm({ provider, entries, login, onCancel, onAdded }: AddAccountFormProps): import("react").JSX.Element;
10
11
  export {};
@@ -16,7 +16,7 @@ import { providerAccountsOf } from '../state/provider-accounts.js';
16
16
  * A name and a color, asked before anything exists, so the account is made once with both. By default
17
17
  * the machine makes the folder; under Advanced a person points at a folder of their own instead.
18
18
  */
19
- export function AddAccountForm({ provider, entries, onCancel, onAdded }) {
19
+ export function AddAccountForm({ provider, entries, login, onCancel, onAdded }) {
20
20
  const { t } = useTranslation('agent-providers');
21
21
  const scope = useChatScope();
22
22
  const { accents } = useChatHost();
@@ -51,5 +51,5 @@ export function AddAccountForm({ provider, entries, onCancel, onAdded }) {
51
51
  setBusy(false);
52
52
  }
53
53
  };
54
- return (_jsxs("form", { className: "contents", onSubmit: (event) => void submit(event), children: [_jsx(DetailHeader, { mark: _jsx(CliTile, { kind: provider.kind }), title: t('add.title', { provider: provider.name }), subtitle: t('add.description') }), _jsxs(SettingsSection, { children: [_jsx(SettingsRow, { label: t('add.name'), control: _jsx(Input, { className: "w-55 max-w-full", value: name, maxLength: 80, autoFocus: true, placeholder: t('add.namePlaceholder'), "aria-label": t('add.name'), onChange: (event) => setName(event.target.value) }) }), _jsx(SettingsRow, { label: t('add.color'), control: _jsx(AccountColors, { value: color, label: t('add.color'), onChange: setColor }) })] }), _jsxs("div", { className: "flex min-w-0 flex-col gap-2.5", children: [_jsxs("button", { type: "button", "aria-expanded": advanced, className: "flex items-center gap-1.5 self-start text-sm font-medium text-text-muted hover:text-text", onClick: () => setAdvanced((open) => !open), children: [_jsx(Icon, { icon: ChevronRight, size: 14, className: clsx('transition-transform', advanced && 'rotate-90') }), t('add.advanced')] }), advanced && (_jsx(SettingsSection, { children: _jsx(SettingsRow, { label: t('add.link.label'), description: t('add.link.description'), children: _jsx(Input, { mono: true, value: folder, spellCheck: false, autoComplete: "off", placeholder: t('add.link.placeholder'), "aria-label": t('add.link.label'), onChange: (event) => setFolder(event.target.value) }) }) }))] }), _jsxs("div", { className: "flex min-w-0 flex-wrap items-center justify-end gap-2", children: [error !== null && (_jsx(FormError, { render: _jsx("span", {}), className: "mr-auto break-words", children: error || t('add.failed') })), _jsx(Button, { onClick: onCancel, children: t('common.cancel') }), _jsx(Button, { type: "submit", variant: "primary", disabled: label === '' || busy, children: linking ? t('add.linkButton') : t('add.create') })] })] }));
54
+ return (_jsxs("form", { className: "contents", onSubmit: (event) => void submit(event), children: [_jsx(DetailHeader, { mark: _jsx(CliTile, { kind: provider.kind }), title: t('add.title', { provider: provider.name }), subtitle: login === 'chat' ? t('add.chatDescription') : t('add.description') }), _jsxs(SettingsSection, { children: [_jsx(SettingsRow, { label: t('add.name'), control: _jsx(Input, { className: "w-55 max-w-full", value: name, maxLength: 80, autoFocus: true, placeholder: t('add.namePlaceholder'), "aria-label": t('add.name'), onChange: (event) => setName(event.target.value) }) }), _jsx(SettingsRow, { label: t('add.color'), control: _jsx(AccountColors, { value: color, label: t('add.color'), onChange: setColor }) })] }), _jsxs("div", { className: "flex min-w-0 flex-col gap-2.5", children: [_jsxs("button", { type: "button", "aria-expanded": advanced, className: "flex items-center gap-1.5 self-start text-sm font-medium text-text-muted hover:text-text", onClick: () => setAdvanced((open) => !open), children: [_jsx(Icon, { icon: ChevronRight, size: 14, className: clsx('transition-transform', advanced && 'rotate-90') }), t('add.advanced')] }), advanced && (_jsx(SettingsSection, { children: _jsx(SettingsRow, { label: t('add.link.label'), description: t('add.link.description'), children: _jsx(Input, { mono: true, value: folder, spellCheck: false, autoComplete: "off", placeholder: t('add.link.placeholder'), "aria-label": t('add.link.label'), onChange: (event) => setFolder(event.target.value) }) }) }))] }), _jsxs("div", { className: "flex min-w-0 flex-wrap items-center justify-end gap-2", children: [error !== null && (_jsx(FormError, { render: _jsx("span", {}), className: "mr-auto break-words", children: error || t('add.failed') })), _jsx(Button, { onClick: onCancel, children: t('common.cancel') }), _jsx(Button, { type: "submit", variant: "primary", disabled: label === '' || busy, children: linking ? t('add.linkButton') : t('add.create') })] })] }));
55
55
  }
@@ -1,12 +1,12 @@
1
1
  import type { ProviderInfo } from '@adecore/agent-contracts';
2
- import { type AccountEntry } from '../agents/accounts.js';
2
+ import { type AccountEntry, type AccountLogin } from '../agents/accounts.js';
3
3
  interface CliDetailProps {
4
4
  provider: ProviderInfo;
5
5
  entries: AccountEntry[] | null;
6
6
  defaults: boolean;
7
- canAddAccount: boolean;
7
+ login: AccountLogin;
8
8
  onAddAccount(): void;
9
9
  onPickAccount(id: string): void;
10
10
  }
11
- export declare function CliDetail({ provider, entries, defaults, canAddAccount, onAddAccount, onPickAccount }: CliDetailProps): import("react").JSX.Element;
11
+ export declare function CliDetail({ provider, entries, defaults, login, onAddAccount, onPickAccount }: CliDetailProps): import("react").JSX.Element;
12
12
  export {};
@@ -21,7 +21,7 @@ function ModelOptionRows({ provider, model, options }) {
21
21
  return (_jsx(_Fragment, { children: model.options.map((option) => (_jsx(SettingsRow, { indent: true, muted: true, label: option.label, control: _jsx(ModelOptionControl, { option: option, options: options, layout: "row", onChange: (value) => setOption(option.id, value) }) }, option.id))) }));
22
22
  }
23
23
  /* One CLI: what it is, what a new chat of it starts with unless `defaults` is off, and its accounts. */
24
- export function CliDetail({ provider, entries, defaults, canAddAccount, onAddAccount, onPickAccount }) {
24
+ export function CliDetail({ provider, entries, defaults, login, onAddAccount, onPickAccount }) {
25
25
  const { t } = useTranslation('agent-providers');
26
26
  const { id: scopeId } = useChatScope();
27
27
  const preferences = useChatPreferences();
@@ -31,7 +31,7 @@ export function CliDetail({ provider, entries, defaults, canAddAccount, onAddAcc
31
31
  const picked = accountFor(preferences, scopeId, provider.kind, machineAccounts) ?? provider.kind;
32
32
  // An account that is off is out of every picker, unless it is the one picked already.
33
33
  const choosable = (entries ?? []).filter((entry) => entry.account.enabled !== false || entry.id === picked);
34
- const canLogIn = canAddAccount;
34
+ const canAddAccount = login !== null;
35
35
  return (_jsxs(_Fragment, { children: [_jsx(DetailHeader, { mark: _jsx(CliTile, { kind: provider.kind }), title: provider.name, subtitle: `${provider.version ? `${t('abilities.version', { version: provider.version })} ` : ''}${providerAbilities(provider)}`, actions: canAddAccount && (_jsxs(Button, { variant: "secondary", className: "whitespace-nowrap", onClick: onAddAccount, children: [_jsx(Icon, { icon: Plus, size: 14 }), t('cli.addAccount')] })) }), defaults && (_jsxs(SettingsSection, { title: t('cli.defaults.title'), description: t('cli.defaults.description'), children: [choosable.length > 0 && (_jsx(SettingsRow, { searchId: "providers.defaults.account", label: t('cli.defaults.account'), control: _jsx(Select, { value: picked, label: t('cli.defaults.accountFor', { provider: provider.name }), align: "end", items: choosable.map((entry) => ({
36
36
  value: entry.id,
37
37
  label: accountName(entry, provider.name),
@@ -48,7 +48,7 @@ export function CliDetail({ provider, entries, defaults, canAddAccount, onAddAcc
48
48
  model: value,
49
49
  options: carryOptions(selection?.options ?? {}, provider.models.find((entry) => entry.slug === value))
50
50
  }) }) })), model && _jsx(ModelOptionRows, { provider: provider.kind, model: model, options: selection?.options ?? {} }), choosable.length === 0 && provider.models.length === 0 && _jsx(SettingsRow, { muted: true, label: t('cli.defaults.usesCliDefault') })] })), entries === null ? (_jsx("p", { className: "text-xs text-text-faint", children: t('list.noAccounts') })) : (_jsx(SettingsSection, { title: t('cli.accounts'), footer: canAddAccount ? undefined : t('cli.onlyDefault', { provider: provider.name }), children: entries.map((entry) => {
51
- const status = accountStatusLine(entry.status, canLogIn);
51
+ const status = accountStatusLine(entry.status, login === 'terminal');
52
52
  return (_jsxs("button", { type: "button", className: "flex w-full min-w-0 items-center gap-3 px-4.5 py-3 text-left first:rounded-t-xl last:rounded-b-xl hover:bg-surface-hover", onClick: () => onPickAccount(entry.id), children: [_jsx(AccountDot, { color: entry.account.color }), _jsx("span", { className: "min-w-0 grow truncate text-sm text-text", children: accountName(entry, provider.name) }), _jsx("span", { className: `shrink-0 text-xs ${ACCOUNT_TONE_CLASSES[status.tone]}`, children: status.text }), _jsx(Icon, { icon: ChevronRight, size: 14, className: "shrink-0 text-text-faint" })] }, entry.id));
53
53
  }) }))] }));
54
54
  }