@adecore/agents-react 0.31.0 → 0.31.2
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/host.d.ts +4 -1
- package/dist/locales/en/agent-providers.d.ts +37 -0
- package/dist/locales/en/agent-providers.js +38 -1
- package/dist/locales/nl/agent-providers.d.ts +37 -0
- package/dist/locales/nl/agent-providers.js +38 -1
- package/dist/providers/InstallPage.d.ts +9 -0
- package/dist/providers/InstallPage.js +74 -0
- package/dist/providers/ProvidersPane.d.ts +3 -1
- package/dist/providers/ProvidersPane.js +18 -7
- package/dist/providers/index.d.ts +1 -0
- package/dist/providers/install-steps.d.ts +14 -0
- package/dist/providers/install-steps.js +19 -0
- package/package.json +8 -8
- package/src/host.ts +11 -1
- package/src/locales/en/agent-providers.ts +38 -1
- package/src/locales/nl/agent-providers.ts +38 -1
- package/src/providers/InstallPage.tsx +221 -0
- package/src/providers/ProvidersPane.tsx +27 -12
- package/src/providers/index.ts +1 -0
- package/src/providers/install-steps.ts +35 -0
package/dist/host.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { Extension } from '@codemirror/state';
|
|
|
3
3
|
import type { EditorView } from '@codemirror/view';
|
|
4
4
|
import type { Highlighter, ThemeRegistration } from 'shiki';
|
|
5
5
|
import type { EditorEngine } from '@adecore/editor';
|
|
6
|
-
import type { AgentKind, ChatCheckpointDiff, ChatConfigurePayload, ChatGoalAction, ChatItem, ChatSubagentItem, ChatUiChoicePayload, ChatUiQueryPayload, ChatUiQueryReading, ChatUiLinkPayload, ChatUiLinkReading } from '@adecore/agent-contracts';
|
|
6
|
+
import type { AgentKind, ChatCheckpointDiff, ChatConfigurePayload, ChatGoalAction, ChatItem, ChatSubagentItem, ChatUiChoicePayload, ChatUiQueryPayload, ChatUiQueryReading, ChatUiLinkPayload, ChatUiLinkReading, ProviderPlatform } from '@adecore/agent-contracts';
|
|
7
7
|
import type { TimelineRow } from './chat/logic/timeline.js';
|
|
8
8
|
import type { FindReveal } from './chat/ui/find-reveal.js';
|
|
9
9
|
import type { PromptAction } from './prompts/logic/prompts.js';
|
|
@@ -249,6 +249,9 @@ export interface ChatHost {
|
|
|
249
249
|
}>;
|
|
250
250
|
openLogin: ((scopeId: string, kind: AgentKind, accountId: string, name: string) => Promise<void>) | null;
|
|
251
251
|
useLoginBlocked(scopeId: string): string | null;
|
|
252
|
+
runProviderCommand?(scopeId: string, kind: AgentKind, command: string): void;
|
|
253
|
+
useMachinePlatform?(scopeId: string): ProviderPlatform | null;
|
|
254
|
+
providerKeyNote?(variable: string): string | null;
|
|
252
255
|
useProjectLook(scopeId: string, projectId: string | null): {
|
|
253
256
|
name: string;
|
|
254
257
|
mark: ReactNode;
|
|
@@ -22,6 +22,43 @@ declare const _default: {
|
|
|
22
22
|
none: string;
|
|
23
23
|
noAccounts: string;
|
|
24
24
|
};
|
|
25
|
+
install: {
|
|
26
|
+
notInstalled: string;
|
|
27
|
+
steps: string;
|
|
28
|
+
platform: string;
|
|
29
|
+
platforms: {
|
|
30
|
+
darwin: string;
|
|
31
|
+
linux: string;
|
|
32
|
+
win32: string;
|
|
33
|
+
};
|
|
34
|
+
copy: string;
|
|
35
|
+
copied: string;
|
|
36
|
+
run: string;
|
|
37
|
+
guide: string;
|
|
38
|
+
install: {
|
|
39
|
+
title: string;
|
|
40
|
+
none: string;
|
|
41
|
+
done: string;
|
|
42
|
+
doneNoVersion: string;
|
|
43
|
+
};
|
|
44
|
+
signIn: {
|
|
45
|
+
title: string;
|
|
46
|
+
command: string;
|
|
47
|
+
env: string;
|
|
48
|
+
chat: string;
|
|
49
|
+
or: string;
|
|
50
|
+
unknown: string;
|
|
51
|
+
};
|
|
52
|
+
start: {
|
|
53
|
+
title: string;
|
|
54
|
+
description: string;
|
|
55
|
+
};
|
|
56
|
+
footer: {
|
|
57
|
+
missing: string;
|
|
58
|
+
found: string;
|
|
59
|
+
};
|
|
60
|
+
fallback: string;
|
|
61
|
+
};
|
|
25
62
|
abilities: {
|
|
26
63
|
version: string;
|
|
27
64
|
chatAndTerminal: string;
|
|
@@ -18,10 +18,47 @@ export default {
|
|
|
18
18
|
addAccount: 'Add a {{provider}} account',
|
|
19
19
|
notInstalled: 'Not installed',
|
|
20
20
|
missing: 'Missing',
|
|
21
|
-
notInstalledNote: '
|
|
21
|
+
notInstalledNote: 'Install one on this machine to use it here.',
|
|
22
22
|
none: 'No agent CLIs found on this machine.',
|
|
23
23
|
noAccounts: 'This machine keeps no accounts yet. Update Ruimte there to add one.'
|
|
24
24
|
},
|
|
25
|
+
install: {
|
|
26
|
+
notInstalled: 'Not installed',
|
|
27
|
+
steps: 'Steps to set up {{provider}}',
|
|
28
|
+
platform: 'System',
|
|
29
|
+
platforms: {
|
|
30
|
+
darwin: 'macOS',
|
|
31
|
+
linux: 'Linux',
|
|
32
|
+
win32: 'Windows'
|
|
33
|
+
},
|
|
34
|
+
copy: 'Copy',
|
|
35
|
+
copied: 'Copied',
|
|
36
|
+
run: 'Run in a terminal',
|
|
37
|
+
guide: 'Install guide',
|
|
38
|
+
install: {
|
|
39
|
+
title: 'Install the CLI',
|
|
40
|
+
none: 'There is no install command for this system.',
|
|
41
|
+
done: 'Installed, version {{version}}.',
|
|
42
|
+
doneNoVersion: 'Installed.'
|
|
43
|
+
},
|
|
44
|
+
signIn: {
|
|
45
|
+
title: 'Sign in',
|
|
46
|
+
command: 'Run this in a terminal and follow its steps:',
|
|
47
|
+
env: 'Set <code>{{variable}}</code> in the environment the CLI starts in.',
|
|
48
|
+
chat: 'The first chat asks you to sign in.',
|
|
49
|
+
or: 'Or',
|
|
50
|
+
unknown: 'The CLI asks you to sign in the first time it runs.'
|
|
51
|
+
},
|
|
52
|
+
start: {
|
|
53
|
+
title: 'Start working',
|
|
54
|
+
description: 'Pick {{provider}} for your next chat or terminal.'
|
|
55
|
+
},
|
|
56
|
+
footer: {
|
|
57
|
+
missing: 'Not found on this machine yet. It is checked again when you come back to this window.',
|
|
58
|
+
found: 'Found on this machine.'
|
|
59
|
+
},
|
|
60
|
+
fallback: 'Install {{provider}} on this machine to use it here.'
|
|
61
|
+
},
|
|
25
62
|
abilities: {
|
|
26
63
|
version: 'Version {{version}}.',
|
|
27
64
|
chatAndTerminal: 'Chat and terminal.',
|
|
@@ -22,6 +22,43 @@ declare const _default: {
|
|
|
22
22
|
none: string;
|
|
23
23
|
noAccounts: string;
|
|
24
24
|
};
|
|
25
|
+
install: {
|
|
26
|
+
notInstalled: string;
|
|
27
|
+
steps: string;
|
|
28
|
+
platform: string;
|
|
29
|
+
platforms: {
|
|
30
|
+
darwin: string;
|
|
31
|
+
linux: string;
|
|
32
|
+
win32: string;
|
|
33
|
+
};
|
|
34
|
+
copy: string;
|
|
35
|
+
copied: string;
|
|
36
|
+
run: string;
|
|
37
|
+
guide: string;
|
|
38
|
+
install: {
|
|
39
|
+
title: string;
|
|
40
|
+
none: string;
|
|
41
|
+
done: string;
|
|
42
|
+
doneNoVersion: string;
|
|
43
|
+
};
|
|
44
|
+
signIn: {
|
|
45
|
+
title: string;
|
|
46
|
+
command: string;
|
|
47
|
+
env: string;
|
|
48
|
+
chat: string;
|
|
49
|
+
or: string;
|
|
50
|
+
unknown: string;
|
|
51
|
+
};
|
|
52
|
+
start: {
|
|
53
|
+
title: string;
|
|
54
|
+
description: string;
|
|
55
|
+
};
|
|
56
|
+
footer: {
|
|
57
|
+
missing: string;
|
|
58
|
+
found: string;
|
|
59
|
+
};
|
|
60
|
+
fallback: string;
|
|
61
|
+
};
|
|
25
62
|
abilities: {
|
|
26
63
|
version: string;
|
|
27
64
|
chatAndTerminal: string;
|
|
@@ -18,10 +18,47 @@ export default {
|
|
|
18
18
|
addAccount: 'Een {{provider}}-account toevoegen',
|
|
19
19
|
notInstalled: 'Niet geïnstalleerd',
|
|
20
20
|
missing: 'Ontbreekt',
|
|
21
|
-
notInstalledNote: 'Installeer er een
|
|
21
|
+
notInstalledNote: 'Installeer er een op deze machine om hem hier te gebruiken.',
|
|
22
22
|
none: "Geen agent-CLI's gevonden op deze machine.",
|
|
23
23
|
noAccounts: 'Deze machine houdt nog geen accounts bij. Werk Ruimte daar bij om er een toe te voegen.'
|
|
24
24
|
},
|
|
25
|
+
install: {
|
|
26
|
+
notInstalled: 'Niet geïnstalleerd',
|
|
27
|
+
steps: '{{provider}} instellen in stappen',
|
|
28
|
+
platform: 'Systeem',
|
|
29
|
+
platforms: {
|
|
30
|
+
darwin: 'macOS',
|
|
31
|
+
linux: 'Linux',
|
|
32
|
+
win32: 'Windows'
|
|
33
|
+
},
|
|
34
|
+
copy: 'Kopiëren',
|
|
35
|
+
copied: 'Gekopieerd',
|
|
36
|
+
run: 'In een terminal uitvoeren',
|
|
37
|
+
guide: 'Installatiegids',
|
|
38
|
+
install: {
|
|
39
|
+
title: 'Installeer de CLI',
|
|
40
|
+
none: 'Er is geen installatiecommando voor dit systeem.',
|
|
41
|
+
done: 'Geïnstalleerd, versie {{version}}.',
|
|
42
|
+
doneNoVersion: 'Geïnstalleerd.'
|
|
43
|
+
},
|
|
44
|
+
signIn: {
|
|
45
|
+
title: 'Log in',
|
|
46
|
+
command: 'Voer dit uit in een terminal en volg de stappen:',
|
|
47
|
+
env: 'Zet <code>{{variable}}</code> in de omgeving waarin de CLI start.',
|
|
48
|
+
chat: 'De eerste chat vraagt je om in te loggen.',
|
|
49
|
+
or: 'Of',
|
|
50
|
+
unknown: 'De CLI vraagt je om in te loggen als hij voor het eerst draait.'
|
|
51
|
+
},
|
|
52
|
+
start: {
|
|
53
|
+
title: 'Aan de slag',
|
|
54
|
+
description: 'Kies {{provider}} voor je volgende chat of terminal.'
|
|
55
|
+
},
|
|
56
|
+
footer: {
|
|
57
|
+
missing: 'Nog niet gevonden op deze machine. Er wordt opnieuw gekeken als je terugkomt in dit venster.',
|
|
58
|
+
found: 'Gevonden op deze machine.'
|
|
59
|
+
},
|
|
60
|
+
fallback: 'Installeer {{provider}} op deze machine om hem hier te gebruiken.'
|
|
61
|
+
},
|
|
25
62
|
abilities: {
|
|
26
63
|
version: 'Versie {{version}}.',
|
|
27
64
|
chatAndTerminal: 'Chat en terminal.',
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ProviderInfo } from '@adecore/agent-contracts';
|
|
2
|
+
import { type ProviderInstallStep } from './install-steps.js';
|
|
3
|
+
interface InstallPageProps {
|
|
4
|
+
provider: ProviderInfo;
|
|
5
|
+
signedIn: boolean;
|
|
6
|
+
extra: ProviderInstallStep | null;
|
|
7
|
+
}
|
|
8
|
+
export declare function InstallPage({ provider, signedIn, extra }: InstallPageProps): import("react").JSX.Element;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import clsx from 'clsx';
|
|
4
|
+
import { Trans, useTranslation } from 'react-i18next';
|
|
5
|
+
import { Check, Copy, ExternalLink, SquareTerminal } from 'lucide-react';
|
|
6
|
+
import { Button, copyText, Icon, Pill, Segmented } from '@adecore/ui';
|
|
7
|
+
import { useChatHost } from '../host.js';
|
|
8
|
+
import { useChatScope } from '../scope.js';
|
|
9
|
+
import { installPlatforms, installStepStates, pagePlatform } from './install-steps.js';
|
|
10
|
+
import { CliMark } from './parts.js';
|
|
11
|
+
const noPlatform = () => null;
|
|
12
|
+
function CommandBlock({ command }) {
|
|
13
|
+
const { t } = useTranslation('agent-providers');
|
|
14
|
+
const [copied, setCopied] = useState(null);
|
|
15
|
+
return (_jsxs("div", { className: "flex min-w-0 items-center gap-2 rounded-lg border border-border bg-surface-sunken py-1 pr-1 pl-3", children: [_jsx("code", { className: "min-w-0 grow font-mono text-code break-all text-text select-text", children: command }), _jsxs(Button, { size: "xs", onClick: () => {
|
|
16
|
+
copyText(command);
|
|
17
|
+
setCopied(command);
|
|
18
|
+
}, children: [_jsx(Icon, { icon: copied === command ? Check : Copy, size: 12 }), copied === command ? t('install.copied') : t('install.copy')] })] }));
|
|
19
|
+
}
|
|
20
|
+
function Step({ index, state, title, children }) {
|
|
21
|
+
return (_jsxs("li", { "aria-current": state === 'current' ? 'step' : undefined, "data-state": state, className: clsx('flex min-w-0 gap-3 rounded-xl border p-4', state === 'current' ? 'border-accent' : 'border-border'), children: [_jsx("span", { className: clsx('grid size-6 shrink-0 place-items-center rounded-full text-xs font-medium', state === 'done' && 'bg-positive text-positive-text', state === 'current' && 'border border-accent text-accent', state === 'later' && 'bg-text/7 text-text-faint'), children: state === 'done' ? _jsx(Icon, { icon: Check, size: 14 }) : index + 1 }), _jsxs("div", { className: "flex min-w-0 grow flex-col gap-3", children: [_jsx("h4", { className: clsx('text-sm leading-6 font-medium', state === 'later' ? 'text-text-muted' : 'text-text'), children: title }), children] })] }));
|
|
22
|
+
}
|
|
23
|
+
function SignInWay({ way, canRun, onRun }) {
|
|
24
|
+
const { t } = useTranslation('agent-providers');
|
|
25
|
+
const { providerKeyNote } = useChatHost();
|
|
26
|
+
switch (way.kind) {
|
|
27
|
+
case 'command':
|
|
28
|
+
return (_jsxs("div", { className: "flex min-w-0 flex-col gap-2", children: [_jsx("p", { className: "text-xs text-text-muted", children: t('install.signIn.command') }), _jsx(CommandBlock, { command: way.command }), canRun && (_jsxs(Button, { variant: "secondary", size: "sm", className: "self-start", onClick: () => onRun(way.command), children: [_jsx(Icon, { icon: SquareTerminal, size: 14 }), t('install.run')] }))] }));
|
|
29
|
+
case 'env': {
|
|
30
|
+
const note = providerKeyNote?.(way.variable) ?? null;
|
|
31
|
+
return (_jsxs("p", { className: "text-xs text-text-muted", children: [_jsx(Trans, { t: t, i18nKey: "install.signIn.env", values: { variable: way.variable }, components: { code: _jsx("code", { className: "font-mono text-code text-text" }) } }), note !== null && ` ${note}`] }));
|
|
32
|
+
}
|
|
33
|
+
case 'chat':
|
|
34
|
+
return _jsx("p", { className: "text-xs text-text-muted", children: t('install.signIn.chat') });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/* How to get a CLI that is not on the machine yet, step by step, until it is there and signed in. */
|
|
38
|
+
export function InstallPage({ provider, signedIn, extra }) {
|
|
39
|
+
const { t } = useTranslation('agent-providers');
|
|
40
|
+
const { id: scopeId } = useChatScope();
|
|
41
|
+
const host = useChatHost();
|
|
42
|
+
const useMachinePlatform = host.useMachinePlatform ?? noPlatform;
|
|
43
|
+
const machine = useMachinePlatform(scopeId) ?? pagePlatform();
|
|
44
|
+
const { offered, initial } = installPlatforms(provider, machine);
|
|
45
|
+
// Only a person's pick is kept, so a machine whose system arrives late still opens on its own.
|
|
46
|
+
const [chosen, setPlatform] = useState(null);
|
|
47
|
+
const platform = chosen ?? initial;
|
|
48
|
+
const install = provider.install;
|
|
49
|
+
const run = host.runProviderCommand;
|
|
50
|
+
const onRun = (command) => run?.(scopeId, provider.kind, command);
|
|
51
|
+
const command = platform === null ? undefined : install?.commands[platform];
|
|
52
|
+
const steps = [];
|
|
53
|
+
steps.push({
|
|
54
|
+
title: t('install.install.title'),
|
|
55
|
+
done: provider.installed,
|
|
56
|
+
body: provider.installed ? (_jsx("p", { className: "text-xs text-text-muted", children: provider.version ? t('install.install.done', { version: provider.version }) : t('install.install.doneNoVersion') })) : (_jsxs(_Fragment, { children: [offered.length > 1 && platform !== null && (_jsx(Segmented, { value: platform, label: t('install.platform'), options: offered.map((id) => ({ id, label: t(`install.platforms.${id}`) })), onValueChange: setPlatform })), command ? _jsx(CommandBlock, { command: command }) : _jsx("p", { className: "text-xs text-text-muted", children: t('install.install.none') }), ((command && run) || install?.guideUrl) && (_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [command && run && (_jsxs(Button, { variant: "primary", onClick: () => onRun(command), children: [_jsx(Icon, { icon: SquareTerminal, size: 14 }), t('install.run')] })), install?.guideUrl && (_jsxs(Button, { href: install.guideUrl, children: [t('install.guide'), _jsx(Icon, { icon: ExternalLink, size: 14 })] }))] }))] }))
|
|
57
|
+
});
|
|
58
|
+
if (extra !== null) {
|
|
59
|
+
steps.push(extra);
|
|
60
|
+
}
|
|
61
|
+
const ways = install?.signIn ?? [];
|
|
62
|
+
steps.push({
|
|
63
|
+
title: t('install.signIn.title'),
|
|
64
|
+
done: provider.installed && signedIn,
|
|
65
|
+
body: ways.length === 0 ? (_jsx("p", { className: "text-xs text-text-muted", children: t('install.signIn.unknown') })) : (ways.map((way, index) => (_jsxs("div", { className: "flex min-w-0 flex-col gap-2", children: [index > 0 && _jsx("span", { className: "text-xs font-medium text-text-faint", children: t('install.signIn.or') }), _jsx(SignInWay, { way: way, canRun: run !== undefined && provider.installed, onRun: onRun })] }, `${way.kind}-${index}`))))
|
|
66
|
+
});
|
|
67
|
+
steps.push({
|
|
68
|
+
title: t('install.start.title'),
|
|
69
|
+
done: false,
|
|
70
|
+
body: _jsx("p", { className: "text-xs text-text-muted", children: t('install.start.description', { provider: provider.name }) })
|
|
71
|
+
});
|
|
72
|
+
const states = installStepStates(steps.map((step) => step.done));
|
|
73
|
+
return (_jsxs(_Fragment, { children: [_jsxs("header", { className: "flex min-w-0 items-start gap-3", children: [_jsx("span", { className: "grid size-10 shrink-0 place-items-center rounded-xl bg-surface-sunken", children: _jsx(CliMark, { kind: provider.kind, size: 20 }) }), _jsxs("div", { className: "flex min-w-0 grow flex-col", children: [_jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [_jsx("h3", { className: "truncate text-lg font-semibold text-text", children: provider.name }), provider.installed ? (provider.version && (_jsx(Pill, { tone: "idle", mono: true, children: provider.version }))) : (_jsx(Pill, { children: t('install.notInstalled') }))] }), install && _jsx("p", { className: "text-xs text-text-muted", children: install.description })] })] }), install ? (_jsx("ol", { "aria-label": t('install.steps', { provider: provider.name }), className: "flex min-w-0 flex-col gap-3", children: steps.map((step, index) => (_jsx(Step, { index: index, state: states[index], title: step.title, children: step.body }, index))) })) : (_jsx("p", { className: "text-xs text-text-muted", children: t('install.fallback', { provider: provider.name }) })), _jsxs("p", { className: "flex min-w-0 items-center gap-2 text-xs text-text-muted", children: [_jsx("span", { "aria-hidden": true, className: clsx('size-2 shrink-0 rounded-full', provider.installed ? 'bg-status-idle' : 'bg-text-faint') }), provider.installed ? t('install.footer.found') : t('install.footer.missing')] })] }));
|
|
74
|
+
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { type ReactNode } from 'react';
|
|
2
2
|
import type { ProviderInfo } from '@adecore/agent-contracts';
|
|
3
|
+
import type { ProviderInstallStep } from './install-steps.js';
|
|
3
4
|
interface ProvidersPaneProps {
|
|
4
5
|
target?: string | null;
|
|
5
6
|
detailOf?(provider: ProviderInfo): ReactNode | null;
|
|
6
7
|
defaults?: boolean;
|
|
8
|
+
installStepOf?(provider: ProviderInfo): ProviderInstallStep | null;
|
|
7
9
|
}
|
|
8
|
-
export declare function ProvidersPane({ target, detailOf, defaults }: ProvidersPaneProps): import("react").JSX.Element;
|
|
10
|
+
export declare function ProvidersPane({ target, detailOf, defaults, installStepOf }: ProvidersPaneProps): import("react").JSX.Element;
|
|
9
11
|
export {};
|
|
@@ -2,16 +2,17 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
2
2
|
import { useMemo, useState } from 'react';
|
|
3
3
|
import clsx from 'clsx';
|
|
4
4
|
import { useTranslation } from 'react-i18next';
|
|
5
|
-
import { Plus
|
|
5
|
+
import { Plus } from 'lucide-react';
|
|
6
6
|
import { AccountDot } from '../agents/AccountDot.js';
|
|
7
7
|
import { ACCOUNT_TONE_CLASSES, accountLoginOf, accountName, accountsOfKind, accountStatusLine } from '../agents/accounts.js';
|
|
8
|
-
import { ErrorBoundary,
|
|
8
|
+
import { ErrorBoundary, IconButton, SectionLabel, Skeleton } from '@adecore/ui';
|
|
9
9
|
import { MasterDetail } from '@adecore/ui/settings';
|
|
10
10
|
import { useChatHost } from '../host.js';
|
|
11
11
|
import { useChatScope } from '../scope.js';
|
|
12
12
|
import { AccountDetail } from './AccountDetail.js';
|
|
13
13
|
import { AddAccountForm } from './AddAccountForm.js';
|
|
14
14
|
import { CliDetail } from './CliDetail.js';
|
|
15
|
+
import { InstallPage } from './InstallPage.js';
|
|
15
16
|
import { CliMark } from './parts.js';
|
|
16
17
|
import { useProviderAccounts } from '../state/provider-accounts.js';
|
|
17
18
|
import { useProviders } from '../state/providers.js';
|
|
@@ -20,7 +21,7 @@ function ListRow({ selected, onSelect, children, trailing, className }) {
|
|
|
20
21
|
return (_jsxs("div", { className: clsx('flex min-w-0 shrink-0 items-center gap-2 rounded-lg pr-2', selected ? 'bg-text/5' : 'hover:bg-surface-hover', className), children: [_jsx("button", { type: "button", "aria-current": selected ? 'true' : undefined, className: "flex min-w-0 grow items-center gap-3 self-stretch rounded-lg py-2 pl-3 text-left focus-visible:-outline-offset-2", onClick: onSelect, children: children }), trailing] }));
|
|
21
22
|
}
|
|
22
23
|
/* The agent CLIs of the host in scope, each with its accounts, beside the detail of what is picked. */
|
|
23
|
-
export function ProvidersPane({ target = null, detailOf, defaults = true }) {
|
|
24
|
+
export function ProvidersPane({ target = null, detailOf, defaults = true, installStepOf }) {
|
|
24
25
|
const { t } = useTranslation('agent-providers');
|
|
25
26
|
const { id: scopeId } = useChatScope();
|
|
26
27
|
const providers = useProviders((s) => s.providers);
|
|
@@ -40,7 +41,8 @@ export function ProvidersPane({ target = null, detailOf, defaults = true }) {
|
|
|
40
41
|
const current = picked !== null && (picked.kind !== 'account' || pickedEntry !== null) ? picked : installed[0] ? { kind: 'cli', cli: installed[0].kind } : null;
|
|
41
42
|
const currentAccount = current?.kind === 'account' ? accounts?.accounts[current.id] : undefined;
|
|
42
43
|
const currentCli = current === null ? null : current.kind === 'account' ? currentAccount?.kind : current.cli;
|
|
43
|
-
const provider = installed.find((entry) => entry.kind === currentCli) ?? null;
|
|
44
|
+
const provider = (current?.kind === 'install' ? providers : installed).find((entry) => entry.kind === currentCli) ?? null;
|
|
45
|
+
const picksCli = (kind) => (current?.kind === 'cli' || current?.kind === 'install') && current.cli === kind;
|
|
44
46
|
// A search result lands on a row of a CLI's detail, so a picked account makes way for its CLI.
|
|
45
47
|
const [seenTarget, setSeenTarget] = useState(target);
|
|
46
48
|
if (target !== seenTarget) {
|
|
@@ -60,18 +62,27 @@ export function ProvidersPane({ target = null, detailOf, defaults = true }) {
|
|
|
60
62
|
};
|
|
61
63
|
const group = (cli) => {
|
|
62
64
|
const entries = cli.installed ? entriesOf(cli.kind) : [];
|
|
63
|
-
return (_jsxs("div", { className: "flex min-w-0 shrink-0 flex-col gap-0.5 pt-1", children: [_jsxs(ListRow, { selected:
|
|
65
|
+
return (_jsxs("div", { className: "flex min-w-0 shrink-0 flex-col gap-0.5 pt-1", children: [_jsxs(ListRow, { selected: picksCli(cli.kind), onSelect: () => setPicked({ kind: 'cli', cli: cli.kind }), className: "pr-1", trailing: loginOf(cli.kind) !== null && (_jsx(IconButton, { icon: Plus, size: "sm", label: t('list.addAccount', { provider: cli.name }), onClick: () => setPicked({ kind: 'add', cli: cli.kind }) })), children: [_jsx(CliMark, { kind: cli.kind }), _jsx("span", { className: "min-w-0 grow truncate text-xs font-medium text-text", children: cli.name }), cli.capabilities.terminal && cli.version && _jsx("span", { className: "shrink-0 font-mono text-code text-text-faint", children: cli.version })] }), entries.map((entry) => {
|
|
64
66
|
const name = accountName(entry, cli.name);
|
|
65
67
|
const status = accountStatusLine(entry.status, loginOf(cli.kind) === 'terminal');
|
|
66
68
|
return (_jsxs(ListRow, { selected: current?.kind === 'account' && current.id === entry.id, onSelect: () => setPicked({ kind: 'account', id: entry.id }), children: [_jsx("span", { className: "grid w-3.5 shrink-0 place-items-center", children: _jsx(AccountDot, { color: entry.account.color, className: "size-2.25" }) }), _jsxs("span", { className: "flex min-w-0 grow flex-col", children: [_jsxs("span", { className: "flex min-w-0 items-center gap-2 text-sm text-text", children: [_jsx("span", { className: "truncate", children: name }), entry.isDefault && (_jsx("span", { className: "shrink-0 rounded-md bg-surface-active px-1.5 text-xs text-text-muted", children: t('list.default') }))] }), _jsx("span", { className: clsx('truncate text-xs', ACCOUNT_TONE_CLASSES[status.tone]), children: status.text })] })] }, entry.id));
|
|
67
69
|
})] }, cli.kind));
|
|
68
70
|
};
|
|
69
|
-
const list = !loaded ? (_jsxs("div", { className: "flex flex-col gap-3 p-3", children: [_jsx(Skeleton, { className: "w-32" }), _jsx(Skeleton, { className: "w-40" })] })) : (_jsxs(_Fragment, { children: [installed.map(group), installed.length === 0 && _jsx("p", { className: "px-3 py-2 text-xs text-text-muted", children: t('list.none') }), missing.length > 0 && (_jsxs("div", { className: "mt-1.5 flex min-w-0 shrink-0 flex-col border-t border-border pt-2.5 pb-2", children: [_jsx(SectionLabel, { className: "px-3 pt-1 pb-1.5", children: t('list.notInstalled') }), missing.map((cli) => (_jsxs(
|
|
71
|
+
const list = !loaded ? (_jsxs("div", { className: "flex flex-col gap-3 p-3", children: [_jsx(Skeleton, { className: "w-32" }), _jsx(Skeleton, { className: "w-40" })] })) : (_jsxs(_Fragment, { children: [installed.map(group), installed.length === 0 && _jsx("p", { className: "px-3 py-2 text-xs text-text-muted", children: t('list.none') }), missing.length > 0 && (_jsxs("div", { className: "mt-1.5 flex min-w-0 shrink-0 flex-col border-t border-border pt-2.5 pb-2", children: [_jsx(SectionLabel, { className: "px-3 pt-1 pb-1.5", children: t('list.notInstalled') }), missing.map((cli) => (_jsxs(ListRow, { selected: picksCli(cli.kind), onSelect: () => setPicked({ kind: 'install', cli: cli.kind }), children: [_jsx(CliMark, { kind: cli.kind, className: "opacity-60" }), _jsx("span", { className: "min-w-0 grow truncate text-xs font-medium text-text-muted", children: cli.name }), _jsx("span", { className: "shrink-0 text-xs text-text-faint", children: t('list.missing') })] }, cli.kind))), missing.every((cli) => cli.install === undefined) && _jsx("p", { className: "mt-1 px-3 text-xs text-text-faint", children: t('list.notInstalledNote') })] }))] }));
|
|
70
72
|
const detail = () => {
|
|
71
73
|
if (current === null || provider === null) {
|
|
72
74
|
return null;
|
|
73
75
|
}
|
|
74
|
-
|
|
76
|
+
if (current.kind === 'install') {
|
|
77
|
+
const extra = installStepOf?.(provider) ?? null;
|
|
78
|
+
const statuses = entriesOf(provider.kind).map((entry) => entry.status?.state);
|
|
79
|
+
// Without accounts to read, nothing says the CLI is signed out, so being there is all it takes.
|
|
80
|
+
const signedIn = statuses.length === 0 || statuses.includes('ready');
|
|
81
|
+
if (!provider.installed || !signedIn || (extra !== null && !extra.done)) {
|
|
82
|
+
return _jsx(InstallPage, { provider: provider, signedIn: signedIn, extra: extra }, provider.kind);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const own = current.kind === 'cli' || current.kind === 'install' ? (detailOf?.(provider) ?? null) : null;
|
|
75
86
|
if (own !== null) {
|
|
76
87
|
return own;
|
|
77
88
|
}
|
|
@@ -7,4 +7,5 @@ export { modelName, useModelName, shortModelName } from '../agents/model-name.js
|
|
|
7
7
|
export { carryOptions } from '../agents/model-options.js';
|
|
8
8
|
export { type ModelRequirement } from '../agents/model-requirement.js';
|
|
9
9
|
export { ProvidersPane } from './ProvidersPane.js';
|
|
10
|
+
export type { ProviderInstallStep } from './install-steps.js';
|
|
10
11
|
export { CliMark, CliTile } from './parts.js';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { ProviderInfo, ProviderPlatform } from '@adecore/agent-contracts';
|
|
3
|
+
export interface ProviderInstallStep {
|
|
4
|
+
title: string;
|
|
5
|
+
done: boolean;
|
|
6
|
+
body: ReactNode;
|
|
7
|
+
}
|
|
8
|
+
export type InstallStepState = 'done' | 'current' | 'later';
|
|
9
|
+
export declare function installStepStates(done: readonly boolean[]): InstallStepState[];
|
|
10
|
+
export declare function pagePlatform(): ProviderPlatform;
|
|
11
|
+
export declare function installPlatforms(provider: ProviderInfo, machine: ProviderPlatform): {
|
|
12
|
+
offered: ProviderPlatform[];
|
|
13
|
+
initial: ProviderPlatform | null;
|
|
14
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { isApplePlatform } from '@adecore/ui';
|
|
2
|
+
const PLATFORMS = ['darwin', 'linux', 'win32'];
|
|
3
|
+
/* The first step not done is the current one. */
|
|
4
|
+
export function installStepStates(done) {
|
|
5
|
+
const first = done.indexOf(false);
|
|
6
|
+
return done.map((value, index) => (value ? 'done' : index === first ? 'current' : 'later'));
|
|
7
|
+
}
|
|
8
|
+
/* The system of the page, for a host that does not say which system its machine runs. */
|
|
9
|
+
export function pagePlatform() {
|
|
10
|
+
if (isApplePlatform()) {
|
|
11
|
+
return 'darwin';
|
|
12
|
+
}
|
|
13
|
+
return typeof navigator !== 'undefined' && /windows/i.test(navigator.userAgent) ? 'win32' : 'linux';
|
|
14
|
+
}
|
|
15
|
+
/* The systems a provider has a command for, and the one the page opens on: the machine's when it has one there. */
|
|
16
|
+
export function installPlatforms(provider, machine) {
|
|
17
|
+
const offered = PLATFORMS.filter((platform) => Boolean(provider.install?.commands[platform]));
|
|
18
|
+
return { offered, initial: offered.includes(machine) ? machine : (offered[0] ?? null) };
|
|
19
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adecore/agents-react",
|
|
3
|
-
"version": "0.31.
|
|
3
|
+
"version": "0.31.2",
|
|
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.2",
|
|
71
|
+
"@adecore/editor": "0.31.2",
|
|
72
|
+
"@adecore/editor-react": "0.31.2",
|
|
73
|
+
"@adecore/intelligent-ui": "0.31.2",
|
|
74
|
+
"@adecore/merge": "0.31.2",
|
|
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.2",
|
|
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.2",
|
|
105
105
|
"@types/bun": "^1.4.2",
|
|
106
106
|
"@types/react": "^19.3.0",
|
|
107
107
|
"@types/react-dom": "^19.3.0",
|
package/src/host.ts
CHANGED
|
@@ -14,7 +14,8 @@ import type {
|
|
|
14
14
|
ChatUiQueryPayload,
|
|
15
15
|
ChatUiQueryReading,
|
|
16
16
|
ChatUiLinkPayload,
|
|
17
|
-
ChatUiLinkReading
|
|
17
|
+
ChatUiLinkReading,
|
|
18
|
+
ProviderPlatform
|
|
18
19
|
} from '@adecore/agent-contracts';
|
|
19
20
|
import { isApplePlatform } from '@adecore/ui';
|
|
20
21
|
import type { TimelineRow } from './chat/logic/timeline';
|
|
@@ -384,6 +385,15 @@ export interface ChatHost {
|
|
|
384
385
|
openLogin: ((scopeId: string, kind: AgentKind, accountId: string, name: string) => Promise<void>) | null;
|
|
385
386
|
/* Why a login cannot start from here right now, or null when it can. */
|
|
386
387
|
useLoginBlocked(scopeId: string): string | null;
|
|
388
|
+
/*
|
|
389
|
+
* Runs a command a provider's install page shows, its install or its sign-in, where a person sees it run,
|
|
390
|
+
* such as in a new terminal on the scope's machine; left out, the page offers to copy the command only.
|
|
391
|
+
*/
|
|
392
|
+
runProviderCommand?(scopeId: string, kind: AgentKind, command: string): void;
|
|
393
|
+
/* The system of the machine the scope's chats run on, whose install command the page shows first; left out, or null, the system of this page. */
|
|
394
|
+
useMachinePlatform?(scopeId: string): ProviderPlatform | null;
|
|
395
|
+
/* A sentence under a key the install page asks for in the environment, such as what the app does with it; left out, or null, none. */
|
|
396
|
+
providerKeyNote?(variable: string): string | null;
|
|
387
397
|
/* The name and the mark of a project of the host, for the usage per project; null draws the folder's own. */
|
|
388
398
|
useProjectLook(scopeId: string, projectId: string | null): { name: string; mark: ReactNode } | null;
|
|
389
399
|
/* What a person calls the machine the scope's chats run on, or null when it has no name worth saying. */
|
|
@@ -18,10 +18,47 @@ export default {
|
|
|
18
18
|
addAccount: 'Add a {{provider}} account',
|
|
19
19
|
notInstalled: 'Not installed',
|
|
20
20
|
missing: 'Missing',
|
|
21
|
-
notInstalledNote: '
|
|
21
|
+
notInstalledNote: 'Install one on this machine to use it here.',
|
|
22
22
|
none: 'No agent CLIs found on this machine.',
|
|
23
23
|
noAccounts: 'This machine keeps no accounts yet. Update Ruimte there to add one.'
|
|
24
24
|
},
|
|
25
|
+
install: {
|
|
26
|
+
notInstalled: 'Not installed',
|
|
27
|
+
steps: 'Steps to set up {{provider}}',
|
|
28
|
+
platform: 'System',
|
|
29
|
+
platforms: {
|
|
30
|
+
darwin: 'macOS',
|
|
31
|
+
linux: 'Linux',
|
|
32
|
+
win32: 'Windows'
|
|
33
|
+
},
|
|
34
|
+
copy: 'Copy',
|
|
35
|
+
copied: 'Copied',
|
|
36
|
+
run: 'Run in a terminal',
|
|
37
|
+
guide: 'Install guide',
|
|
38
|
+
install: {
|
|
39
|
+
title: 'Install the CLI',
|
|
40
|
+
none: 'There is no install command for this system.',
|
|
41
|
+
done: 'Installed, version {{version}}.',
|
|
42
|
+
doneNoVersion: 'Installed.'
|
|
43
|
+
},
|
|
44
|
+
signIn: {
|
|
45
|
+
title: 'Sign in',
|
|
46
|
+
command: 'Run this in a terminal and follow its steps:',
|
|
47
|
+
env: 'Set <code>{{variable}}</code> in the environment the CLI starts in.',
|
|
48
|
+
chat: 'The first chat asks you to sign in.',
|
|
49
|
+
or: 'Or',
|
|
50
|
+
unknown: 'The CLI asks you to sign in the first time it runs.'
|
|
51
|
+
},
|
|
52
|
+
start: {
|
|
53
|
+
title: 'Start working',
|
|
54
|
+
description: 'Pick {{provider}} for your next chat or terminal.'
|
|
55
|
+
},
|
|
56
|
+
footer: {
|
|
57
|
+
missing: 'Not found on this machine yet. It is checked again when you come back to this window.',
|
|
58
|
+
found: 'Found on this machine.'
|
|
59
|
+
},
|
|
60
|
+
fallback: 'Install {{provider}} on this machine to use it here.'
|
|
61
|
+
},
|
|
25
62
|
abilities: {
|
|
26
63
|
version: 'Version {{version}}.',
|
|
27
64
|
chatAndTerminal: 'Chat and terminal.',
|
|
@@ -18,10 +18,47 @@ export default {
|
|
|
18
18
|
addAccount: 'Een {{provider}}-account toevoegen',
|
|
19
19
|
notInstalled: 'Niet geïnstalleerd',
|
|
20
20
|
missing: 'Ontbreekt',
|
|
21
|
-
notInstalledNote: 'Installeer er een
|
|
21
|
+
notInstalledNote: 'Installeer er een op deze machine om hem hier te gebruiken.',
|
|
22
22
|
none: "Geen agent-CLI's gevonden op deze machine.",
|
|
23
23
|
noAccounts: 'Deze machine houdt nog geen accounts bij. Werk Ruimte daar bij om er een toe te voegen.'
|
|
24
24
|
},
|
|
25
|
+
install: {
|
|
26
|
+
notInstalled: 'Niet geïnstalleerd',
|
|
27
|
+
steps: '{{provider}} instellen in stappen',
|
|
28
|
+
platform: 'Systeem',
|
|
29
|
+
platforms: {
|
|
30
|
+
darwin: 'macOS',
|
|
31
|
+
linux: 'Linux',
|
|
32
|
+
win32: 'Windows'
|
|
33
|
+
},
|
|
34
|
+
copy: 'Kopiëren',
|
|
35
|
+
copied: 'Gekopieerd',
|
|
36
|
+
run: 'In een terminal uitvoeren',
|
|
37
|
+
guide: 'Installatiegids',
|
|
38
|
+
install: {
|
|
39
|
+
title: 'Installeer de CLI',
|
|
40
|
+
none: 'Er is geen installatiecommando voor dit systeem.',
|
|
41
|
+
done: 'Geïnstalleerd, versie {{version}}.',
|
|
42
|
+
doneNoVersion: 'Geïnstalleerd.'
|
|
43
|
+
},
|
|
44
|
+
signIn: {
|
|
45
|
+
title: 'Log in',
|
|
46
|
+
command: 'Voer dit uit in een terminal en volg de stappen:',
|
|
47
|
+
env: 'Zet <code>{{variable}}</code> in de omgeving waarin de CLI start.',
|
|
48
|
+
chat: 'De eerste chat vraagt je om in te loggen.',
|
|
49
|
+
or: 'Of',
|
|
50
|
+
unknown: 'De CLI vraagt je om in te loggen als hij voor het eerst draait.'
|
|
51
|
+
},
|
|
52
|
+
start: {
|
|
53
|
+
title: 'Aan de slag',
|
|
54
|
+
description: 'Kies {{provider}} voor je volgende chat of terminal.'
|
|
55
|
+
},
|
|
56
|
+
footer: {
|
|
57
|
+
missing: 'Nog niet gevonden op deze machine. Er wordt opnieuw gekeken als je terugkomt in dit venster.',
|
|
58
|
+
found: 'Gevonden op deze machine.'
|
|
59
|
+
},
|
|
60
|
+
fallback: 'Installeer {{provider}} op deze machine om hem hier te gebruiken.'
|
|
61
|
+
},
|
|
25
62
|
abilities: {
|
|
26
63
|
version: 'Versie {{version}}.',
|
|
27
64
|
chatAndTerminal: 'Chat en terminal.',
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { useState, type ReactNode } from 'react';
|
|
2
|
+
import clsx from 'clsx';
|
|
3
|
+
import { Trans, useTranslation } from 'react-i18next';
|
|
4
|
+
import { Check, Copy, ExternalLink, SquareTerminal } from 'lucide-react';
|
|
5
|
+
import type { ProviderInfo, ProviderPlatform, ProviderSignIn } from '@adecore/agent-contracts';
|
|
6
|
+
import { Button, copyText, Icon, Pill, Segmented } from '@adecore/ui';
|
|
7
|
+
import { useChatHost } from '../host';
|
|
8
|
+
import { useChatScope } from '../scope';
|
|
9
|
+
import { installPlatforms, installStepStates, pagePlatform, type InstallStepState, type ProviderInstallStep } from './install-steps';
|
|
10
|
+
import { CliMark } from './parts';
|
|
11
|
+
|
|
12
|
+
const noPlatform = (): ProviderPlatform | null => null;
|
|
13
|
+
|
|
14
|
+
function CommandBlock({ command }: { command: string }) {
|
|
15
|
+
const { t } = useTranslation('agent-providers');
|
|
16
|
+
const [copied, setCopied] = useState<string | null>(null);
|
|
17
|
+
return (
|
|
18
|
+
<div className="flex min-w-0 items-center gap-2 rounded-lg border border-border bg-surface-sunken py-1 pr-1 pl-3">
|
|
19
|
+
<code className="min-w-0 grow font-mono text-code break-all text-text select-text">{command}</code>
|
|
20
|
+
<Button
|
|
21
|
+
size="xs"
|
|
22
|
+
onClick={() => {
|
|
23
|
+
copyText(command);
|
|
24
|
+
setCopied(command);
|
|
25
|
+
}}
|
|
26
|
+
>
|
|
27
|
+
<Icon icon={copied === command ? Check : Copy} size={12} />
|
|
28
|
+
{copied === command ? t('install.copied') : t('install.copy')}
|
|
29
|
+
</Button>
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function Step({ index, state, title, children }: { index: number; state: InstallStepState; title: string; children?: ReactNode }) {
|
|
35
|
+
return (
|
|
36
|
+
<li
|
|
37
|
+
aria-current={state === 'current' ? 'step' : undefined}
|
|
38
|
+
data-state={state}
|
|
39
|
+
className={clsx('flex min-w-0 gap-3 rounded-xl border p-4', state === 'current' ? 'border-accent' : 'border-border')}
|
|
40
|
+
>
|
|
41
|
+
<span
|
|
42
|
+
className={clsx(
|
|
43
|
+
'grid size-6 shrink-0 place-items-center rounded-full text-xs font-medium',
|
|
44
|
+
state === 'done' && 'bg-positive text-positive-text',
|
|
45
|
+
state === 'current' && 'border border-accent text-accent',
|
|
46
|
+
state === 'later' && 'bg-text/7 text-text-faint'
|
|
47
|
+
)}
|
|
48
|
+
>
|
|
49
|
+
{state === 'done' ? <Icon icon={Check} size={14} /> : index + 1}
|
|
50
|
+
</span>
|
|
51
|
+
<div className="flex min-w-0 grow flex-col gap-3">
|
|
52
|
+
<h4 className={clsx('text-sm leading-6 font-medium', state === 'later' ? 'text-text-muted' : 'text-text')}>{title}</h4>
|
|
53
|
+
{children}
|
|
54
|
+
</div>
|
|
55
|
+
</li>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function SignInWay({ way, canRun, onRun }: { way: ProviderSignIn; canRun: boolean; onRun(command: string): void }) {
|
|
60
|
+
const { t } = useTranslation('agent-providers');
|
|
61
|
+
const { providerKeyNote } = useChatHost();
|
|
62
|
+
switch (way.kind) {
|
|
63
|
+
case 'command':
|
|
64
|
+
return (
|
|
65
|
+
<div className="flex min-w-0 flex-col gap-2">
|
|
66
|
+
<p className="text-xs text-text-muted">{t('install.signIn.command')}</p>
|
|
67
|
+
<CommandBlock command={way.command} />
|
|
68
|
+
{canRun && (
|
|
69
|
+
<Button variant="secondary" size="sm" className="self-start" onClick={() => onRun(way.command)}>
|
|
70
|
+
<Icon icon={SquareTerminal} size={14} />
|
|
71
|
+
{t('install.run')}
|
|
72
|
+
</Button>
|
|
73
|
+
)}
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
case 'env': {
|
|
77
|
+
const note = providerKeyNote?.(way.variable) ?? null;
|
|
78
|
+
return (
|
|
79
|
+
<p className="text-xs text-text-muted">
|
|
80
|
+
<Trans
|
|
81
|
+
t={t}
|
|
82
|
+
i18nKey="install.signIn.env"
|
|
83
|
+
values={{ variable: way.variable }}
|
|
84
|
+
components={{ code: <code className="font-mono text-code text-text" /> }}
|
|
85
|
+
/>
|
|
86
|
+
{note !== null && ` ${note}`}
|
|
87
|
+
</p>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
case 'chat':
|
|
91
|
+
return <p className="text-xs text-text-muted">{t('install.signIn.chat')}</p>;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
interface InstallPageProps {
|
|
96
|
+
provider: ProviderInfo;
|
|
97
|
+
signedIn: boolean;
|
|
98
|
+
/* The host's own step, after the install of the CLI. */
|
|
99
|
+
extra: ProviderInstallStep | null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/* How to get a CLI that is not on the machine yet, step by step, until it is there and signed in. */
|
|
103
|
+
export function InstallPage({ provider, signedIn, extra }: InstallPageProps) {
|
|
104
|
+
const { t } = useTranslation('agent-providers');
|
|
105
|
+
const { id: scopeId } = useChatScope();
|
|
106
|
+
const host = useChatHost();
|
|
107
|
+
const useMachinePlatform = host.useMachinePlatform ?? noPlatform;
|
|
108
|
+
const machine = useMachinePlatform(scopeId) ?? pagePlatform();
|
|
109
|
+
const { offered, initial } = installPlatforms(provider, machine);
|
|
110
|
+
// Only a person's pick is kept, so a machine whose system arrives late still opens on its own.
|
|
111
|
+
const [chosen, setPlatform] = useState<ProviderPlatform | null>(null);
|
|
112
|
+
const platform = chosen ?? initial;
|
|
113
|
+
const install = provider.install;
|
|
114
|
+
const run = host.runProviderCommand;
|
|
115
|
+
const onRun = (command: string): void => run?.(scopeId, provider.kind, command);
|
|
116
|
+
const command = platform === null ? undefined : install?.commands[platform];
|
|
117
|
+
|
|
118
|
+
const steps: { title: string; done: boolean; body: ReactNode }[] = [];
|
|
119
|
+
steps.push({
|
|
120
|
+
title: t('install.install.title'),
|
|
121
|
+
done: provider.installed,
|
|
122
|
+
body: provider.installed ? (
|
|
123
|
+
<p className="text-xs text-text-muted">
|
|
124
|
+
{provider.version ? t('install.install.done', { version: provider.version }) : t('install.install.doneNoVersion')}
|
|
125
|
+
</p>
|
|
126
|
+
) : (
|
|
127
|
+
<>
|
|
128
|
+
{offered.length > 1 && platform !== null && (
|
|
129
|
+
<Segmented<ProviderPlatform>
|
|
130
|
+
value={platform}
|
|
131
|
+
label={t('install.platform')}
|
|
132
|
+
options={offered.map((id) => ({ id, label: t(`install.platforms.${id}`) }))}
|
|
133
|
+
onValueChange={setPlatform}
|
|
134
|
+
/>
|
|
135
|
+
)}
|
|
136
|
+
{command ? <CommandBlock command={command} /> : <p className="text-xs text-text-muted">{t('install.install.none')}</p>}
|
|
137
|
+
{((command && run) || install?.guideUrl) && (
|
|
138
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
139
|
+
{command && run && (
|
|
140
|
+
<Button variant="primary" onClick={() => onRun(command)}>
|
|
141
|
+
<Icon icon={SquareTerminal} size={14} />
|
|
142
|
+
{t('install.run')}
|
|
143
|
+
</Button>
|
|
144
|
+
)}
|
|
145
|
+
{install?.guideUrl && (
|
|
146
|
+
<Button href={install.guideUrl}>
|
|
147
|
+
{t('install.guide')}
|
|
148
|
+
<Icon icon={ExternalLink} size={14} />
|
|
149
|
+
</Button>
|
|
150
|
+
)}
|
|
151
|
+
</div>
|
|
152
|
+
)}
|
|
153
|
+
</>
|
|
154
|
+
)
|
|
155
|
+
});
|
|
156
|
+
if (extra !== null) {
|
|
157
|
+
steps.push(extra);
|
|
158
|
+
}
|
|
159
|
+
const ways = install?.signIn ?? [];
|
|
160
|
+
steps.push({
|
|
161
|
+
title: t('install.signIn.title'),
|
|
162
|
+
done: provider.installed && signedIn,
|
|
163
|
+
body:
|
|
164
|
+
ways.length === 0 ? (
|
|
165
|
+
<p className="text-xs text-text-muted">{t('install.signIn.unknown')}</p>
|
|
166
|
+
) : (
|
|
167
|
+
ways.map((way, index) => (
|
|
168
|
+
<div key={`${way.kind}-${index}`} className="flex min-w-0 flex-col gap-2">
|
|
169
|
+
{index > 0 && <span className="text-xs font-medium text-text-faint">{t('install.signIn.or')}</span>}
|
|
170
|
+
<SignInWay way={way} canRun={run !== undefined && provider.installed} onRun={onRun} />
|
|
171
|
+
</div>
|
|
172
|
+
))
|
|
173
|
+
)
|
|
174
|
+
});
|
|
175
|
+
steps.push({
|
|
176
|
+
title: t('install.start.title'),
|
|
177
|
+
done: false,
|
|
178
|
+
body: <p className="text-xs text-text-muted">{t('install.start.description', { provider: provider.name })}</p>
|
|
179
|
+
});
|
|
180
|
+
const states = installStepStates(steps.map((step) => step.done));
|
|
181
|
+
|
|
182
|
+
return (
|
|
183
|
+
<>
|
|
184
|
+
<header className="flex min-w-0 items-start gap-3">
|
|
185
|
+
<span className="grid size-10 shrink-0 place-items-center rounded-xl bg-surface-sunken">
|
|
186
|
+
<CliMark kind={provider.kind} size={20} />
|
|
187
|
+
</span>
|
|
188
|
+
<div className="flex min-w-0 grow flex-col">
|
|
189
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
190
|
+
<h3 className="truncate text-lg font-semibold text-text">{provider.name}</h3>
|
|
191
|
+
{provider.installed ? (
|
|
192
|
+
provider.version && (
|
|
193
|
+
<Pill tone="idle" mono>
|
|
194
|
+
{provider.version}
|
|
195
|
+
</Pill>
|
|
196
|
+
)
|
|
197
|
+
) : (
|
|
198
|
+
<Pill>{t('install.notInstalled')}</Pill>
|
|
199
|
+
)}
|
|
200
|
+
</div>
|
|
201
|
+
{install && <p className="text-xs text-text-muted">{install.description}</p>}
|
|
202
|
+
</div>
|
|
203
|
+
</header>
|
|
204
|
+
{install ? (
|
|
205
|
+
<ol aria-label={t('install.steps', { provider: provider.name })} className="flex min-w-0 flex-col gap-3">
|
|
206
|
+
{steps.map((step, index) => (
|
|
207
|
+
<Step key={index} index={index} state={states[index]!} title={step.title}>
|
|
208
|
+
{step.body}
|
|
209
|
+
</Step>
|
|
210
|
+
))}
|
|
211
|
+
</ol>
|
|
212
|
+
) : (
|
|
213
|
+
<p className="text-xs text-text-muted">{t('install.fallback', { provider: provider.name })}</p>
|
|
214
|
+
)}
|
|
215
|
+
<p className="flex min-w-0 items-center gap-2 text-xs text-text-muted">
|
|
216
|
+
<span aria-hidden className={clsx('size-2 shrink-0 rounded-full', provider.installed ? 'bg-status-idle' : 'bg-text-faint')} />
|
|
217
|
+
{provider.installed ? t('install.footer.found') : t('install.footer.missing')}
|
|
218
|
+
</p>
|
|
219
|
+
</>
|
|
220
|
+
);
|
|
221
|
+
}
|
|
@@ -1,22 +1,25 @@
|
|
|
1
1
|
import { useMemo, useState, type ReactNode } from 'react';
|
|
2
2
|
import clsx from 'clsx';
|
|
3
3
|
import { useTranslation } from 'react-i18next';
|
|
4
|
-
import { Plus
|
|
4
|
+
import { Plus } from 'lucide-react';
|
|
5
5
|
import type { AgentKind, ProviderInfo } from '@adecore/agent-contracts';
|
|
6
6
|
import { AccountDot } from '../agents/AccountDot';
|
|
7
7
|
import { ACCOUNT_TONE_CLASSES, accountLoginOf, accountName, accountsOfKind, accountStatusLine, type AccountEntry } from '../agents/accounts';
|
|
8
|
-
import { ErrorBoundary,
|
|
8
|
+
import { ErrorBoundary, IconButton, SectionLabel, Skeleton } from '@adecore/ui';
|
|
9
9
|
import { MasterDetail } from '@adecore/ui/settings';
|
|
10
10
|
import { useChatHost } from '../host';
|
|
11
11
|
import { useChatScope } from '../scope';
|
|
12
12
|
import { AccountDetail } from './AccountDetail';
|
|
13
13
|
import { AddAccountForm } from './AddAccountForm';
|
|
14
14
|
import { CliDetail } from './CliDetail';
|
|
15
|
+
import { InstallPage } from './InstallPage';
|
|
16
|
+
import type { ProviderInstallStep } from './install-steps';
|
|
15
17
|
import { CliMark } from './parts';
|
|
16
18
|
import { useProviderAccounts } from '../state/provider-accounts';
|
|
17
19
|
import { useProviders } from '../state/providers';
|
|
18
20
|
|
|
19
|
-
|
|
21
|
+
// `install` is a CLI picked while it was missing: its install page stays until it is installed and signed in.
|
|
22
|
+
type Picked = { kind: 'cli'; cli: AgentKind } | { kind: 'install'; cli: AgentKind } | { kind: 'account'; id: string } | { kind: 'add'; cli: AgentKind };
|
|
20
23
|
|
|
21
24
|
/* A row of the list with a control of its own beside the part that picks it, so the two never nest. */
|
|
22
25
|
function ListRow({
|
|
@@ -54,10 +57,12 @@ interface ProvidersPaneProps {
|
|
|
54
57
|
detailOf?(provider: ProviderInfo): ReactNode | null;
|
|
55
58
|
/* False leaves "Defaults for new chats" out of a CLI's detail, for an app that decides those elsewhere. */
|
|
56
59
|
defaults?: boolean;
|
|
60
|
+
/* A step of the app's own on the install page of a CLI, after its install, such as a server its chat needs; null adds none. */
|
|
61
|
+
installStepOf?(provider: ProviderInfo): ProviderInstallStep | null;
|
|
57
62
|
}
|
|
58
63
|
|
|
59
64
|
/* The agent CLIs of the host in scope, each with its accounts, beside the detail of what is picked. */
|
|
60
|
-
export function ProvidersPane({ target = null, detailOf, defaults = true }: ProvidersPaneProps) {
|
|
65
|
+
export function ProvidersPane({ target = null, detailOf, defaults = true, installStepOf }: ProvidersPaneProps) {
|
|
61
66
|
const { t } = useTranslation('agent-providers');
|
|
62
67
|
const { id: scopeId } = useChatScope();
|
|
63
68
|
const providers = useProviders((s) => s.providers);
|
|
@@ -80,7 +85,8 @@ export function ProvidersPane({ target = null, detailOf, defaults = true }: Prov
|
|
|
80
85
|
picked !== null && (picked.kind !== 'account' || pickedEntry !== null) ? picked : installed[0] ? { kind: 'cli', cli: installed[0].kind } : null;
|
|
81
86
|
const currentAccount = current?.kind === 'account' ? accounts?.accounts[current.id] : undefined;
|
|
82
87
|
const currentCli = current === null ? null : current.kind === 'account' ? (currentAccount?.kind as AgentKind | undefined) : current.cli;
|
|
83
|
-
const provider = installed.find((entry) => entry.kind === currentCli) ?? null;
|
|
88
|
+
const provider = (current?.kind === 'install' ? providers : installed).find((entry) => entry.kind === currentCli) ?? null;
|
|
89
|
+
const picksCli = (kind: AgentKind): boolean => (current?.kind === 'cli' || current?.kind === 'install') && current.cli === kind;
|
|
84
90
|
|
|
85
91
|
// A search result lands on a row of a CLI's detail, so a picked account makes way for its CLI.
|
|
86
92
|
const [seenTarget, setSeenTarget] = useState(target);
|
|
@@ -105,7 +111,7 @@ export function ProvidersPane({ target = null, detailOf, defaults = true }: Prov
|
|
|
105
111
|
return (
|
|
106
112
|
<div key={cli.kind} className="flex min-w-0 shrink-0 flex-col gap-0.5 pt-1">
|
|
107
113
|
<ListRow
|
|
108
|
-
selected={
|
|
114
|
+
selected={picksCli(cli.kind)}
|
|
109
115
|
onSelect={() => setPicked({ kind: 'cli', cli: cli.kind })}
|
|
110
116
|
className="pr-1"
|
|
111
117
|
trailing={
|
|
@@ -164,13 +170,13 @@ export function ProvidersPane({ target = null, detailOf, defaults = true }: Prov
|
|
|
164
170
|
<div className="mt-1.5 flex min-w-0 shrink-0 flex-col border-t border-border pt-2.5 pb-2">
|
|
165
171
|
<SectionLabel className="px-3 pt-1 pb-1.5">{t('list.notInstalled')}</SectionLabel>
|
|
166
172
|
{missing.map((cli) => (
|
|
167
|
-
<
|
|
168
|
-
<
|
|
169
|
-
<span className="min-w-0 grow truncate">{cli.name}</span>
|
|
173
|
+
<ListRow key={cli.kind} selected={picksCli(cli.kind)} onSelect={() => setPicked({ kind: 'install', cli: cli.kind })}>
|
|
174
|
+
<CliMark kind={cli.kind} className="opacity-60" />
|
|
175
|
+
<span className="min-w-0 grow truncate text-xs font-medium text-text-muted">{cli.name}</span>
|
|
170
176
|
<span className="shrink-0 text-xs text-text-faint">{t('list.missing')}</span>
|
|
171
|
-
</
|
|
177
|
+
</ListRow>
|
|
172
178
|
))}
|
|
173
|
-
<p className="mt-1 px-3 text-xs text-text-faint">{t('list.notInstalledNote')}</p>
|
|
179
|
+
{missing.every((cli) => cli.install === undefined) && <p className="mt-1 px-3 text-xs text-text-faint">{t('list.notInstalledNote')}</p>}
|
|
174
180
|
</div>
|
|
175
181
|
)}
|
|
176
182
|
</>
|
|
@@ -180,7 +186,16 @@ export function ProvidersPane({ target = null, detailOf, defaults = true }: Prov
|
|
|
180
186
|
if (current === null || provider === null) {
|
|
181
187
|
return null;
|
|
182
188
|
}
|
|
183
|
-
|
|
189
|
+
if (current.kind === 'install') {
|
|
190
|
+
const extra = installStepOf?.(provider) ?? null;
|
|
191
|
+
const statuses = entriesOf(provider.kind).map((entry) => entry.status?.state);
|
|
192
|
+
// Without accounts to read, nothing says the CLI is signed out, so being there is all it takes.
|
|
193
|
+
const signedIn = statuses.length === 0 || statuses.includes('ready');
|
|
194
|
+
if (!provider.installed || !signedIn || (extra !== null && !extra.done)) {
|
|
195
|
+
return <InstallPage key={provider.kind} provider={provider} signedIn={signedIn} extra={extra} />;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
const own = current.kind === 'cli' || current.kind === 'install' ? (detailOf?.(provider) ?? null) : null;
|
|
184
199
|
if (own !== null) {
|
|
185
200
|
return own;
|
|
186
201
|
}
|
package/src/providers/index.ts
CHANGED
|
@@ -8,4 +8,5 @@ export { modelName, useModelName, shortModelName } from '../agents/model-name';
|
|
|
8
8
|
export { carryOptions } from '../agents/model-options';
|
|
9
9
|
export { type ModelRequirement } from '../agents/model-requirement';
|
|
10
10
|
export { ProvidersPane } from './ProvidersPane';
|
|
11
|
+
export type { ProviderInstallStep } from './install-steps';
|
|
11
12
|
export { CliMark, CliTile } from './parts';
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { ProviderInfo, ProviderPlatform } from '@adecore/agent-contracts';
|
|
3
|
+
import { isApplePlatform } from '@adecore/ui';
|
|
4
|
+
|
|
5
|
+
/* A step of the host's own on a provider's install page, such as a server its chat needs beside the CLI. */
|
|
6
|
+
export interface ProviderInstallStep {
|
|
7
|
+
title: string;
|
|
8
|
+
done: boolean;
|
|
9
|
+
/* Drawn under the title; the page numbers the step and outlines it while it is the current one. */
|
|
10
|
+
body: ReactNode;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type InstallStepState = 'done' | 'current' | 'later';
|
|
14
|
+
|
|
15
|
+
const PLATFORMS: readonly ProviderPlatform[] = ['darwin', 'linux', 'win32'];
|
|
16
|
+
|
|
17
|
+
/* The first step not done is the current one. */
|
|
18
|
+
export function installStepStates(done: readonly boolean[]): InstallStepState[] {
|
|
19
|
+
const first = done.indexOf(false);
|
|
20
|
+
return done.map((value, index) => (value ? 'done' : index === first ? 'current' : 'later'));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/* The system of the page, for a host that does not say which system its machine runs. */
|
|
24
|
+
export function pagePlatform(): ProviderPlatform {
|
|
25
|
+
if (isApplePlatform()) {
|
|
26
|
+
return 'darwin';
|
|
27
|
+
}
|
|
28
|
+
return typeof navigator !== 'undefined' && /windows/i.test(navigator.userAgent) ? 'win32' : 'linux';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/* The systems a provider has a command for, and the one the page opens on: the machine's when it has one there. */
|
|
32
|
+
export function installPlatforms(provider: ProviderInfo, machine: ProviderPlatform): { offered: ProviderPlatform[]; initial: ProviderPlatform | null } {
|
|
33
|
+
const offered = PLATFORMS.filter((platform) => Boolean(provider.install?.commands[platform]));
|
|
34
|
+
return { offered, initial: offered.includes(machine) ? machine : (offered[0] ?? null) };
|
|
35
|
+
}
|