ui: align community agent and settings copy

This commit is contained in:
a.tolmachev
2026-05-03 18:08:14 +00:00
parent 20598ff5dc
commit 18b23fcc0d
6 changed files with 79 additions and 66 deletions
+1 -47
View File
@@ -2,55 +2,9 @@
## Current
### `feat/community-surface-trim`
Status: in_progress
Goal:
- привести открытую редакцию к честному product surface и убрать расхождение между текущим кодом и edition policy.
Main code areas:
- `apps/ui/js/wizard.js`
- `apps/ui/js/wizard-model.js`
- `apps/ui/js/agents.js`
- `apps/ui/js/settings.js`
- `apps/ui/js/i18n.js`
- `docs/product-editions.md`
- `docs/architecture.md`
- `docs/mcp-interface.md`
Implementation slices:
1. Скрыть или честно заблокировать:
- `WebSocket`
- `SOAP`
- `gRPC streaming`
- streaming execution modes
- `elevated/strict` security levels
в Community.
2. Привести copy и feature affordances к open-core границе.
3. Добавить edition-aware explanation в wizard и agents UI.
Progress:
- done: Community wizard protocol list hides premium protocol cards;
- done: Community security note is shown inside wizard flow;
- done: server-side Community enforcement rejects unsupported protocols and premium security levels;
- done: Community protocol capabilities and wizard surface no longer present streaming execution as available;
- pending: привести Agents/Settings copy к окончательной open-core границе.
DoD:
- Community UX не создает ложного ожидания наличия premium functionality;
- protocol set и security levels совпадают в docs, UI и runtime contracts.
Verification:
- e2e checks for wizard protocol list;
- manual Community smoke;
- localized copy review.
## Next
### `feat/private-token-service-seam`
Status: ready
Status: in_progress
Goal:
- подготовить public codebase к private реализации короткоживущих и одноразовых токенов.
+5 -5
View File
@@ -93,7 +93,7 @@
<div class="page-header">
<div>
<h1 class="page-heading" data-i18n="agents.title">Agents</h1>
<p class="page-subheading" data-i18n="agents.subtitle">Named MCP endpoints that expose a curated subset of operations to an LLM</p>
<p class="page-subheading" x-text="subtitleText()">Named MCP endpoints that expose a curated subset of operations to an LLM</p>
</div>
<button class="btn-new" @click="openCreate()">
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M8 3v10M3 8h10"/></svg>
@@ -147,7 +147,7 @@
<!-- Info callout -->
<div class="agents-info-callout">
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="var(--accent)" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="8" r="7"/><path d="M8 11V8M8 5v-.5"/></svg>
<span x-text="tfKey('agents.callout', { count: operations.length })">Each agent gets its own MCP endpoint.</span>
<span x-text="agentCalloutText()">Each agent gets its own MCP endpoint.</span>
</div>
<!-- Loading -->
@@ -167,7 +167,7 @@
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round" style="opacity:0.35"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><path d="M14 17.5h7M17.5 14v7"/></svg>
</div>
<div class="empty-state-title" data-i18n="agents.empty.initial.title">No agents yet</div>
<div class="empty-state-sub" data-i18n="agents.empty.initial.sub">Create your first agent to get a dedicated MCP endpoint with a curated set of tools.</div>
<div class="empty-state-sub" x-text="emptyStateText()">Create your first agent to get a dedicated MCP endpoint with a curated set of tools.</div>
<button class="btn-new" @click="openCreate()" style="margin-top: 16px;">
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M8 3v10M3 8h10"/></svg>
<span data-i18n="agents.new">New agent</span>
@@ -269,7 +269,7 @@
<div class="drawer-header">
<div>
<div class="drawer-title" x-text="drawerMode === 'create' ? tKey('agents.drawer.new_title') : tKey('agents.drawer.edit_title')"></div>
<div class="drawer-subtitle" x-text="drawerMode === 'create' ? tKey('agents.drawer.new_sub') : tKey('agents.drawer.edit_sub')"></div>
<div class="drawer-subtitle" x-text="drawerSubText()"></div>
</div>
<button class="drawer-close" @click="closeDrawer()">
<svg width="14" height="14" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><line x1="1" y1="1" x2="11" y2="11"/><line x1="11" y1="1" x2="1" y2="11"/></svg>
@@ -326,7 +326,7 @@
<div class="drawer-section-title" data-i18n="agents.drawer.operations">Operations</div>
<span class="drawer-section-count" x-text="tfKey('agents.drawer.selected', { count: form.selectedOps.length })"></span>
</div>
<div class="drawer-section-sub" data-i18n="agents.drawer.operations_sub">Select which operations this agent exposes. LLMs connecting to this agent will only see these tools.</div>
<div class="drawer-section-sub" x-text="operationsSubText()">Select which operations this agent exposes. LLMs connecting to this agent will only see these tools.</div>
<div class="ops-picker">
<!-- Search -->
+45
View File
@@ -48,6 +48,7 @@ document.addEventListener('alpine:init', function() {
return {
agents: [],
operations: [],
capabilities: null,
loading: true,
loadError: '',
workspaceId: null,
@@ -76,6 +77,7 @@ document.addEventListener('alpine:init', function() {
await (window.whenWorkspacesReady ? window.whenWorkspacesReady() : Promise.resolve());
var workspace = window.getCurrentWorkspace ? window.getCurrentWorkspace() : null;
this.workspaceId = workspace ? workspace.id : null;
await this.loadCapabilities();
await this.reload();
document.addEventListener('keydown', function(event) {
@@ -90,10 +92,23 @@ document.addEventListener('alpine:init', function() {
window.addEventListener('crank:workspacechange', async function(event) {
self.workspaceId = event.detail ? event.detail.id : null;
await self.loadCapabilities();
await self.reload();
});
},
async loadCapabilities() {
if (!window.CrankApi || typeof window.CrankApi.getCapabilities !== 'function') {
this.capabilities = null;
return;
}
try {
this.capabilities = await window.CrankApi.getCapabilities();
} catch (_error) {
this.capabilities = null;
}
},
async reload() {
this.loading = true;
this.loadError = '';
@@ -150,6 +165,36 @@ document.addEventListener('alpine:init', function() {
}, 0);
},
get isCommunityBuild() {
return !!(this.capabilities && this.capabilities.edition === 'community');
},
agentCalloutText() {
if (this.isCommunityBuild) {
return this.tfKey('agents.callout.community', { count: this.operations.length });
}
return this.tfKey('agents.callout.default', { count: this.operations.length });
},
subtitleText() {
return this.tKey(this.isCommunityBuild ? 'agents.subtitle.community' : 'agents.subtitle');
},
emptyStateText() {
return this.tKey(this.isCommunityBuild ? 'agents.empty.initial.sub_community' : 'agents.empty.initial.sub');
},
drawerSubText() {
if (this.drawerMode === 'edit') {
return this.tKey('agents.drawer.edit_sub');
}
return this.tKey(this.isCommunityBuild ? 'agents.drawer.new_sub_community' : 'agents.drawer.new_sub');
},
operationsSubText() {
return this.tKey(this.isCommunityBuild ? 'agents.drawer.operations_sub_community' : 'agents.drawer.operations_sub');
},
get filteredOps() {
var query = this.opSearch.toLowerCase().trim();
if (!query) return this.operations;
+24 -14
View File
@@ -378,7 +378,7 @@ var TRANSLATIONS = {
'settings.profile.email': 'Email',
'settings.profile.role': 'Role',
'settings.page.title': 'Account settings',
'settings.page.subtitle': 'Manage your profile, password, and workspace settings.',
'settings.page.subtitle': 'Manage your profile, password, workspace settings, and the capability limits of this build.',
'settings.ws.slug_hint': 'Used in API endpoints and across the console. Only lowercase letters, numbers and hyphens.',
'settings.ws.description_optional': 'Description (optional)',
'settings.ws.description_placeholder': 'What does this workspace do?',
@@ -403,9 +403,9 @@ var TRANSLATIONS = {
'settings.security.mismatch': 'New password and confirmation do not match.',
'settings.security.saved': 'Password updated.',
'settings.security.save_error': 'Failed to change password',
'settings.security.capabilities_title': 'More security options',
'settings.security.capabilities_body': 'Two-factor authentication, passkeys, and multi-session controls will be added later. The current live flow uses password sign-in with one HttpOnly browser session.',
'settings.security.not_available': 'Available later.',
'settings.security.capabilities_title': 'Build capability summary',
'settings.security.capabilities_body': 'This section shows what the current build supports. Password sign-in with one HttpOnly browser session is available now; short-lived tokens, one-time tokens, and advanced governance depend on the edition.',
'settings.security.not_available': 'Build capabilities',
'settings.security.capability_title_community': 'Community build',
'settings.security.capability_title_enterprise': 'Enterprise build',
'settings.security.capability_title_cloud': 'Cloud build',
@@ -431,8 +431,8 @@ var TRANSLATIONS = {
'settings.session.current_workspace': 'current workspace',
'settings.session.summary': '{email} · {role} in {workspace}. Use Log out to revoke the current browser session.',
'settings.notifications.title': 'Notifications',
'settings.notifications.not_ready_title': 'Available later.',
'settings.notifications.not_ready_body': 'Notification routing and per-user preferences will be added later.',
'settings.notifications.not_ready_title': 'Not included in this build.',
'settings.notifications.not_ready_body': 'Notification routing and per-user preferences are outside the current Community surface.',
'settings.planned_badge': 'Planned',
'settings.notifications.spike_title': 'Operation error spike',
'settings.notifications.spike_body': 'Alert when error rate exceeds a rolling threshold for a published tool.',
@@ -936,6 +936,7 @@ var TRANSLATIONS = {
// Agents page
'agents.title': 'Agents',
'agents.subtitle': 'Named MCP endpoints that expose a curated subset of operations to an LLM',
'agents.subtitle.community': 'Dedicated MCP endpoints for AI agents. Community keeps machine access on static agent keys.',
'agents.new': 'New agent',
'agents.limit.title': 'Agent limit reached',
'agents.limit.message': 'This Community build allows up to {limit} AI agent. Upgrade the edition or remove an existing agent to add another one.',
@@ -949,13 +950,15 @@ var TRANSLATIONS = {
'agents.stats.calls_sub': 'all agents combined',
'agents.search': 'Search agents...',
'agents.results': '{shown} of {total} agents',
'agents.callout': 'Each agent gets its own MCP endpoint. Connect your LLM client to a specific agent so it only sees the tools it needs — not all {count} operations in this workspace.',
'agents.callout.default': 'Each agent gets its own MCP endpoint. Connect your LLM client to a specific agent so it only sees the tools it needs — not all {count} operations in this workspace.',
'agents.callout.community': 'Each agent gets its own MCP endpoint and static agent keys in Community. Connect your LLM client to a specific agent so it only sees the tools it needs — not all {count} operations in this workspace.',
'agents.loading': 'Loading agents…',
'agents.error.title': 'Failed to load agents',
'agents.error.api': 'Workspace or API is unavailable',
'agents.error.load': 'Failed to load agents',
'agents.empty.initial.title': 'No agents yet',
'agents.empty.initial.sub': 'Create your first agent to get a dedicated MCP endpoint with a curated set of tools.',
'agents.empty.initial.sub_community': 'Create your first agent to get one dedicated MCP endpoint and its static machine-access keys for a focused toolset.',
'agents.empty.search.title': 'No agents match "{query}"',
'agents.empty.search.sub': 'Try a different search term.',
'agents.card.tools': 'tools',
@@ -975,6 +978,7 @@ var TRANSLATIONS = {
'agents.drawer.new_title': 'New agent',
'agents.drawer.edit_title': 'Edit agent',
'agents.drawer.new_sub': 'Configure a named MCP endpoint for your LLM',
'agents.drawer.new_sub_community': 'Configure one MCP endpoint and its static agent-key boundary for a focused LLM use case',
'agents.drawer.edit_sub': 'Update agent settings and tool selection',
'agents.drawer.identity': 'Identity',
'agents.drawer.display_name': 'Display name',
@@ -990,6 +994,7 @@ var TRANSLATIONS = {
'agents.drawer.operations': 'Operations',
'agents.drawer.selected': '{count} selected',
'agents.drawer.operations_sub': 'Select which operations this agent exposes. LLMs connecting to this agent will only see these tools.',
'agents.drawer.operations_sub_community': 'Select which operations this agent exposes. Any keys issued for this agent only work against this MCP endpoint.',
'agents.drawer.filter_ops': 'Filter operations…',
'agents.drawer.ops_no_match': 'No operations match "{query}"',
'agents.drawer.ops_selected': '{count} operations selected',
@@ -1430,7 +1435,7 @@ var TRANSLATIONS = {
'settings.profile.email': 'Email',
'settings.profile.role': 'Роль',
'settings.page.title': 'Настройки аккаунта',
'settings.page.subtitle': 'Управляйте профилем, паролем и настройками воркспейса.',
'settings.page.subtitle': 'Управляйте профилем, паролем, настройками воркспейса и границами возможностей текущей сборки.',
'settings.ws.slug_hint': 'Используется в API endpoint-ах и по всей консоли. Только строчные буквы, цифры и дефисы.',
'settings.ws.description_optional': 'Описание (необязательно)',
'settings.ws.description_placeholder': 'Чем занимается этот воркспейс?',
@@ -1455,9 +1460,9 @@ var TRANSLATIONS = {
'settings.security.mismatch': 'Новый пароль и подтверждение не совпадают.',
'settings.security.saved': 'Пароль обновлен.',
'settings.security.save_error': 'Не удалось изменить пароль',
'settings.security.capabilities_title': 'Дополнительные параметры безопасности',
'settings.security.capabilities_body': 'Двухфакторная аутентификация, passkeys и управление несколькими сессиями появятся позже. Сейчас доступен вход по паролю с одной HttpOnly browser session.',
'settings.security.not_available': 'Появится позже.',
'settings.security.capabilities_title': 'Сводка по возможностям сборки',
'settings.security.capabilities_body': 'Этот блок показывает, что поддерживает текущая сборка. Сейчас доступен вход по паролю с одной HttpOnly browser session; короткоживущие токены, одноразовые токены и расширенное управление доступом зависят от редакции.',
'settings.security.not_available': 'Возможности сборки',
'settings.security.capability_title_community': 'Редакция Community',
'settings.security.capability_title_enterprise': 'Редакция Enterprise',
'settings.security.capability_title_cloud': 'Редакция Cloud',
@@ -1483,8 +1488,8 @@ var TRANSLATIONS = {
'settings.session.current_workspace': 'текущий воркспейс',
'settings.session.summary': '{email} · {role} в {workspace}. Используйте «Выйти», чтобы отозвать текущую browser session.',
'settings.notifications.title': 'Уведомления',
'settings.notifications.not_ready_title': 'Появятся позже.',
'settings.notifications.not_ready_body': 'Маршрутизация уведомлений и персональные настройки появятся позже.',
'settings.notifications.not_ready_title': 'Не входит в эту сборку.',
'settings.notifications.not_ready_body': 'Маршрутизация уведомлений и персональные настройки находятся вне текущей поверхности Community.',
'settings.planned_badge': 'Запланировано',
'settings.notifications.spike_title': 'Всплеск ошибок операции',
'settings.notifications.spike_body': 'Алерт, когда доля ошибок превышает скользящий порог для опубликованного инструмента.',
@@ -1998,6 +2003,7 @@ var TRANSLATIONS = {
// Agents page
'agents.title': 'Агенты',
'agents.subtitle': 'Именованные MCP endpoint-ы, открывающие LLM ограниченный набор операций',
'agents.subtitle.community': 'Отдельные MCP endpoint-ы для AI-агентов. В Community машинный доступ держится на статических ключах агента.',
'agents.new': 'Новый агент',
'agents.limit.title': 'Достигнут лимит агентов',
'agents.limit.message': 'В этой Community-сборке доступно не более {limit} AI-агента. Чтобы добавить еще одного, удалите существующего агента или перейдите на другую редакцию.',
@@ -2011,13 +2017,15 @@ var TRANSLATIONS = {
'agents.stats.calls_sub': 'по всем агентам',
'agents.search': 'Поиск агентов...',
'agents.results': '{shown} из {total} агентов',
'agents.callout': 'У каждого агента свой MCP endpoint. Подключайте LLM-клиент к конкретному агенту, чтобы он видел только нужные инструменты, а не все {count} операций этого воркспейса.',
'agents.callout.default': 'У каждого агента свой MCP endpoint. Подключайте LLM-клиент к конкретному агенту, чтобы он видел только нужные инструменты, а не все {count} операций этого воркспейса.',
'agents.callout.community': 'В Community у каждого агента свой MCP endpoint и свои статические ключи агента. Подключайте LLM-клиент к конкретному агенту, чтобы он видел только нужные инструменты, а не все {count} операций этого воркспейса.',
'agents.loading': 'Загрузка агентов…',
'agents.error.title': 'Не удалось загрузить агентов',
'agents.error.api': 'Воркспейс или API недоступен',
'agents.error.load': 'Не удалось загрузить агентов',
'agents.empty.initial.title': 'Агентов пока нет',
'agents.empty.initial.sub': 'Создайте первого агента, чтобы получить отдельный MCP endpoint с нужным набором инструментов.',
'agents.empty.initial.sub_community': 'Создайте первого агента, чтобы получить один отдельный MCP endpoint и его статические машинные ключи для конкретного набора инструментов.',
'agents.empty.search.title': 'Нет агентов по запросу "{query}"',
'agents.empty.search.sub': 'Попробуйте другой поисковый запрос.',
'agents.card.tools': 'инструментов',
@@ -2037,6 +2045,7 @@ var TRANSLATIONS = {
'agents.drawer.new_title': 'Новый агент',
'agents.drawer.edit_title': 'Редактировать агента',
'agents.drawer.new_sub': 'Настройте именованный MCP endpoint для вашей LLM',
'agents.drawer.new_sub_community': 'Настройте один MCP endpoint и его границу на статических ключах агента для конкретного LLM-сценария',
'agents.drawer.edit_sub': 'Обновите настройки агента и выбор инструментов',
'agents.drawer.identity': 'Идентичность',
'agents.drawer.display_name': 'Отображаемое имя',
@@ -2052,6 +2061,7 @@ var TRANSLATIONS = {
'agents.drawer.operations': 'Операции',
'agents.drawer.selected': 'Выбрано: {count}',
'agents.drawer.operations_sub': 'Выберите операции, которые открывает этот агент. LLM, подключенная к агенту, увидит только эти инструменты.',
'agents.drawer.operations_sub_community': 'Выберите операции, которые открывает этот агент. Любые ключи, выпущенные для этого агента, будут работать только против этого MCP endpoint.',
'agents.drawer.filter_ops': 'Фильтр операций…',
'agents.drawer.ops_no_match': 'Нет операций по запросу "{query}"',
'agents.drawer.ops_selected': 'Выбрано операций: {count}',
+2
View File
@@ -5,7 +5,9 @@ test('agents page shows demo cards and edit drawer opens', async ({ page }) => {
await login(page);
await page.goto('/agents');
await expect(page.locator('.page-heading')).toHaveText(localized('Agents', 'Агенты'));
await expect(page.locator('.agents-info-callout')).toContainText(localized('static agent keys', 'статические ключи агента'));
await page.getByRole('button', { name: localized('New agent', 'Новый агент') }).click();
await expect(page.locator('.drawer-title')).toHaveText(localized('New agent', 'Новый агент'));
await expect(page.locator('.drawer-subtitle')).toContainText(localized('static agent-key boundary', 'статических ключах агента'));
await expect(page.locator('.drawer')).toContainText(/mcp/i);
});
@@ -13,4 +13,6 @@ test('workspace and settings pages show live session data', async ({ page }) =>
await expect(page.locator('.page-title')).toHaveText(localized('Account settings', 'Настройки аккаунта'));
await expect(page.locator('#settings-session-summary')).toBeVisible();
await expect(page.locator('#section-security')).toBeVisible();
await expect(page.locator('#settings-security-capability-title')).toContainText(localized('Community build', 'Редакция Community'));
await expect(page.locator('#settings-security-capability-body')).toContainText(localized('static agent keys', 'статические ключи агентов'));
});