From c3ef4bbb60fcd919ba22738ca8de46354cdcd06f Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Thu, 9 Apr 2026 08:41:05 +0800 Subject: [PATCH] Merge account and ACP settings into advanced config --- lib/app/app_shell_desktop.dart | 46 +- lib/app/workspace_page_registry.dart | 9 +- lib/features/mobile/mobile_shell_core.dart | 4 +- .../mobile/mobile_shell_workspace.dart | 9 - lib/features/settings/settings_page_core.dart | 14 + .../settings/settings_page_gateway.dart | 38 +- .../settings/settings_page_gateway_acp.dart | 480 ++++++++++++++---- .../settings/settings_page_sections.dart | 53 +- .../settings/settings_page_widgets.dart | 9 +- lib/models/app_models.dart | 6 +- lib/widgets/sidebar_navigation.dart | 14 +- .../settings_page_acp_bridge_mode_suite.dart | 62 ++- test/features/settings_page_suite.dart | 101 ++-- test/test_support.dart | 76 +-- test/widgets/sidebar_navigation_suite.dart | 8 +- 15 files changed, 608 insertions(+), 321 deletions(-) diff --git a/lib/app/app_shell_desktop.dart b/lib/app/app_shell_desktop.dart index 4c5cff95..3b4a08ec 100644 --- a/lib/app/app_shell_desktop.dart +++ b/lib/app/app_shell_desktop.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; -import '../features/account/account_page.dart'; import '../features/mobile/mobile_shell.dart'; import '../i18n/app_language.dart'; import '../models/app_models.dart'; @@ -189,7 +188,7 @@ class _AppShellState extends State { constraints.maxWidth > 1280; final mobileDestination = controller.destination == WorkspaceDestination.account - ? WorkspaceDestination.assistant + ? WorkspaceDestination.settings : controller.destination; final availableMobileDestinations = _mobileDestinations .where(controller.capabilities.supportsDestination) @@ -220,33 +219,6 @@ class _AppShellState extends State { ); } - void openAccountSheet() { - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (sheetContext) { - return Container( - margin: EdgeInsets.fromLTRB( - 12, - MediaQuery.of(sheetContext).padding.top + 12, - 12, - 12, - ), - decoration: BoxDecoration( - color: palette.surfacePrimary, - borderRadius: BorderRadius.circular(28), - border: Border.all(color: palette.strokeSoft), - ), - child: SafeArea( - top: false, - child: AccountPage(controller: controller), - ), - ); - }, - ); - } - if (isCompactMobile) { return MobileShell(controller: controller); } @@ -313,21 +285,7 @@ class _AppShellState extends State { ), ], ), - Positioned( - right: 24, - bottom: 96, - child: - controller.capabilities.supportsDestination( - WorkspaceDestination.account, - ) - ? FloatingActionButton.small( - onPressed: openAccountSheet, - child: const Icon( - Icons.account_circle_rounded, - ), - ) - : const SizedBox.shrink(), - ), + const SizedBox.shrink(), ], ); } diff --git a/lib/app/workspace_page_registry.dart b/lib/app/workspace_page_registry.dart index ab243fd1..0a9ab9c6 100644 --- a/lib/app/workspace_page_registry.dart +++ b/lib/app/workspace_page_registry.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; -import '../features/account/account_page.dart'; import '../features/assistant/assistant_page.dart'; import '../features/claw_hub/claw_hub_page.dart'; import '../features/mcp_server/mcp_server_page.dart'; @@ -121,22 +120,22 @@ workspacePageSpecsInternal = { initialTab: controller.settingsTab, initialDetail: controller.settingsDetail, navigationContext: controller.settingsNavigationContext, - showSectionTabs: false, + showSectionTabs: true, ), mobileBuilder: (controller, onOpenDetail) => SettingsPage( controller: controller, initialTab: controller.settingsTab, initialDetail: controller.settingsDetail, navigationContext: controller.settingsNavigationContext, - showSectionTabs: false, + showSectionTabs: true, ), ), WorkspaceDestination.account: WorkspacePageSpec( destination: WorkspaceDestination.account, desktopBuilder: (controller, onOpenDetail) => - AccountPage(controller: controller), + SettingsPage(controller: controller, initialTab: SettingsTab.gateway), mobileBuilder: (controller, onOpenDetail) => - AccountPage(controller: controller), + SettingsPage(controller: controller, initialTab: SettingsTab.gateway), ), }; diff --git a/lib/features/mobile/mobile_shell_core.dart b/lib/features/mobile/mobile_shell_core.dart index c71ec746..df304311 100644 --- a/lib/features/mobile/mobile_shell_core.dart +++ b/lib/features/mobile/mobile_shell_core.dart @@ -101,10 +101,10 @@ class MobileShellStateInternal extends State { WorkspaceDestination.agents || WorkspaceDestination.mcpServer || WorkspaceDestination.clawHub || - WorkspaceDestination.aiGateway || - WorkspaceDestination.account => MobileShellTab.workspace, + WorkspaceDestination.aiGateway => MobileShellTab.workspace, WorkspaceDestination.secrets => MobileShellTab.secrets, WorkspaceDestination.settings => MobileShellTab.settings, + WorkspaceDestination.account => MobileShellTab.settings, }; } diff --git a/lib/features/mobile/mobile_shell_workspace.dart b/lib/features/mobile/mobile_shell_workspace.dart index 3ec8b8f8..3f258932 100644 --- a/lib/features/mobile/mobile_shell_workspace.dart +++ b/lib/features/mobile/mobile_shell_workspace.dart @@ -72,15 +72,6 @@ class MobileWorkspaceLauncherInternal extends StatelessWidget { iconColor: palette.accent, iconBackground: palette.accentMuted, ), - WorkspaceEntryInternal( - destination: WorkspaceDestination.account, - subtitle: appText( - '身份、工作区与会话', - 'Identity, workspace and sessions', - ), - iconColor: palette.success, - iconBackground: palette.success.withValues(alpha: 0.12), - ), ] .where( (entry) => diff --git a/lib/features/settings/settings_page_core.dart b/lib/features/settings/settings_page_core.dart index d80674b8..9f878808 100644 --- a/lib/features/settings/settings_page_core.dart +++ b/lib/features/settings/settings_page_core.dart @@ -60,6 +60,10 @@ class SettingsPageStateInternal extends State { late final TextEditingController aiGatewayApiKeyRefControllerInternal; late final TextEditingController aiGatewayApiKeyControllerInternal; late final TextEditingController aiGatewayModelSearchControllerInternal; + late final TextEditingController accountBaseUrlControllerInternal; + late final TextEditingController accountUsernameControllerInternal; + late final TextEditingController accountPasswordControllerInternal; + late final TextEditingController accountMfaCodeControllerInternal; late final TextEditingController gatewaySetupCodeControllerInternal; late final TextEditingController gatewayHostControllerInternal; late final TextEditingController gatewayPortControllerInternal; @@ -73,6 +77,8 @@ class SettingsPageStateInternal extends State { late final TextEditingController acpBridgeServerUrlControllerInternal; late final TextEditingController acpBridgeServerUsernameControllerInternal; late final TextEditingController acpBridgeServerPasswordControllerInternal; + String accountBaseUrlSyncedValueInternal = ''; + String accountUsernameSyncedValueInternal = ''; late final Map externalAcpLabelControllersInternal; late final Map @@ -135,6 +141,10 @@ class SettingsPageStateInternal extends State { aiGatewayApiKeyRefControllerInternal = TextEditingController(); aiGatewayApiKeyControllerInternal = TextEditingController(); aiGatewayModelSearchControllerInternal = TextEditingController(); + accountBaseUrlControllerInternal = TextEditingController(); + accountUsernameControllerInternal = TextEditingController(); + accountPasswordControllerInternal = TextEditingController(); + accountMfaCodeControllerInternal = TextEditingController(); gatewaySetupCodeControllerInternal = TextEditingController(); gatewayHostControllerInternal = TextEditingController(); gatewayPortControllerInternal = TextEditingController(); @@ -244,6 +254,10 @@ class SettingsPageStateInternal extends State { aiGatewayApiKeyRefControllerInternal.dispose(); aiGatewayApiKeyControllerInternal.dispose(); aiGatewayModelSearchControllerInternal.dispose(); + accountBaseUrlControllerInternal.dispose(); + accountUsernameControllerInternal.dispose(); + accountPasswordControllerInternal.dispose(); + accountMfaCodeControllerInternal.dispose(); gatewaySetupCodeControllerInternal.dispose(); gatewayHostControllerInternal.dispose(); gatewayPortControllerInternal.dispose(); diff --git a/lib/features/settings/settings_page_gateway.dart b/lib/features/settings/settings_page_gateway.dart index 6eaada20..fa187c18 100644 --- a/lib/features/settings/settings_page_gateway.dart +++ b/lib/features/settings/settings_page_gateway.dart @@ -64,10 +64,12 @@ extension SettingsPageGatewayMixinInternal on SettingsPageStateInternal { 'SKILLS 目录授权', 'SKILLS Directory Authorization', ), + GatewayIntegrationSubTabInternal.advancedConfig => appText( + '高级自定义配置', + 'Advanced Custom Configuration', + ), }; return [ - buildAcpBridgeServerModeCardInternal(context, controller, settings), - const SizedBox(height: 16), SectionTabs( items: [ 'OpenClaw Gateway', @@ -75,6 +77,7 @@ extension SettingsPageGatewayMixinInternal on SettingsPageStateInternal { appText('LLM 接入点', 'LLM Endpoints'), appText('ACP 外部接入', 'External ACP'), appText('SKILLS 目录授权', 'SKILLS Directory Authorization'), + appText('高级自定义配置', 'Advanced Custom Configuration'), ], value: tabLabel, onChanged: (value) => setStateInternal(() { @@ -86,7 +89,11 @@ extension SettingsPageGatewayMixinInternal on SettingsPageStateInternal { GatewayIntegrationSubTabInternal.llm, _ when value == appText('ACP 外部接入', 'External ACP') => GatewayIntegrationSubTabInternal.acp, - _ => GatewayIntegrationSubTabInternal.skills, + _ + when value == + appText('SKILLS 目录授权', 'SKILLS Directory Authorization') => + GatewayIntegrationSubTabInternal.skills, + _ => GatewayIntegrationSubTabInternal.advancedConfig, }; }), ), @@ -210,6 +217,11 @@ extension SettingsPageGatewayMixinInternal on SettingsPageStateInternal { ), ), ], + GatewayIntegrationSubTabInternal.advancedConfig => [ + buildOnlineAccountCardInternal(context, controller, settings), + const SizedBox(height: 16), + buildAcpBridgeServerModeCardInternal(context, controller, settings), + ], }, ]; } @@ -224,8 +236,6 @@ extension SettingsPageGatewayMixinInternal on SettingsPageStateInternal { settings.acpBridgeServerModeConfig.mode == AcpBridgeServerMode.advancedCustom; return [ - buildAcpBridgeServerModeCardInternal(context, controller, settings), - const SizedBox(height: 16), Opacity( opacity: advancedEditable ? 1 : 0.72, child: IgnorePointer( @@ -237,7 +247,11 @@ extension SettingsPageGatewayMixinInternal on SettingsPageStateInternal { onChanged: (value) => setStateInternal(() { openClawGatewayExpandedInternal = value; }), - child: buildOpenClawGatewayCardInternal(context, controller, settings), + child: buildOpenClawGatewayCardInternal( + context, + controller, + settings, + ), ), ), ), @@ -254,7 +268,11 @@ extension SettingsPageGatewayMixinInternal on SettingsPageStateInternal { onChanged: (value) => setStateInternal(() { vaultServerExpandedInternal = value; }), - child: buildVaultProviderCardInternal(context, controller, settings), + child: buildVaultProviderCardInternal( + context, + controller, + settings, + ), ), ), ) @@ -280,7 +298,11 @@ extension SettingsPageGatewayMixinInternal on SettingsPageStateInternal { onChanged: (value) => setStateInternal(() { aiGatewayExpandedInternal = value; }), - child: buildLlmEndpointManagerInternal(context, controller, settings), + child: buildLlmEndpointManagerInternal( + context, + controller, + settings, + ), ), ), ), diff --git a/lib/features/settings/settings_page_gateway_acp.dart b/lib/features/settings/settings_page_gateway_acp.dart index 2356b493..b094cb8d 100644 --- a/lib/features/settings/settings_page_gateway_acp.dart +++ b/lib/features/settings/settings_page_gateway_acp.dart @@ -39,17 +39,17 @@ String describeExternalAcpTestFailure(Object error, {Uri? endpoint}) { final bodyRead = detailMap['bodyRead'] == true ? 'yes' : 'no'; return appText( '连接不稳定:服务端在响应体接收完成前提前关闭了连接。' - '${requestUrl.isEmpty ? '' : '\nURL: $requestUrl'}' - '\nHTTP: $statusCode' - '\ncontent-type: ${contentType == null || contentType.isEmpty ? 'n/a' : contentType}' - '\nbody received: $bodyRead' - '\n应用会对这类瞬时错误自动重试一次;如果仍失败,请检查上游服务或反向代理是否提前断流。', + '${requestUrl.isEmpty ? '' : '\nURL: $requestUrl'}' + '\nHTTP: $statusCode' + '\ncontent-type: ${contentType == null || contentType.isEmpty ? 'n/a' : contentType}' + '\nbody received: $bodyRead' + '\n应用会对这类瞬时错误自动重试一次;如果仍失败,请检查上游服务或反向代理是否提前断流。', 'Connection was interrupted before the response body finished arriving.' - '${requestUrl.isEmpty ? '' : '\nURL: $requestUrl'}' - '\nHTTP: $statusCode' - '\ncontent-type: ${contentType == null || contentType.isEmpty ? 'n/a' : contentType}' - '\nbody received: $bodyRead' - '\nThe app retries this transient error once automatically. If it still fails, inspect the upstream service or reverse proxy for early connection termination.', + '${requestUrl.isEmpty ? '' : '\nURL: $requestUrl'}' + '\nHTTP: $statusCode' + '\ncontent-type: ${contentType == null || contentType.isEmpty ? 'n/a' : contentType}' + '\nbody received: $bodyRead' + '\nThe app retries this transient error once automatically. If it still fails, inspect the upstream service or reverse proxy for early connection termination.', ); } } @@ -150,6 +150,299 @@ bool shouldRetryExternalAcpTestFailure(Object error) { } extension SettingsPageGatewayAcpMixinInternal on SettingsPageStateInternal { + void syncAccountDraftControllersInternal(SettingsSnapshot settings) { + if (accountBaseUrlControllerInternal.text == + accountBaseUrlSyncedValueInternal && + settings.accountBaseUrl != accountBaseUrlSyncedValueInternal) { + accountBaseUrlControllerInternal.text = settings.accountBaseUrl; + } + if (accountUsernameControllerInternal.text == + accountUsernameSyncedValueInternal && + settings.accountUsername != accountUsernameSyncedValueInternal) { + accountUsernameControllerInternal.text = settings.accountUsername; + } + accountBaseUrlSyncedValueInternal = settings.accountBaseUrl; + accountUsernameSyncedValueInternal = settings.accountUsername; + } + + Future saveAccountProfileInternal(SettingsSnapshot settings) async { + final nextSettings = settings.copyWith( + accountBaseUrl: accountBaseUrlControllerInternal.text.trim(), + accountUsername: accountUsernameControllerInternal.text.trim(), + ); + await saveSettingsInternal(widget.controller, nextSettings); + accountBaseUrlSyncedValueInternal = nextSettings.accountBaseUrl; + accountUsernameSyncedValueInternal = nextSettings.accountUsername; + } + + Future loginAccountInternal(SettingsSnapshot settings) async { + await saveAccountProfileInternal(settings); + try { + await widget.controller.settingsController.loginAccount( + baseUrl: accountBaseUrlControllerInternal.text.trim(), + identifier: accountUsernameControllerInternal.text.trim(), + password: accountPasswordControllerInternal.text, + ); + } finally { + accountPasswordControllerInternal.clear(); + } + } + + Future verifyAccountMfaInternal() async { + try { + await widget.controller.settingsController.verifyAccountMfa( + baseUrl: accountBaseUrlControllerInternal.text.trim(), + code: accountMfaCodeControllerInternal.text.trim(), + ); + } finally { + accountMfaCodeControllerInternal.clear(); + } + } + + Future syncAccountSettingsInternal(SettingsSnapshot settings) async { + await saveAccountProfileInternal(settings); + await widget.controller.settingsController.syncAccountSettings( + baseUrl: accountBaseUrlControllerInternal.text.trim(), + ); + } + + Future logoutAccountInternal() async { + await widget.controller.settingsController.logoutAccount(); + accountPasswordControllerInternal.clear(); + accountMfaCodeControllerInternal.clear(); + } + + Future cancelAccountMfaInternal() async { + await widget.controller.settingsController.cancelAccountMfaChallenge(); + accountPasswordControllerInternal.clear(); + accountMfaCodeControllerInternal.clear(); + } + + Widget buildOnlineAccountCardInternal( + BuildContext context, + AppController controller, + SettingsSnapshot settings, + ) { + syncAccountDraftControllersInternal(settings); + final accountController = controller.settingsController; + final accountSession = accountController.accountSession; + final accountSyncState = accountController.accountSyncState; + final accountBusy = accountController.accountBusy; + final accountSignedIn = accountController.accountSignedIn; + final accountMfaRequired = accountController.accountMfaRequired; + final signedInLabel = accountSession?.email.trim().isNotEmpty == true + ? accountSession!.email.trim() + : accountSession?.name.trim().isNotEmpty == true + ? accountSession!.name.trim() + : appText('在线账户', 'Online Account'); + final sessionStatusText = accountSignedIn + ? appText('已登录:$signedInLabel', 'Signed in: $signedInLabel') + : accountMfaRequired + ? appText('等待双重验证', 'Waiting for MFA verification') + : appText('未登录', 'Signed out'); + final syncStatusText = accountSyncState == null + ? appText('idle · 尚未同步远程配置', 'idle · Remote config not synced yet') + : '${accountSyncState.syncState} · ${accountSyncState.syncMessage}'; + + Widget buildSignedOutLoginCard() { + final theme = Theme.of(context); + return Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 840), + child: SurfaceCard( + padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 36), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon( + Icons.cloud_outlined, + size: 72, + color: theme.colorScheme.primary, + ), + const SizedBox(height: 16), + Text( + appText('在线账户', 'Online Account'), + style: theme.textTheme.headlineMedium, + textAlign: TextAlign.center, + ), + const SizedBox(height: 10), + Text( + appText( + '请先登录 ACP Bridge Server', + 'Please sign in to ACP Bridge Server', + ), + style: theme.textTheme.titleMedium?.copyWith( + color: theme.textTheme.bodyMedium?.color?.withValues( + alpha: 0.8, + ), + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 28), + TextFormField( + key: const ValueKey('account-base-url-field'), + controller: accountBaseUrlControllerInternal, + decoration: InputDecoration( + labelText: appText('服务地址', 'Service URL'), + prefixIcon: const Icon(Icons.dns_outlined), + ), + onFieldSubmitted: (_) => saveAccountProfileInternal(settings), + ), + const SizedBox(height: 16), + TextFormField( + key: const ValueKey('account-username-field'), + controller: accountUsernameControllerInternal, + decoration: InputDecoration( + labelText: appText('邮箱或账号', 'Email or Username'), + prefixIcon: const Icon(Icons.person_outline_rounded), + ), + onFieldSubmitted: (_) => saveAccountProfileInternal(settings), + ), + const SizedBox(height: 16), + TextFormField( + key: const ValueKey('account-password-field'), + controller: accountPasswordControllerInternal, + obscureText: true, + decoration: InputDecoration( + labelText: appText('密码', 'Password'), + prefixIcon: const Icon(Icons.lock_outline_rounded), + ), + onFieldSubmitted: (_) => loginAccountInternal(settings), + ), + const SizedBox(height: 24), + SizedBox( + width: double.infinity, + child: FilledButton( + key: const ValueKey('account-login-button'), + onPressed: accountBusy + ? null + : () => loginAccountInternal(settings), + child: Text(appText('登录', 'Sign In')), + ), + ), + ], + ), + ), + ), + ); + } + + Widget buildSignedInProfileCard() { + return SurfaceCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + accountSignedIn + ? signedInLabel + : settings.accountUsername.trim().isEmpty + ? appText('本地操作员', 'Local Operator') + : settings.accountUsername, + style: Theme.of(context).textTheme.headlineSmall, + ), + const SizedBox(height: 8), + Text( + appText( + '这里继续只负责在线账户身份、MFA、工作区与同步摘要。ACP Bridge Server 的本地连接与高级配置在下面统一收口。', + 'This card focuses on online account identity, MFA, workspace, and sync summary. Local ACP Bridge Server connection and advanced config are unified below.', + ), + ), + const SizedBox(height: 16), + Text( + sessionStatusText, + key: const ValueKey('account-session-status'), + style: Theme.of(context).textTheme.bodyMedium, + ), + const SizedBox(height: 6), + Text( + syncStatusText, + key: const ValueKey('account-sync-status'), + style: Theme.of(context).textTheme.bodySmall, + ), + const SizedBox(height: 16), + Container( + width: double.infinity, + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(16), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + appText('在线账户同步摘要', 'Online account sync summary'), + style: Theme.of(context).textTheme.titleSmall, + ), + const SizedBox(height: 8), + Text( + '${appText('服务地址', 'Service URL')}: ${settings.accountBaseUrl.trim().isEmpty ? appText('未填写', 'Not set') : settings.accountBaseUrl}', + ), + const SizedBox(height: 6), + Text( + '${appText('在线账户', 'Online Account')}: ${settings.accountUsername.trim().isEmpty ? appText('未填写', 'Not set') : settings.accountUsername}', + ), + const SizedBox(height: 6), + Text( + '${appText('最近同步', 'Last Sync')}: ${accountSyncState == null || settings.acpBridgeServerModeConfig.cloudSynced.lastSyncAt <= 0 ? appText('尚未同步', 'Not synced yet') : DateTime.fromMillisecondsSinceEpoch(settings.acpBridgeServerModeConfig.cloudSynced.lastSyncAt).toLocal().toIso8601String()}', + ), + ], + ), + ), + const SizedBox(height: 16), + if (accountMfaRequired) ...[ + TextFormField( + key: const ValueKey('account-mfa-code-field'), + controller: accountMfaCodeControllerInternal, + decoration: InputDecoration( + labelText: appText('双重验证代码', 'MFA Code'), + ), + onFieldSubmitted: (_) => verifyAccountMfaInternal(), + ), + const SizedBox(height: 16), + ], + Wrap( + spacing: 12, + runSpacing: 12, + children: [ + if (accountMfaRequired) + FilledButton.tonal( + key: const ValueKey('account-verify-mfa-button'), + onPressed: accountBusy ? null : verifyAccountMfaInternal, + child: Text(appText('验证并同步', 'Verify & Sync')), + ), + if (accountMfaRequired) + FilledButton.tonal( + key: const ValueKey('account-edit-button'), + onPressed: accountBusy ? null : cancelAccountMfaInternal, + child: Text(appText('返回编辑', 'Back to Edit')), + ), + if (accountSignedIn) + FilledButton.tonal( + key: const ValueKey('account-sync-button'), + onPressed: accountBusy + ? null + : () => syncAccountSettingsInternal(settings), + child: Text(appText('重新同步', 'Sync Again')), + ), + if (accountSignedIn) + FilledButton.tonal( + key: const ValueKey('account-logout-button'), + onPressed: accountBusy ? null : logoutAccountInternal, + child: Text(appText('退出登录', 'Log Out')), + ), + ], + ), + ], + ), + ); + } + + return accountSignedIn || accountMfaRequired + ? buildSignedInProfileCard() + : buildSignedOutLoginCard(); + } + Widget buildAcpBridgeServerModeCardInternal( BuildContext context, AppController controller, @@ -164,9 +457,9 @@ extension SettingsPageGatewayAcpMixinInternal on SettingsPageStateInternal { final cloudSync = modeConfig.cloudSynced; final remoteSummary = cloudSync.remoteServerSummary; final currentSource = switch (modeConfig.sourceTag) { - 'cloudSynced' => appText('云端同步', 'Cloud Sync'), - 'selfHosted' => appText('本地 Server', 'Self-hosted Server'), - _ => appText('高级覆盖', 'Advanced Override'), + 'cloudSynced' => appText('在线账户', 'Online Account'), + 'selfHosted' => appText('本地账户', 'Local Account'), + _ => appText('高级模式', 'Advanced Mode'), }; final syncStatus = accountSyncState?.syncState.trim().isNotEmpty == true ? accountSyncState!.syncState @@ -181,14 +474,17 @@ extension SettingsPageGatewayAcpMixinInternal on SettingsPageStateInternal { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'XWorkmate ACP Bridge Server', + appText( + 'ACP Bridge Server 连接模式', + 'ACP Bridge Server Connection Mode', + ), style: Theme.of(context).textTheme.titleLarge, ), const SizedBox(height: 8), Text( appText( - 'XWorkmate App 继续只承担纯客户端职责:配置、会话、安全存储与连接编排。云端、自托管和高级自定义都通过这里统一收口,不在 App 内承载服务端逻辑。', - 'XWorkmate App remains a pure client: configuration, session, secure storage, and connection orchestration only. Cloud, self-hosted, and advanced custom flows are unified here without embedding server responsibilities into the app.', + '在线账户负责云端同步,本地账户负责连接 ACP Bridge Server,高级模式是在本地账户基础上再叠加 advanced config 覆盖层。App 只负责配置、会话、安全存储与连接编排,不承载服务端逻辑。', + 'Online account handles cloud sync, local account connects to ACP Bridge Server, and advanced mode layers advanced config on top of the local account. The app stays a pure client for configuration, session handling, secure storage, and connection orchestration.', ), ), const SizedBox(height: 16), @@ -198,7 +494,7 @@ extension SettingsPageGatewayAcpMixinInternal on SettingsPageStateInternal { children: [ ChoiceChip( key: const ValueKey('acp-bridge-mode-cloud'), - label: Text(appText('在线同步配置', 'Cloud Sync')), + label: Text(appText('在线账户', 'Online Account')), selected: modeConfig.mode == AcpBridgeServerMode.cloudSynced, onSelected: (_) => saveSettingsInternal( controller, @@ -212,7 +508,7 @@ extension SettingsPageGatewayAcpMixinInternal on SettingsPageStateInternal { ), ChoiceChip( key: const ValueKey('acp-bridge-mode-self-hosted'), - label: Text(appText('本地模式', 'Self-hosted')), + label: Text(appText('本地账户', 'Local Account')), selected: modeConfig.mode == AcpBridgeServerMode.selfHosted, onSelected: (_) => saveSettingsInternal( controller, @@ -226,7 +522,7 @@ extension SettingsPageGatewayAcpMixinInternal on SettingsPageStateInternal { ), ChoiceChip( key: const ValueKey('acp-bridge-mode-advanced'), - label: Text(appText('高级自定义', 'Advanced Custom')), + label: Text(appText('高级模式', 'Advanced Mode')), selected: modeConfig.mode == AcpBridgeServerMode.advancedCustom, onSelected: (_) => saveSettingsInternal( controller, @@ -246,7 +542,7 @@ extension SettingsPageGatewayAcpMixinInternal on SettingsPageStateInternal { runSpacing: 12, children: [ StatusChipInternal( - label: '${appText('当前来源', 'Source')}: $currentSource', + label: '${appText('当前模式', 'Mode')}: $currentSource', tone: StatusChipToneInternal.ready, ), StatusChipInternal( @@ -263,23 +559,14 @@ extension SettingsPageGatewayAcpMixinInternal on SettingsPageStateInternal { Text( accountSignedIn ? appText( - '已登录云账户,可直接同步远端 ACP Bridge Server 配置。', - 'Signed in to the cloud account. You can sync the remote ACP Bridge Server configuration directly.', + '已登录在线账户,可直接同步云端 ACP Bridge Server 默认配置。', + 'Signed in to the online account. You can sync the cloud ACP Bridge Server defaults directly.', ) : appText( - '当前未登录云账户。普通用户建议先登录,再从云端同步默认配置。', - 'No cloud account is signed in. For most users, sign in first and sync the default configuration from the cloud.', + '当前未登录在线账户。建议先登录,再从云端同步默认配置。', + 'No online account is signed in. Sign in first, then sync the default configuration from the cloud.', ), ), - const SizedBox(height: 12), - Text( - '${appText('服务地址', 'Service URL')}: ${cloudSync.accountBaseUrl.trim().isEmpty ? settings.accountBaseUrl : cloudSync.accountBaseUrl}', - ), - const SizedBox(height: 6), - Text( - '${appText('账号', 'Account')}: ${cloudSync.accountIdentifier.trim().isEmpty ? settings.accountUsername : cloudSync.accountIdentifier}', - ), - const SizedBox(height: 6), Text( '${appText('远端摘要', 'Remote Summary')}: ${remoteSummary.endpoint.trim().isEmpty ? appText('待同步', 'Pending sync') : remoteSummary.endpoint}', ), @@ -297,43 +584,29 @@ extension SettingsPageGatewayAcpMixinInternal on SettingsPageStateInternal { spacing: 10, runSpacing: 10, children: [ - FilledButton.tonal( - key: const ValueKey('acp-bridge-cloud-open-account'), - onPressed: () => - controller.navigateTo(WorkspaceDestination.account), - child: Text(appText('登录 / 管理账号', 'Open Account')), - ), FilledButton.tonal( key: const ValueKey('acp-bridge-cloud-sync'), onPressed: accountBusy || !accountSignedIn ? null - : () => accountController.syncAccountSettings( - baseUrl: settings.accountBaseUrl, - ), + : () => syncAccountSettingsInternal(settings), child: Text(appText('重新同步', 'Sync Again')), ), FilledButton.tonal( key: const ValueKey('acp-bridge-cloud-disconnect'), onPressed: accountBusy || !accountSignedIn ? null - : accountController.logoutAccount, + : logoutAccountInternal, child: Text(appText('断开', 'Disconnect')), ), ], ), ], - AcpBridgeServerMode.selfHosted => [ - buildAcpBridgeServerSelfHostedPanelInternal( - context, - controller, - settings, - ), - ], + AcpBridgeServerMode.selfHosted => [], AcpBridgeServerMode.advancedCustom => [ Text( appText( - '高级自定义会把下面的 OpenClaw Gateway / Vault Server / LLM Endpoint / 外部 ACP Server endpoint / SKILLS 目录 当作覆盖层。未覆盖的值继续继承当前基础模式。', - 'Advanced custom mode treats the OpenClaw Gateway / Vault Server / LLM Endpoint / external ACP server endpoint / SKILLS directory below as overrides. Fields you do not override keep inheriting from the current base mode.', + '高级模式 = 本地账户 + advanced config。下面先保留本地 ACP Bridge Server 连接,再把 OpenClaw Gateway / Vault Server / LLM Endpoint / 外部 ACP Server endpoint / SKILLS 目录 当作覆盖层。未覆盖的值继续继承当前基础模式。', + 'Advanced mode = local account + advanced config. Keep the local ACP Bridge Server connection below, then treat the OpenClaw Gateway / Vault Server / LLM Endpoint / external ACP server endpoint / SKILLS directory as overrides. Fields you do not override keep inheriting from the current base mode.', ), ), const SizedBox(height: 12), @@ -347,6 +620,15 @@ extension SettingsPageGatewayAcpMixinInternal on SettingsPageStateInternal { ), ], }, + const SizedBox(height: 16), + buildAcpBridgeServerSelfHostedPanelInternal( + context, + controller, + settings, + targetMode: modeConfig.mode == AcpBridgeServerMode.advancedCustom + ? AcpBridgeServerMode.advancedCustom + : AcpBridgeServerMode.selfHosted, + ), ], ), ); @@ -355,12 +637,25 @@ extension SettingsPageGatewayAcpMixinInternal on SettingsPageStateInternal { Widget buildAcpBridgeServerSelfHostedPanelInternal( BuildContext context, AppController controller, - SettingsSnapshot settings, - ) { + SettingsSnapshot settings, { + AcpBridgeServerMode targetMode = AcpBridgeServerMode.selfHosted, + }) { final selfHosted = settings.acpBridgeServerModeConfig.selfHosted; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text( + appText('连接 ACP Bridge Server', 'Connect to ACP Bridge Server'), + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 8), + Text( + appText( + '填写本地或私有部署的 ACP Bridge Server 地址、用户名和密码,然后测试连接并保存到安全存储。', + 'Enter the URL, username, and password for your local or private ACP Bridge Server, then test the connection and save it into secure storage.', + ), + ), + const SizedBox(height: 12), TextField( key: const ValueKey('acp-bridge-self-hosted-url'), controller: acpBridgeServerUrlControllerInternal, @@ -375,9 +670,7 @@ extension SettingsPageGatewayAcpMixinInternal on SettingsPageStateInternal { TextField( key: const ValueKey('acp-bridge-self-hosted-username'), controller: acpBridgeServerUsernameControllerInternal, - decoration: InputDecoration( - labelText: appText('用户', 'Username'), - ), + decoration: InputDecoration(labelText: appText('用户', 'Username')), ), const SizedBox(height: 12), TextField( @@ -393,9 +686,7 @@ extension SettingsPageGatewayAcpMixinInternal on SettingsPageStateInternal { ), ), const SizedBox(height: 8), - Text( - '${appText('密码引用', 'Password Ref')}: ${selfHosted.passwordRef}', - ), + Text('${appText('密码引用', 'Password Ref')}: ${selfHosted.passwordRef}'), const SizedBox(height: 14), Wrap( spacing: 10, @@ -417,6 +708,7 @@ extension SettingsPageGatewayAcpMixinInternal on SettingsPageStateInternal { onPressed: () => saveAcpBridgeServerSelfHostedInternal( controller, settings, + targetMode: targetMode, ), child: Text(appText('保存', 'Save')), ), @@ -425,6 +717,7 @@ extension SettingsPageGatewayAcpMixinInternal on SettingsPageStateInternal { onPressed: () => connectAcpBridgeServerSelfHostedInternal( controller, settings, + targetMode: targetMode, ), child: Text(appText('连接', 'Connect')), ), @@ -488,8 +781,9 @@ extension SettingsPageGatewayAcpMixinInternal on SettingsPageStateInternal { Future saveAcpBridgeServerSelfHostedInternal( AppController controller, - SettingsSnapshot settings, - ) async { + SettingsSnapshot settings, { + AcpBridgeServerMode targetMode = AcpBridgeServerMode.selfHosted, + }) async { final modeConfig = settings.acpBridgeServerModeConfig; final nextSelfHosted = modeConfig.selfHosted.copyWith( serverUrl: acpBridgeServerUrlControllerInternal.text, @@ -504,16 +798,15 @@ extension SettingsPageGatewayAcpMixinInternal on SettingsPageStateInternal { module: 'Settings', ); } - final nextSettings = settings.captureAcpBridgeServerAdvancedOverrides().copyWith( - accountLocalMode: true, - acpBridgeServerModeConfig: settings - .captureAcpBridgeServerAdvancedOverrides() - .acpBridgeServerModeConfig - .copyWith( - mode: AcpBridgeServerMode.selfHosted, - selfHosted: nextSelfHosted, - ), - ); + final nextSettings = settings + .captureAcpBridgeServerAdvancedOverrides() + .copyWith( + accountLocalMode: true, + acpBridgeServerModeConfig: settings + .captureAcpBridgeServerAdvancedOverrides() + .acpBridgeServerModeConfig + .copyWith(mode: targetMode, selfHosted: nextSelfHosted), + ); await saveSettingsInternal(controller, nextSettings); if (!mounted) { return; @@ -521,17 +814,22 @@ extension SettingsPageGatewayAcpMixinInternal on SettingsPageStateInternal { acpBridgeServerPasswordControllerInternal.clear(); setStateInternal(() { acpBridgeServerSelfHostedMessageInternal = appText( - 'Self-hosted 配置已保存,密码已进入 secure storage。', - 'The self-hosted configuration was saved and the password is now in secure storage.', + 'ACP Bridge Server 配置已保存,密码已进入 secure storage。', + 'The ACP Bridge Server configuration was saved and the password is now in secure storage.', ); }); } Future connectAcpBridgeServerSelfHostedInternal( AppController controller, - SettingsSnapshot settings, - ) async { - await saveAcpBridgeServerSelfHostedInternal(controller, settings); + SettingsSnapshot settings, { + AcpBridgeServerMode targetMode = AcpBridgeServerMode.selfHosted, + }) async { + await saveAcpBridgeServerSelfHostedInternal( + controller, + settings, + targetMode: targetMode, + ); if (!mounted) { return; } @@ -566,7 +864,11 @@ extension SettingsPageGatewayAcpMixinInternal on SettingsPageStateInternal { acpBridgeServerPasswordControllerInternal.text.trim().isNotEmpty ? acpBridgeServerPasswordControllerInternal.text.trim() : await controller.settingsController.loadSecretValueByRef( - controller.settings.acpBridgeServerModeConfig.selfHosted.passwordRef, + controller + .settings + .acpBridgeServerModeConfig + .selfHosted + .passwordRef, ); final authorization = password.isEmpty ? '' @@ -836,20 +1138,22 @@ extension SettingsPageGatewayAcpMixinInternal on SettingsPageStateInternal { ); GatewayAcpCapabilities capabilities; try { - capabilities = await controller.gatewayAcpClientInternal.loadCapabilities( - forceRefresh: true, - endpointOverride: endpoint, - authorizationOverride: authorization, - ); + capabilities = await controller.gatewayAcpClientInternal + .loadCapabilities( + forceRefresh: true, + endpointOverride: endpoint, + authorizationOverride: authorization, + ); } catch (error) { if (!shouldRetryExternalAcpTestFailure(error)) { rethrow; } - capabilities = await controller.gatewayAcpClientInternal.loadCapabilities( - forceRefresh: true, - endpointOverride: endpoint, - authorizationOverride: authorization, - ); + capabilities = await controller.gatewayAcpClientInternal + .loadCapabilities( + forceRefresh: true, + endpointOverride: endpoint, + authorizationOverride: authorization, + ); } if (!mounted) { return; diff --git a/lib/features/settings/settings_page_sections.dart b/lib/features/settings/settings_page_sections.dart index 2354edab..4b9f0544 100644 --- a/lib/features/settings/settings_page_sections.dart +++ b/lib/features/settings/settings_page_sections.dart @@ -68,9 +68,16 @@ extension SettingsPageSectionsMixinInternal on SettingsPageStateInternal { settings, uiFeatures, ), - SettingsTab.agents => buildAgentsInternal(context, controller, settings), + SettingsTab.agents => buildAgentsInternal( + context, + controller, + settings, + ), SettingsTab.appearance => buildAppearanceInternal(context, controller), - SettingsTab.diagnostics => buildDiagnosticsInternal(context, controller), + SettingsTab.diagnostics => buildDiagnosticsInternal( + context, + controller, + ), SettingsTab.experimental => buildExperimentalInternal( context, controller, @@ -256,10 +263,7 @@ extension SettingsPageSectionsMixinInternal on SettingsPageStateInternal { '当前存在待生效更改。保存并生效:立即按当前配置更新。', 'There are saved changes waiting to be applied. Save & apply updates the current configuration immediately.', ) - : appText( - '当前没有待提交更改。', - 'There are no pending settings changes.', - ), + : appText('当前没有待提交更改。', 'There are no pending settings changes.'), applyLabel: appText('保存并生效', 'Save & apply'), onApply: (!hasDraft && !hasPendingApply) ? null @@ -311,43 +315,6 @@ extension SettingsPageSectionsMixinInternal on SettingsPageStateInternal { ), if (controller.supportsDesktopIntegration) buildLinuxDesktopIntegrationInternal(context, controller, settings), - if (uiFeatures.supportsAccountAccess) - SurfaceCard( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - appText('账号访问', 'Account Access'), - style: Theme.of(context).textTheme.titleLarge, - ), - const SizedBox(height: 16), - EditableFieldInternal( - label: appText('账号服务地址', 'Account Base URL'), - value: settings.accountBaseUrl, - onSubmitted: (value) => saveSettingsInternal( - controller, - settings.copyWith(accountBaseUrl: value), - ), - ), - EditableFieldInternal( - label: appText('账号用户名', 'Account Username'), - value: settings.accountUsername, - onSubmitted: (value) => saveSettingsInternal( - controller, - settings.copyWith(accountUsername: value), - ), - ), - EditableFieldInternal( - label: appText('工作区名称', 'Workspace Label'), - value: settings.accountWorkspace, - onSubmitted: (value) => saveSettingsInternal( - controller, - settings.copyWith(accountWorkspace: value), - ), - ), - ], - ), - ), ]; } diff --git a/lib/features/settings/settings_page_widgets.dart b/lib/features/settings/settings_page_widgets.dart index 1c5ecfd0..8f08e14d 100644 --- a/lib/features/settings/settings_page_widgets.dart +++ b/lib/features/settings/settings_page_widgets.dart @@ -498,7 +498,14 @@ class WorkflowStepInternal extends StatelessWidget { } } -enum GatewayIntegrationSubTabInternal { gateway, vault, llm, acp, skills } +enum GatewayIntegrationSubTabInternal { + gateway, + vault, + llm, + acp, + skills, + advancedConfig, +} enum LlmEndpointSlotInternal { aiGateway, ollamaLocal, ollamaCloud } diff --git a/lib/models/app_models.dart b/lib/models/app_models.dart index abfb1987..012b1d6d 100644 --- a/lib/models/app_models.dart +++ b/lib/models/app_models.dart @@ -28,7 +28,7 @@ extension WorkspaceDestinationCopy on WorkspaceDestination { WorkspaceDestination.secrets => appText('密钥', 'Secrets'), WorkspaceDestination.aiGateway => 'LLM API', WorkspaceDestination.settings => appText('设置', 'Settings'), - WorkspaceDestination.account => appText('账号', 'Account'), + WorkspaceDestination.account => appText('在线账户', 'Online Account'), }; IconData get icon => switch (this) { @@ -87,8 +87,8 @@ extension WorkspaceDestinationCopy on WorkspaceDestination { 'Global settings and diagnostics, separated from business modules.', ), WorkspaceDestination.account => appText( - '用户身份、工作区切换与登录会话管理。', - 'Identity, workspace switching, and session management.', + '在线账户、工作区切换、登录会话与 ACP Bridge Server 同步管理。', + 'Online account, workspace switching, login sessions, and ACP Bridge Server sync.', ), }; diff --git a/lib/widgets/sidebar_navigation.dart b/lib/widgets/sidebar_navigation.dart index b0f3814b..cce3d922 100644 --- a/lib/widgets/sidebar_navigation.dart +++ b/lib/widgets/sidebar_navigation.dart @@ -113,10 +113,7 @@ class SidebarNavigation extends StatelessWidget { children: [ const SizedBox(height: 4), if (isCollapsed && showCollapseControl) ...[ - SidebarHeader( - isCollapsed: true, - onTap: onExpandFromCollapsed, - ), + SidebarHeader(isCollapsed: true, onTap: onExpandFromCollapsed), const SizedBox(height: AppSpacing.xs), ], Expanded( @@ -158,13 +155,8 @@ class SidebarNavigation extends StatelessWidget { sidebarState: sidebarState, onCycleSidebarState: onCycleSidebarState, onOpenAccount: onOpenAccount, - showAccountButton: - availableDestinations == null || - availableDestinations!.contains( - WorkspaceDestination.account, - ), - accountSelected: - currentSection == WorkspaceDestination.account, + showAccountButton: false, + accountSelected: false, showCollapseControl: false, ), ], diff --git a/test/features/settings_page_acp_bridge_mode_suite.dart b/test/features/settings_page_acp_bridge_mode_suite.dart index 7214b6f5..bcb03f3f 100644 --- a/test/features/settings_page_acp_bridge_mode_suite.dart +++ b/test/features/settings_page_acp_bridge_mode_suite.dart @@ -9,29 +9,45 @@ import 'package:xworkmate/models/app_models.dart'; import '../test_support.dart'; void main() { - testWidgets('SettingsPage shows ACP bridge server mode card on integrations', ( - WidgetTester tester, - ) async { - final controller = await createTestController(tester); - controller.openSettings(tab: SettingsTab.gateway); + testWidgets( + 'SettingsPage shows ACP bridge server mode card in advanced custom config', + (WidgetTester tester) async { + final controller = await createTestController(tester); + controller.openSettings(tab: SettingsTab.gateway); - await pumpPage( - tester, - child: SettingsPage( - controller: controller, - initialTab: SettingsTab.gateway, - ), - ); + await pumpPage( + tester, + child: SettingsPage( + controller: controller, + initialTab: SettingsTab.gateway, + showSectionTabs: true, + ), + ); - expect(find.text('XWorkmate ACP Bridge Server'), findsOneWidget); - expect(find.byKey(const ValueKey('acp-bridge-mode-cloud')), findsOneWidget); - expect( - find.byKey(const ValueKey('acp-bridge-mode-self-hosted')), - findsOneWidget, - ); - expect( - find.byKey(const ValueKey('acp-bridge-mode-advanced')), - findsOneWidget, - ); - }); + await tester.tap(find.byKey(const ValueKey('section-tab-高级自定义配置'))); + await tester.pumpAndSettle(); + + expect(find.text('ACP Bridge Server 连接模式'), findsOneWidget); + expect( + find.byKey(const ValueKey('acp-bridge-mode-cloud')), + findsOneWidget, + ); + expect( + find.byKey(const ValueKey('acp-bridge-mode-self-hosted')), + findsOneWidget, + ); + expect( + find.byKey(const ValueKey('acp-bridge-mode-advanced')), + findsOneWidget, + ); + expect( + find.byKey(const ValueKey('acp-bridge-self-hosted-url')), + findsOneWidget, + ); + expect( + find.byKey(const ValueKey('acp-bridge-self-hosted-connect')), + findsOneWidget, + ); + }, + ); } diff --git a/test/features/settings_page_suite.dart b/test/features/settings_page_suite.dart index 99726dda..0b2e01ef 100644 --- a/test/features/settings_page_suite.dart +++ b/test/features/settings_page_suite.dart @@ -196,47 +196,50 @@ void main() { expect(controller.themeMode, ThemeMode.light); }); - testWidgets('SettingsPage hides account access controls by default', ( - WidgetTester tester, - ) async { - final controller = await createTestController(tester); + testWidgets( + 'SettingsPage gateway advanced config tab merges online account and ACP Bridge Server', + (WidgetTester tester) async { + final controller = await createTestController(tester); + controller.setSettingsTab(SettingsTab.gateway); - await pumpPage( - tester, - child: SettingsPage(controller: controller), - platform: TargetPlatform.macOS, - ); + await pumpPage( + tester, + child: SettingsPage( + controller: controller, + initialTab: SettingsTab.gateway, + showSectionTabs: true, + ), + platform: TargetPlatform.macOS, + ); - expect(find.text('账号访问'), findsNothing); - expect(find.text('Account Access'), findsNothing); - expect(find.text('账号本地模式'), findsNothing); - expect(find.text('Account local mode'), findsNothing); - }); + expect( + find.byKey(const ValueKey('account-base-url-field')), + findsNothing, + ); + expect(find.byKey(const ValueKey('acp-bridge-mode-cloud')), findsNothing); - testWidgets('SettingsPage can expose account access when feature enabled', ( - WidgetTester tester, - ) async { - final manifest = UiFeatureManifest.fallback().copyWithFeature( - platform: UiFeaturePlatform.desktop, - module: 'settings', - feature: 'account_access', - enabled: true, - releaseTier: UiFeatureReleaseTier.experimental, - ); - final controller = await createTestController( - tester, - uiFeatureManifest: manifest, - ); + await tester.tap(find.byKey(const ValueKey('section-tab-高级自定义配置'))); + await tester.pumpAndSettle(); - await pumpPage( - tester, - child: SettingsPage(controller: controller), - platform: TargetPlatform.macOS, - ); - - expect(find.text('账号访问'), findsOneWidget); - expect(find.text('账号本地模式'), findsOneWidget); - }); + expect(find.text('ACP Bridge Server 连接模式'), findsOneWidget); + expect( + find.byKey(const ValueKey('account-base-url-field')), + findsOneWidget, + ); + expect( + find.byKey(const ValueKey('acp-bridge-mode-cloud')), + findsOneWidget, + ); + expect( + find.byKey(const ValueKey('acp-bridge-mode-self-hosted')), + findsOneWidget, + ); + expect( + find.byKey(const ValueKey('acp-bridge-mode-advanced')), + findsOneWidget, + ); + }, + ); testWidgets( 'SettingsPage workspace tab no longer exposes remote project root', @@ -860,15 +863,16 @@ paths: final remoteProfile = controller.settings.primaryRemoteGatewayProfile; setGatewaySnapshotForTest( controller, - GatewayConnectionSnapshot.initial(mode: RuntimeConnectionMode.remote) - .copyWith( - status: RuntimeConnectionStatus.connected, - statusText: 'Connected', - remoteAddress: '${remoteProfile.host}:${remoteProfile.port}', - lastError: 'NOT_PAIRED: pairing required', - lastErrorCode: 'NOT_PAIRED', - lastErrorDetailCode: 'PAIRING_REQUIRED', - ), + GatewayConnectionSnapshot.initial( + mode: RuntimeConnectionMode.remote, + ).copyWith( + status: RuntimeConnectionStatus.connected, + statusText: 'Connected', + remoteAddress: '${remoteProfile.host}:${remoteProfile.port}', + lastError: 'NOT_PAIRED: pairing required', + lastErrorCode: 'NOT_PAIRED', + lastErrorDetailCode: 'PAIRING_REQUIRED', + ), ); await _pumpWithoutSettling( @@ -876,7 +880,10 @@ paths: child: ConnectionChipInternal(controller: controller), ); - expect(find.byKey(const Key('assistant-connection-chip')), findsOneWidget); + expect( + find.byKey(const Key('assistant-connection-chip')), + findsOneWidget, + ); expect( find.textContaining( '已连接 · ${remoteProfile.host}:${remoteProfile.port}', diff --git a/test/test_support.dart b/test/test_support.dart index 7fd4f074..9f4612da 100644 --- a/test/test_support.dart +++ b/test/test_support.dart @@ -187,38 +187,44 @@ class _TestFakeGatewayRuntime extends GatewayRuntime { case 'device.pair.list': return { 'pending': _pairingList.pending - .map((item) => { - 'requestId': item.requestId, - 'deviceId': item.deviceId, - 'label': item.label, - 'role': item.role, - 'scopes': item.scopes, - 'remoteIp': item.remoteIp, - 'requestedAtMs': item.requestedAtMs, - 'repair': item.isRepair, - }) + .map( + (item) => { + 'requestId': item.requestId, + 'deviceId': item.deviceId, + 'label': item.label, + 'role': item.role, + 'scopes': item.scopes, + 'remoteIp': item.remoteIp, + 'requestedAtMs': item.requestedAtMs, + 'repair': item.isRepair, + }, + ) .toList(growable: false), 'paired': _pairingList.paired - .map((item) => { - 'deviceId': item.deviceId, - 'displayName': item.displayName, - 'roles': item.roles, - 'scopes': item.scopes, - 'remoteIp': item.remoteIp, - 'tokens': item.tokens - .map((token) => { - 'role': token.role, - 'scopes': token.scopes, - 'createdAtMs': token.createdAtMs, - 'rotatedAtMs': token.rotatedAtMs, - 'revokedAtMs': token.revokedAtMs, - 'lastUsedAtMs': token.lastUsedAtMs, - }) - .toList(growable: false), - 'createdAtMs': item.createdAtMs, - 'approvedAtMs': item.approvedAtMs, - 'currentDevice': item.currentDevice, - }) + .map( + (item) => { + 'deviceId': item.deviceId, + 'displayName': item.displayName, + 'roles': item.roles, + 'scopes': item.scopes, + 'remoteIp': item.remoteIp, + 'tokens': item.tokens + .map( + (token) => { + 'role': token.role, + 'scopes': token.scopes, + 'createdAtMs': token.createdAtMs, + 'rotatedAtMs': token.rotatedAtMs, + 'revokedAtMs': token.revokedAtMs, + 'lastUsedAtMs': token.lastUsedAtMs, + }, + ) + .toList(growable: false), + 'createdAtMs': item.createdAtMs, + 'approvedAtMs': item.approvedAtMs, + 'currentDevice': item.currentDevice, + }, + ) .toList(growable: false), }; case 'system-presence': @@ -235,7 +241,9 @@ void setGatewaySnapshotForTest( ) { final runtime = controller.runtime; if (runtime is! _TestFakeGatewayRuntime) { - throw StateError('createTestController() runtime does not support mutation'); + throw StateError( + 'createTestController() runtime does not support mutation', + ); } runtime.setSnapshotForTest(snapshot); } @@ -246,7 +254,9 @@ void setGatewayPairingListForTest( ) { final runtime = controller.runtime; if (runtime is! _TestFakeGatewayRuntime) { - throw StateError('createTestController() runtime does not support mutation'); + throw StateError( + 'createTestController() runtime does not support mutation', + ); } runtime.setDevicePairingForTest(pairingList); } @@ -262,7 +272,7 @@ class _TestFakeCodexRuntime extends CodexRuntime { Future pumpPage( WidgetTester tester, { required Widget child, - Size size = const Size(1600, 1000), + Size size = const Size(1600, 4000), TargetPlatform? platform, }) async { tester.view.devicePixelRatio = 1; diff --git a/test/widgets/sidebar_navigation_suite.dart b/test/widgets/sidebar_navigation_suite.dart index 681ac7e2..7a18a0a4 100644 --- a/test/widgets/sidebar_navigation_suite.dart +++ b/test/widgets/sidebar_navigation_suite.dart @@ -85,7 +85,7 @@ void main() { expect(find.text('ClawHub'), findsNothing); expect(find.text('回到 APP首页'), findsNothing); expect(find.text('设置'), findsOneWidget); - expect(find.text('账户'), findsOneWidget); + expect(find.text('账户'), findsNothing); expect(find.text('语言'), findsOneWidget); expect(find.text('主题'), findsOneWidget); @@ -107,11 +107,11 @@ void main() { await tester.pumpAndSettle(); expect(themeToggled, 1); - await tester.tap( + expect( find.byKey(const ValueKey('sidebar-footer-account')), + findsNothing, ); - await tester.pumpAndSettle(); - expect(accountOpened, 1); + expect(accountOpened, 0); await tester.tap( find.byKey(const Key('workspace-sidebar-collapse-button')),