Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

36 changes: 36 additions & 0 deletions apps/web/src/assets/NOTICE
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,42 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

Agent setup marks
-----------------

The API-key agent setup tabs wear the following agent marks, each pinned to the
upstream commit whose artwork it began from:

- omp.svg
https://github.com/can1357/oh-my-pi/blob/565d53515b54df32fada2564d1fe9caf1a17b738/packages/collab-web/public/favicon.svg
- opencode.svg
https://github.com/anomalyco/opencode/blob/1251a870cb384543c150c4a72fb101b55eec971b/packages/identity/mark.svg
- vscode.svg
https://github.com/devicons/devicon/blob/65476530e1afea9b1f850efb1cc0efec19ee0ef0/icons/vscode/vscode-original.svg
- zed.svg
https://github.com/zed-industries/zed/blob/ccc939124fa2f366b3029926447fd0a0c46a85c7/assets/icons/logo_96.svg

All four get the shared icon conveniences (a `<title>`, `height`/`width`
of 1em, and `style="flex:none"`). Beyond that:

- omp.svg is verbatim: the dark rounded chip and gradient Pi glyph as
upstream publishes it.
- opencode.svg is verbatim except that the outer SVG wrapper and the inert
`prefers-color-scheme` style block are dropped.
- vscode.svg is the upstream mark with its two Gaussian-blur drop-shadow
filters dropped; they blur a 0.25-opacity black halo past the silhouette
that no surface in this app wants. Masks, fills, and the gradient sheen
are as upstream ships them.
- zed.svg is the official white glyph rescaled from 96 to 24 units and
filled with the brand blue #084CCF (the colour zed.dev paints its own
material in, https://zed.dev) so the mark keeps reading on the app's
WinUI dark surface, where the wall asset's white-on-transparent glyph
would vanish. The shape is otherwise unmodified.

Devicon is MIT-licensed. oh-my-pi and opencode are MIT-licensed. Zed's editor
repository is GPL-licensed; the logo is its author's trademark, reproduced for
identification as the other marks here are.

Meslo LG S
----------

Expand Down
1 change: 1 addition & 0 deletions apps/web/src/assets/omp.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions apps/web/src/assets/opencode.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions apps/web/src/assets/vscode.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions apps/web/src/assets/zed.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
76 changes: 54 additions & 22 deletions apps/web/src/components/api-keys/agent-setup-card.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useMemo, useState } from 'react';
import type { ReactElement } from 'react';

import {
buildAgentClaudeSnippet,
Expand All @@ -15,6 +16,10 @@ import { agentSetupCommand, useAgentSetup } from './use-agent-setup';
import type { ApiKey, ControlPlaneModel } from '../../api/types';
import claudeIconUrl from '../../assets/claude-color.svg';
import codexIconUrl from '../../assets/codex.svg';
import ompIconUrl from '../../assets/omp.svg';
import opencodeIconUrl from '../../assets/opencode.svg';
import vscodeIconUrl from '../../assets/vscode.svg';
import zedIconUrl from '../../assets/zed.svg';
import { fluentComponents } from '../../fluent';
import { Trans, useTranslation } from '../../i18n/translation';
import { filterModelOptions } from '../../lib/model-query';
Expand All @@ -28,8 +33,12 @@ import { SwitchSetting } from '../ui/switch-setting';
import type { ClipboardCopy } from '../ui/use-copy-to-clipboard';

const { Button, Field, Option, Tab, TabList, Text } = fluentComponents;
type Agent = 'claude' | 'codex';
type Agent = 'claude' | 'codex' | 'omp' | 'vscode' | 'zed' | 'opencode';
type Platform = AgentSetupPlatform;
// The harness agents convert every model the gateway serves, so they take no
// model selection or per-agent configuration.
type ConfigurableAgent = Extract<Agent, 'claude' | 'codex'>;
const isConfigurableAgent = (agent: Agent): agent is ConfigurableAgent => agent === 'claude' || agent === 'codex';
// The option that stands for no override. Model overrides reject NUL at the
// gateway boundary, so this UI-only value cannot collide with an opaque model
// id.
Expand Down Expand Up @@ -75,9 +84,13 @@ export function AgentSetupCard({ clipboard, initialApiKeyId, initialError, initi

<div className={`grid ${PANE_GAP_CLASS} min-w-0 grid-cols-[190px_minmax(0,1fr)] max-[680px]:grid-cols-1`}>
<nav className="grid content-start">
<TabList aria-label={t('dashboard.apiKeys.agentSetup.agent')} onTabSelect={(_, data) => setAgent(data.value === 'codex' ? 'codex' : 'claude')} selectedValue={agent} vertical>
<AgentTab icon={claudeIconUrl} label={t('dashboard.apiKeys.configuration.claudeCode')} value="claude" />
<AgentTab icon={codexIconUrl} label={t('dashboard.apiKeys.configuration.codex')} value="codex" />
<TabList aria-label={t('dashboard.apiKeys.agentSetup.agent')} onTabSelect={(_, data) => setAgent(data.value as Agent)} selectedValue={agent} vertical>
<AgentTab icon={<img alt="" className="h-4 w-4" src={claudeIconUrl} />} label={t('dashboard.apiKeys.configuration.claudeCode')} value="claude" />
<AgentTab icon={<img alt="" className="h-4 w-4" src={codexIconUrl} />} label={t('dashboard.apiKeys.configuration.codex')} value="codex" />
<AgentTab icon={<img alt="" className="h-4 w-4" src={ompIconUrl} />} label={t('dashboard.apiKeys.configuration.omp')} value="omp" />
<AgentTab icon={<img alt="" className="h-4 w-4" src={vscodeIconUrl} />} label={t('dashboard.apiKeys.configuration.vscode')} value="vscode" />
<AgentTab icon={<img alt="" className="h-4 w-4" src={zedIconUrl} />} label={t('dashboard.apiKeys.configuration.zed')} value="zed" />
<AgentTab icon={<img alt="" className="h-4 w-4" src={opencodeIconUrl} />} label={t('dashboard.apiKeys.configuration.opencode')} value="opencode" />
</TabList>
</nav>

Expand All @@ -91,10 +104,10 @@ export function AgentSetupCard({ clipboard, initialApiKeyId, initialError, initi
>{setup.createError}</OutcomeMessageBar>
: setup.error && <OutcomeMessageBar onDismiss={setup.dismissError}>{setup.error}</OutcomeMessageBar>}

<section className={SECTION_STACK_CLASS}>
{isConfigurableAgent(agent) && <section className={SECTION_STACK_CLASS}>
<SectionHeader level={3} title={t('dashboard.apiKeys.agentSetup.modelSelection')} />
<AgentConfigurationFields agent={agent} configuration={setup.draft} models={models} onChange={setup.updateDraft} />
</section>
</section>}

{view === 'snippets' && selectedKey
? <AgentConfigSnippets agent={agent} apiKey={selectedKey.key} configuration={setup.draft} clipboard={clipboard} onPlatformChange={setPlatform} platform={platform} />
Expand Down Expand Up @@ -161,30 +174,49 @@ function AgentConfigSnippets({ agent, apiKey, clipboard, configuration, onPlatfo
<CodeBlock code={snippet} copyOutcome={clipboard.outcomeFor('agent-snippet-claude')} language="json" onCopy={() => clipboard.copy(snippet, 'agent-snippet-claude')} />
</div>;
}
// Both snippets are one platform's pair -- the config's auth command reads the
// file the credential snippet writes -- so one choice drives them and each
// block carries the picker, whichever the reader reaches first.
const config = buildAgentCodexSnippet(origin, configuration.codex, platform);
const credential = platform === 'windows' ? codexWindowsCredentialSnippet(apiKey) : codexUnixCredentialSnippet(apiKey);
const configTag = platform === 'windows' ? 'agent-snippet-codex-windows' : 'agent-snippet-codex-unix';
const credentialTag = `${configTag}-token`;
const tabs = <PlatformTabs onChange={onPlatformChange} platform={platform} />;
return <div className="grid gap-3 border-t border-t-solid border-fui-divider pt-4">
if (agent === 'codex') {
// Both snippets are one platform's pair -- the config's auth command reads the
// file the credential snippet writes -- so one choice drives them and each
// block carries the picker, whichever the reader reaches first.
const config = buildAgentCodexSnippet(origin, configuration.codex, platform);
const credential = platform === 'windows' ? codexWindowsCredentialSnippet(apiKey) : codexUnixCredentialSnippet(apiKey);
const configTag = platform === 'windows' ? 'agent-snippet-codex-windows' : 'agent-snippet-codex-unix';
const credentialTag = `${configTag}-token`;
const tabs = <PlatformTabs onChange={onPlatformChange} platform={platform} />;
return <div className="grid gap-3 border-t border-t-solid border-fui-divider pt-4">
<Text size={200} className="text-fui-fg2">
<Trans components={{ path: <code className="font-mono mono-size-xs" /> }} i18nKey={platform === 'windows' ? 'dashboard.apiKeys.configuration.codexConfigHintWindows' : 'dashboard.apiKeys.configuration.codexConfigHint'} />
</Text>
<CodeBlock code={config} copyOutcome={clipboard.outcomeFor(configTag)} header={tabs} language="toml" onCopy={() => clipboard.copy(config, configTag)} />
<Text size={200} className="text-fui-fg2">{t('dashboard.apiKeys.configuration.codexAuthHint')}</Text>
<CodeBlock code={credential} copyOutcome={clipboard.outcomeFor(credentialTag)} header={tabs} language={platform === 'windows' ? 'powershell' : 'bash'} onCopy={() => clipboard.copy(credential, credentialTag)} />
</div>;
}
// The harness agents have no config file to paste: the setup script fetches
// the model list and writes (omp, zed, opencode) or prints (vscode) the
// settings, so the snippet view just points at where the installer puts the
// result.
const harnessHints = {
omp: 'dashboard.apiKeys.configuration.ompHint',
vscode: 'dashboard.apiKeys.configuration.vscodeHint',
zed: 'dashboard.apiKeys.configuration.zedHint',
opencode: 'dashboard.apiKeys.configuration.opencodeHint',
} as const;
const hintKey = harnessHints[agent];
return <div className="grid gap-2 border-t border-t-solid border-fui-divider pt-4">
<Text size={200} className="text-fui-fg2">
<Trans components={{ path: <code className="font-mono mono-size-xs" /> }} i18nKey={platform === 'windows' ? 'dashboard.apiKeys.configuration.codexConfigHintWindows' : 'dashboard.apiKeys.configuration.codexConfigHint'} />
<Trans components={{ path: <code className="font-mono mono-size-xs" /> }} i18nKey={hintKey} />
</Text>
<CodeBlock code={config} copyOutcome={clipboard.outcomeFor(configTag)} header={tabs} language="toml" onCopy={() => clipboard.copy(config, configTag)} />
<Text size={200} className="text-fui-fg2">{t('dashboard.apiKeys.configuration.codexAuthHint')}</Text>
<CodeBlock code={credential} copyOutcome={clipboard.outcomeFor(credentialTag)} header={tabs} language={platform === 'windows' ? 'powershell' : 'bash'} onCopy={() => clipboard.copy(credential, credentialTag)} />
<Text size={200} className="text-fui-fg2">{t('dashboard.apiKeys.agentSetup.harnessHint')}</Text>
</div>;
}

function AgentTab({ icon, label, value }: { icon: string; label: string; value: Agent }) {
return <Tab value={value} icon={<img alt="" className="h-4 w-4" src={icon} />}>{label}</Tab>;
function AgentTab({ icon, label, value }: { icon: ReactElement; label: string; value: Agent }) {
return <Tab value={value} icon={icon}>{label}</Tab>;
}

function AgentConfigurationFields({ agent, configuration, models, onChange }: {
agent: Agent;
agent: ConfigurableAgent;
configuration: AgentSetupConfiguration;
models: ControlPlaneModel[];
onChange: (update: (current: AgentSetupConfiguration) => AgentSetupConfiguration) => void;
Expand Down
Loading