diff --git a/src/web-ui/src/app/components/RemoteConnectDialog/AccountPanel.scss b/src/web-ui/src/app/components/RemoteConnectDialog/AccountPanel.scss index e0e5e2e16e..ccf6767665 100644 --- a/src/web-ui/src/app/components/RemoteConnectDialog/AccountPanel.scss +++ b/src/web-ui/src/app/components/RemoteConnectDialog/AccountPanel.scss @@ -1,4 +1,5 @@ -/** Account content inherits the Remote Connect surface's spacing and typography. */ +@use './remote-connect-surface' as surface; + .account-panel { position: relative; display: flex; @@ -6,135 +7,118 @@ min-width: 0; min-height: 0; flex-direction: column; - font-family: var(--openbitfun-type-body-sm-font-family); &__scroll { + display: flex; flex: 1; min-height: 0; - padding: var(--openbitfun-space-6) var(--openbitfun-space-8) var(--openbitfun-space-8); + flex-direction: column; + gap: surface.$section-gap; + padding: surface.$page-content-gap surface.$page-inline surface.$page-block-start; + + > * { + flex-shrink: 0; + } } &__error-banner { display: flex; + flex-shrink: 0; flex-direction: column; align-items: stretch; gap: var(--openbitfun-space-2); - flex-shrink: 0; - padding: var(--openbitfun-space-3) var(--openbitfun-space-8) 0; + padding: var(--openbitfun-space-4) surface.$page-inline 0; + } + + &__section &__error-banner { + padding: 0; } &__error-action { align-self: flex-start; } - &__login-card { - display: flex; - flex-direction: column; - align-items: flex-start; - gap: var(--openbitfun-space-3); - padding: var(--openbitfun-space-6); - border-radius: var(--openbitfun-layout-field-group-radius); - background: var(--openbitfun-color-surface-tertiary); + &__section { + @include surface.section; } - &__login-icon { - display: inline-flex; - margin-bottom: var(--openbitfun-space-2); - color: var(--openbitfun-color-content-secondary); - } - - &__value-prop { - margin: 0; - color: var(--openbitfun-color-content-primary); - font-size: var(--openbitfun-type-body-md-font-size); - font-weight: var(--openbitfun-type-label-selected-font-weight); - line-height: var(--openbitfun-type-body-md-line-height); + &__section-heading { + @include surface.section-heading; + align-items: center; } - &__security-note { - max-width: 48ch; - margin: 0; - color: var(--openbitfun-color-content-secondary); - font-size: var(--openbitfun-type-body-sm-font-size); - line-height: var(--openbitfun-type-body-sm-line-height); + &__section-title { + @include surface.section-title; } - &__actions { - display: flex; - gap: var(--openbitfun-space-2); - flex-wrap: wrap; - margin-top: var(--openbitfun-space-3); + &__card { + @include surface.card; } - [data-openbitfun-component='button'][data-openbitfun-variant='text'] { color: var(--openbitfun-color-content-secondary); } + // ---------- Account ---------- &__identity-line { - display: flex; - align-items: center; - gap: var(--openbitfun-space-3); - padding-bottom: var(--openbitfun-space-6); - color: var(--openbitfun-color-content-secondary); + @include surface.card-row; + flex-direction: row; - > :first-child { flex-shrink: 0; } + > :first-child { + flex-shrink: 0; + } } &__identity-copy { - display: flex; - flex: 1; - min-width: 0; - flex-direction: column; - gap: var(--openbitfun-space-1); + @include surface.row-copy; + } + + &__identity-name { + @include surface.row-title; + user-select: text; } &__identity-label { - color: var(--openbitfun-color-content-muted); - font-size: var(--openbitfun-type-body-sm-font-size); - line-height: var(--openbitfun-type-body-sm-line-height); + @include surface.supporting-text; } - &__identity-name { - min-width: 0; - color: var(--openbitfun-color-content-primary); - font-size: var(--openbitfun-type-body-md-font-size); - font-weight: var(--openbitfun-type-label-selected-font-weight); - line-height: var(--openbitfun-type-body-md-line-height); - user-select: text; + &__login-card { + @include surface.card-row; + flex-direction: row; + flex-wrap: wrap; } - &__section-heading { - display: flex; - align-items: center; - justify-content: space-between; - gap: var(--openbitfun-space-3); - margin-bottom: var(--openbitfun-space-3); - - h3 { - margin: 0; - color: var(--openbitfun-color-content-secondary); - font-size: var(--openbitfun-type-body-sm-font-size); - font-weight: var(--openbitfun-type-label-selected-font-weight); - line-height: var(--openbitfun-type-body-sm-line-height); - } + &__login-icon { + @include surface.icon-slot; + align-self: flex-start; + } + + &__login-copy { + @include surface.row-copy; + flex-basis: 240px; + } + + &__value-prop { + @include surface.row-title; } - &__devices-card { - overflow: hidden; - border-radius: var(--openbitfun-layout-field-group-radius); - background: var(--openbitfun-color-surface-tertiary); + &__security-note { + @include surface.supporting-text; + max-width: 52ch; + } - .account-panel__error-banner { padding: var(--openbitfun-space-4); } + &__actions { + @include surface.row-trailing; + margin-inline-start: auto; } + // ---------- Devices ---------- + &__device-list { display: flex; flex-direction: column; } &__empty { - padding: var(--openbitfun-space-8) var(--openbitfun-space-4); - color: var(--openbitfun-color-content-muted); - font-size: var(--openbitfun-type-body-sm-font-size); - line-height: var(--openbitfun-type-body-sm-line-height); + @include surface.supporting-text; + padding: var(--openbitfun-space-6) surface.$row-inline; text-align: center; &--loading { @@ -146,16 +130,27 @@ } &__device-card { + position: relative; display: flex; + min-width: 0; + min-height: 64px; align-items: center; - gap: var(--openbitfun-space-3); - min-height: 72px; - padding: var(--openbitfun-space-4); + gap: var(--openbitfun-space-2); + padding: var(--openbitfun-space-3) surface.$row-inline; transition: background-color var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard); - & + & { border-top: var(--openbitfun-border-width-default) solid var(--openbitfun-color-border-subtle); } - &.selectable:has(> .account-panel__device-select:hover) { background: var(--openbitfun-color-action-neutral-surface); } - &.selectable:has(> .account-panel__device-select:active) { background: var(--openbitfun-color-action-neutral-surface-pressed); } + & + & { + @include surface.card-divider; + } + + &.selectable:has(> .account-panel__device-select:hover) { + background: var(--openbitfun-color-action-neutral-surface); + } + + &.selectable:has(> .account-panel__device-select:active) { + background: var(--openbitfun-color-action-neutral-surface-pressed); + } + &.selectable:has(> .account-panel__device-select:focus-visible) { outline: var(--openbitfun-focus-width) solid var(--openbitfun-color-focus-ring); outline-offset: calc(0px - var(--openbitfun-focus-width)); @@ -167,71 +162,120 @@ min-width: 0; flex: 1; align-items: center; - gap: var(--openbitfun-space-3); + gap: surface.$row-gap; padding: 0; border: 0; background: transparent; color: var(--openbitfun-color-content-secondary); font: inherit; - text-align: left; + text-align: start; cursor: default; - > svg { flex-shrink: 0; } - &:is(button):not(:disabled) { cursor: pointer; } - &:focus-visible { outline: 0; } + &:is(button) { + cursor: pointer; + + // The whole row opens the device; row actions sit above this layer. + &::after { + position: absolute; + inset: 0; + content: ''; + } + } + + &:is(button):disabled { + cursor: default; + } + + &:focus-visible { + outline: 0; + } + } + + &__device-glyph { + @include surface.icon-slot; } &__device-info { - display: flex; - min-width: 0; - flex: 1; - flex-direction: column; - gap: var(--openbitfun-space-1); + @include surface.row-copy; } &__device-name { + @include surface.row-title; display: flex; align-items: center; gap: var(--openbitfun-space-2); - min-width: 0; - color: var(--openbitfun-color-content-primary); - font-size: var(--openbitfun-type-body-md-font-size); - font-weight: var(--openbitfun-type-label-selected-font-weight); - line-height: var(--openbitfun-type-body-md-line-height); - > span:not([data-overflow-content]):first-child { min-width: 0; overflow: hidden; } - > .account-panel__device-badge { flex-shrink: 0; } + > span:not([data-overflow-content]):first-child { + min-width: 0; + overflow: hidden; + } + + > .account-panel__device-badge { + flex-shrink: 0; + } } &__device-meta { - /* Plain inline flow: the model/OS text must wrap inside the row instead of - overflowing it, which a wrapping flex row cannot guarantee for a - max-content child. */ + // Inline flow lets the model and OS text wrap inside the row. + @include surface.supporting-text; display: block; - color: var(--openbitfun-color-content-muted); - font-size: var(--openbitfun-type-body-sm-font-size); - line-height: var(--openbitfun-type-body-sm-line-height); overflow-wrap: break-word; } &__device-status { color: var(--openbitfun-color-content-secondary); + + &::before { + display: inline-block; + inline-size: 6px; + block-size: 6px; + margin-inline-end: var(--openbitfun-space-1); + border-radius: var(--openbitfun-radius-pill); + background: var(--openbitfun-color-content-muted); + content: ''; + vertical-align: middle; + } + + &[data-openbitfun-state='online']::before { + background: var(--openbitfun-color-status-success-content); + } } &__device-incompatible { color: var(--openbitfun-color-status-warning-content); + font-family: var(--openbitfun-type-body-sm-font-family); font-size: var(--openbitfun-type-body-sm-font-size); line-height: var(--openbitfun-type-body-sm-line-height); overflow-wrap: break-word; } + &__device-actions { + position: relative; + z-index: 1; + display: flex; + flex-shrink: 0; + align-items: center; + gap: var(--openbitfun-space-1); + } + + &__device-chevron { + @include surface.chevron; + display: inline-flex; + inline-size: 16px; + justify-content: center; + } + &__device-alias-editor { + position: relative; + z-index: 1; display: flex; + min-width: 0; align-items: center; gap: var(--openbitfun-space-2); - min-width: 0; - > [data-openbitfun-component='icon-button'] { flex-shrink: 0; } + > [data-openbitfun-component='icon-button'] { + flex-shrink: 0; + } } &__device-alias-input { @@ -251,11 +295,14 @@ border-radius: inherit; background: color-mix(in srgb, var(--openbitfun-color-surface-raised) 92%, transparent); color: var(--openbitfun-color-content-primary); + font-family: var(--openbitfun-type-body-sm-font-family); font-size: var(--openbitfun-type-body-sm-font-size); backdrop-filter: var(--openbitfun-effect-blur-subtle); } - .spinning { animation: openbitfun-account-panel-spin var(--openbitfun-motion-duration-lazy) linear infinite; } + .spinning { + animation: openbitfun-account-panel-spin var(--openbitfun-motion-duration-lazy) linear infinite; + } } @keyframes openbitfun-account-panel-spin { @@ -264,17 +311,37 @@ } @media (max-width: 760px) { - .account-panel__scroll { padding: var(--openbitfun-space-5); } - .account-panel__error-banner { padding-inline: var(--openbitfun-space-5); } - .account-panel__login-card { padding: var(--openbitfun-space-5); } + .account-panel__scroll { + padding: var(--openbitfun-space-5) surface.$page-inline-compact; + } + + .account-panel__error-banner { + padding-inline: surface.$page-inline-compact; + } } @media (max-width: 380px) { - .account-panel__scroll { padding-inline: var(--openbitfun-space-4); } - .account-panel__device-card { padding-inline: var(--openbitfun-space-3); gap: var(--openbitfun-space-2); } + .account-panel__scroll { + padding-inline: surface.$page-inline-narrow; + } + + .account-panel__error-banner { + padding-inline: surface.$page-inline-narrow; + } + + .account-panel__identity-line, + .account-panel__login-card, + .account-panel__device-card { + padding-inline: var(--openbitfun-space-4); + } } @media (prefers-reduced-motion: reduce) { - .account-panel__device-card { transition: none; } - .account-panel .spinning { animation: none; } + .account-panel__device-card { + transition: none; + } + + .account-panel .spinning { + animation: none; + } } diff --git a/src/web-ui/src/app/components/RemoteConnectDialog/AccountPanel.test.tsx b/src/web-ui/src/app/components/RemoteConnectDialog/AccountPanel.test.tsx index dac7bd05ba..f208f47741 100644 --- a/src/web-ui/src/app/components/RemoteConnectDialog/AccountPanel.test.tsx +++ b/src/web-ui/src/app/components/RemoteConnectDialog/AccountPanel.test.tsx @@ -152,6 +152,10 @@ it('does not offer an update check in a runtime that cannot install updates', as await act(async () => { root.render( {}} />); }); expect(container.textContent).toContain('accountLogin.relayFailureVersionRetired'); expect(container.textContent).not.toContain('update.checkForUpdates'); + // Without a banner action the section heading still offers a way forward. + const retryButtons = Array.from(container.querySelectorAll('button')) + .filter(node => node.textContent === 'accountLogin.retryConnect'); + expect(retryButtons).toHaveLength(1); }); it('offers a retry when the Relay is temporarily unavailable', async () => { @@ -161,7 +165,10 @@ it('offers a retry when the Relay is temporarily unavailable', async () => { await act(async () => { root.render( {}} />); }); expect(container.textContent).toContain('accountLogin.relayFailureUnavailable'); expect(container.textContent).not.toContain('502'); - expect(container.textContent).toContain('accountLogin.retryConnect'); + const retryButtons = Array.from(container.querySelectorAll('button')) + .filter(node => node.textContent === 'accountLogin.retryConnect'); + expect(retryButtons).toHaveLength(1); + expect(retryButtons[0]!.closest('[data-openbitfun-part="error"]')).not.toBeNull(); }); async function retryConnection() { @@ -441,3 +448,34 @@ it('still allows switching to a device whose compatibility flag is absent', asyn await act(async () => { peer!.click(); }); expect(mocks.switchToDevice).toHaveBeenCalledWith('peer', 'Peer build'); }); + +it('groups the account and its devices into titled sections with fixed row columns', async () => { + mocks.accountConnectDevices.mockResolvedValue([]); + mocks.getDeviceInfo.mockReset().mockResolvedValue({ device_id: 'local' }); + mocks.accountListDevices.mockResolvedValue([ + { device_id: 'local', device_name: 'My computer', online: true }, + { device_id: 'peer', device_name: 'Peer build', online: false }, + ]); + await act(async () => { root.render( {}} />); }); + const titles = Array.from(container.querySelectorAll('.account-panel__section-title')).map(node => node.textContent); + expect(titles).toEqual(['accountLogin.accountSectionTitle', 'accountLogin.linkedDevices']); + const statuses = Array.from(container.querySelectorAll('.account-panel__device-status')) + .map(node => node.getAttribute('data-openbitfun-state')); + expect(statuses).toEqual(['online', 'offline']); + for (const row of container.querySelectorAll('[data-openbitfun-part="deviceCard"]')) { + const children = Array.from(row.children).map(node => node.className); + expect(children).toEqual([ + 'account-panel__device-select', + 'account-panel__device-actions', + 'account-panel__device-chevron', + ]); + } +}); + +it('shows the sign-in action inside the account section when signed out', async () => { + mocks.identity = { resolved: true, status: 'signed-out', me: null }; + mocks.accountStatus.mockReset().mockResolvedValue({ logged_in: false }); + await act(async () => { root.render( {}} />); }); + expect(container.querySelector('.account-panel__section-title')?.textContent).toBe('accountLogin.accountSectionTitle'); + expect(container.querySelector('[data-openbitfun-part="form"]')?.textContent).toContain('accountLogin.login'); +}); diff --git a/src/web-ui/src/app/components/RemoteConnectDialog/AccountPanel.tsx b/src/web-ui/src/app/components/RemoteConnectDialog/AccountPanel.tsx index 6eccbb7e06..bfba4d5da5 100644 --- a/src/web-ui/src/app/components/RemoteConnectDialog/AccountPanel.tsx +++ b/src/web-ui/src/app/components/RemoteConnectDialog/AccountPanel.tsx @@ -7,7 +7,7 @@ import { useI18n } from '@/infrastructure/i18n'; import { confirmDanger, } from '@/infrastructure/confirm-dialog'; -import { LogIn, Pencil, Check, X } from 'lucide-react'; +import { LogIn, LogOut, Pencil, Check, X } from 'lucide-react'; import { DeviceSystemGlyph } from '../NavPanel/components/DeviceSystemGlyph'; import { reportedHostKind } from '../NavPanel/deviceInterconnectionOverview'; import { remoteConnectAPI, deviceDisplayName, deviceMetadataLabel } from '@/infrastructure/api/service-api/RemoteConnectAPI'; @@ -138,16 +138,23 @@ interface FailureBannerProps { onAction: (action: RelayFailureAction) => void; } +/** + * The button a failure banner offers, if any. An update check only exists where + * updates can be installed; elsewhere the banner stays a statement instead of + * offering an action that cannot run. + */ +const failureBannerAction = (failure: PanelFailure): RelayFailureAction | null => ( + failure.action === 'retry' || (failure.action === 'check-updates' && canCheckForAppUpdates()) + ? failure.action + : null +); + /** * Red banner plus the single next step for a classified failure. A failure whose * action is the existing sign-in flow renders no extra button. */ const FailureBanner: React.FC = ({ failure, t, busy, onClose, onAction }) => { - // An update check only exists where updates can be installed; elsewhere the - // banner stays a statement instead of offering an action that cannot run. - const actionable = failure.action === 'retry' || (failure.action === 'check-updates' && canCheckForAppUpdates()) - ? failure.action - : null; + const actionable = failureBannerAction(failure); return (
= ({ } }, [handleCheckForUpdates, handleLogin, handleRetryConnect, view]); + const renderSectionHeading = (id: string, title: string, action?: React.ReactNode) => ( +
+

{title}

+ {action} +
+ ); + return ( <>
@@ -824,63 +838,73 @@ export const AccountPanel: React.FC = ({ {loading && view === 'devices' && (
- + {t('accountLogin.processing')}
)} {view === 'login' && ( -
- -

{t('accountLogin.loginValueProp')}

-

{t('accountLogin.securityNote')}

-
- +
+ {renderSectionHeading('account-panel-account-title', t('accountLogin.accountSectionTitle'))} +
+ + + {t('accountLogin.loginValueProp')} + {t('accountLogin.securityNote')} + +
+ +
-
+ )} {view === 'devices' && ( -
- - {username.trim().charAt(0).toUpperCase() || } - - - {t('accountLogin.signedInAccount')} - {username.trim()} - - -
-
-

{t('accountLogin.linkedDevices')}

- -
-
+
+ {renderSectionHeading('account-panel-account-title', t('accountLogin.accountSectionTitle'))} +
+ + {username.trim().charAt(0).toUpperCase() || } + + + {username.trim()} + {t('accountLogin.signedInAccount')} + + +
+
+
+ {renderSectionHeading('account-panel-devices-title', t('accountLogin.linkedDevices'), ( + relayFailure && failureBannerAction(relayFailure) ? null : ( + + ) + ))} {aliasCapability === 'unsupported' && } {relayFailure && ( )} -
- {!relayFailure && devicesReady && devices.length === 0 && ( -
{t('accountLogin.noDevices')}
- )} - {!relayFailure && !devicesReady && ( -
- - {t('accountLogin.loadingDevices')} -
- )} - {!relayFailure && sortedDevices.map((d) => { + {!relayFailure &&
+
+ {devicesReady && devices.length === 0 && ( +
{t('accountLogin.noDevices')}
+ )} + {!devicesReady && ( +
+ + {t('accountLogin.loadingDevices')} +
+ )} + {sortedDevices.map((d) => { const isLocal = localDeviceId === d.device_id; // This machine is selectable while the window renders a peer, // so the dialog can bring the UI back without disconnecting. @@ -927,7 +951,9 @@ export const AccountPanel: React.FC = ({ 'aria-label': t('accountLogin.openDevice', { name: displayName }), } : {})} > - + {/* Renaming replaces the name in place: showing the old name next to an editor reads as two device names. */} @@ -986,12 +1012,11 @@ export const AccountPanel: React.FC = ({ )} - {isSelectable && } {/* The open editor owns the row's actions; keeping rename and delete beside it rendered two competing icon clusters. */} {editingDeviceId !== d.device_id && ( - <> + = ({ title={removeLabel} variant="quiet" /> - + )} +
); })} -
-
+
+
} +
)}
diff --git a/src/web-ui/src/app/components/RemoteConnectDialog/RemoteConnectDialog.contract.test.ts b/src/web-ui/src/app/components/RemoteConnectDialog/RemoteConnectDialog.contract.test.ts index 83d034e3e4..4dc564fe07 100644 --- a/src/web-ui/src/app/components/RemoteConnectDialog/RemoteConnectDialog.contract.test.ts +++ b/src/web-ui/src/app/components/RemoteConnectDialog/RemoteConnectDialog.contract.test.ts @@ -95,6 +95,76 @@ describe('Remote Connect safety contracts', () => { expect(dialogSource).toContain("aria-current={activeView === view ? 'page' : undefined}"); }); + it('gives every view, including the overview, the same page header', () => { + const viewHeader = dialogSource.slice( + dialogSource.indexOf('const renderViewHeader'), + dialogSource.indexOf('const renderConnectedMarker'), + ); + const viewCopy = dialogSource.slice( + dialogSource.indexOf('const VIEW_COPY'), + dialogSource.indexOf('const FEISHU_SETUP_GUIDE_URLS'), + ); + + expect(viewHeader).not.toContain('return null'); + expect(viewHeader).toContain(' { + const accountStyleSource = readFileSync(new URL('./AccountPanel.scss', import.meta.url), 'utf8'); + const surfaceStyleSource = readFileSync(new URL('./_remote-connect-surface.scss', import.meta.url), 'utf8'); + const networkSource = readFileSync(new URL('./RemoteNetworkConnections.tsx', import.meta.url), 'utf8'); + + for (const source of [dialogStyleSource, accountStyleSource]) { + expect(source).toContain("@use './remote-connect-surface' as surface;"); + expect(source).not.toMatch(/font-size:\s*\d/); + expect(source).not.toMatch(/#[0-9a-f]{3,8}\b/i); + } + expect(surfaceStyleSource).toContain('@mixin action-bar'); + expect(dialogStyleSource).toContain('@include surface.action-bar;'); + expect(dialogSource).toContain('className="openbitfun-remote-connect__card-actions"'); + expect(networkSource).toContain('className="openbitfun-remote-connect__card-actions"'); + }); + + it('uses one destructive treatment and real buttons for every link-like action', () => { + const networkSource = readFileSync(new URL('./RemoteNetworkConnections.tsx', import.meta.url), 'utf8'); + + expect(dialogSource).not.toContain('role="link"'); + expect(dialogSource).toContain('className="openbitfun-remote-connect__inline-link" onClick={handleOpenFeishuPlatform}'); + expect(dialogSource).toContain(' -
- {connectionOwner === 'network' && networkConnection.invitationConnected && ( -

{t('remoteConnect.connectedHint')}

- )} - - ); - }; - // ── Network group content ──────────────────────────────────────── const networkLabel = (tabId: NetworkTab | null): string | null => { @@ -917,6 +974,11 @@ export const RemoteConnectDialog: React.FC = ({ return tab ? t(tab.labelKey) : null; }; + const networkDescription = (tabId: NetworkTab): string => { + const tab = NETWORK_TABS.find(item => item.id === tabId); + return tab ? t(tab.descriptionKey) : ''; + }; + const renderNetworkContent = () => { const invitation = connectionOwner === 'network' ? connectionResult : null; const relayUrl = invitationRelayUrl(invitation) @@ -925,17 +987,27 @@ export const RemoteConnectDialog: React.FC = ({ return : } title={networkLabel(networkTab) ?? ''} + description={networkDescription(networkTab)} relayUrl={relayUrl} - settings={networkTab === 'lan' && !invitation && !loading && setSelectedLanIp(String(value))} + options={(lanNetworkInfo?.availableIps ?? []).map(entry => ({ + label: `${entry.ip} — ${entry.interface_name}`, value: entry.ip, + }))} + /> + + )} invitation={invitation} statusState={statusState} loading={loading} @@ -952,291 +1024,235 @@ export const RemoteConnectDialog: React.FC = ({ const renderBotContent = () => { if (statusState !== 'ready' && !connectionResult && !weixinQrSessionKey && !loading) { - return {}} />; + return renderBotCard(renderBotIdentity(renderBotStatus(false))); } + if (isBotConnected && connectedBotTab === botTab) { - const connectedLabel = botLabel(botTab) ?? botTab; - const connectedDescription = t('remoteConnect.botConnectedDescription'); - return ( -
-
- - - {connectedLabel} - {connectedDescription} - -
- {t('remoteConnect.stateConnected')} -
-
+ return renderBotCard(( + <> + {renderBotIdentity(renderBotStatus(true), t('remoteConnect.botConnectedDescription'))} {botTab === 'weixin' && ( -
-