diff --git a/apps/aevatar-console-web/docs/2026-09-15-channel-runtime-editor.md b/apps/aevatar-console-web/docs/2026-09-15-channel-runtime-editor.md
index a386bf48d..0796c17af 100644
--- a/apps/aevatar-console-web/docs/2026-09-15-channel-runtime-editor.md
+++ b/apps/aevatar-console-web/docs/2026-09-15-channel-runtime-editor.md
@@ -5,40 +5,53 @@ Channel details now provides an Edit action at
Edit and Remove sit at the top right beside the breadcrumb, with a 12px gap.
On narrow screens, the action group wraps below the breadcrumb and stays right aligned.
The editor follows the approved [Figma frame](https://www.figma.com/design/FaVJx5IZeQX9jHcUi55ndB/?node-id=63-327)
-and the existing Telegram connection form.
+and the existing Telegram connection form, with the September 16 request
+to limit editing to Label and Skill name.
## Shared Components
- `ChannelSkillField` is shared by connection and editing, including its label,
optional state, input, and field error presentation.
-- Both pages use `ChannelServicePicker`, `useChannelServiceChoices`, the existing
- connection styles, shell, loading skeleton, and toast provider.
-- The form contains only Skill name and Services, with no Advanced settings
- section or inputs for Skill version and Bot instructions.
-- Channel names and bot tokens are not editable through this API. Tool sets,
- extra tool names, instructions, skill version, and credential source are
- retained from the loaded config.
+- Both pages reuse the existing connection styles, centered content, shell,
+ loading skeleton, and toast provider.
+- The edit form contains only Label and Skill name. It does not load the
+ UserService inventory or display a Services selector. Creation retains its
+ existing service selection; details retain their read-only authorization list.
+- Bot tokens are not editable. Tool sets, extra tool names, instructions,
+ service selectors, skill version, and credential source are retained from the
+ loaded config (clearing Skill name also clears its version).
## API Behavior
-The API contract was checked against `origin/feature/integrate` at
-`6c929a00db3d7636c91bc719478043a5fd331f7c`, specifically
-`ChannelCallbackEndpoints.cs` and `ChannelRegistrationServiceSelection.cs`.
+The runtime-config contract was checked against `origin/feature/integrate` at
+`6c929a00db3d7636c91bc719478043a5fd331f7c`, specifically `ChannelCallbackEndpoints.cs`. Label updates use NyxID's existing
+`backend/src/handlers/channel_bots.rs` contract; no backend changes are required.
- GET and POST use `/api/channels/registrations/{registrationId}/runtime-config`.
Detail identity and scope must match the route.
-- Opening the editor requires a fresh successful GET, including when Query has
- cached data from an earlier visit. Subsequent readback never resets user input.
-- POST sends the complete known runtime config because omitted fields are not
- patches. Strings edited by the user are trimmed as the backend parser expects.
-- Clearing Skill name also clears its version. Removing service authorization
- removes selectors for the deselected service, preserving other selectors.
-- Services use the same authenticated inventory and bearer grant filtering as
- creation. Saved services absent from current choices remain visible as
- unavailable until deliberately removed. They are never silently dropped.
-- Existing `nyxid_default` registrations retain their mode until the user
- explicitly switches to individual selection. Zero individually selected
- services is sent as an explicit empty allowlist.
+- Opening the editor requires fresh successful config, registration-list, and
+ personal NyxID bot-list reads, even when Query has cached data. The exact owned
+ registration's `nyx_channel_bot_id` and matching platform identify the bot.
+ Missing/cross-scope identities block the editor; registration IDs never serve
+ as bot IDs. Later readback never resets user input.
+- Label uses the actual NyxID bot label, not the runtime-config `label` placeholder
+ (currently the registration ID). Only a changed label triggers
+ `PATCH /api/v1/channel-bots/{botId}` with `{ "label": "..." }`. NyxID requires a
+ non-empty trimmed label of at most 128 UTF-8 bytes. The PATCH response must
+ confirm the exact bot ID, platform, and label before the safe query cache is
+ updated; credential fields and raw diagnostic bodies are discarded.
+- A changed Skill name submits the complete known runtime config because omitted
+ runtime fields are not patches. Only Skill name is trimmed; other fields are
+ retained. The request omits `authorization_mode` and `service_ids`, allowing
+ the backend to preserve its current service selection, including legacy
+ defaults and explicit empty allowlists. Backend authorization checks still
+ apply; there is no frontend authorization override.
+- When both fields change, Label saves first, then Skill name. A rejected Label
+ request leaves both inputs intact and skips the runtime update. If Label has
+ saved but Skill fails, the editor explicitly reports partial success and a
+ retry submits only the still-unsaved changes. The two APIs are not atomic.
+- A Label-only save returns to details after the confirmed PATCH without
+ submitting runtime config or performing an unnecessary config readback.
- After an acknowledged POST, the form makes one GET for accurate feedback and
returns to channel details. The Save action remains busy across both requests
and rejects duplicate submission. There is no separate confirmation step,
@@ -75,18 +88,17 @@ The API contract was checked against `origin/feature/integrate` at
Focused integration cases cover exact identity, safe decoding, unsupported
config, detail-to-edit navigation, whole-config preservation, accepted readback,
-explicit clearing, unavailable services, field errors, cached detail revalidation,
+explicit clearing, label validation and exact bot identity, partial-save retry,
+field errors, cached detail revalidation,
legacy authorization, delayed/failed readback navigation, duplicate-submit
protection, failed-save retry, unsaved navigation, and unmounts during POST/GET.
Existing creation, channel listing/details, API, navigation, route configuration,
and locale tests protect the reused surfaces.
The original editor was verified against the configured remote backend for
-configuration prefill, service selection, and desktop and 390px layouts.
-The save-flow revision uses the focused integration cases above; the local
-preview compiles on port 5173, its API proxy responds, and the browser shows the
-login page. Its OAuth callback uses the same origin. No live bot configuration
-was changed during verification.
+configuration prefill and desktop and 390px layouts. The names-only revision is
+covered by API-boundary integration tests. No live bot configuration is changed
+for verification.
Local verification is restricted to affected Jest files, changed-file Biome,
the test stability guard, baseline integrity, and diff checks. Full frontend
diff --git a/apps/aevatar-console-web/docs/features/2026-09-14-channel-pages.md b/apps/aevatar-console-web/docs/features/2026-09-14-channel-pages.md
index 1acceaf47..ea448aa27 100644
--- a/apps/aevatar-console-web/docs/features/2026-09-14-channel-pages.md
+++ b/apps/aevatar-console-web/docs/features/2026-09-14-channel-pages.md
@@ -4,9 +4,11 @@ Channels is a first-level destination in the Workflow Activity vNext sidebar.
It lets the signed-in owner view their connected bots and inspect a connection.
The three pages follow the latest simplified [Figma design](https://www.figma.com/design/FaVJx5IZeQX9jHcUi55ndB?node-id=7-226)
-(list frame `7:2`, detail frame `6:131`, Telegram form `7:226`). The current design intentionally keeps
-channel details read-only with Remove as the sole resource action. The earlier
-runtime-configuration editor is outside this iteration of issue #3617.
+(list frame `7:2`, detail frame `6:131`, Telegram form `7:226`). The initial detail
+design used Remove as its sole resource action. The current detail page also
+links to the editor documented in
+[Channel Runtime Editor](../2026-09-15-channel-runtime-editor.md); its edit form
+now exposes only Label and Skill name, preserving existing service authorization.
Channel refresh is explicitly user driven. Do not add background polling or
refresh on focus/reconnection to these pages.
diff --git a/apps/aevatar-console-web/src/locales/channelMessages.en-US.ts b/apps/aevatar-console-web/src/locales/channelMessages.en-US.ts
index 2a360d2a4..e660df8f4 100644
--- a/apps/aevatar-console-web/src/locales/channelMessages.en-US.ts
+++ b/apps/aevatar-console-web/src/locales/channelMessages.en-US.ts
@@ -1,4 +1,11 @@
export default {
+ 'channels.edit.label': 'Label',
+ 'channels.edit.labelError':
+ 'Enter a non-empty label. If it is too long, shorten it and try again.',
+ 'channels.edit.labelFailed':
+ 'Could not save the label. Check it and try again.',
+ 'channels.edit.partialSave':
+ 'Label saved, but the skill name could not be updated. Try saving again.',
'channels.edit': 'Edit',
'channels.edit.title': 'Edit {platform}',
'channels.edit.loadingTitle': 'Edit channel',
diff --git a/apps/aevatar-console-web/src/locales/channelMessages.zh-CN.ts b/apps/aevatar-console-web/src/locales/channelMessages.zh-CN.ts
index d97beb847..67c28c2ca 100644
--- a/apps/aevatar-console-web/src/locales/channelMessages.zh-CN.ts
+++ b/apps/aevatar-console-web/src/locales/channelMessages.zh-CN.ts
@@ -1,4 +1,9 @@
export default {
+ 'channels.edit.label': 'Label',
+ 'channels.edit.labelError': 'Label 不能为空;如果名称过长,请缩短后重试。',
+ 'channels.edit.labelFailed': '无法保存 Label,请检查后重试。',
+ 'channels.edit.partialSave':
+ 'Label 已保存,但 Skill 名称更新失败,请再次保存。',
'channels.edit': '编辑',
'channels.edit.title': '编辑 {platform}',
'channels.edit.loadingTitle': '编辑渠道',
diff --git a/apps/aevatar-console-web/src/pages/workflow-activity-vnext/channels/ChannelDetailsPage.tsx b/apps/aevatar-console-web/src/pages/workflow-activity-vnext/channels/ChannelDetailsPage.tsx
index da28c8874..fb526693e 100644
--- a/apps/aevatar-console-web/src/pages/workflow-activity-vnext/channels/ChannelDetailsPage.tsx
+++ b/apps/aevatar-console-web/src/pages/workflow-activity-vnext/channels/ChannelDetailsPage.tsx
@@ -5,12 +5,13 @@ import {
ExportOutlined,
ReloadOutlined,
} from '@ant-design/icons';
-import { Button, Modal, Tooltip } from 'antd';
+import { Button, Modal } from 'antd';
import * as React from 'react';
import { channelsApi } from '@/shared/api/channelsApi';
import { t } from '@/shared/i18n/messages';
import { history } from '@/shared/navigation/history';
import { AevatarContentSkeleton } from '@/shared/ui/AevatarContentSkeleton';
+import AevatarTooltip from '@/shared/ui/AevatarTooltip';
import { useConsoleToast } from '@/shared/ui/ConsoleToast';
import {
buildChannelEditHref,
@@ -206,7 +207,7 @@ export default function ChannelDetailsPage({
? t('channels.name.loading', 'Loading name…')
: t('channels.name.unavailable', 'Name unavailable'))}
{registration.botId && !channelName && !bots.isPending ? (
-
diff --git a/apps/aevatar-console-web/src/pages/workflow-activity-vnext/channels/ChannelEditPage.test.tsx b/apps/aevatar-console-web/src/pages/workflow-activity-vnext/channels/ChannelEditPage.test.tsx
index 0f0849a4e..44615769e 100644
--- a/apps/aevatar-console-web/src/pages/workflow-activity-vnext/channels/ChannelEditPage.test.tsx
+++ b/apps/aevatar-console-web/src/pages/workflow-activity-vnext/channels/ChannelEditPage.test.tsx
@@ -41,13 +41,24 @@ const response = (value: unknown, status = 200) =>
status,
json: async () => value,
}) as Response;
-const service = {
- id: 'user-service-github',
- slug: 'api-github',
- label: 'GitHub work',
- is_active: true,
- credential_source: { type: 'personal' },
+const botsPath = 'https://nyx.example.test/api/v1/channel-bots';
+const bot = {
+ id: 'bot-nyx-alpha',
+ platform: 'telegram',
+ label: 'Team channel',
};
+const registration = {
+ id: 'registration-alpha',
+ scope_id: 'scope-alpha',
+ platform: 'telegram',
+ nyx_channel_bot_id: bot.id,
+ owned: true,
+};
+function identityResponse(input: unknown) {
+ if (input === botsPath) return response({ bots: [bot] });
+ if (input === '/api/channels/registrations') return response([registration]);
+ return undefined;
+}
const receipt = {
status: 'accepted',
registration_id: 'registration-alpha',
@@ -56,6 +67,10 @@ const receipt = {
const posts = () =>
fetchMock.mock.calls.filter(([, init]) => init?.method === 'POST');
const postBody = () => JSON.parse(String(posts().at(-1)?.[1]?.body));
+const patches = () =>
+ fetchMock.mock.calls.filter(([, init]) => init?.method === 'PATCH');
+const editLabel = (value: string) =>
+ fireEvent.change(screen.getByLabelText('Label'), { target: { value } });
const editSkill = (value: string) =>
fireEvent.change(screen.getByLabelText(/^Skill name/), { target: { value } });
const save = () =>
@@ -75,9 +90,7 @@ beforeEach(() => {
fetchMock.mockImplementation(async (input, init) =>
init?.method === 'POST'
? response(receipt, 202)
- : input === servicePath
- ? response({ services: [service] })
- : response(fixture),
+ : (identityResponse(input) ?? response(fixture)),
);
});
@@ -89,16 +102,8 @@ it('opens Edit from channel details and saves once, preserving hidden config and
let current = fixture;
fetchMock.mockImplementation(async (input, init) => {
if (init?.method === 'POST') return pendingPost;
- if (input === servicePath) return response({ services: [service] });
- if (input === '/api/channels/registrations')
- return response([
- {
- id: 'registration-alpha',
- scope_id: 'scope-alpha',
- platform: 'telegram',
- owned: true,
- },
- ]);
+ const identity = identityResponse(input);
+ if (identity) return identity;
if (String(input).endsWith('/status'))
return response({
registration_id: 'registration-alpha',
@@ -127,9 +132,12 @@ it('opens Edit from channel details and saves once, preserving hidden config and
expect(await screen.findByLabelText(/^Skill name/)).toHaveValue(
'team-helper',
);
- expect(
- await screen.findByRole('checkbox', { name: /GitHub work/ }),
- ).toBeChecked();
+ expect(await screen.findByLabelText('Label')).toHaveValue('Team channel');
+ expect(screen.queryByRole('checkbox')).not.toBeInTheDocument();
+ expect(screen.queryByText('Services')).not.toBeInTheDocument();
+ expect(fetchMock.mock.calls.some(([input]) => input === servicePath)).toBe(
+ false,
+ );
expect(screen.getByRole('button', { name: 'Save changes' })).toBeDisabled();
expect(screen.queryByText('Advanced settings')).not.toBeInTheDocument();
expect(
@@ -143,8 +151,6 @@ it('opens Edit from channel details and saves once, preserving hidden config and
expect(posts()).toHaveLength(1);
expect(posts()[0][0]).toBe(path);
expect(postBody()).toEqual({
- authorization_mode: 'explicit_service_allowlist',
- service_ids: ['user-service-github'],
runtime_config: {
...fixture.runtime_config,
default_skill: { name: 'changed-helper', version: '2.1' },
@@ -177,7 +183,7 @@ it('opens Edit from channel details and saves once, preserving hidden config and
it('returns to details when an accepted change is still delayed, without requiring another confirmation or resubmitting', async () => {
renderEditor();
- fireEvent.click(await screen.findByRole('checkbox', { name: /GitHub work/ }));
+ await screen.findByLabelText('Label');
editSkill('');
save();
await screen.findByText(
@@ -196,20 +202,18 @@ it('returns to details when an accepted change is still delayed, without requiri
3,
);
expect(postBody()).toEqual({
- authorization_mode: 'explicit_service_allowlist',
- service_ids: [],
runtime_config: {
...fixture.runtime_config,
default_skill: { name: '', version: '' },
- nyxid_service_selectors: [],
},
});
});
-it('keeps unavailable saved services visible until explicitly deselected and handles rejected fields without leaking diagnostics', async () => {
+it('does not fetch service choices and handles rejected skill fields without leaking diagnostics', async () => {
let rejected = false;
fetchMock.mockImplementation(async (input, init) => {
- if (input === servicePath) return response({ services: [] });
+ const identity = identityResponse(input);
+ if (identity) return identity;
if (init?.method === 'POST') {
rejected = true;
return response(
@@ -229,14 +233,12 @@ it('keeps unavailable saved services visible until explicitly deselected and han
return response(fixture);
});
renderEditor();
- expect(
- await screen.findByRole('checkbox', { name: /Unavailable service/ }),
- ).toBeChecked();
- editSkill('changed-helper');
- expect(screen.getByRole('button', { name: 'Save changes' })).toBeDisabled();
- fireEvent.click(
- screen.getByRole('checkbox', { name: /Unavailable service/ }),
+ await screen.findByLabelText('Label');
+ expect(fetchMock.mock.calls.some(([input]) => input === servicePath)).toBe(
+ false,
);
+ editSkill('changed-helper');
+ expect(screen.getByRole('button', { name: 'Save changes' })).toBeEnabled();
save();
await screen.findByText(
'Check the skill name. Use no more than 128 characters.',
@@ -305,7 +307,8 @@ it('preserves legacy default authorization and returns to details with accurate
runtime_config: { ...fixture.runtime_config, nyxid_service_selectors: [] },
};
fetchMock.mockImplementation(async (input, init) => {
- if (input === servicePath) return response({ services: [] });
+ const identity = identityResponse(input);
+ if (identity) return identity;
if (init?.method === 'POST') {
submitted = true;
return response(receipt, 202);
@@ -314,18 +317,16 @@ it('preserves legacy default authorization and returns to details with accurate
return response(legacy);
});
renderEditor();
- expect(
- await screen.findByRole('checkbox', { name: 'Use NyxID defaults' }),
- ).toBeChecked();
+ await screen.findByLabelText('Label');
+ expect(screen.queryByRole('checkbox')).not.toBeInTheDocument();
editSkill('updated');
save();
await screen.findByText(
'Changes submitted, but the latest configuration could not be loaded. Refresh the channel details to view it.',
);
- expect(postBody()).toMatchObject({
- authorization_mode: 'nyxid_default',
- service_ids: [],
- });
+ expect(postBody()).not.toHaveProperty('authorization_mode');
+ expect(postBody()).not.toHaveProperty('service_ids');
+ expect(postBody().runtime_config.nyxid_service_selectors).toEqual([]);
expect(history.replace).toHaveBeenCalledWith(
'/scopes/scope-alpha/workflow-activity-vnext/channels/registration-alpha',
);
@@ -345,12 +346,13 @@ it('keeps Save pending through readback and ignores its result after leaving the
resolveRead = resolve;
});
fetchMock.mockImplementation(async (input, init) => {
- if (input === servicePath) return response({ services: [service] });
+ const identity = identityResponse(input);
+ if (identity) return identity;
if (init?.method === 'POST') return response(receipt, 202);
return posts().length ? pendingRead : response(fixture);
});
const view = renderEditor();
- await screen.findByRole('checkbox', { name: /GitHub work/ });
+ await screen.findByLabelText('Label');
editSkill('updated');
save();
await waitFor(() =>
@@ -387,21 +389,17 @@ it('protects unsaved navigation and ignores a response after the editor unmounts
fetchMock.mockImplementation(async (input, init) =>
init?.method === 'POST'
? deferred
- : input === servicePath
- ? response({ services: [] })
- : response({
- ...fixture,
- service_ids: [],
- runtime_config: {
- ...fixture.runtime_config,
- nyxid_service_selectors: [],
- },
- }),
+ : (identityResponse(input) ??
+ response({
+ ...fixture,
+ service_ids: [],
+ runtime_config: {
+ ...fixture.runtime_config,
+ },
+ })),
);
const view = renderEditor();
- await screen.findByText(
- 'No services are available with your current authorization.',
- );
+ await screen.findByLabelText('Label');
editSkill('unsaved');
const unload = new Event('beforeunload', { cancelable: true });
window.dispatchEvent(unload);
@@ -422,3 +420,123 @@ it('protects unsaved navigation and ignores a response after the editor unmounts
2,
);
});
+
+it('updates only the label using the exact NyxID bot ID and refreshes the safe identity cache', async () => {
+ fetchMock.mockImplementation(async (input, init) => {
+ if (init?.method === 'PATCH')
+ return response({
+ ...bot,
+ label: 'New label',
+ access_token: 'TEST_ONLY_SECRET',
+ });
+ return identityResponse(input) ?? response(fixture);
+ });
+ const view = renderEditor();
+ await screen.findByLabelText('Label');
+ editLabel(' New label ');
+ save();
+ await screen.findByText('Channel changes saved.');
+ expect(patches()).toHaveLength(1);
+ expect(patches()[0][0]).toBe(`${botsPath}/bot-nyx-alpha`);
+ expect(JSON.parse(String(patches()[0][1]?.body))).toEqual({
+ label: 'New label',
+ });
+ expect(posts()).toHaveLength(0);
+ expect(fetchMock.mock.calls.filter(([input]) => input === path)).toHaveLength(
+ 1,
+ );
+ expect(
+ view.queryClient.getQueryData(channelKeys.bots('scope-alpha')),
+ ).toEqual([{ ...bot, label: 'New label' }]);
+ expect(
+ JSON.stringify(
+ view.queryClient.getQueryData(channelKeys.bots('scope-alpha')),
+ ),
+ ).not.toContain('TEST_ONLY_SECRET');
+ expect(history.replace).toHaveBeenCalledWith(
+ '/scopes/scope-alpha/workflow-activity-vnext/channels/registration-alpha',
+ );
+});
+
+it('reports a partial save and retries only the failed skill update without changing authorization', async () => {
+ let attempts = 0;
+ fetchMock.mockImplementation(async (input, init) => {
+ if (init?.method === 'PATCH')
+ return response({ ...bot, label: 'New label' });
+ if (init?.method === 'POST')
+ return ++attempts === 1 ? response({}, 503) : response(receipt, 202);
+ return identityResponse(input) ?? response(fixture);
+ });
+ renderEditor();
+ await screen.findByLabelText('Label');
+ editLabel('New label');
+ editSkill('new-skill');
+ save();
+ await screen.findByText(
+ 'Label saved, but the skill name could not be updated. Try saving again.',
+ );
+ expect(history.replace).not.toHaveBeenCalled();
+ expect(screen.getByLabelText('Label')).toHaveValue('New label');
+ expect(screen.getByLabelText(/^Skill name/)).toHaveValue('new-skill');
+ save();
+ await screen.findByText(
+ 'Changes submitted. They may take a moment to appear in channel details.',
+ );
+ expect(patches()).toHaveLength(1);
+ expect(posts()).toHaveLength(2);
+ expect(postBody()).toEqual({
+ runtime_config: {
+ ...fixture.runtime_config,
+ default_skill: { name: 'new-skill', version: '2.1' },
+ },
+ });
+});
+
+it('validates the label before either write and keeps both values when the label request fails', async () => {
+ fetchMock.mockImplementation(async (input, init) => {
+ if (init?.method === 'PATCH')
+ return response({ token: 'TEST_ONLY_SECRET' }, 503);
+ return identityResponse(input) ?? response(fixture);
+ });
+ renderEditor();
+ await screen.findByLabelText('Label');
+ editSkill('new-skill');
+ for (const value of [' ', '界'.repeat(43)]) {
+ editLabel(value);
+ save();
+ expect(screen.getByLabelText('Label')).toHaveAttribute(
+ 'aria-invalid',
+ 'true',
+ );
+ expect(patches()).toHaveLength(0);
+ expect(posts()).toHaveLength(0);
+ }
+ editLabel('Valid label');
+ save();
+ await screen.findByText('Could not save the label. Check it and try again.');
+ expect(screen.getByLabelText('Label')).toHaveValue('Valid label');
+ expect(screen.getByLabelText(/^Skill name/)).toHaveValue('new-skill');
+ expect(posts()).toHaveLength(0);
+ expect(history.replace).not.toHaveBeenCalled();
+ expect(document.body).not.toHaveTextContent('TEST_ONLY_SECRET');
+});
+
+it.each([
+ { ...registration, owned: false },
+ { ...registration, scope_id: 'other-scope' },
+ { ...registration, nyx_channel_bot_id: 'unrelated-bot' },
+ { ...registration, platform: 'lark' },
+])('does not expose an editor without an exact owned bot mapping: %j', async (row) => {
+ fetchMock.mockImplementation(async (input) =>
+ input === '/api/channels/registrations'
+ ? response([row])
+ : (identityResponse(input) ?? response(fixture)),
+ );
+ renderEditor();
+ await screen.findByText(
+ 'This channel is unavailable or you do not have access.',
+ );
+ expect(screen.queryByRole('form')).not.toBeInTheDocument();
+ expect(patches()).toHaveLength(0);
+ expect(posts()).toHaveLength(0);
+});
diff --git a/apps/aevatar-console-web/src/pages/workflow-activity-vnext/channels/ChannelEditPage.tsx b/apps/aevatar-console-web/src/pages/workflow-activity-vnext/channels/ChannelEditPage.tsx
index 38a4e9ccf..89a2cbe1d 100644
--- a/apps/aevatar-console-web/src/pages/workflow-activity-vnext/channels/ChannelEditPage.tsx
+++ b/apps/aevatar-console-web/src/pages/workflow-activity-vnext/channels/ChannelEditPage.tsx
@@ -1,7 +1,12 @@
import { ArrowLeftOutlined } from '@ant-design/icons';
import { useQuery, useQueryClient } from '@tanstack/react-query';
-import { Button, Checkbox, Modal } from 'antd';
+import { Button, Input, Modal } from 'antd';
import * as React from 'react';
+import {
+ type ChannelBotIdentity,
+ isValidChannelBotLabel,
+ updateChannelBotLabel,
+} from '@/shared/api/channelBotsApi';
import {
type ChannelConfigDetail,
ChannelConfigError,
@@ -10,7 +15,6 @@ import {
channelConfigMatches,
channelRuntimeConfigApi,
} from '@/shared/api/channelRuntimeConfigApi';
-import type { ChannelServiceChoice } from '@/shared/api/channelServicesApi';
import { ChannelApiError } from '@/shared/api/channelsApi';
import { t } from '@/shared/i18n/messages';
import { history } from '@/shared/navigation/history';
@@ -21,11 +25,14 @@ import {
buildWorkflowActivitySectionHref,
} from '../navigation';
import WorkflowActivityVNextShell from '../WorkflowActivityVNextShell';
-import ChannelServicePicker from './ChannelServicePicker';
import ChannelSkillField from './ChannelSkillField';
import { channelConnectionCss } from './connectionStyles';
import { ChannelLoadError, platformName } from './presentation';
-import { channelKeys, useChannelServiceChoices } from './queries';
+import {
+ channelKeys,
+ useChannelBotIdentities,
+ useChannelRegistrations,
+} from './queries';
import { channelsCss } from './styles';
export default function ChannelEditPage({
@@ -35,9 +42,10 @@ export default function ChannelEditPage({
readonly scopeId: string;
readonly registrationId: string;
}) {
- const [initial, setInitial] = React.useState
+ {labelError}
+
- {t(
- 'channels.edit.missingServices',
- 'Some saved services are no longer available. Deselect them before saving.',
- )}
-
- {errorText('services')}
- {title}
- {!initial && !config.isError ? (
+ {!initial && !error ? (
{t('channels.edit.configuration', 'Configuration')}
+