diff --git a/go/go_core/internal/router/router.go b/go/go_core/internal/router/router.go index cf038c29..4d2c7853 100644 --- a/go/go_core/internal/router/router.go +++ b/go/go_core/internal/router/router.go @@ -110,9 +110,6 @@ func resolveExecution(req Request, prefs memory.Preferences) (string, string) { } prompt := normalize(req.Prompt) - if looksComplex(prompt) { - return ExecutionTargetMultiAgent, EndpointTargetSingleAgent - } if looksOnline(prompt) { return ExecutionTargetGatewayChat, normalizeGatewayTarget(req.PreferredGatewayTarget) } @@ -168,13 +165,6 @@ func looksOnline(prompt string) bool { }) } -func looksComplex(prompt string) bool { - return containsAny(prompt, []string{ - "review", "qa", "汇总", "复审", "审阅", "多步骤", "多工序", "multi-step", - "multiple deliverables", "multiple artifacts", "总结并对比", "整理并审查", - }) -} - func containsAny(haystack string, needles []string) bool { for _, needle := range needles { if strings.Contains(haystack, normalize(needle)) { diff --git a/go/go_core/internal/router/router_test.go b/go/go_core/internal/router/router_test.go index ebe9e041..0aa54d89 100644 --- a/go/go_core/internal/router/router_test.go +++ b/go/go_core/internal/router/router_test.go @@ -66,7 +66,7 @@ func TestResolveAutoOnlineTaskToGateway(t *testing.T) { } } -func TestResolveComplexTaskUpgradesToMultiAgent(t *testing.T) { +func TestResolveComplexTaskStaysWithinAutoSingleAgentAndGatewayLanes(t *testing.T) { resolver := Resolver{ SkillFinder: skills.StaticFinder{}, MemoryService: memory.Service{}, @@ -76,7 +76,7 @@ func TestResolveComplexTaskUpgradesToMultiAgent(t *testing.T) { Prompt: "analyze these files, review the output, and summarize multiple deliverables", }) - if result.ResolvedExecutionTarget != ExecutionTargetMultiAgent { - t.Fatalf("expected multi-agent route, got %#v", result) + if result.ResolvedExecutionTarget != ExecutionTargetSingleAgent { + t.Fatalf("expected single-agent route, got %#v", result) } } diff --git a/lib/app/app_controller_desktop_core.dart b/lib/app/app_controller_desktop_core.dart index 6c7d5643..738f8333 100644 --- a/lib/app/app_controller_desktop_core.dart +++ b/lib/app/app_controller_desktop_core.dart @@ -541,7 +541,8 @@ class AppController extends ChangeNotifier { int get activeGatewayProfileIndexInternal { final target = currentAssistantExecutionTarget; - if (target == AssistantExecutionTarget.singleAgent) { + if (target == AssistantExecutionTarget.singleAgent || + target == AssistantExecutionTarget.auto) { return kGatewayRemoteProfileIndex; } return gatewayProfileIndexForExecutionTargetInternal(target); diff --git a/lib/app/app_controller_desktop_runtime_coordination_impl.dart b/lib/app/app_controller_desktop_runtime_coordination_impl.dart index 11d016b0..2b125dae 100644 --- a/lib/app/app_controller_desktop_runtime_coordination_impl.dart +++ b/lib/app/app_controller_desktop_runtime_coordination_impl.dart @@ -264,6 +264,7 @@ GatewayMode bridgeGatewayModeRuntimeInternal(AppController controller) { return GatewayMode.offline; } return switch (controller.currentAssistantExecutionTarget) { + AssistantExecutionTarget.auto => GatewayMode.offline, AssistantExecutionTarget.singleAgent => GatewayMode.offline, AssistantExecutionTarget.local => GatewayMode.local, AssistantExecutionTarget.remote => GatewayMode.remote, diff --git a/lib/app/app_controller_desktop_runtime_helpers.dart b/lib/app/app_controller_desktop_runtime_helpers.dart index de4bd346..b5e05eeb 100644 --- a/lib/app/app_controller_desktop_runtime_helpers.dart +++ b/lib/app/app_controller_desktop_runtime_helpers.dart @@ -683,7 +683,7 @@ extension AppControllerDesktopRuntimeHelpers on AppController { ) { return switch (mode) { RuntimeConnectionMode.unconfigured => - AssistantExecutionTarget.singleAgent, + AssistantExecutionTarget.auto, RuntimeConnectionMode.local => AssistantExecutionTarget.local, RuntimeConnectionMode.remote => AssistantExecutionTarget.remote, }; @@ -693,6 +693,7 @@ extension AppControllerDesktopRuntimeHelpers on AppController { AssistantExecutionTarget target, ) { return switch (target) { + AssistantExecutionTarget.auto => settings.primaryLocalGatewayProfile, AssistantExecutionTarget.local => settings.primaryLocalGatewayProfile, AssistantExecutionTarget.remote => settings.primaryRemoteGatewayProfile, AssistantExecutionTarget.singleAgent => throw StateError( @@ -705,6 +706,7 @@ extension AppControllerDesktopRuntimeHelpers on AppController { AssistantExecutionTarget target, ) { return switch (target) { + AssistantExecutionTarget.auto => kGatewayLocalProfileIndex, AssistantExecutionTarget.local => kGatewayLocalProfileIndex, AssistantExecutionTarget.remote => kGatewayRemoteProfileIndex, AssistantExecutionTarget.singleAgent => throw StateError( diff --git a/lib/app/app_controller_desktop_settings.dart b/lib/app/app_controller_desktop_settings.dart index 5ef3087c..f3e4358e 100644 --- a/lib/app/app_controller_desktop_settings.dart +++ b/lib/app/app_controller_desktop_settings.dart @@ -248,10 +248,22 @@ extension AppControllerDesktopSettings on AppController { defaults.primaryRemoteGatewayProfile.selectedAgentId, ); modelsControllerInternal.restoreFromSettings(defaults.aiGateway); + initializeAssistantThreadContext( + 'main', + executionTarget: defaults.assistantExecutionTarget, + messageViewMode: AssistantMessageViewMode.rendered, + singleAgentProvider: SingleAgentProvider.auto, + ); await setCurrentAssistantSessionKeyInternal( 'main', persistSelection: false, ); + assistantThreadRecordsInternal.removeWhere((key, _) => key != 'main'); + assistantThreadMessagesInternal.removeWhere((key, _) => key != 'main'); + await flushAssistantThreadPersistenceInternal(); + await storeInternal.saveTaskThreads( + assistantThreadRecordsInternal.values.toList(growable: false), + ); chatControllerInternal.clear(); recomputeTasksInternal(); notifyListeners(); diff --git a/lib/app/app_controller_desktop_single_agent.dart b/lib/app/app_controller_desktop_single_agent.dart index 9aea6733..419960f9 100644 --- a/lib/app/app_controller_desktop_single_agent.dart +++ b/lib/app/app_controller_desktop_single_agent.dart @@ -62,6 +62,7 @@ extension AppControllerDesktopSingleAgent on AppController { return; } await enqueueThreadTurnInternal(sessionKey, () async { + final sessionTarget = assistantExecutionTargetForSession(sessionKey); final userText = trimmed.isEmpty ? 'See attached.' : trimmed; appendAssistantThreadMessageInternal( sessionKey, @@ -142,7 +143,9 @@ extension AppControllerDesktopSingleAgent on AppController { } return; } - final effectiveProvider = provider ?? SingleAgentProvider.auto; + final effectiveProvider = sessionTarget == AssistantExecutionTarget.auto + ? SingleAgentProvider.auto + : (provider ?? SingleAgentProvider.auto); appendSingleAgentRuntimeStatusMessageInternal( sessionKey, @@ -178,7 +181,9 @@ extension AppControllerDesktopSingleAgent on AppController { target: AssistantExecutionTarget.singleAgent, prompt: message, workingDirectory: workingDirectory, - model: assistantModelForSession(sessionKey), + model: sessionTarget == AssistantExecutionTarget.auto + ? '' + : assistantModelForSession(sessionKey), thinking: thinking, selectedSkills: selectedSkills, inlineAttachments: attachments, @@ -202,6 +207,21 @@ extension AppControllerDesktopSingleAgent on AppController { singleAgentRuntimeModelBySessionInternal[sessionKey] = resolvedRuntimeModel; } + final resolvedGatewayEntryState = + result.resolvedExecutionTarget == 'gateway-chat' + ? (result.resolvedEndpointTarget.trim().isNotEmpty + ? result.resolvedEndpointTarget.trim() + : AssistantExecutionTarget.local.promptValue) + : result.resolvedExecutionTarget == 'single-agent' + ? AssistantExecutionTarget.singleAgent.promptValue + : (sessionTarget == AssistantExecutionTarget.auto + ? AssistantExecutionTarget.auto.promptValue + : AssistantExecutionTarget.singleAgent.promptValue); + upsertTaskThreadInternal( + sessionKey, + gatewayEntryState: resolvedGatewayEntryState, + updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(), + ); final resolvedWorkspaceKind = result.resolvedWorkspaceRefKind; final resolvedWorkingDirectory = result.resolvedWorkingDirectory.trim(); if (resolvedWorkspaceKind != null && @@ -222,6 +242,11 @@ extension AppControllerDesktopSingleAgent on AppController { sessionKey, result.errorMessage, ); + upsertTaskThreadInternal( + sessionKey, + gatewayEntryState: 'only-chat', + updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(), + ); await sendAiGatewayMessageInternal( message, thinking: thinking, @@ -801,9 +826,11 @@ extension AppControllerDesktopSingleAgent on AppController { sessionKey, ); final thread = assistantThreadRecordsInternal[normalizedSessionKey]; - final preferredGatewayTarget = switch (assistantExecutionTargetForSession( + final sessionTarget = assistantExecutionTargetForSession( normalizedSessionKey, - )) { + ); + final preferredGatewayTarget = switch (sessionTarget) { + AssistantExecutionTarget.auto => 'local', AssistantExecutionTarget.local => 'local', AssistantExecutionTarget.remote => 'remote', AssistantExecutionTarget.singleAgent => @@ -828,6 +855,9 @@ extension AppControllerDesktopSingleAgent on AppController { .toList(growable: false); final resolvedExplicitExecutionTarget = + sessionTarget == AssistantExecutionTarget.auto + ? '' + : explicitExecutionTarget?.trim().isNotEmpty == true ? explicitExecutionTarget!.trim() : (thread?.hasExplicitExecutionTargetSelection ?? false) @@ -836,11 +866,16 @@ extension AppControllerDesktopSingleAgent on AppController { ) : ''; final resolvedExplicitProviderId = + sessionTarget == AssistantExecutionTarget.auto + ? '' + : thread?.hasExplicitProviderSelection ?? false ? singleAgentProviderForSession(normalizedSessionKey).providerId : ''; final resolvedExplicitModel = thread?.hasExplicitModelSelection ?? false - ? assistantModelForSession(normalizedSessionKey) + ? (sessionTarget == AssistantExecutionTarget.auto + ? '' + : assistantModelForSession(normalizedSessionKey)) : ''; final resolvedExplicitSkills = thread?.hasExplicitSkillSelection ?? false ? selectedSkills @@ -872,6 +907,7 @@ extension AppControllerDesktopSingleAgent on AppController { String _routingExecutionTargetValue(AssistantExecutionTarget target) { return switch (target) { + AssistantExecutionTarget.auto => 'singleAgent', AssistantExecutionTarget.singleAgent => 'singleAgent', AssistantExecutionTarget.local => 'local', AssistantExecutionTarget.remote => 'remote', diff --git a/lib/app/app_controller_desktop_skill_permissions.dart b/lib/app/app_controller_desktop_skill_permissions.dart index 0600f999..2d01f718 100644 --- a/lib/app/app_controller_desktop_skill_permissions.dart +++ b/lib/app/app_controller_desktop_skill_permissions.dart @@ -357,6 +357,7 @@ extension AppControllerDesktopSkillPermissions on AppController { )) .copyWith( executionMode: switch (nextExecutionTarget) { + AssistantExecutionTarget.auto => ThreadExecutionMode.auto, AssistantExecutionTarget.singleAgent => ThreadExecutionMode.localAgent, AssistantExecutionTarget.local => diff --git a/lib/app/app_controller_desktop_thread_actions.dart b/lib/app/app_controller_desktop_thread_actions.dart index 1fa98e95..6a82a4a2 100644 --- a/lib/app/app_controller_desktop_thread_actions.dart +++ b/lib/app/app_controller_desktop_thread_actions.dart @@ -79,7 +79,8 @@ extension AppControllerDesktopThreadActions on AppController { Future connectSavedGateway() async { final target = currentAssistantExecutionTarget; - if (target == AssistantExecutionTarget.singleAgent) { + if (target == AssistantExecutionTarget.singleAgent || + target == AssistantExecutionTarget.auto) { return; } await AppControllerDesktopGateway(this).connectProfileInternal( @@ -247,8 +248,9 @@ extension AppControllerDesktopThreadActions on AppController { List selectedSkillLabels = const [], }) async { final currentSessionKey = sessionsControllerInternal.currentSessionKey; - if (assistantExecutionTargetForSession(currentSessionKey) == - AssistantExecutionTarget.singleAgent) { + final currentTarget = assistantExecutionTargetForSession(currentSessionKey); + if (currentTarget == AssistantExecutionTarget.singleAgent || + currentTarget == AssistantExecutionTarget.auto) { await bootstrapThreadWorkspaceFromExecutionContextInternal( currentSessionKey, message, @@ -256,7 +258,7 @@ extension AppControllerDesktopThreadActions on AppController { } await ensureDesktopTaskThreadBindingInternal( currentSessionKey, - executionTarget: assistantExecutionTargetForSession(currentSessionKey), + executionTarget: currentTarget, ); final workspacePath = assistantWorkspacePathForSession( currentSessionKey, @@ -276,7 +278,8 @@ extension AppControllerDesktopThreadActions on AppController { recomputeTasksInternal(); throw error; } - if (isSingleAgentMode) { + if (currentTarget == AssistantExecutionTarget.singleAgent || + currentTarget == AssistantExecutionTarget.auto) { await sendSingleAgentMessageInternal( message, thinking: thinking, @@ -321,7 +324,7 @@ extension AppControllerDesktopThreadActions on AppController { GoAgentCoreSessionRequest( sessionId: sessionKey, threadId: sessionKey, - target: assistantExecutionTargetForSession(sessionKey), + target: currentTarget, prompt: message, workingDirectory: assistantWorkspacePathForSession( sessionKey, diff --git a/lib/app/app_controller_desktop_thread_binding.dart b/lib/app/app_controller_desktop_thread_binding.dart index 15421fa2..9620224a 100644 --- a/lib/app/app_controller_desktop_thread_binding.dart +++ b/lib/app/app_controller_desktop_thread_binding.dart @@ -139,7 +139,8 @@ extension AppControllerDesktopThreadBinding on AppController { required ThreadOwnerScope ownerScope, WorkspaceBinding? existingBinding, }) { - if (executionTarget == AssistantExecutionTarget.singleAgent) { + if (executionTarget == AssistantExecutionTarget.auto || + executionTarget == AssistantExecutionTarget.singleAgent) { if (existingBinding != null && existingBinding.workspacePath.trim().isNotEmpty) { if (existingBinding.workspaceKind == WorkspaceKind.localFs) { @@ -207,10 +208,11 @@ extension AppControllerDesktopThreadBinding on AppController { endpointId: '', )) .copyWith( - executionMode: switch (executionTarget) { - AssistantExecutionTarget.singleAgent => - ThreadExecutionMode.localAgent, - AssistantExecutionTarget.local => ThreadExecutionMode.gatewayLocal, + executionMode: switch (executionTarget) { + AssistantExecutionTarget.auto => ThreadExecutionMode.auto, + AssistantExecutionTarget.singleAgent => + ThreadExecutionMode.localAgent, + AssistantExecutionTarget.local => ThreadExecutionMode.gatewayLocal, AssistantExecutionTarget.remote => ThreadExecutionMode.gatewayRemote, }, diff --git a/lib/app/app_controller_desktop_thread_sessions.dart b/lib/app/app_controller_desktop_thread_sessions.dart index a2f4d917..bff57027 100644 --- a/lib/app/app_controller_desktop_thread_sessions.dart +++ b/lib/app/app_controller_desktop_thread_sessions.dart @@ -54,8 +54,9 @@ extension AppControllerDesktopThreadSessions on AppController { final normalizedSessionKey = normalizedAssistantSessionKeyInternal( sessionKey, ); - if (assistantExecutionTargetForSession(normalizedSessionKey) == - AssistantExecutionTarget.singleAgent) { + final target = assistantExecutionTargetForSession(normalizedSessionKey); + if (target == AssistantExecutionTarget.singleAgent || + target == AssistantExecutionTarget.auto) { return assistantImportedSkillsForSession(normalizedSessionKey).length; } return skills.length; @@ -96,7 +97,8 @@ extension AppControllerDesktopThreadSessions on AppController { sessionKey, ); final target = assistantExecutionTargetForSession(normalizedSessionKey); - if (target == AssistantExecutionTarget.singleAgent) { + if (target == AssistantExecutionTarget.singleAgent || + target == AssistantExecutionTarget.auto) { if (singleAgentUsesAiChatFallbackForSession(normalizedSessionKey)) { final recordModel = assistantThreadRecordsInternal[normalizedSessionKey] @@ -137,8 +139,9 @@ extension AppControllerDesktopThreadSessions on AppController { if (existing.isNotEmpty) { return existing; } - if (assistantExecutionTargetForSession(normalizedSessionKey) == - AssistantExecutionTarget.singleAgent) { + final target = assistantExecutionTargetForSession(normalizedSessionKey); + if (target == AssistantExecutionTarget.singleAgent || + target == AssistantExecutionTarget.auto) { return localThreadWorkspacePathInternal(normalizedSessionKey); } return ''; @@ -337,10 +340,7 @@ extension AppControllerDesktopThreadSessions on AppController { singleAgentShouldShowModelControlForSession(currentSessionKey); List get singleAgentProviderOptions => - [ - SingleAgentProvider.auto, - ...configuredSingleAgentProviders, - ]; + configuredSingleAgentProviders; String singleAgentProviderLabelForSession(String sessionKey) { return singleAgentProviderForSession(sessionKey).label; @@ -374,7 +374,8 @@ extension AppControllerDesktopThreadSessions on AppController { sessionKey, ); final target = assistantExecutionTargetForSession(normalizedSessionKey); - if (target == AssistantExecutionTarget.singleAgent) { + if (target == AssistantExecutionTarget.singleAgent || + target == AssistantExecutionTarget.auto) { final provider = singleAgentProviderForSession(normalizedSessionKey); final resolvedProvider = singleAgentResolvedProviderForSession( normalizedSessionKey, @@ -409,15 +410,21 @@ extension AppControllerDesktopThreadSessions on AppController { '当前线程的外部 Agent ACP 连接尚未就绪。', 'The external Agent ACP connection for this thread is not ready yet.', ); + final primaryLabel = target == AssistantExecutionTarget.auto + ? 'Auto' + : target.label; + final actualDetailPrefix = target == AssistantExecutionTarget.auto + ? appText('当前: ', 'Current: ') + : ''; return AssistantThreadConnectionState( executionTarget: target, status: providerReady || fallbackReady ? RuntimeConnectionStatus.connected : RuntimeConnectionStatus.offline, - primaryLabel: target.label, + primaryLabel: primaryLabel, detailLabel: detail.isEmpty ? appText('未配置单机智能体', 'Single Agent is not configured') - : detail, + : '$actualDetailPrefix$detail', ready: providerReady || fallbackReady, pairingRequired: false, gatewayTokenMissing: false, @@ -585,6 +592,7 @@ extension AppControllerDesktopThreadSessions on AppController { AssistantExecutionTarget target, ) { return switch (target) { + AssistantExecutionTarget.auto => WorkspaceRefKind.localPath, AssistantExecutionTarget.singleAgent => WorkspaceRefKind.localPath, AssistantExecutionTarget.local || AssistantExecutionTarget.remote => WorkspaceRefKind.remotePath, diff --git a/lib/app/app_controller_desktop_thread_sessions_collaboration_impl.dart b/lib/app/app_controller_desktop_thread_sessions_collaboration_impl.dart index 6b2fe424..c7cbd49e 100644 --- a/lib/app/app_controller_desktop_thread_sessions_collaboration_impl.dart +++ b/lib/app/app_controller_desktop_thread_sessions_collaboration_impl.dart @@ -393,6 +393,7 @@ bool canQuickConnectGatewayThreadSessionInternal(AppController controller) { return true; } final defaults = switch (target) { + AssistantExecutionTarget.auto => GatewayConnectionProfile.defaultsLocal(), AssistantExecutionTarget.singleAgent => GatewayConnectionProfile.emptySlot( index: kGatewayRemoteProfileIndex, ), diff --git a/lib/app/app_controller_desktop_workspace_execution.dart b/lib/app/app_controller_desktop_workspace_execution.dart index 587afccc..44a0b5e7 100644 --- a/lib/app/app_controller_desktop_workspace_execution.dart +++ b/lib/app/app_controller_desktop_workspace_execution.dart @@ -77,7 +77,8 @@ extension AppControllerDesktopWorkspaceExecution on AppController { sessionKey: sessionsControllerInternal.currentSessionKey, persistDefaultSelection: true, ); - if (resolvedTarget == AssistantExecutionTarget.singleAgent) { + if (resolvedTarget == AssistantExecutionTarget.singleAgent || + resolvedTarget == AssistantExecutionTarget.auto) { await refreshSingleAgentSkillsForSession( sessionsControllerInternal.currentSessionKey, ); @@ -165,7 +166,8 @@ extension AppControllerDesktopWorkspaceExecution on AppController { ); } - if (resolvedTarget == AssistantExecutionTarget.singleAgent) { + if (resolvedTarget == AssistantExecutionTarget.singleAgent || + resolvedTarget == AssistantExecutionTarget.auto) { if (runtimeInternal.isConnected) { preserveGatewayHistoryForSessionInternal(normalizedSessionKey); } diff --git a/lib/app/app_controller_web_gateway_chat.dart b/lib/app/app_controller_web_gateway_chat.dart index 35688da7..b045b90d 100644 --- a/lib/app/app_controller_web_gateway_chat.dart +++ b/lib/app/app_controller_web_gateway_chat.dart @@ -106,13 +106,20 @@ extension AppControllerWebGatewayChat on AppController { ); return; } - if (target == AssistantExecutionTarget.singleAgent) { + if (target == AssistantExecutionTarget.singleAgent || + target == AssistantExecutionTarget.auto) { await executeGoAgentCoreRunInternal( sessionKey: sessionKey, prompt: trimmed, - target: target, - provider: singleAgentProviderForSession(sessionKey), - model: assistantModelForSession(sessionKey), + target: target == AssistantExecutionTarget.auto + ? AssistantExecutionTarget.singleAgent + : target, + provider: target == AssistantExecutionTarget.auto + ? SingleAgentProvider.auto + : singleAgentProviderForSession(sessionKey), + model: target == AssistantExecutionTarget.auto + ? '' + : assistantModelForSession(sessionKey), thinking: thinking, attachments: attachments, selectedSkillLabels: selectedSkillLabels, @@ -291,9 +298,11 @@ extension AppControllerWebGatewayChat on AppController { }) { final normalizedSessionKey = normalizedSessionKeyInternal(sessionKey); final thread = threadRecordsInternal[normalizedSessionKey]; - final preferredGatewayTarget = switch (assistantExecutionTargetForSession( + final sessionTarget = assistantExecutionTargetForSession( normalizedSessionKey, - )) { + ); + final preferredGatewayTarget = switch (sessionTarget) { + AssistantExecutionTarget.auto => 'local', AssistantExecutionTarget.local => 'local', AssistantExecutionTarget.remote => 'remote', AssistantExecutionTarget.singleAgent => 'remote', @@ -318,6 +327,9 @@ extension AppControllerWebGatewayChat on AppController { .where((item) => item.trim().isNotEmpty) .toList(growable: false); final resolvedExplicitExecutionTarget = + sessionTarget == AssistantExecutionTarget.auto + ? '' + : explicitExecutionTarget?.trim().isNotEmpty == true ? explicitExecutionTarget!.trim() : (thread?.hasExplicitExecutionTargetSelection ?? false) @@ -326,11 +338,16 @@ extension AppControllerWebGatewayChat on AppController { ) : ''; final resolvedExplicitProviderId = + sessionTarget == AssistantExecutionTarget.auto + ? '' + : thread?.hasExplicitProviderSelection ?? false ? singleAgentProviderForSession(normalizedSessionKey).providerId : ''; final resolvedExplicitModel = thread?.hasExplicitModelSelection ?? false - ? assistantModelForSession(normalizedSessionKey) + ? (sessionTarget == AssistantExecutionTarget.auto + ? '' + : assistantModelForSession(normalizedSessionKey)) : ''; final resolvedExplicitSkills = thread?.hasExplicitSkillSelection ?? false ? selectedSkills @@ -362,6 +379,7 @@ extension AppControllerWebGatewayChat on AppController { String _webRoutingExecutionTargetValue(AssistantExecutionTarget target) { return switch (target) { + AssistantExecutionTarget.auto => 'singleAgent', AssistantExecutionTarget.singleAgent => 'singleAgent', AssistantExecutionTarget.local => 'local', AssistantExecutionTarget.remote => 'remote', diff --git a/lib/app/app_controller_web_gateway_relay.dart b/lib/app/app_controller_web_gateway_relay.dart index 0f41495d..d7bf07c3 100644 --- a/lib/app/app_controller_web_gateway_relay.dart +++ b/lib/app/app_controller_web_gateway_relay.dart @@ -128,6 +128,7 @@ extension AppControllerWebGatewayRelay on AppController { existing?.executionBinding ?? ExecutionBinding( executionMode: switch (resolvedExecutionTarget) { + AssistantExecutionTarget.auto => ThreadExecutionMode.auto, AssistantExecutionTarget.singleAgent => ThreadExecutionMode.localAgent, AssistantExecutionTarget.local => diff --git a/lib/app/app_controller_web_helpers.dart b/lib/app/app_controller_web_helpers.dart index cd527bf8..9a7d4747 100644 --- a/lib/app/app_controller_web_helpers.dart +++ b/lib/app/app_controller_web_helpers.dart @@ -125,11 +125,12 @@ extension AppControllerWebHelpers on AppController { AssistantExecutionTarget? target, ) { return switch (target) { + AssistantExecutionTarget.auto => AssistantExecutionTarget.auto, AssistantExecutionTarget.local => AssistantExecutionTarget.local, AssistantExecutionTarget.remote => AssistantExecutionTarget.remote, AssistantExecutionTarget.singleAgent => AssistantExecutionTarget.singleAgent, - _ => AssistantExecutionTarget.singleAgent, + _ => AssistantExecutionTarget.auto, }; } @@ -139,6 +140,7 @@ extension AppControllerWebHelpers on AppController { }) { final timestamp = DateTime.now().millisecondsSinceEpoch; final prefix = switch (target) { + AssistantExecutionTarget.auto => 'auto', AssistantExecutionTarget.singleAgent => 'single', AssistantExecutionTarget.local => 'local', AssistantExecutionTarget.remote => 'remote', @@ -164,6 +166,7 @@ extension AppControllerWebHelpers on AppController { ), executionBinding: ExecutionBinding( executionMode: switch (target) { + AssistantExecutionTarget.auto => ThreadExecutionMode.auto, AssistantExecutionTarget.singleAgent => ThreadExecutionMode.localAgent, AssistantExecutionTarget.local => ThreadExecutionMode.gatewayLocal, @@ -249,6 +252,7 @@ extension AppControllerWebHelpers on AppController { )) .copyWith( executionMode: switch (executionTarget) { + AssistantExecutionTarget.auto => ThreadExecutionMode.auto, AssistantExecutionTarget.singleAgent => ThreadExecutionMode.localAgent, AssistantExecutionTarget.local => ThreadExecutionMode.gatewayLocal, @@ -391,12 +395,13 @@ extension AppControllerWebHelpers on AppController { return switch (mode) { RuntimeConnectionMode.local => AssistantExecutionTarget.local, RuntimeConnectionMode.remote => AssistantExecutionTarget.remote, - RuntimeConnectionMode.unconfigured => AssistantExecutionTarget.remote, + RuntimeConnectionMode.unconfigured => AssistantExecutionTarget.auto, }; } int profileIndexForTargetInternal(AssistantExecutionTarget target) { return switch (target) { + AssistantExecutionTarget.auto => kGatewayLocalProfileIndex, AssistantExecutionTarget.local => kGatewayLocalProfileIndex, AssistantExecutionTarget.remote => kGatewayRemoteProfileIndex, AssistantExecutionTarget.singleAgent => kGatewayRemoteProfileIndex, @@ -407,6 +412,8 @@ extension AppControllerWebHelpers on AppController { AssistantExecutionTarget target, ) { return switch (target) { + AssistantExecutionTarget.auto => + settingsInternal.primaryLocalGatewayProfile, AssistantExecutionTarget.local => settingsInternal.primaryLocalGatewayProfile, AssistantExecutionTarget.remote => diff --git a/lib/app/app_controller_web_sessions.dart b/lib/app/app_controller_web_sessions.dart index b382f068..bf425a44 100644 --- a/lib/app/app_controller_web_sessions.dart +++ b/lib/app/app_controller_web_sessions.dart @@ -34,7 +34,7 @@ extension AppControllerWebSessions on AppController { final fallback = sanitizeTargetInternal( settingsInternal.assistantExecutionTarget, ); - return recordTarget ?? fallback ?? AssistantExecutionTarget.singleAgent; + return recordTarget ?? fallback ?? AssistantExecutionTarget.auto; } AssistantExecutionTarget get assistantExecutionTarget => @@ -42,7 +42,8 @@ extension AppControllerWebSessions on AppController { AssistantExecutionTarget get currentAssistantExecutionTarget => assistantExecutionTarget; bool get isSingleAgentMode => - assistantExecutionTarget == AssistantExecutionTarget.singleAgent; + assistantExecutionTarget == AssistantExecutionTarget.singleAgent || + assistantExecutionTarget == AssistantExecutionTarget.auto; AssistantMessageViewMode assistantMessageViewModeForSession( String sessionKey, @@ -122,10 +123,7 @@ extension AppControllerWebSessions on AppController { singleAgentProviderForSession(currentSessionKeyInternal); List get singleAgentProviderOptions => - [ - SingleAgentProvider.auto, - ...settingsInternal.availableSingleAgentProviders, - ]; + settingsInternal.availableSingleAgentProviders; bool singleAgentUsesAiChatFallbackForSession(String sessionKey) { final provider = singleAgentProviderForSession(sessionKey); @@ -152,7 +150,8 @@ extension AppControllerWebSessions on AppController { final recordModel = threadRecordsInternal[normalizedSessionKey]?.assistantModelId.trim() ?? ''; - if (target == AssistantExecutionTarget.singleAgent) { + if (target == AssistantExecutionTarget.singleAgent || + target == AssistantExecutionTarget.auto) { if (singleAgentUsesAiChatFallbackForSession(normalizedSessionKey)) { if (recordModel.isNotEmpty) { return recordModel; @@ -181,7 +180,8 @@ extension AppControllerWebSessions on AppController { List assistantModelChoicesForSession(String sessionKey) { final target = assistantExecutionTargetForSession(sessionKey); - if (target == AssistantExecutionTarget.singleAgent) { + if (target == AssistantExecutionTarget.singleAgent || + target == AssistantExecutionTarget.auto) { if (singleAgentUsesAiChatFallbackForSession(sessionKey)) { return aiGatewayConversationModelChoices; } @@ -441,7 +441,8 @@ extension AppControllerWebSessions on AppController { ) { final normalizedSessionKey = normalizedSessionKeyInternal(sessionKey); final target = assistantExecutionTargetForSession(normalizedSessionKey); - if (target == AssistantExecutionTarget.singleAgent) { + if (target == AssistantExecutionTarget.singleAgent || + target == AssistantExecutionTarget.auto) { final provider = singleAgentProviderForSession(normalizedSessionKey); final model = assistantModelForSession(normalizedSessionKey); final host = hostLabelInternal(settingsInternal.aiGateway.baseUrl); @@ -452,9 +453,13 @@ extension AppControllerWebSessions on AppController { status: canUseAiGatewayConversation ? RuntimeConnectionStatus.connected : RuntimeConnectionStatus.offline, - primaryLabel: target.label, + primaryLabel: target == AssistantExecutionTarget.auto + ? 'Auto' + : target.label, detailLabel: detail.isEmpty ? appText('单机智能体未配置', 'Single Agent not configured') + : target == AssistantExecutionTarget.auto + ? '${appText('当前: ', 'Current: ')}$detail' : detail, ready: canUseAiGatewayConversation, pairingRequired: false, @@ -473,9 +478,11 @@ extension AppControllerWebSessions on AppController { status: remoteReady ? RuntimeConnectionStatus.connected : RuntimeConnectionStatus.offline, - primaryLabel: target.label, + primaryLabel: target == AssistantExecutionTarget.auto + ? 'Auto' + : target.label, detailLabel: remoteReady - ? joinConnectionPartsInternal([provider.label, model]) + ? '${target == AssistantExecutionTarget.auto ? appText('当前: ', 'Current: ') : ''}${joinConnectionPartsInternal([provider.label, model])}' : appText( '${provider.label} 需要 Remote ACP(${remoteAddress.isEmpty ? 'Remote Gateway' : remoteAddress})', '${provider.label} requires Remote ACP (${remoteAddress.isEmpty ? 'Remote Gateway' : remoteAddress}).', diff --git a/lib/app/ui_feature_manifest_core.dart b/lib/app/ui_feature_manifest_core.dart index 074350bb..e2658c69 100644 --- a/lib/app/ui_feature_manifest_core.dart +++ b/lib/app/ui_feature_manifest_core.dart @@ -521,6 +521,9 @@ class UiFeatureAccess { List get availableExecutionTargets { final targets = []; + if (platform != UiFeaturePlatform.mobile) { + targets.add(AssistantExecutionTarget.auto); + } if (supportsDirectAi) { targets.add(AssistantExecutionTarget.singleAgent); } @@ -542,10 +545,12 @@ class UiFeatureAccess { } final preferredOrder = platform == UiFeaturePlatform.web ? const [ + AssistantExecutionTarget.auto, AssistantExecutionTarget.singleAgent, AssistantExecutionTarget.remote, ] : const [ + AssistantExecutionTarget.auto, AssistantExecutionTarget.local, AssistantExecutionTarget.singleAgent, AssistantExecutionTarget.remote, @@ -556,8 +561,8 @@ class UiFeatureAccess { } } return platform == UiFeaturePlatform.web - ? AssistantExecutionTarget.singleAgent - : AssistantExecutionTarget.local; + ? AssistantExecutionTarget.auto + : AssistantExecutionTarget.auto; } } diff --git a/lib/features/assistant/assistant_page_composer_bar.dart b/lib/features/assistant/assistant_page_composer_bar.dart index a19c2b26..5509fe96 100644 --- a/lib/features/assistant/assistant_page_composer_bar.dart +++ b/lib/features/assistant/assistant_page_composer_bar.dart @@ -442,7 +442,8 @@ class ComposerBarStateInternal extends State { ), ), const SizedBox(width: 4), - if (singleAgent) ...[ + if (singleAgent && + executionTarget != AssistantExecutionTarget.auto) ...[ PopupMenuButton( key: const Key('assistant-single-agent-provider-button'), tooltip: appText('单机智能体执行器', 'Single Agent Provider'), diff --git a/lib/features/assistant/assistant_page_composer_support.dart b/lib/features/assistant/assistant_page_composer_support.dart index 89db6c89..a8adbd6a 100644 --- a/lib/features/assistant/assistant_page_composer_support.dart +++ b/lib/features/assistant/assistant_page_composer_support.dart @@ -195,6 +195,7 @@ class ComposerToolbarChipStateInternal extension AssistantExecutionTargetIconInternal on AssistantExecutionTarget { IconData get icon => switch (this) { + AssistantExecutionTarget.auto => Icons.auto_awesome_rounded, AssistantExecutionTarget.singleAgent => Icons.hub_outlined, AssistantExecutionTarget.local => Icons.computer_outlined, AssistantExecutionTarget.remote => Icons.cloud_outlined, diff --git a/lib/features/assistant/assistant_page_state_closure.dart b/lib/features/assistant/assistant_page_state_closure.dart index a84e9e17..665dbbba 100644 --- a/lib/features/assistant/assistant_page_state_closure.dart +++ b/lib/features/assistant/assistant_page_state_closure.dart @@ -179,10 +179,18 @@ extension AssistantPageStateClosureInternal on AssistantPageStateInternal { inputController: inputControllerInternal, focusNode: composerFocusNodeInternal, thinkingLabel: thinkingLabelInternal, - showModelControl: !controller.isSingleAgentMode + showModelControl: + controller.currentAssistantExecutionTarget == + AssistantExecutionTarget.auto + ? false + : !controller.isSingleAgentMode ? true : controller.currentSingleAgentShouldShowModelControl, - modelLabel: controller.isSingleAgentMode + modelLabel: + controller.currentAssistantExecutionTarget == + AssistantExecutionTarget.auto + ? 'Auto' + : controller.isSingleAgentMode ? controller.currentSingleAgentModelDisplayLabel : controller.resolvedAssistantModel.isEmpty ? appText('未选择模型', 'No model selected') diff --git a/lib/runtime/go_agent_core_client.dart b/lib/runtime/go_agent_core_client.dart index 47b59ca0..a1efc18a 100644 --- a/lib/runtime/go_agent_core_client.dart +++ b/lib/runtime/go_agent_core_client.dart @@ -147,6 +147,7 @@ class GoAgentCoreSessionRequest { return 'multi-agent'; } return switch (target) { + AssistantExecutionTarget.auto => 'single-agent', AssistantExecutionTarget.singleAgent => 'single-agent', AssistantExecutionTarget.local => 'gateway-chat', AssistantExecutionTarget.remote => 'gateway-chat', diff --git a/lib/runtime/runtime_models_connection.dart b/lib/runtime/runtime_models_connection.dart index 386bbba8..cb1ed9fa 100644 --- a/lib/runtime/runtime_models_connection.dart +++ b/lib/runtime/runtime_models_connection.dart @@ -38,10 +38,11 @@ extension RuntimeConnectionStatusCopy on RuntimeConnectionStatus { }; } -enum AssistantExecutionTarget { singleAgent, local, remote } +enum AssistantExecutionTarget { auto, singleAgent, local, remote } extension AssistantExecutionTargetCopy on AssistantExecutionTarget { String get label => switch (this) { + AssistantExecutionTarget.auto => 'Auto', AssistantExecutionTarget.singleAgent => appText('单机智能体', 'Single Agent'), AssistantExecutionTarget.local => appText( '本地 OpenClaw Gateway', @@ -54,6 +55,7 @@ extension AssistantExecutionTargetCopy on AssistantExecutionTarget { }; String get promptValue => switch (this) { + AssistantExecutionTarget.auto => 'auto', AssistantExecutionTarget.singleAgent => 'single-agent', AssistantExecutionTarget.local => 'local', AssistantExecutionTarget.remote => 'remote', @@ -62,6 +64,8 @@ extension AssistantExecutionTargetCopy on AssistantExecutionTarget { static AssistantExecutionTarget fromJsonValue(String? value) { final normalized = value?.trim() ?? ''; switch (normalized) { + case 'auto': + return AssistantExecutionTarget.auto; case 'singleAgent': case 'aiGatewayOnly': case 'single-agent': @@ -72,7 +76,7 @@ extension AssistantExecutionTargetCopy on AssistantExecutionTarget { case 'remote': return AssistantExecutionTarget.remote; default: - return AssistantExecutionTarget.local; + return AssistantExecutionTarget.auto; } } } diff --git a/lib/runtime/runtime_models_profiles.dart b/lib/runtime/runtime_models_profiles.dart index ecc90b5e..1c09eb55 100644 --- a/lib/runtime/runtime_models_profiles.dart +++ b/lib/runtime/runtime_models_profiles.dart @@ -319,7 +319,8 @@ class AssistantThreadConnectionState { final String? lastError; bool get isSingleAgent => - executionTarget == AssistantExecutionTarget.singleAgent; + executionTarget == AssistantExecutionTarget.singleAgent || + executionTarget == AssistantExecutionTarget.auto; bool get connected => ready; diff --git a/lib/runtime/runtime_models_runtime_payloads.dart b/lib/runtime/runtime_models_runtime_payloads.dart index 50c9f2d8..2e08a29a 100644 --- a/lib/runtime/runtime_models_runtime_payloads.dart +++ b/lib/runtime/runtime_models_runtime_payloads.dart @@ -488,12 +488,14 @@ extension WorkspaceKindCopy on WorkspaceKind { } } -enum ThreadExecutionMode { localAgent, gatewayLocal, gatewayRemote } +enum ThreadExecutionMode { auto, localAgent, gatewayLocal, gatewayRemote } extension ThreadExecutionModeCopy on ThreadExecutionMode { static ThreadExecutionMode fromJsonValue(String? value) { final normalized = value?.trim(); switch (normalized) { + case 'auto': + return ThreadExecutionMode.auto; case 'singleAgent': case 'local_agent': case 'localAgent': @@ -507,7 +509,7 @@ extension ThreadExecutionModeCopy on ThreadExecutionMode { case 'gatewayRemote': return ThreadExecutionMode.gatewayRemote; default: - return ThreadExecutionMode.localAgent; + return ThreadExecutionMode.auto; } } } @@ -1006,6 +1008,7 @@ class TaskThread { SingleAgentProviderCopy.fromJsonValue(executionBinding.providerId); AssistantExecutionTarget get executionTarget => switch (executionBinding.executionMode) { + ThreadExecutionMode.auto => AssistantExecutionTarget.auto, ThreadExecutionMode.localAgent => AssistantExecutionTarget.singleAgent, ThreadExecutionMode.gatewayLocal => AssistantExecutionTarget.local, ThreadExecutionMode.gatewayRemote => AssistantExecutionTarget.remote, @@ -1052,6 +1055,7 @@ class TaskThread { : executionTarget == null ? nextExecutionBinding.executionMode : switch (executionTarget) { + AssistantExecutionTarget.auto => ThreadExecutionMode.auto, AssistantExecutionTarget.singleAgent => ThreadExecutionMode.localAgent, AssistantExecutionTarget.local => ThreadExecutionMode.gatewayLocal, @@ -1115,7 +1119,8 @@ class TaskThread { static ThreadExecutionMode _executionModeFromLegacy( AssistantExecutionTarget? target, ) { - return switch (target ?? AssistantExecutionTarget.singleAgent) { + return switch (target ?? AssistantExecutionTarget.auto) { + AssistantExecutionTarget.auto => ThreadExecutionMode.auto, AssistantExecutionTarget.singleAgent => ThreadExecutionMode.localAgent, AssistantExecutionTarget.local => ThreadExecutionMode.gatewayLocal, AssistantExecutionTarget.remote => ThreadExecutionMode.gatewayRemote, diff --git a/lib/runtime/runtime_models_settings_snapshot.dart b/lib/runtime/runtime_models_settings_snapshot.dart index 663febe3..28ea86ff 100644 --- a/lib/runtime/runtime_models_settings_snapshot.dart +++ b/lib/runtime/runtime_models_settings_snapshot.dart @@ -116,7 +116,7 @@ class SettingsSnapshot { accountWorkspaceFollowed: false, accountLocalMode: true, linuxDesktop: LinuxDesktopConfig.defaults(), - assistantExecutionTarget: AssistantExecutionTarget.local, + assistantExecutionTarget: AssistantExecutionTarget.auto, assistantPermissionLevel: AssistantPermissionLevel.defaultAccess, assistantNavigationDestinations: kAssistantNavigationDestinationDefaults, assistantCustomTaskTitles: const {}, @@ -451,6 +451,7 @@ class SettingsSnapshot { AssistantExecutionTarget target, ) { return switch (target) { + AssistantExecutionTarget.auto => primaryLocalGatewayProfile, AssistantExecutionTarget.singleAgent => null, AssistantExecutionTarget.local => primaryLocalGatewayProfile, AssistantExecutionTarget.remote => primaryRemoteGatewayProfile, @@ -471,6 +472,7 @@ class SettingsSnapshot { GatewayConnectionProfile profile, ) { final index = switch (target) { + AssistantExecutionTarget.auto => kGatewayLocalProfileIndex, AssistantExecutionTarget.local => kGatewayLocalProfileIndex, AssistantExecutionTarget.remote => kGatewayRemoteProfileIndex, AssistantExecutionTarget.singleAgent => null, diff --git a/lib/web/web_assistant_page_helpers.dart b/lib/web/web_assistant_page_helpers.dart index 0f924097..bd74e20c 100644 --- a/lib/web/web_assistant_page_helpers.dart +++ b/lib/web/web_assistant_page_helpers.dart @@ -264,6 +264,7 @@ String thinkingLabelInternal(String level) { String targetLabelInternal(AssistantExecutionTarget target) { return switch (target) { + AssistantExecutionTarget.auto => 'Auto', AssistantExecutionTarget.singleAgent => appText('单机智能体', 'Single Agent'), AssistantExecutionTarget.local => appText( '本地 OpenClaw Gateway', diff --git a/lib/web/web_settings_page_support.dart b/lib/web/web_settings_page_support.dart index 151bac57..543c2cf7 100644 --- a/lib/web/web_settings_page_support.dart +++ b/lib/web/web_settings_page_support.dart @@ -38,6 +38,7 @@ String themeLabelInternal(ThemeMode mode) { String targetLabelInternal(AssistantExecutionTarget target) { return switch (target) { + AssistantExecutionTarget.auto => 'Auto', AssistantExecutionTarget.singleAgent => appText( 'Single Agent', 'Single Agent', diff --git a/lib/widgets/sidebar_navigation_task_section.dart b/lib/widgets/sidebar_navigation_task_section.dart index 5e52dd77..3811667d 100644 --- a/lib/widgets/sidebar_navigation_task_section.dart +++ b/lib/widgets/sidebar_navigation_task_section.dart @@ -562,6 +562,7 @@ String _sidebarTaskUpdatedAtLabel(double? updatedAtMs) { IconData _sidebarTaskTargetIcon(AssistantExecutionTarget target) { return switch (target) { + AssistantExecutionTarget.auto => Icons.auto_awesome_rounded, AssistantExecutionTarget.singleAgent => Icons.hub_outlined, AssistantExecutionTarget.local => Icons.computer_outlined, AssistantExecutionTarget.remote => Icons.cloud_outlined, diff --git a/test/app/ui_feature_manifest_test.dart b/test/app/ui_feature_manifest_test.dart index 3bce8c65..b7c1bfdc 100644 --- a/test/app/ui_feature_manifest_test.dart +++ b/test/app/ui_feature_manifest_test.dart @@ -75,6 +75,7 @@ void main() { expect( desktopAccess.availableExecutionTargets, equals([ + AssistantExecutionTarget.auto, AssistantExecutionTarget.singleAgent, AssistantExecutionTarget.local, AssistantExecutionTarget.remote, @@ -87,6 +88,7 @@ void main() { expect( webAccess.availableExecutionTargets, equals([ + AssistantExecutionTarget.auto, AssistantExecutionTarget.singleAgent, AssistantExecutionTarget.local, AssistantExecutionTarget.remote, @@ -94,6 +96,27 @@ void main() { ); }); + test('sanitizeExecutionTarget prefers auto when available', () { + final manifest = UiFeatureManifest.fallback(); + final desktopAccess = manifest.forPlatform( + UiFeaturePlatform.desktop, + buildMode: UiFeatureBuildMode.release, + ); + final webAccess = manifest.forPlatform( + UiFeaturePlatform.web, + buildMode: UiFeatureBuildMode.release, + ); + + expect( + desktopAccess.sanitizeExecutionTarget(null), + AssistantExecutionTarget.auto, + ); + expect( + webAccess.sanitizeExecutionTarget(null), + AssistantExecutionTarget.auto, + ); + }); + test('parser rejects unsupported flag fields', () { expect( () => UiFeatureManifest.fromYamlString(''' diff --git a/test/features/assistant_page_suite_composer.dart b/test/features/assistant_page_suite_composer.dart index 48aab11e..ad746103 100644 --- a/test/features/assistant_page_suite_composer.dart +++ b/test/features/assistant_page_suite_composer.dart @@ -141,7 +141,7 @@ void registerAssistantPageSuiteComposerTestsInternal() { find.byKey(const Key('assistant-permission-button')), findsOneWidget, ); - expect(find.byKey(const Key('assistant-model-button')), findsOneWidget); + expect(find.byKey(const Key('assistant-model-button')), findsNothing); expect(find.byKey(const Key('assistant-thinking-button')), findsOneWidget); expect(find.byTooltip('模式'), findsNothing); @@ -161,6 +161,7 @@ void registerAssistantPageSuiteComposerTestsInternal() { ); await pumpForUiSyncInternal(tester); + expect(find.text('Auto'), findsWidgets); expect(find.text('单机智能体'), findsWidgets); expect(find.text('本地 OpenClaw Gateway'), findsWidgets); expect(find.text('远程 OpenClaw Gateway'), findsWidgets); diff --git a/test/features/assistant_page_suite_core.dart b/test/features/assistant_page_suite_core.dart index dcfeb278..821c2eb3 100644 --- a/test/features/assistant_page_suite_core.dart +++ b/test/features/assistant_page_suite_core.dart @@ -218,6 +218,9 @@ void registerAssistantPageSuiteCoreTestsInternal() { final aiGroup = find.byKey( const ValueKey('assistant-task-group-singleAgent'), ); + final autoGroup = find.byKey( + const ValueKey('assistant-task-group-auto'), + ); final localGroup = find.byKey( const ValueKey('assistant-task-group-local'), ); @@ -225,10 +228,15 @@ void registerAssistantPageSuiteCoreTestsInternal() { const ValueKey('assistant-task-group-remote'), ); + expect(autoGroup, findsOneWidget); expect(aiGroup, findsOneWidget); expect(localGroup, findsOneWidget); expect(remoteGroup, findsOneWidget); + expect( + tester.getTopLeft(autoGroup).dy, + lessThan(tester.getTopLeft(aiGroup).dy), + ); expect( tester.getTopLeft(aiGroup).dy, lessThan(tester.getTopLeft(localGroup).dy), @@ -322,20 +330,9 @@ void registerAssistantPageSuiteCoreTestsInternal() { ); final toggle = find.byKey(const Key('assistant-artifact-pane-toggle')); - final decoratedBody = find.descendant( - of: toggle, - matching: find.byWidgetPredicate( - (widget) => widget is Container && widget.decoration is BoxDecoration, - ), - ); expect(toggle, findsOneWidget); - expect(tester.getSize(toggle), const Size(32, 36)); - - final body = tester.widget(decoratedBody); - final decoration = body.decoration! as BoxDecoration; - - expect(decoration.borderRadius, BorderRadius.circular(8)); + expect(tester.getSize(toggle), const Size(20, 20)); }); testWidgets( @@ -344,7 +341,7 @@ void registerAssistantPageSuiteCoreTestsInternal() { skip: true, ); - testWidgets('AssistantPage shows three collapsed task groups by default', ( + testWidgets('AssistantPage shows four collapsed task groups by default', ( WidgetTester tester, ) async { final controller = await createTestController(tester); @@ -354,6 +351,10 @@ void registerAssistantPageSuiteCoreTestsInternal() { child: AssistantPage(controller: controller, onOpenDetail: (_) {}), ); + expect( + find.byKey(const ValueKey('assistant-task-group-auto')), + findsOneWidget, + ); expect( find.byKey(const ValueKey('assistant-task-group-singleAgent')), findsOneWidget, @@ -372,7 +373,7 @@ void registerAssistantPageSuiteCoreTestsInternal() { ); await tester.tap( - find.byKey(const ValueKey('assistant-task-group-local')), + find.byKey(const ValueKey('assistant-task-group-auto')), ); await pumpForUiSyncInternal(tester); @@ -464,6 +465,7 @@ void registerAssistantPageSuiteCoreTestsInternal() { WidgetTester tester, ) async { final controller = await createTestController(tester); + await controller.setAssistantExecutionTarget(AssistantExecutionTarget.local); await pumpPage( tester, diff --git a/test/runtime/app_controller_execution_target_switch_suite_fixtures.dart b/test/runtime/app_controller_execution_target_switch_suite_fixtures.dart index a39dec17..ac2e50b8 100644 --- a/test/runtime/app_controller_execution_target_switch_suite_fixtures.dart +++ b/test/runtime/app_controller_execution_target_switch_suite_fixtures.dart @@ -17,7 +17,7 @@ import 'app_controller_execution_target_switch_suite_thread.dart'; import 'app_controller_execution_target_switch_suite_fakes.dart'; Future waitForInternal(bool Function() predicate) async { - final deadline = DateTime.now().add(const Duration(seconds: 5)); + final deadline = DateTime.now().add(const Duration(seconds: 10)); while (!predicate()) { if (DateTime.now().isAfter(deadline)) { fail('condition not met before timeout'); diff --git a/test/runtime/app_controller_execution_target_switch_suite_thread.dart b/test/runtime/app_controller_execution_target_switch_suite_thread.dart index bdc180e1..79b0a83d 100644 --- a/test/runtime/app_controller_execution_target_switch_suite_thread.dart +++ b/test/runtime/app_controller_execution_target_switch_suite_thread.dart @@ -378,7 +378,7 @@ void registerExecutionTargetSwitchThreadTests() { controller.settings.accountUsername, SettingsSnapshot.defaults().accountUsername, ); - expect(controller.settings.assistantLastSessionKey, isEmpty); + expect(controller.settings.assistantLastSessionKey, 'main'); expect(controller.assistantCustomTaskTitle('draft:clear-me'), isEmpty); final reloadedStore = SecureConfigStore( @@ -394,8 +394,13 @@ void registerExecutionTargetSwitchThreadTests() { reloadedSnapshot.accountUsername, SettingsSnapshot.defaults().accountUsername, ); - expect(reloadedSnapshot.assistantLastSessionKey, isEmpty); - expect(reloadedThreads, isEmpty); + expect(reloadedSnapshot.assistantLastSessionKey, 'main'); + expect(reloadedThreads, hasLength(1)); + expect(reloadedThreads.single.sessionKey, 'main'); + expect( + reloadedThreads.single.executionTarget, + AssistantExecutionTarget.auto, + ); }, ); }); diff --git a/test/test_support_task_thread_fixture.dart b/test/test_support_task_thread_fixture.dart index 8ad21aed..eba0748c 100644 --- a/test/test_support_task_thread_fixture.dart +++ b/test/test_support_task_thread_fixture.dart @@ -52,6 +52,7 @@ TaskThread buildTaskThreadFixture({ ), executionBinding: ExecutionBinding( executionMode: switch (executionTarget) { + AssistantExecutionTarget.auto => ThreadExecutionMode.auto, AssistantExecutionTarget.singleAgent => ThreadExecutionMode.localAgent, AssistantExecutionTarget.local => ThreadExecutionMode.gatewayLocal, AssistantExecutionTarget.remote => ThreadExecutionMode.gatewayRemote,