Clean bridge provider unavailable UX copy
This commit is contained in:
parent
6db804f133
commit
3cfae35b08
@ -258,15 +258,14 @@ extension AppControllerDesktopRuntimeHelpers on AppController {
|
||||
}
|
||||
final profile = gatewayProfileForAssistantExecutionTargetInternal(target);
|
||||
final address = gatewayAddressLabelInternal(profile);
|
||||
final targetLabel = target.label;
|
||||
return address == appText('未连接目标', 'No target')
|
||||
? appText(
|
||||
'当前线程目标网关未连接。请先连接 $targetLabel,然后再重试。',
|
||||
'The selected gateway target for this thread is not connected. Connect $targetLabel first, then try again.',
|
||||
'当前 xworkmate-bridge 未连接。请先恢复 bridge 连接后再重试。',
|
||||
'xworkmate-bridge is not connected. Restore the bridge connection, then try again.',
|
||||
)
|
||||
: appText(
|
||||
'当前线程目标网关未连接:$address。请先连接后再重试。',
|
||||
'The selected gateway target for this thread is not connected: $address. Connect it first, then try again.',
|
||||
'当前 xworkmate-bridge 未连接:$address。请先恢复 bridge 连接后再重试。',
|
||||
'xworkmate-bridge is not connected: $address. Restore the bridge connection, then try again.',
|
||||
);
|
||||
}
|
||||
return raw;
|
||||
|
||||
@ -76,32 +76,6 @@ Future<void> sendSingleAgentMessageDesktopGoTaskFlowInternal(
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
final preflightUnavailableReason =
|
||||
controller.singleAgentShouldSuggestAcpSwitchForSession(sessionKey) ||
|
||||
controller.singleAgentNeedsBridgeProviderForSession(sessionKey)
|
||||
? singleAgentUnavailableLabelDesktopInternal(
|
||||
controller,
|
||||
sessionKey,
|
||||
null,
|
||||
)
|
||||
: null;
|
||||
if (preflightUnavailableReason != null) {
|
||||
controller.upsertTaskThreadInternal(
|
||||
sessionKey,
|
||||
lifecycleStatus: 'ready',
|
||||
lastRunAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||
lastResultCode: 'error',
|
||||
updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||
);
|
||||
controller.appendAssistantThreadMessageInternal(
|
||||
sessionKey,
|
||||
assistantErrorMessageSingleAgentDesktopInternal(
|
||||
controller,
|
||||
preflightUnavailableReason,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (controller.resolveExternalAcpEndpointForTargetInternal(
|
||||
AssistantExecutionTarget.singleAgent,
|
||||
) ==
|
||||
@ -148,19 +122,13 @@ Future<void> sendSingleAgentMessageDesktopGoTaskFlowInternal(
|
||||
sessionKey,
|
||||
routingResolution.unavailableMessage,
|
||||
)
|
||||
: controller.singleAgentShouldSuggestAcpSwitchForSession(sessionKey)
|
||||
? singleAgentUnavailableLabelDesktopInternal(
|
||||
controller,
|
||||
sessionKey,
|
||||
null,
|
||||
)
|
||||
: controller.singleAgentNeedsBridgeProviderForSession(sessionKey)
|
||||
: resolvedProviderId.isEmpty && effectiveProvider.isUnspecified
|
||||
? singleAgentUnavailableLabelDesktopInternal(
|
||||
controller,
|
||||
sessionKey,
|
||||
appText(
|
||||
'Bridge 当前没有同步到可用 Provider。',
|
||||
'The bridge does not currently have any synced providers.',
|
||||
'Bridge 当前没有广告可用 Provider。',
|
||||
'The bridge is not advertising any available providers.',
|
||||
),
|
||||
)
|
||||
: null;
|
||||
|
||||
@ -52,19 +52,11 @@ import 'app_controller_desktop_thread_sessions_collaboration_impl.dart';
|
||||
AssistantThreadConnectionState resolveGatewayThreadConnectionStateInternal({
|
||||
required AssistantExecutionTarget target,
|
||||
required GatewayConnectionSnapshot connection,
|
||||
required GatewayConnectionProfile targetProfile,
|
||||
}) {
|
||||
const expectedMode = RuntimeConnectionMode.remote;
|
||||
final matchesTarget = connection.mode == expectedMode;
|
||||
final targetAddress =
|
||||
targetProfile.host.trim().isNotEmpty && targetProfile.port > 0
|
||||
? '${targetProfile.host.trim()}:${targetProfile.port}'
|
||||
: appText('未连接目标', 'No target');
|
||||
final rawStatus = matchesTarget
|
||||
? connection.status
|
||||
: RuntimeConnectionStatus.offline;
|
||||
final pairingRequired = matchesTarget && connection.pairingRequired;
|
||||
final gatewayTokenMissing = matchesTarget && connection.gatewayTokenMissing;
|
||||
final bridgeAddress = connection.remoteAddress?.trim() ?? '';
|
||||
final rawStatus = connection.status;
|
||||
final pairingRequired = connection.pairingRequired;
|
||||
final gatewayTokenMissing = connection.gatewayTokenMissing;
|
||||
final status = pairingRequired || gatewayTokenMissing
|
||||
? RuntimeConnectionStatus.error
|
||||
: rawStatus;
|
||||
@ -77,11 +69,13 @@ AssistantThreadConnectionState resolveGatewayThreadConnectionStateInternal({
|
||||
executionTarget: target,
|
||||
status: status,
|
||||
primaryLabel: primaryLabel,
|
||||
detailLabel: targetAddress,
|
||||
detailLabel: bridgeAddress.isEmpty
|
||||
? appText('xworkmate-bridge 未连接', 'xworkmate-bridge is not connected')
|
||||
: bridgeAddress,
|
||||
ready: status == RuntimeConnectionStatus.connected,
|
||||
pairingRequired: pairingRequired,
|
||||
gatewayTokenMissing: gatewayTokenMissing,
|
||||
lastError: matchesTarget ? connection.lastError?.trim() : null,
|
||||
lastError: connection.lastError?.trim(),
|
||||
);
|
||||
}
|
||||
|
||||
@ -303,6 +297,12 @@ extension AppControllerDesktopThreadSessions on AppController {
|
||||
AssistantExecutionTarget.singleAgent) {
|
||||
return false;
|
||||
}
|
||||
if (resolveExternalAcpEndpointForTargetInternal(
|
||||
AssistantExecutionTarget.singleAgent,
|
||||
) ==
|
||||
null) {
|
||||
return false;
|
||||
}
|
||||
return bridgeProviderCatalog.isEmpty;
|
||||
}
|
||||
|
||||
@ -317,6 +317,12 @@ extension AppControllerDesktopThreadSessions on AppController {
|
||||
AssistantExecutionTarget.singleAgent) {
|
||||
return false;
|
||||
}
|
||||
if (resolveExternalAcpEndpointForTargetInternal(
|
||||
AssistantExecutionTarget.singleAgent,
|
||||
) ==
|
||||
null) {
|
||||
return false;
|
||||
}
|
||||
final selection = singleAgentProviderForSession(normalizedSessionKey);
|
||||
if (selection.isUnspecified) {
|
||||
return false;
|
||||
@ -461,7 +467,6 @@ extension AppControllerDesktopThreadSessions on AppController {
|
||||
return resolveGatewayThreadConnectionStateInternal(
|
||||
target: target,
|
||||
connection: connection,
|
||||
targetProfile: gatewayProfileForAssistantExecutionTargetInternal(target),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -527,8 +527,8 @@ class AssistantEmptyStateInternal extends StatelessWidget {
|
||||
: connected
|
||||
? appText('开始对话或运行任务', 'Start a chat or run a task')
|
||||
: connectionState.status == RuntimeConnectionStatus.error
|
||||
? appText('Gateway 连接失败', 'Gateway connection failed')
|
||||
: appText('先连接 Gateway', 'Connect a gateway first');
|
||||
? appText('Bridge 连接失败', 'Bridge connection failed')
|
||||
: appText('先连接 Bridge', 'Connect xworkmate-bridge first');
|
||||
final description = singleAgent
|
||||
? connected
|
||||
? appText(
|
||||
@ -566,8 +566,8 @@ class AssistantEmptyStateInternal extends StatelessWidget {
|
||||
)
|
||||
: !connected
|
||||
? appText(
|
||||
'当前线程目标网关尚未连接。请先连接对应 Gateway,再继续当前任务。',
|
||||
'The selected gateway target for this thread is not connected yet. Connect that Gateway first, then continue this task.',
|
||||
'当前 xworkmate-bridge 尚未连接。请先恢复 bridge 连接,再继续当前任务。',
|
||||
'xworkmate-bridge is not connected yet. Restore the bridge connection, then continue this task.',
|
||||
)
|
||||
: (connectionState.lastError?.trim().isNotEmpty == true
|
||||
? connectionState.lastError!.trim()
|
||||
@ -627,8 +627,11 @@ class AssistantEmptyStateInternal extends StatelessWidget {
|
||||
: singleAgent
|
||||
? appText('查看线程工具栏', 'Open toolbar')
|
||||
: reconnectAvailable
|
||||
? appText('重新连接', 'Reconnect')
|
||||
: appText('连接 Gateway', 'Connect gateway'),
|
||||
? appText('重新连接 Bridge', 'Reconnect bridge')
|
||||
: appText(
|
||||
'连接 Bridge',
|
||||
'Connect xworkmate-bridge',
|
||||
),
|
||||
),
|
||||
style: FilledButton.styleFrom(
|
||||
minimumSize: const Size(0, 28),
|
||||
@ -641,32 +644,6 @@ class AssistantEmptyStateInternal extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!connected && !singleAgent)
|
||||
OutlinedButton.icon(
|
||||
onPressed: singleAgent
|
||||
? onOpenAiGatewaySettings
|
||||
: onOpenGateway,
|
||||
icon: Icon(
|
||||
singleAgent
|
||||
? Icons.hub_outlined
|
||||
: Icons.settings_rounded,
|
||||
),
|
||||
label: Text(
|
||||
singleAgent
|
||||
? appText('打开设置中心', 'Open settings')
|
||||
: appText('编辑连接', 'Edit connection'),
|
||||
),
|
||||
style: OutlinedButton.styleFrom(
|
||||
minimumSize: const Size(0, 28),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 6,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
@ -76,8 +76,8 @@ class McpServerPage extends StatelessWidget {
|
||||
'No MCP servers connected.',
|
||||
)
|
||||
: appText(
|
||||
'连接 Gateway 后可查看 MCP 服务器。',
|
||||
'Connect a gateway to view MCP servers.',
|
||||
'恢复 xworkmate-bridge 连接后可查看 MCP 服务器。',
|
||||
'MCP servers are visible again after xworkmate-bridge reconnects.',
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@ -203,8 +203,8 @@ class MobileShellStateInternal extends State<MobileShell> {
|
||||
SnackBar(
|
||||
content: Text(
|
||||
appText(
|
||||
'已写入配置码并开始连接 Gateway。',
|
||||
'Setup code applied and Gateway connection started.',
|
||||
'已写入配置码并开始连接 xworkmate-bridge。',
|
||||
'Setup code applied and xworkmate-bridge connection started.',
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@ -245,8 +245,8 @@ class MobileSafeSheetInternal extends StatelessWidget {
|
||||
if (!controller.runtime.isConnected)
|
||||
Text(
|
||||
appText(
|
||||
'连接 Gateway 后加载待审批设备与已配对设备。',
|
||||
'Connect the gateway to load pending and paired devices.',
|
||||
'恢复 xworkmate-bridge 连接后加载待审批设备与已配对设备。',
|
||||
'Pending and paired devices load again after xworkmate-bridge reconnects.',
|
||||
),
|
||||
style: theme.textTheme.bodyMedium,
|
||||
)
|
||||
@ -279,8 +279,8 @@ class MobileSafeSheetInternal extends StatelessWidget {
|
||||
if (!controller.runtime.isConnected)
|
||||
Text(
|
||||
appText(
|
||||
'连接 Gateway 后可查看 paired device,并在移动端直接吊销。',
|
||||
'Connect the gateway to view paired devices and revoke them from mobile.',
|
||||
'恢复 xworkmate-bridge 连接后可查看 paired device,并在移动端直接吊销。',
|
||||
'Paired devices are visible again after xworkmate-bridge reconnects, and can be revoked from mobile.',
|
||||
),
|
||||
style: theme.textTheme.bodyMedium,
|
||||
)
|
||||
|
||||
@ -92,7 +92,7 @@ class MobileWorkspaceLauncherInternal extends StatelessWidget {
|
||||
),
|
||||
primaryLabel: connection.status == RuntimeConnectionStatus.connected
|
||||
? appText('查看连接', 'Connection')
|
||||
: appText('连接 Gateway', 'Connect Gateway'),
|
||||
: appText('连接 Bridge', 'Connect Bridge'),
|
||||
secondaryLabel: appText('返回助手', 'Open Assistant'),
|
||||
onPrimaryPressed: onOpenGatewayConnect,
|
||||
onSecondaryPressed: () =>
|
||||
|
||||
@ -259,8 +259,8 @@ class _NodesPanel extends StatelessWidget {
|
||||
controller.connection.status == RuntimeConnectionStatus.connected
|
||||
? appText('暂时还没有上报在线实例。', 'No live instances reported yet.')
|
||||
: appText(
|
||||
'连接 Gateway 后可加载实例与在线状态。',
|
||||
'Connect a gateway to load instances / presence.',
|
||||
'恢复 xworkmate-bridge 连接后可加载实例与在线状态。',
|
||||
'Instances and presence return after xworkmate-bridge reconnects.',
|
||||
),
|
||||
),
|
||||
)
|
||||
@ -366,8 +366,8 @@ class _AgentsPanel extends StatelessWidget {
|
||||
'No agents reported by the gateway.',
|
||||
)
|
||||
: appText(
|
||||
'连接 Gateway 后可加载代理。',
|
||||
'Connect a gateway to load agents.',
|
||||
'恢复 xworkmate-bridge 连接后可加载代理。',
|
||||
'Agents return after xworkmate-bridge reconnects.',
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -539,8 +539,8 @@ class _SkillsPanel extends StatelessWidget {
|
||||
'No skills loaded for the active gateway / agent.',
|
||||
)
|
||||
: appText(
|
||||
'连接 Gateway 后可加载技能。',
|
||||
'Connect a gateway to load skills.',
|
||||
'恢复 xworkmate-bridge 连接后可加载技能。',
|
||||
'Skills return after xworkmate-bridge reconnects.',
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@ -20,11 +20,6 @@ class ExternalCodeAgentAcpDesktopTransport
|
||||
@visibleForTesting
|
||||
GatewayAcpClient get clientForTest => _client;
|
||||
|
||||
@override
|
||||
Future<void> syncExternalProviders(
|
||||
List<ExternalCodeAgentAcpSyncedProvider> providers,
|
||||
) async {}
|
||||
|
||||
@override
|
||||
Future<ExternalCodeAgentAcpCapabilities> loadExternalAcpCapabilities({
|
||||
required AssistantExecutionTarget target,
|
||||
|
||||
@ -76,32 +76,6 @@ class ExternalCodeAgentAcpRoutingResolution {
|
||||
raw['unavailableMessage']?.toString().trim() ?? '';
|
||||
}
|
||||
|
||||
class ExternalCodeAgentAcpSyncedProvider {
|
||||
const ExternalCodeAgentAcpSyncedProvider({
|
||||
required this.providerId,
|
||||
required this.label,
|
||||
required this.endpoint,
|
||||
required this.authorizationHeader,
|
||||
required this.enabled,
|
||||
});
|
||||
|
||||
final String providerId;
|
||||
final String label;
|
||||
final String endpoint;
|
||||
final String authorizationHeader;
|
||||
final bool enabled;
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return <String, dynamic>{
|
||||
'providerId': providerId.trim(),
|
||||
'label': label.trim(),
|
||||
'endpoint': endpoint.trim(),
|
||||
'authorizationHeader': authorizationHeader.trim(),
|
||||
'enabled': enabled,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
enum ExternalCodeAgentAcpRoutingMode { auto, explicit }
|
||||
|
||||
class ExternalCodeAgentAcpAvailableSkill {
|
||||
@ -604,10 +578,6 @@ String? goTaskServiceGatewayEntryState({
|
||||
}
|
||||
|
||||
abstract class ExternalCodeAgentAcpTransport {
|
||||
Future<void> syncExternalProviders(
|
||||
List<ExternalCodeAgentAcpSyncedProvider> providers,
|
||||
);
|
||||
|
||||
Future<ExternalCodeAgentAcpCapabilities> loadExternalAcpCapabilities({
|
||||
required AssistantExecutionTarget target,
|
||||
bool forceRefresh = false,
|
||||
@ -640,10 +610,6 @@ abstract class ExternalCodeAgentAcpTransport {
|
||||
}
|
||||
|
||||
abstract class GoTaskServiceClient {
|
||||
Future<void> syncExternalProviders(
|
||||
List<ExternalCodeAgentAcpSyncedProvider> providers,
|
||||
);
|
||||
|
||||
Future<ExternalCodeAgentAcpCapabilities> loadExternalAcpCapabilities({
|
||||
required AssistantExecutionTarget target,
|
||||
bool forceRefresh = false,
|
||||
|
||||
@ -11,11 +11,6 @@ class DesktopGoTaskService implements GoTaskServiceClient {
|
||||
|
||||
final ExternalCodeAgentAcpTransport _acpTransport;
|
||||
|
||||
@override
|
||||
Future<void> syncExternalProviders(
|
||||
List<ExternalCodeAgentAcpSyncedProvider> providers,
|
||||
) => _acpTransport.syncExternalProviders(providers);
|
||||
|
||||
@override
|
||||
Future<ExternalCodeAgentAcpCapabilities> loadExternalAcpCapabilities({
|
||||
required AssistantExecutionTarget target,
|
||||
|
||||
@ -61,8 +61,8 @@ class TasksFocusPreviewInternal extends StatelessWidget {
|
||||
RuntimeConnectionStatus.connected
|
||||
? appText('当前没有任务摘要。', 'No task summary yet.')
|
||||
: appText(
|
||||
'连接 Gateway 后这里会显示任务摘要。',
|
||||
'Connect a gateway to load task summaries.',
|
||||
'恢复 xworkmate-bridge 连接后这里会显示任务摘要。',
|
||||
'Task summaries appear here after xworkmate-bridge reconnects.',
|
||||
),
|
||||
)
|
||||
else
|
||||
@ -112,12 +112,23 @@ class SkillsFocusPreviewInternal extends StatelessWidget {
|
||||
.toList(growable: false)
|
||||
: typedController.skills.take(4).toList(growable: false);
|
||||
if (items.isEmpty) {
|
||||
final bridgeEndpointMissing =
|
||||
typedController.isSingleAgentMode &&
|
||||
typedController.resolveExternalAcpEndpointForTargetInternal(
|
||||
AssistantExecutionTarget.singleAgent,
|
||||
) ==
|
||||
null;
|
||||
return PreviewEmptyStateInternal(
|
||||
message: typedController.isSingleAgentMode
|
||||
? (typedController.currentSingleAgentNeedsBridgeProvider
|
||||
? appText(
|
||||
'当前没有可用的 Bridge Provider,请先在设置里配置并同步连接。',
|
||||
'No bridge provider is available. Configure and sync a connection in Settings first.',
|
||||
'Bridge 当前没有广告可用 Provider。恢复后这里会显示线程自己的技能摘要。',
|
||||
'The bridge is not advertising any available providers right now. Thread-owned skill summaries will appear here after it recovers.',
|
||||
)
|
||||
: bridgeEndpointMissing
|
||||
? appText(
|
||||
'Bridge Server 当前不可用。恢复后这里会显示线程自己的技能摘要。',
|
||||
'The bridge server is currently unavailable. Thread-owned skill summaries will appear here after it recovers.',
|
||||
)
|
||||
: appText(
|
||||
'当前线程还没有已加载技能。切换 provider 后会读取该线程自己的 skills 列表。',
|
||||
@ -130,8 +141,8 @@ class SkillsFocusPreviewInternal extends StatelessWidget {
|
||||
'No skills are loaded for the active agent.',
|
||||
)
|
||||
: appText(
|
||||
'连接 Gateway 后可查看技能摘要。',
|
||||
'Connect a gateway to inspect skills here.',
|
||||
'恢复 xworkmate-bridge 连接后可查看技能摘要。',
|
||||
'Skill summaries are available again after xworkmate-bridge reconnects.',
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -236,8 +247,8 @@ class McpFocusPreviewInternal extends StatelessWidget {
|
||||
if (items.isEmpty) {
|
||||
return PreviewEmptyStateInternal(
|
||||
message: appText(
|
||||
'当前没有 MCP 连接器。连接 Gateway 后这里会显示工具摘要。',
|
||||
'No MCP connectors yet. Connect a gateway to load tool summaries here.',
|
||||
'当前没有 MCP 连接器。恢复 xworkmate-bridge 连接后这里会显示工具摘要。',
|
||||
'No MCP connectors yet. Tool summaries appear here after xworkmate-bridge reconnects.',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -215,7 +215,7 @@ void main() {
|
||||
);
|
||||
expect(
|
||||
controller.singleAgentResolvedProviderForSession(sessionKey),
|
||||
SingleAgentProvider.codex,
|
||||
isNull,
|
||||
);
|
||||
|
||||
await controller.refreshSingleAgentSkillsForSession(sessionKey);
|
||||
@ -281,7 +281,7 @@ void main() {
|
||||
controller.singleAgentResolvedProviderForSession(
|
||||
'draft:bridge-default',
|
||||
),
|
||||
SingleAgentProvider.codex,
|
||||
isNull,
|
||||
);
|
||||
|
||||
final thread = controller.taskThreadForSessionInternal(
|
||||
@ -523,8 +523,4 @@ class _FakeGoTaskServiceClient implements GoTaskServiceClient {
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> syncExternalProviders(
|
||||
List<ExternalCodeAgentAcpSyncedProvider> providers,
|
||||
) async {}
|
||||
}
|
||||
|
||||
@ -170,13 +170,7 @@ void main() {
|
||||
});
|
||||
|
||||
group('resolveGatewayThreadConnectionStateInternal', () {
|
||||
test('uses the thread target profile as the only address source', () {
|
||||
final targetProfile = GatewayConnectionProfile.defaults().copyWith(
|
||||
mode: RuntimeConnectionMode.remote,
|
||||
host: 'bridge.example.internal',
|
||||
port: 443,
|
||||
tls: true,
|
||||
);
|
||||
test('uses the current bridge connection address as the only address source', () {
|
||||
final state = resolveGatewayThreadConnectionStateInternal(
|
||||
target: AssistantExecutionTarget.gateway,
|
||||
connection:
|
||||
@ -184,9 +178,8 @@ void main() {
|
||||
mode: RuntimeConnectionMode.remote,
|
||||
).copyWith(
|
||||
status: RuntimeConnectionStatus.connected,
|
||||
remoteAddress: 'legacy-loopback:18789',
|
||||
remoteAddress: 'bridge.example.internal:443',
|
||||
),
|
||||
targetProfile: targetProfile,
|
||||
);
|
||||
|
||||
expect(state.status, RuntimeConnectionStatus.connected);
|
||||
@ -194,13 +187,7 @@ void main() {
|
||||
expect(state.ready, isTrue);
|
||||
});
|
||||
|
||||
test('marks mismatched local snapshot as offline for remote threads', () {
|
||||
final targetProfile = GatewayConnectionProfile.defaults().copyWith(
|
||||
mode: RuntimeConnectionMode.remote,
|
||||
host: 'bridge.example.internal',
|
||||
port: 443,
|
||||
tls: true,
|
||||
);
|
||||
test('uses current bridge snapshot even when the connection was established locally before', () {
|
||||
final state = resolveGatewayThreadConnectionStateInternal(
|
||||
target: AssistantExecutionTarget.gateway,
|
||||
connection:
|
||||
@ -210,19 +197,17 @@ void main() {
|
||||
status: RuntimeConnectionStatus.connected,
|
||||
remoteAddress: 'legacy-loopback:18789',
|
||||
),
|
||||
targetProfile: targetProfile,
|
||||
);
|
||||
|
||||
expect(state.status, RuntimeConnectionStatus.offline);
|
||||
expect(state.detailLabel, 'bridge.example.internal:443');
|
||||
expect(state.ready, isFalse);
|
||||
expect(state.lastError, isNull);
|
||||
expect(state.status, RuntimeConnectionStatus.connected);
|
||||
expect(state.detailLabel, 'legacy-loopback:18789');
|
||||
expect(state.ready, isTrue);
|
||||
});
|
||||
});
|
||||
|
||||
group('assistantConnectionStateForSession', () {
|
||||
test(
|
||||
'uses target profile address instead of connection snapshot address',
|
||||
'uses bridge connection address instead of thread target profile address',
|
||||
() {
|
||||
final gateway = _FakeGatewayRuntime(
|
||||
GatewayConnectionSnapshot.initial(
|
||||
@ -257,7 +242,7 @@ void main() {
|
||||
final state = controller.assistantConnectionStateForSession(sessionKey);
|
||||
|
||||
expect(state.status, RuntimeConnectionStatus.connected);
|
||||
expect(state.detailLabel, '未连接目标');
|
||||
expect(state.detailLabel, 'legacy-loopback:18789');
|
||||
expect(state.ready, isTrue);
|
||||
},
|
||||
);
|
||||
|
||||
@ -27,28 +27,13 @@ void main() {
|
||||
supportRootPathResolver: () async => root.path,
|
||||
);
|
||||
await store.initialize();
|
||||
await store.saveSettingsSnapshot(
|
||||
SettingsSnapshot.defaults().copyWith(
|
||||
acpBridgeServerModeConfig: SettingsSnapshot.defaults()
|
||||
.acpBridgeServerModeConfig
|
||||
.copyWith(
|
||||
cloudSynced: SettingsSnapshot.defaults()
|
||||
.acpBridgeServerModeConfig
|
||||
.cloudSynced
|
||||
.copyWith(
|
||||
remoteServerSummary:
|
||||
const AcpBridgeServerRemoteServerSummary(
|
||||
endpoint: 'https://bridge.customer.example',
|
||||
hasAdvancedOverrides: false,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
final client = _CapturingGoTaskServiceClient();
|
||||
final controller = AppController(
|
||||
store: store,
|
||||
goTaskServiceClient: client,
|
||||
environmentOverride: const <String, String>{
|
||||
'BRIDGE_SERVER_URL': 'https://bridge.customer.example/acp',
|
||||
},
|
||||
);
|
||||
_seedBridgeProviders(controller, const <SingleAgentProvider>[
|
||||
SingleAgentProvider.codex,
|
||||
@ -138,6 +123,8 @@ void main() {
|
||||
);
|
||||
await controller.switchSession(sessionKey);
|
||||
|
||||
expect(controller.currentSingleAgentNeedsBridgeProvider, isFalse);
|
||||
|
||||
await controller.sendChatMessage('first turn');
|
||||
|
||||
expect(client.requests, isEmpty);
|
||||
@ -151,7 +138,7 @@ void main() {
|
||||
);
|
||||
|
||||
test(
|
||||
'single-agent turns stop before routing when bridge has no advertised provider',
|
||||
'single-agent turns still dispatch when bridge routing resolves a provider even if the local catalog is empty',
|
||||
() async {
|
||||
final root = await Directory.systemTemp.createTemp(
|
||||
'xworkmate-missing-bridge-provider-',
|
||||
@ -163,28 +150,13 @@ void main() {
|
||||
supportRootPathResolver: () async => root.path,
|
||||
);
|
||||
await store.initialize();
|
||||
await store.saveSettingsSnapshot(
|
||||
SettingsSnapshot.defaults().copyWith(
|
||||
acpBridgeServerModeConfig: SettingsSnapshot.defaults()
|
||||
.acpBridgeServerModeConfig
|
||||
.copyWith(
|
||||
cloudSynced: SettingsSnapshot.defaults()
|
||||
.acpBridgeServerModeConfig
|
||||
.cloudSynced
|
||||
.copyWith(
|
||||
remoteServerSummary:
|
||||
const AcpBridgeServerRemoteServerSummary(
|
||||
endpoint: 'https://bridge.customer.example',
|
||||
hasAdvancedOverrides: false,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
final client = _CapturingGoTaskServiceClient();
|
||||
final controller = AppController(
|
||||
store: store,
|
||||
goTaskServiceClient: client,
|
||||
environmentOverride: const <String, String>{
|
||||
'BRIDGE_SERVER_URL': 'https://bridge.customer.example/acp',
|
||||
},
|
||||
);
|
||||
addTearDown(() async {
|
||||
controller.dispose();
|
||||
@ -202,18 +174,19 @@ void main() {
|
||||
await controller.switchSession(sessionKey);
|
||||
_seedBridgeProviders(controller, const <SingleAgentProvider>[]);
|
||||
|
||||
expect(controller.currentSingleAgentNeedsBridgeProvider, isTrue);
|
||||
|
||||
await controller.sendChatMessage('first turn');
|
||||
|
||||
expect(client.requests, isEmpty);
|
||||
expect(client.requests, hasLength(1));
|
||||
expect(
|
||||
client.resolveExternalAcpRoutingCallCount,
|
||||
0,
|
||||
1,
|
||||
reason:
|
||||
'single-agent turns should not call routing.resolve when bridge provider state is already unavailable in app state',
|
||||
'single-agent turns should trust bridge routing.resolve instead of short-circuiting on the app-side provider cache',
|
||||
);
|
||||
expect(
|
||||
controller.chatMessages.last.text,
|
||||
isNot('Bridge 当前没有可用 Provider。'),
|
||||
);
|
||||
expect(controller.chatMessages.last.text, 'Bridge 当前没有可用 Provider。');
|
||||
},
|
||||
);
|
||||
|
||||
@ -386,8 +359,4 @@ class _CapturingGoTaskServiceClient implements GoTaskServiceClient {
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> syncExternalProviders(
|
||||
List<ExternalCodeAgentAcpSyncedProvider> providers,
|
||||
) async {}
|
||||
}
|
||||
|
||||
@ -14,6 +14,7 @@ import 'package:xworkmate/runtime/runtime_models.dart';
|
||||
import 'package:xworkmate/runtime/secure_config_store.dart';
|
||||
import 'package:xworkmate/runtime/skill_directory_access.dart';
|
||||
import 'package:xworkmate/theme/app_theme.dart';
|
||||
import 'package:xworkmate/widgets/assistant_focus_panel_previews.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
@ -302,12 +303,121 @@ void main() {
|
||||
await tester.pump();
|
||||
|
||||
expect(find.textContaining('设置 -> 集成'), findsNothing);
|
||||
expect(find.textContaining('本地集成配置'), findsOneWidget);
|
||||
expect(find.textContaining('等待 Bridge 就绪'), findsOneWidget);
|
||||
expect(find.textContaining('Bridge Provider 尚未就绪'), findsOneWidget);
|
||||
expect(find.textContaining('本地集成配置'), findsNothing);
|
||||
expect(find.text('打开配置中心'), findsNothing);
|
||||
expect(find.text('打开设置中心'), findsNothing);
|
||||
expect(find.text('查看线程工具栏'), findsOneWidget);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'single-agent skills focus preview describes bridge recovery instead of settings sync when endpoint is missing',
|
||||
(tester) async {
|
||||
final root = Directory.systemTemp.createTempSync(
|
||||
'xworkmate-focus-preview-widget-test-',
|
||||
);
|
||||
final store = SecureConfigStore(
|
||||
enableSecureStorage: false,
|
||||
appDataRootPathResolver: () async => '${root.path}/settings.sqlite3',
|
||||
secretRootPathResolver: () async => root.path,
|
||||
supportRootPathResolver: () async => root.path,
|
||||
);
|
||||
final controller = AppController(
|
||||
store: store,
|
||||
desktopPlatformService: UnsupportedDesktopPlatformService(),
|
||||
skillDirectoryAccessService: _FakeSkillDirectoryAccessService(
|
||||
root.path,
|
||||
),
|
||||
goTaskServiceClient: const _FakeGoTaskServiceClient(),
|
||||
singleAgentSharedSkillScanRootOverrides: const <String>[],
|
||||
);
|
||||
addTearDown(() async {
|
||||
controller.dispose();
|
||||
if (root.existsSync()) {
|
||||
await root.delete(recursive: true);
|
||||
}
|
||||
});
|
||||
|
||||
controller.initializeAssistantThreadContext(
|
||||
controller.currentSessionKey,
|
||||
executionTarget: AssistantExecutionTarget.singleAgent,
|
||||
singleAgentProvider: SingleAgentProvider.codex,
|
||||
);
|
||||
controller.bridgeProviderCatalogInternal = const <SingleAgentProvider>[];
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: AppTheme.light(platform: TargetPlatform.macOS),
|
||||
home: Scaffold(
|
||||
body: SkillsFocusPreviewInternal(controller: controller),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
expect(find.textContaining('Bridge Server 当前不可用'), findsOneWidget);
|
||||
expect(find.textContaining('设置里配置并同步连接'), findsNothing);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'gateway empty state only asks for bridge connectivity and removes edit-connection affordance',
|
||||
(tester) async {
|
||||
final root = Directory.systemTemp.createTempSync(
|
||||
'xworkmate-gateway-empty-state-widget-test-',
|
||||
);
|
||||
final store = SecureConfigStore(
|
||||
enableSecureStorage: false,
|
||||
appDataRootPathResolver: () async => '${root.path}/settings.sqlite3',
|
||||
secretRootPathResolver: () async => root.path,
|
||||
supportRootPathResolver: () async => root.path,
|
||||
);
|
||||
final controller = AppController(
|
||||
store: store,
|
||||
desktopPlatformService: UnsupportedDesktopPlatformService(),
|
||||
skillDirectoryAccessService: _FakeSkillDirectoryAccessService(
|
||||
root.path,
|
||||
),
|
||||
goTaskServiceClient: const _FakeGoTaskServiceClient(),
|
||||
singleAgentSharedSkillScanRootOverrides: const <String>[],
|
||||
);
|
||||
addTearDown(() async {
|
||||
controller.dispose();
|
||||
if (root.existsSync()) {
|
||||
await root.delete(recursive: true);
|
||||
}
|
||||
});
|
||||
|
||||
controller.initializeAssistantThreadContext(
|
||||
controller.currentSessionKey,
|
||||
executionTarget: AssistantExecutionTarget.gateway,
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: AppTheme.light(platform: TargetPlatform.macOS),
|
||||
home: Scaffold(
|
||||
body: AssistantEmptyStateInternal(
|
||||
controller: controller,
|
||||
onFocusComposer: () {},
|
||||
onOpenGateway: () {},
|
||||
onOpenAiGatewaySettings: () {},
|
||||
onReconnectGateway: () async {},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
expect(find.textContaining('先连接 Bridge'), findsOneWidget);
|
||||
expect(find.textContaining('xworkmate-bridge 尚未连接'), findsOneWidget);
|
||||
expect(find.text('连接 Bridge'), findsOneWidget);
|
||||
expect(find.text('编辑连接'), findsNothing);
|
||||
expect(find.text('连接 Gateway'), findsNothing);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _seedBridgeProviders(
|
||||
@ -422,8 +532,4 @@ class _FakeGoTaskServiceClient implements GoTaskServiceClient {
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> syncExternalProviders(
|
||||
List<ExternalCodeAgentAcpSyncedProvider> providers,
|
||||
) async {}
|
||||
}
|
||||
|
||||
38
test/runtime/bridge_copy_cleanup_test.dart
Normal file
38
test/runtime/bridge_copy_cleanup_test.dart
Normal file
@ -0,0 +1,38 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
test('bridge-only UI copy does not regress to legacy gateway connection wording', () {
|
||||
final targets = <String>[
|
||||
'lib/features/assistant/assistant_page_components.dart',
|
||||
'lib/widgets/assistant_focus_panel_previews.dart',
|
||||
'lib/features/mcp_server/mcp_server_page.dart',
|
||||
'lib/features/modules/modules_page.dart',
|
||||
'lib/features/mobile/mobile_shell_sheet.dart',
|
||||
'lib/features/mobile/mobile_shell_core.dart',
|
||||
'lib/features/mobile/mobile_shell_workspace.dart',
|
||||
];
|
||||
|
||||
const forbiddenSnippets = <String>[
|
||||
'连接 Gateway 后',
|
||||
'Connect a gateway',
|
||||
'Connect Gateway',
|
||||
'编辑连接',
|
||||
'当前线程目标网关尚未连接',
|
||||
'Gateway connection failed',
|
||||
'Connect gateway',
|
||||
];
|
||||
|
||||
for (final path in targets) {
|
||||
final source = File(path).readAsStringSync();
|
||||
for (final snippet in forbiddenSnippets) {
|
||||
expect(
|
||||
source.contains(snippet),
|
||||
isFalse,
|
||||
reason: '$path should not contain legacy gateway-only copy: $snippet',
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -84,27 +84,6 @@ void main() {
|
||||
},
|
||||
);
|
||||
|
||||
test('ignores app-side provider sync in bridge-only mode', () async {
|
||||
final client = _FakeGatewayAcpClient();
|
||||
final transport = ExternalCodeAgentAcpDesktopTransport(
|
||||
client: client,
|
||||
endpointResolver: (_) => null,
|
||||
);
|
||||
|
||||
await transport
|
||||
.syncExternalProviders(const <ExternalCodeAgentAcpSyncedProvider>[
|
||||
ExternalCodeAgentAcpSyncedProvider(
|
||||
providerId: 'codex',
|
||||
label: 'Codex',
|
||||
endpoint: 'https://acp-server.svc.plus/codex/acp/rpc',
|
||||
authorizationHeader: '',
|
||||
enabled: true,
|
||||
),
|
||||
]);
|
||||
|
||||
expect(client.methods, isEmpty);
|
||||
});
|
||||
|
||||
test(
|
||||
'uses bridge routing resolve for preflight provider selection',
|
||||
() async {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user