fix(assistant): rebind stale thread workspaces

This commit is contained in:
Haitao Pan 2026-03-27 19:30:37 +08:00
parent 7187c4843a
commit acaa8e8908
2 changed files with 130 additions and 5 deletions

View File

@ -1344,6 +1344,70 @@ class AppController extends ChangeNotifier {
normalizedRef == settings.workspacePath.trim();
}
bool _usesDefaultThreadWorkspaceRefFromAnotherRoot(
String sessionKey, {
AssistantExecutionTarget? executionTarget,
String? workspaceRef,
WorkspaceRefKind? workspaceRefKind,
}) {
final normalizedSessionKey = _normalizedAssistantSessionKey(sessionKey);
final resolvedTarget =
executionTarget ??
assistantExecutionTargetForSession(normalizedSessionKey);
if (resolvedTarget == AssistantExecutionTarget.remote) {
return false;
}
final normalizedRef = workspaceRef?.trim() ?? '';
if (normalizedRef.isEmpty || workspaceRefKind != WorkspaceRefKind.localPath) {
return false;
}
final expectedDefault = _defaultWorkspaceRefForSession(
normalizedSessionKey,
).trim();
if (expectedDefault.isEmpty) {
return false;
}
final normalizedPath = _trimTrailingPathSeparator(
normalizedRef.replaceAll('\\', '/'),
);
final normalizedExpected = _trimTrailingPathSeparator(
expectedDefault.replaceAll('\\', '/'),
);
if (normalizedPath == normalizedExpected) {
return false;
}
if (normalizedSessionKey == 'main') {
return normalizedPath == SettingsSnapshot.defaults().workspacePath;
}
final expectedSuffix =
'/.xworkmate/threads/${_threadWorkspaceDirectoryName(normalizedSessionKey)}';
return normalizedPath.endsWith(expectedSuffix);
}
bool _shouldMigrateWorkspaceRef(
String sessionKey, {
AssistantExecutionTarget? executionTarget,
String? workspaceRef,
WorkspaceRefKind? workspaceRefKind,
}) {
final normalizedRef = workspaceRef?.trim() ?? '';
if (normalizedRef.isEmpty) {
return true;
}
return _usesLegacySharedWorkspaceRef(
sessionKey,
executionTarget: executionTarget,
workspaceRef: normalizedRef,
workspaceRefKind: workspaceRefKind,
) ||
_usesDefaultThreadWorkspaceRefFromAnotherRoot(
sessionKey,
executionTarget: executionTarget,
workspaceRef: normalizedRef,
workspaceRefKind: workspaceRefKind,
);
}
WorkspaceRefKind _defaultWorkspaceRefKindForTarget(
AssistantExecutionTarget target,
) {
@ -1373,7 +1437,7 @@ class AppController extends ChangeNotifier {
if (existing != null &&
existingWorkspaceRef.isNotEmpty &&
existing.workspaceRefKind == nextWorkspaceRefKind &&
!_usesLegacySharedWorkspaceRef(
!_shouldMigrateWorkspaceRef(
normalizedSessionKey,
executionTarget: resolvedTarget,
workspaceRef: existingWorkspaceRef,
@ -2704,8 +2768,7 @@ class AppController extends ChangeNotifier {
_resolvedUserHomeDirectory = await _skillDirectoryAccessService
.resolveUserHomeDirectory();
await _settingsController.initialize();
_restoreAssistantThreads(await _store.loadAssistantThreadRecords());
await _restoreSharedSingleAgentLocalSkillsCache();
final storedAssistantThreads = await _store.loadAssistantThreadRecords();
if (_disposed) {
return;
}
@ -2737,6 +2800,11 @@ class AppController extends ChangeNotifier {
return;
}
}
_restoreAssistantThreads(storedAssistantThreads);
await _restoreSharedSingleAgentLocalSkillsCache();
if (_disposed) {
return;
}
_lastObservedSettingsSnapshot = settings;
_modelsController.restoreFromSettings(settings.aiGateway);
_multiAgentOrchestrator.updateConfig(settings.multiAgent);
@ -3559,8 +3627,7 @@ class AppController extends ChangeNotifier {
}
final titleFromSettings = assistantCustomTaskTitle(sessionKey);
final shouldMigrateWorkspaceRef =
record.workspaceRef.trim().isEmpty ||
_usesLegacySharedWorkspaceRef(
_shouldMigrateWorkspaceRef(
sessionKey,
executionTarget:
record.executionTarget ?? settings.assistantExecutionTarget,

View File

@ -231,4 +231,62 @@ void main() {
);
},
);
test(
'AppController rebinds default thread workspaces after bootstrap updates the workspace root',
() async {
SharedPreferences.setMockInitialValues(<String, Object>{});
final tempDirectory = await Directory.systemTemp.createTemp(
'xworkmate-thread-workspace-bootstrap-migrate-',
);
addTearDown(() async {
if (await tempDirectory.exists()) {
try {
await tempDirectory.delete(recursive: true);
} catch (_) {}
}
});
final store = SecureConfigStore(
enableSecureStorage: false,
databasePathResolver: () async => '${tempDirectory.path}/settings.db',
fallbackDirectoryPathResolver: () async => tempDirectory.path,
);
await store.initialize();
await store.saveSettingsSnapshot(SettingsSnapshot.defaults());
await store.saveAssistantThreadRecords(<AssistantThreadRecord>[
AssistantThreadRecord(
sessionKey: 'draft:artifact-thread',
messages: const <GatewayChatMessage>[],
updatedAtMs: 1,
title: 'Artifact Thread',
archived: false,
executionTarget: AssistantExecutionTarget.singleAgent,
messageViewMode: AssistantMessageViewMode.rendered,
workspaceRef: '/opt/data/.xworkmate/threads/draft-artifact-thread',
workspaceRefKind: WorkspaceRefKind.localPath,
),
]);
final controller = AppController(store: store);
addTearDown(controller.dispose);
final deadline = DateTime.now().add(const Duration(seconds: 5));
while (controller.initializing) {
if (DateTime.now().isAfter(deadline)) {
fail('controller did not initialize in time');
}
await Future<void>.delayed(const Duration(milliseconds: 20));
}
expect(controller.settings.workspacePath, isNot('/opt/data'));
final migratedWorkspace = controller.assistantWorkspaceRefForSession(
'draft:artifact-thread',
);
expect(
migratedWorkspace,
'${controller.settings.workspacePath}/.xworkmate/threads/draft-artifact-thread',
);
expect(Directory(migratedWorkspace).existsSync(), isTrue);
},
);
}