@lobehub/lobehub 2.0.0-next.313 → 2.0.0-next.315

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/apps/desktop/src/main/appBrowsers.ts +4 -1
  3. package/apps/desktop/src/main/controllers/BrowserWindowsCtr.ts +15 -3
  4. package/apps/desktop/src/main/core/browser/Browser.ts +14 -4
  5. package/apps/desktop/src/main/core/browser/BrowserManager.ts +7 -2
  6. package/changelog/v1.json +18 -0
  7. package/e2e/src/steps/community/detail-pages.steps.ts +2 -2
  8. package/e2e/src/steps/community/interactions.steps.ts +6 -6
  9. package/e2e/src/steps/hooks.ts +19 -3
  10. package/package.json +1 -1
  11. package/packages/desktop-bridge/src/index.ts +5 -0
  12. package/packages/electron-client-ipc/src/types/window.ts +3 -2
  13. package/src/app/[variants]/(desktop)/desktop-onboarding/_layout/index.tsx +6 -3
  14. package/src/app/[variants]/(desktop)/desktop-onboarding/components/OnboardingFooterActions.tsx +38 -0
  15. package/src/app/[variants]/(desktop)/desktop-onboarding/features/DataModeStep.tsx +19 -14
  16. package/src/app/[variants]/(desktop)/desktop-onboarding/features/LoginStep.tsx +53 -19
  17. package/src/app/[variants]/(desktop)/desktop-onboarding/features/PermissionsStep.tsx +19 -14
  18. package/src/app/[variants]/(desktop)/desktop-onboarding/index.tsx +8 -7
  19. package/src/app/manifest.ts +1 -1
  20. package/src/features/Electron/titlebar/NavigationBar.tsx +1 -2
  21. package/src/server/manifest.ts +2 -2
  22. package/src/server/services/aiAgent/index.ts +28 -11
  23. package/src/server/services/klavis/index.ts +228 -0
  24. package/src/server/services/market/index.ts +13 -0
  25. package/src/server/services/sandbox/index.ts +84 -18
  26. package/src/server/services/toolExecution/builtin.ts +12 -0
  27. package/src/server/services/toolExecution/index.ts +70 -4
  28. package/src/server/services/toolExecution/serverRuntimes/cloudSandbox.ts +7 -0
  29. package/src/services/electron/system.ts +5 -5
package/CHANGELOG.md CHANGED
@@ -2,6 +2,56 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ## [Version 2.0.0-next.315](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.314...v2.0.0-next.315)
6
+
7
+ <sup>Released on **2026-01-19**</sup>
8
+
9
+ #### ✨ Features
10
+
11
+ - **misc**: Add the cloudEndpoint & Klavis Tools Call in Excuation Task.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### What's improved
19
+
20
+ - **misc**: Add the cloudEndpoint & Klavis Tools Call in Excuation Task, closes [#11627](https://github.com/lobehub/lobe-chat/issues/11627) ([0ffe6c4](https://github.com/lobehub/lobe-chat/commit/0ffe6c4))
21
+
22
+ </details>
23
+
24
+ <div align="right">
25
+
26
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
27
+
28
+ </div>
29
+
30
+ ## [Version 2.0.0-next.314](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.313...v2.0.0-next.314)
31
+
32
+ <sup>Released on **2026-01-19**</sup>
33
+
34
+ #### ✨ Features
35
+
36
+ - **misc**: Improve desktop onboarding window management and footer actions.
37
+
38
+ <br/>
39
+
40
+ <details>
41
+ <summary><kbd>Improvements and Fixes</kbd></summary>
42
+
43
+ #### What's improved
44
+
45
+ - **misc**: Improve desktop onboarding window management and footer actions, closes [#11619](https://github.com/lobehub/lobe-chat/issues/11619) ([6ed280e](https://github.com/lobehub/lobe-chat/commit/6ed280e))
46
+
47
+ </details>
48
+
49
+ <div align="right">
50
+
51
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
52
+
53
+ </div>
54
+
5
55
  ## [Version 2.0.0-next.313](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.312...v2.0.0-next.313)
6
56
 
7
57
  <sup>Released on **2026-01-19**</sup>
@@ -1,3 +1,5 @@
1
+ import { APP_WINDOW_MIN_SIZE } from '@lobechat/desktop-bridge';
2
+
1
3
  import type { BrowserWindowOpts } from './core/browser/Browser';
2
4
 
3
5
  export const BrowsersIdentifiers = {
@@ -11,7 +13,8 @@ export const appBrowsers = {
11
13
  height: 800,
12
14
  identifier: 'app',
13
15
  keepAlive: true,
14
- minWidth: 400,
16
+ minHeight: APP_WINDOW_MIN_SIZE.height,
17
+ minWidth: APP_WINDOW_MIN_SIZE.width,
15
18
  path: '/',
16
19
  showOnInit: true,
17
20
  titleBarStyle: 'hidden',
@@ -1,7 +1,7 @@
1
1
  import type {
2
2
  InterceptRouteParams,
3
3
  OpenSettingsWindowOptions,
4
- WindowResizableParams,
4
+ WindowMinimumSizeParams,
5
5
  WindowSizeParams,
6
6
  } from '@lobechat/electron-client-ipc';
7
7
  import { findMatchingRoute } from '~common/routes';
@@ -81,9 +81,21 @@ export default class BrowserWindowsCtr extends ControllerModule {
81
81
  }
82
82
 
83
83
  @IpcMethod()
84
- setWindowResizable(params: WindowResizableParams) {
84
+ setWindowMinimumSize(params: WindowMinimumSizeParams) {
85
85
  this.withSenderIdentifier((identifier) => {
86
- this.app.browserManager.setWindowResizable(identifier, params.resizable);
86
+ const currentSize = this.app.browserManager.getWindowSize(identifier);
87
+ const nextWindowSize = {
88
+ ...currentSize,
89
+ };
90
+ if (params.height) {
91
+ nextWindowSize.height = Math.max(currentSize.height, params.height);
92
+ }
93
+ if (params.width) {
94
+ nextWindowSize.width = Math.max(currentSize.width, params.width);
95
+ }
96
+
97
+ this.app.browserManager.setWindowSize(identifier, nextWindowSize);
98
+ this.app.browserManager.setWindowMinimumSize(identifier, params);
87
99
  });
88
100
  }
89
101
 
@@ -1,4 +1,4 @@
1
- import { TITLE_BAR_HEIGHT } from '@lobechat/desktop-bridge';
1
+ import { APP_WINDOW_MIN_SIZE, TITLE_BAR_HEIGHT } from '@lobechat/desktop-bridge';
2
2
  import { MainBroadcastEventKey, MainBroadcastParams } from '@lobechat/electron-client-ipc';
3
3
  import {
4
4
  BrowserWindow,
@@ -291,9 +291,19 @@ export default class Browser {
291
291
  });
292
292
  }
293
293
 
294
- setWindowResizable(resizable: boolean): void {
295
- logger.debug(`[${this.identifier}] Setting window resizable: ${resizable}`);
296
- this._browserWindow?.setResizable(resizable);
294
+ setWindowMinimumSize(size: { height?: number; width?: number }): void {
295
+ logger.debug(`[${this.identifier}] Setting window minimum size: ${JSON.stringify(size)}`);
296
+
297
+ const currentMinimumSize = this._browserWindow?.getMinimumSize?.() ?? [0, 0];
298
+ const rawWidth = size.width ?? currentMinimumSize[0];
299
+ const rawHeight = size.height ?? currentMinimumSize[1];
300
+
301
+ // Electron doesn't "reset" minimum size with 0x0 reliably.
302
+ // Treat 0 / negative as fallback to app-level default preset.
303
+ const width = rawWidth > 0 ? rawWidth : APP_WINDOW_MIN_SIZE.width;
304
+ const height = rawHeight > 0 ? rawHeight : APP_WINDOW_MIN_SIZE.height;
305
+
306
+ this._browserWindow?.setMinimumSize?.(width, height);
297
307
  }
298
308
 
299
309
  // ==================== Window Position ====================
@@ -250,9 +250,14 @@ export class BrowserManager {
250
250
  browser?.setWindowSize(size);
251
251
  }
252
252
 
253
- setWindowResizable(identifier: string, resizable: boolean) {
253
+ getWindowSize(identifier: string) {
254
254
  const browser = this.browsers.get(identifier);
255
- browser?.setWindowResizable(resizable);
255
+ return browser?.browserWindow.getBounds();
256
+ }
257
+
258
+ setWindowMinimumSize(identifier: string, size: { height?: number; width?: number }) {
259
+ const browser = this.browsers.get(identifier);
260
+ browser?.setWindowMinimumSize(size);
256
261
  }
257
262
 
258
263
  getIdentifierByWebContents(webContents: WebContents): string | null {
package/changelog/v1.json CHANGED
@@ -1,4 +1,22 @@
1
1
  [
2
+ {
3
+ "children": {
4
+ "features": [
5
+ "Add the cloudEndpoint & Klavis Tools Call in Excuation Task."
6
+ ]
7
+ },
8
+ "date": "2026-01-19",
9
+ "version": "2.0.0-next.315"
10
+ },
11
+ {
12
+ "children": {
13
+ "features": [
14
+ "Improve desktop onboarding window management and footer actions."
15
+ ]
16
+ },
17
+ "date": "2026-01-19",
18
+ "version": "2.0.0-next.314"
19
+ },
2
20
  {
3
21
  "children": {
4
22
  "fixes": [
@@ -68,7 +68,7 @@ Then('I should be on an assistant detail page', async function (this: CustomWorl
68
68
 
69
69
  const currentUrl = this.page.url();
70
70
  // Check if URL matches assistant detail page pattern
71
- const hasAssistantDetail = /\/community\/assistant\/[^#?]+/.test(currentUrl);
71
+ const hasAssistantDetail = /\/community\/agent\/[^#?]+/.test(currentUrl);
72
72
  expect(
73
73
  hasAssistantDetail,
74
74
  `Expected URL to match assistant detail page pattern, but got: ${currentUrl}`,
@@ -138,7 +138,7 @@ Then('I should be on the assistant list page', async function (this: CustomWorld
138
138
  // After back navigation, URL should be /community/agent or /community
139
139
  const isListPage =
140
140
  (currentUrl.includes('/community/agent') &&
141
- !/\/community\/assistant\/[\dA-Za-z-]+$/.test(currentUrl)) ||
141
+ !/\/community\/agent\/[\dA-Za-z-]+$/.test(currentUrl)) ||
142
142
  currentUrl.endsWith('/community') ||
143
143
  currentUrl.includes('/community#');
144
144
 
@@ -230,10 +230,10 @@ When('I click on the sort dropdown', async function (this: CustomWorld) {
230
230
  });
231
231
 
232
232
  When('I select a sort option', async function (this: CustomWorld) {
233
- await this.page.waitForTimeout(500);
233
+ await this.page.waitForTimeout(1000);
234
234
 
235
- // Find and click a sort option (assuming dropdown opens a menu)
236
- const sortOptions = this.page.locator('[role="option"], [role="menuitem"]');
235
+ // The sort dropdown uses checkbox items with role="menuitemcheckbox"
236
+ const sortOptions = this.page.locator('[role="menuitemcheckbox"]');
237
237
 
238
238
  // Wait for options to appear
239
239
  await sortOptions.first().waitFor({ state: 'visible', timeout: 30_000 });
@@ -381,7 +381,7 @@ Then('the URL should contain the category parameter', async function (this: Cust
381
381
  currentUrl.includes('category=') ||
382
382
  currentUrl.includes('tag=') ||
383
383
  // For path-based routing like /community/agent/category-name
384
- /\/community\/assistant\/[^/?]+/.test(currentUrl);
384
+ /\/community\/agent\/[^/?]+/.test(currentUrl);
385
385
 
386
386
  expect(
387
387
  hasCategory,
@@ -433,8 +433,8 @@ Then('I should be navigated to the assistant detail page', async function (this:
433
433
  await this.page.waitForLoadState('networkidle', { timeout: 30_000 });
434
434
 
435
435
  const currentUrl = this.page.url();
436
- // Verify that URL changed and contains /assistant/ followed by an identifier
437
- const hasAssistantDetail = /\/community\/assistant\/[^#?]+/.test(currentUrl);
436
+ // Verify that URL changed and contains /agent/ followed by an identifier
437
+ const hasAssistantDetail = /\/community\/agent\/[^#?]+/.test(currentUrl);
438
438
  const urlChanged = currentUrl !== this.testContext.previousUrl;
439
439
 
440
440
  expect(
@@ -45,10 +45,24 @@ BeforeAll({ timeout: 600_000 }, async function () {
45
45
  // Navigate to signin page
46
46
  await page.goto(`${baseUrl}/signin`, { waitUntil: 'networkidle' });
47
47
 
48
+ // Wait for the page to fully hydrate
49
+ await page.waitForTimeout(2000);
50
+
51
+ // Check if we can find the email input
52
+ const emailInput = page
53
+ .locator('input[id="email"], input[name="email"], input[type="text"]')
54
+ .first();
55
+ const emailInputVisible = await emailInput.isVisible().catch(() => false);
56
+
57
+ if (!emailInputVisible) {
58
+ console.log(
59
+ '⚠️ Login form not available, skipping authentication (tests requiring auth may fail)',
60
+ );
61
+ return;
62
+ }
63
+
48
64
  // Step 1: Enter email
49
65
  console.log(' Step 1: Entering email...');
50
- const emailInput = page.locator('input[id="email"]').first();
51
- await emailInput.waitFor({ state: 'visible', timeout: 30_000 });
52
66
  await emailInput.fill(TEST_USER.email);
53
67
 
54
68
  // Click the next button
@@ -57,7 +71,9 @@ BeforeAll({ timeout: 600_000 }, async function () {
57
71
 
58
72
  // Step 2: Wait for password step and enter password
59
73
  console.log(' Step 2: Entering password...');
60
- const passwordInput = page.locator('input[id="password"]').first();
74
+ const passwordInput = page
75
+ .locator('input[id="password"], input[name="password"], input[type="password"]')
76
+ .first();
61
77
  await passwordInput.waitFor({ state: 'visible', timeout: 30_000 });
62
78
  await passwordInput.fill(TEST_USER.password);
63
79
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/lobehub",
3
- "version": "2.0.0-next.313",
3
+ "version": "2.0.0-next.315",
4
4
  "description": "LobeHub - an open-source,comprehensive AI Agent framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
5
5
  "keywords": [
6
6
  "framework",
@@ -10,3 +10,8 @@ export {
10
10
 
11
11
  // Desktop window constants
12
12
  export const TITLE_BAR_HEIGHT = 38;
13
+
14
+ export const APP_WINDOW_MIN_SIZE = {
15
+ height: 600,
16
+ width: 1000,
17
+ } as const;
@@ -3,6 +3,7 @@ export interface WindowSizeParams {
3
3
  width?: number;
4
4
  }
5
5
 
6
- export interface WindowResizableParams {
7
- resizable: boolean;
6
+ export interface WindowMinimumSizeParams {
7
+ height?: number;
8
+ width?: number;
8
9
  }
@@ -3,7 +3,7 @@
3
3
  import { TITLE_BAR_HEIGHT } from '@lobechat/desktop-bridge';
4
4
  import { Center, Flexbox, Text } from '@lobehub/ui';
5
5
  import { Divider } from 'antd';
6
- import { cx } from 'antd-style';
6
+ import { css, cx } from 'antd-style';
7
7
  import type { FC, PropsWithChildren } from 'react';
8
8
 
9
9
  import SimpleTitleBar from '@/features/Electron/titlebar/SimpleTitleBar';
@@ -13,6 +13,9 @@ import { useIsDark } from '@/hooks/useIsDark';
13
13
 
14
14
  import { styles } from './style';
15
15
 
16
+ const contentContainer = css`
17
+ overflow: auto;
18
+ `;
16
19
  const OnboardingContainer: FC<PropsWithChildren> = ({ children }) => {
17
20
  const isDarkMode = useIsDark();
18
21
  return (
@@ -44,9 +47,9 @@ const OnboardingContainer: FC<PropsWithChildren> = ({ children }) => {
44
47
  <ThemeButton placement={'bottomRight'} size={18} />
45
48
  </Flexbox>
46
49
  </Flexbox>
47
- <Center height={'100%'} padding={16} width={'100%'}>
50
+ <Flexbox align={'center'} className={cx(contentContainer)} height={'100%'} width={'100%'}>
48
51
  {children}
49
- </Center>
52
+ </Flexbox>
50
53
  <Center padding={24}>
51
54
  <Text align={'center'} type={'secondary'}>
52
55
  © 2025 LobeHub. All rights reserved.
@@ -0,0 +1,38 @@
1
+ import { Flexbox, type FlexboxProps } from '@lobehub/ui';
2
+ import { cssVar } from 'antd-style';
3
+ import { type ReactNode, memo } from 'react';
4
+
5
+ interface OnboardingFooterActionsProps extends Omit<FlexboxProps, 'children'> {
6
+ left?: ReactNode;
7
+ right?: ReactNode;
8
+ }
9
+
10
+ const OnboardingFooterActions = memo<OnboardingFooterActionsProps>(
11
+ ({ left, right, style, ...rest }) => {
12
+ return (
13
+ <Flexbox
14
+ align={'center'}
15
+ horizontal
16
+ justify={'space-between'}
17
+ style={{
18
+ background: cssVar.colorBgContainer,
19
+ bottom: 0,
20
+ marginTop: 'auto',
21
+ paddingTop: 16,
22
+ position: 'sticky',
23
+ width: '100%',
24
+ zIndex: 10,
25
+ ...style,
26
+ }}
27
+ {...rest}
28
+ >
29
+ <div>{left}</div>
30
+ <div>{right}</div>
31
+ </Flexbox>
32
+ );
33
+ },
34
+ );
35
+
36
+ OnboardingFooterActions.displayName = 'OnboardingFooterActions';
37
+
38
+ export default OnboardingFooterActions;
@@ -10,6 +10,7 @@ import { useUserStore } from '@/store/user';
10
10
  import { userGeneralSettingsSelectors } from '@/store/user/selectors';
11
11
 
12
12
  import LobeMessage from '../components/LobeMessage';
13
+ import OnboardingFooterActions from '../components/OnboardingFooterActions';
13
14
 
14
15
  type DataMode = 'share' | 'privacy';
15
16
 
@@ -48,7 +49,7 @@ const DataModeStep = memo<DataModeStepProps>(({ onBack, onNext }) => {
48
49
  );
49
50
 
50
51
  return (
51
- <Flexbox gap={16}>
52
+ <Flexbox gap={16} style={{ height: '100%', minHeight: '100%' }}>
52
53
  <Flexbox>
53
54
  <LobeMessage sentences={[t('screen4.title'), t('screen4.title2'), t('screen4.title3')]} />
54
55
  <Text as={'p'}>{t('screen4.description')}</Text>
@@ -113,19 +114,23 @@ const DataModeStep = memo<DataModeStepProps>(({ onBack, onNext }) => {
113
114
  <Text color={cssVar.colorTextSecondary} fontSize={12} style={{ marginTop: 16 }}>
114
115
  {t('screen4.footerNote')}
115
116
  </Text>
116
- <Flexbox horizontal justify={'space-between'} style={{ marginTop: 32 }}>
117
- <Button
118
- icon={Undo2Icon}
119
- onClick={onBack}
120
- style={{ color: cssVar.colorTextDescription }}
121
- type={'text'}
122
- >
123
- {t('back')}
124
- </Button>
125
- <Button onClick={onNext} type={'primary'}>
126
- {t('next')}
127
- </Button>
128
- </Flexbox>
117
+ <OnboardingFooterActions
118
+ left={
119
+ <Button
120
+ icon={Undo2Icon}
121
+ onClick={onBack}
122
+ style={{ color: cssVar.colorTextDescription }}
123
+ type={'text'}
124
+ >
125
+ {t('back')}
126
+ </Button>
127
+ }
128
+ right={
129
+ <Button onClick={onNext} type={'primary'}>
130
+ {t('next')}
131
+ </Button>
132
+ }
133
+ />
129
134
  </Flexbox>
130
135
  );
131
136
  });
@@ -1,6 +1,10 @@
1
1
  'use client';
2
2
 
3
- import { AuthorizationProgress, useWatchBroadcast } from '@lobechat/electron-client-ipc';
3
+ import {
4
+ AuthorizationPhase,
5
+ AuthorizationProgress,
6
+ useWatchBroadcast,
7
+ } from '@lobechat/electron-client-ipc';
4
8
  import { Alert, Button, Center, Flexbox, Icon, Input, Text } from '@lobehub/ui';
5
9
  import { Divider } from 'antd';
6
10
  import { cssVar } from 'antd-style';
@@ -9,6 +13,7 @@ import { memo, useEffect, useState } from 'react';
9
13
  import { useTranslation } from 'react-i18next';
10
14
 
11
15
  import { isDesktop } from '@/const/version';
16
+ import UserInfo from '@/features/User/UserInfo';
12
17
  import { remoteServerService } from '@/services/electron/remoteServer';
13
18
  import { useElectronStore } from '@/store/electron';
14
19
  import { setDesktopAutoOidcFirstOpenHandled } from '@/utils/electron/autoOidc';
@@ -21,6 +26,13 @@ type LoginMethod = 'cloud' | 'selfhost';
21
26
  // 登录状态类型
22
27
  type LoginStatus = 'idle' | 'loading' | 'success' | 'error';
23
28
 
29
+ const authorizationPhaseI18nKeyMap: Record<AuthorizationPhase, string> = {
30
+ browser_opened: 'screen5.auth.phase.browserOpened',
31
+ cancelled: 'screen5.actions.cancel',
32
+ verifying: 'screen5.auth.phase.verifying',
33
+ waiting_for_auth: 'screen5.auth.phase.waitingForAuth',
34
+ };
35
+
24
36
  const loginMethodMetas = {
25
37
  cloud: {
26
38
  descriptionKey: 'screen5.methods.cloud.description',
@@ -181,6 +193,7 @@ const LoginStep = memo<LoginStepProps>(({ onBack, onNext }) => {
181
193
  setAuthProgress(progress);
182
194
  if (progress.phase === 'cancelled') {
183
195
  setCloudLoginStatus('idle');
196
+ setSelfhostLoginStatus('idle');
184
197
  setAuthProgress(null);
185
198
  }
186
199
  });
@@ -188,6 +201,7 @@ const LoginStep = memo<LoginStepProps>(({ onBack, onNext }) => {
188
201
  const handleCancelAuth = async () => {
189
202
  await remoteServerService.cancelAuthorization();
190
203
  setCloudLoginStatus('idle');
204
+ setSelfhostLoginStatus('idle');
191
205
  setAuthProgress(null);
192
206
  };
193
207
 
@@ -195,13 +209,19 @@ const LoginStep = memo<LoginStepProps>(({ onBack, onNext }) => {
195
209
  const renderCloudContent = () => {
196
210
  if (cloudLoginStatus === 'success') {
197
211
  return (
198
- <Flexbox gap={12} style={{ width: '100%' }}>
212
+ <Flexbox gap={16} style={{ width: '100%' }}>
199
213
  <Alert
200
214
  description={t('authResult.success.desc')}
201
215
  style={{ width: '100%' }}
202
216
  title={t('authResult.success.title')}
203
217
  type={'success'}
204
218
  />
219
+ <UserInfo
220
+ style={{
221
+ background: cssVar.colorFillSecondary,
222
+ borderRadius: 8,
223
+ }}
224
+ />
205
225
  <Button
206
226
  block
207
227
  disabled={isSigningOut || isConnectingServer}
@@ -239,27 +259,35 @@ const LoginStep = memo<LoginStepProps>(({ onBack, onNext }) => {
239
259
  }
240
260
 
241
261
  if (cloudLoginStatus === 'loading') {
262
+ const phaseText = t(authorizationPhaseI18nKeyMap[authProgress?.phase ?? 'browser_opened'], {
263
+ defaultValue: t('screen5.actions.signingIn'),
264
+ });
265
+ const remainingSeconds = authProgress
266
+ ? Math.max(0, Math.ceil((authProgress.maxPollTime - authProgress.elapsed) / 1000))
267
+ : null;
268
+
242
269
  return (
243
270
  <Flexbox gap={8} style={{ width: '100%' }}>
244
271
  <Button block disabled={true} icon={Cloud} loading={true} size={'large'} type={'primary'}>
245
- {authProgress
246
- ? t(`screen5.auth.phase.${authProgress.phase}`, {
247
- defaultValue: t('screen5.actions.signingIn'),
248
- })
249
- : t('screen5.actions.signingIn')}
272
+ {t('screen5.actions.signingIn')}
250
273
  </Button>
251
- {authProgress && (
252
- <Flexbox align={'center'} horizontal justify={'space-between'}>
274
+ <Text style={{ color: cssVar.colorTextDescription }} type={'secondary'}>
275
+ {phaseText}
276
+ </Text>
277
+ <Flexbox align={'center'} horizontal justify={'space-between'}>
278
+ {remainingSeconds !== null ? (
253
279
  <Text style={{ color: cssVar.colorTextDescription }} type={'secondary'}>
254
280
  {t('screen5.auth.remaining', {
255
- time: Math.round((authProgress.maxPollTime - authProgress.elapsed) / 1000),
281
+ time: remainingSeconds,
256
282
  })}
257
283
  </Text>
258
- <Button onClick={handleCancelAuth} size={'small'} type={'text'}>
259
- {t('screen5.actions.cancel')}
260
- </Button>
261
- </Flexbox>
262
- )}
284
+ ) : (
285
+ <div />
286
+ )}
287
+ <Button onClick={handleCancelAuth} size={'small'} type={'text'}>
288
+ {t('screen5.actions.cancel')}
289
+ </Button>
290
+ </Flexbox>
263
291
  </Flexbox>
264
292
  );
265
293
  }
@@ -283,13 +311,19 @@ const LoginStep = memo<LoginStepProps>(({ onBack, onNext }) => {
283
311
  const renderSelfhostContent = () => {
284
312
  if (selfhostLoginStatus === 'success') {
285
313
  return (
286
- <Flexbox gap={12} style={{ width: '100%' }}>
314
+ <Flexbox gap={16} style={{ width: '100%' }}>
287
315
  <Alert
288
316
  description={t('authResult.success.desc')}
289
317
  style={{ width: '100%' }}
290
318
  title={t('authResult.success.title')}
291
319
  type={'success'}
292
320
  />
321
+ <UserInfo
322
+ style={{
323
+ background: cssVar.colorFillSecondary,
324
+ borderRadius: 8,
325
+ }}
326
+ />
293
327
  <Button
294
328
  block
295
329
  disabled={isSigningOut || isConnectingServer}
@@ -354,8 +388,8 @@ const LoginStep = memo<LoginStepProps>(({ onBack, onNext }) => {
354
388
  };
355
389
 
356
390
  return (
357
- <Flexbox gap={32}>
358
- <Flexbox>
391
+ <Center gap={32} style={{ height: '100%', minHeight: '100%' }}>
392
+ <Flexbox align={'flex-start'} justify={'flex-start'} style={{ width: '100%' }}>
359
393
  <LobeMessage sentences={[t('screen5.title'), t('screen5.title2'), t('screen5.title3')]} />
360
394
  <Text as={'p'}>{t('screen5.description')}</Text>
361
395
  </Flexbox>
@@ -401,7 +435,7 @@ const LoginStep = memo<LoginStepProps>(({ onBack, onNext }) => {
401
435
  </Button>
402
436
  </Flexbox>
403
437
  )}
404
- </Flexbox>
438
+ </Center>
405
439
  );
406
440
  });
407
441
 
@@ -18,6 +18,7 @@ import { useTranslation } from 'react-i18next';
18
18
  import { ensureElectronIpc } from '@/utils/electron/ipc';
19
19
 
20
20
  import LobeMessage from '../components/LobeMessage';
21
+ import OnboardingFooterActions from '../components/OnboardingFooterActions';
21
22
 
22
23
  type PermissionMeta = {
23
24
  descriptionKey: string;
@@ -154,7 +155,7 @@ const PermissionsStep = memo<PermissionsStepProps>(({ onBack, onNext }) => {
154
155
  };
155
156
 
156
157
  return (
157
- <Flexbox gap={16}>
158
+ <Flexbox gap={16} style={{ height: '100%', minHeight: '100%' }}>
158
159
  <Flexbox>
159
160
  <LobeMessage sentences={[t('screen3.title'), t('screen3.title2'), t('screen3.title3')]} />
160
161
  <Text as={'p'}>{t('screen3.description')}</Text>
@@ -207,19 +208,23 @@ const PermissionsStep = memo<PermissionsStepProps>(({ onBack, onNext }) => {
207
208
  </Block>
208
209
  ))}
209
210
  </Block>
210
- <Flexbox horizontal justify={'space-between'} style={{ marginTop: 32 }}>
211
- <Button
212
- icon={Undo2Icon}
213
- onClick={onBack}
214
- style={{ color: cssVar.colorTextDescription }}
215
- type={'text'}
216
- >
217
- {t('back')}
218
- </Button>
219
- <Button onClick={onNext} type={'primary'}>
220
- {t('next')}
221
- </Button>
222
- </Flexbox>
211
+ <OnboardingFooterActions
212
+ left={
213
+ <Button
214
+ icon={Undo2Icon}
215
+ onClick={onBack}
216
+ style={{ color: cssVar.colorTextDescription }}
217
+ type={'text'}
218
+ >
219
+ {t('back')}
220
+ </Button>
221
+ }
222
+ right={
223
+ <Button onClick={onNext} type={'primary'}>
224
+ {t('next')}
225
+ </Button>
226
+ }
227
+ />
223
228
  </Flexbox>
224
229
  );
225
230
  });