Remove bridge fallback runtime code

This commit is contained in:
Haitao Pan 2026-04-13 11:34:31 +08:00
parent 0d41b5c317
commit 37f4cbcb32
17 changed files with 102 additions and 1396 deletions

View File

@ -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);
}

View File

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

View File

@ -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(

View File

@ -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,

View File

@ -1,2 +1 @@
export 'ui_feature_manifest_core.dart';
export 'ui_feature_manifest_fallback.dart';

View File

@ -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',
);
}
}
}

View File

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

View File

@ -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>{

View File

@ -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,
});
}

View File

@ -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',
);
}

View File

@ -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,

View File

@ -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();

View File

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

View File

@ -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) {

View File

@ -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);
}

View File

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

View File

@ -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,
},
);
});
});
}