diff --git a/app/src/lib/i18n/ar.ts b/app/src/lib/i18n/ar.ts
index 9e1a16f670..57051d6cfa 100644
--- a/app/src/lib/i18n/ar.ts
+++ b/app/src/lib/i18n/ar.ts
@@ -902,6 +902,8 @@ const messages: TranslationMap = {
'channels.status.error': 'خطأ',
'channels.status.configuring': 'جارٍ الضبط',
'channels.defaultMessaging': 'قناة المراسلة الافتراضية',
+ 'channels.setAsDefault': 'تعيين كافتراضي',
+ 'channels.defaultBadge': 'افتراضي',
'webhooks.title': 'الـ Webhooks',
'webhooks.create': 'إنشاء Webhook',
'webhooks.noWebhooks': 'لا توجد Webhooks مضبوطة',
diff --git a/app/src/lib/i18n/bn.ts b/app/src/lib/i18n/bn.ts
index 1bee18079b..92adf39614 100644
--- a/app/src/lib/i18n/bn.ts
+++ b/app/src/lib/i18n/bn.ts
@@ -925,6 +925,8 @@ const messages: TranslationMap = {
'channels.status.error': 'ত্রুটি',
'channels.status.configuring': 'কনফিগার হচ্ছে',
'channels.defaultMessaging': 'ডিফল্ট মেসেজিং চ্যানেল',
+ 'channels.setAsDefault': 'ডিফল্ট হিসেবে সেট করুন',
+ 'channels.defaultBadge': 'ডিফল্ট',
'webhooks.title': 'ওয়েবহুক',
'webhooks.create': 'Webhook তৈরি করুন',
'webhooks.noWebhooks': 'কোনো webhook কনফিগার করা হয়নি',
diff --git a/app/src/lib/i18n/de.ts b/app/src/lib/i18n/de.ts
index 886cae97a3..10dc8844f7 100644
--- a/app/src/lib/i18n/de.ts
+++ b/app/src/lib/i18n/de.ts
@@ -948,6 +948,8 @@ const messages: TranslationMap = {
'channels.status.error': 'Fehler',
'channels.status.configuring': 'Konfigurieren',
'channels.defaultMessaging': 'Standard-Messaging-Kanal',
+ 'channels.setAsDefault': 'Als Standard festlegen',
+ 'channels.defaultBadge': 'Standard',
'webhooks.title': 'Webhooks',
'webhooks.create': 'Erstelle einen Webhook',
'webhooks.noWebhooks': 'Keine Webhooks konfiguriert',
diff --git a/app/src/lib/i18n/en.ts b/app/src/lib/i18n/en.ts
index dfc84facc7..5dc1290946 100644
--- a/app/src/lib/i18n/en.ts
+++ b/app/src/lib/i18n/en.ts
@@ -1258,6 +1258,8 @@ const en: TranslationMap = {
'channels.status.error': 'Error',
'channels.status.configuring': 'Configuring',
'channels.defaultMessaging': 'Default Messaging Channel',
+ 'channels.setAsDefault': 'Set as default',
+ 'channels.defaultBadge': 'Default',
// Webhooks
'webhooks.title': 'Webhooks',
diff --git a/app/src/lib/i18n/es.ts b/app/src/lib/i18n/es.ts
index a5eb55d0f6..9b0ce59e65 100644
--- a/app/src/lib/i18n/es.ts
+++ b/app/src/lib/i18n/es.ts
@@ -943,6 +943,8 @@ const messages: TranslationMap = {
'channels.status.error': 'error',
'channels.status.configuring': 'Configurando',
'channels.defaultMessaging': 'Canal de mensajería predeterminado',
+ 'channels.setAsDefault': 'Establecer como predeterminado',
+ 'channels.defaultBadge': 'Predeterminado',
'webhooks.title': 'Ganchos web',
'webhooks.create': 'Crear webhook',
'webhooks.noWebhooks': 'Sin webhooks configurados',
diff --git a/app/src/lib/i18n/fr.ts b/app/src/lib/i18n/fr.ts
index 2be47ecaa6..c58da8f754 100644
--- a/app/src/lib/i18n/fr.ts
+++ b/app/src/lib/i18n/fr.ts
@@ -946,6 +946,8 @@ const messages: TranslationMap = {
'channels.status.error': 'Erreur',
'channels.status.configuring': 'Configuration en cours',
'channels.defaultMessaging': 'Canal de messagerie par défaut',
+ 'channels.setAsDefault': 'Définir par défaut',
+ 'channels.defaultBadge': 'Par défaut',
'webhooks.title': 'Webhooks',
'webhooks.create': 'Créer un webhook',
'webhooks.noWebhooks': 'Aucun webhook configuré',
diff --git a/app/src/lib/i18n/hi.ts b/app/src/lib/i18n/hi.ts
index 1215abcf53..0dbd720fe7 100644
--- a/app/src/lib/i18n/hi.ts
+++ b/app/src/lib/i18n/hi.ts
@@ -921,6 +921,8 @@ const messages: TranslationMap = {
'channels.status.error': 'एरर',
'channels.status.configuring': 'कॉन्फिगर हो रहा है',
'channels.defaultMessaging': 'डिफ़ॉल्ट मैसेजिंग चैनल',
+ 'channels.setAsDefault': 'डिफ़ॉल्ट के रूप में सेट करें',
+ 'channels.defaultBadge': 'डिफ़ॉल्ट',
'webhooks.title': 'वेबहुक्स',
'webhooks.create': 'Webhook बनाएं',
'webhooks.noWebhooks': 'कोई webhook कॉन्फिगर नहीं है',
diff --git a/app/src/lib/i18n/id.ts b/app/src/lib/i18n/id.ts
index fc59d79241..62f4a37fcf 100644
--- a/app/src/lib/i18n/id.ts
+++ b/app/src/lib/i18n/id.ts
@@ -928,6 +928,8 @@ const messages: TranslationMap = {
'channels.status.error': 'Kesalahan',
'channels.status.configuring': 'Mengonfigurasi',
'channels.defaultMessaging': 'Kanal Pesan Default',
+ 'channels.setAsDefault': 'Jadikan bawaan',
+ 'channels.defaultBadge': 'Bawaan',
'webhooks.title': 'Webhook',
'webhooks.create': 'Buat Webhook',
'webhooks.noWebhooks': 'Belum ada webhook yang dikonfigurasi',
diff --git a/app/src/lib/i18n/it.ts b/app/src/lib/i18n/it.ts
index a161dab496..3916f6697a 100644
--- a/app/src/lib/i18n/it.ts
+++ b/app/src/lib/i18n/it.ts
@@ -940,6 +940,8 @@ const messages: TranslationMap = {
'channels.status.error': 'Errore',
'channels.status.configuring': 'Configurazione',
'channels.defaultMessaging': 'Canale di messaggistica predefinito',
+ 'channels.setAsDefault': 'Imposta come predefinito',
+ 'channels.defaultBadge': 'Predefinito',
'webhooks.title': 'Webhook',
'webhooks.create': 'Crea webhook',
'webhooks.noWebhooks': 'Nessun webhook configurato',
diff --git a/app/src/lib/i18n/ko.ts b/app/src/lib/i18n/ko.ts
index 83028b1ef5..dbdae564c8 100644
--- a/app/src/lib/i18n/ko.ts
+++ b/app/src/lib/i18n/ko.ts
@@ -921,6 +921,8 @@ const messages: TranslationMap = {
'channels.status.error': '오류',
'channels.status.configuring': '구성 중',
'channels.defaultMessaging': '기본 메시징 채널',
+ 'channels.setAsDefault': '기본값으로 설정',
+ 'channels.defaultBadge': '기본값',
'webhooks.title': '웹훅',
'webhooks.create': '웹훅 생성',
'webhooks.noWebhooks': '구성된 웹훅이 없습니다',
diff --git a/app/src/lib/i18n/pl.ts b/app/src/lib/i18n/pl.ts
index ac4ff09f55..30c35e16d4 100644
--- a/app/src/lib/i18n/pl.ts
+++ b/app/src/lib/i18n/pl.ts
@@ -935,6 +935,8 @@ const messages: TranslationMap = {
'channels.status.error': 'Błąd',
'channels.status.configuring': 'Konfigurowanie',
'channels.defaultMessaging': 'Domyślny kanał wiadomości',
+ 'channels.setAsDefault': 'Ustaw jako domyślny',
+ 'channels.defaultBadge': 'Domyślny',
'webhooks.title': 'Webhooki',
'webhooks.create': 'Utwórz webhook',
'webhooks.noWebhooks': 'Brak skonfigurowanych webhooków',
diff --git a/app/src/lib/i18n/pt.ts b/app/src/lib/i18n/pt.ts
index a04a15c116..93f0ad77ea 100644
--- a/app/src/lib/i18n/pt.ts
+++ b/app/src/lib/i18n/pt.ts
@@ -945,6 +945,8 @@ const messages: TranslationMap = {
'channels.status.error': 'Erro',
'channels.status.configuring': 'Configurando',
'channels.defaultMessaging': 'Canal de Mensagens Padrão',
+ 'channels.setAsDefault': 'Definir como padrão',
+ 'channels.defaultBadge': 'Padrão',
'webhooks.title': 'Webhooks',
'webhooks.create': 'Criar Webhook',
'webhooks.noWebhooks': 'Nenhum webhook configurado',
diff --git a/app/src/lib/i18n/ru.ts b/app/src/lib/i18n/ru.ts
index 7fb7b797af..5facab53e7 100644
--- a/app/src/lib/i18n/ru.ts
+++ b/app/src/lib/i18n/ru.ts
@@ -932,6 +932,8 @@ const messages: TranslationMap = {
'channels.status.error': 'Ошибка',
'channels.status.configuring': 'Настройка',
'channels.defaultMessaging': 'Канал по умолчанию',
+ 'channels.setAsDefault': 'Сделать по умолчанию',
+ 'channels.defaultBadge': 'По умолчанию',
'webhooks.title': 'Вебхуки',
'webhooks.create': 'Создать вебхук',
'webhooks.noWebhooks': 'Вебхуки не настроены',
diff --git a/app/src/lib/i18n/zh-CN.ts b/app/src/lib/i18n/zh-CN.ts
index 91140ba13b..65c72f0285 100644
--- a/app/src/lib/i18n/zh-CN.ts
+++ b/app/src/lib/i18n/zh-CN.ts
@@ -881,6 +881,8 @@ const messages: TranslationMap = {
'channels.status.error': '错误',
'channels.status.configuring': '配置中',
'channels.defaultMessaging': '默认消息渠道',
+ 'channels.setAsDefault': '设为默认',
+ 'channels.defaultBadge': '默认',
'webhooks.title': 'Webhook',
'webhooks.create': '创建 Webhook',
'webhooks.noWebhooks': '未配置任何 Webhook',
diff --git a/app/src/pages/Skills.tsx b/app/src/pages/Skills.tsx
index ebc5358775..e057cc8e3b 100644
--- a/app/src/pages/Skills.tsx
+++ b/app/src/pages/Skills.tsx
@@ -256,9 +256,27 @@ interface ChannelTileProps {
icon: React.ReactNode;
testId?: string;
onOpen: () => void;
+ /** Whether this channel is the current default messaging channel. */
+ isDefault: boolean;
+ /** Set this channel as the default. */
+ onSetDefault: () => void;
+ /** Test id for the "set as default" control (kept stable for E2E). */
+ setDefaultTestId?: string;
+ /** Disable the default control while a write is in flight. */
+ setDefaultBusy?: boolean;
}
-function ChannelTile({ def, status, icon, testId, onOpen }: ChannelTileProps) {
+function ChannelTile({
+ def,
+ status,
+ icon,
+ testId,
+ onOpen,
+ isDefault,
+ onSetDefault,
+ setDefaultTestId,
+ setDefaultBusy,
+}: ChannelTileProps) {
const { t } = useT();
const isConnected = status === 'connected';
const isPending = status === 'connecting';
@@ -266,34 +284,81 @@ function ChannelTile({ def, status, icon, testId, onOpen }: ChannelTileProps) {
const statusLabel = channelStatusLabel(status, t);
const ctaLabel = isConnected ? t('skills.configure') : t('channels.setup');
+ // Horizontal tile: icon on the left; name → status → default control stacked
+ // on the right. The tile is a container (not one button) so "configure" (the
+ // icon + name row) and "set as default" stay distinct, focusable controls —
+ // collapsing the old two-selector layout (connect grid + separate picker)
+ // into one place. "Set as default" only appears for channels you can actually
+ // route through (connected); the default badge still shows on whichever
+ // channel is persisted as default, connected or not.
+ const showDefaultControl = isDefault || isConnected;
+
return (
-
-
- {icon}
-
-
-
- {def.display_name}
-
-
- {statusLabel}
-
-
-
+
+
+ {icon}
+
+
+
+ {def.display_name}
+
+
+ {statusLabel}
+
+
+
+ {showDefaultControl && (
+ // Aligns under the name/status text (icon 2.5rem + gap 0.75rem).
+
+ {isDefault ? (
+
+
+
+
+ {t('channels.defaultBadge')}
+
+ ) : (
+
+ {t('channels.setAsDefault')}
+
+ )}
+
+ )}
+
);
}
@@ -1031,49 +1096,55 @@ export default function Skills() {
{t('channels.defaultMessaging')}
-
- {channelsGroup.items.map(item => (
-
-
setChannelModalDef(item.channelDef!)}
- />
+ {/* One unified surface: each tile shows connection status,
+ opens setup/configure on click, and owns the "default
+ messaging channel" selection via its footer control.
+ Connected channels and not-yet-connected channels are
+ rendered as two separate grids (no divider/label) so
+ each group occupies its own rows. */}
+ {(() => {
+ // The built-in web channel needs no connection — treat it
+ // as always available so it stays selectable as default.
+ const statusFor = (def: ChannelDefinition): ChannelConnectionStatus =>
+ def.id === 'web' ? 'connected' : bestChannelStatus(def.id as ChannelType);
+ const renderTile = (def: ChannelDefinition) => {
+ const channelId = def.id as ChannelType;
+ return (
+
+ setChannelModalDef(def)}
+ isDefault={channelConnections.defaultMessagingChannel === channelId}
+ onSetDefault={() => void handleSetDefaultChannel(channelId)}
+ setDefaultTestId={`channel-select-${channelId}`}
+ setDefaultBusy={defaultChannelBusy !== null}
+ />
+
+ );
+ };
+ const connected = channelDefs.filter(d => statusFor(d) === 'connected');
+ const notConnected = channelDefs.filter(d => statusFor(d) !== 'connected');
+ const gridStyle = {
+ gridTemplateColumns: 'repeat(auto-fill, minmax(13rem, 1fr))',
+ };
+ return (
+
+ {connected.length > 0 && (
+
+ {connected.map(renderTile)}
+
+ )}
+ {notConnected.length > 0 && (
+
+ {notConnected.map(renderTile)}
+
+ )}
- ))}
-
-
-
-
- {t('channels.defaultMessaging')}
-
-
- {channelDefs.map(def => {
- const channelId = def.id as ChannelType;
- const selected =
- channelConnections.defaultMessagingChannel === channelId;
- return (
- void handleSetDefaultChannel(channelId)}
- disabled={defaultChannelBusy !== null}
- className={`rounded-lg border px-3 py-2 text-xs font-medium transition-colors ${
- selected
- ? 'border-primary-500/60 bg-primary-50 dark:bg-primary-500/10 text-primary-600 dark:text-primary-300'
- : 'border-stone-200 dark:border-neutral-800 bg-stone-50 dark:bg-neutral-800/60 text-stone-600 dark:text-neutral-300 hover:border-stone-300 dark:hover:border-neutral-700'
- }`}>
- {def.display_name}
-
- );
- })}
-
-
+ );
+ })()}
)}
diff --git a/app/src/pages/__tests__/Skills.channels-grid.test.tsx b/app/src/pages/__tests__/Skills.channels-grid.test.tsx
index 78df779d1c..14a4c5d728 100644
--- a/app/src/pages/__tests__/Skills.channels-grid.test.tsx
+++ b/app/src/pages/__tests__/Skills.channels-grid.test.tsx
@@ -24,14 +24,33 @@ const imessageDef: ChannelDefinition = {
capabilities: [],
};
+// The built-in web channel needs no connection, so the grid always renders it
+// as connected — making it the reliable "connected, not default" tile.
+const webDef: ChannelDefinition = {
+ id: 'web',
+ display_name: 'Web',
+ description: 'Chat via the built-in web UI.',
+ icon: 'web',
+ auth_modes: [],
+ capabilities: [],
+};
+
vi.mock('../../hooks/useChannelDefinitions', () => ({
useChannelDefinitions: () => ({
- definitions: [telegramDef, imessageDef],
+ definitions: [telegramDef, imessageDef, webDef],
loading: false,
error: null,
}),
}));
+const { updatePreferencesMock } = vi.hoisted(() => ({
+ updatePreferencesMock: vi.fn<(channel: string) => Promise>(),
+}));
+
+vi.mock('../../services/api/channelConnectionsApi', () => ({
+ channelConnectionsApi: { updatePreferences: (channel: string) => updatePreferencesMock(channel) },
+}));
+
vi.mock('../../services/api/workflowsApi', async () => {
const actual = await vi.importActual(
'../../services/api/workflowsApi'
@@ -64,6 +83,76 @@ vi.mock('../../lib/composio/hooks', () => ({
describe('Skills page — Channels grid', () => {
beforeEach(() => {
// The default tab is 'composio'; click 'Messaging' to reveal the Channels card.
+ updatePreferencesMock.mockReset();
+ updatePreferencesMock.mockResolvedValue(undefined);
+ });
+
+ it('groups connected channels ahead of not-connected ones', () => {
+ const preloadedState = {
+ channelConnections: {
+ schemaVersion: 1,
+ migrationCompleted: true,
+ defaultMessagingChannel: 'web' as const,
+ connections: {
+ telegram: {
+ managed_dm: undefined,
+ oauth: {
+ channel: 'telegram' as const,
+ authMode: 'oauth' as const,
+ status: 'connected' as const,
+ selectedDefault: false,
+ lastError: null,
+ capabilities: [],
+ updatedAt: new Date().toISOString(),
+ },
+ bot_token: undefined,
+ api_key: undefined,
+ },
+ },
+ },
+ };
+
+ renderWithProviders( , { initialEntries: ['/connections'], preloadedState });
+ fireEvent.click(screen.getByTestId('two-pane-nav-channels'));
+
+ const channelsCard = screen.getByRole('heading', { name: 'Messaging' }).closest('.rounded-2xl');
+ const order = within(channelsCard as HTMLElement)
+ .getAllByTestId(/^skill-row-channel-/)
+ .map(el => el.getAttribute('data-testid'));
+
+ // Connected channels (Telegram + always-on Web) come before the
+ // disconnected one (iMessage), with no separator between the groups.
+ const idxTelegram = order.indexOf('skill-row-channel-telegram');
+ const idxWeb = order.indexOf('skill-row-channel-web');
+ const idxImessage = order.indexOf('skill-row-channel-imessage');
+ expect(idxTelegram).toBeGreaterThanOrEqual(0);
+ expect(idxWeb).toBeGreaterThanOrEqual(0);
+ expect(idxImessage).toBeGreaterThan(idxTelegram);
+ expect(idxImessage).toBeGreaterThan(idxWeb);
+ });
+
+ it('offers "set as default" only on connected tiles and switches the default', async () => {
+ const { store } = renderWithProviders( , { initialEntries: ['/connections'] });
+ fireEvent.click(screen.getByTestId('two-pane-nav-channels'));
+
+ const channelsCard = screen.getByRole('heading', { name: 'Messaging' }).closest('.rounded-2xl');
+ const within$ = within(channelsCard as HTMLElement);
+
+ // The redux default starts on Telegram, so its tile shows the "Default"
+ // badge (there is no longer a second, separate channel-picker list below).
+ expect(within$.getByTestId('channel-select-telegram')).toHaveTextContent('Default');
+ // Web is always connected but not the default → offers "Set as default".
+ const setWebDefault = within$.getByTestId('channel-select-web');
+ expect(setWebDefault).toHaveTextContent('Set as default');
+ // A disconnected, non-default channel offers no default control at all.
+ expect(within$.queryByTestId('channel-select-imessage')).toBeNull();
+
+ fireEvent.click(setWebDefault);
+
+ await vi.waitFor(() =>
+ expect(store.getState().channelConnections.defaultMessagingChannel).toBe('web')
+ );
+ expect(updatePreferencesMock).toHaveBeenCalledWith('web');
});
it('renders configured channels as tiles in a dedicated card and opens the setup modal on click', async () => {
@@ -143,7 +232,12 @@ describe('Skills page — Channels grid', () => {
const telegramTile = within(channelsCard as HTMLElement).getByRole('button', {
name: new RegExp(`Telegram.*${labelPattern.source}`, 'i'),
});
- expect(telegramTile.className).toMatch(classPattern);
+ // The connection-status colour now lives on the tile container (the
+ // inner button only owns the "configure" affordance), so assert against
+ // the wrapping tile rather than the button itself.
+ const tileContainer = telegramTile.closest('.rounded-2xl');
+ expect(tileContainer).not.toBeNull();
+ expect((tileContainer as HTMLElement).className).toMatch(classPattern);
}
);
diff --git a/app/test/e2e/specs/settings-channels-permissions.spec.ts b/app/test/e2e/specs/settings-channels-permissions.spec.ts
index 155e64490f..faba179be8 100644
--- a/app/test/e2e/specs/settings-channels-permissions.spec.ts
+++ b/app/test/e2e/specs/settings-channels-permissions.spec.ts
@@ -11,11 +11,26 @@
* - 13.2.2 Privacy panel renders + analytics toggle is present
*/
import { waitForApp } from '../helpers/app-helpers';
+import { callOpenhumanRpc } from '../helpers/core-rpc';
import { clickSelector, textExists, waitForText } from '../helpers/element-helpers';
import { resetApp } from '../helpers/reset-app';
import { navigateViaHash } from '../helpers/shared-flows';
import { startMockServer, stopMockServer } from '../mock-server';
+/** Read the persisted default messaging channel from the renderer's redux store. */
+async function defaultMessagingChannel(): Promise {
+ return browser.execute(() => {
+ const win = window as unknown as {
+ __OPENHUMAN_STORE__?: {
+ getState?: () => { channelConnections?: { defaultMessagingChannel?: string | null } };
+ };
+ };
+ return (
+ win.__OPENHUMAN_STORE__?.getState?.().channelConnections?.defaultMessagingChannel ?? null
+ );
+ });
+}
+
const USER_ID = 'e2e-settings-channels';
describe('Settings - Channels & Permissions', () => {
@@ -30,35 +45,37 @@ describe('Settings - Channels & Permissions', () => {
});
it('allows switching default messaging channel (13.2.1)', async () => {
- // Phase 2: Default Messaging Channel UI is at /connections (Messaging tab).
- // Old /skills?tab=channels → /connections?tab=messaging.
+ // The messaging panel now offers "Set as default" only on *connected*
+ // channels. In a fresh workspace the only always-connected channel is Web
+ // (the built-in chat), so we make Telegram the default first — that turns
+ // Web into a connected, non-default tile that exposes the control — then
+ // switch the default to Web through the UI.
+ await callOpenhumanRpc('openhuman.channels_set_default', { channel: 'telegram' });
+
+ // Navigate away and back so the messaging panel re-seeds the default from
+ // the core (it reads the persisted default when the page mounts).
+ await navigateViaHash('/home');
await navigateViaHash('/connections?tab=messaging');
await waitForText('Default Messaging Channel', 15_000);
expect(await textExists('Telegram')).toBe(true);
- expect(await textExists('Discord')).toBe(true);
+ expect(await textExists('Web')).toBe(true);
- // Select via the stable channel-select test id rather than the ambiguous
- // "Discord" text (which also appears on connection tiles / help copy).
- await clickSelector('[data-testid="channel-select-discord"]');
- // Confirm the selection persisted to redux state (the Connections messaging
- // tab no longer renders the legacy "Active route" line).
- await browser.waitUntil(
- async () =>
- (await browser.execute(() => {
- const win = window as unknown as {
- __OPENHUMAN_STORE__?: {
- getState?: () => { channelConnections?: { defaultMessagingChannel?: string | null } };
- };
- };
- return win.__OPENHUMAN_STORE__?.getState?.().channelConnections?.defaultMessagingChannel;
- })) === 'discord',
- {
- timeout: 10_000,
- interval: 500,
- timeoutMsg: 'default messaging channel did not switch to discord',
- }
- );
+ // Confirm the panel seeded Telegram as the default before we switch.
+ await browser.waitUntil(async () => (await defaultMessagingChannel()) === 'telegram', {
+ timeout: 10_000,
+ interval: 500,
+ timeoutMsg: 'messaging panel did not seed Telegram as the default',
+ });
+
+ // Switch to Web via the stable channel-select test id (its "Set as default"
+ // control is present because Web is always connected).
+ await clickSelector('[data-testid="channel-select-web"]');
+ await browser.waitUntil(async () => (await defaultMessagingChannel()) === 'web', {
+ timeout: 10_000,
+ interval: 500,
+ timeoutMsg: 'default messaging channel did not switch to web',
+ });
});
it('renders privacy settings and analytics toggle (13.2.2)', async () => {
diff --git a/app/test/e2e/specs/settings-feature-preferences.spec.ts b/app/test/e2e/specs/settings-feature-preferences.spec.ts
index 284a2300a7..ec0617417f 100644
--- a/app/test/e2e/specs/settings-feature-preferences.spec.ts
+++ b/app/test/e2e/specs/settings-feature-preferences.spec.ts
@@ -82,15 +82,27 @@ describe('Settings - Feature Preferences', () => {
});
it('persists the default messaging channel through redux state', async () => {
- // Phase 2: Default Messaging Channel moved to /connections (Messaging tab).
- // Old /skills?tab=channels → /connections?tab=messaging.
+ // The messaging panel exposes "Set as default" only on *connected* channels.
+ // In a fresh workspace the only always-connected channel is Web (built-in
+ // chat), so make Telegram the default first — that turns Web into a
+ // connected, non-default tile with the control — then switch to Web.
+ await callOpenhumanRpc('openhuman.channels_set_default', { channel: 'telegram' });
+
+ // Navigate away and back so the panel re-seeds the default from the core.
+ await navigateViaHash('/home');
await navigateViaHash('/connections?tab=messaging');
await waitForText('Default Messaging Channel', 15_000);
- // Use the stable channel-select test id — "Discord" text also appears on
- // connection tiles and help copy, so clickText could hit the wrong node.
- await clickSelector('[data-testid="channel-select-discord"]', 10_000);
- await browser.waitUntil(async () => (await defaultMessagingChannelFromStore()) === 'discord', {
+ await browser.waitUntil(async () => (await defaultMessagingChannelFromStore()) === 'telegram', {
+ timeout: 10_000,
+ interval: 500,
+ timeoutMsg: 'messaging panel did not seed Telegram as the default',
+ });
+
+ // Switch to Web via its stable channel-select test id (Web is always
+ // connected, so its "Set as default" control is present).
+ await clickSelector('[data-testid="channel-select-web"]', 10_000);
+ await browser.waitUntil(async () => (await defaultMessagingChannelFromStore()) === 'web', {
timeout: 10_000,
interval: 500,
timeoutMsg: 'default channel did not update',
diff --git a/app/test/playwright/specs/settings-channels-permissions.spec.ts b/app/test/playwright/specs/settings-channels-permissions.spec.ts
index 815bf0128d..dd4d7bee05 100644
--- a/app/test/playwright/specs/settings-channels-permissions.spec.ts
+++ b/app/test/playwright/specs/settings-channels-permissions.spec.ts
@@ -2,6 +2,7 @@ import { expect, test } from '@playwright/test';
import {
bootAuthenticatedPage,
+ callCoreRpc,
dismissWalkthroughIfPresent,
waitForAppReady,
} from '../helpers/core-rpc';
@@ -27,7 +28,14 @@ test.describe('Settings - Channels & Permissions', () => {
});
test('allows switching default messaging channel', async ({ page }) => {
- // Phase 2: default messaging channel UI moved to /connections (Messaging tab)
+ // "Set as default" now appears only on *connected* channels. In a fresh
+ // workspace the only always-connected channel is Web (built-in chat), so
+ // make Telegram the default first (turning Web into a connected,
+ // non-default tile with the control), then switch the default to Web.
+ await callCoreRpc('openhuman.channels_set_default', { channel: 'telegram' });
+
+ // Phase 2: default messaging channel UI moved to /connections (Messaging tab).
+ // Mounting the panel re-seeds the default from the core.
await page.goto('/#/connections?tab=messaging');
await waitForAppReady(page);
await dismissWalkthroughIfPresent(page);
@@ -39,10 +47,13 @@ test.describe('Settings - Channels & Permissions', () => {
await expect(page.getByText('Default Messaging Channel').last()).toBeVisible();
await expect(page.getByText('Telegram').last()).toBeVisible();
- await expect(page.getByText('Discord').last()).toBeVisible();
+ await expect(page.getByText('Web').last()).toBeVisible();
+
+ // Confirm the panel seeded Telegram as the default before switching.
+ await expect.poll(() => getDefaultMessagingChannel(page)).toBe('telegram');
- await page.getByText('Discord').last().click();
- await expect.poll(() => getDefaultMessagingChannel(page)).toBe('discord');
+ await page.getByTestId('channel-select-web').click();
+ await expect.poll(() => getDefaultMessagingChannel(page)).toBe('web');
});
test('renders privacy settings and analytics toggle', async ({ page }) => {
diff --git a/app/test/playwright/specs/settings-feature-preferences.spec.ts b/app/test/playwright/specs/settings-feature-preferences.spec.ts
index e73b505845..8ecc3e76c7 100644
--- a/app/test/playwright/specs/settings-feature-preferences.spec.ts
+++ b/app/test/playwright/specs/settings-feature-preferences.spec.ts
@@ -101,22 +101,27 @@ test.describe('Settings - Feature Preferences', () => {
});
test('persists the default messaging channel through redux state', async ({ page }) => {
- // Phase 2: default messaging channel moved to /connections (Messaging tab)
+ // Phase 2: default messaging channel moved to /connections (Messaging tab).
await openAuthenticatedRoute(page, 'pw-settings-default-channel', '/connections?tab=messaging');
+ // "Set as default" now appears only on *connected* channels. In a fresh
+ // workspace the only always-connected channel is Web (built-in chat), so
+ // make Telegram the default first (turning Web into a connected,
+ // non-default tile with the control), reload so the panel re-seeds, then
+ // switch the default to Web.
+ await callCoreRpc('openhuman.channels_set_default', { channel: 'telegram' });
+ await reloadAndWait(page);
+
const messagingTab = page.getByTestId('two-pane-nav-channels');
if (await messagingTab.isVisible().catch(() => false)) {
await messagingTab.click();
}
await expect(page.getByText('Default Messaging Channel').last()).toBeVisible();
- await page
- .locator('button')
- .filter({ hasText: /^Discord$/ })
- .last()
- .click();
+ await expect.poll(() => getDefaultMessagingChannel(page)).toBe('telegram');
- await expect.poll(() => getDefaultMessagingChannel(page)).toBe('discord');
+ await page.getByTestId('channel-select-web').click();
+ await expect.poll(() => getDefaultMessagingChannel(page)).toBe('web');
});
test('persists tools preferences to the core app-state snapshot', async ({ page }) => {