@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,7 +3,7 @@ import { Trans, useTranslation } from 'react-i18next';
3
3
  import { LogIn } from 'lucide-react';
4
4
  import type { ProviderInfo } from '@adecore/agent-contracts';
5
5
  import { AccountDot } from '../agents/AccountDot';
6
- import { accountName, FOLDER_VARIABLES, type AccountEntry } from '../agents/accounts';
6
+ import { accountName, FOLDER_VARIABLES, type AccountEntry, type AccountLogin } from '../agents/accounts';
7
7
  import { ConfirmDialog, DetailHeader, SettingsRow, SettingsSection } from '@adecore/ui/settings';
8
8
  import { Button, copyText, Icon, Input, Switch, Tooltip } from '@adecore/ui';
9
9
  import { useChatHost } from '../host';
@@ -19,8 +19,10 @@ const TROUBLE = new Set(['folder-missing', 'unavailable', 'failed']);
19
19
  interface AccountDetailProps {
20
20
  provider: ProviderInfo;
21
21
  entry: AccountEntry;
22
- /* False for a CLI without a config folder: it has only its own login, and nothing to sign in here. */
23
- canLogIn: boolean;
22
+ /* Null for a CLI without a config folder: it has only its own login, and nothing to sign in here. */
23
+ login: AccountLogin;
24
+ /* Whether the host can open a terminal for a login at all. */
25
+ canOpenLogin: boolean;
24
26
  /* Why a login cannot open a terminal right now, or null when it can. */
25
27
  loginBlocked: string | null;
26
28
  secretsAvailable: boolean;
@@ -56,7 +58,7 @@ function NameField({ value, label, onSave }: { value: string; label: string; onS
56
58
  }
57
59
 
58
60
  /* One account: how it looks in a picker, who is signed in, where its folder is, its variables. */
59
- export function AccountDetail({ provider, entry, canLogIn, loginBlocked, secretsAvailable, onRemoved }: AccountDetailProps) {
61
+ export function AccountDetail({ provider, entry, login, canOpenLogin, loginBlocked, secretsAvailable, onRemoved }: AccountDetailProps) {
60
62
  const { t } = useTranslation('agent-providers');
61
63
  const scope = useChatScope();
62
64
  const host = useChatHost();
@@ -72,9 +74,11 @@ export function AccountDetail({ provider, entry, canLogIn, loginBlocked, secrets
72
74
  host.notify({ kind: 'success', title: t('account.folderCopied'), description: folder });
73
75
  };
74
76
 
77
+ const trouble = status !== null && TROUBLE.has(status.state) && status.message ? status.message : null;
78
+
75
79
  const loginDetail = (): string => {
76
- if (status !== null && TROUBLE.has(status.state) && status.message) {
77
- return status.message;
80
+ if (trouble !== null) {
81
+ return trouble;
78
82
  }
79
83
  if (!signedIn) {
80
84
  return t('account.login.signedOutDetail');
@@ -149,27 +153,31 @@ export function AccountDetail({ provider, entry, canLogIn, loginBlocked, secrets
149
153
  }
150
154
  />
151
155
  </SettingsSection>
152
- {canLogIn && (
156
+ {(login === 'chat' || (login === 'terminal' && canOpenLogin)) && (
153
157
  <SettingsSection title={t('account.login.title')}>
154
- <SettingsRow
155
- label={
156
- signedIn
157
- ? status?.email
158
- ? t('account.login.signedInAs', { email: status.email })
159
- : t('account.login.signedIn')
160
- : t('account.login.signedOut')
161
- }
162
- description={loginBlocked === null ? loginDetail() : `${loginDetail()} ${loginBlocked}`}
163
- control={
164
- loginBlocked === null ? (
165
- loginButton
166
- ) : (
167
- <Tooltip label={loginBlocked}>
168
- <span className="inline-flex">{loginButton}</span>
169
- </Tooltip>
170
- )
171
- }
172
- />
158
+ {login === 'chat' ? (
159
+ <SettingsRow label={t('account.login.inChat')} description={trouble ?? t('account.login.inChatDetail')} />
160
+ ) : (
161
+ <SettingsRow
162
+ label={
163
+ signedIn
164
+ ? status?.email
165
+ ? t('account.login.signedInAs', { email: status.email })
166
+ : t('account.login.signedIn')
167
+ : t('account.login.signedOut')
168
+ }
169
+ description={loginBlocked === null ? loginDetail() : `${loginDetail()} ${loginBlocked}`}
170
+ control={
171
+ loginBlocked === null ? (
172
+ loginButton
173
+ ) : (
174
+ <Tooltip label={loginBlocked}>
175
+ <span className="inline-flex">{loginButton}</span>
176
+ </Tooltip>
177
+ )
178
+ }
179
+ />
180
+ )}
173
181
  <SettingsRow label={t('account.folder.label')}>
174
182
  <Input mono className="h-8.5" value={folder} readOnly aria-label={t('account.folder.label')} />
175
183
  <p className="-mt-1 text-xs text-text-muted">
@@ -3,7 +3,7 @@ import clsx from 'clsx';
3
3
  import { useTranslation } from 'react-i18next';
4
4
  import { ChevronRight } from 'lucide-react';
5
5
  import type { ProviderInfo } from '@adecore/agent-contracts';
6
- import { freeAccountColor, mintAccountId, type AccountEntry } from '../agents/accounts';
6
+ import { freeAccountColor, mintAccountId, type AccountEntry, type AccountLogin } from '../agents/accounts';
7
7
  import { useChatHost } from '../host';
8
8
  import { useChatScope } from '../scope';
9
9
  import { createAccount, linkAccount } from './account-actions';
@@ -16,6 +16,7 @@ import { providerAccountsOf } from '../state/provider-accounts';
16
16
  interface AddAccountFormProps {
17
17
  provider: ProviderInfo;
18
18
  entries: AccountEntry[];
19
+ login: AccountLogin;
19
20
  onCancel(): void;
20
21
  /* The account exists; `made` says the machine made its folder, so nobody signed in there yet. */
21
22
  onAdded(id: string, name: string, made: boolean): void;
@@ -25,7 +26,7 @@ interface AddAccountFormProps {
25
26
  * A name and a color, asked before anything exists, so the account is made once with both. By default
26
27
  * the machine makes the folder; under Advanced a person points at a folder of their own instead.
27
28
  */
28
- export function AddAccountForm({ provider, entries, onCancel, onAdded }: AddAccountFormProps) {
29
+ export function AddAccountForm({ provider, entries, login, onCancel, onAdded }: AddAccountFormProps) {
29
30
  const { t } = useTranslation('agent-providers');
30
31
  const scope = useChatScope();
31
32
  const { accents } = useChatHost();
@@ -62,7 +63,11 @@ export function AddAccountForm({ provider, entries, onCancel, onAdded }: AddAcco
62
63
 
63
64
  return (
64
65
  <form className="contents" onSubmit={(event) => void submit(event)}>
65
- <DetailHeader mark={<CliTile kind={provider.kind} />} title={t('add.title', { provider: provider.name })} subtitle={t('add.description')} />
66
+ <DetailHeader
67
+ mark={<CliTile kind={provider.kind} />}
68
+ title={t('add.title', { provider: provider.name })}
69
+ subtitle={login === 'chat' ? t('add.chatDescription') : t('add.description')}
70
+ />
66
71
  <SettingsSection>
67
72
  <SettingsRow
68
73
  label={t('add.name')}
@@ -4,7 +4,7 @@ import type { AgentKind, ModelInfo, ProviderInfo } from '@adecore/agent-contract
4
4
  import { AccountDot } from '../agents/AccountDot';
5
5
  import { ModelOptionControl } from '../agents/ModelOptionControl';
6
6
  import { carryOptions } from '../agents/model-options';
7
- import { ACCOUNT_TONE_CLASSES, accountName, accountStatusLine, type AccountEntry } from '../agents/accounts';
7
+ import { ACCOUNT_TONE_CLASSES, accountName, accountStatusLine, type AccountEntry, type AccountLogin } from '../agents/accounts';
8
8
  import { accountFor, forgetChatSelection, rememberChatAccount, rememberChatSelection, selectionFor, useChatPreferences } from '../chat/preferences';
9
9
  import { useChatScope } from '../scope';
10
10
  import { providerAbilities } from './provider-abilities';
@@ -40,13 +40,13 @@ interface CliDetailProps {
40
40
  /* Null for a machine that keeps no accounts. */
41
41
  entries: AccountEntry[] | null;
42
42
  defaults: boolean;
43
- canAddAccount: boolean;
43
+ login: AccountLogin;
44
44
  onAddAccount(): void;
45
45
  onPickAccount(id: string): void;
46
46
  }
47
47
 
48
48
  /* One CLI: what it is, what a new chat of it starts with unless `defaults` is off, and its accounts. */
49
- export function CliDetail({ provider, entries, defaults, canAddAccount, onAddAccount, onPickAccount }: CliDetailProps) {
49
+ export function CliDetail({ provider, entries, defaults, login, onAddAccount, onPickAccount }: CliDetailProps) {
50
50
  const { t } = useTranslation('agent-providers');
51
51
  const { id: scopeId } = useChatScope();
52
52
  const preferences = useChatPreferences();
@@ -56,7 +56,7 @@ export function CliDetail({ provider, entries, defaults, canAddAccount, onAddAcc
56
56
  const picked = accountFor(preferences, scopeId, provider.kind, machineAccounts) ?? provider.kind;
57
57
  // An account that is off is out of every picker, unless it is the one picked already.
58
58
  const choosable = (entries ?? []).filter((entry) => entry.account.enabled !== false || entry.id === picked);
59
- const canLogIn = canAddAccount;
59
+ const canAddAccount = login !== null;
60
60
 
61
61
  return (
62
62
  <>
@@ -135,7 +135,7 @@ export function CliDetail({ provider, entries, defaults, canAddAccount, onAddAcc
135
135
  ) : (
136
136
  <SettingsSection title={t('cli.accounts')} footer={canAddAccount ? undefined : t('cli.onlyDefault', { provider: provider.name })}>
137
137
  {entries.map((entry) => {
138
- const status = accountStatusLine(entry.status, canLogIn);
138
+ const status = accountStatusLine(entry.status, login === 'terminal');
139
139
  return (
140
140
  <button
141
141
  key={entry.id}
@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
4
4
  import { Plus, Terminal } from 'lucide-react';
5
5
  import type { AgentKind, ProviderInfo } from '@adecore/agent-contracts';
6
6
  import { AccountDot } from '../agents/AccountDot';
7
- import { ACCOUNT_TONE_CLASSES, accountName, accountsOfKind, accountStatusLine, type AccountEntry } from '../agents/accounts';
7
+ import { ACCOUNT_TONE_CLASSES, accountLoginOf, accountName, accountsOfKind, accountStatusLine, type AccountEntry } from '../agents/accounts';
8
8
  import { ErrorBoundary, Icon, IconButton, SectionLabel, Skeleton } from '@adecore/ui';
9
9
  import { MasterDetail } from '@adecore/ui/settings';
10
10
  import { useChatHost } from '../host';
@@ -68,10 +68,11 @@ export function ProvidersPane({ target = null, detailOf, defaults = true }: Prov
68
68
  const loginBlocked = host.useLoginBlocked(scopeId);
69
69
  const [picked, setPicked] = useState<Picked | null>(() => (target === 'providers.apple' ? { kind: 'cli', cli: 'apple' } : null));
70
70
 
71
- const installed = useMemo(() => providers.filter((provider) => provider.installed || provider.kind === 'apple'), [providers]);
72
- const missing = useMemo(() => providers.filter((provider) => !provider.installed && provider.kind !== 'apple'), [providers]);
71
+ // A provider that never runs in a terminal is no CLI to install: it is turned on in its own detail, so it is always listed.
72
+ const installed = useMemo(() => providers.filter((provider) => provider.installed || !provider.capabilities.terminal), [providers]);
73
+ const missing = useMemo(() => providers.filter((provider) => !provider.installed && provider.capabilities.terminal), [providers]);
73
74
  const entriesOf = (kind: AgentKind): AccountEntry[] => accountsOfKind(accounts, kind);
74
- const canAddAccount = (kind: AgentKind): boolean => accounts?.loginCommands?.[kind] !== undefined;
75
+ const loginOf = (kind: AgentKind) => accountLoginOf(accounts, kind);
75
76
 
76
77
  // What was picked may be gone since (an account removed elsewhere); the first CLI stands in for it.
77
78
  const pickedEntry = picked?.kind === 'account' ? (Object.hasOwn(accounts?.accounts ?? {}, picked.id) ? picked.id : null) : null;
@@ -94,7 +95,7 @@ export function ProvidersPane({ target = null, detailOf, defaults = true }: Prov
94
95
 
95
96
  const added = (id: string, name: string, made: boolean): void => {
96
97
  setPicked({ kind: 'account', id });
97
- if (made && provider !== null && openLogin !== null && loginBlocked === null) {
98
+ if (made && provider !== null && loginOf(provider.kind) === 'terminal' && openLogin !== null && loginBlocked === null) {
98
99
  void openLogin(scopeId, provider.kind, id, name);
99
100
  }
100
101
  };
@@ -108,7 +109,7 @@ export function ProvidersPane({ target = null, detailOf, defaults = true }: Prov
108
109
  onSelect={() => setPicked({ kind: 'cli', cli: cli.kind })}
109
110
  className="pr-1"
110
111
  trailing={
111
- canAddAccount(cli.kind) && (
112
+ loginOf(cli.kind) !== null && (
112
113
  <IconButton
113
114
  icon={Plus}
114
115
  size="sm"
@@ -120,11 +121,11 @@ export function ProvidersPane({ target = null, detailOf, defaults = true }: Prov
120
121
  >
121
122
  <CliMark kind={cli.kind} />
122
123
  <span className="min-w-0 grow truncate text-xs font-medium text-text">{cli.name}</span>
123
- {cli.kind !== 'apple' && cli.version && <span className="shrink-0 font-mono text-code text-text-faint">{cli.version}</span>}
124
+ {cli.capabilities.terminal && cli.version && <span className="shrink-0 font-mono text-code text-text-faint">{cli.version}</span>}
124
125
  </ListRow>
125
126
  {entries.map((entry) => {
126
127
  const name = accountName(entry, cli.name);
127
- const status = accountStatusLine(entry.status, canAddAccount(cli.kind));
128
+ const status = accountStatusLine(entry.status, loginOf(cli.kind) === 'terminal');
128
129
  return (
129
130
  <ListRow
130
131
  key={entry.id}
@@ -189,6 +190,7 @@ export function ProvidersPane({ target = null, detailOf, defaults = true }: Prov
189
190
  key={provider.kind}
190
191
  provider={provider}
191
192
  entries={entriesOf(provider.kind)}
193
+ login={loginOf(provider.kind)}
192
194
  onCancel={() => setPicked({ kind: 'cli', cli: provider.kind })}
193
195
  onAdded={added}
194
196
  />
@@ -202,7 +204,8 @@ export function ProvidersPane({ target = null, detailOf, defaults = true }: Prov
202
204
  key={entry.id}
203
205
  provider={provider}
204
206
  entry={entry}
205
- canLogIn={openLogin !== null && canAddAccount(provider.kind)}
207
+ login={loginOf(provider.kind)}
208
+ canOpenLogin={openLogin !== null}
206
209
  loginBlocked={loginBlocked}
207
210
  secretsAvailable={accounts?.secretsAvailable === true}
208
211
  onRemoved={() => setPicked({ kind: 'cli', cli: provider.kind })}
@@ -215,7 +218,7 @@ export function ProvidersPane({ target = null, detailOf, defaults = true }: Prov
215
218
  provider={provider}
216
219
  entries={accounts === null ? null : entriesOf(provider.kind)}
217
220
  defaults={defaults}
218
- canAddAccount={canAddAccount(provider.kind)}
221
+ login={loginOf(provider.kind)}
219
222
  onAddAccount={() => setPicked({ kind: 'add', cli: provider.kind })}
220
223
  onPickAccount={(id) => setPicked({ kind: 'account', id })}
221
224
  />
@@ -2,17 +2,18 @@ import clsx from 'clsx';
2
2
  import type { AgentKind } from '@adecore/agent-contracts';
3
3
  import { AgentIcon } from '../agents/AgentIcon';
4
4
 
5
- // Each CLI in its color of the usage charts, so a mark here reads as the same CLI there.
6
- const CLI_COLORS: Record<AgentKind, string> = {
7
- apple: 'text-text-muted',
5
+ // Each CLI in its color of the usage charts, so a mark here reads as the same CLI there. A CLI without
6
+ // a chart color of its own is muted.
7
+ const CLI_COLORS: Partial<Record<AgentKind, string>> = {
8
8
  claude: 'text-chart-claude',
9
9
  codex: 'text-chart-codex',
10
10
  gemini: 'text-chart-gemini',
11
- copilot: 'text-chart-copilot'
11
+ copilot: 'text-chart-copilot',
12
+ nanogpt: 'text-chart-nanogpt'
12
13
  };
13
14
 
14
15
  export function CliMark({ kind, size = 14, className }: { kind: AgentKind; size?: number; className?: string }) {
15
- return <AgentIcon kind={kind} size={size} className={clsx('shrink-0', CLI_COLORS[kind], className)} />;
16
+ return <AgentIcon kind={kind} size={size} className={clsx('shrink-0', CLI_COLORS[kind] ?? 'text-text-muted', className)} />;
16
17
  }
17
18
 
18
19
  /* As tall as the title's first line, so the mark sits beside the name rather than between the two lines. */
package/src/theme.css CHANGED
@@ -13,6 +13,7 @@
13
13
  --chart-codex: #30b0c7;
14
14
  --chart-gemini: #3b82f6;
15
15
  --chart-copilot: #64748b;
16
+ --chart-nanogpt: #34c759;
16
17
  /* What a chat's context holds, one color per part; the rest is the system's and stays gray. */
17
18
  --chart-context-tools: #4d7ce6;
18
19
  --chart-context-files: #9a63e0;
@@ -45,6 +46,7 @@
45
46
  --chart-codex: #40c8e0;
46
47
  --chart-gemini: #60a5fa;
47
48
  --chart-copilot: #94a3b8;
49
+ --chart-nanogpt: #30d158;
48
50
  --chart-context-tools: #8aa7f5;
49
51
  --chart-context-files: #b98af3;
50
52
  --chart-context-conversation: #6dd08b;
@@ -82,6 +84,7 @@
82
84
  --color-chart-codex: var(--chart-codex);
83
85
  --color-chart-gemini: var(--chart-gemini);
84
86
  --color-chart-copilot: var(--chart-copilot);
87
+ --color-chart-nanogpt: var(--chart-nanogpt);
85
88
  --color-chart-context-tools: var(--chart-context-tools);
86
89
  --color-chart-context-files: var(--chart-context-files);
87
90
  --color-chart-context-conversation: var(--chart-context-conversation);
@@ -28,9 +28,13 @@ export function moneyFormat(currency: UsageCurrency, rate: UsageRate | null): (u
28
28
  };
29
29
  }
30
30
 
31
- export const PROVIDER_LABELS: Record<UsageProvider, string> = { claude: 'Claude Code', codex: 'Codex' };
31
+ export const PROVIDER_LABELS: Record<UsageProvider, string> = { claude: 'Claude Code', codex: 'Codex', nanogpt: 'NanoGPT' };
32
32
 
33
- export const PROVIDER_COLORS: Record<UsageProvider, string> = { claude: 'var(--chart-claude)', codex: 'var(--chart-codex)' };
33
+ export const PROVIDER_COLORS: Record<UsageProvider, string> = {
34
+ claude: 'var(--chart-claude)',
35
+ codex: 'var(--chart-codex)',
36
+ nanogpt: 'var(--chart-nanogpt)'
37
+ };
34
38
 
35
39
  /*
36
40
  * A slot is `2026-09-10` or `2026-09-10T14`, already in the viewer's own zone, so it is read as plain
@@ -108,7 +108,8 @@ export function explain(provider: UsageLimitsProvider): string | null {
108
108
  return i18next.t('agent-usage:limits.notInstalled');
109
109
  }
110
110
  if (provider.unavailable.reason === 'no-subscription') {
111
- return i18next.t('agent-usage:limits.noSubscription');
111
+ // A provider that knows why it has no plan says so; the rest are accounts on an API key.
112
+ return provider.unavailable.message ?? i18next.t('agent-usage:limits.noSubscription');
112
113
  }
113
114
  return provider.unavailable.message ?? i18next.t('agent-usage:limits.unreachable');
114
115
  }