From e5e1a06e2a3d836cf000add3e5c924733561af05 Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Sun, 28 Jun 2026 12:19:10 +0800 Subject: [PATCH] fix: reveal artifact files without blocking --- .../assistant_page_state_closure.dart | 17 +---- lib/runtime/local_file_revealer.dart | 39 +++++++++++ test/runtime/local_file_revealer_test.dart | 64 +++++++++++++++++++ 3 files changed, 105 insertions(+), 15 deletions(-) create mode 100644 lib/runtime/local_file_revealer.dart create mode 100644 test/runtime/local_file_revealer_test.dart diff --git a/lib/features/assistant/assistant_page_state_closure.dart b/lib/features/assistant/assistant_page_state_closure.dart index 97fe7850..fa3c7e9a 100644 --- a/lib/features/assistant/assistant_page_state_closure.dart +++ b/lib/features/assistant/assistant_page_state_closure.dart @@ -17,6 +17,7 @@ import '../../app/ui_feature_manifest.dart'; import '../../i18n/app_language.dart'; import '../../models/app_models.dart'; import '../../runtime/gateway_acp_client.dart'; +import '../../runtime/local_file_revealer.dart'; import '../../runtime/runtime_models.dart'; import '../../theme/app_palette.dart'; import '../../theme/app_theme.dart'; @@ -404,21 +405,7 @@ extension AssistantPageStateClosureInternal on AssistantPageStateInternal { entry.relativePath.contains(':\\') ? entry.relativePath : '${workspacePath.replaceAll(RegExp(r'[\\/]+$'), '')}${Platform.pathSeparator}${entry.relativePath}'; - if (Platform.isMacOS) { - await Process.run('open', ['-R', targetPath]); - return; - } - if (Platform.isLinux) { - await Process.run('xdg-open', [ - File(targetPath).parent.path, - ]); - return; - } - if (Platform.isWindows) { - await Process.run('explorer.exe', [ - '/select,$targetPath', - ]); - } + await revealLocalFile(targetPath); }, loadSnapshot: () => controller.loadAssistantArtifactSnapshot( sessionKey: activeSessionKey, diff --git a/lib/runtime/local_file_revealer.dart b/lib/runtime/local_file_revealer.dart new file mode 100644 index 00000000..53e09733 --- /dev/null +++ b/lib/runtime/local_file_revealer.dart @@ -0,0 +1,39 @@ +import 'dart:io'; + +typedef DetachedProcessLauncher = + Future Function( + String executable, + List arguments, { + required ProcessStartMode mode, + }); + +Future revealLocalFile( + String targetPath, { + String? operatingSystem, + DetachedProcessLauncher? launchDetached, +}) async { + final launcher = launchDetached ?? _launchDetached; + switch (operatingSystem ?? Platform.operatingSystem) { + case 'macos': + await launcher('open', [ + '-R', + targetPath, + ], mode: ProcessStartMode.detached); + case 'linux': + await launcher('xdg-open', [ + File(targetPath).parent.path, + ], mode: ProcessStartMode.detached); + case 'windows': + await launcher('explorer.exe', [ + '/select,$targetPath', + ], mode: ProcessStartMode.detached); + } +} + +Future _launchDetached( + String executable, + List arguments, { + required ProcessStartMode mode, +}) async { + await Process.start(executable, arguments, mode: mode); +} diff --git a/test/runtime/local_file_revealer_test.dart b/test/runtime/local_file_revealer_test.dart new file mode 100644 index 00000000..a935fe9f --- /dev/null +++ b/test/runtime/local_file_revealer_test.dart @@ -0,0 +1,64 @@ +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:xworkmate/runtime/local_file_revealer.dart'; + +void main() { + group('revealLocalFile', () { + test('reveals the file in Finder on macOS', () async { + final invocation = await _captureInvocation( + operatingSystem: 'macos', + targetPath: '/tmp/thread/report.pdf', + ); + + expect(invocation.executable, 'open'); + expect(invocation.arguments, ['-R', '/tmp/thread/report.pdf']); + expect(invocation.mode, ProcessStartMode.detached); + }); + + test('opens the parent directory on Linux', () async { + final invocation = await _captureInvocation( + operatingSystem: 'linux', + targetPath: '/tmp/thread/reports/report.pdf', + ); + + expect(invocation.executable, 'xdg-open'); + expect(invocation.arguments, ['/tmp/thread/reports']); + expect(invocation.mode, ProcessStartMode.detached); + }); + + test('selects the file in Explorer on Windows', () async { + final invocation = await _captureInvocation( + operatingSystem: 'windows', + targetPath: r'C:\thread\report.pdf', + ); + + expect(invocation.executable, 'explorer.exe'); + expect(invocation.arguments, [r'/select,C:\thread\report.pdf']); + expect(invocation.mode, ProcessStartMode.detached); + }); + }); +} + +Future<_ProcessInvocation> _captureInvocation({ + required String operatingSystem, + required String targetPath, +}) async { + late _ProcessInvocation invocation; + await revealLocalFile( + targetPath, + operatingSystem: operatingSystem, + launchDetached: (executable, arguments, {required mode}) async { + invocation = _ProcessInvocation(executable, arguments, mode); + }, + ); + return invocation; +} + +class _ProcessInvocation { + const _ProcessInvocation(this.executable, this.arguments, this.mode); + + final String executable; + final List arguments; + final ProcessStartMode mode; +}