feat: sync existing workspace directory artifacts recursively

This commit is contained in:
Haitao Pan 2026-05-29 19:17:38 +08:00
parent fcc579e679
commit 7bf9ed4e40
2 changed files with 148 additions and 2 deletions

View File

@ -792,7 +792,17 @@ extension AppControllerDesktopRuntimeHelpers on AppController {
}
final bytes = bytesResult.bytes;
if (bytes == null) {
skippedArtifact = true;
final existingArtifactPaths =
await _existingWorkspaceArtifactPathsInternal(root, relativePath);
if (existingArtifactPaths.isEmpty) {
skippedArtifact = true;
continue;
}
wroteArtifact = true;
_appendArtifactRelativePathsInternal(
currentTaskArtifactRelativePaths,
existingArtifactPaths,
);
continue;
}
final target = await _nextArtifactTargetFileInternal(root, relativePath);
@ -813,7 +823,10 @@ extension AppControllerDesktopRuntimeHelpers on AppController {
target.path,
);
if (writtenRelativePath != null && writtenRelativePath.isNotEmpty) {
currentTaskArtifactRelativePaths.add(writtenRelativePath);
_appendArtifactRelativePathsInternal(
currentTaskArtifactRelativePaths,
<String>[writtenRelativePath],
);
}
}
@ -1145,6 +1158,57 @@ extension AppControllerDesktopRuntimeHelpers on AppController {
) => kGatewayRemoteProfileIndex;
}
void _appendArtifactRelativePathsInternal(
List<String> target,
List<String> paths,
) {
for (final path in paths) {
if (!target.contains(path)) {
target.add(path);
}
}
}
Future<List<String>> _existingWorkspaceArtifactPathsInternal(
Directory root,
String relativePath,
) async {
final targetPath = DesktopThreadArtifactService.resolveAbsolutePathInternal(
root.path,
relativePath,
);
final targetType = await FileSystemEntity.type(
targetPath,
followLinks: false,
);
if (targetType == FileSystemEntityType.file) {
final resolvedRelativePath =
DesktopThreadArtifactService.relativePathInternal(
root.path,
targetPath,
);
return resolvedRelativePath == null || resolvedRelativePath.isEmpty
? const <String>[]
: <String>[resolvedRelativePath];
}
if (targetType != FileSystemEntityType.directory) {
return const <String>[];
}
final files = await DesktopThreadArtifactService().collectFilesInternal(
Directory(targetPath),
);
final paths = <String>[];
for (final file in files) {
final resolvedRelativePath =
DesktopThreadArtifactService.relativePathInternal(root.path, file.path);
if (resolvedRelativePath != null && resolvedRelativePath.isNotEmpty) {
paths.add(resolvedRelativePath);
}
}
paths.sort();
return paths;
}
String _normalizeAuthorizationHeaderInternal(String raw) {
final trimmed = raw.trim();
if (trimmed.isEmpty) {

View File

@ -589,6 +589,88 @@ void main() {
},
);
test('syncs existing workspace directory artifacts recursively', () async {
final controller = AppController(
environmentOverride: const <String, String>{},
);
addTearDown(controller.dispose);
final localWorkspace = await Directory.systemTemp.createTemp(
'xworkmate-recursive-artifact-workspace-',
);
addTearDown(() async {
if (await localWorkspace.exists()) {
await localWorkspace.delete(recursive: true);
}
});
await Directory(
'${localWorkspace.path}/assets/images/chapters',
).create(recursive: true);
await File(
'${localWorkspace.path}/assets/images/cover.png',
).writeAsBytes(<int>[1, 2, 3]);
await File(
'${localWorkspace.path}/assets/images/chapters/chapter-1.png',
).writeAsBytes(<int>[4, 5, 6]);
await File(
'${localWorkspace.path}/chapters/codex-chapter-breakdown.md',
).create(recursive: true);
controller.upsertTaskThreadInternal(
'unit-fixture-task-a',
workspaceBinding: WorkspaceBinding(
workspaceId: 'unit-fixture-task-a',
workspaceKind: WorkspaceKind.localFs,
workspacePath: localWorkspace.path,
displayPath: localWorkspace.path,
writable: true,
),
);
final result = GoTaskServiceResult(
success: true,
message: 'generated files',
turnId: 'turn-recursive',
raw: <String, dynamic>{
'artifacts': <Map<String, dynamic>>[
<String, dynamic>{'relativePath': 'assets/images/'},
<String, dynamic>{
'relativePath': 'chapters/codex-chapter-breakdown.md',
},
],
},
errorMessage: '',
resolvedModel: '',
route: GoTaskServiceRoute.externalAcpSingle,
);
await controller.persistGoTaskArtifactsForSessionInternal(
'unit-fixture-task-a',
result,
);
final thread = controller.requireTaskThreadForSessionInternal(
'unit-fixture-task-a',
);
expect(thread.lastArtifactSyncStatus, 'synced');
expect(thread.lastTaskArtifactRelativePaths, <String>[
'assets/images/chapters/chapter-1.png',
'assets/images/cover.png',
'chapters/codex-chapter-breakdown.md',
]);
final snapshot = await controller.loadAssistantArtifactSnapshot(
sessionKey: 'unit-fixture-task-a',
);
expect(
snapshot.resultEntries.map((entry) => entry.relativePath),
containsAll(<String>[
'assets/images/chapters/chapter-1.png',
'assets/images/cover.png',
'chapters/codex-chapter-breakdown.md',
]),
);
});
test(
'downloads bridge URL artifacts into the local thread workspace',
() async {