Remove bridge fallback runtime code
This commit is contained in:
parent
0d41b5c317
commit
37f4cbcb32
@ -11,9 +11,9 @@ import 'app_shell.dart';
|
||||
import 'ui_feature_manifest.dart';
|
||||
|
||||
class XWorkmateApp extends StatefulWidget {
|
||||
const XWorkmateApp({super.key, this.featureManifest});
|
||||
const XWorkmateApp({super.key, required this.featureManifest});
|
||||
|
||||
final UiFeatureManifest? featureManifest;
|
||||
final UiFeatureManifest featureManifest;
|
||||
|
||||
@override
|
||||
State<XWorkmateApp> createState() => _XWorkmateAppState();
|
||||
@ -31,9 +31,7 @@ class _XWorkmateAppState extends State<XWorkmateApp> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
_themeSurface = resolveAppThemeSurface();
|
||||
_controller = AppController(
|
||||
uiFeatureManifest: widget.featureManifest ?? UiFeatureManifest.fallback(),
|
||||
);
|
||||
_controller = AppController(uiFeatureManifest: widget.featureManifest);
|
||||
if (_supportsDesktopLifecycleChannel) {
|
||||
_appLifecycleChannel.setMethodCallHandler(_handleAppLifecycleCall);
|
||||
}
|
||||
|
||||
@ -119,6 +119,7 @@ class AppController extends ChangeNotifier {
|
||||
RuntimeCoordinator? runtimeCoordinator,
|
||||
DesktopPlatformService? desktopPlatformService,
|
||||
UiFeatureManifest? uiFeatureManifest,
|
||||
List<SingleAgentProvider>? initialBridgeProviderCatalog,
|
||||
SkillDirectoryAccessService? skillDirectoryAccessService,
|
||||
AccountRuntimeClient Function(String baseUrl)? accountClientFactory,
|
||||
Map<String, String>? environmentOverride,
|
||||
@ -129,7 +130,7 @@ class AppController extends ChangeNotifier {
|
||||
}) {
|
||||
storeInternal = store ?? SecureConfigStore();
|
||||
uiFeatureManifestInternal =
|
||||
uiFeatureManifest ?? UiFeatureManifest.fallback();
|
||||
uiFeatureManifest ?? loadRepoUiFeatureManifestSyncInternal();
|
||||
hostUiFeaturePlatformInternal = Platform.isIOS || Platform.isAndroid
|
||||
? UiFeaturePlatform.mobile
|
||||
: UiFeaturePlatform.desktop;
|
||||
@ -230,6 +231,9 @@ class AppController extends ChangeNotifier {
|
||||
endpointResolver: resolveGatewayAcpEndpointInternal,
|
||||
),
|
||||
);
|
||||
bridgeProviderCatalogInternal = normalizeBridgeOwnedSingleAgentProviderList(
|
||||
initialBridgeProviderCatalog ?? const <SingleAgentProvider>[],
|
||||
);
|
||||
|
||||
attachChildListenersInternal();
|
||||
unawaited(initializeInternal());
|
||||
@ -436,7 +440,6 @@ class AppController extends ChangeNotifier {
|
||||
bool isCodexBridgeEnabledInternal = false;
|
||||
bool isCodexBridgeBusyInternal = false;
|
||||
String? codexBridgeErrorInternal;
|
||||
String? codexRuntimeWarningInternal;
|
||||
CodexCooperationState codexCooperationStateInternal =
|
||||
CodexCooperationState.notStarted;
|
||||
SettingsController get settingsController => settingsControllerInternal;
|
||||
@ -521,7 +524,6 @@ class AppController extends ChangeNotifier {
|
||||
settingsControllerInternal.hasEffectiveAiGatewayApiKey;
|
||||
bool get isCodexBridgeBusy => isCodexBridgeBusyInternal;
|
||||
String? get codexBridgeError => codexBridgeErrorInternal;
|
||||
String? get codexRuntimeWarning => codexRuntimeWarningInternal;
|
||||
CodeAgentRuntimeMode get configuredCodeAgentRuntimeMode =>
|
||||
settings.codeAgentRuntimeMode;
|
||||
CodeAgentRuntimeMode get effectiveCodeAgentRuntimeMode =>
|
||||
@ -568,15 +570,10 @@ class AppController extends ChangeNotifier {
|
||||
List<SingleAgentProvider> get bridgeProviderCatalog =>
|
||||
normalizeSingleAgentProviderList(bridgeProviderCatalogInternal);
|
||||
|
||||
List<SingleAgentProvider> get assistantProviderCatalog {
|
||||
final catalog = normalizeBridgeOwnedSingleAgentProviderList(
|
||||
bridgeProviderCatalogInternal,
|
||||
);
|
||||
if (catalog.isNotEmpty) {
|
||||
return catalog;
|
||||
}
|
||||
return kPresetExternalAcpProviders;
|
||||
}
|
||||
List<SingleAgentProvider> get assistantProviderCatalog =>
|
||||
normalizeBridgeOwnedSingleAgentProviderList(
|
||||
bridgeProviderCatalogInternal,
|
||||
);
|
||||
|
||||
SingleAgentProvider? bridgeProviderForId(String providerId) {
|
||||
final normalizedProviderId = normalizeSingleAgentProviderId(providerId);
|
||||
@ -623,6 +620,16 @@ class AppController extends ChangeNotifier {
|
||||
return resolveAssistantProvider(thread?.executionBinding.providerId);
|
||||
}
|
||||
|
||||
UiFeatureManifest loadRepoUiFeatureManifestSyncInternal() {
|
||||
final file = File(UiFeatureManifest.assetPath);
|
||||
if (!file.existsSync()) {
|
||||
throw StateError(
|
||||
'UiFeatureManifest is required and "${UiFeatureManifest.assetPath}" is missing.',
|
||||
);
|
||||
}
|
||||
return UiFeatureManifest.fromYamlString(file.readAsStringSync());
|
||||
}
|
||||
|
||||
List<AssistantExecutionTarget> visibleAssistantExecutionTargets(
|
||||
Iterable<AssistantExecutionTarget> supportedTargets,
|
||||
) => compactAssistantExecutionTargets(supportedTargets);
|
||||
|
||||
@ -50,13 +50,20 @@ Future<void> refreshAcpCapabilitiesRuntimeInternal(
|
||||
bool forceRefresh = false,
|
||||
bool persistMountTargets = false,
|
||||
}) async {
|
||||
GatewayAcpCapabilities? capabilities;
|
||||
try {
|
||||
await controller.gatewayAcpClientInternal.loadCapabilities(
|
||||
capabilities = await controller.gatewayAcpClientInternal.loadCapabilities(
|
||||
forceRefresh: forceRefresh,
|
||||
);
|
||||
} catch (_) {
|
||||
// Keep mount refresh resilient when ACP is temporarily unavailable.
|
||||
}
|
||||
if (capabilities != null) {
|
||||
controller.bridgeProviderCatalogInternal =
|
||||
normalizeBridgeOwnedSingleAgentProviderList(
|
||||
capabilities.providerCatalog,
|
||||
);
|
||||
}
|
||||
if (persistMountTargets && !controller.disposedInternal) {
|
||||
final currentConfig = controller.settings.multiAgent;
|
||||
final nextConfig = await controller.multiAgentMountManagerInternal
|
||||
@ -79,7 +86,21 @@ Future<void> refreshAcpCapabilitiesRuntimeInternal(
|
||||
Future<void> refreshSingleAgentCapabilitiesRuntimeInternal(
|
||||
AppController controller, {
|
||||
bool forceRefresh = false,
|
||||
}) async {}
|
||||
}) async {
|
||||
try {
|
||||
final capabilities = await controller.gatewayAcpClientInternal
|
||||
.loadCapabilities(forceRefresh: forceRefresh);
|
||||
controller.bridgeProviderCatalogInternal =
|
||||
normalizeBridgeOwnedSingleAgentProviderList(
|
||||
capabilities.providerCatalog,
|
||||
);
|
||||
} catch (_) {
|
||||
controller.bridgeProviderCatalogInternal = const <SingleAgentProvider>[];
|
||||
}
|
||||
if (!controller.disposedInternal) {
|
||||
controller.notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
List<ManagedMountTargetState>
|
||||
mergeAcpCapabilitiesIntoMountTargetsRuntimeInternal(
|
||||
|
||||
@ -388,23 +388,7 @@ extension AppControllerDesktopRuntimeHelpers on AppController {
|
||||
|
||||
SettingsSnapshot sanitizeCodeAgentSettingsInternal(
|
||||
SettingsSnapshot snapshot,
|
||||
) {
|
||||
final normalizedRuntimeMode =
|
||||
snapshot.codeAgentRuntimeMode == CodeAgentRuntimeMode.builtIn
|
||||
? CodeAgentRuntimeMode.externalCli
|
||||
: snapshot.codeAgentRuntimeMode;
|
||||
codexRuntimeWarningInternal =
|
||||
snapshot.codeAgentRuntimeMode == CodeAgentRuntimeMode.builtIn
|
||||
? appText(
|
||||
'内置 Codex 运行时当前仅保留为未来扩展位;已自动切换为 External Codex CLI。',
|
||||
'Built-in Codex runtime is reserved for a future release; XWorkmate switched back to External Codex CLI automatically.',
|
||||
)
|
||||
: null;
|
||||
if (normalizedRuntimeMode == snapshot.codeAgentRuntimeMode) {
|
||||
return snapshot;
|
||||
}
|
||||
return snapshot.copyWith(codeAgentRuntimeMode: normalizedRuntimeMode);
|
||||
}
|
||||
) => snapshot;
|
||||
|
||||
Future<void> refreshAcpCapabilitiesInternal({
|
||||
bool forceRefresh = false,
|
||||
|
||||
@ -1,2 +1 @@
|
||||
export 'ui_feature_manifest_core.dart';
|
||||
export 'ui_feature_manifest_fallback.dart';
|
||||
|
||||
@ -6,7 +6,6 @@ import 'package:flutter/services.dart';
|
||||
import 'package:yaml/yaml.dart';
|
||||
import '../models/app_models.dart';
|
||||
import '../runtime/runtime_models.dart';
|
||||
import 'ui_feature_manifest_fallback.dart';
|
||||
|
||||
enum UiFeaturePlatform { mobile, desktop, web }
|
||||
|
||||
@ -127,7 +126,6 @@ class UiFeatureManifest {
|
||||
|
||||
static const String assetPath = 'config/feature_flags.yaml';
|
||||
|
||||
static const String fallbackYaml = fallbackUiFeatureManifestYamlInternal;
|
||||
final Map<UiFeatureBuildMode, Set<UiFeatureReleaseTier>> releasePolicy;
|
||||
final Map<UiFeaturePlatform, Map<String, Map<String, UiFeatureFlag>>>
|
||||
flagsByPlatformInternal;
|
||||
@ -152,10 +150,6 @@ class UiFeatureManifest {
|
||||
);
|
||||
}
|
||||
|
||||
factory UiFeatureManifest.fallback() {
|
||||
return UiFeatureManifest.fromYamlString(fallbackYaml);
|
||||
}
|
||||
|
||||
UiFeatureAccess forPlatform(
|
||||
UiFeaturePlatform platform, {
|
||||
UiFeatureBuildMode? buildMode,
|
||||
@ -538,8 +532,10 @@ class UiFeatureManifestLoader {
|
||||
try {
|
||||
final raw = await bundle.loadString(assetPath);
|
||||
return UiFeatureManifest.fromYamlString(raw);
|
||||
} catch (_) {
|
||||
return UiFeatureManifest.fallback();
|
||||
} catch (error) {
|
||||
throw StateError(
|
||||
'Failed to load required UI feature manifest "$assetPath": $error',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,585 +0,0 @@
|
||||
// ignore_for_file: unused_import, unnecessary_import
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:yaml/yaml.dart';
|
||||
import '../models/app_models.dart';
|
||||
import '../runtime/runtime_models.dart';
|
||||
import 'ui_feature_manifest_core.dart';
|
||||
|
||||
const String fallbackUiFeatureManifestYamlInternal = '''
|
||||
release_policy:
|
||||
debug: [stable, beta, experimental]
|
||||
profile: [stable, beta]
|
||||
release: [stable]
|
||||
|
||||
mobile:
|
||||
navigation:
|
||||
assistant:
|
||||
enabled: true
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Mobile assistant destination
|
||||
ui_surface: mobile_shell
|
||||
tasks:
|
||||
enabled: false
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Mobile tasks destination
|
||||
ui_surface: mobile_shell
|
||||
workspace:
|
||||
enabled: true
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Mobile workspace hub destination
|
||||
ui_surface: mobile_shell
|
||||
secrets:
|
||||
enabled: true
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Mobile secrets destination
|
||||
ui_surface: mobile_shell
|
||||
settings:
|
||||
enabled: true
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Mobile settings destination
|
||||
ui_surface: mobile_shell
|
||||
workspace:
|
||||
skills:
|
||||
enabled: false
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Mobile workspace skills launcher
|
||||
ui_surface: mobile_workspace_hub
|
||||
nodes:
|
||||
enabled: false
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Mobile workspace nodes launcher
|
||||
ui_surface: mobile_workspace_hub
|
||||
agents:
|
||||
enabled: false
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Mobile workspace agents launcher
|
||||
ui_surface: mobile_workspace_hub
|
||||
mcp_server:
|
||||
enabled: false
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Mobile workspace MCP launcher
|
||||
ui_surface: mobile_workspace_hub
|
||||
claw_hub:
|
||||
enabled: false
|
||||
release_tier: experimental
|
||||
build_modes: [debug, profile, release]
|
||||
description: Mobile workspace ClawHub launcher
|
||||
ui_surface: mobile_workspace_hub
|
||||
connectors:
|
||||
enabled: false
|
||||
release_tier: experimental
|
||||
build_modes: [debug, profile, release]
|
||||
description: Mobile workspace connectors launcher
|
||||
ui_surface: mobile_workspace_hub
|
||||
ai_gateway:
|
||||
enabled: true
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Mobile workspace LLM API launcher
|
||||
ui_surface: mobile_workspace_hub
|
||||
account:
|
||||
enabled: true
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Mobile workspace account launcher
|
||||
ui_surface: mobile_workspace_hub
|
||||
assistant:
|
||||
direct_ai:
|
||||
enabled: false
|
||||
release_tier: experimental
|
||||
build_modes: []
|
||||
description: Mobile does not expose direct AI assistant mode
|
||||
ui_surface: assistant_page
|
||||
local_gateway:
|
||||
enabled: false
|
||||
release_tier: experimental
|
||||
build_modes: []
|
||||
description: Mobile does not expose a separate gateway assistant mode
|
||||
ui_surface: assistant_page
|
||||
relay_gateway:
|
||||
enabled: true
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Mobile relay gateway assistant mode
|
||||
ui_surface: assistant_page
|
||||
file_attachments:
|
||||
enabled: true
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Mobile file attachment action in assistant composer
|
||||
ui_surface: assistant_page
|
||||
multi_agent:
|
||||
enabled: true
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Mobile gateway toggle in assistant composer
|
||||
ui_surface: assistant_page
|
||||
local_runtime:
|
||||
enabled: false
|
||||
release_tier: experimental
|
||||
build_modes: []
|
||||
description: Mobile does not expose desktop runtime controls
|
||||
ui_surface: assistant_page
|
||||
settings:
|
||||
general:
|
||||
enabled: true
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Mobile settings general tab
|
||||
ui_surface: settings_page
|
||||
workspace:
|
||||
enabled: true
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Mobile settings workspace tab
|
||||
ui_surface: settings_page
|
||||
gateway:
|
||||
enabled: true
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Mobile settings gateway tab
|
||||
ui_surface: settings_page
|
||||
account_access:
|
||||
enabled: false
|
||||
release_tier: experimental
|
||||
build_modes: [debug, profile, release]
|
||||
description: Mobile account access section
|
||||
ui_surface: settings_page
|
||||
vault_server:
|
||||
enabled: false
|
||||
release_tier: experimental
|
||||
build_modes: [debug, profile, release]
|
||||
description: Mobile Vault server integration section
|
||||
ui_surface: settings_page
|
||||
gateway_self_hosted_base:
|
||||
enabled: false
|
||||
release_tier: experimental
|
||||
build_modes: [debug, profile, release]
|
||||
description: Mobile self-hosted base connection controls
|
||||
ui_surface: settings_page
|
||||
gateway_advanced_custom_mode:
|
||||
enabled: false
|
||||
release_tier: experimental
|
||||
build_modes: [debug, profile, release]
|
||||
description: Mobile advanced custom override mode
|
||||
ui_surface: settings_page
|
||||
gateway_setup_code:
|
||||
enabled: true
|
||||
release_tier: experimental
|
||||
build_modes: [debug, profile, release]
|
||||
description: Mobile gateway setup code editor
|
||||
ui_surface: settings_page
|
||||
agents:
|
||||
enabled: false
|
||||
release_tier: experimental
|
||||
build_modes: [debug, profile, release]
|
||||
description: Mobile settings gateway tab
|
||||
ui_surface: settings_page
|
||||
appearance:
|
||||
enabled: true
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Mobile settings appearance tab
|
||||
ui_surface: settings_page
|
||||
diagnostics:
|
||||
enabled: true
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Mobile settings diagnostics tab
|
||||
ui_surface: settings_page
|
||||
experimental:
|
||||
enabled: true
|
||||
release_tier: experimental
|
||||
build_modes: [debug, profile, release]
|
||||
description: Mobile settings experimental tab
|
||||
ui_surface: settings_page
|
||||
about:
|
||||
enabled: true
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Mobile settings about tab
|
||||
ui_surface: settings_page
|
||||
experimental_canvas:
|
||||
enabled: true
|
||||
release_tier: experimental
|
||||
build_modes: [debug, profile, release]
|
||||
description: Mobile experimental canvas host toggle
|
||||
ui_surface: settings_page
|
||||
experimental_bridge:
|
||||
enabled: true
|
||||
release_tier: experimental
|
||||
build_modes: [debug, profile, release]
|
||||
description: Mobile experimental bridge toggle
|
||||
ui_surface: settings_page
|
||||
experimental_debug:
|
||||
enabled: true
|
||||
release_tier: experimental
|
||||
build_modes: [debug, profile, release]
|
||||
description: Mobile experimental debug runtime toggle
|
||||
ui_surface: settings_page
|
||||
|
||||
desktop:
|
||||
navigation:
|
||||
assistant:
|
||||
enabled: true
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Desktop assistant destination
|
||||
ui_surface: sidebar_navigation
|
||||
tasks:
|
||||
enabled: true
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Desktop tasks destination
|
||||
ui_surface: sidebar_navigation
|
||||
skills:
|
||||
enabled: true
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Desktop skills destination
|
||||
ui_surface: sidebar_navigation
|
||||
nodes:
|
||||
enabled: true
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Desktop nodes destination
|
||||
ui_surface: sidebar_navigation
|
||||
agents:
|
||||
enabled: true
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Desktop agents destination
|
||||
ui_surface: sidebar_navigation
|
||||
mcp_server:
|
||||
enabled: true
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Desktop MCP Hub destination
|
||||
ui_surface: sidebar_navigation
|
||||
claw_hub:
|
||||
enabled: false
|
||||
release_tier: experimental
|
||||
build_modes: [debug, profile, release]
|
||||
description: Desktop ClawHub destination
|
||||
ui_surface: sidebar_navigation
|
||||
secrets:
|
||||
enabled: false
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Desktop secrets destination
|
||||
ui_surface: sidebar_navigation
|
||||
ai_gateway:
|
||||
enabled: false
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Desktop LLM API destination
|
||||
ui_surface: sidebar_navigation
|
||||
settings:
|
||||
enabled: true
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Desktop settings destination
|
||||
ui_surface: sidebar_navigation
|
||||
account:
|
||||
enabled: false
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Desktop account destination
|
||||
ui_surface: sidebar_navigation
|
||||
workspace:
|
||||
claw_hub:
|
||||
enabled: false
|
||||
release_tier: experimental
|
||||
build_modes: [debug, profile, release]
|
||||
description: Desktop workspace ClawHub tab
|
||||
ui_surface: modules_page
|
||||
connectors:
|
||||
enabled: false
|
||||
release_tier: experimental
|
||||
build_modes: [debug, profile, release]
|
||||
description: Desktop workspace connectors tab
|
||||
ui_surface: modules_page
|
||||
assistant:
|
||||
direct_ai:
|
||||
enabled: true
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Desktop direct AI assistant mode
|
||||
ui_surface: assistant_page
|
||||
local_gateway:
|
||||
enabled: true
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Desktop gateway assistant mode
|
||||
ui_surface: assistant_page
|
||||
relay_gateway:
|
||||
enabled: true
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Desktop relay gateway assistant mode
|
||||
ui_surface: assistant_page
|
||||
file_attachments:
|
||||
enabled: true
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Desktop file attachment action in assistant composer
|
||||
ui_surface: assistant_page
|
||||
multi_agent:
|
||||
enabled: true
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Desktop gateway toggle in assistant composer
|
||||
ui_surface: assistant_page
|
||||
local_runtime:
|
||||
enabled: true
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Desktop local runtime and gateway orchestration entry
|
||||
ui_surface: assistant_page
|
||||
settings:
|
||||
general:
|
||||
enabled: true
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Desktop settings general tab
|
||||
ui_surface: settings_page
|
||||
workspace:
|
||||
enabled: true
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Desktop settings workspace tab
|
||||
ui_surface: settings_page
|
||||
gateway:
|
||||
enabled: true
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Desktop settings gateway tab
|
||||
ui_surface: settings_page
|
||||
account_access:
|
||||
enabled: false
|
||||
release_tier: experimental
|
||||
build_modes: [debug, profile, release]
|
||||
description: Desktop account access section
|
||||
ui_surface: settings_page
|
||||
vault_server:
|
||||
enabled: true
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Desktop Vault server integration section
|
||||
ui_surface: settings_page
|
||||
gateway_self_hosted_base:
|
||||
enabled: false
|
||||
release_tier: experimental
|
||||
build_modes: [debug, profile, release]
|
||||
description: Desktop self-hosted base connection controls
|
||||
ui_surface: settings_page
|
||||
gateway_advanced_custom_mode:
|
||||
enabled: false
|
||||
release_tier: experimental
|
||||
build_modes: [debug, profile, release]
|
||||
description: Desktop advanced custom override mode
|
||||
ui_surface: settings_page
|
||||
gateway_setup_code:
|
||||
enabled: false
|
||||
release_tier: experimental
|
||||
build_modes: [debug, profile, release]
|
||||
description: Desktop gateway setup code editor
|
||||
ui_surface: settings_page
|
||||
agents:
|
||||
enabled: false
|
||||
release_tier: experimental
|
||||
build_modes: [debug, profile, release]
|
||||
description: Desktop settings gateway tab
|
||||
ui_surface: settings_page
|
||||
appearance:
|
||||
enabled: true
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Desktop settings appearance tab
|
||||
ui_surface: settings_page
|
||||
diagnostics:
|
||||
enabled: true
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Desktop settings diagnostics tab
|
||||
ui_surface: settings_page
|
||||
experimental:
|
||||
enabled: true
|
||||
release_tier: experimental
|
||||
build_modes: [debug, profile, release]
|
||||
description: Desktop settings experimental tab
|
||||
ui_surface: settings_page
|
||||
about:
|
||||
enabled: true
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Desktop settings about tab
|
||||
ui_surface: settings_page
|
||||
experimental_canvas:
|
||||
enabled: true
|
||||
release_tier: experimental
|
||||
build_modes: [debug, profile, release]
|
||||
description: Desktop experimental canvas host toggle
|
||||
ui_surface: settings_page
|
||||
experimental_bridge:
|
||||
enabled: true
|
||||
release_tier: experimental
|
||||
build_modes: [debug, profile, release]
|
||||
description: Desktop experimental bridge toggle
|
||||
ui_surface: settings_page
|
||||
experimental_debug:
|
||||
enabled: true
|
||||
release_tier: experimental
|
||||
build_modes: [debug, profile, release]
|
||||
description: Desktop experimental debug runtime toggle
|
||||
ui_surface: settings_page
|
||||
|
||||
web:
|
||||
navigation:
|
||||
assistant:
|
||||
enabled: true
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Web assistant destination
|
||||
ui_surface: web_shell
|
||||
tasks:
|
||||
enabled: true
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Web tasks destination
|
||||
ui_surface: web_shell
|
||||
skills:
|
||||
enabled: true
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Web skills destination
|
||||
ui_surface: web_shell
|
||||
nodes:
|
||||
enabled: true
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Web nodes destination
|
||||
ui_surface: web_shell
|
||||
secrets:
|
||||
enabled: true
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Web secrets destination
|
||||
ui_surface: web_shell
|
||||
ai_gateway:
|
||||
enabled: true
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Web LLM API destination
|
||||
ui_surface: web_shell
|
||||
settings:
|
||||
enabled: true
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Web settings destination
|
||||
ui_surface: web_shell
|
||||
assistant:
|
||||
direct_ai:
|
||||
enabled: true
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Web direct AI assistant mode
|
||||
ui_surface: web_assistant_page
|
||||
relay_gateway:
|
||||
enabled: true
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Web relay gateway assistant mode
|
||||
ui_surface: web_assistant_page
|
||||
file_attachments:
|
||||
enabled: true
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Web file attachment action in assistant composer
|
||||
ui_surface: web_assistant_page
|
||||
multi_agent:
|
||||
enabled: true
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Web gateway toggle in assistant composer
|
||||
ui_surface: web_assistant_page
|
||||
local_gateway:
|
||||
enabled: true
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Web gateway assistant mode
|
||||
ui_surface: web_assistant_page
|
||||
local_runtime:
|
||||
enabled: false
|
||||
release_tier: experimental
|
||||
build_modes: []
|
||||
description: Web does not expose desktop runtime controls
|
||||
ui_surface: web_assistant_page
|
||||
settings:
|
||||
general:
|
||||
enabled: true
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Web settings general tab
|
||||
ui_surface: web_settings_page
|
||||
gateway:
|
||||
enabled: true
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Web settings gateway tab
|
||||
ui_surface: web_settings_page
|
||||
account_access:
|
||||
enabled: false
|
||||
release_tier: experimental
|
||||
build_modes: []
|
||||
description: Web does not expose account access section
|
||||
ui_surface: web_settings_page
|
||||
vault_server:
|
||||
enabled: false
|
||||
release_tier: experimental
|
||||
build_modes: []
|
||||
description: Web does not expose vault server integration
|
||||
ui_surface: web_settings_page
|
||||
gateway_self_hosted_base:
|
||||
enabled: false
|
||||
release_tier: experimental
|
||||
build_modes: []
|
||||
description: Web does not expose self-hosted base connection controls
|
||||
ui_surface: web_settings_page
|
||||
gateway_advanced_custom_mode:
|
||||
enabled: false
|
||||
release_tier: experimental
|
||||
build_modes: []
|
||||
description: Web does not expose advanced custom override mode
|
||||
ui_surface: web_settings_page
|
||||
gateway_setup_code:
|
||||
enabled: false
|
||||
release_tier: experimental
|
||||
build_modes: []
|
||||
description: Web does not expose gateway setup code editor
|
||||
ui_surface: web_settings_page
|
||||
appearance:
|
||||
enabled: true
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Web settings appearance tab
|
||||
ui_surface: web_settings_page
|
||||
about:
|
||||
enabled: true
|
||||
release_tier: stable
|
||||
build_modes: [debug, profile, release]
|
||||
description: Web settings about tab
|
||||
ui_surface: web_settings_page
|
||||
''';
|
||||
@ -94,10 +94,7 @@ class CodeAgentNodeOrchestrator {
|
||||
'state': state.bridgeState,
|
||||
'gatewayConnected': state.gatewayConnected,
|
||||
'runtimeMode': state.runtimeMode.name,
|
||||
'localTransport': switch (state.runtimeMode) {
|
||||
CodeAgentRuntimeMode.externalCli => 'stdio-jsonrpc',
|
||||
CodeAgentRuntimeMode.builtIn => 'ffi-runtime',
|
||||
},
|
||||
'localTransport': 'stdio-jsonrpc',
|
||||
},
|
||||
if (provider != null)
|
||||
'provider': <String, dynamic>{
|
||||
|
||||
@ -1,339 +0,0 @@
|
||||
// FFI bindings for Codex CLI integration.
|
||||
//
|
||||
// These bindings provide direct access to the native Rust library.
|
||||
|
||||
import 'dart:ffi';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:ffi/ffi.dart';
|
||||
|
||||
// ============================================================================
|
||||
// FFI Structures
|
||||
// ============================================================================
|
||||
|
||||
/// FFI-compatible result type.
|
||||
final class CodexResultFFI extends Struct {
|
||||
@Bool()
|
||||
external bool success;
|
||||
|
||||
@Int32()
|
||||
external int errorCode;
|
||||
|
||||
external Pointer<Utf8> errorMessage;
|
||||
}
|
||||
|
||||
/// FFI-compatible message type.
|
||||
final class CodexMessageFFI extends Struct {
|
||||
external Pointer<Utf8> messageType;
|
||||
external Pointer<Utf8> content;
|
||||
external Pointer<Utf8> threadId;
|
||||
external Pointer<Utf8> turnId;
|
||||
}
|
||||
|
||||
/// FFI-compatible event type.
|
||||
final class CodexEventFFI extends Struct {
|
||||
external Pointer<Utf8> eventType;
|
||||
external Pointer<Utf8> threadId;
|
||||
external Pointer<Utf8> turnId;
|
||||
external Pointer<Utf8> data;
|
||||
@Int64()
|
||||
external int timestamp;
|
||||
}
|
||||
|
||||
/// FFI-compatible configuration.
|
||||
final class CodexConfigFFI extends Struct {
|
||||
external Pointer<Utf8> codexPath;
|
||||
external Pointer<Utf8> workingDirectory;
|
||||
@Int32()
|
||||
external int sandboxMode;
|
||||
@Int32()
|
||||
external int approvalPolicy;
|
||||
external Pointer<Utf8> model;
|
||||
external Pointer<Utf8> apiKey;
|
||||
external Pointer<Utf8> gatewayUrl;
|
||||
@Bool()
|
||||
external bool debug;
|
||||
}
|
||||
|
||||
/// Opaque thread handle.
|
||||
final class ThreadHandleFFI extends Struct {
|
||||
@Uint64()
|
||||
external int id;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Native Functions
|
||||
// ============================================================================
|
||||
|
||||
typedef _CodexInitNative = Int32 Function();
|
||||
typedef _CodexInitDart = int Function();
|
||||
|
||||
typedef _CodexRuntimeCreateNative =
|
||||
Pointer<CodexRuntime> Function(Pointer<CodexConfigFFI> config);
|
||||
typedef _CodexRuntimeCreateDart =
|
||||
Pointer<CodexRuntime> Function(Pointer<CodexConfigFFI> config);
|
||||
|
||||
typedef _CodexRuntimeDestroyNative =
|
||||
Void Function(Pointer<CodexRuntime> runtime);
|
||||
typedef _CodexRuntimeDestroyDart = void Function(Pointer<CodexRuntime> runtime);
|
||||
|
||||
typedef _CodexStartThreadNative =
|
||||
ThreadHandleFFI Function(Pointer<CodexRuntime> runtime, Pointer<Utf8> cwd);
|
||||
typedef _CodexStartThreadDart =
|
||||
ThreadHandleFFI Function(Pointer<CodexRuntime> runtime, Pointer<Utf8> cwd);
|
||||
|
||||
typedef _CodexSendMessageNative =
|
||||
Int32 Function(
|
||||
Pointer<CodexRuntime> runtime,
|
||||
ThreadHandleFFI thread,
|
||||
Pointer<Utf8> message,
|
||||
);
|
||||
typedef _CodexSendMessageDart =
|
||||
int Function(
|
||||
Pointer<CodexRuntime> runtime,
|
||||
ThreadHandleFFI thread,
|
||||
Pointer<Utf8> message,
|
||||
);
|
||||
|
||||
typedef _CodexPollEventsNative =
|
||||
UintPtr Function(
|
||||
Pointer<CodexRuntime> runtime,
|
||||
Pointer<CodexEventFFI> events,
|
||||
UintPtr maxEvents,
|
||||
);
|
||||
typedef _CodexPollEventsDart =
|
||||
int Function(
|
||||
Pointer<CodexRuntime> runtime,
|
||||
Pointer<CodexEventFFI> events,
|
||||
int maxEvents,
|
||||
);
|
||||
|
||||
typedef _CodexShutdownNative = Int32 Function(Pointer<CodexRuntime> runtime);
|
||||
typedef _CodexShutdownDart = int Function(Pointer<CodexRuntime> runtime);
|
||||
|
||||
typedef _CodexLastErrorNative =
|
||||
Pointer<Utf8> Function(Pointer<CodexRuntime> runtime);
|
||||
typedef _CodexLastErrorDart =
|
||||
Pointer<Utf8> Function(Pointer<CodexRuntime> runtime);
|
||||
|
||||
// Opaque runtime type
|
||||
final class CodexRuntime extends Opaque {}
|
||||
|
||||
// ============================================================================
|
||||
// Dart Wrapper Class
|
||||
// ============================================================================
|
||||
|
||||
/// Dart wrapper for Codex FFI.
|
||||
class CodexFFIBindings {
|
||||
final DynamicLibrary _lib;
|
||||
late final _CodexInitDart _init;
|
||||
late final _CodexRuntimeCreateDart _runtimeCreate;
|
||||
late final _CodexRuntimeDestroyDart _runtimeDestroy;
|
||||
late final _CodexStartThreadDart _startThread;
|
||||
late final _CodexSendMessageDart _sendMessage;
|
||||
late final _CodexPollEventsDart _pollEvents;
|
||||
late final _CodexShutdownDart _shutdown;
|
||||
late final _CodexLastErrorDart _lastError;
|
||||
|
||||
Pointer<CodexRuntime>? _runtime;
|
||||
|
||||
CodexFFIBindings() : _lib = _loadLibrary() {
|
||||
_init = _lib.lookupFunction<_CodexInitNative, _CodexInitDart>('codex_init');
|
||||
_runtimeCreate = _lib
|
||||
.lookupFunction<_CodexRuntimeCreateNative, _CodexRuntimeCreateDart>(
|
||||
'codex_runtime_create',
|
||||
);
|
||||
_runtimeDestroy = _lib
|
||||
.lookupFunction<_CodexRuntimeDestroyNative, _CodexRuntimeDestroyDart>(
|
||||
'codex_runtime_destroy',
|
||||
);
|
||||
_startThread = _lib
|
||||
.lookupFunction<_CodexStartThreadNative, _CodexStartThreadDart>(
|
||||
'codex_start_thread',
|
||||
);
|
||||
_sendMessage = _lib
|
||||
.lookupFunction<_CodexSendMessageNative, _CodexSendMessageDart>(
|
||||
'codex_send_message',
|
||||
);
|
||||
_pollEvents = _lib
|
||||
.lookupFunction<_CodexPollEventsNative, _CodexPollEventsDart>(
|
||||
'codex_poll_events',
|
||||
);
|
||||
_shutdown = _lib.lookupFunction<_CodexShutdownNative, _CodexShutdownDart>(
|
||||
'codex_shutdown',
|
||||
);
|
||||
_lastError = _lib
|
||||
.lookupFunction<_CodexLastErrorNative, _CodexLastErrorDart>(
|
||||
'codex_last_error',
|
||||
);
|
||||
}
|
||||
|
||||
static DynamicLibrary _loadLibrary() {
|
||||
if (Platform.isMacOS) {
|
||||
return DynamicLibrary.open('libcodex_ffi.dylib');
|
||||
} else if (Platform.isLinux) {
|
||||
return DynamicLibrary.open('libcodex_ffi.so');
|
||||
} else if (Platform.isWindows) {
|
||||
return DynamicLibrary.open('codex_ffi.dll');
|
||||
}
|
||||
throw UnsupportedError('Unsupported platform');
|
||||
}
|
||||
|
||||
/// Initialize the library.
|
||||
void initialize() {
|
||||
final result = _init();
|
||||
if (result != 0) {
|
||||
throw StateError('Failed to initialize Codex FFI');
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a runtime with configuration.
|
||||
void createRuntime(CodexConfig config) {
|
||||
if (_runtime != null) {
|
||||
throw StateError('Runtime already created');
|
||||
}
|
||||
|
||||
final configPtr = _createConfigFFI(config);
|
||||
try {
|
||||
_runtime = _runtimeCreate(configPtr);
|
||||
if (_runtime == nullptr) {
|
||||
throw StateError('Failed to create runtime');
|
||||
}
|
||||
} finally {
|
||||
_freeConfigFFI(configPtr);
|
||||
}
|
||||
}
|
||||
|
||||
/// Destroy the runtime.
|
||||
void destroyRuntime() {
|
||||
if (_runtime != null) {
|
||||
_runtimeDestroy(_runtime!);
|
||||
_runtime = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
/// Start a new thread.
|
||||
int startThread(String cwd) {
|
||||
_ensureRuntime();
|
||||
final cwdPtr = cwd.toNativeUtf8();
|
||||
try {
|
||||
final handle = _startThread(_runtime!, cwdPtr);
|
||||
return handle.id;
|
||||
} finally {
|
||||
calloc.free(cwdPtr);
|
||||
}
|
||||
}
|
||||
|
||||
/// Send a message to the thread.
|
||||
int sendMessage(int threadId, String message) {
|
||||
_ensureRuntime();
|
||||
final messagePtr = message.toNativeUtf8();
|
||||
final handlePtr = calloc<ThreadHandleFFI>();
|
||||
try {
|
||||
handlePtr.ref.id = threadId;
|
||||
return _sendMessage(_runtime!, handlePtr.ref, messagePtr);
|
||||
} finally {
|
||||
calloc.free(messagePtr);
|
||||
calloc.free(handlePtr);
|
||||
}
|
||||
}
|
||||
|
||||
/// Poll for events.
|
||||
List<Map<String, dynamic>> pollEvents(int maxEvents) {
|
||||
_ensureRuntime();
|
||||
final eventsPtr = calloc<CodexEventFFI>(maxEvents);
|
||||
try {
|
||||
final count = _pollEvents(_runtime!, eventsPtr, maxEvents);
|
||||
final events = <Map<String, dynamic>>[];
|
||||
for (var i = 0; i < count; i++) {
|
||||
final event = eventsPtr[i];
|
||||
events.add({
|
||||
'eventType': event.eventType.toDartString(),
|
||||
'threadId': event.threadId.toDartString(),
|
||||
'turnId': event.turnId.toDartString(),
|
||||
'data': event.data.toDartString(),
|
||||
'timestamp': event.timestamp,
|
||||
});
|
||||
}
|
||||
return events;
|
||||
} finally {
|
||||
calloc.free(eventsPtr);
|
||||
}
|
||||
}
|
||||
|
||||
/// Shutdown the runtime.
|
||||
void shutdown() {
|
||||
_ensureRuntime();
|
||||
_shutdown(_runtime!);
|
||||
}
|
||||
|
||||
/// Get last error message.
|
||||
String? lastError() {
|
||||
if (_runtime == null) return null;
|
||||
final ptr = _lastError(_runtime!);
|
||||
if (ptr == nullptr) return null;
|
||||
return ptr.toDartString();
|
||||
}
|
||||
|
||||
void _ensureRuntime() {
|
||||
if (_runtime == null) {
|
||||
throw StateError('Runtime not initialized');
|
||||
}
|
||||
}
|
||||
|
||||
Pointer<CodexConfigFFI> _createConfigFFI(CodexConfig config) {
|
||||
final ptr = calloc<CodexConfigFFI>();
|
||||
ptr.ref.codexPath = config.codexPath?.toNativeUtf8() ?? nullptr;
|
||||
ptr.ref.workingDirectory =
|
||||
config.workingDirectory?.toNativeUtf8() ?? nullptr;
|
||||
ptr.ref.sandboxMode = config.sandboxMode;
|
||||
ptr.ref.approvalPolicy = config.approvalPolicy;
|
||||
ptr.ref.model = config.model?.toNativeUtf8() ?? nullptr;
|
||||
ptr.ref.apiKey = config.apiKey?.toNativeUtf8() ?? nullptr;
|
||||
ptr.ref.gatewayUrl = config.gatewayUrl?.toNativeUtf8() ?? nullptr;
|
||||
ptr.ref.debug = config.debug;
|
||||
return ptr;
|
||||
}
|
||||
|
||||
void _freeConfigFFI(Pointer<CodexConfigFFI> ptr) {
|
||||
if (ptr.ref.codexPath != nullptr) {
|
||||
calloc.free(ptr.ref.codexPath);
|
||||
}
|
||||
if (ptr.ref.workingDirectory != nullptr) {
|
||||
calloc.free(ptr.ref.workingDirectory);
|
||||
}
|
||||
if (ptr.ref.model != nullptr) {
|
||||
calloc.free(ptr.ref.model);
|
||||
}
|
||||
if (ptr.ref.apiKey != nullptr) {
|
||||
calloc.free(ptr.ref.apiKey);
|
||||
}
|
||||
if (ptr.ref.gatewayUrl != nullptr) {
|
||||
calloc.free(ptr.ref.gatewayUrl);
|
||||
}
|
||||
calloc.free(ptr);
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration for Codex FFI.
|
||||
class CodexConfig {
|
||||
final String? codexPath;
|
||||
final String? workingDirectory;
|
||||
final int sandboxMode;
|
||||
final int approvalPolicy;
|
||||
final String? model;
|
||||
final String? apiKey;
|
||||
final String? gatewayUrl;
|
||||
final bool debug;
|
||||
|
||||
const CodexConfig({
|
||||
this.codexPath,
|
||||
this.workingDirectory,
|
||||
this.sandboxMode = 1, // workspace-write
|
||||
this.approvalPolicy = 0, // suggest
|
||||
this.model,
|
||||
this.apiKey,
|
||||
this.gatewayUrl,
|
||||
this.debug = false,
|
||||
});
|
||||
}
|
||||
@ -1,84 +0,0 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'go_task_service_client.dart';
|
||||
|
||||
class DesktopThreadArtifactSyncResult {
|
||||
const DesktopThreadArtifactSyncResult({
|
||||
required this.wroteArtifact,
|
||||
required this.writtenFiles,
|
||||
});
|
||||
|
||||
final bool wroteArtifact;
|
||||
final List<String> writtenFiles;
|
||||
}
|
||||
|
||||
Future<DesktopThreadArtifactSyncResult> syncInlineArtifactsToLocalWorkspace({
|
||||
required Directory root,
|
||||
required List<GoTaskServiceArtifact> artifacts,
|
||||
}) async {
|
||||
await root.create(recursive: true);
|
||||
final writtenFiles = <String>[];
|
||||
for (final artifact in artifacts) {
|
||||
if (!artifact.hasInlineContent) {
|
||||
continue;
|
||||
}
|
||||
final relativePath = sanitizeArtifactRelativePath(artifact.relativePath);
|
||||
if (relativePath.isEmpty) {
|
||||
continue;
|
||||
}
|
||||
final target = await nextArtifactTargetFile(root, relativePath);
|
||||
await target.parent.create(recursive: true);
|
||||
await target.writeAsBytes(decodeArtifactContent(artifact), flush: true);
|
||||
writtenFiles.add(target.path);
|
||||
}
|
||||
return DesktopThreadArtifactSyncResult(
|
||||
wroteArtifact: writtenFiles.isNotEmpty,
|
||||
writtenFiles: List<String>.unmodifiable(writtenFiles),
|
||||
);
|
||||
}
|
||||
|
||||
String sanitizeArtifactRelativePath(String raw) {
|
||||
final trimmed = raw.trim().replaceAll('\\', '/');
|
||||
if (trimmed.isEmpty) {
|
||||
return '';
|
||||
}
|
||||
return trimmed
|
||||
.split('/')
|
||||
.where(
|
||||
(segment) => segment.isNotEmpty && segment != '.' && segment != '..',
|
||||
)
|
||||
.join('/');
|
||||
}
|
||||
|
||||
List<int> decodeArtifactContent(GoTaskServiceArtifact artifact) {
|
||||
final encoding = artifact.encoding.trim().toLowerCase();
|
||||
if (encoding == 'base64') {
|
||||
return base64Decode(artifact.content);
|
||||
}
|
||||
return utf8.encode(artifact.content);
|
||||
}
|
||||
|
||||
Future<File> nextArtifactTargetFile(Directory root, String relativePath) async {
|
||||
final segments = relativePath.split('/');
|
||||
final fileName = segments.removeLast();
|
||||
final parent = segments.isEmpty
|
||||
? root
|
||||
: Directory('${root.path}/${segments.join('/')}');
|
||||
final dotIndex = fileName.lastIndexOf('.');
|
||||
final baseName = dotIndex <= 0 ? fileName : fileName.substring(0, dotIndex);
|
||||
final extension = dotIndex <= 0 ? '' : fileName.substring(dotIndex);
|
||||
var candidate = File('${parent.path}/$fileName');
|
||||
if (!await candidate.exists()) {
|
||||
return candidate;
|
||||
}
|
||||
for (var version = 2; version < 1000; version += 1) {
|
||||
candidate = File('${parent.path}/$baseName.v$version$extension');
|
||||
if (!await candidate.exists()) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return File(
|
||||
'${parent.path}/$baseName.${DateTime.now().millisecondsSinceEpoch}$extension',
|
||||
);
|
||||
}
|
||||
@ -107,7 +107,7 @@ class GatewayAcpClient {
|
||||
return _cachedCapabilities;
|
||||
}
|
||||
|
||||
final response = await _requestWithFallback(
|
||||
final response = await _requestForResolvedEndpoint(
|
||||
_GatewayAcpRpcRequest(
|
||||
id: _nextRequestId('capabilities'),
|
||||
method: 'acp.capabilities',
|
||||
@ -196,7 +196,7 @@ class GatewayAcpClient {
|
||||
);
|
||||
var lastSequence = -1;
|
||||
try {
|
||||
final response = await _requestWithFallback(
|
||||
final response = await _requestForResolvedEndpoint(
|
||||
rpcRequest,
|
||||
onNotification: (notification) {
|
||||
final event = _multiAgentEventFromNotification(notification);
|
||||
@ -256,7 +256,7 @@ class GatewayAcpClient {
|
||||
Uri? endpointOverride,
|
||||
String authorizationOverride = '',
|
||||
}) async {
|
||||
await _requestWithFallback(
|
||||
await _requestForResolvedEndpoint(
|
||||
_GatewayAcpRpcRequest(
|
||||
id: _nextRequestId('cancel'),
|
||||
method: 'session.cancel',
|
||||
@ -274,7 +274,7 @@ class GatewayAcpClient {
|
||||
Uri? endpointOverride,
|
||||
String authorizationOverride = '',
|
||||
}) async {
|
||||
await _requestWithFallback(
|
||||
await _requestForResolvedEndpoint(
|
||||
_GatewayAcpRpcRequest(
|
||||
id: _nextRequestId('close'),
|
||||
method: 'session.close',
|
||||
@ -293,7 +293,7 @@ class GatewayAcpClient {
|
||||
Uri? endpointOverride,
|
||||
String authorizationOverride = '',
|
||||
}) async {
|
||||
return _requestWithFallback(
|
||||
return _requestForResolvedEndpoint(
|
||||
_GatewayAcpRpcRequest(
|
||||
id: _nextRequestId(method),
|
||||
method: method,
|
||||
@ -307,7 +307,7 @@ class GatewayAcpClient {
|
||||
|
||||
Future<void> dispose() async {}
|
||||
|
||||
Future<Map<String, dynamic>> _requestWithFallback(
|
||||
Future<Map<String, dynamic>> _requestForResolvedEndpoint(
|
||||
_GatewayAcpRpcRequest request, {
|
||||
required void Function(Map<String, dynamic>) onNotification,
|
||||
Uri? endpointOverride,
|
||||
|
||||
@ -26,13 +26,10 @@ class GatewayRuntime extends ChangeNotifier with GatewayRuntimeHelpersInternal {
|
||||
required SecureConfigStore store,
|
||||
required DeviceIdentityStore identityStore,
|
||||
GatewayRuntimeSessionClient? sessionClient,
|
||||
bool allowDirectSocketFallbackOnSessionClientFailure = false,
|
||||
String runtimeId = '',
|
||||
}) : storeInternal = store,
|
||||
identityStoreInternal = identityStore,
|
||||
sessionClientInternal = sessionClient,
|
||||
allowDirectSocketFallbackOnSessionClientFailureInternal =
|
||||
allowDirectSocketFallbackOnSessionClientFailure,
|
||||
runtimeIdInternal = runtimeId.trim().isNotEmpty
|
||||
? runtimeId.trim()
|
||||
: randomIdInternal();
|
||||
@ -40,7 +37,6 @@ class GatewayRuntime extends ChangeNotifier with GatewayRuntimeHelpersInternal {
|
||||
final SecureConfigStore storeInternal;
|
||||
final DeviceIdentityStore identityStoreInternal;
|
||||
final GatewayRuntimeSessionClient? sessionClientInternal;
|
||||
final bool allowDirectSocketFallbackOnSessionClientFailureInternal;
|
||||
final String runtimeIdInternal;
|
||||
final StreamController<GatewayPushEvent> eventsInternal =
|
||||
StreamController<GatewayPushEvent>.broadcast();
|
||||
@ -316,50 +312,40 @@ class GatewayRuntime extends ChangeNotifier with GatewayRuntimeHelpersInternal {
|
||||
notifyListeners();
|
||||
return;
|
||||
} on GatewayRuntimeException catch (error) {
|
||||
if (allowDirectSocketFallbackOnSessionClientFailureInternal &&
|
||||
_shouldFallbackToDirectRuntimeInternal(error)) {
|
||||
if (error.detailCode == 'AUTH_DEVICE_TOKEN_MISMATCH' &&
|
||||
deviceToken.isNotEmpty &&
|
||||
sharedToken.isEmpty) {
|
||||
await storeInternal.clearDeviceToken(
|
||||
deviceId: identity.deviceId,
|
||||
role: 'operator',
|
||||
);
|
||||
} else if (usedStoredDeviceTokenOnly &&
|
||||
isPairingRequiredErrorInternal(error.code, error.detailCode)) {
|
||||
await storeInternal.clearDeviceToken(
|
||||
deviceId: identity.deviceId,
|
||||
role: 'operator',
|
||||
);
|
||||
appendLogInternal(
|
||||
this,
|
||||
'warn',
|
||||
'connect',
|
||||
'go-core runtime unavailable, falling back to direct websocket | code: ${error.code ?? 'unknown'}',
|
||||
'auth',
|
||||
'cleared stale device token after pairing-required response',
|
||||
);
|
||||
} else {
|
||||
if (error.detailCode == 'AUTH_DEVICE_TOKEN_MISMATCH' &&
|
||||
deviceToken.isNotEmpty &&
|
||||
sharedToken.isEmpty) {
|
||||
await storeInternal.clearDeviceToken(
|
||||
deviceId: identity.deviceId,
|
||||
role: 'operator',
|
||||
);
|
||||
} else if (usedStoredDeviceTokenOnly &&
|
||||
isPairingRequiredErrorInternal(error.code, error.detailCode)) {
|
||||
await storeInternal.clearDeviceToken(
|
||||
deviceId: identity.deviceId,
|
||||
role: 'operator',
|
||||
);
|
||||
appendLogInternal(
|
||||
this,
|
||||
'warn',
|
||||
'auth',
|
||||
'cleared stale device token after pairing-required response',
|
||||
);
|
||||
}
|
||||
snapshotInternal = snapshotInternal.copyWith(
|
||||
status: RuntimeConnectionStatus.error,
|
||||
statusText: 'Connection failed',
|
||||
lastError: error.toString(),
|
||||
lastErrorCode: error.code,
|
||||
lastErrorDetailCode: error.detailCode,
|
||||
connectAuthMode: connectAuthMode,
|
||||
connectAuthFields: connectAuthFields,
|
||||
connectAuthSources: connectAuthSources,
|
||||
hasSharedAuth: sharedToken.isNotEmpty || password.isNotEmpty,
|
||||
hasDeviceToken: deviceToken.isNotEmpty,
|
||||
);
|
||||
notifyListeners();
|
||||
rethrow;
|
||||
}
|
||||
snapshotInternal = snapshotInternal.copyWith(
|
||||
status: RuntimeConnectionStatus.error,
|
||||
statusText: 'Connection failed',
|
||||
lastError: error.toString(),
|
||||
lastErrorCode: error.code,
|
||||
lastErrorDetailCode: error.detailCode,
|
||||
connectAuthMode: connectAuthMode,
|
||||
connectAuthFields: connectAuthFields,
|
||||
connectAuthSources: connectAuthSources,
|
||||
hasSharedAuth: sharedToken.isNotEmpty || password.isNotEmpty,
|
||||
hasDeviceToken: deviceToken.isNotEmpty,
|
||||
);
|
||||
notifyListeners();
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@ -556,19 +542,6 @@ class GatewayRuntime extends ChangeNotifier with GatewayRuntimeHelpersInternal {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool _shouldFallbackToDirectRuntimeInternal(GatewayRuntimeException error) {
|
||||
switch (error.code) {
|
||||
case 'GO_GATEWAY_RUNTIME_ENDPOINT_MISSING':
|
||||
case 'GO_GATEWAY_RUNTIME_TRANSPORT_UNAVAILABLE':
|
||||
case 'GO_GATEWAY_RUNTIME_WS_CONNECT_TIMEOUT':
|
||||
case 'GO_GATEWAY_RUNTIME_WS_CLOSED':
|
||||
case 'GO_GATEWAY_RUNTIME_WS_ERROR':
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> health() => _healthInternal();
|
||||
|
||||
Future<Map<String, dynamic>> status() => _statusInternal();
|
||||
|
||||
@ -316,13 +316,6 @@ List<SingleAgentProvider> normalizeSingleAgentProviderList(
|
||||
return normalized;
|
||||
}
|
||||
|
||||
const List<SingleAgentProvider> kPresetExternalAcpProviders =
|
||||
<SingleAgentProvider>[
|
||||
SingleAgentProvider.codex,
|
||||
SingleAgentProvider.opencode,
|
||||
SingleAgentProvider.gemini,
|
||||
];
|
||||
|
||||
const String kCanonicalGatewayProviderId = 'openclaw';
|
||||
const String kCanonicalGatewayProviderLabel = 'OpenClaw';
|
||||
|
||||
|
||||
@ -10,231 +10,6 @@ import 'runtime_models_runtime_payloads.dart';
|
||||
import 'runtime_models_gateway_entities.dart';
|
||||
import 'runtime_models_multi_agent.dart';
|
||||
|
||||
class ExternalAcpEndpointProfile {
|
||||
const ExternalAcpEndpointProfile({
|
||||
required this.providerKey,
|
||||
required this.label,
|
||||
required this.badge,
|
||||
required this.endpoint,
|
||||
required this.authRef,
|
||||
required this.enabled,
|
||||
});
|
||||
|
||||
final String providerKey;
|
||||
final String label;
|
||||
final String badge;
|
||||
final String endpoint;
|
||||
final String authRef;
|
||||
final bool enabled;
|
||||
|
||||
factory ExternalAcpEndpointProfile.defaultsForProvider(
|
||||
SingleAgentProvider provider,
|
||||
) {
|
||||
return ExternalAcpEndpointProfile(
|
||||
providerKey: provider.providerId,
|
||||
label: provider.label,
|
||||
badge: provider.badge,
|
||||
endpoint: '',
|
||||
authRef: '',
|
||||
enabled: true,
|
||||
);
|
||||
}
|
||||
|
||||
ExternalAcpEndpointProfile copyWith({
|
||||
String? providerKey,
|
||||
String? label,
|
||||
String? badge,
|
||||
String? endpoint,
|
||||
String? authRef,
|
||||
bool? enabled,
|
||||
}) {
|
||||
return ExternalAcpEndpointProfile(
|
||||
providerKey: normalizeSingleAgentProviderId(
|
||||
providerKey ?? this.providerKey,
|
||||
),
|
||||
label: (label ?? this.label).trim(),
|
||||
badge: (badge ?? this.badge).trim(),
|
||||
endpoint: (endpoint ?? this.endpoint).trim(),
|
||||
authRef: (authRef ?? this.authRef).trim(),
|
||||
enabled: enabled ?? this.enabled,
|
||||
);
|
||||
}
|
||||
|
||||
SingleAgentProvider? get builtinProvider {
|
||||
final normalized = providerKey.trim().toLowerCase();
|
||||
for (final provider in kPresetExternalAcpProviders) {
|
||||
if (provider.providerId == normalized) {
|
||||
return provider;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
bool get isPreset =>
|
||||
kPresetExternalAcpProviders.any((item) => item.providerId == providerKey);
|
||||
|
||||
SingleAgentProvider toProvider() {
|
||||
final builtin = builtinProvider;
|
||||
return SingleAgentProvider.fromJsonValue(
|
||||
providerKey,
|
||||
label: label,
|
||||
badge: badge,
|
||||
).copyWith(
|
||||
source: builtin?.source ?? SingleAgentProviderSource.externalExtension,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'providerKey': providerKey,
|
||||
'label': label,
|
||||
'badge': badge,
|
||||
'endpoint': endpoint,
|
||||
'authRef': authRef,
|
||||
'enabled': enabled,
|
||||
};
|
||||
}
|
||||
|
||||
factory ExternalAcpEndpointProfile.fromJson(Map<String, dynamic> json) {
|
||||
final providerKey = normalizeSingleAgentProviderId(
|
||||
json['providerKey']?.toString() ?? '',
|
||||
);
|
||||
final builtin = SingleAgentProviderCopy.fromJsonValue(providerKey);
|
||||
final fallbackLabel = builtin.isUnspecified ? providerKey : builtin.label;
|
||||
final label = json['label']?.toString().trim().isNotEmpty == true
|
||||
? json['label'].toString().trim()
|
||||
: fallbackLabel;
|
||||
return ExternalAcpEndpointProfile(
|
||||
providerKey: providerKey,
|
||||
label: label,
|
||||
badge: json['badge']?.toString().trim().isNotEmpty == true
|
||||
? json['badge'].toString().trim()
|
||||
: singleAgentProviderFallbackBadgeInternal(
|
||||
providerId: providerKey,
|
||||
label: label,
|
||||
),
|
||||
endpoint: json['endpoint']?.toString().trim() ?? '',
|
||||
authRef: json['authRef']?.toString().trim() ?? '',
|
||||
enabled: json['enabled'] as bool? ?? true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
List<ExternalAcpEndpointProfile> normalizeExternalAcpEndpoints({
|
||||
Iterable<ExternalAcpEndpointProfile>? profiles,
|
||||
}) {
|
||||
final incoming =
|
||||
profiles?.toList(growable: false) ?? const <ExternalAcpEndpointProfile>[];
|
||||
final byKey = <String, ExternalAcpEndpointProfile>{};
|
||||
|
||||
SingleAgentProvider? canonicalProviderForProfile(
|
||||
ExternalAcpEndpointProfile profile,
|
||||
) {
|
||||
final key = profile.providerKey.trim().toLowerCase();
|
||||
for (final provider in kPresetExternalAcpProviders) {
|
||||
if (provider.providerId == key) {
|
||||
return provider;
|
||||
}
|
||||
}
|
||||
final label = profile.label.trim();
|
||||
final badge = profile.badge.trim();
|
||||
for (final provider in kPresetExternalAcpProviders) {
|
||||
if (provider.label == label && provider.badge == badge) {
|
||||
return provider;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
for (final item in incoming) {
|
||||
final originalKey = item.providerKey.trim().toLowerCase();
|
||||
final canonicalProvider = canonicalProviderForProfile(item);
|
||||
final key = canonicalProvider?.providerId ?? originalKey;
|
||||
if (key.isEmpty) {
|
||||
continue;
|
||||
}
|
||||
if (!isBridgeOwnedSingleAgentProviderId(originalKey) &&
|
||||
item.endpoint.trim().isEmpty) {
|
||||
continue;
|
||||
}
|
||||
final normalizedItem = item.copyWith(
|
||||
providerKey: key,
|
||||
label: canonicalProvider?.label ?? item.label,
|
||||
badge: canonicalProvider?.badge ?? item.badge,
|
||||
);
|
||||
final existing = byKey[key];
|
||||
if (existing == null ||
|
||||
(existing.endpoint.trim().isEmpty &&
|
||||
normalizedItem.endpoint.trim().isNotEmpty)) {
|
||||
byKey[key] = normalizedItem;
|
||||
}
|
||||
}
|
||||
|
||||
final normalized = <ExternalAcpEndpointProfile>[
|
||||
for (final provider in kPresetExternalAcpProviders)
|
||||
byKey.remove(provider.providerId) ??
|
||||
ExternalAcpEndpointProfile.defaultsForProvider(provider),
|
||||
...byKey.values,
|
||||
];
|
||||
return List<ExternalAcpEndpointProfile>.unmodifiable(normalized);
|
||||
}
|
||||
|
||||
List<ExternalAcpEndpointProfile> replaceExternalAcpEndpointForProvider(
|
||||
List<ExternalAcpEndpointProfile> profiles,
|
||||
SingleAgentProvider provider,
|
||||
ExternalAcpEndpointProfile profile,
|
||||
) {
|
||||
final normalized = normalizeExternalAcpEndpoints(profiles: profiles);
|
||||
final next = List<ExternalAcpEndpointProfile>.from(normalized);
|
||||
final index = next.indexWhere(
|
||||
(item) => item.providerKey.trim().toLowerCase() == provider.providerId,
|
||||
);
|
||||
final resolved = profile.copyWith(
|
||||
providerKey: provider.providerId,
|
||||
label: profile.label.trim().isEmpty ? provider.label : profile.label,
|
||||
badge: profile.badge.trim().isEmpty ? provider.badge : profile.badge,
|
||||
);
|
||||
if (index == -1) {
|
||||
next.add(resolved);
|
||||
} else {
|
||||
next[index] = resolved;
|
||||
}
|
||||
return normalizeExternalAcpEndpoints(profiles: next);
|
||||
}
|
||||
|
||||
ExternalAcpEndpointProfile buildCustomExternalAcpEndpointProfile(
|
||||
Iterable<ExternalAcpEndpointProfile> profiles, {
|
||||
required String label,
|
||||
required String endpoint,
|
||||
}) {
|
||||
final normalizedProfiles = normalizeExternalAcpEndpoints(profiles: profiles);
|
||||
var suffix = normalizedProfiles.length + 1;
|
||||
|
||||
String providerKey() => 'custom-agent-$suffix';
|
||||
|
||||
final existingKeys = normalizedProfiles
|
||||
.map((item) => item.providerKey)
|
||||
.toSet();
|
||||
while (existingKeys.contains(providerKey())) {
|
||||
suffix += 1;
|
||||
}
|
||||
|
||||
final normalizedLabel = label.trim().isEmpty
|
||||
? 'Custom ACP Endpoint $suffix'
|
||||
: label.trim();
|
||||
return ExternalAcpEndpointProfile(
|
||||
providerKey: providerKey(),
|
||||
label: normalizedLabel,
|
||||
badge: singleAgentProviderFallbackBadgeInternal(
|
||||
providerId: providerKey(),
|
||||
label: normalizedLabel,
|
||||
),
|
||||
endpoint: endpoint.trim(),
|
||||
authRef: '',
|
||||
enabled: true,
|
||||
);
|
||||
}
|
||||
|
||||
String normalizeAuthorizedSkillDirectoryPath(String path) {
|
||||
var trimmed = path.trim();
|
||||
if (trimmed.isEmpty) {
|
||||
@ -377,7 +152,7 @@ extension AssistantPermissionLevelCopy on AssistantPermissionLevel {
|
||||
}
|
||||
}
|
||||
|
||||
enum CodeAgentRuntimeMode { builtIn, externalCli }
|
||||
enum CodeAgentRuntimeMode { externalCli }
|
||||
|
||||
extension CodeAgentRuntimeModeCopy on CodeAgentRuntimeMode {
|
||||
String get label => switch (this) {
|
||||
@ -385,7 +160,6 @@ extension CodeAgentRuntimeModeCopy on CodeAgentRuntimeMode {
|
||||
'外部 Codex CLI',
|
||||
'External Codex CLI',
|
||||
),
|
||||
CodeAgentRuntimeMode.builtIn => appText('内置 Codex', 'Built-in Codex'),
|
||||
};
|
||||
|
||||
static CodeAgentRuntimeMode fromJsonValue(String? value) {
|
||||
|
||||
@ -1,26 +0,0 @@
|
||||
import 'runtime_models.dart';
|
||||
|
||||
class SingleAgentCapabilities {
|
||||
const SingleAgentCapabilities({
|
||||
required this.available,
|
||||
required this.supportedProviders,
|
||||
required this.endpoint,
|
||||
this.errorMessage,
|
||||
});
|
||||
|
||||
const SingleAgentCapabilities.unavailable({
|
||||
required this.endpoint,
|
||||
this.errorMessage,
|
||||
}) : available = false,
|
||||
supportedProviders = const <SingleAgentProvider>[];
|
||||
|
||||
final bool available;
|
||||
final List<SingleAgentProvider> supportedProviders;
|
||||
final String endpoint;
|
||||
final String? errorMessage;
|
||||
|
||||
bool get supportsCodex => supportsProvider(SingleAgentProvider.codex);
|
||||
|
||||
bool supportsProvider(SingleAgentProvider provider) =>
|
||||
supportedProviders.contains(provider);
|
||||
}
|
||||
@ -4,13 +4,20 @@ import 'package:xworkmate/app/app_controller.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_main.dart';
|
||||
import 'package:xworkmate/runtime/runtime_models.dart';
|
||||
import 'package:xworkmate/theme/app_theme.dart';
|
||||
import 'package:xworkmate/widgets/surface_card.dart';
|
||||
|
||||
void main() {
|
||||
group('AssistantLowerPaneInternal', () {
|
||||
testWidgets('shows agent and gateway task dialog modes', (tester) async {
|
||||
final controller = AppController();
|
||||
final controller = AppController(
|
||||
initialBridgeProviderCatalog: const <SingleAgentProvider>[
|
||||
SingleAgentProvider.codex,
|
||||
SingleAgentProvider.opencode,
|
||||
SingleAgentProvider.gemini,
|
||||
],
|
||||
);
|
||||
addTearDown(controller.dispose);
|
||||
|
||||
await controller.sessionsController.switchSession('session-1');
|
||||
@ -66,7 +73,13 @@ void main() {
|
||||
testWidgets('shows assistant providers and allows switching provider', (
|
||||
tester,
|
||||
) async {
|
||||
final controller = AppController();
|
||||
final controller = AppController(
|
||||
initialBridgeProviderCatalog: const <SingleAgentProvider>[
|
||||
SingleAgentProvider.codex,
|
||||
SingleAgentProvider.opencode,
|
||||
SingleAgentProvider.gemini,
|
||||
],
|
||||
);
|
||||
addTearDown(controller.dispose);
|
||||
|
||||
await controller.sessionsController.switchSession('session-1');
|
||||
|
||||
@ -22,20 +22,5 @@ void main() {
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test('fallback manifest only exposes assistant and settings on desktop', () {
|
||||
final desktop = UiFeatureManifest.fallback().forPlatform(
|
||||
UiFeaturePlatform.desktop,
|
||||
buildMode: UiFeatureBuildMode.debug,
|
||||
);
|
||||
|
||||
expect(
|
||||
desktop.allowedDestinations,
|
||||
<WorkspaceDestination>{
|
||||
WorkspaceDestination.assistant,
|
||||
WorkspaceDestination.settings,
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user