diff --git a/lib/web/web_focus_panel.dart b/lib/web/web_focus_panel.dart index c42d1282..652a6389 100644 --- a/lib/web/web_focus_panel.dart +++ b/lib/web/web_focus_panel.dart @@ -10,3 +10,5 @@ import '../widgets/settings_focus_quick_actions.dart'; import '../widgets/surface_card.dart'; part 'web_focus_panel_core.part.dart'; +part 'web_focus_panel_previews.part.dart'; +part 'web_focus_panel_support.part.dart'; diff --git a/lib/web/web_focus_panel_core.part.dart b/lib/web/web_focus_panel_core.part.dart index 8e8db2b7..897ae078 100644 --- a/lib/web/web_focus_panel_core.part.dart +++ b/lib/web/web_focus_panel_core.part.dart @@ -318,685 +318,3 @@ class _AssistantFocusPreview extends StatelessWidget { } } -class _TasksFocusPreview extends StatelessWidget { - const _TasksFocusPreview({required this.controller}); - - final AppController controller; - - @override - Widget build(BuildContext context) { - final items = [ - ...controller.tasksController.running.take(2), - ...controller.tasksController.queue.take(2), - ...controller.tasksController.history.take(1), - ].take(4).toList(growable: false); - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Wrap( - spacing: 8, - runSpacing: 8, - children: [ - _FocusPill( - label: appText( - '运行中 ${controller.tasksController.running.length}', - 'Running ${controller.tasksController.running.length}', - ), - ), - _FocusPill( - label: appText( - '队列 ${controller.tasksController.queue.length}', - 'Queue ${controller.tasksController.queue.length}', - ), - ), - _FocusPill( - label: appText( - '计划 ${controller.tasksController.scheduled.length}', - 'Scheduled ${controller.tasksController.scheduled.length}', - ), - ), - ], - ), - const SizedBox(height: 12), - if (items.isEmpty) - _PreviewEmptyState( - message: - controller.connection.status == - RuntimeConnectionStatus.connected - ? appText('当前没有任务摘要。', 'No task summary yet.') - : appText( - '连接 Gateway 后这里会显示任务摘要。', - 'Connect a gateway to load task summaries.', - ), - ) - else - ...items.map( - (item) => Padding( - padding: const EdgeInsets.only(bottom: 8), - child: _FocusListTile( - title: item.title, - subtitle: item.summary, - trailing: item.status, - ), - ), - ), - ], - ); - } -} - -class _SkillsFocusPreview extends StatelessWidget { - const _SkillsFocusPreview({required this.controller}); - - final AppController controller; - - @override - Widget build(BuildContext context) { - final items = controller.isSingleAgentMode - ? controller - .assistantImportedSkillsForSession(controller.currentSessionKey) - .take(4) - .map( - (skill) => GatewaySkillSummary( - name: skill.label, - description: skill.description, - source: skill.sourcePath, - skillKey: skill.key, - primaryEnv: null, - eligible: true, - disabled: false, - missingBins: const [], - missingEnv: const [], - missingConfig: const [], - ), - ) - .toList(growable: false) - : controller.skills.take(4).toList(growable: false); - if (items.isEmpty) { - return _PreviewEmptyState( - message: controller.isSingleAgentMode - ? (controller.currentSingleAgentNeedsAiGatewayConfiguration - ? appText( - '当前没有可用的外部 Agent ACP 端点,请先配置 LLM API fallback。', - 'No external Agent ACP endpoint is available. Configure LLM API fallback first.', - ) - : appText( - '当前线程还没有已加载技能。切换 provider 后会读取该线程自己的 skills 列表。', - 'No skills are loaded for this thread yet. Switching the provider reloads the thread-owned skills list.', - )) - : controller.connection.status == RuntimeConnectionStatus.connected - ? appText( - '当前代理没有已加载技能。', - 'No skills are loaded for the active agent.', - ) - : appText( - '连接 Gateway 后可查看技能摘要。', - 'Connect a gateway to inspect skills here.', - ), - ); - } - return Column( - children: items - .map( - (skill) => Padding( - padding: const EdgeInsets.only(bottom: 8), - child: _FocusListTile( - title: skill.name, - subtitle: skill.description, - trailing: skill.disabled - ? appText('已禁用', 'Disabled') - : appText('已启用', 'Enabled'), - ), - ), - ) - .toList(growable: false), - ); - } -} - -class _NodesFocusPreview extends StatelessWidget { - const _NodesFocusPreview({required this.controller}); - - final AppController controller; - - @override - Widget build(BuildContext context) { - final items = controller.instances.take(4).toList(growable: false); - if (items.isEmpty) { - return _PreviewEmptyState( - message: appText('当前没有节点可显示。', 'No nodes are available right now.'), - ); - } - return Column( - children: items - .map( - (instance) => Padding( - padding: const EdgeInsets.only(bottom: 8), - child: _FocusListTile( - title: instance.host?.trim().isNotEmpty == true - ? instance.host! - : instance.id, - subtitle: - [instance.platform, instance.deviceFamily, instance.ip] - .whereType() - .where((item) => item.trim().isNotEmpty) - .join(' · '), - trailing: instance.mode ?? appText('未知', 'Unknown'), - ), - ), - ) - .toList(growable: false), - ); - } -} - -class _AgentsFocusPreview extends StatelessWidget { - const _AgentsFocusPreview({required this.controller}); - - final AppController controller; - - @override - Widget build(BuildContext context) { - final items = controller.agents.take(5).toList(growable: false); - if (items.isEmpty) { - return _PreviewEmptyState( - message: appText('当前没有代理摘要。', 'No agents are available right now.'), - ); - } - return Column( - children: items - .map( - (agent) => Padding( - padding: const EdgeInsets.only(bottom: 8), - child: _FocusListTile( - title: '${agent.emoji} ${agent.name}', - subtitle: agent.id, - trailing: agent.name == controller.activeAgentName - ? appText('当前', 'Active') - : agent.theme, - ), - ), - ) - .toList(growable: false), - ); - } -} - -class _McpFocusPreview extends StatelessWidget { - const _McpFocusPreview({required this.controller}); - - final AppController controller; - - @override - Widget build(BuildContext context) { - final items = controller.connectors.take(4).toList(growable: false); - if (items.isEmpty) { - return _PreviewEmptyState( - message: appText( - '当前没有 MCP 连接器。连接 Gateway 后这里会显示工具摘要。', - 'No MCP connectors yet. Connect a gateway to load tool summaries here.', - ), - ); - } - return Column( - children: items - .map( - (connector) => Padding( - padding: const EdgeInsets.only(bottom: 8), - child: _FocusListTile( - title: connector.label, - subtitle: connector.detailLabel, - trailing: connector.status, - ), - ), - ) - .toList(growable: false), - ); - } -} - -class _ClawHubFocusPreview extends StatelessWidget { - const _ClawHubFocusPreview({required this.controller}); - - final AppController controller; - - @override - Widget build(BuildContext context) { - final skillCount = controller.isSingleAgentMode - ? controller.currentAssistantSkillCount - : controller.skills.length; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Wrap( - spacing: 8, - runSpacing: 8, - children: [ - _FocusPill( - label: appText('已加载技能 $skillCount', 'Loaded skills $skillCount'), - ), - _FocusPill( - label: appText( - '关注入口 ${controller.assistantNavigationDestinations.length}', - 'Pinned ${controller.assistantNavigationDestinations.length}', - ), - ), - ], - ), - const SizedBox(height: 12), - _PreviewEmptyState( - message: appText( - 'ClawHub 适合放在侧板做快速搜索或安装入口;需要完整终端交互时,再打开全页。', - 'Use ClawHub in the side panel for quick access. Open the full page when you need the terminal workflow.', - ), - ), - ], - ); - } -} - -class _SecretsFocusPreview extends StatelessWidget { - const _SecretsFocusPreview({required this.controller}); - - final AppController controller; - - @override - Widget build(BuildContext context) { - final items = controller.secretReferences.take(4).toList(growable: false); - if (items.isEmpty) { - return _PreviewEmptyState( - message: appText( - '当前没有密钥引用摘要。', - 'No masked secret references are available yet.', - ), - ); - } - return Column( - children: items - .map( - (secret) => Padding( - padding: const EdgeInsets.only(bottom: 8), - child: _FocusListTile( - title: secret.name, - subtitle: '${secret.provider} · ${secret.module}', - trailing: secret.status, - ), - ), - ) - .toList(growable: false), - ); - } -} - -class _AiGatewayFocusPreview extends StatelessWidget { - const _AiGatewayFocusPreview({required this.controller}); - - final AppController controller; - - @override - Widget build(BuildContext context) { - final items = controller.models.take(4).toList(growable: false); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Wrap( - spacing: 8, - runSpacing: 8, - children: [ - _FocusPill(label: controller.connection.status.label), - _FocusPill( - label: appText( - '模型 ${controller.models.length}', - 'Models ${controller.models.length}', - ), - ), - ], - ), - const SizedBox(height: 12), - if (items.isEmpty) - _PreviewEmptyState( - message: appText( - '当前没有 LLM API 模型摘要。', - 'No LLM API model summary is available yet.', - ), - ) - else - ...items.map( - (model) => Padding( - padding: const EdgeInsets.only(bottom: 8), - child: _FocusListTile( - title: model.name, - subtitle: model.provider, - trailing: model.id, - ), - ), - ), - ], - ); - } -} - -class _SettingsFocusPreview extends StatelessWidget { - const _SettingsFocusPreview({required this.controller}); - - final AppController controller; - - @override - Widget build(BuildContext context) { - final languageLabel = controller.appLanguage == AppLanguage.zh - ? appText('中文', 'Chinese') - : 'English'; - final themeLabel = switch (controller.themeMode) { - ThemeMode.dark => appText('深色', 'Dark'), - ThemeMode.light => appText('浅色', 'Light'), - ThemeMode.system => appText('跟随系统', 'System'), - }; - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SettingsFocusQuickActions( - appLanguage: controller.appLanguage, - themeMode: controller.themeMode, - onToggleLanguage: controller.toggleAppLanguage, - onToggleTheme: () { - controller.setThemeMode( - controller.themeMode == ThemeMode.dark - ? ThemeMode.light - : ThemeMode.dark, - ); - }, - languageButtonKey: const Key( - 'assistant-focus-settings-language-toggle', - ), - themeButtonKey: const Key('assistant-focus-settings-theme-toggle'), - ), - const SizedBox(height: 12), - _FocusListTile( - title: appText('语言', 'Language'), - subtitle: appText('当前界面语言', 'Current interface language'), - trailing: languageLabel, - ), - const SizedBox(height: 8), - _FocusListTile( - title: appText('主题', 'Theme'), - subtitle: appText('当前显示模式', 'Current display mode'), - trailing: themeLabel, - ), - const SizedBox(height: 8), - _FocusListTile( - title: appText('执行目标', 'Execution target'), - subtitle: appText( - 'Assistant 默认运行位置', - 'Default assistant execution target', - ), - trailing: controller.assistantExecutionTarget.label, - ), - const SizedBox(height: 8), - _FocusListTile( - title: appText('权限', 'Permissions'), - subtitle: appText( - 'Assistant 默认权限级别', - 'Default assistant permission level', - ), - trailing: controller.assistantPermissionLevel.label, - ), - ], - ); - } -} - -class _LanguageFocusPreview extends StatelessWidget { - const _LanguageFocusPreview({required this.controller}); - - final AppController controller; - - @override - Widget build(BuildContext context) { - final currentLabel = controller.appLanguage == AppLanguage.zh - ? appText('中文', 'Chinese') - : 'English'; - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ChromeLanguageActionButton( - key: const Key('assistant-focus-language-toggle'), - appLanguage: controller.appLanguage, - compact: false, - tooltip: appText('切换语言', 'Toggle language'), - onPressed: controller.toggleAppLanguage, - ), - const SizedBox(height: 12), - _FocusListTile( - title: appText('当前语言', 'Current language'), - subtitle: appText( - '点击上方按钮即可在中英文界面之间切换。', - 'Use the button above to switch between Chinese and English.', - ), - trailing: currentLabel, - ), - ], - ); - } -} - -class _ThemeFocusPreview extends StatelessWidget { - const _ThemeFocusPreview({required this.controller}); - - final AppController controller; - - @override - Widget build(BuildContext context) { - final themeLabel = switch (controller.themeMode) { - ThemeMode.dark => appText('深色', 'Dark'), - ThemeMode.light => appText('浅色', 'Light'), - ThemeMode.system => appText('跟随系统', 'System'), - }; - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ChromeIconActionButton( - key: const Key('assistant-focus-theme-toggle'), - icon: chromeThemeToggleIcon(controller.themeMode), - tooltip: chromeThemeToggleTooltip(controller.themeMode), - onPressed: () { - controller.setThemeMode( - controller.themeMode == ThemeMode.dark - ? ThemeMode.light - : ThemeMode.dark, - ); - }, - ), - const SizedBox(height: 12), - _FocusListTile( - title: appText('当前主题', 'Current theme'), - subtitle: appText( - '点击上方按钮即可切换亮度模式。', - 'Use the button above to switch appearance mode.', - ), - trailing: themeLabel, - ), - ], - ); - } -} - -class _FocusListTile extends StatelessWidget { - const _FocusListTile({ - required this.title, - required this.subtitle, - required this.trailing, - }); - - final String title; - final String subtitle; - final String trailing; - - @override - Widget build(BuildContext context) { - final palette = context.palette; - final theme = Theme.of(context); - - return Container( - width: double.infinity, - padding: const EdgeInsets.fromLTRB(12, 12, 12, 12), - decoration: BoxDecoration( - color: palette.surfaceSecondary, - borderRadius: BorderRadius.circular(14), - border: Border.all(color: palette.strokeSoft), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: theme.textTheme.titleSmall?.copyWith( - fontWeight: FontWeight.w600, - ), - ), - const SizedBox(height: 4), - Text( - subtitle, - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: theme.textTheme.bodySmall?.copyWith( - color: palette.textSecondary, - height: 1.3, - ), - ), - const SizedBox(height: 8), - Text( - trailing, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: theme.textTheme.labelLarge?.copyWith( - color: palette.textPrimary, - ), - ), - ], - ), - ); - } -} - -class _FocusPill extends StatelessWidget { - const _FocusPill({required this.label}); - - final String label; - - @override - Widget build(BuildContext context) { - final palette = context.palette; - final theme = Theme.of(context); - - return Container( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), - decoration: BoxDecoration( - color: palette.surfaceSecondary, - borderRadius: BorderRadius.circular(999), - border: Border.all(color: palette.strokeSoft), - ), - child: Text( - label, - style: theme.textTheme.labelLarge?.copyWith( - color: palette.textSecondary, - ), - ), - ); - } -} - -class _PreviewEmptyState extends StatelessWidget { - const _PreviewEmptyState({required this.message}); - - final String message; - - @override - Widget build(BuildContext context) { - final palette = context.palette; - final theme = Theme.of(context); - - return Container( - width: double.infinity, - padding: const EdgeInsets.all(14), - decoration: BoxDecoration( - color: palette.surfaceSecondary, - borderRadius: BorderRadius.circular(14), - border: Border.all(color: palette.strokeSoft), - ), - child: Text( - message, - style: theme.textTheme.bodySmall?.copyWith( - color: palette.textSecondary, - height: 1.35, - ), - ), - ); - } -} - -class _AssistantFocusEmptyState extends StatelessWidget { - const _AssistantFocusEmptyState({ - required this.message, - required this.available, - required this.onAdd, - }); - - final String message; - final List available; - final Future Function(AssistantFocusEntry destination) onAdd; - - @override - Widget build(BuildContext context) { - final palette = context.palette; - final theme = Theme.of(context); - - return ListView( - padding: const EdgeInsets.fromLTRB(12, 12, 12, 12), - children: [ - Container( - width: double.infinity, - padding: const EdgeInsets.all(14), - decoration: BoxDecoration( - color: palette.surfaceSecondary, - borderRadius: BorderRadius.circular(14), - border: Border.all(color: palette.strokeSoft), - ), - child: Text( - message, - style: theme.textTheme.bodySmall?.copyWith( - color: palette.textSecondary, - height: 1.35, - ), - ), - ), - if (available.isNotEmpty) ...[ - const SizedBox(height: 12), - Wrap( - spacing: 8, - runSpacing: 8, - children: available - .map( - (destination) => ActionChip( - key: ValueKey( - 'assistant-focus-add-${destination.name}', - ), - avatar: Icon(destination.icon, size: 16), - label: Text(destination.label), - onPressed: () async { - await onAdd(destination); - }, - ), - ) - .toList(growable: false), - ), - ], - ], - ); - } -} diff --git a/lib/web/web_focus_panel_previews.part.dart b/lib/web/web_focus_panel_previews.part.dart new file mode 100644 index 00000000..a2d895ec --- /dev/null +++ b/lib/web/web_focus_panel_previews.part.dart @@ -0,0 +1,624 @@ +part of 'web_focus_panel.dart'; + +class _TasksFocusPreview extends StatelessWidget { + const _TasksFocusPreview({required this.controller}); + + final AppController controller; + + @override + Widget build(BuildContext context) { + final items = [ + ...controller.tasksController.running.take(2), + ...controller.tasksController.queue.take(2), + ...controller.tasksController.history.take(1), + ].take(4).toList(growable: false); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Wrap( + spacing: 8, + runSpacing: 8, + children: [ + _FocusPill( + label: appText( + '运行中 ${controller.tasksController.running.length}', + 'Running ${controller.tasksController.running.length}', + ), + ), + _FocusPill( + label: appText( + '队列 ${controller.tasksController.queue.length}', + 'Queue ${controller.tasksController.queue.length}', + ), + ), + _FocusPill( + label: appText( + '计划 ${controller.tasksController.scheduled.length}', + 'Scheduled ${controller.tasksController.scheduled.length}', + ), + ), + ], + ), + const SizedBox(height: 12), + if (items.isEmpty) + _PreviewEmptyState( + message: + controller.connection.status == + RuntimeConnectionStatus.connected + ? appText('当前没有任务摘要。', 'No task summary yet.') + : appText( + '连接 Gateway 后这里会显示任务摘要。', + 'Connect a gateway to load task summaries.', + ), + ) + else + ...items.map( + (item) => Padding( + padding: const EdgeInsets.only(bottom: 8), + child: _FocusListTile( + title: item.title, + subtitle: item.summary, + trailing: item.status, + ), + ), + ), + ], + ); + } +} + +class _SkillsFocusPreview extends StatelessWidget { + const _SkillsFocusPreview({required this.controller}); + + final AppController controller; + + @override + Widget build(BuildContext context) { + final items = controller.isSingleAgentMode + ? controller + .assistantImportedSkillsForSession(controller.currentSessionKey) + .take(4) + .map( + (skill) => GatewaySkillSummary( + name: skill.label, + description: skill.description, + source: skill.sourcePath, + skillKey: skill.key, + primaryEnv: null, + eligible: true, + disabled: false, + missingBins: const [], + missingEnv: const [], + missingConfig: const [], + ), + ) + .toList(growable: false) + : controller.skills.take(4).toList(growable: false); + if (items.isEmpty) { + return _PreviewEmptyState( + message: controller.isSingleAgentMode + ? (controller.currentSingleAgentNeedsAiGatewayConfiguration + ? appText( + '当前没有可用的外部 Agent ACP 端点,请先配置 LLM API fallback。', + 'No external Agent ACP endpoint is available. Configure LLM API fallback first.', + ) + : appText( + '当前线程还没有已加载技能。切换 provider 后会读取该线程自己的 skills 列表。', + 'No skills are loaded for this thread yet. Switching the provider reloads the thread-owned skills list.', + )) + : controller.connection.status == RuntimeConnectionStatus.connected + ? appText( + '当前代理没有已加载技能。', + 'No skills are loaded for the active agent.', + ) + : appText( + '连接 Gateway 后可查看技能摘要。', + 'Connect a gateway to inspect skills here.', + ), + ); + } + return Column( + children: items + .map( + (skill) => Padding( + padding: const EdgeInsets.only(bottom: 8), + child: _FocusListTile( + title: skill.name, + subtitle: skill.description, + trailing: skill.disabled + ? appText('已禁用', 'Disabled') + : appText('已启用', 'Enabled'), + ), + ), + ) + .toList(growable: false), + ); + } +} + +class _NodesFocusPreview extends StatelessWidget { + const _NodesFocusPreview({required this.controller}); + + final AppController controller; + + @override + Widget build(BuildContext context) { + final items = controller.instances.take(4).toList(growable: false); + if (items.isEmpty) { + return _PreviewEmptyState( + message: appText('当前没有节点可显示。', 'No nodes are available right now.'), + ); + } + return Column( + children: items + .map( + (instance) => Padding( + padding: const EdgeInsets.only(bottom: 8), + child: _FocusListTile( + title: instance.host?.trim().isNotEmpty == true + ? instance.host! + : instance.id, + subtitle: + [instance.platform, instance.deviceFamily, instance.ip] + .whereType() + .where((item) => item.trim().isNotEmpty) + .join(' · '), + trailing: instance.mode ?? appText('未知', 'Unknown'), + ), + ), + ) + .toList(growable: false), + ); + } +} + +class _AgentsFocusPreview extends StatelessWidget { + const _AgentsFocusPreview({required this.controller}); + + final AppController controller; + + @override + Widget build(BuildContext context) { + final items = controller.agents.take(5).toList(growable: false); + if (items.isEmpty) { + return _PreviewEmptyState( + message: appText('当前没有代理摘要。', 'No agents are available right now.'), + ); + } + return Column( + children: items + .map( + (agent) => Padding( + padding: const EdgeInsets.only(bottom: 8), + child: _FocusListTile( + title: '${agent.emoji} ${agent.name}', + subtitle: agent.id, + trailing: agent.name == controller.activeAgentName + ? appText('当前', 'Active') + : agent.theme, + ), + ), + ) + .toList(growable: false), + ); + } +} + +class _McpFocusPreview extends StatelessWidget { + const _McpFocusPreview({required this.controller}); + + final AppController controller; + + @override + Widget build(BuildContext context) { + final items = controller.connectors.take(4).toList(growable: false); + if (items.isEmpty) { + return _PreviewEmptyState( + message: appText( + '当前没有 MCP 连接器。连接 Gateway 后这里会显示工具摘要。', + 'No MCP connectors yet. Connect a gateway to load tool summaries here.', + ), + ); + } + return Column( + children: items + .map( + (connector) => Padding( + padding: const EdgeInsets.only(bottom: 8), + child: _FocusListTile( + title: connector.label, + subtitle: connector.detailLabel, + trailing: connector.status, + ), + ), + ) + .toList(growable: false), + ); + } +} + +class _ClawHubFocusPreview extends StatelessWidget { + const _ClawHubFocusPreview({required this.controller}); + + final AppController controller; + + @override + Widget build(BuildContext context) { + final skillCount = controller.isSingleAgentMode + ? controller.currentAssistantSkillCount + : controller.skills.length; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Wrap( + spacing: 8, + runSpacing: 8, + children: [ + _FocusPill( + label: appText('已加载技能 $skillCount', 'Loaded skills $skillCount'), + ), + _FocusPill( + label: appText( + '关注入口 ${controller.assistantNavigationDestinations.length}', + 'Pinned ${controller.assistantNavigationDestinations.length}', + ), + ), + ], + ), + const SizedBox(height: 12), + _PreviewEmptyState( + message: appText( + 'ClawHub 适合放在侧板做快速搜索或安装入口;需要完整终端交互时,再打开全页。', + 'Use ClawHub in the side panel for quick access. Open the full page when you need the terminal workflow.', + ), + ), + ], + ); + } +} + +class _SecretsFocusPreview extends StatelessWidget { + const _SecretsFocusPreview({required this.controller}); + + final AppController controller; + + @override + Widget build(BuildContext context) { + final items = controller.secretReferences.take(4).toList(growable: false); + if (items.isEmpty) { + return _PreviewEmptyState( + message: appText( + '当前没有密钥引用摘要。', + 'No masked secret references are available yet.', + ), + ); + } + return Column( + children: items + .map( + (secret) => Padding( + padding: const EdgeInsets.only(bottom: 8), + child: _FocusListTile( + title: secret.name, + subtitle: '${secret.provider} · ${secret.module}', + trailing: secret.status, + ), + ), + ) + .toList(growable: false), + ); + } +} + +class _AiGatewayFocusPreview extends StatelessWidget { + const _AiGatewayFocusPreview({required this.controller}); + + final AppController controller; + + @override + Widget build(BuildContext context) { + final items = controller.models.take(4).toList(growable: false); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Wrap( + spacing: 8, + runSpacing: 8, + children: [ + _FocusPill(label: controller.connection.status.label), + _FocusPill( + label: appText( + '模型 ${controller.models.length}', + 'Models ${controller.models.length}', + ), + ), + ], + ), + const SizedBox(height: 12), + if (items.isEmpty) + _PreviewEmptyState( + message: appText( + '当前没有 LLM API 模型摘要。', + 'No LLM API model summary is available yet.', + ), + ) + else + ...items.map( + (model) => Padding( + padding: const EdgeInsets.only(bottom: 8), + child: _FocusListTile( + title: model.name, + subtitle: model.provider, + trailing: model.id, + ), + ), + ), + ], + ); + } +} + +class _SettingsFocusPreview extends StatelessWidget { + const _SettingsFocusPreview({required this.controller}); + + final AppController controller; + + @override + Widget build(BuildContext context) { + final languageLabel = controller.appLanguage == AppLanguage.zh + ? appText('中文', 'Chinese') + : 'English'; + final themeLabel = switch (controller.themeMode) { + ThemeMode.dark => appText('深色', 'Dark'), + ThemeMode.light => appText('浅色', 'Light'), + ThemeMode.system => appText('跟随系统', 'System'), + }; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SettingsFocusQuickActions( + appLanguage: controller.appLanguage, + themeMode: controller.themeMode, + onToggleLanguage: controller.toggleAppLanguage, + onToggleTheme: () { + controller.setThemeMode( + controller.themeMode == ThemeMode.dark + ? ThemeMode.light + : ThemeMode.dark, + ); + }, + languageButtonKey: const Key( + 'assistant-focus-settings-language-toggle', + ), + themeButtonKey: const Key('assistant-focus-settings-theme-toggle'), + ), + const SizedBox(height: 12), + _FocusListTile( + title: appText('语言', 'Language'), + subtitle: appText('当前界面语言', 'Current interface language'), + trailing: languageLabel, + ), + const SizedBox(height: 8), + _FocusListTile( + title: appText('主题', 'Theme'), + subtitle: appText('当前显示模式', 'Current display mode'), + trailing: themeLabel, + ), + const SizedBox(height: 8), + _FocusListTile( + title: appText('执行目标', 'Execution target'), + subtitle: appText( + 'Assistant 默认运行位置', + 'Default assistant execution target', + ), + trailing: controller.assistantExecutionTarget.label, + ), + const SizedBox(height: 8), + _FocusListTile( + title: appText('权限', 'Permissions'), + subtitle: appText( + 'Assistant 默认权限级别', + 'Default assistant permission level', + ), + trailing: controller.assistantPermissionLevel.label, + ), + ], + ); + } +} + +class _LanguageFocusPreview extends StatelessWidget { + const _LanguageFocusPreview({required this.controller}); + + final AppController controller; + + @override + Widget build(BuildContext context) { + final currentLabel = controller.appLanguage == AppLanguage.zh + ? appText('中文', 'Chinese') + : 'English'; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ChromeLanguageActionButton( + key: const Key('assistant-focus-language-toggle'), + appLanguage: controller.appLanguage, + compact: false, + tooltip: appText('切换语言', 'Toggle language'), + onPressed: controller.toggleAppLanguage, + ), + const SizedBox(height: 12), + _FocusListTile( + title: appText('当前语言', 'Current language'), + subtitle: appText( + '点击上方按钮即可在中英文界面之间切换。', + 'Use the button above to switch between Chinese and English.', + ), + trailing: currentLabel, + ), + ], + ); + } +} + +class _ThemeFocusPreview extends StatelessWidget { + const _ThemeFocusPreview({required this.controller}); + + final AppController controller; + + @override + Widget build(BuildContext context) { + final themeLabel = switch (controller.themeMode) { + ThemeMode.dark => appText('深色', 'Dark'), + ThemeMode.light => appText('浅色', 'Light'), + ThemeMode.system => appText('跟随系统', 'System'), + }; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ChromeIconActionButton( + key: const Key('assistant-focus-theme-toggle'), + icon: chromeThemeToggleIcon(controller.themeMode), + tooltip: chromeThemeToggleTooltip(controller.themeMode), + onPressed: () { + controller.setThemeMode( + controller.themeMode == ThemeMode.dark + ? ThemeMode.light + : ThemeMode.dark, + ); + }, + ), + const SizedBox(height: 12), + _FocusListTile( + title: appText('当前主题', 'Current theme'), + subtitle: appText( + '点击上方按钮即可切换亮度模式。', + 'Use the button above to switch appearance mode.', + ), + trailing: themeLabel, + ), + ], + ); + } +} + +class _FocusListTile extends StatelessWidget { + const _FocusListTile({ + required this.title, + required this.subtitle, + required this.trailing, + }); + + final String title; + final String subtitle; + final String trailing; + + @override + Widget build(BuildContext context) { + final palette = context.palette; + final theme = Theme.of(context); + + return Container( + width: double.infinity, + padding: const EdgeInsets.fromLTRB(12, 12, 12, 12), + decoration: BoxDecoration( + color: palette.surfaceSecondary, + borderRadius: BorderRadius.circular(14), + border: Border.all(color: palette.strokeSoft), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 4), + Text( + subtitle, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.bodySmall?.copyWith( + color: palette.textSecondary, + height: 1.3, + ), + ), + const SizedBox(height: 8), + Text( + trailing, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.labelLarge?.copyWith( + color: palette.textPrimary, + ), + ), + ], + ), + ); + } +} + +class _FocusPill extends StatelessWidget { + const _FocusPill({required this.label}); + + final String label; + + @override + Widget build(BuildContext context) { + final palette = context.palette; + final theme = Theme.of(context); + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), + decoration: BoxDecoration( + color: palette.surfaceSecondary, + borderRadius: BorderRadius.circular(999), + border: Border.all(color: palette.strokeSoft), + ), + child: Text( + label, + style: theme.textTheme.labelLarge?.copyWith( + color: palette.textSecondary, + ), + ), + ); + } +} + +class _PreviewEmptyState extends StatelessWidget { + const _PreviewEmptyState({required this.message}); + + final String message; + + @override + Widget build(BuildContext context) { + final palette = context.palette; + final theme = Theme.of(context); + + return Container( + width: double.infinity, + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: palette.surfaceSecondary, + borderRadius: BorderRadius.circular(14), + border: Border.all(color: palette.strokeSoft), + ), + child: Text( + message, + style: theme.textTheme.bodySmall?.copyWith( + color: palette.textSecondary, + height: 1.35, + ), + ), + ); + } +} + diff --git a/lib/web/web_focus_panel_support.part.dart b/lib/web/web_focus_panel_support.part.dart new file mode 100644 index 00000000..bac1c3bb --- /dev/null +++ b/lib/web/web_focus_panel_support.part.dart @@ -0,0 +1,62 @@ +part of 'web_focus_panel.dart'; + +class _AssistantFocusEmptyState extends StatelessWidget { + const _AssistantFocusEmptyState({ + required this.message, + required this.available, + required this.onAdd, + }); + + final String message; + final List available; + final Future Function(AssistantFocusEntry destination) onAdd; + + @override + Widget build(BuildContext context) { + final palette = context.palette; + final theme = Theme.of(context); + + return ListView( + padding: const EdgeInsets.fromLTRB(12, 12, 12, 12), + children: [ + Container( + width: double.infinity, + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: palette.surfaceSecondary, + borderRadius: BorderRadius.circular(14), + border: Border.all(color: palette.strokeSoft), + ), + child: Text( + message, + style: theme.textTheme.bodySmall?.copyWith( + color: palette.textSecondary, + height: 1.35, + ), + ), + ), + if (available.isNotEmpty) ...[ + const SizedBox(height: 12), + Wrap( + spacing: 8, + runSpacing: 8, + children: available + .map( + (destination) => ActionChip( + key: ValueKey( + 'assistant-focus-add-${destination.name}', + ), + avatar: Icon(destination.icon, size: 16), + label: Text(destination.label), + onPressed: () async { + await onAdd(destination); + }, + ), + ) + .toList(growable: false), + ), + ], + ], + ); + } +} diff --git a/lib/widgets/assistant_focus_panel.dart b/lib/widgets/assistant_focus_panel.dart index d416ee7e..4abf5c0d 100644 --- a/lib/widgets/assistant_focus_panel.dart +++ b/lib/widgets/assistant_focus_panel.dart @@ -10,3 +10,5 @@ import 'settings_focus_quick_actions.dart'; import 'surface_card.dart'; part 'assistant_focus_panel_core.part.dart'; +part 'assistant_focus_panel_previews.part.dart'; +part 'assistant_focus_panel_support.part.dart'; diff --git a/lib/widgets/assistant_focus_panel_core.part.dart b/lib/widgets/assistant_focus_panel_core.part.dart index 7806ac78..8e241338 100644 --- a/lib/widgets/assistant_focus_panel_core.part.dart +++ b/lib/widgets/assistant_focus_panel_core.part.dart @@ -318,685 +318,3 @@ class _AssistantFocusPreview extends StatelessWidget { } } -class _TasksFocusPreview extends StatelessWidget { - const _TasksFocusPreview({required this.controller}); - - final AppController controller; - - @override - Widget build(BuildContext context) { - final items = [ - ...controller.tasksController.running.take(2), - ...controller.tasksController.queue.take(2), - ...controller.tasksController.history.take(1), - ].take(4).toList(growable: false); - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Wrap( - spacing: 8, - runSpacing: 8, - children: [ - _FocusPill( - label: appText( - '运行中 ${controller.tasksController.running.length}', - 'Running ${controller.tasksController.running.length}', - ), - ), - _FocusPill( - label: appText( - '队列 ${controller.tasksController.queue.length}', - 'Queue ${controller.tasksController.queue.length}', - ), - ), - _FocusPill( - label: appText( - '计划 ${controller.tasksController.scheduled.length}', - 'Scheduled ${controller.tasksController.scheduled.length}', - ), - ), - ], - ), - const SizedBox(height: 12), - if (items.isEmpty) - _PreviewEmptyState( - message: - controller.connection.status == - RuntimeConnectionStatus.connected - ? appText('当前没有任务摘要。', 'No task summary yet.') - : appText( - '连接 Gateway 后这里会显示任务摘要。', - 'Connect a gateway to load task summaries.', - ), - ) - else - ...items.map( - (item) => Padding( - padding: const EdgeInsets.only(bottom: 8), - child: _FocusListTile( - title: item.title, - subtitle: item.summary, - trailing: item.status, - ), - ), - ), - ], - ); - } -} - -class _SkillsFocusPreview extends StatelessWidget { - const _SkillsFocusPreview({required this.controller}); - - final AppController controller; - - @override - Widget build(BuildContext context) { - final items = controller.isSingleAgentMode - ? controller - .assistantImportedSkillsForSession(controller.currentSessionKey) - .take(4) - .map( - (skill) => GatewaySkillSummary( - name: skill.label, - description: skill.description, - source: skill.sourcePath, - skillKey: skill.key, - primaryEnv: null, - eligible: true, - disabled: false, - missingBins: const [], - missingEnv: const [], - missingConfig: const [], - ), - ) - .toList(growable: false) - : controller.skills.take(4).toList(growable: false); - if (items.isEmpty) { - return _PreviewEmptyState( - message: controller.isSingleAgentMode - ? (controller.currentSingleAgentNeedsAiGatewayConfiguration - ? appText( - '当前没有可用的外部 Agent ACP 端点,请先配置 LLM API fallback。', - 'No external Agent ACP endpoint is available. Configure LLM API fallback first.', - ) - : appText( - '当前线程还没有已加载技能。切换 provider 后会读取该线程自己的 skills 列表。', - 'No skills are loaded for this thread yet. Switching the provider reloads the thread-owned skills list.', - )) - : controller.connection.status == RuntimeConnectionStatus.connected - ? appText( - '当前代理没有已加载技能。', - 'No skills are loaded for the active agent.', - ) - : appText( - '连接 Gateway 后可查看技能摘要。', - 'Connect a gateway to inspect skills here.', - ), - ); - } - return Column( - children: items - .map( - (skill) => Padding( - padding: const EdgeInsets.only(bottom: 8), - child: _FocusListTile( - title: skill.name, - subtitle: skill.description, - trailing: skill.disabled - ? appText('已禁用', 'Disabled') - : appText('已启用', 'Enabled'), - ), - ), - ) - .toList(growable: false), - ); - } -} - -class _NodesFocusPreview extends StatelessWidget { - const _NodesFocusPreview({required this.controller}); - - final AppController controller; - - @override - Widget build(BuildContext context) { - final items = controller.instances.take(4).toList(growable: false); - if (items.isEmpty) { - return _PreviewEmptyState( - message: appText('当前没有节点可显示。', 'No nodes are available right now.'), - ); - } - return Column( - children: items - .map( - (instance) => Padding( - padding: const EdgeInsets.only(bottom: 8), - child: _FocusListTile( - title: instance.host?.trim().isNotEmpty == true - ? instance.host! - : instance.id, - subtitle: - [instance.platform, instance.deviceFamily, instance.ip] - .whereType() - .where((item) => item.trim().isNotEmpty) - .join(' · '), - trailing: instance.mode ?? appText('未知', 'Unknown'), - ), - ), - ) - .toList(growable: false), - ); - } -} - -class _AgentsFocusPreview extends StatelessWidget { - const _AgentsFocusPreview({required this.controller}); - - final AppController controller; - - @override - Widget build(BuildContext context) { - final items = controller.agents.take(5).toList(growable: false); - if (items.isEmpty) { - return _PreviewEmptyState( - message: appText('当前没有代理摘要。', 'No agents are available right now.'), - ); - } - return Column( - children: items - .map( - (agent) => Padding( - padding: const EdgeInsets.only(bottom: 8), - child: _FocusListTile( - title: '${agent.emoji} ${agent.name}', - subtitle: agent.id, - trailing: agent.name == controller.activeAgentName - ? appText('当前', 'Active') - : agent.theme, - ), - ), - ) - .toList(growable: false), - ); - } -} - -class _McpFocusPreview extends StatelessWidget { - const _McpFocusPreview({required this.controller}); - - final AppController controller; - - @override - Widget build(BuildContext context) { - final items = controller.connectors.take(4).toList(growable: false); - if (items.isEmpty) { - return _PreviewEmptyState( - message: appText( - '当前没有 MCP 连接器。连接 Gateway 后这里会显示工具摘要。', - 'No MCP connectors yet. Connect a gateway to load tool summaries here.', - ), - ); - } - return Column( - children: items - .map( - (connector) => Padding( - padding: const EdgeInsets.only(bottom: 8), - child: _FocusListTile( - title: connector.label, - subtitle: connector.detailLabel, - trailing: connector.status, - ), - ), - ) - .toList(growable: false), - ); - } -} - -class _ClawHubFocusPreview extends StatelessWidget { - const _ClawHubFocusPreview({required this.controller}); - - final AppController controller; - - @override - Widget build(BuildContext context) { - final skillCount = controller.isSingleAgentMode - ? controller.currentAssistantSkillCount - : controller.skills.length; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Wrap( - spacing: 8, - runSpacing: 8, - children: [ - _FocusPill( - label: appText('已加载技能 $skillCount', 'Loaded skills $skillCount'), - ), - _FocusPill( - label: appText( - '关注入口 ${controller.assistantNavigationDestinations.length}', - 'Pinned ${controller.assistantNavigationDestinations.length}', - ), - ), - ], - ), - const SizedBox(height: 12), - _PreviewEmptyState( - message: appText( - 'ClawHub 适合放在侧板做快速搜索或安装入口;需要完整终端交互时,再打开全页。', - 'Use ClawHub in the side panel for quick access. Open the full page when you need the terminal workflow.', - ), - ), - ], - ); - } -} - -class _SecretsFocusPreview extends StatelessWidget { - const _SecretsFocusPreview({required this.controller}); - - final AppController controller; - - @override - Widget build(BuildContext context) { - final items = controller.secretReferences.take(4).toList(growable: false); - if (items.isEmpty) { - return _PreviewEmptyState( - message: appText( - '当前没有密钥引用摘要。', - 'No masked secret references are available yet.', - ), - ); - } - return Column( - children: items - .map( - (secret) => Padding( - padding: const EdgeInsets.only(bottom: 8), - child: _FocusListTile( - title: secret.name, - subtitle: '${secret.provider} · ${secret.module}', - trailing: secret.status, - ), - ), - ) - .toList(growable: false), - ); - } -} - -class _AiGatewayFocusPreview extends StatelessWidget { - const _AiGatewayFocusPreview({required this.controller}); - - final AppController controller; - - @override - Widget build(BuildContext context) { - final items = controller.models.take(4).toList(growable: false); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Wrap( - spacing: 8, - runSpacing: 8, - children: [ - _FocusPill(label: controller.connection.status.label), - _FocusPill( - label: appText( - '模型 ${controller.models.length}', - 'Models ${controller.models.length}', - ), - ), - ], - ), - const SizedBox(height: 12), - if (items.isEmpty) - _PreviewEmptyState( - message: appText( - '当前没有 LLM API 模型摘要。', - 'No LLM API model summary is available yet.', - ), - ) - else - ...items.map( - (model) => Padding( - padding: const EdgeInsets.only(bottom: 8), - child: _FocusListTile( - title: model.name, - subtitle: model.provider, - trailing: model.id, - ), - ), - ), - ], - ); - } -} - -class _SettingsFocusPreview extends StatelessWidget { - const _SettingsFocusPreview({required this.controller}); - - final AppController controller; - - @override - Widget build(BuildContext context) { - final languageLabel = controller.appLanguage == AppLanguage.zh - ? appText('中文', 'Chinese') - : 'English'; - final themeLabel = switch (controller.themeMode) { - ThemeMode.dark => appText('深色', 'Dark'), - ThemeMode.light => appText('浅色', 'Light'), - ThemeMode.system => appText('跟随系统', 'System'), - }; - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SettingsFocusQuickActions( - appLanguage: controller.appLanguage, - themeMode: controller.themeMode, - onToggleLanguage: controller.toggleAppLanguage, - onToggleTheme: () { - controller.setThemeMode( - controller.themeMode == ThemeMode.dark - ? ThemeMode.light - : ThemeMode.dark, - ); - }, - languageButtonKey: const Key( - 'assistant-focus-settings-language-toggle', - ), - themeButtonKey: const Key('assistant-focus-settings-theme-toggle'), - ), - const SizedBox(height: 12), - _FocusListTile( - title: appText('语言', 'Language'), - subtitle: appText('当前界面语言', 'Current interface language'), - trailing: languageLabel, - ), - const SizedBox(height: 8), - _FocusListTile( - title: appText('主题', 'Theme'), - subtitle: appText('当前显示模式', 'Current display mode'), - trailing: themeLabel, - ), - const SizedBox(height: 8), - _FocusListTile( - title: appText('执行目标', 'Execution target'), - subtitle: appText( - 'Assistant 默认运行位置', - 'Default assistant execution target', - ), - trailing: controller.assistantExecutionTarget.label, - ), - const SizedBox(height: 8), - _FocusListTile( - title: appText('权限', 'Permissions'), - subtitle: appText( - 'Assistant 默认权限级别', - 'Default assistant permission level', - ), - trailing: controller.assistantPermissionLevel.label, - ), - ], - ); - } -} - -class _LanguageFocusPreview extends StatelessWidget { - const _LanguageFocusPreview({required this.controller}); - - final AppController controller; - - @override - Widget build(BuildContext context) { - final currentLabel = controller.appLanguage == AppLanguage.zh - ? appText('中文', 'Chinese') - : 'English'; - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ChromeLanguageActionButton( - key: const Key('assistant-focus-language-toggle'), - appLanguage: controller.appLanguage, - compact: false, - tooltip: appText('切换语言', 'Toggle language'), - onPressed: controller.toggleAppLanguage, - ), - const SizedBox(height: 12), - _FocusListTile( - title: appText('当前语言', 'Current language'), - subtitle: appText( - '点击上方按钮即可在中英文界面之间切换。', - 'Use the button above to switch between Chinese and English.', - ), - trailing: currentLabel, - ), - ], - ); - } -} - -class _ThemeFocusPreview extends StatelessWidget { - const _ThemeFocusPreview({required this.controller}); - - final AppController controller; - - @override - Widget build(BuildContext context) { - final themeLabel = switch (controller.themeMode) { - ThemeMode.dark => appText('深色', 'Dark'), - ThemeMode.light => appText('浅色', 'Light'), - ThemeMode.system => appText('跟随系统', 'System'), - }; - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ChromeIconActionButton( - key: const Key('assistant-focus-theme-toggle'), - icon: chromeThemeToggleIcon(controller.themeMode), - tooltip: chromeThemeToggleTooltip(controller.themeMode), - onPressed: () { - controller.setThemeMode( - controller.themeMode == ThemeMode.dark - ? ThemeMode.light - : ThemeMode.dark, - ); - }, - ), - const SizedBox(height: 12), - _FocusListTile( - title: appText('当前主题', 'Current theme'), - subtitle: appText( - '点击上方按钮即可切换亮度模式。', - 'Use the button above to switch appearance mode.', - ), - trailing: themeLabel, - ), - ], - ); - } -} - -class _FocusListTile extends StatelessWidget { - const _FocusListTile({ - required this.title, - required this.subtitle, - required this.trailing, - }); - - final String title; - final String subtitle; - final String trailing; - - @override - Widget build(BuildContext context) { - final palette = context.palette; - final theme = Theme.of(context); - - return Container( - width: double.infinity, - padding: const EdgeInsets.fromLTRB(12, 12, 12, 12), - decoration: BoxDecoration( - color: palette.surfaceSecondary, - borderRadius: BorderRadius.circular(14), - border: Border.all(color: palette.strokeSoft), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: theme.textTheme.titleSmall?.copyWith( - fontWeight: FontWeight.w600, - ), - ), - const SizedBox(height: 4), - Text( - subtitle, - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: theme.textTheme.bodySmall?.copyWith( - color: palette.textSecondary, - height: 1.3, - ), - ), - const SizedBox(height: 8), - Text( - trailing, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: theme.textTheme.labelLarge?.copyWith( - color: palette.textPrimary, - ), - ), - ], - ), - ); - } -} - -class _FocusPill extends StatelessWidget { - const _FocusPill({required this.label}); - - final String label; - - @override - Widget build(BuildContext context) { - final palette = context.palette; - final theme = Theme.of(context); - - return Container( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), - decoration: BoxDecoration( - color: palette.surfaceSecondary, - borderRadius: BorderRadius.circular(999), - border: Border.all(color: palette.strokeSoft), - ), - child: Text( - label, - style: theme.textTheme.labelLarge?.copyWith( - color: palette.textSecondary, - ), - ), - ); - } -} - -class _PreviewEmptyState extends StatelessWidget { - const _PreviewEmptyState({required this.message}); - - final String message; - - @override - Widget build(BuildContext context) { - final palette = context.palette; - final theme = Theme.of(context); - - return Container( - width: double.infinity, - padding: const EdgeInsets.all(14), - decoration: BoxDecoration( - color: palette.surfaceSecondary, - borderRadius: BorderRadius.circular(14), - border: Border.all(color: palette.strokeSoft), - ), - child: Text( - message, - style: theme.textTheme.bodySmall?.copyWith( - color: palette.textSecondary, - height: 1.35, - ), - ), - ); - } -} - -class _AssistantFocusEmptyState extends StatelessWidget { - const _AssistantFocusEmptyState({ - required this.message, - required this.available, - required this.onAdd, - }); - - final String message; - final List available; - final Future Function(AssistantFocusEntry destination) onAdd; - - @override - Widget build(BuildContext context) { - final palette = context.palette; - final theme = Theme.of(context); - - return ListView( - padding: const EdgeInsets.fromLTRB(12, 12, 12, 12), - children: [ - Container( - width: double.infinity, - padding: const EdgeInsets.all(14), - decoration: BoxDecoration( - color: palette.surfaceSecondary, - borderRadius: BorderRadius.circular(14), - border: Border.all(color: palette.strokeSoft), - ), - child: Text( - message, - style: theme.textTheme.bodySmall?.copyWith( - color: palette.textSecondary, - height: 1.35, - ), - ), - ), - if (available.isNotEmpty) ...[ - const SizedBox(height: 12), - Wrap( - spacing: 8, - runSpacing: 8, - children: available - .map( - (destination) => ActionChip( - key: ValueKey( - 'assistant-focus-add-${destination.name}', - ), - avatar: Icon(destination.icon, size: 16), - label: Text(destination.label), - onPressed: () async { - await onAdd(destination); - }, - ), - ) - .toList(growable: false), - ), - ], - ], - ); - } -} diff --git a/lib/widgets/assistant_focus_panel_previews.part.dart b/lib/widgets/assistant_focus_panel_previews.part.dart new file mode 100644 index 00000000..83765453 --- /dev/null +++ b/lib/widgets/assistant_focus_panel_previews.part.dart @@ -0,0 +1,624 @@ +part of 'assistant_focus_panel.dart'; + +class _TasksFocusPreview extends StatelessWidget { + const _TasksFocusPreview({required this.controller}); + + final AppController controller; + + @override + Widget build(BuildContext context) { + final items = [ + ...controller.tasksController.running.take(2), + ...controller.tasksController.queue.take(2), + ...controller.tasksController.history.take(1), + ].take(4).toList(growable: false); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Wrap( + spacing: 8, + runSpacing: 8, + children: [ + _FocusPill( + label: appText( + '运行中 ${controller.tasksController.running.length}', + 'Running ${controller.tasksController.running.length}', + ), + ), + _FocusPill( + label: appText( + '队列 ${controller.tasksController.queue.length}', + 'Queue ${controller.tasksController.queue.length}', + ), + ), + _FocusPill( + label: appText( + '计划 ${controller.tasksController.scheduled.length}', + 'Scheduled ${controller.tasksController.scheduled.length}', + ), + ), + ], + ), + const SizedBox(height: 12), + if (items.isEmpty) + _PreviewEmptyState( + message: + controller.connection.status == + RuntimeConnectionStatus.connected + ? appText('当前没有任务摘要。', 'No task summary yet.') + : appText( + '连接 Gateway 后这里会显示任务摘要。', + 'Connect a gateway to load task summaries.', + ), + ) + else + ...items.map( + (item) => Padding( + padding: const EdgeInsets.only(bottom: 8), + child: _FocusListTile( + title: item.title, + subtitle: item.summary, + trailing: item.status, + ), + ), + ), + ], + ); + } +} + +class _SkillsFocusPreview extends StatelessWidget { + const _SkillsFocusPreview({required this.controller}); + + final AppController controller; + + @override + Widget build(BuildContext context) { + final items = controller.isSingleAgentMode + ? controller + .assistantImportedSkillsForSession(controller.currentSessionKey) + .take(4) + .map( + (skill) => GatewaySkillSummary( + name: skill.label, + description: skill.description, + source: skill.sourcePath, + skillKey: skill.key, + primaryEnv: null, + eligible: true, + disabled: false, + missingBins: const [], + missingEnv: const [], + missingConfig: const [], + ), + ) + .toList(growable: false) + : controller.skills.take(4).toList(growable: false); + if (items.isEmpty) { + return _PreviewEmptyState( + message: controller.isSingleAgentMode + ? (controller.currentSingleAgentNeedsAiGatewayConfiguration + ? appText( + '当前没有可用的外部 Agent ACP 端点,请先配置 LLM API fallback。', + 'No external Agent ACP endpoint is available. Configure LLM API fallback first.', + ) + : appText( + '当前线程还没有已加载技能。切换 provider 后会读取该线程自己的 skills 列表。', + 'No skills are loaded for this thread yet. Switching the provider reloads the thread-owned skills list.', + )) + : controller.connection.status == RuntimeConnectionStatus.connected + ? appText( + '当前代理没有已加载技能。', + 'No skills are loaded for the active agent.', + ) + : appText( + '连接 Gateway 后可查看技能摘要。', + 'Connect a gateway to inspect skills here.', + ), + ); + } + return Column( + children: items + .map( + (skill) => Padding( + padding: const EdgeInsets.only(bottom: 8), + child: _FocusListTile( + title: skill.name, + subtitle: skill.description, + trailing: skill.disabled + ? appText('已禁用', 'Disabled') + : appText('已启用', 'Enabled'), + ), + ), + ) + .toList(growable: false), + ); + } +} + +class _NodesFocusPreview extends StatelessWidget { + const _NodesFocusPreview({required this.controller}); + + final AppController controller; + + @override + Widget build(BuildContext context) { + final items = controller.instances.take(4).toList(growable: false); + if (items.isEmpty) { + return _PreviewEmptyState( + message: appText('当前没有节点可显示。', 'No nodes are available right now.'), + ); + } + return Column( + children: items + .map( + (instance) => Padding( + padding: const EdgeInsets.only(bottom: 8), + child: _FocusListTile( + title: instance.host?.trim().isNotEmpty == true + ? instance.host! + : instance.id, + subtitle: + [instance.platform, instance.deviceFamily, instance.ip] + .whereType() + .where((item) => item.trim().isNotEmpty) + .join(' · '), + trailing: instance.mode ?? appText('未知', 'Unknown'), + ), + ), + ) + .toList(growable: false), + ); + } +} + +class _AgentsFocusPreview extends StatelessWidget { + const _AgentsFocusPreview({required this.controller}); + + final AppController controller; + + @override + Widget build(BuildContext context) { + final items = controller.agents.take(5).toList(growable: false); + if (items.isEmpty) { + return _PreviewEmptyState( + message: appText('当前没有代理摘要。', 'No agents are available right now.'), + ); + } + return Column( + children: items + .map( + (agent) => Padding( + padding: const EdgeInsets.only(bottom: 8), + child: _FocusListTile( + title: '${agent.emoji} ${agent.name}', + subtitle: agent.id, + trailing: agent.name == controller.activeAgentName + ? appText('当前', 'Active') + : agent.theme, + ), + ), + ) + .toList(growable: false), + ); + } +} + +class _McpFocusPreview extends StatelessWidget { + const _McpFocusPreview({required this.controller}); + + final AppController controller; + + @override + Widget build(BuildContext context) { + final items = controller.connectors.take(4).toList(growable: false); + if (items.isEmpty) { + return _PreviewEmptyState( + message: appText( + '当前没有 MCP 连接器。连接 Gateway 后这里会显示工具摘要。', + 'No MCP connectors yet. Connect a gateway to load tool summaries here.', + ), + ); + } + return Column( + children: items + .map( + (connector) => Padding( + padding: const EdgeInsets.only(bottom: 8), + child: _FocusListTile( + title: connector.label, + subtitle: connector.detailLabel, + trailing: connector.status, + ), + ), + ) + .toList(growable: false), + ); + } +} + +class _ClawHubFocusPreview extends StatelessWidget { + const _ClawHubFocusPreview({required this.controller}); + + final AppController controller; + + @override + Widget build(BuildContext context) { + final skillCount = controller.isSingleAgentMode + ? controller.currentAssistantSkillCount + : controller.skills.length; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Wrap( + spacing: 8, + runSpacing: 8, + children: [ + _FocusPill( + label: appText('已加载技能 $skillCount', 'Loaded skills $skillCount'), + ), + _FocusPill( + label: appText( + '关注入口 ${controller.assistantNavigationDestinations.length}', + 'Pinned ${controller.assistantNavigationDestinations.length}', + ), + ), + ], + ), + const SizedBox(height: 12), + _PreviewEmptyState( + message: appText( + 'ClawHub 适合放在侧板做快速搜索或安装入口;需要完整终端交互时,再打开全页。', + 'Use ClawHub in the side panel for quick access. Open the full page when you need the terminal workflow.', + ), + ), + ], + ); + } +} + +class _SecretsFocusPreview extends StatelessWidget { + const _SecretsFocusPreview({required this.controller}); + + final AppController controller; + + @override + Widget build(BuildContext context) { + final items = controller.secretReferences.take(4).toList(growable: false); + if (items.isEmpty) { + return _PreviewEmptyState( + message: appText( + '当前没有密钥引用摘要。', + 'No masked secret references are available yet.', + ), + ); + } + return Column( + children: items + .map( + (secret) => Padding( + padding: const EdgeInsets.only(bottom: 8), + child: _FocusListTile( + title: secret.name, + subtitle: '${secret.provider} · ${secret.module}', + trailing: secret.status, + ), + ), + ) + .toList(growable: false), + ); + } +} + +class _AiGatewayFocusPreview extends StatelessWidget { + const _AiGatewayFocusPreview({required this.controller}); + + final AppController controller; + + @override + Widget build(BuildContext context) { + final items = controller.models.take(4).toList(growable: false); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Wrap( + spacing: 8, + runSpacing: 8, + children: [ + _FocusPill(label: controller.connection.status.label), + _FocusPill( + label: appText( + '模型 ${controller.models.length}', + 'Models ${controller.models.length}', + ), + ), + ], + ), + const SizedBox(height: 12), + if (items.isEmpty) + _PreviewEmptyState( + message: appText( + '当前没有 LLM API 模型摘要。', + 'No LLM API model summary is available yet.', + ), + ) + else + ...items.map( + (model) => Padding( + padding: const EdgeInsets.only(bottom: 8), + child: _FocusListTile( + title: model.name, + subtitle: model.provider, + trailing: model.id, + ), + ), + ), + ], + ); + } +} + +class _SettingsFocusPreview extends StatelessWidget { + const _SettingsFocusPreview({required this.controller}); + + final AppController controller; + + @override + Widget build(BuildContext context) { + final languageLabel = controller.appLanguage == AppLanguage.zh + ? appText('中文', 'Chinese') + : 'English'; + final themeLabel = switch (controller.themeMode) { + ThemeMode.dark => appText('深色', 'Dark'), + ThemeMode.light => appText('浅色', 'Light'), + ThemeMode.system => appText('跟随系统', 'System'), + }; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SettingsFocusQuickActions( + appLanguage: controller.appLanguage, + themeMode: controller.themeMode, + onToggleLanguage: controller.toggleAppLanguage, + onToggleTheme: () { + controller.setThemeMode( + controller.themeMode == ThemeMode.dark + ? ThemeMode.light + : ThemeMode.dark, + ); + }, + languageButtonKey: const Key( + 'assistant-focus-settings-language-toggle', + ), + themeButtonKey: const Key('assistant-focus-settings-theme-toggle'), + ), + const SizedBox(height: 12), + _FocusListTile( + title: appText('语言', 'Language'), + subtitle: appText('当前界面语言', 'Current interface language'), + trailing: languageLabel, + ), + const SizedBox(height: 8), + _FocusListTile( + title: appText('主题', 'Theme'), + subtitle: appText('当前显示模式', 'Current display mode'), + trailing: themeLabel, + ), + const SizedBox(height: 8), + _FocusListTile( + title: appText('执行目标', 'Execution target'), + subtitle: appText( + 'Assistant 默认运行位置', + 'Default assistant execution target', + ), + trailing: controller.assistantExecutionTarget.label, + ), + const SizedBox(height: 8), + _FocusListTile( + title: appText('权限', 'Permissions'), + subtitle: appText( + 'Assistant 默认权限级别', + 'Default assistant permission level', + ), + trailing: controller.assistantPermissionLevel.label, + ), + ], + ); + } +} + +class _LanguageFocusPreview extends StatelessWidget { + const _LanguageFocusPreview({required this.controller}); + + final AppController controller; + + @override + Widget build(BuildContext context) { + final currentLabel = controller.appLanguage == AppLanguage.zh + ? appText('中文', 'Chinese') + : 'English'; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ChromeLanguageActionButton( + key: const Key('assistant-focus-language-toggle'), + appLanguage: controller.appLanguage, + compact: false, + tooltip: appText('切换语言', 'Toggle language'), + onPressed: controller.toggleAppLanguage, + ), + const SizedBox(height: 12), + _FocusListTile( + title: appText('当前语言', 'Current language'), + subtitle: appText( + '点击上方按钮即可在中英文界面之间切换。', + 'Use the button above to switch between Chinese and English.', + ), + trailing: currentLabel, + ), + ], + ); + } +} + +class _ThemeFocusPreview extends StatelessWidget { + const _ThemeFocusPreview({required this.controller}); + + final AppController controller; + + @override + Widget build(BuildContext context) { + final themeLabel = switch (controller.themeMode) { + ThemeMode.dark => appText('深色', 'Dark'), + ThemeMode.light => appText('浅色', 'Light'), + ThemeMode.system => appText('跟随系统', 'System'), + }; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ChromeIconActionButton( + key: const Key('assistant-focus-theme-toggle'), + icon: chromeThemeToggleIcon(controller.themeMode), + tooltip: chromeThemeToggleTooltip(controller.themeMode), + onPressed: () { + controller.setThemeMode( + controller.themeMode == ThemeMode.dark + ? ThemeMode.light + : ThemeMode.dark, + ); + }, + ), + const SizedBox(height: 12), + _FocusListTile( + title: appText('当前主题', 'Current theme'), + subtitle: appText( + '点击上方按钮即可切换亮度模式。', + 'Use the button above to switch appearance mode.', + ), + trailing: themeLabel, + ), + ], + ); + } +} + +class _FocusListTile extends StatelessWidget { + const _FocusListTile({ + required this.title, + required this.subtitle, + required this.trailing, + }); + + final String title; + final String subtitle; + final String trailing; + + @override + Widget build(BuildContext context) { + final palette = context.palette; + final theme = Theme.of(context); + + return Container( + width: double.infinity, + padding: const EdgeInsets.fromLTRB(12, 12, 12, 12), + decoration: BoxDecoration( + color: palette.surfaceSecondary, + borderRadius: BorderRadius.circular(14), + border: Border.all(color: palette.strokeSoft), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 4), + Text( + subtitle, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.bodySmall?.copyWith( + color: palette.textSecondary, + height: 1.3, + ), + ), + const SizedBox(height: 8), + Text( + trailing, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.labelLarge?.copyWith( + color: palette.textPrimary, + ), + ), + ], + ), + ); + } +} + +class _FocusPill extends StatelessWidget { + const _FocusPill({required this.label}); + + final String label; + + @override + Widget build(BuildContext context) { + final palette = context.palette; + final theme = Theme.of(context); + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), + decoration: BoxDecoration( + color: palette.surfaceSecondary, + borderRadius: BorderRadius.circular(999), + border: Border.all(color: palette.strokeSoft), + ), + child: Text( + label, + style: theme.textTheme.labelLarge?.copyWith( + color: palette.textSecondary, + ), + ), + ); + } +} + +class _PreviewEmptyState extends StatelessWidget { + const _PreviewEmptyState({required this.message}); + + final String message; + + @override + Widget build(BuildContext context) { + final palette = context.palette; + final theme = Theme.of(context); + + return Container( + width: double.infinity, + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: palette.surfaceSecondary, + borderRadius: BorderRadius.circular(14), + border: Border.all(color: palette.strokeSoft), + ), + child: Text( + message, + style: theme.textTheme.bodySmall?.copyWith( + color: palette.textSecondary, + height: 1.35, + ), + ), + ); + } +} + diff --git a/lib/widgets/assistant_focus_panel_support.part.dart b/lib/widgets/assistant_focus_panel_support.part.dart new file mode 100644 index 00000000..5d4d6b12 --- /dev/null +++ b/lib/widgets/assistant_focus_panel_support.part.dart @@ -0,0 +1,62 @@ +part of 'assistant_focus_panel.dart'; + +class _AssistantFocusEmptyState extends StatelessWidget { + const _AssistantFocusEmptyState({ + required this.message, + required this.available, + required this.onAdd, + }); + + final String message; + final List available; + final Future Function(AssistantFocusEntry destination) onAdd; + + @override + Widget build(BuildContext context) { + final palette = context.palette; + final theme = Theme.of(context); + + return ListView( + padding: const EdgeInsets.fromLTRB(12, 12, 12, 12), + children: [ + Container( + width: double.infinity, + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: palette.surfaceSecondary, + borderRadius: BorderRadius.circular(14), + border: Border.all(color: palette.strokeSoft), + ), + child: Text( + message, + style: theme.textTheme.bodySmall?.copyWith( + color: palette.textSecondary, + height: 1.35, + ), + ), + ), + if (available.isNotEmpty) ...[ + const SizedBox(height: 12), + Wrap( + spacing: 8, + runSpacing: 8, + children: available + .map( + (destination) => ActionChip( + key: ValueKey( + 'assistant-focus-add-${destination.name}', + ), + avatar: Icon(destination.icon, size: 16), + label: Text(destination.label), + onPressed: () async { + await onAdd(destination); + }, + ), + ) + .toList(growable: false), + ), + ], + ], + ); + } +} diff --git a/test/widgets/assistant_focus_panel_suite.dart b/test/widgets/assistant_focus_panel_suite.dart index d847a728..861c1076 100644 --- a/test/widgets/assistant_focus_panel_suite.dart +++ b/test/widgets/assistant_focus_panel_suite.dart @@ -1,6 +1,8 @@ @TestOn('vm') library; +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:xworkmate/i18n/app_language.dart'; @@ -11,6 +13,23 @@ import 'package:xworkmate/widgets/assistant_focus_panel.dart'; import '../test_support.dart'; void main() { + test('assistant focus panel core files stay within 800 lines', () { + const targets = [ + 'lib/widgets/assistant_focus_panel_core.part.dart', + 'lib/web/web_focus_panel_core.part.dart', + ]; + + for (final path in targets) { + final file = File(path); + expect(file.existsSync(), isTrue, reason: 'missing file: $path'); + expect( + file.readAsLinesSync().length, + lessThanOrEqualTo(800), + reason: '$path should be split into smaller parts', + ); + } + }); + testWidgets( 'Settings focused preview reuses language and theme quick actions', (WidgetTester tester) async {