feat(devices): add browser device pairing flow (#15678)

*  feat: add browser device pairing flow to /settings/devices

- Add "Via Browser" tab to ConnectDeviceModal with pairing code display and input
- Add "Register this browser as a device" callout card above DeviceList
- Support ?pair=<code> URL param to auto-open browser pairing modal with pre-filled code
- Improve DeviceList empty state with method cards (Desktop + CLI)
- Ship en-US and zh-CN i18n keys for all new browser/sync strings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 🔨 fix(devices): fix lint warnings — import sort order and empty catch block

* fix(devices): add pair API route and invalidate device list cache

- Create /api/devices/pair POST handler that authenticates the user via
  Better Auth session, validates the code against the user's registered
  devices via DeviceModel.findByDeviceId, and returns JSON.
- Replace the setListKey/key-prop re-mount trick with
  lambdaQuery.useUtils().device.listDevices.invalidate() so the tRPC
  React Query cache is properly busted after a successful pair (fixes
  staleTime: 30s preventing the new device from appearing).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* ♻️ refactor(devices): drop browser pairing, fix modal close, redesign UI

- Remove the "Via Browser" pairing flow entirely: browser tab in
  ConnectDeviceModal, the "register this browser" callout card, the
  ?pair=<code> deep-link, and the /api/devices/pair stub route. Only the
  real Desktop and CLI connection methods remain.
- Fix the modal that couldn't be closed: @lobehub/ui Modal closes via
  onCancel (antd), not onClose — the X button was a no-op.
- Redesign the connect modal (segmented tabs, numbered steps, command
  blocks with copy, security footer) and the empty state (onboarding
  hero with Desktop/CLI options + capability cards).
- Clean up browser/sync i18n keys; add capabilities + footer keys for
  en-US and zh-CN.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* 💄 fix(devices): apply card radius — cssVar.borderRadius already has unit

The radius tokens (cssVar.borderRadius / borderRadiusLG) already include
their unit, so the trailing `px` produced `var(--…)px`, which browsers
drop — leaving the cards with sharp corners. Drop the `px` so the cards
pick up the same rounded radius as the appearance settings FormGroup.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
LiJian
2026-06-11 19:50:28 +08:00
committed by GitHub
parent e20496e444
commit 1130f7df32
6 changed files with 561 additions and 13 deletions
+33 -1
View File
@@ -280,7 +280,33 @@
"defaultAgent.title": "New Agent",
"devices.actions.edit": "Edit",
"devices.actions.remove": "Remove",
"devices.capabilities.commands.desc": "Safely execute terminal commands in your environment.",
"devices.capabilities.commands.title": "Run commands",
"devices.capabilities.files.desc": "Let agents directly access and organize the files on your computer.",
"devices.capabilities.files.title": "Read & write local files",
"devices.capabilities.title": "What you can do once connected",
"devices.capabilities.tools.desc": "Connect local tools to extend what agents can do.",
"devices.capabilities.tools.title": "Call system tools",
"devices.channel.connected": "Connected {{time}}",
"devices.connectWizard.button": "Connect Device",
"devices.connectWizard.cli.connectDesc": "Start the background daemon to keep the device online and listening for remote operations.",
"devices.connectWizard.cli.connectTitle": "Start the daemon",
"devices.connectWizard.cli.installDesc": "Install the LobeHub CLI globally with your preferred package manager to enable device connectivity and management.",
"devices.connectWizard.cli.installTitle": "Install the CLI",
"devices.connectWizard.cli.loginDesc": "Complete OAuth authorization in your browser to link the CLI with your account.",
"devices.connectWizard.cli.loginTitle": "Sign in",
"devices.connectWizard.desktop.downloadLink": "Download LobeHub Desktop",
"devices.connectWizard.desktop.step1": "Download the desktop app",
"devices.connectWizard.desktop.step1Desc": "Visit the LobeHub downloads page and get the app for your operating system.",
"devices.connectWizard.desktop.step2": "Sign in and open the device gateway",
"devices.connectWizard.desktop.step2Desc": "After signing in, click the device gateway icon in the top-right corner and confirm it's turned on.",
"devices.connectWizard.desktop.step3": "Your device appears automatically",
"devices.connectWizard.desktop.step3Desc": "The desktop app registers itself as a device on launch — you'll see it in the list once connected.",
"devices.connectWizard.footer": "Only device metadata is registered — your data is never accessed.",
"devices.connectWizard.method.cli": "Via CLI",
"devices.connectWizard.method.desktop": "Via Desktop",
"devices.connectWizard.subtitle": "Choose how to connect your computer to LobeHub.",
"devices.connectWizard.title": "Connect Device",
"devices.currentBadge": "This device",
"devices.detail.addDir": "Add directory",
"devices.detail.connections": "Connections",
@@ -294,7 +320,13 @@
"devices.edit.friendlyNamePlaceholder": "A name to recognize this device",
"devices.edit.save": "Save",
"devices.edit.title": "Edit device",
"devices.empty": "No devices yet. Connect one with `lh connect` or by signing in to the desktop app.",
"devices.empty.desc": "Once connected, LobeHub agents can read/write files, run commands, and call system tools directly on your computer.",
"devices.empty.methodCli.desc": "Install the CLI in your terminal — great for servers or headless machines.",
"devices.empty.methodCli.title": "Connect via CLI",
"devices.empty.methodDesktop.badge": "Recommended",
"devices.empty.methodDesktop.desc": "Download the desktop app, sign in, and your device connects automatically.",
"devices.empty.methodDesktop.title": "Connect via Desktop",
"devices.empty.title": "Connect your first device",
"devices.fallbackBadge": "Unstable identity",
"devices.fallbackTooltip": "This device couldn't be identified by its machine ID, so reinstalling the app may create a duplicate entry.",
"devices.lastSeen": "Last active {{time}}",
+33 -1
View File
@@ -280,7 +280,33 @@
"defaultAgent.title": "新建助理",
"devices.actions.edit": "编辑",
"devices.actions.remove": "移除",
"devices.capabilities.commands.desc": "在你的环境中安全地执行终端命令。",
"devices.capabilities.commands.title": "运行命令",
"devices.capabilities.files.desc": "让智能体直接访问并整理你电脑上的文件。",
"devices.capabilities.files.title": "读写本地文件",
"devices.capabilities.title": "连接后你能做什么",
"devices.capabilities.tools.desc": "连接本地工具,扩展智能体的能力边界。",
"devices.capabilities.tools.title": "调用系统工具",
"devices.channel.connected": "已连接 {{time}}",
"devices.connectWizard.button": "连接设备",
"devices.connectWizard.cli.connectDesc": "启动后台守护进程,保持设备在线并等待远程操作。",
"devices.connectWizard.cli.connectTitle": "启动守护进程",
"devices.connectWizard.cli.installDesc": "使用你喜欢的包管理器全局安装 LobeHub CLI,提供设备连接与管理能力。",
"devices.connectWizard.cli.installTitle": "安装 CLI",
"devices.connectWizard.cli.loginDesc": "在浏览器中完成 OAuth 授权,将 CLI 与你的账号关联。",
"devices.connectWizard.cli.loginTitle": "登录账号",
"devices.connectWizard.desktop.downloadLink": "下载 LobeHub 桌面端",
"devices.connectWizard.desktop.step1": "下载桌面端",
"devices.connectWizard.desktop.step1Desc": "前往 LobeHub 下载页获取适用于你操作系统的桌面版应用。",
"devices.connectWizard.desktop.step2": "登录并打开设备网关",
"devices.connectWizard.desktop.step2Desc": "登录后,点击右上角的设备网关图标,确认开关已打开。",
"devices.connectWizard.desktop.step3": "设备自动出现",
"devices.connectWizard.desktop.step3Desc": "桌面端启动后会自动注册为设备,连接成功后即可在列表中看到它。",
"devices.connectWizard.footer": "仅注册设备信息,不会访问你的数据。",
"devices.connectWizard.method.cli": "通过 CLI",
"devices.connectWizard.method.desktop": "通过桌面端",
"devices.connectWizard.subtitle": "选择一种方式将你的电脑连接到 LobeHub。",
"devices.connectWizard.title": "连接设备",
"devices.currentBadge": "当前设备",
"devices.detail.addDir": "添加目录",
"devices.detail.connections": "连接通道",
@@ -294,7 +320,13 @@
"devices.edit.friendlyNamePlaceholder": "便于识别此设备的名称",
"devices.edit.save": "保存",
"devices.edit.title": "编辑设备",
"devices.empty": "还没有设备。用 `lh connect` 连接,或登录桌面端应用。",
"devices.empty.desc": "连接后,LobeHub 上的智能体可以直接在你的电脑上读写文件、运行命令和调用系统工具。",
"devices.empty.methodCli.desc": "通过终端安装 CLI,适用于服务器或无桌面环境。",
"devices.empty.methodCli.title": "使用 CLI 连接",
"devices.empty.methodDesktop.badge": "推荐",
"devices.empty.methodDesktop.desc": "下载桌面端,登录后设备自动连接。",
"devices.empty.methodDesktop.title": "使用桌面端连接",
"devices.empty.title": "连接你的第一台设备",
"devices.fallbackBadge": "身份不稳定",
"devices.fallbackTooltip": "未能通过机器 ID 识别此设备,重装应用可能会产生重复条目。",
"devices.lastSeen": "最近活跃 {{time}}",
+44 -2
View File
@@ -353,8 +353,50 @@ export default {
'devices.edit.friendlyNamePlaceholder': 'A name to recognize this device',
'devices.edit.save': 'Save',
'devices.edit.title': 'Edit device',
'devices.empty':
'No devices yet. Connect one with `lh connect` or by signing in to the desktop app.',
'devices.capabilities.commands.desc': 'Safely execute terminal commands in your environment.',
'devices.capabilities.commands.title': 'Run commands',
'devices.capabilities.files.desc':
'Let agents directly access and organize the files on your computer.',
'devices.capabilities.files.title': 'Read & write local files',
'devices.capabilities.title': 'What you can do once connected',
'devices.capabilities.tools.desc': 'Connect local tools to extend what agents can do.',
'devices.capabilities.tools.title': 'Call system tools',
'devices.connectWizard.button': 'Connect Device',
'devices.connectWizard.cli.connectDesc':
'Start the background daemon to keep the device online and listening for remote operations.',
'devices.connectWizard.cli.connectTitle': 'Start the daemon',
'devices.connectWizard.cli.installDesc':
'Install the LobeHub CLI globally with your preferred package manager to enable device connectivity and management.',
'devices.connectWizard.cli.installTitle': 'Install the CLI',
'devices.connectWizard.cli.loginDesc':
'Complete OAuth authorization in your browser to link the CLI with your account.',
'devices.connectWizard.cli.loginTitle': 'Sign in',
'devices.connectWizard.desktop.downloadLink': 'Download LobeHub Desktop',
'devices.connectWizard.desktop.step1': 'Download the desktop app',
'devices.connectWizard.desktop.step1Desc':
'Visit the LobeHub downloads page and get the app for your operating system.',
'devices.connectWizard.desktop.step2': 'Sign in and open the device gateway',
'devices.connectWizard.desktop.step2Desc':
"After signing in, click the device gateway icon in the top-right corner and confirm it's turned on.",
'devices.connectWizard.desktop.step3': 'Your device appears automatically',
'devices.connectWizard.desktop.step3Desc':
"The desktop app registers itself as a device on launch — you'll see it in the list once connected.",
'devices.connectWizard.footer':
'Only device metadata is registered — your data is never accessed.',
'devices.connectWizard.method.cli': 'Via CLI',
'devices.connectWizard.method.desktop': 'Via Desktop',
'devices.connectWizard.subtitle': 'Choose how to connect your computer to LobeHub.',
'devices.connectWizard.title': 'Connect Device',
'devices.empty.desc':
'Once connected, LobeHub agents can read/write files, run commands, and call system tools directly on your computer.',
'devices.empty.methodCli.desc':
'Install the CLI in your terminal — great for servers or headless machines.',
'devices.empty.methodCli.title': 'Connect via CLI',
'devices.empty.methodDesktop.badge': 'Recommended',
'devices.empty.methodDesktop.desc':
'Download the desktop app, sign in, and your device connects automatically.',
'devices.empty.methodDesktop.title': 'Connect via Desktop',
'devices.empty.title': 'Connect your first device',
'devices.fallbackBadge': 'Unstable identity',
'devices.fallbackTooltip':
"This device couldn't be identified by its machine ID, so reinstalling the app may create a duplicate entry.",
@@ -0,0 +1,215 @@
'use client';
import { DOWNLOAD_URL } from '@lobechat/const';
import { Button, CopyButton, Flexbox, Icon, Modal, Segmented, Text } from '@lobehub/ui';
import { createStaticStyles, cssVar } from 'antd-style';
import { DownloadIcon, MonitorDownIcon, ShieldCheckIcon, TerminalIcon } from 'lucide-react';
import { memo, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
const styles = createStaticStyles(({ css }) => ({
codeBlock: css`
display: flex;
gap: 12px;
align-items: center;
padding-block: 10px;
padding-inline: 14px;
border: 1px solid ${cssVar.colorBorderSecondary};
border-radius: ${cssVar.borderRadius};
background: ${cssVar.colorFillQuaternary};
`,
command: css`
overflow: hidden;
flex: 1;
font-family: ${cssVar.fontFamilyCode};
font-size: 13px;
color: ${cssVar.colorText};
text-overflow: ellipsis;
white-space: nowrap;
`,
footer: css`
margin-block-start: 4px;
padding-block-start: 16px;
border-block-start: 1px solid ${cssVar.colorBorderSecondary};
font-size: 12px;
color: ${cssVar.colorTextTertiary};
`,
index: css`
display: flex;
flex-shrink: 0;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: 50%;
font-size: 12px;
font-weight: 600;
color: ${cssVar.colorPrimary};
background: ${cssVar.colorPrimaryBg};
`,
line: css`
flex: 1;
width: 1px;
margin-block-start: 4px;
background: ${cssVar.colorBorderSecondary};
`,
stepDesc: css`
font-size: 13px;
line-height: 1.6;
color: ${cssVar.colorTextTertiary};
`,
subtitle: css`
font-size: 13px;
color: ${cssVar.colorTextTertiary};
`,
}));
interface ConnectDeviceModalProps {
initialTab?: 'cli' | 'desktop';
onClose: () => void;
open: boolean;
}
interface StepProps {
children?: React.ReactNode;
desc: string;
index: number;
last?: boolean;
title: string;
}
const Step = memo<StepProps>(({ index, title, desc, children, last }) => (
<Flexbox horizontal gap={14}>
<Flexbox align={'center'}>
<span className={styles.index}>{index}</span>
{!last && <span className={styles.line} />}
</Flexbox>
<Flexbox flex={1} gap={4} style={{ paddingBlockEnd: last ? 0 : 24 }}>
<Text style={{ fontSize: 14, fontWeight: 500 }}>{title}</Text>
<Text className={styles.stepDesc}>{desc}</Text>
{children && <div style={{ marginBlockStart: 12 }}>{children}</div>}
</Flexbox>
</Flexbox>
));
const CommandLine = memo<{ command: string }>(({ command }) => (
<div className={styles.codeBlock}>
<code className={styles.command}>{command}</code>
<CopyButton content={command} size={'small'} />
</div>
));
const cliCommands = {
connect: 'lh connect --daemon',
install: 'npm install -g @lobehub/cli',
login: 'lh login',
};
const ConnectDeviceModal = memo<ConnectDeviceModalProps>(({ onClose, open, initialTab }) => {
const { t } = useTranslation('setting');
const [active, setActive] = useState<'cli' | 'desktop'>(initialTab ?? 'desktop');
useEffect(() => {
if (open) setActive(initialTab ?? 'desktop');
}, [open, initialTab]);
return (
<Modal
footer={null}
open={open}
title={t('devices.connectWizard.title')}
width={560}
onCancel={onClose}
>
<Flexbox gap={20}>
<Text className={styles.subtitle}>{t('devices.connectWizard.subtitle')}</Text>
<Segmented
block
value={active}
options={[
{
icon: <Icon icon={MonitorDownIcon} />,
label: t('devices.connectWizard.method.desktop'),
value: 'desktop',
},
{
icon: <Icon icon={TerminalIcon} />,
label: t('devices.connectWizard.method.cli'),
value: 'cli',
},
]}
onChange={(value) => setActive(value as 'cli' | 'desktop')}
/>
{active === 'desktop' ? (
<Flexbox>
<Step
desc={t('devices.connectWizard.desktop.step1Desc')}
index={1}
title={t('devices.connectWizard.desktop.step1')}
>
<a href={DOWNLOAD_URL.default} rel="noreferrer" target="_blank">
<Button icon={<Icon icon={DownloadIcon} />} size={'small'} type={'primary'}>
{t('devices.connectWizard.desktop.downloadLink')}
</Button>
</a>
</Step>
<Step
desc={t('devices.connectWizard.desktop.step2Desc')}
index={2}
title={t('devices.connectWizard.desktop.step2')}
/>
<Step
last
desc={t('devices.connectWizard.desktop.step3Desc')}
index={3}
title={t('devices.connectWizard.desktop.step3')}
/>
</Flexbox>
) : (
<Flexbox>
<Step
desc={t('devices.connectWizard.cli.installDesc')}
index={1}
title={t('devices.connectWizard.cli.installTitle')}
>
<CommandLine command={cliCommands.install} />
</Step>
<Step
desc={t('devices.connectWizard.cli.loginDesc')}
index={2}
title={t('devices.connectWizard.cli.loginTitle')}
>
<CommandLine command={cliCommands.login} />
</Step>
<Step
last
desc={t('devices.connectWizard.cli.connectDesc')}
index={3}
title={t('devices.connectWizard.cli.connectTitle')}
>
<CommandLine command={cliCommands.connect} />
</Step>
</Flexbox>
)}
<Flexbox horizontal align={'center'} className={styles.footer} gap={8}>
<Icon icon={ShieldCheckIcon} size={14} style={{ color: cssVar.colorPrimary }} />
{t('devices.connectWizard.footer')}
</Flexbox>
</Flexbox>
</Modal>
);
});
ConnectDeviceModal.displayName = 'ConnectDeviceModal';
export default ConnectDeviceModal;
@@ -1,36 +1,166 @@
'use client';
import { isDesktop } from '@lobechat/const';
import { Flexbox, Skeleton, Text } from '@lobehub/ui';
import { Flexbox, Icon, Skeleton, Text } from '@lobehub/ui';
import { createStaticStyles, cssVar } from 'antd-style';
import {
ChevronRightIcon,
FolderCogIcon,
type LucideIcon,
MonitorDownIcon,
ShieldCheckIcon,
TerminalIcon,
ZapIcon,
} from 'lucide-react';
import { memo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { lambdaQuery } from '@/libs/trpc/client';
import { useElectronStore } from '@/store/electron';
import ConnectDeviceModal from './ConnectDeviceModal';
import DeviceDetailPanel from './DeviceDetailPanel';
import DeviceItem from './DeviceItem';
const styles = createStaticStyles(({ css }) => ({
badge: css`
padding-block: 1px;
padding-inline: 8px;
border-radius: 999px;
font-size: 11px;
font-weight: 500;
color: ${cssVar.colorPrimary};
background: ${cssVar.colorPrimaryBg};
`,
capabilityCard: css`
padding: 16px;
border: 1px solid ${cssVar.colorBorderSecondary};
border-radius: ${cssVar.borderRadiusLG};
background: ${cssVar.colorBgContainer};
transition: border-color 0.2s;
&:hover {
border-color: ${cssVar.colorPrimaryBorder};
}
`,
capabilityIcon: css`
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: ${cssVar.borderRadius};
color: ${cssVar.colorText};
background: ${cssVar.colorFillSecondary};
`,
detailCol: css`
align-self: stretch;
min-width: 0;
border: 1px solid ${cssVar.colorBorderSecondary};
border-radius: ${cssVar.borderRadiusLG};
`,
empty: css`
padding-block: 48px;
color: ${cssVar.colorTextTertiary};
emptyCard: css`
overflow: hidden;
border: 1px solid ${cssVar.colorBorderSecondary};
border-radius: ${cssVar.borderRadiusLG};
background: ${cssVar.colorBgContainer};
`,
emptyHero: css`
padding-block: 40px;
padding-inline: 32px;
border-block-end: 1px solid ${cssVar.colorBorderSecondary};
text-align: center;
background: ${cssVar.colorFillQuaternary};
`,
heroIcon: css`
display: flex;
align-items: center;
justify-content: center;
width: 56px;
height: 56px;
border-radius: ${cssVar.borderRadiusLG};
color: ${cssVar.colorPrimary};
background: ${cssVar.colorPrimaryBg};
`,
listCol: css`
min-width: 0;
border: 1px solid ${cssVar.colorBorderSecondary};
border-radius: ${cssVar.borderRadiusLG};
`,
option: css`
cursor: pointer;
padding: 20px;
background: ${cssVar.colorBgContainer};
transition: background 0.2s;
&:hover {
background: ${cssVar.colorFillQuaternary};
}
`,
optionGrid: css`
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1px;
background: ${cssVar.colorBorderSecondary};
`,
optionIcon: css`
display: flex;
flex-shrink: 0;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
border-radius: ${cssVar.borderRadius};
color: ${cssVar.colorTextSecondary};
background: ${cssVar.colorFillSecondary};
`,
subtitle: css`
font-size: 13px;
color: ${cssVar.colorTextTertiary};
`,
}));
interface ConnectOptionProps {
badge?: string;
desc: string;
icon: LucideIcon;
onClick: () => void;
title: string;
}
const ConnectOption = memo<ConnectOptionProps>(({ icon, title, desc, badge, onClick }) => (
<Flexbox horizontal align={'flex-start'} className={styles.option} gap={14} onClick={onClick}>
<span className={styles.optionIcon}>
<Icon icon={icon} size={20} />
</span>
<Flexbox flex={1} gap={2}>
<Flexbox horizontal align={'center'} gap={8}>
<Text style={{ fontSize: 14, fontWeight: 500 }}>{title}</Text>
{badge && <span className={styles.badge}>{badge}</span>}
</Flexbox>
<Text className={styles.subtitle} style={{ fontSize: 12 }}>
{desc}
</Text>
</Flexbox>
<Icon icon={ChevronRightIcon} size={16} style={{ color: cssVar.colorTextQuaternary }} />
</Flexbox>
));
const DeviceList = memo(() => {
const { t } = useTranslation('setting');
const { data: devices, isLoading } = lambdaQuery.device.listDevices.useQuery(undefined, {
@@ -48,14 +178,93 @@ const DeviceList = memo(() => {
// No device is selected by default — the detail panel only appears once the
// user clicks a row.
const [selectedId, setSelectedId] = useState<string>();
const [connectTab, setConnectTab] = useState<'cli' | 'desktop'>();
const openConnect = (tab: 'cli' | 'desktop') => setConnectTab(tab);
if (isLoading) return <Skeleton active paragraph={{ rows: 4 }} title={false} />;
if (!devices || devices.length === 0)
return (
<Flexbox align={'center'} className={styles.empty} justify={'center'}>
<Text type={'secondary'}>{t('devices.empty')}</Text>
</Flexbox>
<>
<Flexbox gap={32}>
{/* Onboarding card: hero + the two real connection methods */}
<Flexbox className={styles.emptyCard}>
<Flexbox align={'center'} className={styles.emptyHero} gap={12}>
<span className={styles.heroIcon}>
<Icon icon={MonitorDownIcon} size={28} />
</span>
<Text style={{ fontSize: 18, fontWeight: 600 }}>{t('devices.empty.title')}</Text>
<Text className={styles.subtitle} style={{ maxWidth: 440 }}>
{t('devices.empty.desc')}
</Text>
</Flexbox>
<div className={styles.optionGrid}>
<ConnectOption
badge={t('devices.empty.methodDesktop.badge')}
desc={t('devices.empty.methodDesktop.desc')}
icon={MonitorDownIcon}
title={t('devices.empty.methodDesktop.title')}
onClick={() => openConnect('desktop')}
/>
<ConnectOption
desc={t('devices.empty.methodCli.desc')}
icon={TerminalIcon}
title={t('devices.empty.methodCli.title')}
onClick={() => openConnect('cli')}
/>
</div>
</Flexbox>
{/* Capabilities unlocked once a device is connected */}
<Flexbox gap={16}>
<Flexbox horizontal align={'center'} gap={8}>
<Icon icon={ShieldCheckIcon} size={16} style={{ color: cssVar.colorPrimary }} />
<Text style={{ fontSize: 14, fontWeight: 500 }}>
{t('devices.capabilities.title')}
</Text>
</Flexbox>
<Flexbox horizontal gap={16}>
{[
{
desc: t('devices.capabilities.files.desc'),
icon: FolderCogIcon,
title: t('devices.capabilities.files.title'),
},
{
desc: t('devices.capabilities.commands.desc'),
icon: TerminalIcon,
title: t('devices.capabilities.commands.title'),
},
{
desc: t('devices.capabilities.tools.desc'),
icon: ZapIcon,
title: t('devices.capabilities.tools.title'),
},
].map((cap) => (
<Flexbox className={styles.capabilityCard} flex={1} gap={12} key={cap.title}>
<span className={styles.capabilityIcon}>
<Icon icon={cap.icon} size={18} />
</span>
<Flexbox gap={2}>
<Text style={{ fontSize: 14, fontWeight: 500 }}>{cap.title}</Text>
<Text className={styles.subtitle} style={{ fontSize: 12 }}>
{cap.desc}
</Text>
</Flexbox>
</Flexbox>
))}
</Flexbox>
</Flexbox>
</Flexbox>
<ConnectDeviceModal
initialTab={connectTab}
open={!!connectTab}
onClose={() => setConnectTab(undefined)}
/>
</>
);
const selected = selectedId ? devices.find((d) => d.deviceId === selectedId) : undefined;
+20 -2
View File
@@ -1,19 +1,37 @@
'use client';
import { memo } from 'react';
import { Button, Icon } from '@lobehub/ui';
import { MonitorUpIcon } from 'lucide-react';
import { memo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import SettingHeader from '@/routes/(main)/settings/features/SettingHeader';
import ConnectDeviceModal from './features/ConnectDeviceModal';
import DeviceList from './features/DeviceList';
const Page = memo(() => {
const { t } = useTranslation('setting');
const [connectModalOpen, setConnectModalOpen] = useState(false);
return (
<>
<SettingHeader title={t('devices.title')} />
<SettingHeader
title={t('devices.title')}
extra={
<Button
icon={<Icon icon={MonitorUpIcon} />}
size={'small'}
onClick={() => setConnectModalOpen(true)}
>
{t('devices.connectWizard.button')}
</Button>
}
/>
<DeviceList />
<ConnectDeviceModal open={connectModalOpen} onClose={() => setConnectModalOpen(false)} />
</>
);
});