diff --git a/.changeset/30896-connect-agent-client-picker.md b/.changeset/30896-connect-agent-client-picker.md new file mode 100644 index 00000000000..6b309858636 --- /dev/null +++ b/.changeset/30896-connect-agent-client-picker.md @@ -0,0 +1,11 @@ +--- +'@object-ui/app-shell': patch +'@object-ui/i18n': patch +--- + +feat(app-shell): Connect an Agent asks which AI client you use and shows only that client's steps + +Clause-②: no +Metadata-protocol: none + +The Connect an Agent page no longer lays out every client at once. Under the endpoint and the SKILL.md download, which stay on top for everyone, the page asks which AI assistant you use (Claude, Codex, Cursor, WorkBuddy, Doubao Work, QwenWork) and shows that one's steps; switching is one click, and this browser remembers your choice (`os:pref:connect.client` in the console's workspace preference store). The API key form for scripts and CI moves out of the client list into a collapsed "Advanced: scripts and CI" section at the bottom. The three new strings ship in all ten console languages. diff --git a/ui/packages/app-shell/src/console/connect/ConnectAgentWidget.keyName-13355.test.tsx b/ui/packages/app-shell/src/console/connect/ConnectAgentWidget.keyName-13355.test.tsx index 6f3b51c238a..2f33d537a78 100644 --- a/ui/packages/app-shell/src/console/connect/ConnectAgentWidget.keyName-13355.test.tsx +++ b/ui/packages/app-shell/src/console/connect/ConnectAgentWidget.keyName-13355.test.tsx @@ -17,6 +17,11 @@ * - §3 confirming posts exactly the typed (trimmed) name, never a default, and * the key is revealed once. * + * The key form lives in the collapsed "Advanced: scripts and CI" section, so + * those cases open it first. The last block holds the page's shape: the + * endpoint and SKILL.md on top, a picker that shows one client's steps and + * remembers the choice on this browser, and the key form collapsed. + * * `fetch` is stubbed by path; `t` echoes the KEY (with the `name` option * appended when one is passed), as the sibling suites do, so the assertions * read which string the widget asked for and with which name. @@ -49,6 +54,7 @@ function keyPosts(): unknown[] { } beforeEach(() => { + window.localStorage.clear(); fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => { const url = String(input); if (url.includes('/api/v1/discovery')) { @@ -74,11 +80,35 @@ afterEach(() => { vi.unstubAllGlobals(); }); +/** Render and wait for discovery to settle: the client picker is up. */ +async function renderPage() { + const view = render(); + await screen.findByRole('tablist'); + return view; +} + +/** The API key form sits in the collapsed Advanced section; open it and return its name box. */ async function renderSettled() { - render(); + await renderPage(); + fireEvent.click(screen.getByRole('button', { name: /«connectAgent\.advanced\.title»/ })); return screen.findByPlaceholderText('«connectAgent.apiKey.namePlaceholder»'); } +/** Pick a client in the picker (Radix tabs activate on mouse-down). */ +function pickClient(label: string) { + fireEvent.mouseDown(screen.getByRole('tab', { name: label }), { button: 0 }); +} + +const PICKER_LABELS = [ + 'Claude', + 'Codex', + 'Cursor', + 'WorkBuddy', + // Product names a pack may write in its own script (zh: 豆包工作 / 千问办公). + '«connectAgent.doubao.title»', + '«connectAgent.qwenwork.title»', +]; + const createButton = () => screen.getByRole('button', { name: '«connectAgent.apiKey.mint»' }); describe('Connect an Agent mints a key only under a typed, confirmed name (objectui#13355)', () => { @@ -143,53 +173,48 @@ describe('Connect an Agent offers the Chinese clients after the international on const copyable = () => Array.from(document.querySelectorAll('code, dd')).map((el) => el.textContent ?? ''); - it('renders one card list, international clients first, then WorkBuddy, Doubao Work and QwenWork', async () => { - await renderSettled(); - const order = [ - // One Claude card carries both ways in: the connector, then the CLI. - 'Claude', - 'claude.ai / Claude Desktop', - 'Claude Code', - 'Codex', - 'Cursor', - 'WorkBuddy', - // Product names a pack may write in its own script (zh: 豆包工作 / 千问办公). - '«connectAgent.doubao.title»', - '«connectAgent.qwenwork.title»', - ].map((title) => screen.getByText(title)); - for (let i = 1; i < order.length; i++) { - const follows = order[i - 1].compareDocumentPosition(order[i]) & Node.DOCUMENT_POSITION_FOLLOWING; - expect(follows, `${order[i].textContent} renders after ${order[i - 1].textContent}`).toBeTruthy(); - } - // One list: no group headings split the cards. - expect(document.querySelectorAll('h3')).toHaveLength(0); + it('offers one picker, international clients first, then WorkBuddy, Doubao Work and QwenWork', async () => { + await renderPage(); + expect(screen.getAllByRole('tab').map((tab) => tab.textContent)).toEqual(PICKER_LABELS); for (const dropped of ['DuMate', 'VS Code / Copilot']) { expect(screen.queryByText(dropped)).not.toBeInTheDocument(); } - for (const key of ['codex', 'workbuddy', 'doubao', 'qwenwork']) { - expect(screen.getByText(`«connectAgent.${key}.body»`)).toBeInTheDocument(); + // Claude carries both ways in: the connector, then the CLI. + const desktop = screen.getByText('claude.ai / Claude Desktop'); + const code = screen.getByText('Claude Code'); + expect(desktop.compareDocumentPosition(code) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy(); + for (const [label, key] of [ + ['Codex', 'codex'], + ['WorkBuddy', 'workbuddy'], + ['«connectAgent.doubao.title»', 'doubao'], + ['«connectAgent.qwenwork.title»', 'qwenwork'], + ]) { + pickClient(label); + expect(await screen.findByText(`«connectAgent.${key}.body»`)).toBeInTheDocument(); expect(screen.getByText(`«connectAgent.${key}.hint»`)).toBeInTheDocument(); } }); it('hands each client its own transport spelling for this deployment\'s MCP URL', async () => { - await renderSettled(); - const values = copyable(); + await renderPage(); const json = (type: string) => JSON.stringify({ mcpServers: { objectstack: { type, url: MCP_URL } } }, null, 2); // WorkBuddy's mcp.json and QwenWork's JSON import spell Streamable HTTP differently. - expect(values).toContain(json('streamableHttp')); - expect(values).toContain(json('streamable-http')); - // The Codex app and Doubao Work are dialog forms: name, transport and URL, each copyable. - expect(values.filter((v) => v === 'objectstack')).toHaveLength(2); - expect(values).toContain('Streamable HTTP'); - expect(values).toContain('HTTP'); - expect(values.filter((v) => v === MCP_URL).length).toBeGreaterThanOrEqual(4); - // The Codex card describes the app's form, not a config.toml snippet. - expect(values.some((v) => v.includes('[mcp_servers.objectstack]'))).toBe(false); + pickClient('WorkBuddy'); + await waitFor(() => expect(copyable()).toContain(json('streamableHttp'))); + pickClient('«connectAgent.qwenwork.title»'); + await waitFor(() => expect(copyable()).toContain(json('streamable-http'))); + // The Codex app and Doubao Work are dialog forms: name, transport and URL, each copyable + // (after the endpoint's own URL on top). + pickClient('Codex'); + await waitFor(() => expect(copyable()).toEqual([MCP_URL, 'objectstack', 'Streamable HTTP', MCP_URL])); + // The Codex steps describe the app's form, not a config.toml snippet. + expect(copyable().some((v) => v.includes('[mcp_servers.objectstack]'))).toBe(false); + pickClient('«connectAgent.doubao.title»'); + await waitFor(() => expect(copyable()).toEqual([MCP_URL, 'objectstack', 'HTTP', MCP_URL])); }); - it('reveals a minted key in the Authorization: Bearer form the cards point to', async () => { + it('reveals a minted key in the Authorization: Bearer form the steps point to', async () => { const input = await renderSettled(); fireEvent.change(input, { target: { value: 'ci-agent' } }); fireEvent.click(createButton()); @@ -202,3 +227,55 @@ describe('Connect an Agent offers the Chinese clients after the international on expect(screen.getByText(`x-api-key: ${MINTED_KEY}`)).toBeInTheDocument(); }); }); + +describe('Connect an Agent asks which client the person uses and shows only those steps', () => { + const PREFERENCE_KEY = 'os:pref:connect.client'; + const CLAUDE_CODE = /^claude mcp add --transport http objectstack /; + + it('§1 the endpoint and SKILL.md stay on top; one client\'s steps show; the API key form is collapsed', async () => { + await renderPage(); + // On top, for everyone: the endpoint and the skill download. + const endpoint = screen.getByText('«connectAgent.url.title»'); + const picker = screen.getByText('«connectAgent.client.title»'); + expect(endpoint.compareDocumentPosition(picker) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy(); + expect(screen.getByRole('link', { name: /«connectAgent\.url\.downloadSkill»/ })).toHaveAttribute( + 'download', + 'SKILL.md', + ); + // A person who never chose sees Claude, and only Claude. + expect(screen.getByRole('tab', { name: 'Claude' })).toHaveAttribute('aria-selected', 'true'); + expect(screen.getByText(CLAUDE_CODE)).toBeInTheDocument(); + for (const key of ['codex', 'cursor', 'workbuddy', 'doubao', 'qwenwork']) { + expect(screen.queryByText(`«connectAgent.${key}.body»`)).not.toBeInTheDocument(); + } + // Scripts and CI: the API key form waits behind the collapsed Advanced section, after the picker. + const advanced = screen.getByRole('button', { name: /«connectAgent\.advanced\.title»/ }); + expect(advanced).toHaveAttribute('aria-expanded', 'false'); + expect(picker.compareDocumentPosition(advanced) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy(); + expect(screen.queryByText('«connectAgent.apiKey.title»')).not.toBeInTheDocument(); + expect(screen.queryByPlaceholderText('«connectAgent.apiKey.namePlaceholder»')).not.toBeInTheDocument(); + fireEvent.click(advanced); + expect(advanced).toHaveAttribute('aria-expanded', 'true'); + expect(await screen.findByText('«connectAgent.apiKey.title»')).toBeInTheDocument(); + }); + + it('§2 one click switches clients, and this browser remembers the choice', async () => { + const first = await renderPage(); + pickClient('Cursor'); + expect(await screen.findByText('«connectAgent.cursor.body»')).toBeInTheDocument(); + expect(screen.queryByText(CLAUDE_CODE)).not.toBeInTheDocument(); + expect(JSON.parse(window.localStorage.getItem(PREFERENCE_KEY) ?? 'null')).toEqual({ client: 'cursor', v: 1 }); + + first.unmount(); + await renderPage(); + expect(screen.getByRole('tab', { name: 'Cursor' })).toHaveAttribute('aria-selected', 'true'); + expect(screen.getByText('«connectAgent.cursor.body»')).toBeInTheDocument(); + }); + + it('§3 a stored client the page does not offer reads as no choice', async () => { + window.localStorage.setItem(PREFERENCE_KEY, JSON.stringify({ client: 'copilot', v: 1 })); + await renderPage(); + expect(screen.getByRole('tab', { name: 'Claude' })).toHaveAttribute('aria-selected', 'true'); + expect(screen.getByText(CLAUDE_CODE)).toBeInTheDocument(); + }); +}); diff --git a/ui/packages/app-shell/src/console/connect/ConnectAgentWidget.tsx b/ui/packages/app-shell/src/console/connect/ConnectAgentWidget.tsx index 37a73b61682..a5b9a50dc40 100644 --- a/ui/packages/app-shell/src/console/connect/ConnectAgentWidget.tsx +++ b/ui/packages/app-shell/src/console/connect/ConnectAgentWidget.tsx @@ -10,20 +10,24 @@ * for people who don't know what MCP is: * * 1. the environment's MCP URL (from `/discovery` → `routes.mcp`) + copy, - * 2. per-client connect cards with copy-paste-ready content — one Claude - * card (claude.ai / Claude Desktop and Claude Code), the Codex app and - * Cursor deeplink first, then the Chinese clients WorkBuddy, Doubao Work - * and QwenWork. - * OAuth is the default story on every card (the deployment is its own + * 2. the portable SKILL.md download (`GET /api/v1/mcp/skill`), beside the + * URL, for everyone, + * 3. a picker that asks which client the person uses and shows only that + * client's steps — Claude (claude.ai / Claude Desktop and Claude Code), + * the Codex app, Cursor, then the Chinese clients WorkBuddy, Doubao Work + * and QwenWork. The choice is the reader's, kept on this browser + * (`CONNECT_CLIENT_PREFERENCE`); switching is one click. + * OAuth is the default story for every client (the deployment is its own * authorization server; you sign in as yourself); the Chinese clients' - * cards name the API-key header as the fallback for a client version + * steps name the API-key header as the fallback for a client version * without the OAuth flow, - * 3. the portable SKILL.md download (`GET /api/v1/mcp/skill`), * 4. API-key minting for headless callers (CI, machine accounts) via the - * existing `POST /api/v1/keys` — shown once, copy-once. As on Setup → - * API Keys → Create Key, the key must be named (Create stays disabled - * on a blank name; nothing is minted under a default name), and an - * in-app confirm naming the key stands between the click and the mint. + * existing `POST /api/v1/keys`, in a collapsed "Advanced: scripts and CI" + * section at the bottom — no client, so not a peer of the clients. + * Shown once, copy-once. As on Setup → API Keys → Create Key, the key + * must be named (Create stays disabled on a blank name; nothing is + * minted under a default name), and an in-app confirm naming the key + * stands between the click and the mint. * * When `/discovery` does not advertise `routes.mcp` (deployment opted out via * `OS_MCP_SERVER_ENABLED=false`) the widget renders a disabled empty state — @@ -46,12 +50,21 @@ import { CardTitle, Badge, Button, + Collapsible, + CollapsibleContent, + CollapsibleTrigger, Input, + cn, Skeleton, + Tabs, + TabsContent, + TabsList, + TabsTrigger, } from '@object-ui/components'; import { Bot, Check, + ChevronRight, Copy, Download, ExternalLink, @@ -61,6 +74,14 @@ import { import { TokenStorage } from '@object-ui/auth'; import { useObjectTranslation } from '@object-ui/i18n'; import { ComponentRegistry } from '@object-ui/core'; +import { useWorkspacePreference } from '../../utils/workspacePreference.js'; +import { + CONNECT_CLIENT_IDS, + CONNECT_CLIENT_PREFERENCE, + DEFAULT_CONNECT_CLIENT, + isConnectClientId, + type ConnectClientId, +} from './connectClientPreference.js'; const SERVER_URL = (import.meta.env.VITE_SERVER_URL || '').replace(/\/$/, ''); @@ -144,6 +165,12 @@ export function ConnectAgent() { const [loading, setLoading] = useState(true); const [mcpUrl, setMcpUrl] = useState(null); const [loadError, setLoadError] = useState(null); + const [client, setClient] = useWorkspacePreference( + CONNECT_CLIENT_PREFERENCE, + 'user', + DEFAULT_CONNECT_CLIENT, + ); + const [advancedOpen, setAdvancedOpen] = useState(false); // API-key minting state const [keyName, setKeyName] = useState(''); @@ -179,6 +206,17 @@ export function ConnectAgent() { return `cursor://anysphere.cursor-deeplink/mcp/install?name=objectstack&config=${encodeURIComponent(config)}`; }, [mcpUrl]); + // Brand names stay as the client spells them; the two Chinese clients' + // names are translated (their English names differ from their Chinese ones). + const clientLabels: Record = { + claude: 'Claude', + codex: 'Codex', + cursor: 'Cursor', + workbuddy: 'WorkBuddy', + doubao: t('connectAgent.doubao.title'), + qwenwork: t('connectAgent.qwenwork.title'), + }; + const mintKey = async (name: string) => { // A key is always minted under the name the administrator typed — never // a default one (the route would otherwise store an anonymous key). @@ -258,52 +296,58 @@ export function ConnectAgent() { - {/* ── Per-client cards: international clients first, then the Chinese ones ── */} -
- {/* Claude: one card, two ways in — the claude.ai / Desktop connector and - the Claude Code CLI. A full row, so the six cards below pair up. */} - - - Claude - - -
-

claude.ai / Claude Desktop

-

{t('connectAgent.claude.body')}

- -

{t('connectAgent.claude.reachability')}

-
-
-

Claude Code

-

{t('connectAgent.claudeCode.body')}

- -

{t('connectAgent.claudeCode.plugin')}

- -
-
-
+ {/* ── Which client: one client's steps at a time ─────────────────── */} + + + {t('connectAgent.client.title')} +

{t('connectAgent.client.hint')}

+
+ + { + if (isConnectClientId(next)) setClient(next); + }} + > + + {CONNECT_CLIENT_IDS.map((id) => ( + + {clientLabels[id]} + + ))} + - - - Codex - - -

{t('connectAgent.codex.body')}

- -

{t('connectAgent.codex.hint')}

-
-
+ {/* Claude: two ways in — the claude.ai / Desktop connector and the + Claude Code CLI. */} + +
+

claude.ai / Claude Desktop

+

{t('connectAgent.claude.body')}

+ +

{t('connectAgent.claude.reachability')}

+
+
+

Claude Code

+

{t('connectAgent.claudeCode.body')}

+ +

{t('connectAgent.claudeCode.plugin')}

+ +
+
+ + +

{t('connectAgent.codex.body')}

+ +

{t('connectAgent.codex.hint')}

+
- - - - Cursor + {cursorDeeplink && ( )} - - - -

{t('connectAgent.cursor.body')}

- -
-
+

{t('connectAgent.cursor.body')}

+ + - - - WorkBuddy - - -

{t('connectAgent.workbuddy.body')}

- -

{t('connectAgent.workbuddy.hint')}

-
-
+ +

{t('connectAgent.workbuddy.body')}

+ +

{t('connectAgent.workbuddy.hint')}

+
- - - {t('connectAgent.doubao.title')} - - -

{t('connectAgent.doubao.body')}

- -

{t('connectAgent.doubao.hint')}

-
-
+ +

{t('connectAgent.doubao.body')}

+ +

{t('connectAgent.doubao.hint')}

+
- - - {t('connectAgent.qwenwork.title')} - - -

{t('connectAgent.qwenwork.body')}

- -

{t('connectAgent.qwenwork.hint')}

-
-
+ +

{t('connectAgent.qwenwork.body')}

+ +

{t('connectAgent.qwenwork.hint')}

+
+
+
+
- {/* ── API keys (headless) ──────────────────────────────────── */} - - - - - {t('connectAgent.apiKey.title')} - {t('connectAgent.apiKey.badge')} - - - -

{t('connectAgent.apiKey.body')}

- {minted ? ( -
-

- {t('connectAgent.apiKey.showOnce', { name: minted.name })} -

- - - - -
- ) : ( -
- setKeyName(e.target.value)} - placeholder={t('connectAgent.apiKey.namePlaceholder')} - className="h-8 max-w-56" - /> - -
- )} - { - if (!open) setConfirmName(null); - }} - > - - - - {t('connectAgent.apiKey.confirmTitle', { name: confirmName ?? '' })} - - {t('connectAgent.apiKey.body')} - - - {t('common.cancel')} - { - const name = confirmName ?? ''; - setConfirmName(null); - void mintKey(name); - }} + {/* ── Advanced: API keys for scripts and CI, collapsed ─────────────── */} + + + + + + + + + + {t('connectAgent.apiKey.title')} + {t('connectAgent.apiKey.badge')} + + + +

{t('connectAgent.apiKey.body')}

+ {minted ? ( +
+

+ {t('connectAgent.apiKey.showOnce', { name: minted.name })} +

+ + + + +
+ ) : ( +
+ setKeyName(e.target.value)} + placeholder={t('connectAgent.apiKey.namePlaceholder')} + className="h-8 max-w-56" + /> +
+ {minting ? t('connectAgent.apiKey.minting') : t('connectAgent.apiKey.mint')} + +
+ )} + { + if (!open) setConfirmName(null); + }} + > + + + + {t('connectAgent.apiKey.confirmTitle', { name: confirmName ?? '' })} + + {t('connectAgent.apiKey.body')} + + + {t('common.cancel')} + { + const name = confirmName ?? ''; + setConfirmName(null); + void mintKey(name); + }} + > + {t('connectAgent.apiKey.mint')} + + + + + {mintError && ( +

+ {mintError} +

+ )} + + + + ); } diff --git a/ui/packages/app-shell/src/console/connect/connectClientPreference.ts b/ui/packages/app-shell/src/console/connect/connectClientPreference.ts new file mode 100644 index 00000000000..2b45df5603e --- /dev/null +++ b/ui/packages/app-shell/src/console/connect/connectClientPreference.ts @@ -0,0 +1,44 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +import { + defineWorkspacePreference, + versionedPreferenceCodec, +} from '../../utils/workspacePreference.js'; + +/** + * The AI clients Connect an Agent has steps for, in the order its picker + * offers them: international clients first, then the Chinese ones. + */ +export const CONNECT_CLIENT_IDS = [ + 'claude', + 'codex', + 'cursor', + 'workbuddy', + 'doubao', + 'qwenwork', +] as const; + +export type ConnectClientId = (typeof CONNECT_CLIENT_IDS)[number]; + +/** What a reader who never chose sees: the first client the picker offers. */ +export const DEFAULT_CONNECT_CLIENT: ConnectClientId = 'claude'; + +export function isConnectClientId(value: unknown): value is ConnectClientId { + return typeof value === 'string' && (CONNECT_CLIENT_IDS as readonly string[]).includes(value); +} + +/** + * Which client the reader connects with — the reader's own choice on this + * browser, never metadata. One value across the console (`user` scope), + * stored as `{"client":"CLIENT_ID","v":1}` under `os:pref:connect.client`. + * A stored client the page no longer offers, or a record of another + * version, is no choice and reads as {@link DEFAULT_CONNECT_CLIENT}. + */ +export const CONNECT_CLIENT_PREFERENCE = defineWorkspacePreference({ + name: 'connect.client', + codec: versionedPreferenceCodec({ + version: 1, + toRecord: (client) => ({ client }), + fromRecord: (record) => (isConnectClientId(record.client) ? record.client : undefined), + }), +}); diff --git a/ui/packages/i18n/src/locales/ar.ts b/ui/packages/i18n/src/locales/ar.ts index c7655377dc7..4ffc03cf482 100644 --- a/ui/packages/i18n/src/locales/ar.ts +++ b/ui/packages/i18n/src/locales/ar.ts @@ -4105,6 +4105,13 @@ const ar = { body: 'الإضافات ← الموصلات ← + إضافة ← لصق إعداد JSON:', hint: 'أو أضفه يدويًا كـ Streamable HTTP بهذا العنوان. إذا لم يدعم إصدارك تسجيل الدخول عبر المتصفح، أضف ترويسة الطلب Authorization: Bearer <مفتاح API> (أنشئ مفتاحًا أدناه).', }, + client: { + title: 'أي مساعد ذكاء اصطناعي تستخدم؟', + hint: 'اختر مساعدك لترى خطواته فقط. يتذكر هذا المتصفح اختيارك.', + }, + advanced: { + title: 'متقدم: البرامج النصية وCI', + }, apiKey: { title: 'مفاتيح API', badge: 'بدون واجهة', diff --git a/ui/packages/i18n/src/locales/de.ts b/ui/packages/i18n/src/locales/de.ts index 763522ae905..17f91ef5326 100644 --- a/ui/packages/i18n/src/locales/de.ts +++ b/ui/packages/i18n/src/locales/de.ts @@ -3800,6 +3800,13 @@ const de = { body: 'Erweiterungen → Connectors → + Hinzufügen → JSON-Konfiguration einfügen:', hint: 'Oder manuell als Streamable HTTP mit dieser URL hinzufügen. Bietet Ihre Version keine Browser-Anmeldung, fügen Sie den Request-Header Authorization: Bearer hinzu (Schlüssel unten erstellen).', }, + client: { + title: 'Welchen KI-Assistenten verwenden Sie?', + hint: 'Wählen Sie Ihren aus, um nur seine Schritte zu sehen. Dieser Browser merkt sich Ihre Wahl.', + }, + advanced: { + title: 'Erweitert: Skripte und CI', + }, apiKey: { title: 'API-Schlüssel', badge: 'headless', diff --git a/ui/packages/i18n/src/locales/en.ts b/ui/packages/i18n/src/locales/en.ts index 8215481ff5e..979eed4107c 100644 --- a/ui/packages/i18n/src/locales/en.ts +++ b/ui/packages/i18n/src/locales/en.ts @@ -4607,6 +4607,13 @@ const en = { body: 'Extensions → Connectors → + Add → Paste JSON config:', hint: 'Or add it by hand as Streamable HTTP with this URL. If your version offers no browser sign-in, add the request header Authorization: Bearer (create a key below).', }, + client: { + title: 'Which AI assistant do you use?', + hint: 'Choose yours to see only its steps. This browser remembers your choice.', + }, + advanced: { + title: 'Advanced: scripts and CI', + }, apiKey: { title: 'API keys', badge: 'headless', diff --git a/ui/packages/i18n/src/locales/es.ts b/ui/packages/i18n/src/locales/es.ts index d35bb980a9b..d50a8d73b2a 100644 --- a/ui/packages/i18n/src/locales/es.ts +++ b/ui/packages/i18n/src/locales/es.ts @@ -3877,6 +3877,13 @@ const es = { body: 'Extensiones → Conectores → + Añadir → Pegar configuración JSON:', hint: 'O añádelo a mano como Streamable HTTP con esta URL. Si tu versión no ofrece inicio de sesión en el navegador, añade la cabecera Authorization: Bearer (crea una clave abajo).', }, + client: { + title: '¿Qué asistente de IA usas?', + hint: 'Elige el tuyo para ver solo sus pasos. Este navegador recuerda tu elección.', + }, + advanced: { + title: 'Avanzado: scripts y CI', + }, apiKey: { title: 'Claves API', badge: 'headless', diff --git a/ui/packages/i18n/src/locales/fr.ts b/ui/packages/i18n/src/locales/fr.ts index 6db530a2240..cf66f7a325e 100644 --- a/ui/packages/i18n/src/locales/fr.ts +++ b/ui/packages/i18n/src/locales/fr.ts @@ -3873,6 +3873,13 @@ const fr = { body: 'Extensions → Connecteurs → + Ajouter → Coller la configuration JSON :', hint: 'Ou ajoutez-le à la main en Streamable HTTP avec cette URL. Si votre version ne propose pas de connexion par navigateur, ajoutez l’en-tête Authorization: Bearer (créez une clé ci-dessous).', }, + client: { + title: 'Quel assistant IA utilisez-vous ?', + hint: 'Choisissez le vôtre pour n’afficher que ses étapes. Ce navigateur mémorise votre choix.', + }, + advanced: { + title: 'Avancé : scripts et CI', + }, apiKey: { title: 'Clés API', badge: 'headless', diff --git a/ui/packages/i18n/src/locales/ja.ts b/ui/packages/i18n/src/locales/ja.ts index affc8dbd4bc..d8ae6e7df95 100644 --- a/ui/packages/i18n/src/locales/ja.ts +++ b/ui/packages/i18n/src/locales/ja.ts @@ -3802,6 +3802,13 @@ const ja = { body: '拡張 → コネクタ → + 追加 → JSON 設定を貼り付け:', hint: '手動で追加する場合は Streamable HTTP を選び、この URL を入力します。ブラウザでのサインインに対応していないバージョンでは、リクエストヘッダー Authorization: Bearer を追加してください(キーは下で作成)。', }, + client: { + title: 'どの AI アシスタントを使っていますか?', + hint: 'お使いのアシスタントを選ぶと、その手順だけが表示されます。選択はこのブラウザーに保存されます。', + }, + advanced: { + title: '詳細設定:スクリプトと CI', + }, apiKey: { title: 'API キー', badge: 'ヘッドレス', diff --git a/ui/packages/i18n/src/locales/ko.ts b/ui/packages/i18n/src/locales/ko.ts index f8e1f683042..1116fa578a2 100644 --- a/ui/packages/i18n/src/locales/ko.ts +++ b/ui/packages/i18n/src/locales/ko.ts @@ -3799,6 +3799,13 @@ const ko = { body: '확장 → 커넥터 → + 추가 → JSON 구성 붙여넣기:', hint: '직접 추가하려면 Streamable HTTP를 선택하고 이 URL을 입력하세요. 브라우저 로그인을 지원하지 않는 버전이라면 요청 헤더 Authorization: Bearer 를 추가하세요(키는 아래에서 생성).', }, + client: { + title: '어떤 AI 어시스턴트를 사용하시나요?', + hint: '사용하는 어시스턴트를 선택하면 해당 단계만 표시됩니다. 이 브라우저가 선택을 기억합니다.', + }, + advanced: { + title: '고급: 스크립트 및 CI', + }, apiKey: { title: 'API 키', badge: '헤드리스', diff --git a/ui/packages/i18n/src/locales/pt.ts b/ui/packages/i18n/src/locales/pt.ts index cb2a77eaf12..edeb86deece 100644 --- a/ui/packages/i18n/src/locales/pt.ts +++ b/ui/packages/i18n/src/locales/pt.ts @@ -3872,6 +3872,13 @@ const pt = { body: 'Extensões → Conectores → + Adicionar → Colar configuração JSON:', hint: 'Ou adicione manualmente como Streamable HTTP com esta URL. Se a sua versão não oferecer login pelo navegador, adicione o cabeçalho Authorization: Bearer (crie uma chave abaixo).', }, + client: { + title: 'Qual assistente de IA você usa?', + hint: 'Escolha o seu para ver apenas os passos dele. Este navegador lembra a sua escolha.', + }, + advanced: { + title: 'Avançado: scripts e CI', + }, apiKey: { title: 'Chaves de API', badge: 'headless', diff --git a/ui/packages/i18n/src/locales/ru.ts b/ui/packages/i18n/src/locales/ru.ts index e7cb7e644de..5030666be48 100644 --- a/ui/packages/i18n/src/locales/ru.ts +++ b/ui/packages/i18n/src/locales/ru.ts @@ -3963,6 +3963,13 @@ const ru = { body: 'Расширения → Коннекторы → + Добавить → Вставить JSON-конфигурацию:', hint: 'Или добавьте вручную как Streamable HTTP с этим URL. Если ваша версия не поддерживает вход через браузер, добавьте заголовок запроса Authorization: Bearer <ключ API> (создайте ключ ниже).', }, + client: { + title: 'Каким ИИ-ассистентом вы пользуетесь?', + hint: 'Выберите свой, чтобы видеть только его шаги. Этот браузер запомнит ваш выбор.', + }, + advanced: { + title: 'Дополнительно: скрипты и CI', + }, apiKey: { title: 'API-ключи', badge: 'headless', diff --git a/ui/packages/i18n/src/locales/zh.ts b/ui/packages/i18n/src/locales/zh.ts index a71de6f4f0e..b0b65b81d56 100644 --- a/ui/packages/i18n/src/locales/zh.ts +++ b/ui/packages/i18n/src/locales/zh.ts @@ -4006,6 +4006,13 @@ const zh = { body: '扩展 → 连接器 → + 添加 → 粘贴 JSON 配置:', hint: '也可手动添加:传输类型选 Streamable HTTP 并填写此 URL。如果你的版本不支持浏览器登录,请添加请求头 Authorization: Bearer (在下方创建)。', }, + client: { + title: '你用的是哪个 AI 助手?', + hint: '选择你的助手,只看它的步骤。本浏览器会记住你的选择。', + }, + advanced: { + title: '高级:脚本与 CI', + }, apiKey: { title: 'API 密钥', badge: '无头场景',