Add top-level Auto task dialog mode
This commit is contained in:
parent
e018fc15dd
commit
283dcdd43c
@ -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)) {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -62,6 +62,7 @@ extension AppControllerDesktopSingleAgent on AppController {
|
||||
return;
|
||||
}
|
||||
await enqueueThreadTurnInternal<void>(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',
|
||||
|
||||
@ -357,6 +357,7 @@ extension AppControllerDesktopSkillPermissions on AppController {
|
||||
))
|
||||
.copyWith(
|
||||
executionMode: switch (nextExecutionTarget) {
|
||||
AssistantExecutionTarget.auto => ThreadExecutionMode.auto,
|
||||
AssistantExecutionTarget.singleAgent =>
|
||||
ThreadExecutionMode.localAgent,
|
||||
AssistantExecutionTarget.local =>
|
||||
|
||||
@ -79,7 +79,8 @@ extension AppControllerDesktopThreadActions on AppController {
|
||||
|
||||
Future<void> 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<String> selectedSkillLabels = const <String>[],
|
||||
}) 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,
|
||||
|
||||
@ -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,
|
||||
},
|
||||
|
||||
@ -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<SingleAgentProvider> get singleAgentProviderOptions =>
|
||||
<SingleAgentProvider>[
|
||||
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,
|
||||
|
||||
@ -393,6 +393,7 @@ bool canQuickConnectGatewayThreadSessionInternal(AppController controller) {
|
||||
return true;
|
||||
}
|
||||
final defaults = switch (target) {
|
||||
AssistantExecutionTarget.auto => GatewayConnectionProfile.defaultsLocal(),
|
||||
AssistantExecutionTarget.singleAgent => GatewayConnectionProfile.emptySlot(
|
||||
index: kGatewayRemoteProfileIndex,
|
||||
),
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -128,6 +128,7 @@ extension AppControllerWebGatewayRelay on AppController {
|
||||
existing?.executionBinding ??
|
||||
ExecutionBinding(
|
||||
executionMode: switch (resolvedExecutionTarget) {
|
||||
AssistantExecutionTarget.auto => ThreadExecutionMode.auto,
|
||||
AssistantExecutionTarget.singleAgent =>
|
||||
ThreadExecutionMode.localAgent,
|
||||
AssistantExecutionTarget.local =>
|
||||
|
||||
@ -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 =>
|
||||
|
||||
@ -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<SingleAgentProvider> get singleAgentProviderOptions =>
|
||||
<SingleAgentProvider>[
|
||||
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<String> 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(<String>[provider.label, model])
|
||||
? '${target == AssistantExecutionTarget.auto ? appText('当前: ', 'Current: ') : ''}${joinConnectionPartsInternal(<String>[provider.label, model])}'
|
||||
: appText(
|
||||
'${provider.label} 需要 Remote ACP(${remoteAddress.isEmpty ? 'Remote Gateway' : remoteAddress})',
|
||||
'${provider.label} requires Remote ACP (${remoteAddress.isEmpty ? 'Remote Gateway' : remoteAddress}).',
|
||||
|
||||
@ -521,6 +521,9 @@ class UiFeatureAccess {
|
||||
|
||||
List<AssistantExecutionTarget> get availableExecutionTargets {
|
||||
final targets = <AssistantExecutionTarget>[];
|
||||
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>[
|
||||
AssistantExecutionTarget.auto,
|
||||
AssistantExecutionTarget.singleAgent,
|
||||
AssistantExecutionTarget.remote,
|
||||
]
|
||||
: const <AssistantExecutionTarget>[
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -442,7 +442,8 @@ class ComposerBarStateInternal extends State<ComposerBarInternal> {
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
if (singleAgent) ...[
|
||||
if (singleAgent &&
|
||||
executionTarget != AssistantExecutionTarget.auto) ...[
|
||||
PopupMenuButton<SingleAgentProvider>(
|
||||
key: const Key('assistant-single-agent-provider-button'),
|
||||
tooltip: appText('单机智能体执行器', 'Single Agent Provider'),
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 <String, String>{},
|
||||
@ -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,
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -75,6 +75,7 @@ void main() {
|
||||
expect(
|
||||
desktopAccess.availableExecutionTargets,
|
||||
equals(<AssistantExecutionTarget>[
|
||||
AssistantExecutionTarget.auto,
|
||||
AssistantExecutionTarget.singleAgent,
|
||||
AssistantExecutionTarget.local,
|
||||
AssistantExecutionTarget.remote,
|
||||
@ -87,6 +88,7 @@ void main() {
|
||||
expect(
|
||||
webAccess.availableExecutionTargets,
|
||||
equals(<AssistantExecutionTarget>[
|
||||
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('''
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -218,6 +218,9 @@ void registerAssistantPageSuiteCoreTestsInternal() {
|
||||
final aiGroup = find.byKey(
|
||||
const ValueKey<String>('assistant-task-group-singleAgent'),
|
||||
);
|
||||
final autoGroup = find.byKey(
|
||||
const ValueKey<String>('assistant-task-group-auto'),
|
||||
);
|
||||
final localGroup = find.byKey(
|
||||
const ValueKey<String>('assistant-task-group-local'),
|
||||
);
|
||||
@ -225,10 +228,15 @@ void registerAssistantPageSuiteCoreTestsInternal() {
|
||||
const ValueKey<String>('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<Container>(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<String>('assistant-task-group-auto')),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
find.byKey(const ValueKey<String>('assistant-task-group-singleAgent')),
|
||||
findsOneWidget,
|
||||
@ -372,7 +373,7 @@ void registerAssistantPageSuiteCoreTestsInternal() {
|
||||
);
|
||||
|
||||
await tester.tap(
|
||||
find.byKey(const ValueKey<String>('assistant-task-group-local')),
|
||||
find.byKey(const ValueKey<String>('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,
|
||||
|
||||
@ -17,7 +17,7 @@ import 'app_controller_execution_target_switch_suite_thread.dart';
|
||||
import 'app_controller_execution_target_switch_suite_fakes.dart';
|
||||
|
||||
Future<void> 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');
|
||||
|
||||
@ -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,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user