refactor: split focus panel core files
This commit is contained in:
parent
5e996955ae
commit
7dd3a15445
@ -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';
|
||||
|
||||
@ -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),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
624
lib/web/web_focus_panel_previews.part.dart
Normal file
624
lib/web/web_focus_panel_previews.part.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
62
lib/web/web_focus_panel_support.part.dart
Normal file
62
lib/web/web_focus_panel_support.part.dart
Normal 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),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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';
|
||||
|
||||
@ -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),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
624
lib/widgets/assistant_focus_panel_previews.part.dart
Normal file
624
lib/widgets/assistant_focus_panel_previews.part.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
62
lib/widgets/assistant_focus_panel_support.part.dart
Normal file
62
lib/widgets/assistant_focus_panel_support.part.dart
Normal 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),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user