refactor: split focus panel core files

This commit is contained in:
Haitao Pan 2026-03-28 11:52:59 +08:00
parent 5e996955ae
commit 7dd3a15445
9 changed files with 1395 additions and 1364 deletions

View File

@ -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';

View File

@ -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 = <DerivedTaskItem>[
...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 <String>[],
missingEnv: const <String>[],
missingConfig: const <String>[],
),
)
.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<String>()
.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<AssistantFocusEntry> available;
final Future<void> 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<String>(
'assistant-focus-add-${destination.name}',
),
avatar: Icon(destination.icon, size: 16),
label: Text(destination.label),
onPressed: () async {
await onAdd(destination);
},
),
)
.toList(growable: false),
),
],
],
);
}
}

View File

@ -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 = <DerivedTaskItem>[
...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 <String>[],
missingEnv: const <String>[],
missingConfig: const <String>[],
),
)
.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<String>()
.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,
),
),
);
}
}

View File

@ -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<AssistantFocusEntry> available;
final Future<void> 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<String>(
'assistant-focus-add-${destination.name}',
),
avatar: Icon(destination.icon, size: 16),
label: Text(destination.label),
onPressed: () async {
await onAdd(destination);
},
),
)
.toList(growable: false),
),
],
],
);
}
}

View File

@ -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';

View File

@ -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 = <DerivedTaskItem>[
...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 <String>[],
missingEnv: const <String>[],
missingConfig: const <String>[],
),
)
.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<String>()
.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<AssistantFocusEntry> available;
final Future<void> 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<String>(
'assistant-focus-add-${destination.name}',
),
avatar: Icon(destination.icon, size: 16),
label: Text(destination.label),
onPressed: () async {
await onAdd(destination);
},
),
)
.toList(growable: false),
),
],
],
);
}
}

View File

@ -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 = <DerivedTaskItem>[
...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 <String>[],
missingEnv: const <String>[],
missingConfig: const <String>[],
),
)
.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<String>()
.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,
),
),
);
}
}

View File

@ -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<AssistantFocusEntry> available;
final Future<void> 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<String>(
'assistant-focus-add-${destination.name}',
),
avatar: Icon(destination.icon, size: 16),
label: Text(destination.label),
onPressed: () async {
await onAdd(destination);
},
),
)
.toList(growable: false),
),
],
],
);
}
}

View File

@ -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 = <String>[
'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 {