xworkmate-app/lib/app/ui_feature_manifest.dart

996 lines
32 KiB
Dart

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';
enum UiFeaturePlatform { mobile, desktop, web }
enum UiFeatureReleaseTier { stable, beta, experimental }
enum UiFeatureBuildMode { debug, profile, release }
UiFeatureBuildMode currentUiFeatureBuildMode() {
if (kReleaseMode) {
return UiFeatureBuildMode.release;
}
if (kProfileMode) {
return UiFeatureBuildMode.profile;
}
return UiFeatureBuildMode.debug;
}
UiFeaturePlatform resolveUiFeaturePlatformFromContext(BuildContext context) {
if (kIsWeb) {
return UiFeaturePlatform.web;
}
final platform = Theme.of(context).platform;
if (platform == TargetPlatform.iOS || platform == TargetPlatform.android) {
return UiFeaturePlatform.mobile;
}
return UiFeaturePlatform.desktop;
}
abstract final class UiFeatureKeys {
static const navigationAssistant = 'navigation.assistant';
static const navigationTasks = 'navigation.tasks';
static const navigationWorkspace = 'navigation.workspace';
static const navigationSkills = 'navigation.skills';
static const navigationNodes = 'navigation.nodes';
static const navigationAgents = 'navigation.agents';
static const navigationMcpServer = 'navigation.mcp_server';
static const navigationClawHub = 'navigation.claw_hub';
static const navigationSecrets = 'navigation.secrets';
static const navigationAiGateway = 'navigation.ai_gateway';
static const navigationSettings = 'navigation.settings';
static const navigationAccount = 'navigation.account';
static const workspaceSkills = 'workspace.skills';
static const workspaceNodes = 'workspace.nodes';
static const workspaceAgents = 'workspace.agents';
static const workspaceMcpServer = 'workspace.mcp_server';
static const workspaceClawHub = 'workspace.claw_hub';
static const workspaceAiGateway = 'workspace.ai_gateway';
static const workspaceAccount = 'workspace.account';
static const assistantDirectAi = 'assistant.direct_ai';
static const assistantLocalGateway = 'assistant.local_gateway';
static const assistantRelayGateway = 'assistant.relay_gateway';
static const assistantFileAttachments = 'assistant.file_attachments';
static const assistantMultiAgent = 'assistant.multi_agent';
static const assistantLocalRuntime = 'assistant.local_runtime';
static const settingsGeneral = 'settings.general';
static const settingsWorkspace = 'settings.workspace';
static const settingsGateway = 'settings.gateway';
static const settingsAgents = 'settings.agents';
static const settingsAppearance = 'settings.appearance';
static const settingsDiagnostics = 'settings.diagnostics';
static const settingsExperimental = 'settings.experimental';
static const settingsAbout = 'settings.about';
static const settingsExperimentalCanvas = 'settings.experimental_canvas';
static const settingsExperimentalBridge = 'settings.experimental_bridge';
static const settingsExperimentalDebug = 'settings.experimental_debug';
}
@immutable
class UiFeatureFlag {
const UiFeatureFlag({
required this.enabled,
required this.releaseTier,
required this.buildModes,
required this.description,
required this.uiSurface,
});
final bool enabled;
final UiFeatureReleaseTier releaseTier;
final Set<UiFeatureBuildMode> buildModes;
final String description;
final String uiSurface;
UiFeatureFlag copyWith({
bool? enabled,
UiFeatureReleaseTier? releaseTier,
Set<UiFeatureBuildMode>? buildModes,
String? description,
String? uiSurface,
}) {
return UiFeatureFlag(
enabled: enabled ?? this.enabled,
releaseTier: releaseTier ?? this.releaseTier,
buildModes: buildModes ?? this.buildModes,
description: description ?? this.description,
uiSurface: uiSurface ?? this.uiSurface,
);
}
}
class UiFeatureManifest {
UiFeatureManifest._({
required this.releasePolicy,
required Map<UiFeaturePlatform, Map<String, Map<String, UiFeatureFlag>>>
flagsByPlatform,
}) : _flagsByPlatform = flagsByPlatform;
static const String assetPath = 'config/feature_flags.yaml';
static const String fallbackYaml = '''
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: true
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: true
release_tier: stable
build_modes: [debug, profile, release]
description: Mobile workspace skills launcher
ui_surface: mobile_workspace_hub
nodes:
enabled: true
release_tier: stable
build_modes: [debug, profile, release]
description: Mobile workspace nodes launcher
ui_surface: mobile_workspace_hub
agents:
enabled: true
release_tier: stable
build_modes: [debug, profile, release]
description: Mobile workspace agents launcher
ui_surface: mobile_workspace_hub
mcp_server:
enabled: true
release_tier: stable
build_modes: [debug, profile, release]
description: Mobile workspace MCP launcher
ui_surface: mobile_workspace_hub
claw_hub:
enabled: true
release_tier: stable
build_modes: [debug, profile, release]
description: Mobile workspace ClawHub launcher
ui_surface: mobile_workspace_hub
ai_gateway:
enabled: true
release_tier: stable
build_modes: [debug, profile, release]
description: Mobile workspace AI Gateway 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: true
release_tier: stable
build_modes: [debug, profile, release]
description: Mobile direct AI assistant mode
ui_surface: assistant_page
local_gateway:
enabled: true
release_tier: stable
build_modes: [debug, profile, release]
description: Mobile local 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 multi-agent 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
agents:
enabled: true
release_tier: stable
build_modes: [debug, profile, release]
description: Mobile settings multi-agent 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: true
release_tier: stable
build_modes: [debug, profile, release]
description: Desktop ClawHub destination
ui_surface: sidebar_navigation
secrets:
enabled: true
release_tier: stable
build_modes: [debug, profile, release]
description: Desktop secrets destination
ui_surface: sidebar_navigation
ai_gateway:
enabled: true
release_tier: stable
build_modes: [debug, profile, release]
description: Desktop AI Gateway 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: true
release_tier: stable
build_modes: [debug, profile, release]
description: Desktop account destination
ui_surface: sidebar_navigation
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 local 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 multi-agent 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
agents:
enabled: true
release_tier: stable
build_modes: [debug, profile, release]
description: Desktop settings multi-agent 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
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: false
release_tier: experimental
build_modes: []
description: Web does not expose file attachments in assistant composer
ui_surface: web_assistant_page
multi_agent:
enabled: false
release_tier: experimental
build_modes: []
description: Web does not expose multi-agent assistant toggle
ui_surface: web_assistant_page
local_gateway:
enabled: false
release_tier: experimental
build_modes: []
description: Web does not expose local 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
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
''';
final Map<UiFeatureBuildMode, Set<UiFeatureReleaseTier>> releasePolicy;
final Map<UiFeaturePlatform, Map<String, Map<String, UiFeatureFlag>>>
_flagsByPlatform;
factory UiFeatureManifest.fromYamlString(String raw) {
final root = loadYaml(raw);
if (root is! YamlMap) {
throw const FormatException('Feature manifest root must be a YAML map.');
}
final releasePolicy = _parseReleasePolicy(root['release_policy']);
final flagsByPlatform =
<UiFeaturePlatform, Map<String, Map<String, UiFeatureFlag>>>{};
for (final platform in UiFeaturePlatform.values) {
flagsByPlatform[platform] = _parsePlatformModules(
platform: platform,
raw: root[platform.name],
);
}
return UiFeatureManifest._(
releasePolicy: releasePolicy,
flagsByPlatform: flagsByPlatform,
);
}
factory UiFeatureManifest.fallback() {
return UiFeatureManifest.fromYamlString(fallbackYaml);
}
UiFeatureAccess forPlatform(
UiFeaturePlatform platform, {
UiFeatureBuildMode? buildMode,
}) {
return UiFeatureAccess._(
manifest: this,
platform: platform,
buildMode: buildMode ?? currentUiFeatureBuildMode(),
);
}
UiFeatureFlag? lookup(
UiFeaturePlatform platform,
String module,
String feature,
) {
return _flagsByPlatform[platform]?[module]?[feature];
}
UiFeatureManifest copyWithFeature({
required UiFeaturePlatform platform,
required String module,
required String feature,
bool? enabled,
UiFeatureReleaseTier? releaseTier,
Set<UiFeatureBuildMode>? buildModes,
String? description,
String? uiSurface,
}) {
final current = lookup(platform, module, feature);
if (current == null) {
throw StateError('Unknown feature: ${platform.name}.$module.$feature');
}
final updated = current.copyWith(
enabled: enabled,
releaseTier: releaseTier,
buildModes: buildModes,
description: description,
uiSurface: uiSurface,
);
final nextPlatforms =
<UiFeaturePlatform, Map<String, Map<String, UiFeatureFlag>>>{};
for (final entry in _flagsByPlatform.entries) {
nextPlatforms[entry.key] = entry.value.map(
(moduleName, features) => MapEntry(
moduleName,
features.map((featureName, flag) => MapEntry(featureName, flag)),
),
);
}
nextPlatforms[platform]![module]![feature] = updated;
return UiFeatureManifest._(
releasePolicy: releasePolicy,
flagsByPlatform: nextPlatforms,
);
}
static Map<UiFeatureBuildMode, Set<UiFeatureReleaseTier>> _parseReleasePolicy(
Object? raw,
) {
if (raw is! YamlMap) {
throw const FormatException(
'release_policy must define debug/profile/release tiers.',
);
}
final policy = <UiFeatureBuildMode, Set<UiFeatureReleaseTier>>{};
for (final mode in UiFeatureBuildMode.values) {
final rawValue = raw[mode.name];
if (rawValue is! YamlList) {
throw FormatException(
'release_policy.${mode.name} must be a list of tiers.',
);
}
policy[mode] = rawValue
.map((value) => _parseReleaseTier(value, context: mode.name))
.toSet();
}
return policy;
}
static Map<String, Map<String, UiFeatureFlag>> _parsePlatformModules({
required UiFeaturePlatform platform,
required Object? raw,
}) {
if (raw is! YamlMap) {
throw FormatException('${platform.name} must be a YAML map.');
}
final modules = <String, Map<String, UiFeatureFlag>>{};
for (final entry in raw.entries) {
final moduleName = '${entry.key}'.trim();
if (moduleName.isEmpty) {
throw FormatException('${platform.name} contains an empty module key.');
}
final rawModule = entry.value;
if (rawModule is! YamlMap) {
throw FormatException('${platform.name}.$moduleName must be a map.');
}
final features = <String, UiFeatureFlag>{};
for (final featureEntry in rawModule.entries) {
final featureName = '${featureEntry.key}'.trim();
if (featureName.isEmpty) {
throw FormatException(
'${platform.name}.$moduleName contains an empty feature key.',
);
}
features[featureName] = _parseFeatureFlag(
platform: platform,
moduleName: moduleName,
featureName: featureName,
raw: featureEntry.value,
);
}
modules[moduleName] = features;
}
return modules;
}
static UiFeatureFlag _parseFeatureFlag({
required UiFeaturePlatform platform,
required String moduleName,
required String featureName,
required Object? raw,
}) {
if (raw is! YamlMap) {
throw FormatException(
'${platform.name}.$moduleName.$featureName must be a map.',
);
}
const allowedKeys = <String>{
'enabled',
'release_tier',
'build_modes',
'description',
'ui_surface',
};
for (final key in raw.keys) {
final name = '$key';
if (!allowedKeys.contains(name)) {
throw FormatException(
'Unsupported key "$name" in '
'${platform.name}.$moduleName.$featureName.',
);
}
}
final enabled = raw['enabled'];
final releaseTier = raw['release_tier'];
final buildModes = raw['build_modes'];
final description = raw['description'];
final uiSurface = raw['ui_surface'];
if (enabled is! bool) {
throw FormatException(
'${platform.name}.$moduleName.$featureName.enabled must be bool.',
);
}
if (buildModes is! YamlList) {
throw FormatException(
'${platform.name}.$moduleName.$featureName.build_modes must be a list.',
);
}
if (description is! String || description.trim().isEmpty) {
throw FormatException(
'${platform.name}.$moduleName.$featureName.description is required.',
);
}
if (uiSurface is! String || uiSurface.trim().isEmpty) {
throw FormatException(
'${platform.name}.$moduleName.$featureName.ui_surface is required.',
);
}
return UiFeatureFlag(
enabled: enabled,
releaseTier: _parseReleaseTier(
releaseTier,
context: '${platform.name}.$moduleName.$featureName',
),
buildModes: buildModes
.map(
(value) => _parseBuildMode(
value,
context: '${platform.name}.$moduleName.$featureName',
),
)
.toSet(),
description: description.trim(),
uiSurface: uiSurface.trim(),
);
}
static UiFeatureReleaseTier _parseReleaseTier(
Object? raw, {
required String context,
}) {
final value = '$raw'.trim();
return UiFeatureReleaseTier.values.firstWhere(
(item) => item.name == value,
orElse: () {
throw FormatException('Unknown release tier "$value" at $context.');
},
);
}
static UiFeatureBuildMode _parseBuildMode(
Object? raw, {
required String context,
}) {
final value = '$raw'.trim();
return UiFeatureBuildMode.values.firstWhere(
(item) => item.name == value,
orElse: () {
throw FormatException('Unknown build mode "$value" at $context.');
},
);
}
}
class UiFeatureAccess {
UiFeatureAccess._({
required UiFeatureManifest manifest,
required this.platform,
required this.buildMode,
}) : _manifest = manifest;
final UiFeatureManifest _manifest;
final UiFeaturePlatform platform;
final UiFeatureBuildMode buildMode;
static const Map<UiFeaturePlatform, Map<String, WorkspaceDestination>>
_destinationMappings = <UiFeaturePlatform, Map<String, WorkspaceDestination>>{
UiFeaturePlatform.mobile: <String, WorkspaceDestination>{
UiFeatureKeys.navigationAssistant: WorkspaceDestination.assistant,
UiFeatureKeys.navigationTasks: WorkspaceDestination.tasks,
UiFeatureKeys.navigationSecrets: WorkspaceDestination.secrets,
UiFeatureKeys.navigationSettings: WorkspaceDestination.settings,
UiFeatureKeys.workspaceSkills: WorkspaceDestination.skills,
UiFeatureKeys.workspaceNodes: WorkspaceDestination.nodes,
UiFeatureKeys.workspaceAgents: WorkspaceDestination.agents,
UiFeatureKeys.workspaceMcpServer: WorkspaceDestination.mcpServer,
UiFeatureKeys.workspaceClawHub: WorkspaceDestination.clawHub,
UiFeatureKeys.workspaceAiGateway: WorkspaceDestination.aiGateway,
UiFeatureKeys.workspaceAccount: WorkspaceDestination.account,
},
UiFeaturePlatform.desktop: <String, WorkspaceDestination>{
UiFeatureKeys.navigationAssistant: WorkspaceDestination.assistant,
UiFeatureKeys.navigationTasks: WorkspaceDestination.tasks,
UiFeatureKeys.navigationSkills: WorkspaceDestination.skills,
UiFeatureKeys.navigationNodes: WorkspaceDestination.nodes,
UiFeatureKeys.navigationAgents: WorkspaceDestination.agents,
UiFeatureKeys.navigationMcpServer: WorkspaceDestination.mcpServer,
UiFeatureKeys.navigationClawHub: WorkspaceDestination.clawHub,
UiFeatureKeys.navigationSecrets: WorkspaceDestination.secrets,
UiFeatureKeys.navigationAiGateway: WorkspaceDestination.aiGateway,
UiFeatureKeys.navigationSettings: WorkspaceDestination.settings,
UiFeatureKeys.navigationAccount: WorkspaceDestination.account,
},
UiFeaturePlatform.web: <String, WorkspaceDestination>{
UiFeatureKeys.navigationAssistant: WorkspaceDestination.assistant,
UiFeatureKeys.navigationSettings: WorkspaceDestination.settings,
},
};
static const Map<String, SettingsTab> _settingsTabMappings =
<String, SettingsTab>{
UiFeatureKeys.settingsGeneral: SettingsTab.general,
UiFeatureKeys.settingsWorkspace: SettingsTab.workspace,
UiFeatureKeys.settingsGateway: SettingsTab.gateway,
UiFeatureKeys.settingsAgents: SettingsTab.agents,
UiFeatureKeys.settingsAppearance: SettingsTab.appearance,
UiFeatureKeys.settingsDiagnostics: SettingsTab.diagnostics,
UiFeatureKeys.settingsExperimental: SettingsTab.experimental,
UiFeatureKeys.settingsAbout: SettingsTab.about,
};
bool isEnabledPath(String path) {
final parts = path.split('.');
if (parts.length != 2) {
throw ArgumentError.value(path, 'path', 'Expected module.feature');
}
return isEnabled(parts[0], parts[1]);
}
bool isEnabled(String module, String feature) {
final flag = _manifest.lookup(platform, module, feature);
if (flag == null || !flag.enabled) {
return false;
}
if (!flag.buildModes.contains(buildMode)) {
return false;
}
final allowedTiers = _manifest.releasePolicy[buildMode] ?? const {};
return allowedTiers.contains(flag.releaseTier);
}
Set<WorkspaceDestination> get allowedDestinations {
final mappings = _destinationMappings[platform] ?? const {};
final allowed = <WorkspaceDestination>{};
for (final entry in mappings.entries) {
if (isEnabledPath(entry.key)) {
allowed.add(entry.value);
}
}
return allowed;
}
bool get showsWorkspaceHub =>
platform == UiFeaturePlatform.mobile &&
isEnabledPath(UiFeatureKeys.navigationWorkspace);
bool get supportsDirectAi => isEnabledPath(UiFeatureKeys.assistantDirectAi);
bool get supportsLocalGateway =>
isEnabledPath(UiFeatureKeys.assistantLocalGateway);
bool get supportsRelayGateway =>
isEnabledPath(UiFeatureKeys.assistantRelayGateway);
bool get supportsFileAttachments =>
isEnabledPath(UiFeatureKeys.assistantFileAttachments);
bool get supportsMultiAgent =>
isEnabledPath(UiFeatureKeys.assistantMultiAgent);
bool get supportsDesktopRuntime =>
platform == UiFeaturePlatform.desktop &&
isEnabledPath(UiFeatureKeys.assistantLocalRuntime);
bool get supportsDiagnostics =>
isEnabledPath(UiFeatureKeys.settingsDiagnostics);
List<SettingsTab> get availableSettingsTabs {
return SettingsTab.values
.where(
(tab) => _settingsTabMappings.entries.any(
(entry) => entry.value == tab && isEnabledPath(entry.key),
),
)
.toList(growable: false);
}
SettingsTab sanitizeSettingsTab(SettingsTab tab) {
final available = availableSettingsTabs;
if (available.contains(tab)) {
return tab;
}
if (available.isNotEmpty) {
return available.first;
}
return SettingsTab.general;
}
bool allowsExperimentalSetting(String keyPath) {
return isEnabledPath(keyPath);
}
List<AssistantExecutionTarget> get availableExecutionTargets {
final targets = <AssistantExecutionTarget>[];
if (supportsDirectAi) {
targets.add(AssistantExecutionTarget.aiGatewayOnly);
}
if (supportsLocalGateway) {
targets.add(AssistantExecutionTarget.local);
}
if (supportsRelayGateway) {
targets.add(AssistantExecutionTarget.remote);
}
return targets;
}
AssistantExecutionTarget sanitizeExecutionTarget(
AssistantExecutionTarget? target,
) {
final available = availableExecutionTargets;
if (target != null && available.contains(target)) {
return target;
}
final preferredOrder = platform == UiFeaturePlatform.web
? const <AssistantExecutionTarget>[
AssistantExecutionTarget.aiGatewayOnly,
AssistantExecutionTarget.remote,
]
: const <AssistantExecutionTarget>[
AssistantExecutionTarget.local,
AssistantExecutionTarget.aiGatewayOnly,
AssistantExecutionTarget.remote,
];
for (final candidate in preferredOrder) {
if (available.contains(candidate)) {
return candidate;
}
}
return platform == UiFeaturePlatform.web
? AssistantExecutionTarget.aiGatewayOnly
: AssistantExecutionTarget.local;
}
}
class UiFeatureManifestLoader {
const UiFeatureManifestLoader._();
static Future<UiFeatureManifest> load({
AssetBundle? assetBundle,
String assetPath = UiFeatureManifest.assetPath,
}) async {
final bundle = assetBundle ?? rootBundle;
try {
final raw = await bundle.loadString(assetPath);
return UiFeatureManifest.fromYamlString(raw);
} catch (_) {
return UiFeatureManifest.fallback();
}
}
}