From be24832f805373c7eb47e8c8331f6dd45dcf1d3a Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Tue, 5 May 2026 12:46:00 +0800 Subject: [PATCH] fix: sync bridge artifacts into sidebar --- ...pp_controller_desktop_thread_sessions.dart | 6 ++ .../assistant_page_state_closure.dart | 4 + lib/runtime/go_task_service_client.dart | 19 ++++- lib/widgets/assistant_artifact_sidebar.dart | 10 +-- .../assistant_artifact_sidebar_test.dart | 73 +++++++++++++++++++ .../runtime/gateway_acp_client_auth_test.dart | 43 +++++++++++ 6 files changed, 147 insertions(+), 8 deletions(-) create mode 100644 test/features/assistant/assistant_artifact_sidebar_test.dart diff --git a/lib/app/app_controller_desktop_thread_sessions.dart b/lib/app/app_controller_desktop_thread_sessions.dart index bea88ab0..f52735b8 100644 --- a/lib/app/app_controller_desktop_thread_sessions.dart +++ b/lib/app/app_controller_desktop_thread_sessions.dart @@ -327,6 +327,12 @@ extension AppControllerDesktopThreadSessions on AppController { ''; } + double? assistantArtifactSyncAtMsForSession(String sessionKey) { + return taskThreadForSessionInternal( + normalizedAssistantSessionKeyInternal(sessionKey), + )?.lastArtifactSyncAtMs; + } + Future loadAssistantArtifactSnapshot({ String? sessionKey, }) { diff --git a/lib/features/assistant/assistant_page_state_closure.dart b/lib/features/assistant/assistant_page_state_closure.dart index 555f0d7a..f4e30289 100644 --- a/lib/features/assistant/assistant_page_state_closure.dart +++ b/lib/features/assistant/assistant_page_state_closure.dart @@ -304,6 +304,10 @@ extension AssistantPageStateClosureInternal on AssistantPageStateInternal { workspaceKind: controller.assistantWorkspaceKindForSession( controller.currentSessionKey, ), + artifactSyncAtMs: controller + .assistantArtifactSyncAtMsForSession( + controller.currentSessionKey, + ), onCollapse: () { setState(() { artifactPaneCollapsedInternal = true; diff --git a/lib/runtime/go_task_service_client.dart b/lib/runtime/go_task_service_client.dart index 7a8203e1..d8deb780 100644 --- a/lib/runtime/go_task_service_client.dart +++ b/lib/runtime/go_task_service_client.dart @@ -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 result) { + final artifacts = []; for (final candidate in [ 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({ diff --git a/lib/widgets/assistant_artifact_sidebar.dart b/lib/widgets/assistant_artifact_sidebar.dart index 0f56cfc0..c84ad840 100644 --- a/lib/widgets/assistant_artifact_sidebar.dart +++ b/lib/widgets/assistant_artifact_sidebar.dart @@ -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 { 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), ), ); } diff --git a/test/features/assistant/assistant_artifact_sidebar_test.dart b/test/features/assistant/assistant_artifact_sidebar_test.dart new file mode 100644 index 00000000..4395e5f4 --- /dev/null +++ b/test/features/assistant/assistant_artifact_sidebar_test.dart @@ -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 loadSnapshot() async { + loadCount += 1; + return AssistantArtifactSnapshot( + workspacePath: '/tmp/thread', + workspaceKind: WorkspaceRefKind.localPath, + fileEntries: [ + 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 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(), + ), + ), + ), + ); +} diff --git a/test/runtime/gateway_acp_client_auth_test.dart b/test/runtime/gateway_acp_client_auth_test.dart index 2e9dc57c..44e02c5e 100644 --- a/test/runtime/gateway_acp_client_auth_test.dart +++ b/test/runtime/gateway_acp_client_auth_test.dart @@ -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({ + 'jsonrpc': '2.0', + 'id': 'request-id', + 'result': { + 'success': true, + 'message': 'created files', + 'payload': { + 'files': >[ + { + 'path': 'reports/summary.pdf', + 'downloadUrl': + 'https://xworkmate-bridge.svc.plus/artifacts/summary.pdf', + 'contentType': 'application/pdf', + }, + ], + }, + 'data': { + 'attachments': >[ + { + '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(['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', () {