Merge branch 'release/v1.1.4' into main
This commit is contained in:
commit
db956dba2f
@ -232,9 +232,17 @@ extension AppControllerDesktopGateway on AppController {
|
||||
await refreshGatewayHealth();
|
||||
await refreshAgents();
|
||||
await refreshSessions();
|
||||
await modelsControllerInternal.refresh();
|
||||
await cronJobsControllerInternal.refresh();
|
||||
await devicesControllerInternal.refresh(quiet: true);
|
||||
// Refresh independent controllers concurrently.
|
||||
await Future.wait(<Future<void>>[
|
||||
skillsControllerInternal.refresh(
|
||||
agentId: agentsControllerInternal.selectedAgentId.isEmpty
|
||||
? null
|
||||
: agentsControllerInternal.selectedAgentId,
|
||||
),
|
||||
modelsControllerInternal.refresh(),
|
||||
cronJobsControllerInternal.refresh(),
|
||||
devicesControllerInternal.refresh(quiet: true),
|
||||
]);
|
||||
await settingsControllerInternal.refreshDerivedState();
|
||||
try {
|
||||
await refreshAcpCapabilitiesInternal(
|
||||
|
||||
@ -677,7 +677,6 @@ extension AppControllerDesktopRuntimeHelpers on AppController {
|
||||
agentsControllerInternal.addListener(relayChildChangeInternal);
|
||||
sessionsControllerInternal.addListener(relayChildChangeInternal);
|
||||
chatControllerInternal.addListener(relayChildChangeInternal);
|
||||
skillsControllerInternal.addListener(relayChildChangeInternal);
|
||||
modelsControllerInternal.addListener(relayChildChangeInternal);
|
||||
cronJobsControllerInternal.addListener(relayChildChangeInternal);
|
||||
devicesControllerInternal.addListener(relayChildChangeInternal);
|
||||
@ -693,7 +692,6 @@ extension AppControllerDesktopRuntimeHelpers on AppController {
|
||||
agentsControllerInternal.removeListener(relayChildChangeInternal);
|
||||
sessionsControllerInternal.removeListener(relayChildChangeInternal);
|
||||
chatControllerInternal.removeListener(relayChildChangeInternal);
|
||||
skillsControllerInternal.removeListener(relayChildChangeInternal);
|
||||
modelsControllerInternal.removeListener(relayChildChangeInternal);
|
||||
cronJobsControllerInternal.removeListener(relayChildChangeInternal);
|
||||
devicesControllerInternal.removeListener(relayChildChangeInternal);
|
||||
|
||||
@ -179,6 +179,11 @@ extension AppControllerDesktopThreadActions on AppController {
|
||||
if (isAppOwnedAssistantSessionKeyInternal(sessionKey)) {
|
||||
await chatControllerInternal.loadSession(sessionKey);
|
||||
}
|
||||
await skillsControllerInternal.refresh(
|
||||
agentId: agentsControllerInternal.selectedAgentId.isEmpty
|
||||
? null
|
||||
: agentsControllerInternal.selectedAgentId,
|
||||
);
|
||||
recomputeTasksInternal();
|
||||
}
|
||||
|
||||
|
||||
@ -6,7 +6,6 @@ export 'assistant_page_composer_support.dart';
|
||||
export 'assistant_page_tooltip_labels.dart';
|
||||
export 'assistant_page_message_widgets.dart';
|
||||
export 'assistant_page_task_models.dart';
|
||||
export 'assistant_page_composer_skill_models.dart';
|
||||
export 'assistant_page_composer_skill_picker.dart';
|
||||
export 'assistant_page_composer_clipboard.dart';
|
||||
export 'assistant_page_components_core.dart';
|
||||
|
||||
@ -32,7 +32,6 @@ import 'assistant_page_composer_support.dart';
|
||||
import 'assistant_page_tooltip_labels.dart';
|
||||
import 'assistant_page_message_widgets.dart';
|
||||
import 'assistant_page_task_models.dart';
|
||||
import 'assistant_page_composer_skill_models.dart';
|
||||
import 'assistant_page_composer_skill_picker.dart';
|
||||
import 'assistant_page_composer_clipboard.dart';
|
||||
import 'assistant_page_components_core.dart';
|
||||
|
||||
@ -33,7 +33,6 @@ import 'assistant_page_composer_support.dart';
|
||||
import 'assistant_page_tooltip_labels.dart';
|
||||
import 'assistant_page_message_widgets.dart';
|
||||
import 'assistant_page_task_models.dart';
|
||||
import 'assistant_page_composer_skill_models.dart';
|
||||
import 'assistant_page_composer_skill_picker.dart';
|
||||
import 'assistant_page_composer_clipboard.dart';
|
||||
|
||||
|
||||
@ -31,7 +31,6 @@ import 'assistant_page_composer_support.dart';
|
||||
import 'assistant_page_tooltip_labels.dart';
|
||||
import 'assistant_page_message_widgets.dart';
|
||||
import 'assistant_page_task_models.dart';
|
||||
import 'assistant_page_composer_skill_models.dart';
|
||||
import 'assistant_page_composer_skill_picker.dart';
|
||||
import 'assistant_page_composer_clipboard.dart';
|
||||
import 'assistant_page_components_core.dart';
|
||||
|
||||
@ -33,7 +33,6 @@ import 'assistant_page_composer_support.dart';
|
||||
import 'assistant_page_tooltip_labels.dart';
|
||||
import 'assistant_page_message_widgets.dart';
|
||||
import 'assistant_page_task_models.dart';
|
||||
import 'assistant_page_composer_skill_models.dart';
|
||||
import 'assistant_page_composer_skill_picker.dart';
|
||||
import 'assistant_page_components_core.dart';
|
||||
|
||||
|
||||
@ -1,124 +0,0 @@
|
||||
// ignore_for_file: unused_import, unnecessary_import
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:math' as math;
|
||||
import 'package:file_selector/file_selector.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
import 'package:markdown/markdown.dart' as md;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:super_clipboard/super_clipboard.dart';
|
||||
import '../../app/app_controller.dart';
|
||||
import '../../app/app_metadata.dart';
|
||||
import '../../app/ui_feature_manifest.dart';
|
||||
import '../../i18n/app_language.dart';
|
||||
import '../../models/app_models.dart';
|
||||
import '../../runtime/multi_agent_orchestrator.dart';
|
||||
import '../../runtime/runtime_models.dart';
|
||||
import '../../theme/app_palette.dart';
|
||||
import '../../theme/app_theme.dart';
|
||||
import '../../widgets/assistant_focus_panel.dart';
|
||||
import '../../widgets/assistant_artifact_sidebar.dart';
|
||||
import '../../widgets/desktop_workspace_scaffold.dart';
|
||||
import '../../widgets/pane_resize_handle.dart';
|
||||
import '../../widgets/surface_card.dart';
|
||||
import 'assistant_page_main.dart';
|
||||
import 'assistant_page_components.dart';
|
||||
import 'assistant_page_composer_bar.dart';
|
||||
import 'assistant_page_composer_state_helpers.dart';
|
||||
import 'assistant_page_composer_support.dart';
|
||||
import 'assistant_page_tooltip_labels.dart';
|
||||
import 'assistant_page_message_widgets.dart';
|
||||
import 'assistant_page_task_models.dart';
|
||||
import 'assistant_page_composer_skill_picker.dart';
|
||||
import 'assistant_page_composer_clipboard.dart';
|
||||
import 'assistant_page_components_core.dart';
|
||||
|
||||
ComposerSkillOptionInternal skillOptionFromGatewayInternal(
|
||||
GatewaySkillSummary skill,
|
||||
) {
|
||||
final key = skill.skillKey.trim().isEmpty
|
||||
? skill.name.trim().toLowerCase()
|
||||
: skill.skillKey.trim();
|
||||
final label = skill.name.trim().isEmpty ? key : skill.name.trim();
|
||||
final sourceLabel = skill.source.trim().isEmpty ? 'Gateway' : skill.source;
|
||||
final group = skillGroupForSourceInternal(skill.source);
|
||||
final description = skill.description.trim().isEmpty
|
||||
? appText('可在当前任务中调用的技能。', 'Skill available in the current task.')
|
||||
: skill.description.trim();
|
||||
|
||||
return ComposerSkillOptionInternal(
|
||||
key: key,
|
||||
label: label,
|
||||
description: description,
|
||||
sourceLabel: sourceLabel,
|
||||
groupLabel: group.label,
|
||||
groupSortOrder: group.sortOrder,
|
||||
icon: Icons.key_rounded,
|
||||
);
|
||||
}
|
||||
|
||||
ComposerSkillGroupInternal skillGroupForSourceInternal(String source) {
|
||||
final normalized = source.trim().toLowerCase();
|
||||
if (normalized == 'openclaw-workspace') {
|
||||
return const ComposerSkillGroupInternal(
|
||||
label: 'Workspace Skills',
|
||||
sortOrder: 0,
|
||||
);
|
||||
}
|
||||
if (normalized.startsWith('agents-skills-') ||
|
||||
normalized == 'agent' ||
|
||||
normalized.startsWith('agent-') ||
|
||||
normalized.contains('personal')) {
|
||||
return const ComposerSkillGroupInternal(
|
||||
label: 'Agent Skills',
|
||||
sortOrder: 1,
|
||||
);
|
||||
}
|
||||
if (normalized == 'bridge' || normalized == 'gateway') {
|
||||
return const ComposerSkillGroupInternal(
|
||||
label: 'Gateway Skills',
|
||||
sortOrder: 2,
|
||||
);
|
||||
}
|
||||
if (normalized.isEmpty) {
|
||||
return const ComposerSkillGroupInternal(
|
||||
label: 'Gateway Skills',
|
||||
sortOrder: 2,
|
||||
);
|
||||
}
|
||||
return const ComposerSkillGroupInternal(label: 'Other Skills', sortOrder: 3);
|
||||
}
|
||||
|
||||
class ComposerSkillGroupInternal {
|
||||
const ComposerSkillGroupInternal({
|
||||
required this.label,
|
||||
required this.sortOrder,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final int sortOrder;
|
||||
}
|
||||
|
||||
class ComposerSkillOptionInternal {
|
||||
const ComposerSkillOptionInternal({
|
||||
required this.key,
|
||||
required this.label,
|
||||
required this.description,
|
||||
required this.sourceLabel,
|
||||
required this.groupLabel,
|
||||
required this.groupSortOrder,
|
||||
required this.icon,
|
||||
});
|
||||
|
||||
final String key;
|
||||
final String label;
|
||||
final String description;
|
||||
final String sourceLabel;
|
||||
final String groupLabel;
|
||||
final int groupSortOrder;
|
||||
final IconData icon;
|
||||
}
|
||||
@ -33,7 +33,6 @@ import 'assistant_page_composer_support.dart';
|
||||
import 'assistant_page_tooltip_labels.dart';
|
||||
import 'assistant_page_message_widgets.dart';
|
||||
import 'assistant_page_task_models.dart';
|
||||
import 'assistant_page_composer_skill_models.dart';
|
||||
import 'assistant_page_composer_clipboard.dart';
|
||||
import 'assistant_page_components_core.dart';
|
||||
|
||||
@ -79,6 +78,7 @@ class SkillPickerPopoverInternal extends StatelessWidget {
|
||||
required this.hasQuery,
|
||||
required this.onQueryChanged,
|
||||
required this.onToggleSkill,
|
||||
this.onRetry,
|
||||
});
|
||||
|
||||
final double maxHeight;
|
||||
@ -91,6 +91,7 @@ class SkillPickerPopoverInternal extends StatelessWidget {
|
||||
final bool hasQuery;
|
||||
final ValueChanged<String> onQueryChanged;
|
||||
final ValueChanged<String> onToggleSkill;
|
||||
final VoidCallback? onRetry;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -191,6 +192,31 @@ class SkillPickerPopoverInternal extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
],
|
||||
if (!hasError && !isLoading && !hasQuery) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
appText(
|
||||
'技能来源于 Gateway 工作区。请确认 OpenClaw'
|
||||
' Gateway 已连接且安装了技能包。',
|
||||
'Skills come from the Gateway workspace.'
|
||||
' Make sure OpenClaw Gateway is connected'
|
||||
' and skills are installed.',
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: palette.textMuted,
|
||||
),
|
||||
),
|
||||
],
|
||||
if ((hasError || (!isLoading && !hasQuery)) &&
|
||||
onRetry != null) ...[
|
||||
const SizedBox(height: 12),
|
||||
TextButton.icon(
|
||||
onPressed: onRetry,
|
||||
icon: const Icon(Icons.refresh_rounded, size: 16),
|
||||
label: Text(appText('重试', 'Retry')),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -349,3 +375,89 @@ class SkillPickerTileInternal extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ComposerSkillOptionInternal skillOptionFromGatewayInternal(
|
||||
GatewaySkillSummary skill,
|
||||
) {
|
||||
final key = skill.skillKey.trim().isEmpty
|
||||
? skill.name.trim().toLowerCase()
|
||||
: skill.skillKey.trim();
|
||||
final label = skill.name.trim().isEmpty ? key : skill.name.trim();
|
||||
final sourceLabel = skill.source.trim().isEmpty ? 'Gateway' : skill.source;
|
||||
final group = skillGroupForSourceInternal(skill.source);
|
||||
final description = skill.description.trim().isEmpty
|
||||
? appText('可在当前任务中调用的技能。', 'Skill available in the current task.')
|
||||
: skill.description.trim();
|
||||
|
||||
return ComposerSkillOptionInternal(
|
||||
key: key,
|
||||
label: label,
|
||||
description: description,
|
||||
sourceLabel: sourceLabel,
|
||||
groupLabel: group.label,
|
||||
groupSortOrder: group.sortOrder,
|
||||
icon: Icons.key_rounded,
|
||||
);
|
||||
}
|
||||
|
||||
ComposerSkillGroupInternal skillGroupForSourceInternal(String source) {
|
||||
final normalized = source.trim().toLowerCase();
|
||||
if (normalized == 'openclaw-workspace') {
|
||||
return const ComposerSkillGroupInternal(
|
||||
label: 'Workspace Skills',
|
||||
sortOrder: 0,
|
||||
);
|
||||
}
|
||||
if (normalized.startsWith('agents-skills-') ||
|
||||
normalized == 'agent' ||
|
||||
normalized.startsWith('agent-') ||
|
||||
normalized.contains('personal')) {
|
||||
return const ComposerSkillGroupInternal(
|
||||
label: 'Agent Skills',
|
||||
sortOrder: 1,
|
||||
);
|
||||
}
|
||||
if (normalized == 'bridge' || normalized == 'gateway') {
|
||||
return const ComposerSkillGroupInternal(
|
||||
label: 'Gateway Skills',
|
||||
sortOrder: 2,
|
||||
);
|
||||
}
|
||||
if (normalized.isEmpty) {
|
||||
return const ComposerSkillGroupInternal(
|
||||
label: 'Gateway Skills',
|
||||
sortOrder: 2,
|
||||
);
|
||||
}
|
||||
return const ComposerSkillGroupInternal(label: 'Other Skills', sortOrder: 3);
|
||||
}
|
||||
|
||||
class ComposerSkillGroupInternal {
|
||||
const ComposerSkillGroupInternal({
|
||||
required this.label,
|
||||
required this.sortOrder,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final int sortOrder;
|
||||
}
|
||||
|
||||
class ComposerSkillOptionInternal {
|
||||
const ComposerSkillOptionInternal({
|
||||
required this.key,
|
||||
required this.label,
|
||||
required this.description,
|
||||
required this.sourceLabel,
|
||||
required this.groupLabel,
|
||||
required this.groupSortOrder,
|
||||
required this.icon,
|
||||
});
|
||||
|
||||
final String key;
|
||||
final String label;
|
||||
final String description;
|
||||
final String sourceLabel;
|
||||
final String groupLabel;
|
||||
final int groupSortOrder;
|
||||
final IconData icon;
|
||||
}
|
||||
|
||||
@ -32,7 +32,6 @@ import 'assistant_page_composer_support.dart';
|
||||
import 'assistant_page_tooltip_labels.dart';
|
||||
import 'assistant_page_message_widgets.dart';
|
||||
import 'assistant_page_task_models.dart';
|
||||
import 'assistant_page_composer_skill_models.dart';
|
||||
import 'assistant_page_composer_skill_picker.dart';
|
||||
import 'assistant_page_composer_clipboard.dart';
|
||||
import 'assistant_page_components_core.dart';
|
||||
@ -99,6 +98,7 @@ Widget buildSkillPickerOverlayForInternal(
|
||||
hasQuery: state.skillPickerQueryInternal.trim().isNotEmpty,
|
||||
onQueryChanged: state.setSkillPickerQueryInternal,
|
||||
onToggleSkill: (skillKey) => state.widget.onToggleSkill(skillKey),
|
||||
onRetry: () => state.widget.controller.skillsController.refresh(),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@ -32,7 +32,6 @@ import 'assistant_page_composer_state_helpers.dart';
|
||||
import 'assistant_page_tooltip_labels.dart';
|
||||
import 'assistant_page_message_widgets.dart';
|
||||
import 'assistant_page_task_models.dart';
|
||||
import 'assistant_page_composer_skill_models.dart';
|
||||
import 'assistant_page_composer_skill_picker.dart';
|
||||
import 'assistant_page_composer_clipboard.dart';
|
||||
import 'assistant_page_components_core.dart';
|
||||
|
||||
@ -32,7 +32,6 @@ import 'assistant_page_composer_support.dart';
|
||||
import 'assistant_page_tooltip_labels.dart';
|
||||
import 'assistant_page_message_widgets.dart';
|
||||
import 'assistant_page_task_models.dart';
|
||||
import 'assistant_page_composer_skill_models.dart';
|
||||
import 'assistant_page_composer_skill_picker.dart';
|
||||
import 'assistant_page_composer_clipboard.dart';
|
||||
import 'assistant_page_components_core.dart';
|
||||
|
||||
@ -32,7 +32,6 @@ import 'assistant_page_composer_state_helpers.dart';
|
||||
import 'assistant_page_composer_support.dart';
|
||||
import 'assistant_page_tooltip_labels.dart';
|
||||
import 'assistant_page_task_models.dart';
|
||||
import 'assistant_page_composer_skill_models.dart';
|
||||
import 'assistant_page_composer_skill_picker.dart';
|
||||
import 'assistant_page_composer_clipboard.dart';
|
||||
import 'assistant_page_components_core.dart';
|
||||
|
||||
@ -32,7 +32,6 @@ import 'assistant_page_composer_support.dart';
|
||||
import 'assistant_page_tooltip_labels.dart';
|
||||
import 'assistant_page_message_widgets.dart';
|
||||
import 'assistant_page_task_models.dart';
|
||||
import 'assistant_page_composer_skill_models.dart';
|
||||
import 'assistant_page_composer_skill_picker.dart';
|
||||
import 'assistant_page_composer_clipboard.dart';
|
||||
import 'assistant_attachment_payloads.dart';
|
||||
|
||||
@ -35,7 +35,6 @@ import 'assistant_page_composer_support.dart';
|
||||
import 'assistant_page_tooltip_labels.dart';
|
||||
import 'assistant_page_message_widgets.dart';
|
||||
import 'assistant_page_task_models.dart';
|
||||
import 'assistant_page_composer_skill_models.dart';
|
||||
import 'assistant_page_composer_skill_picker.dart';
|
||||
import 'assistant_page_composer_clipboard.dart';
|
||||
import 'assistant_page_components_core.dart';
|
||||
|
||||
@ -32,7 +32,6 @@ import 'assistant_page_composer_state_helpers.dart';
|
||||
import 'assistant_page_composer_support.dart';
|
||||
import 'assistant_page_tooltip_labels.dart';
|
||||
import 'assistant_page_message_widgets.dart';
|
||||
import 'assistant_page_composer_skill_models.dart';
|
||||
import 'assistant_page_composer_skill_picker.dart';
|
||||
import 'assistant_page_composer_clipboard.dart';
|
||||
import 'assistant_page_components_core.dart';
|
||||
|
||||
@ -32,7 +32,6 @@ import 'assistant_page_composer_state_helpers.dart';
|
||||
import 'assistant_page_composer_support.dart';
|
||||
import 'assistant_page_message_widgets.dart';
|
||||
import 'assistant_page_task_models.dart';
|
||||
import 'assistant_page_composer_skill_models.dart';
|
||||
import 'assistant_page_composer_skill_picker.dart';
|
||||
import 'assistant_page_composer_clipboard.dart';
|
||||
import 'assistant_page_components_core.dart';
|
||||
|
||||
@ -162,37 +162,10 @@ Offset? desktopContentPosition(
|
||||
}) {
|
||||
if (viewportSize.width <= 0 || viewportSize.height <= 0) return null;
|
||||
|
||||
final resolvedContentSize = contentSize;
|
||||
if (resolvedContentSize == null ||
|
||||
resolvedContentSize.width <= 0 ||
|
||||
resolvedContentSize.height <= 0) {
|
||||
return Offset(
|
||||
(localPosition.dx / viewportSize.width).clamp(0.0, 1.0),
|
||||
(localPosition.dy / viewportSize.height).clamp(0.0, 1.0),
|
||||
);
|
||||
}
|
||||
|
||||
final viewportAspect = viewportSize.width / viewportSize.height;
|
||||
final contentAspect = resolvedContentSize.width / resolvedContentSize.height;
|
||||
|
||||
double drawnWidth;
|
||||
double drawnHeight;
|
||||
double offsetX;
|
||||
double offsetY;
|
||||
if (viewportAspect > contentAspect) {
|
||||
drawnHeight = viewportSize.height;
|
||||
drawnWidth = drawnHeight * contentAspect;
|
||||
offsetX = (viewportSize.width - drawnWidth) / 2;
|
||||
offsetY = 0;
|
||||
} else {
|
||||
drawnWidth = viewportSize.width;
|
||||
drawnHeight = drawnWidth / contentAspect;
|
||||
offsetX = 0;
|
||||
offsetY = (viewportSize.height - drawnHeight) / 2;
|
||||
}
|
||||
|
||||
// We are using FittedBox(fit: BoxFit.fill) which stretches the video
|
||||
// to fill the viewport exactly, without any padding/offsets.
|
||||
return Offset(
|
||||
((localPosition.dx - offsetX) / drawnWidth).clamp(0.0, 1.0),
|
||||
((localPosition.dy - offsetY) / drawnHeight).clamp(0.0, 1.0),
|
||||
(localPosition.dx / viewportSize.width).clamp(0.0, 1.0),
|
||||
(localPosition.dy / viewportSize.height).clamp(0.0, 1.0),
|
||||
);
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ import 'desktop_client.dart';
|
||||
import 'desktop_input_handler.dart';
|
||||
import '../../app/app_controller.dart';
|
||||
import '../../widgets/surface_card.dart';
|
||||
import '../../i18n/app_language.dart';
|
||||
|
||||
class DesktopView extends StatefulWidget {
|
||||
const DesktopView({
|
||||
@ -48,6 +49,7 @@ class _DesktopViewState extends State<DesktopView> {
|
||||
bool _useGpu = false;
|
||||
bool _adaptiveResolution = true;
|
||||
bool _showAdvancedOptions = false;
|
||||
bool _showControlPanel = true;
|
||||
String _connectionState = 'disconnected';
|
||||
bool _hasStream = false;
|
||||
bool _isFocused = false;
|
||||
@ -152,7 +154,9 @@ class _DesktopViewState extends State<DesktopView> {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Failed to connect remote desktop: $e'),
|
||||
content: Text(
|
||||
appText('连接AI工作空间失败: $e', 'Failed to connect AI Workspace: $e'),
|
||||
),
|
||||
backgroundColor: Colors.redAccent,
|
||||
),
|
||||
);
|
||||
@ -178,12 +182,13 @@ class _DesktopViewState extends State<DesktopView> {
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// Control panel card
|
||||
SurfaceCard(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (_showControlPanel)
|
||||
SurfaceCard(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Wrap(
|
||||
spacing: 16,
|
||||
runSpacing: 16,
|
||||
@ -214,10 +219,10 @@ class _DesktopViewState extends State<DesktopView> {
|
||||
),
|
||||
label: Text(
|
||||
_connectionState == 'connected'
|
||||
? '断开连接'
|
||||
? appText('断开连接', 'Disconnect')
|
||||
: (_connectionState == 'connecting'
|
||||
? '正在连接...'
|
||||
: '连接桌面'),
|
||||
? appText('正在连接...', 'Connecting...')
|
||||
: appText('连接AI工作空间', 'Connect AI Workspace')),
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
@ -261,10 +266,10 @@ class _DesktopViewState extends State<DesktopView> {
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
_connectionState == 'connected'
|
||||
? '已连接'
|
||||
? appText('已连接', 'Connected')
|
||||
: (_connectionState == 'connecting'
|
||||
? '连接中'
|
||||
: '未连接'),
|
||||
? appText('连接中', 'Connecting')
|
||||
: appText('已断开', 'Disconnected')),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
@ -292,19 +297,25 @@ class _DesktopViewState extends State<DesktopView> {
|
||||
),
|
||||
label: const Text('高级选项'),
|
||||
),
|
||||
// Maximize Toggle
|
||||
if (widget.onToggleMaximize != null)
|
||||
IconButton(
|
||||
onPressed: widget.onToggleMaximize,
|
||||
icon: Icon(
|
||||
widget.isMaximized
|
||||
? Icons.fullscreen_exit_rounded
|
||||
: Icons.fullscreen_rounded,
|
||||
// Maximize Toggle
|
||||
if (widget.onToggleMaximize != null)
|
||||
IconButton(
|
||||
onPressed: widget.onToggleMaximize,
|
||||
icon: Icon(
|
||||
widget.isMaximized
|
||||
? Icons.fullscreen_exit_rounded
|
||||
: Icons.fullscreen_rounded,
|
||||
),
|
||||
tooltip: widget.isMaximized ? '恢复默认大小' : '最大化',
|
||||
),
|
||||
tooltip: widget.isMaximized ? '恢复默认大小' : '最大化',
|
||||
// Collapse Toggle
|
||||
IconButton(
|
||||
onPressed: () => setState(() => _showControlPanel = false),
|
||||
icon: const Icon(Icons.expand_less),
|
||||
tooltip: '折叠面板',
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
if (_showAdvancedOptions) ...[
|
||||
const SizedBox(height: 16),
|
||||
Wrap(
|
||||
@ -398,7 +409,21 @@ class _DesktopViewState extends State<DesktopView> {
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
if (!_showControlPanel)
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: FilledButton.tonalIcon(
|
||||
onPressed: () => setState(() => _showControlPanel = true),
|
||||
icon: const Icon(Icons.expand_more, size: 18),
|
||||
label: const Text('展开控制面板'),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
if (_showControlPanel)
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Stream Viewport Card
|
||||
Expanded(
|
||||
@ -484,10 +509,18 @@ class _DesktopViewState extends State<DesktopView> {
|
||||
_inputHandler!.handleScroll(event);
|
||||
}
|
||||
},
|
||||
child: RTCVideoView(
|
||||
_localRenderer,
|
||||
objectFit: RTCVideoViewObjectFit
|
||||
.RTCVideoViewObjectFitContain,
|
||||
child: SizedBox.expand(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.fill,
|
||||
child: SizedBox(
|
||||
width: _remoteDesktopSize.width > 0 ? _remoteDesktopSize.width : 1280,
|
||||
height: _remoteDesktopSize.height > 0 ? _remoteDesktopSize.height : 720,
|
||||
child: RTCVideoView(
|
||||
_localRenderer,
|
||||
objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitContain,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -511,8 +544,8 @@ class _DesktopViewState extends State<DesktopView> {
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
_connectionState == 'connecting'
|
||||
? '正在建立 WebRTC 连接,请稍候...'
|
||||
: '未开启远程桌面流。点击“连接桌面”启动视频流。',
|
||||
? appText('正在建立 WebRTC 连接,请稍候...', 'Establishing WebRTC connection, please wait...')
|
||||
: appText('未开启 AI 工作空间流。点击“连接AI工作空间”启动视频流。', 'AI Workspace stream not enabled. Click "Connect AI Workspace" to start the video stream.'),
|
||||
style: TextStyle(
|
||||
color: theme.colorScheme.onSurface
|
||||
.withValues(alpha: 0.6),
|
||||
|
||||
@ -59,7 +59,7 @@ class _SettingsRemoteDesktopPanelState extends State<SettingsRemoteDesktopPanel>
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
appText('远程桌面', 'Remote Desktop'),
|
||||
appText('AI工作空间', 'AI Workspace'),
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
|
||||
@ -155,7 +155,7 @@ extension SettingsTabCopy on SettingsTab {
|
||||
String get label => switch (this) {
|
||||
SettingsTab.gateway => appText('集成', 'Integrations'),
|
||||
SettingsTab.archivedTasks => appText('归档任务', 'Archived tasks'),
|
||||
SettingsTab.remoteDesktop => appText('远程桌面', 'Remote Desktop'),
|
||||
SettingsTab.remoteDesktop => appText('AI工作空间', 'AI Workspace'),
|
||||
SettingsTab.logs => appText('运行日志', 'Runtime Logs'),
|
||||
};
|
||||
}
|
||||
|
||||
@ -203,18 +203,62 @@ extension GatewayRuntimeApiInternal on GatewayRuntime {
|
||||
if (agentId != null && agentId.trim().isNotEmpty)
|
||||
'agentId': agentId.trim(),
|
||||
};
|
||||
if (sessionClientInternal == null) {
|
||||
throw GatewayRuntimeException(
|
||||
'skills.status requires bridge session (ACP transport)',
|
||||
code: 'BRIDGE_NOT_CONFIGURED',
|
||||
);
|
||||
}
|
||||
// Use allowErrorPayload so the bridge can return cached skills even when
|
||||
// the upstream OpenClaw gateway is temporarily offline (ok:false + payload).
|
||||
final payload = asMap(
|
||||
sessionClientInternal == null
|
||||
? await request('skills.status', params: params)
|
||||
: await sessionClientInternal!.request(
|
||||
runtimeId: runtimeIdInternal,
|
||||
method: 'skills.status',
|
||||
params: params,
|
||||
allowErrorPayload: true,
|
||||
),
|
||||
await sessionClientInternal!.request(
|
||||
runtimeId: runtimeIdInternal,
|
||||
method: 'skills.status',
|
||||
params: params,
|
||||
allowErrorPayload: true,
|
||||
),
|
||||
);
|
||||
final statusPayload = skillsStatusPayloadInternal(payload);
|
||||
return asList(statusPayload['skills'])
|
||||
final skillsList = asList(payload['skills']);
|
||||
// When the skills key is entirely absent (not just an empty list), the
|
||||
// gateway may have returned a stub or error payload. Distinguish between
|
||||
// "genuinely no skills" and "gateway responded without skills data".
|
||||
if (!payload.containsKey('skills')) {
|
||||
final hasWorkspaceMeta = payload.containsKey('workspaceDir') ||
|
||||
payload.containsKey('managedSkillsDir');
|
||||
if (!hasWorkspaceMeta) {
|
||||
appendLogInternal(
|
||||
this,
|
||||
'warn',
|
||||
'skills',
|
||||
'skills.status returned payload without skills key and without'
|
||||
' workspace metadata — likely a gateway error or unimplemented method',
|
||||
);
|
||||
throw GatewayRuntimeException(
|
||||
'OpenClaw gateway did not return skills data.'
|
||||
' The gateway may not have skills.status implemented.',
|
||||
code: 'SKILLS_STATUS_MISSING',
|
||||
);
|
||||
}
|
||||
// Gateway responded with workspace metadata but no skills key —
|
||||
// genuinely no skills installed. Return empty list.
|
||||
appendLogInternal(
|
||||
this,
|
||||
'debug',
|
||||
'skills',
|
||||
'skills.status returned workspace metadata with zero skills',
|
||||
);
|
||||
return const <GatewaySkillSummary>[];
|
||||
}
|
||||
if (skillsList.isEmpty) {
|
||||
appendLogInternal(
|
||||
this,
|
||||
'debug',
|
||||
'skills',
|
||||
'skills.status returned empty skills list (${payload.length} payload keys)',
|
||||
);
|
||||
}
|
||||
return skillsList
|
||||
.map((item) {
|
||||
final map = asMap(item);
|
||||
return GatewaySkillSummary(
|
||||
@ -561,24 +605,6 @@ extension GatewayRuntimeApiInternal on GatewayRuntime {
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic> skillsStatusPayloadInternal(Map<String, dynamic> payload) {
|
||||
if (asList(payload['skills']).isNotEmpty) {
|
||||
return payload;
|
||||
}
|
||||
for (final key in const <String>[
|
||||
'status',
|
||||
'skillStatus',
|
||||
'data',
|
||||
'payload',
|
||||
]) {
|
||||
final nested = asMap(payload[key]);
|
||||
if (asList(nested['skills']).isNotEmpty) {
|
||||
return nested;
|
||||
}
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
List<String> skillMissingListInternal(
|
||||
Map<String, dynamic> skill,
|
||||
String nestedKey,
|
||||
|
||||
@ -12,39 +12,86 @@ import 'runtime_controllers_gateway.dart';
|
||||
import 'runtime_controllers_derived_tasks.dart';
|
||||
|
||||
class SkillsController extends ChangeNotifier {
|
||||
SkillsController(this.runtimeInternal);
|
||||
SkillsController(this.runtimeInternal) {
|
||||
_runtimeListener = () {
|
||||
if (!runtimeInternal.isConnected) {
|
||||
// Reset auto-refresh flag on disconnect so a subsequent reconnect
|
||||
// will trigger a fresh load.
|
||||
_hasAutoRefreshed = false;
|
||||
return;
|
||||
}
|
||||
// Auto-refresh on first gateway connect only when skills are empty,
|
||||
// not already loading, and haven't auto-refreshed this session.
|
||||
if (loadingInternal || itemsInternal.isNotEmpty || _hasAutoRefreshed) {
|
||||
return;
|
||||
}
|
||||
_hasAutoRefreshed = true;
|
||||
refresh();
|
||||
};
|
||||
runtimeInternal.addListener(_runtimeListener!);
|
||||
}
|
||||
|
||||
final GatewayRuntime runtimeInternal;
|
||||
|
||||
List<GatewaySkillSummary> itemsInternal = const <GatewaySkillSummary>[];
|
||||
bool loadingInternal = false;
|
||||
String? errorInternal;
|
||||
int _retryCount = 0;
|
||||
static const int _maxRetries = 2;
|
||||
VoidCallback? _runtimeListener;
|
||||
bool _hasAutoRefreshed = false;
|
||||
|
||||
List<GatewaySkillSummary> get items => itemsInternal;
|
||||
bool get loading => loadingInternal;
|
||||
String? get error => errorInternal;
|
||||
|
||||
/// Whether the user can manually retry (non-empty error + not loading).
|
||||
bool get canRetry =>
|
||||
(errorInternal?.isNotEmpty ?? false) && !loadingInternal;
|
||||
|
||||
Future<void> refresh({String? agentId}) async {
|
||||
if (!runtimeInternal.isConnected) {
|
||||
if (!runtimeInternal.canConnectBridgeSession) {
|
||||
itemsInternal = const <GatewaySkillSummary>[];
|
||||
errorInternal = null;
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
errorInternal = 'Gateway 未连接,无法加载技能列表。';
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
loadingInternal = true;
|
||||
errorInternal = null;
|
||||
_retryCount = 0;
|
||||
notifyListeners();
|
||||
await _doRefresh(agentId: agentId);
|
||||
}
|
||||
|
||||
Future<void> _doRefresh({String? agentId}) async {
|
||||
try {
|
||||
itemsInternal = await runtimeInternal.listSkills(agentId: agentId);
|
||||
errorInternal = null;
|
||||
_retryCount = 0;
|
||||
} catch (error) {
|
||||
if (_retryCount < _maxRetries && runtimeInternal.isConnected) {
|
||||
_retryCount++;
|
||||
final delay = Duration(seconds: _retryCount * 2);
|
||||
await Future<void>.delayed(delay);
|
||||
if (runtimeInternal.isConnected) {
|
||||
await _doRefresh(agentId: agentId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
errorInternal = error.toString();
|
||||
} finally {
|
||||
loadingInternal = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (_runtimeListener != null) {
|
||||
runtimeInternal.removeListener(_runtimeListener!);
|
||||
_runtimeListener = null;
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class ModelsController extends ChangeNotifier {
|
||||
|
||||
@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import 'top_bar.dart';
|
||||
|
||||
class SettingsPageBodyShell extends StatelessWidget {
|
||||
class SettingsPageBodyShell extends StatefulWidget {
|
||||
const SettingsPageBodyShell({
|
||||
super.key,
|
||||
required this.padding,
|
||||
@ -22,25 +22,59 @@ class SettingsPageBodyShell extends StatelessWidget {
|
||||
final Widget? globalApplyBar;
|
||||
final List<Widget> bodyChildren;
|
||||
|
||||
@override
|
||||
State<SettingsPageBodyShell> createState() => _SettingsPageBodyShellState();
|
||||
}
|
||||
|
||||
class _SettingsPageBodyShellState extends State<SettingsPageBodyShell> {
|
||||
bool _isHeaderCollapsed = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SingleChildScrollView(
|
||||
padding: padding,
|
||||
padding: widget.padding,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TopBar(
|
||||
breadcrumbs: breadcrumbs,
|
||||
title: title,
|
||||
subtitle: subtitle,
|
||||
trailing: trailing,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
if (globalApplyBar != null) ...[
|
||||
globalApplyBar!,
|
||||
if (!_isHeaderCollapsed) ...[
|
||||
TopBar(
|
||||
breadcrumbs: widget.breadcrumbs,
|
||||
title: widget.title,
|
||||
subtitle: widget.subtitle,
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
widget.trailing,
|
||||
const SizedBox(width: 8),
|
||||
IconButton.filledTonal(
|
||||
onPressed: () => setState(() => _isHeaderCollapsed = true),
|
||||
icon: const Icon(Icons.expand_less),
|
||||
tooltip: '折叠顶部面板',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
if (widget.globalApplyBar != null) ...[
|
||||
widget.globalApplyBar!,
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
] else ...[
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: AppBreadcrumbs(items: widget.breadcrumbs),
|
||||
),
|
||||
IconButton.filledTonal(
|
||||
onPressed: () => setState(() => _isHeaderCollapsed = false),
|
||||
icon: const Icon(Icons.expand_more),
|
||||
tooltip: '展开顶部面板',
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
...bodyChildren,
|
||||
...widget.bodyChildren,
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@ -5,7 +5,6 @@ import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:xworkmate/app/app_controller.dart';
|
||||
import 'package:xworkmate/app/ui_feature_manifest.dart';
|
||||
import 'package:xworkmate/features/assistant/assistant_page_composer_clipboard.dart';
|
||||
import 'package:xworkmate/features/assistant/assistant_page_composer_skill_models.dart';
|
||||
import 'package:xworkmate/features/assistant/assistant_page_composer_skill_picker.dart';
|
||||
import 'package:xworkmate/features/assistant/assistant_page_main.dart';
|
||||
import 'package:xworkmate/runtime/runtime_models.dart';
|
||||
|
||||
@ -29,8 +29,8 @@ void main() {
|
||||
);
|
||||
|
||||
// Verify the panel headers and titles
|
||||
expect(find.text('远程桌面'), findsOneWidget);
|
||||
expect(find.text('连接桌面'), findsOneWidget);
|
||||
expect(find.text('AI工作空间'), findsOneWidget);
|
||||
expect(find.text('连接AI工作空间'), findsOneWidget);
|
||||
|
||||
// Verify advanced options are hidden initially
|
||||
expect(find.text('GPU 加速'), findsNothing);
|
||||
|
||||
@ -8,7 +8,7 @@ import 'package:xworkmate/app/app_controller.dart';
|
||||
import 'package:xworkmate/app/app_controller_desktop_external_acp_routing.dart';
|
||||
import 'package:xworkmate/app/app_controller_openclaw_task_queue.dart';
|
||||
import 'package:xworkmate/app/ui_feature_manifest.dart';
|
||||
import 'package:xworkmate/features/assistant/assistant_page_composer_skill_models.dart';
|
||||
import 'package:xworkmate/features/assistant/assistant_page_composer_skill_picker.dart';
|
||||
import 'package:xworkmate/runtime/gateway_acp_client.dart';
|
||||
import 'package:xworkmate/runtime/go_task_service_client.dart';
|
||||
import 'package:xworkmate/runtime/runtime_models.dart';
|
||||
@ -4384,21 +4384,7 @@ Future<void> _waitForThreadLastResultCode(
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _waitForOpenClawActiveTaskCount(
|
||||
AppController controller,
|
||||
int expectedCount,
|
||||
) async {
|
||||
final deadline = DateTime.now().add(const Duration(seconds: 15));
|
||||
while (DateTime.now().isBefore(deadline)) {
|
||||
if (controller.openClawGatewayActiveTasksInternal == expectedCount) {
|
||||
return;
|
||||
}
|
||||
await Future<void>.delayed(const Duration(milliseconds: 10));
|
||||
}
|
||||
throw StateError(
|
||||
'Timed out waiting for OpenClaw active task count $expectedCount. Current count: ${controller.openClawGatewayActiveTasksInternal}.',
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
class _RecordingGoTaskServiceClient implements GoTaskServiceClient {
|
||||
int executeCount = 0;
|
||||
|
||||
@ -236,109 +236,4 @@ void main() {
|
||||
expect(controller.items.single.eligible, isFalse);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'GatewayRuntime loads skills from nested bridge status payload',
|
||||
() async {
|
||||
final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
|
||||
final subscription = server.listen((request) async {
|
||||
final body = await utf8.decoder.bind(request).join();
|
||||
final rpc = jsonDecode(body) as Map<String, dynamic>;
|
||||
final method = rpc['method']?.toString().trim() ?? '';
|
||||
request.response.headers.contentType = ContentType.json;
|
||||
|
||||
if (method == 'xworkmate.gateway.connect') {
|
||||
request.response.write(
|
||||
jsonEncode(<String, dynamic>{
|
||||
'jsonrpc': '2.0',
|
||||
'id': rpc['id'],
|
||||
'result': <String, dynamic>{
|
||||
'ok': true,
|
||||
'snapshot': <String, dynamic>{
|
||||
'status': 'connected',
|
||||
'mode': 'remote',
|
||||
'statusText': 'Connected',
|
||||
'mainSessionKey': 'main',
|
||||
},
|
||||
'auth': <String, dynamic>{'role': 'operator'},
|
||||
'returnedDeviceToken': '',
|
||||
},
|
||||
}),
|
||||
);
|
||||
await request.response.close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (method == 'xworkmate.gateway.request') {
|
||||
request.response.write(
|
||||
jsonEncode(<String, dynamic>{
|
||||
'jsonrpc': '2.0',
|
||||
'id': rpc['id'],
|
||||
'result': <String, dynamic>{
|
||||
'ok': true,
|
||||
'payload': <String, dynamic>{
|
||||
'status': <String, dynamic>{
|
||||
'workspaceDir': '/home/ubuntu/.openclaw/workspace',
|
||||
'managedSkillsDir': '/home/ubuntu/.openclaw/skills',
|
||||
'skills': <Map<String, dynamic>>[
|
||||
<String, dynamic>{
|
||||
'name': 'Browser Automation',
|
||||
'description': 'Drive browser workflows.',
|
||||
'source': 'agent',
|
||||
'id': 'browser-automation',
|
||||
'eligible': true,
|
||||
'disabled': false,
|
||||
'missingBins': <String>[],
|
||||
'missingEnv': <String>[],
|
||||
'missingConfig': <String>[],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
await request.response.close();
|
||||
return;
|
||||
}
|
||||
|
||||
request.response.statusCode = HttpStatus.badRequest;
|
||||
await request.response.close();
|
||||
});
|
||||
|
||||
final tempDir = await Directory.systemTemp.createTemp(
|
||||
'xworkmate-bridge-skills-nested-test-',
|
||||
);
|
||||
final store = SecureConfigStore(
|
||||
enableSecureStorage: false,
|
||||
appDataRootPathResolver: () async => '${tempDir.path}/settings.sqlite3',
|
||||
secretRootPathResolver: () async => tempDir.path,
|
||||
);
|
||||
final acpClient = GatewayAcpClient(
|
||||
endpointResolver: () => Uri.parse('http://127.0.0.1:${server.port}'),
|
||||
authorizationResolver: (_) async => 'bridge-token',
|
||||
);
|
||||
final identityStore = DeviceIdentityStore(store);
|
||||
final runtime = GatewayRuntime(
|
||||
store: store,
|
||||
identityStore: identityStore,
|
||||
sessionClient: GatewayAcpRuntimeSessionClient(client: acpClient),
|
||||
);
|
||||
await runtime.initialize();
|
||||
addTearDown(() async {
|
||||
runtime.dispose();
|
||||
await subscription.cancel();
|
||||
await server.close(force: true);
|
||||
await tempDir.delete(recursive: true);
|
||||
});
|
||||
|
||||
final controller = SkillsController(runtime);
|
||||
await controller.refresh(agentId: 'main');
|
||||
|
||||
expect(controller.error, isNull);
|
||||
expect(controller.items, hasLength(1));
|
||||
expect(controller.items.single.skillKey, 'browser-automation');
|
||||
expect(controller.items.single.missingBins, isEmpty);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user