fix: sync bridge artifacts into sidebar

This commit is contained in:
Haitao Pan 2026-05-05 12:46:00 +08:00
parent af01142441
commit be24832f80
6 changed files with 147 additions and 8 deletions

View File

@ -327,6 +327,12 @@ extension AppControllerDesktopThreadSessions on AppController {
'';
}
double? assistantArtifactSyncAtMsForSession(String sessionKey) {
return taskThreadForSessionInternal(
normalizedAssistantSessionKeyInternal(sessionKey),
)?.lastArtifactSyncAtMs;
}
Future<AssistantArtifactSnapshot> loadAssistantArtifactSnapshot({
String? sessionKey,
}) {

View File

@ -304,6 +304,10 @@ extension AssistantPageStateClosureInternal on AssistantPageStateInternal {
workspaceKind: controller.assistantWorkspaceKindForSession(
controller.currentSessionKey,
),
artifactSyncAtMs: controller
.assistantArtifactSyncAtMsForSession(
controller.currentSessionKey,
),
onCollapse: () {
setState(() {
artifactPaneCollapsedInternal = true;

View File

@ -420,7 +420,11 @@ class GoTaskServiceArtifact {
}
return GoTaskServiceArtifact(
relativePath: json['relativePath']?.toString().trim() ?? '',
relativePath:
json['relativePath']?.toString().trim() ??
json['path']?.toString().trim() ??
json['name']?.toString().trim() ??
'',
label: json['label']?.toString().trim() ?? '',
contentType: json['contentType']?.toString().trim() ?? '',
encoding: json['encoding']?.toString().trim() ?? '',
@ -544,17 +548,26 @@ class GoTaskServiceResult {
}
Object? _firstGoTaskArtifactList(Map<String, dynamic> result) {
final artifacts = <Object?>[];
for (final candidate in <Object?>[
result['artifacts'],
result['files'],
result['attachments'],
_castMap(result['payload'])['artifacts'],
_castMap(result['payload'])['files'],
_castMap(result['payload'])['attachments'],
_castMap(result['result'])['artifacts'],
_castMap(result['result'])['files'],
_castMap(result['result'])['attachments'],
_castMap(result['data'])['artifacts'],
_castMap(result['data'])['files'],
_castMap(result['data'])['attachments'],
]) {
if (candidate is List) {
return candidate;
artifacts.addAll(candidate);
}
}
return null;
return artifacts.isEmpty ? null : artifacts;
}
String? goTaskServiceGatewayEntryState({

View File

@ -29,6 +29,7 @@ class AssistantArtifactSidebar extends StatefulWidget {
required this.threadTitle,
required this.workspacePath,
required this.workspaceKind,
required this.artifactSyncAtMs,
required this.onCollapse,
required this.loadSnapshot,
required this.loadPreview,
@ -39,6 +40,7 @@ class AssistantArtifactSidebar extends StatefulWidget {
final String threadTitle;
final String workspacePath;
final WorkspaceRefKind workspaceKind;
final double? artifactSyncAtMs;
final VoidCallback onCollapse;
final AssistantArtifactSnapshotLoader loadSnapshot;
final AssistantArtifactPreviewLoader loadPreview;
@ -69,7 +71,8 @@ class _AssistantArtifactSidebarState extends State<AssistantArtifactSidebar> {
super.didUpdateWidget(oldWidget);
if (oldWidget.sessionKey != widget.sessionKey ||
oldWidget.workspacePath != widget.workspacePath ||
oldWidget.workspaceKind != widget.workspaceKind) {
oldWidget.workspaceKind != widget.workspaceKind ||
oldWidget.artifactSyncAtMs != widget.artifactSyncAtMs) {
_activeTab = AssistantArtifactSidebarTab.files;
_selectedEntry = null;
_preview = const AssistantArtifactPreview.empty();
@ -537,10 +540,7 @@ class AssistantArtifactSidebarRevealButton extends StatelessWidget {
side: BorderSide.none,
shape: const CircleBorder(),
),
icon: const Icon(
Icons.keyboard_double_arrow_left_rounded,
size: 20,
),
icon: const Icon(Icons.keyboard_double_arrow_left_rounded, size: 20),
),
);
}

View File

@ -0,0 +1,73 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:xworkmate/runtime/assistant_artifacts.dart';
import 'package:xworkmate/runtime/runtime_models.dart';
import 'package:xworkmate/theme/app_theme.dart';
import 'package:xworkmate/widgets/assistant_artifact_sidebar.dart';
void main() {
testWidgets('refreshes snapshot when artifact sync timestamp changes', (
tester,
) async {
var loadCount = 0;
Future<AssistantArtifactSnapshot> loadSnapshot() async {
loadCount += 1;
return AssistantArtifactSnapshot(
workspacePath: '/tmp/thread',
workspaceKind: WorkspaceRefKind.localPath,
fileEntries: <AssistantArtifactEntry>[
AssistantArtifactEntry(
id: 'entry-$loadCount',
label: 'artifact-$loadCount.txt',
relativePath: 'artifact-$loadCount.txt',
kind: AssistantArtifactEntryKind.file,
mimeType: 'text/plain',
previewable: true,
workspacePath: '/tmp/thread',
),
],
);
}
await tester.pumpWidget(
_buildTestApp(artifactSyncAtMs: 1, loadSnapshot: loadSnapshot),
);
await tester.pumpAndSettle();
expect(loadCount, 1);
expect(find.text('artifact-1.txt'), findsAtLeastNWidgets(1));
await tester.pumpWidget(
_buildTestApp(artifactSyncAtMs: 2, loadSnapshot: loadSnapshot),
);
await tester.pumpAndSettle();
expect(loadCount, 2);
expect(find.text('artifact-2.txt'), findsAtLeastNWidgets(1));
});
}
Widget _buildTestApp({
required double artifactSyncAtMs,
required Future<AssistantArtifactSnapshot> Function() loadSnapshot,
}) {
return MaterialApp(
theme: AppTheme.light(),
home: Material(
child: SizedBox(
width: 460,
height: 640,
child: AssistantArtifactSidebar(
sessionKey: 'session-1',
threadTitle: 'Thread',
workspacePath: '/tmp/thread',
workspaceKind: WorkspaceRefKind.localPath,
artifactSyncAtMs: artifactSyncAtMs,
onCollapse: () {},
loadSnapshot: loadSnapshot,
loadPreview: (_) async => const AssistantArtifactPreview.empty(),
),
),
),
);
}

View File

@ -162,6 +162,49 @@ void main() {
expect(result.artifacts.single.relativePath, 'hello.txt');
expect(result.artifacts.single.content, 'nested artifact body');
});
test('uses bridge files and attachments aliases as artifacts', () {
final result = goTaskServiceResultFromAcpResponse(<String, dynamic>{
'jsonrpc': '2.0',
'id': 'request-id',
'result': <String, dynamic>{
'success': true,
'message': 'created files',
'payload': <String, dynamic>{
'files': <Map<String, dynamic>>[
<String, dynamic>{
'path': 'reports/summary.pdf',
'downloadUrl':
'https://xworkmate-bridge.svc.plus/artifacts/summary.pdf',
'contentType': 'application/pdf',
},
],
},
'data': <String, dynamic>{
'attachments': <Map<String, dynamic>>[
<String, dynamic>{
'name': 'deck.pptx',
'content': 'pptx-body',
'contentType':
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
},
],
},
},
}, route: GoTaskServiceRoute.externalAcpSingle);
expect(result.message, 'created files');
expect(
result.artifacts.map((item) => item.relativePath),
containsAll(<String>['reports/summary.pdf', 'deck.pptx']),
);
expect(
result.artifacts
.singleWhere((item) => item.relativePath == 'reports/summary.pdf')
.downloadUrl,
'https://xworkmate-bridge.svc.plus/artifacts/summary.pdf',
);
});
});
group('GatewayAcpClient authorization', () {