Remove project selection from task threads
This commit is contained in:
parent
e15898fb24
commit
49d3036090
@ -49,8 +49,8 @@ extension AppControllerDesktopExternalAcpRouting on AppController {
|
||||
normalizedSessionKey,
|
||||
);
|
||||
final preferredGatewayTarget = switch (sessionTarget) {
|
||||
AssistantExecutionTarget.gateway => 'gateway',
|
||||
AssistantExecutionTarget.singleAgent => 'gateway',
|
||||
AssistantExecutionTarget.gateway => kCanonicalGatewayProviderId,
|
||||
AssistantExecutionTarget.singleAgent => kCanonicalGatewayProviderId,
|
||||
};
|
||||
final availableSkills =
|
||||
assistantImportedSkillsForSession(normalizedSessionKey)
|
||||
|
||||
@ -157,9 +157,16 @@ String? assistantWorkingDirectoryForSessionRuntimeInternal(
|
||||
AppController controller,
|
||||
String sessionKey,
|
||||
) {
|
||||
final candidate = controller
|
||||
.assistantSelectedWorkingDirectoryForSession(sessionKey)
|
||||
.trim();
|
||||
final normalizedSessionKey = controller.normalizedAssistantSessionKeyInternal(
|
||||
sessionKey,
|
||||
);
|
||||
final candidate =
|
||||
controller
|
||||
.assistantThreadRecordsInternal[normalizedSessionKey]
|
||||
?.workspaceBinding
|
||||
.workspacePath
|
||||
.trim() ??
|
||||
'';
|
||||
if (candidate.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -237,8 +237,6 @@ extension AppControllerDesktopSkillPermissions on AppController {
|
||||
ThreadSelectionSource? singleAgentProviderSource,
|
||||
ThreadSelectionSource? assistantModelSource,
|
||||
ThreadSelectionSource? selectedSkillsSource,
|
||||
String? selectedWorkingDirectory,
|
||||
bool clearSelectedWorkingDirectory = false,
|
||||
String? gatewayEntryState,
|
||||
String? latestResolvedRuntimeModel,
|
||||
String? lifecycleStatus,
|
||||
@ -350,7 +348,6 @@ extension AppControllerDesktopSkillPermissions on AppController {
|
||||
permissionLevel: AssistantPermissionLevel.defaultAccess,
|
||||
messageViewMode: AssistantMessageViewMode.rendered,
|
||||
latestResolvedRuntimeModel: '',
|
||||
selectedWorkingDirectory: null,
|
||||
gatewayEntryState: gatewayEntryStateForTargetInternal(
|
||||
nextExecutionTarget,
|
||||
),
|
||||
@ -375,8 +372,6 @@ extension AppControllerDesktopSkillPermissions on AppController {
|
||||
selectedSkillsSource ??
|
||||
existing?.contextState.selectedSkillsSource,
|
||||
latestResolvedRuntimeModel: latestResolvedRuntimeModel,
|
||||
selectedWorkingDirectory: selectedWorkingDirectory?.trim(),
|
||||
clearSelectedWorkingDirectory: clearSelectedWorkingDirectory,
|
||||
gatewayEntryState: gatewayEntryState,
|
||||
lastRemoteWorkingDirectory: lastRemoteWorkingDirectory,
|
||||
lastRemoteWorkspaceRefKind: lastRemoteWorkspaceRefKind,
|
||||
|
||||
@ -247,14 +247,16 @@ extension AppControllerDesktopThreadActions on AppController {
|
||||
currentSessionKey,
|
||||
executionTarget: currentTarget,
|
||||
);
|
||||
final workingDirectory = assistantSelectedWorkingDirectoryForSession(
|
||||
currentSessionKey,
|
||||
).trim();
|
||||
final workingDirectory =
|
||||
assistantWorkingDirectoryForSessionInternal(
|
||||
currentSessionKey,
|
||||
)?.trim() ??
|
||||
'';
|
||||
if (workingDirectory.isEmpty) {
|
||||
final error = StateError(
|
||||
appText(
|
||||
'当前线程尚未选择项目目录,无法运行。请先选择项目。',
|
||||
'This thread has no project directory yet. Select a project before running.',
|
||||
'当前任务线程缺少可运行的 workingDirectory,无法执行。',
|
||||
'This task thread has no runnable workingDirectory yet.',
|
||||
),
|
||||
);
|
||||
appendAssistantThreadMessageInternal(
|
||||
|
||||
@ -198,17 +198,16 @@ extension AppControllerDesktopThreadBinding on AppController {
|
||||
}) {
|
||||
if (executionTarget == AssistantExecutionTarget.singleAgent) {
|
||||
if (existingBinding != null &&
|
||||
existingBinding.workspaceKind == WorkspaceKind.localFs &&
|
||||
!isManagedLocalThreadWorkspacePathInternal(
|
||||
existingBinding.workspacePath,
|
||||
sessionKey,
|
||||
) &&
|
||||
ensureLocalWorkspaceDirectoryInternal(
|
||||
existingBinding.workspacePath,
|
||||
)) {
|
||||
return existingBinding.copyWith(
|
||||
displayPath: existingBinding.workspacePath,
|
||||
);
|
||||
existingBinding.workspaceKind == WorkspaceKind.localFs) {
|
||||
final existingPath = existingBinding.workspacePath.trim();
|
||||
if (existingPath.isNotEmpty &&
|
||||
ensureLocalWorkspaceDirectoryInternal(existingPath)) {
|
||||
// A task thread owns one stable local workingDirectory for its
|
||||
// lifetime. Do not silently rebind it after the initial allocation.
|
||||
return existingBinding.copyWith(
|
||||
displayPath: existingBinding.workspacePath,
|
||||
);
|
||||
}
|
||||
}
|
||||
final localPath = localThreadWorkspacePathInternal(sessionKey);
|
||||
if (localPath.isEmpty) {
|
||||
@ -255,15 +254,16 @@ extension AppControllerDesktopThreadBinding on AppController {
|
||||
required SingleAgentProvider singleAgentProvider,
|
||||
ExecutionBinding? existingBinding,
|
||||
}) {
|
||||
final sanitizedProvider =
|
||||
executionTarget == AssistantExecutionTarget.singleAgent
|
||||
? settings.sanitizeSingleAgentProviderSelection(singleAgentProvider)
|
||||
: SingleAgentProvider.unspecified;
|
||||
final providerId = executionTarget == AssistantExecutionTarget.singleAgent
|
||||
? settings
|
||||
.sanitizeSingleAgentProviderSelection(singleAgentProvider)
|
||||
.providerId
|
||||
: kCanonicalGatewayProviderId;
|
||||
return (existingBinding ??
|
||||
ExecutionBinding(
|
||||
executionMode: ThreadExecutionMode.localAgent,
|
||||
executorId: sanitizedProvider.providerId,
|
||||
providerId: sanitizedProvider.providerId,
|
||||
executorId: providerId,
|
||||
providerId: providerId,
|
||||
endpointId: '',
|
||||
))
|
||||
.copyWith(
|
||||
@ -272,8 +272,8 @@ extension AppControllerDesktopThreadBinding on AppController {
|
||||
ThreadExecutionMode.localAgent,
|
||||
AssistantExecutionTarget.gateway => ThreadExecutionMode.gateway,
|
||||
},
|
||||
executorId: sanitizedProvider.providerId,
|
||||
providerId: sanitizedProvider.providerId,
|
||||
executorId: providerId,
|
||||
providerId: providerId,
|
||||
providerSource:
|
||||
executionTarget == AssistantExecutionTarget.singleAgent
|
||||
? existingBinding?.providerSource
|
||||
|
||||
@ -216,35 +216,6 @@ extension AppControllerDesktopThreadSessions on AppController {
|
||||
'';
|
||||
}
|
||||
|
||||
String assistantSelectedWorkingDirectoryForSession(String sessionKey) {
|
||||
final normalizedSessionKey = normalizedAssistantSessionKeyInternal(
|
||||
sessionKey,
|
||||
);
|
||||
return taskThreadForSessionInternal(
|
||||
normalizedSessionKey,
|
||||
)?.selectedWorkingDirectory?.trim() ??
|
||||
'';
|
||||
}
|
||||
|
||||
String assistantSelectedWorkingDirectoryDisplayLabelForSession(
|
||||
String sessionKey,
|
||||
) {
|
||||
final workingDirectory = assistantSelectedWorkingDirectoryForSession(
|
||||
sessionKey,
|
||||
);
|
||||
if (workingDirectory.isEmpty) {
|
||||
return appText('选择项目', 'Select Project');
|
||||
}
|
||||
final segments = workingDirectory
|
||||
.split(RegExp(r'[\\/]'))
|
||||
.where((item) => item.trim().isNotEmpty)
|
||||
.toList(growable: false);
|
||||
if (segments.isEmpty) {
|
||||
return workingDirectory;
|
||||
}
|
||||
return segments.last;
|
||||
}
|
||||
|
||||
Future<AssistantArtifactSnapshot> loadAssistantArtifactSnapshot({
|
||||
String? sessionKey,
|
||||
}) {
|
||||
|
||||
@ -698,7 +698,11 @@ extension AppControllerDesktopThreadStorage on AppController {
|
||||
record.executionBinding.providerId,
|
||||
),
|
||||
)
|
||||
: SingleAgentProvider.unspecified;
|
||||
: const SingleAgentProvider(
|
||||
providerId: kCanonicalGatewayProviderId,
|
||||
label: kCanonicalGatewayProviderLabel,
|
||||
badge: 'OC',
|
||||
);
|
||||
final workspaceBinding = record.workspaceBinding.copyWith(
|
||||
workspaceId: sessionKey,
|
||||
displayPath: record.workspaceKind == WorkspaceKind.localFs
|
||||
|
||||
@ -372,32 +372,6 @@ extension AppControllerDesktopWorkspaceExecution on AppController {
|
||||
notifyIfActiveInternal();
|
||||
}
|
||||
|
||||
Future<void> saveAssistantSelectedWorkingDirectoryForSession(
|
||||
String sessionKey,
|
||||
String workingDirectory,
|
||||
) async {
|
||||
final normalizedSessionKey = normalizedAssistantSessionKeyInternal(
|
||||
sessionKey,
|
||||
);
|
||||
final normalizedWorkingDirectory = workingDirectory.trim();
|
||||
if (normalizedWorkingDirectory.isEmpty) {
|
||||
upsertTaskThreadInternal(
|
||||
normalizedSessionKey,
|
||||
clearSelectedWorkingDirectory: true,
|
||||
updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||
);
|
||||
} else {
|
||||
upsertTaskThreadInternal(
|
||||
normalizedSessionKey,
|
||||
selectedWorkingDirectory: normalizedWorkingDirectory,
|
||||
updatedAtMs: DateTime.now().millisecondsSinceEpoch.toDouble(),
|
||||
);
|
||||
}
|
||||
recomputeTasksInternal();
|
||||
notifyIfActiveInternal();
|
||||
await flushAssistantThreadPersistenceInternal();
|
||||
}
|
||||
|
||||
Future<void> refreshSingleAgentSkillsForSession(String sessionKey) async {
|
||||
final normalizedSessionKey = normalizedAssistantSessionKeyInternal(
|
||||
sessionKey,
|
||||
|
||||
@ -4,7 +4,6 @@ import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:math' as math;
|
||||
import 'package:file_selector/file_selector.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
@ -343,31 +342,6 @@ class ComposerBarStateInternal extends State<ComposerBarInternal> {
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> pickWorkingDirectoryInternal() async {
|
||||
final controller = widget.controller;
|
||||
final sessionKey = controller.currentSessionKey;
|
||||
final selectedWorkingDirectory = controller
|
||||
.assistantSelectedWorkingDirectoryForSession(sessionKey)
|
||||
.trim();
|
||||
final fallbackWorkspacePath = controller
|
||||
.assistantWorkspacePathForSession(sessionKey)
|
||||
.trim();
|
||||
final initialDirectory = selectedWorkingDirectory.isNotEmpty
|
||||
? selectedWorkingDirectory
|
||||
: fallbackWorkspacePath;
|
||||
final pickedDirectory = await getDirectoryPath(
|
||||
confirmButtonText: appText('选择项目', 'Select Project'),
|
||||
initialDirectory: initialDirectory.isNotEmpty ? initialDirectory : null,
|
||||
);
|
||||
if (!mounted || pickedDirectory == null || pickedDirectory.trim().isEmpty) {
|
||||
return;
|
||||
}
|
||||
await controller.saveAssistantSelectedWorkingDirectoryForSession(
|
||||
sessionKey,
|
||||
pickedDirectory,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final palette = context.palette;
|
||||
@ -400,15 +374,6 @@ class ComposerBarStateInternal extends State<ComposerBarInternal> {
|
||||
final selectedSkills = widget.availableSkills
|
||||
.where((skill) => widget.selectedSkillKeys.contains(skill.key))
|
||||
.toList(growable: false);
|
||||
final selectedWorkingDirectory = controller
|
||||
.assistantSelectedWorkingDirectoryForSession(
|
||||
controller.currentSessionKey,
|
||||
)
|
||||
.trim();
|
||||
final selectedWorkingDirectoryLabel = controller
|
||||
.assistantSelectedWorkingDirectoryDisplayLabelForSession(
|
||||
controller.currentSessionKey,
|
||||
);
|
||||
final submitLabel = connected
|
||||
? appText('提交', 'Submit')
|
||||
: singleAgent
|
||||
@ -505,57 +470,6 @@ class ComposerBarStateInternal extends State<ComposerBarInternal> {
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
Tooltip(
|
||||
message: selectedWorkingDirectory.isEmpty
|
||||
? appText(
|
||||
'选择 bridge 执行使用的项目目录(workingDirectory)',
|
||||
'Choose the bridge project directory (workingDirectory).',
|
||||
)
|
||||
: selectedWorkingDirectory,
|
||||
child: InkWell(
|
||||
key: const Key('assistant-working-directory-button'),
|
||||
onTap: () => unawaited(pickWorkingDirectoryInternal()),
|
||||
borderRadius: BorderRadius.circular(AppRadius.chip),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: palette.surfacePrimary,
|
||||
borderRadius: BorderRadius.circular(AppRadius.chip),
|
||||
border: Border.all(color: palette.strokeSoft),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.folder_open_rounded,
|
||||
size: 16,
|
||||
color: selectedWorkingDirectory.isEmpty
|
||||
? palette.textMuted
|
||||
: palette.textPrimary,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 160),
|
||||
child: Text(
|
||||
selectedWorkingDirectoryLabel,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.labelMedium
|
||||
?.copyWith(
|
||||
color: selectedWorkingDirectory.isEmpty
|
||||
? palette.textMuted
|
||||
: palette.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
if (singleAgent) ...[
|
||||
PopupMenuButton<SingleAgentProvider>(
|
||||
key: const Key('assistant-single-agent-provider-button'),
|
||||
@ -599,6 +513,36 @@ class ComposerBarStateInternal extends State<ComposerBarInternal> {
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
] else ...[
|
||||
PopupMenuButton<String>(
|
||||
key: const Key('assistant-gateway-provider-button'),
|
||||
tooltip: appText('Gateway Provider', 'Gateway Provider'),
|
||||
onSelected: (_) {},
|
||||
itemBuilder: (context) => const <PopupMenuEntry<String>>[
|
||||
PopupMenuItem<String>(
|
||||
value: kCanonicalGatewayProviderId,
|
||||
key: Key('assistant-gateway-provider-menu-item-openclaw'),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.cloud_outlined, size: 18),
|
||||
SizedBox(width: 10),
|
||||
Expanded(child: Text(kCanonicalGatewayProviderLabel)),
|
||||
Icon(Icons.check_rounded, size: 18),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
child: ComposerToolbarChipInternal(
|
||||
icon: Icons.cloud_outlined,
|
||||
tooltip: gatewayProviderTooltipInternal(),
|
||||
showChevron: true,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 6,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
if (widget.showModelControl) ...[
|
||||
widget.modelOptions.isEmpty
|
||||
|
||||
@ -50,6 +50,11 @@ String singleAgentProviderTooltipInternal(
|
||||
'Bridge Provider: ${provider.label.trim().isEmpty ? appText('未配置', 'Unconfigured') : provider.label}',
|
||||
);
|
||||
|
||||
String gatewayProviderTooltipInternal() => appText(
|
||||
'Gateway Provider: $kCanonicalGatewayProviderLabel',
|
||||
'Gateway Provider: $kCanonicalGatewayProviderLabel',
|
||||
);
|
||||
|
||||
String modelTooltipInternal(String modelLabel) =>
|
||||
appText('模型: $modelLabel', 'Model: $modelLabel');
|
||||
|
||||
|
||||
@ -347,8 +347,8 @@ class GoTaskServiceRequest {
|
||||
ExternalCodeAgentAcpRoutingConfig _synthesizedRouting() {
|
||||
final gatewayTarget = normalizedTarget;
|
||||
final preferredGatewayTarget = switch (gatewayTarget) {
|
||||
AssistantExecutionTarget.gateway => 'gateway',
|
||||
_ => 'gateway',
|
||||
AssistantExecutionTarget.gateway => kCanonicalGatewayProviderId,
|
||||
_ => kCanonicalGatewayProviderId,
|
||||
};
|
||||
final explicitExecutionTarget = switch (gatewayTarget) {
|
||||
AssistantExecutionTarget.singleAgent => 'single-agent',
|
||||
|
||||
@ -333,6 +333,9 @@ const List<SingleAgentProvider> kPresetExternalAcpProviders =
|
||||
SingleAgentProvider.gemini,
|
||||
];
|
||||
|
||||
const String kCanonicalGatewayProviderId = 'openclaw';
|
||||
const String kCanonicalGatewayProviderLabel = 'OpenClaw';
|
||||
|
||||
const List<SingleAgentProvider> kKnownSingleAgentProviders =
|
||||
<SingleAgentProvider>[
|
||||
SingleAgentProvider.codex,
|
||||
|
||||
@ -755,7 +755,6 @@ class ThreadContextState {
|
||||
required this.latestResolvedRuntimeModel,
|
||||
this.selectedModelSource = ThreadSelectionSource.inherited,
|
||||
this.selectedSkillsSource = ThreadSelectionSource.inherited,
|
||||
this.selectedWorkingDirectory,
|
||||
this.gatewayEntryState,
|
||||
this.lastRemoteWorkingDirectory,
|
||||
this.lastRemoteWorkspaceRefKind,
|
||||
@ -772,7 +771,6 @@ class ThreadContextState {
|
||||
final String latestResolvedRuntimeModel;
|
||||
final ThreadSelectionSource selectedModelSource;
|
||||
final ThreadSelectionSource selectedSkillsSource;
|
||||
final String? selectedWorkingDirectory;
|
||||
final String? gatewayEntryState;
|
||||
final String? lastRemoteWorkingDirectory;
|
||||
final WorkspaceRefKind? lastRemoteWorkspaceRefKind;
|
||||
@ -789,8 +787,6 @@ class ThreadContextState {
|
||||
String? latestResolvedRuntimeModel,
|
||||
ThreadSelectionSource? selectedModelSource,
|
||||
ThreadSelectionSource? selectedSkillsSource,
|
||||
String? selectedWorkingDirectory,
|
||||
bool clearSelectedWorkingDirectory = false,
|
||||
String? gatewayEntryState,
|
||||
bool clearGatewayEntryState = false,
|
||||
String? lastRemoteWorkingDirectory,
|
||||
@ -809,9 +805,6 @@ class ThreadContextState {
|
||||
latestResolvedRuntimeModel ?? this.latestResolvedRuntimeModel,
|
||||
selectedModelSource: selectedModelSource ?? this.selectedModelSource,
|
||||
selectedSkillsSource: selectedSkillsSource ?? this.selectedSkillsSource,
|
||||
selectedWorkingDirectory: clearSelectedWorkingDirectory
|
||||
? null
|
||||
: (selectedWorkingDirectory ?? this.selectedWorkingDirectory),
|
||||
gatewayEntryState: clearGatewayEntryState
|
||||
? null
|
||||
: (gatewayEntryState ?? this.gatewayEntryState),
|
||||
@ -838,7 +831,6 @@ class ThreadContextState {
|
||||
'latestResolvedRuntimeModel': latestResolvedRuntimeModel,
|
||||
'selectedModelSource': selectedModelSource.name,
|
||||
'selectedSkillsSource': selectedSkillsSource.name,
|
||||
'selectedWorkingDirectory': selectedWorkingDirectory,
|
||||
'gatewayEntryState': gatewayEntryState,
|
||||
'lastRemoteWorkingDirectory': lastRemoteWorkingDirectory,
|
||||
'lastRemoteWorkspaceRefKind': lastRemoteWorkspaceRefKind?.name,
|
||||
@ -904,7 +896,6 @@ class ThreadContextState {
|
||||
selectedSkillsSource: ThreadSelectionSourceCopy.fromJsonValue(
|
||||
json['selectedSkillsSource']?.toString(),
|
||||
),
|
||||
selectedWorkingDirectory: json['selectedWorkingDirectory']?.toString(),
|
||||
gatewayEntryState: json['gatewayEntryState']?.toString(),
|
||||
lastRemoteWorkingDirectory: json['lastRemoteWorkingDirectory']
|
||||
?.toString(),
|
||||
@ -1000,7 +991,6 @@ class TaskThread {
|
||||
String? latestResolvedRuntimeModel,
|
||||
double? lastRunAtMs,
|
||||
String? lastResultCode,
|
||||
String? selectedWorkingDirectory,
|
||||
String? lastRemoteWorkingDirectory,
|
||||
WorkspaceRefKind? lastRemoteWorkspaceRefKind,
|
||||
double? lastArtifactSyncAtMs,
|
||||
@ -1038,10 +1028,6 @@ class TaskThread {
|
||||
messageViewMode ?? AssistantMessageViewMode.rendered,
|
||||
latestResolvedRuntimeModel:
|
||||
latestResolvedRuntimeModel?.trim() ?? '',
|
||||
selectedWorkingDirectory:
|
||||
selectedWorkingDirectory?.trim().isNotEmpty == true
|
||||
? selectedWorkingDirectory!.trim()
|
||||
: null,
|
||||
gatewayEntryState: gatewayEntryState?.trim(),
|
||||
lastRemoteWorkingDirectory:
|
||||
lastRemoteWorkingDirectory?.trim().isNotEmpty == true
|
||||
@ -1084,7 +1070,6 @@ class TaskThread {
|
||||
List<String> get selectedSkillKeys => contextState.selectedSkillKeys;
|
||||
String get assistantModelId => contextState.selectedModelId;
|
||||
AssistantMessageViewMode get messageViewMode => contextState.messageViewMode;
|
||||
String? get selectedWorkingDirectory => contextState.selectedWorkingDirectory;
|
||||
String? get gatewayEntryState => contextState.gatewayEntryState;
|
||||
String? get lastRemoteWorkingDirectory =>
|
||||
contextState.lastRemoteWorkingDirectory;
|
||||
@ -1125,8 +1110,6 @@ class TaskThread {
|
||||
String? assistantModelId,
|
||||
ThreadSelectionSource? assistantModelSource,
|
||||
ThreadSelectionSource? selectedSkillsSource,
|
||||
String? selectedWorkingDirectory,
|
||||
bool clearSelectedWorkingDirectory = false,
|
||||
String? gatewayEntryState,
|
||||
bool clearGatewayEntryState = false,
|
||||
String? latestResolvedRuntimeModel,
|
||||
@ -1150,8 +1133,6 @@ class TaskThread {
|
||||
selectedModelSource: assistantModelSource,
|
||||
selectedSkillsSource: selectedSkillsSource,
|
||||
latestResolvedRuntimeModel: latestResolvedRuntimeModel,
|
||||
selectedWorkingDirectory: selectedWorkingDirectory,
|
||||
clearSelectedWorkingDirectory: clearSelectedWorkingDirectory,
|
||||
gatewayEntryState: gatewayEntryState,
|
||||
clearGatewayEntryState: clearGatewayEntryState,
|
||||
lastRemoteWorkingDirectory: lastRemoteWorkingDirectory,
|
||||
@ -1268,7 +1249,6 @@ class TaskThread {
|
||||
'latestResolvedRuntimeModel': json['latestResolvedRuntimeModel'],
|
||||
'selectedModelSource': json['assistantModelSource'],
|
||||
'selectedSkillsSource': json['selectedSkillsSource'],
|
||||
'selectedWorkingDirectory': json['selectedWorkingDirectory'],
|
||||
'gatewayEntryState': json['gatewayEntryState'],
|
||||
'lastRemoteWorkingDirectory': json['lastRemoteWorkingDirectory'],
|
||||
'lastRemoteWorkspaceRefKind': json['lastRemoteWorkspaceRefKind'],
|
||||
|
||||
@ -394,41 +394,26 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
group('selected working directory', () {
|
||||
group('thread working directory', () {
|
||||
test(
|
||||
'persists thread project directory without changing local workspace binding',
|
||||
'uses the unique thread workspace as the only workingDirectory source',
|
||||
() async {
|
||||
final controller = AppController();
|
||||
addTearDown(controller.dispose);
|
||||
|
||||
const sessionKey = 'draft:project-dir';
|
||||
const sessionKey = 'draft:thread-working-directory';
|
||||
controller.initializeAssistantThreadContext(
|
||||
sessionKey,
|
||||
executionTarget: AssistantExecutionTarget.singleAgent,
|
||||
);
|
||||
final originalWorkspacePath = controller
|
||||
.assistantWorkspacePathForSession(sessionKey);
|
||||
|
||||
await controller.saveAssistantSelectedWorkingDirectoryForSession(
|
||||
sessionKey,
|
||||
'/tmp/project-alpha',
|
||||
);
|
||||
|
||||
final record = controller.requireTaskThreadForSessionInternal(
|
||||
sessionKey,
|
||||
);
|
||||
expect(record.selectedWorkingDirectory, '/tmp/project-alpha');
|
||||
|
||||
expect(
|
||||
controller.assistantSelectedWorkingDirectoryForSession(sessionKey),
|
||||
'/tmp/project-alpha',
|
||||
controller.assistantWorkingDirectoryForSessionInternal(sessionKey),
|
||||
record.workspaceBinding.workspacePath,
|
||||
);
|
||||
expect(
|
||||
controller.assistantSelectedWorkingDirectoryDisplayLabelForSession(
|
||||
sessionKey,
|
||||
),
|
||||
'project-alpha',
|
||||
);
|
||||
expect(record.workspaceBinding.workspacePath, originalWorkspacePath);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:xworkmate/app/app_controller_desktop_core.dart';
|
||||
import 'package:xworkmate/app/app_controller_desktop_runtime_helpers.dart';
|
||||
import 'package:xworkmate/app/app_controller_desktop_thread_actions.dart';
|
||||
import 'package:xworkmate/app/app_controller_desktop_thread_sessions.dart';
|
||||
import 'package:xworkmate/app/app_controller_desktop_workspace_execution.dart';
|
||||
@ -13,24 +12,16 @@ void main() {
|
||||
|
||||
group('thread workingDirectory dispatch', () {
|
||||
test(
|
||||
'single-agent requests reuse the thread selected workingDirectory',
|
||||
'single-agent requests reuse the unique thread workspace workingDirectory',
|
||||
() async {
|
||||
final client = _CapturingGoTaskServiceClient();
|
||||
final projectDir = Directory.systemTemp.createTempSync(
|
||||
'xworkmate-project-alpha-',
|
||||
);
|
||||
final controller = AppController(
|
||||
goTaskServiceClient: client,
|
||||
availableSingleAgentProvidersOverride: const <SingleAgentProvider>[
|
||||
SingleAgentProvider.codex,
|
||||
],
|
||||
);
|
||||
addTearDown(() async {
|
||||
controller.dispose();
|
||||
if (projectDir.existsSync()) {
|
||||
await projectDir.delete(recursive: true);
|
||||
}
|
||||
});
|
||||
addTearDown(controller.dispose);
|
||||
|
||||
const sessionKey = 'draft:single-agent-working-directory';
|
||||
controller.initializeAssistantThreadContext(
|
||||
@ -38,10 +29,10 @@ void main() {
|
||||
executionTarget: AssistantExecutionTarget.singleAgent,
|
||||
);
|
||||
await controller.switchSession(sessionKey);
|
||||
await controller.saveAssistantSelectedWorkingDirectoryForSession(
|
||||
sessionKey,
|
||||
projectDir.path,
|
||||
);
|
||||
final expectedThreadWorkingDirectory = controller
|
||||
.requireTaskThreadForSessionInternal(sessionKey)
|
||||
.workspaceBinding
|
||||
.workspacePath;
|
||||
|
||||
await controller.sendChatMessage('first turn');
|
||||
await controller.sendChatMessage('second turn');
|
||||
@ -57,60 +48,51 @@ void main() {
|
||||
]);
|
||||
expect(
|
||||
client.requests.map((item) => item.workingDirectory).toList(),
|
||||
<String>[projectDir.path, projectDir.path],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'gateway threads persist their selected workingDirectory separately from workspace binding',
|
||||
() async {
|
||||
final projectDir = Directory.systemTemp.createTempSync(
|
||||
'xworkmate-project-beta-',
|
||||
);
|
||||
final controller = AppController(
|
||||
availableSingleAgentProvidersOverride: const <SingleAgentProvider>[
|
||||
SingleAgentProvider.codex,
|
||||
<String>[
|
||||
expectedThreadWorkingDirectory,
|
||||
expectedThreadWorkingDirectory,
|
||||
],
|
||||
);
|
||||
addTearDown(() async {
|
||||
controller.dispose();
|
||||
if (projectDir.existsSync()) {
|
||||
await projectDir.delete(recursive: true);
|
||||
}
|
||||
});
|
||||
|
||||
controller.appUiStateInternal = controller.appUiState.copyWith(
|
||||
savedGatewayTargets: const <String>['gateway'],
|
||||
);
|
||||
controller.lastObservedSettingsSnapshotInternal =
|
||||
controller.settingsController.snapshotInternal;
|
||||
|
||||
const sessionKey = 'draft:gateway-working-directory';
|
||||
controller.initializeAssistantThreadContext(
|
||||
sessionKey,
|
||||
executionTarget: AssistantExecutionTarget.gateway,
|
||||
);
|
||||
await controller.switchSession(sessionKey);
|
||||
await controller.setAssistantExecutionTarget(
|
||||
AssistantExecutionTarget.gateway,
|
||||
);
|
||||
await controller.saveAssistantSelectedWorkingDirectoryForSession(
|
||||
sessionKey,
|
||||
projectDir.path,
|
||||
);
|
||||
|
||||
final record = controller.requireTaskThreadForSessionInternal(
|
||||
sessionKey,
|
||||
);
|
||||
expect(
|
||||
controller.assistantExecutionTargetForSession(sessionKey),
|
||||
AssistantExecutionTarget.gateway,
|
||||
);
|
||||
expect(record.selectedWorkingDirectory, projectDir.path);
|
||||
expect(record.workspaceBinding.workspacePath, isNot(projectDir.path));
|
||||
},
|
||||
);
|
||||
|
||||
test('each task thread keeps an independent workingDirectory', () async {
|
||||
final controller = AppController(
|
||||
availableSingleAgentProvidersOverride: const <SingleAgentProvider>[
|
||||
SingleAgentProvider.codex,
|
||||
],
|
||||
);
|
||||
addTearDown(controller.dispose);
|
||||
|
||||
const sessionKey = 'draft:thread-working-directory-a';
|
||||
const otherSessionKey = 'draft:thread-working-directory-b';
|
||||
controller.initializeAssistantThreadContext(
|
||||
sessionKey,
|
||||
executionTarget: AssistantExecutionTarget.singleAgent,
|
||||
);
|
||||
controller.initializeAssistantThreadContext(
|
||||
otherSessionKey,
|
||||
executionTarget: AssistantExecutionTarget.singleAgent,
|
||||
);
|
||||
final recordA = controller.requireTaskThreadForSessionInternal(
|
||||
sessionKey,
|
||||
);
|
||||
final recordB = controller.requireTaskThreadForSessionInternal(
|
||||
otherSessionKey,
|
||||
);
|
||||
expect(
|
||||
controller.assistantWorkingDirectoryForSessionInternal(sessionKey),
|
||||
recordA.workspaceBinding.workspacePath,
|
||||
);
|
||||
expect(
|
||||
controller.assistantWorkingDirectoryForSessionInternal(otherSessionKey),
|
||||
recordB.workspaceBinding.workspacePath,
|
||||
);
|
||||
expect(
|
||||
recordA.workspaceBinding.workspacePath,
|
||||
isNot(recordB.workspaceBinding.workspacePath),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -18,7 +18,7 @@ void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
testWidgets(
|
||||
'mode picker keeps single-agent and gateway visible while project selector stays available',
|
||||
'mode picker keeps single-agent and gateway visible while thread-only provider controls stay available',
|
||||
(tester) async {
|
||||
final root = Directory.systemTemp.createTempSync(
|
||||
'xworkmate-picker-widget-test-',
|
||||
@ -57,10 +57,6 @@ void main() {
|
||||
);
|
||||
controller.lastObservedSettingsSnapshotInternal =
|
||||
controller.settingsController.snapshotInternal;
|
||||
await controller.saveAssistantSelectedWorkingDirectoryForSession(
|
||||
controller.currentSessionKey,
|
||||
'/tmp/project-alpha',
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
@ -122,19 +118,112 @@ void main() {
|
||||
]);
|
||||
expect(
|
||||
find.byKey(const Key('assistant-working-directory-button')),
|
||||
findsNothing,
|
||||
);
|
||||
expect(
|
||||
find.byKey(const Key('assistant-single-agent-provider-button')),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.text('project-alpha'), findsOneWidget);
|
||||
expect(
|
||||
find.byKey(const Key('assistant-collaboration-toggle')),
|
||||
findsNothing,
|
||||
);
|
||||
|
||||
await tester.pumpWidget(const SizedBox.shrink());
|
||||
controller.dispose();
|
||||
await tester.pump();
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('gateway mode shows the canonical OpenClaw provider selector', (
|
||||
tester,
|
||||
) async {
|
||||
final root = Directory.systemTemp.createTempSync(
|
||||
'xworkmate-picker-widget-gateway-test-',
|
||||
);
|
||||
final store = SecureConfigStore(
|
||||
enableSecureStorage: false,
|
||||
appDataRootPathResolver: () async => '${root.path}/settings.sqlite3',
|
||||
secretRootPathResolver: () async => root.path,
|
||||
supportRootPathResolver: () async => root.path,
|
||||
);
|
||||
final controller = AppController(
|
||||
store: store,
|
||||
desktopPlatformService: UnsupportedDesktopPlatformService(),
|
||||
skillDirectoryAccessService: _FakeSkillDirectoryAccessService(root.path),
|
||||
goTaskServiceClient: const _FakeGoTaskServiceClient(),
|
||||
singleAgentSharedSkillScanRootOverrides: const <String>[],
|
||||
availableSingleAgentProvidersOverride: const <SingleAgentProvider>[
|
||||
SingleAgentProvider.codex,
|
||||
],
|
||||
);
|
||||
final inputController = TextEditingController();
|
||||
final focusNode = FocusNode();
|
||||
addTearDown(() async {
|
||||
controller.dispose();
|
||||
inputController.dispose();
|
||||
focusNode.dispose();
|
||||
if (root.existsSync()) {
|
||||
await root.delete(recursive: true);
|
||||
}
|
||||
});
|
||||
|
||||
controller.appUiStateInternal = controller.appUiState.copyWith(
|
||||
savedGatewayTargets: const <String>['gateway'],
|
||||
);
|
||||
controller.lastObservedSettingsSnapshotInternal =
|
||||
controller.settingsController.snapshotInternal;
|
||||
controller.initializeAssistantThreadContext(
|
||||
controller.currentSessionKey,
|
||||
executionTarget: AssistantExecutionTarget.gateway,
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: AppTheme.light(platform: TargetPlatform.macOS),
|
||||
home: Scaffold(
|
||||
body: ComposerBarInternal(
|
||||
controller: controller,
|
||||
inputController: inputController,
|
||||
focusNode: focusNode,
|
||||
thinkingLabel: 'Normal',
|
||||
showModelControl: false,
|
||||
modelLabel: '',
|
||||
modelOptions: const <String>[],
|
||||
attachments: const <ComposerAttachmentInternal>[],
|
||||
availableSkills: const <ComposerSkillOptionInternal>[],
|
||||
selectedSkillKeys: const <String>[],
|
||||
onRemoveAttachment: (_) {},
|
||||
onToggleSkill: (_) {},
|
||||
onThinkingChanged: (_) {},
|
||||
onModelChanged: (_) async {},
|
||||
onOpenGateway: () {},
|
||||
onOpenAiGatewaySettings: () {},
|
||||
onReconnectGateway: () async {},
|
||||
onPickAttachments: () {},
|
||||
onAddAttachment: (_) {},
|
||||
onPasteImageAttachment: () async => null,
|
||||
onContentHeightChanged: (_) {},
|
||||
onInputHeightChanged: (_) {},
|
||||
onSend: () async {},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
|
||||
expect(
|
||||
find.byKey(const Key('assistant-single-agent-provider-button')),
|
||||
findsNothing,
|
||||
);
|
||||
expect(
|
||||
find.byKey(const Key('assistant-gateway-provider-button')),
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
await tester.pumpWidget(const SizedBox.shrink());
|
||||
await tester.pump();
|
||||
});
|
||||
}
|
||||
|
||||
class _FakeSkillDirectoryAccessService implements SkillDirectoryAccessService {
|
||||
|
||||
@ -3,7 +3,6 @@ import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:xworkmate/app/app_controller_desktop_core.dart';
|
||||
import 'package:xworkmate/app/app_controller_desktop_workspace_execution.dart';
|
||||
import 'package:xworkmate/features/assistant/assistant_page_composer_bar.dart';
|
||||
import 'package:xworkmate/features/assistant/assistant_page_composer_clipboard.dart';
|
||||
import 'package:xworkmate/features/assistant/assistant_page_composer_skill_models.dart';
|
||||
@ -17,7 +16,7 @@ import 'package:xworkmate/theme/app_theme.dart';
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
testWidgets('renders composer with project workingDirectory chip', (
|
||||
testWidgets('renders composer with thread provider controls only', (
|
||||
tester,
|
||||
) async {
|
||||
await tester.binding.setSurfaceSize(const Size(1400, 320));
|
||||
@ -61,10 +60,6 @@ void main() {
|
||||
);
|
||||
controller.lastObservedSettingsSnapshotInternal =
|
||||
controller.settingsController.snapshotInternal;
|
||||
await controller.saveAssistantSelectedWorkingDirectoryForSession(
|
||||
controller.currentSessionKey,
|
||||
'${root.path}/project-alpha',
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 6.9 KiB |
Loading…
Reference in New Issue
Block a user