diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index 14faea58..f3d6328e 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -1648,6 +1648,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T174782740 -- The log file does not exist: {0} UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T1807514273"] = "The log file does not exist: {0}" +-- Could not open the log file location. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T1828231197"] = "Could not open the log file location." + -- Other UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T1849229205"] = "Other" @@ -1666,6 +1669,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T237835357 -- No matching log lines. UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T2511997530"] = "No matching log lines." +-- Could not open the log file location: {0} +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T2533784927"] = "Could not open the log file location: {0}" + -- Source details UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T2686813966"] = "Source details" @@ -1678,6 +1684,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T315262563 -- Loading log file... UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T333036481"] = "Loading log file..." +-- Unknown error +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T3461425987"] = "Unknown error" + -- The log file path is not available yet. UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T3686775689"] = "The log file path is not available yet." @@ -1696,12 +1705,18 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T397479987 -- Usage log UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T4031747274"] = "Usage log" +-- Open in folder +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T4048746540"] = "Open in folder" + -- Log file UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T4129341821"] = "Log file" -- Log Viewer UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T4130241777"] = "Log Viewer" +-- Opened the log file location. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T4162897654"] = "Opened the log file location." + -- Show timestamps UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T469116133"] = "Show timestamps" @@ -8584,24 +8599,42 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T1110203516"] = "Failed -- Failed to store the API key due to an API issue. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T1704298921"] = "Failed to store the API key due to an API issue." +-- The runtime file manager endpoint returned '{0}'. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T2158262203"] = "The runtime file manager endpoint returned '{0}'." + -- Failed to delete the secret data due to an API issue. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T2303057928"] = "Failed to delete the secret data due to an API issue." +-- The runtime file manager endpoint is not available. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T2486847754"] = "The runtime file manager endpoint is not available." + +-- The runtime file manager endpoint failed without details. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T3082220817"] = "The runtime file manager endpoint failed without details." + -- Successfully copied the text to your clipboard UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T3351807428"] = "Successfully copied the text to your clipboard" +-- The path does not exist and its parent folder could not be found. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T3606079336"] = "The path does not exist and its parent folder could not be found." + -- Failed to delete the API key due to an API issue. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T3658273365"] = "Failed to delete the API key due to an API issue." -- Failed to copy the text to your clipboard. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T3724548108"] = "Failed to copy the text to your clipboard." +-- The local file manager command failed: {0} +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T381756374"] = "The local file manager command failed: {0}" + -- Failed to get the API key due to an API issue. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T3875720022"] = "Failed to get the API key due to an API issue." -- Failed to get the secret data due to an API issue. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T4007657575"] = "Failed to get the secret data due to an API issue." +-- The local file manager command did not start. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T613631541"] = "The local file manager command did not start." + -- No update found. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::UPDATESERVICE::T1015418291"] = "No update found." diff --git a/app/MindWork AI Studio/Assistants/LogViewer/AssistantLogViewer.razor b/app/MindWork AI Studio/Assistants/LogViewer/AssistantLogViewer.razor index 45337bfb..a7b9e5fa 100644 --- a/app/MindWork AI Studio/Assistants/LogViewer/AssistantLogViewer.razor +++ b/app/MindWork AI Studio/Assistants/LogViewer/AssistantLogViewer.razor @@ -22,6 +22,9 @@ } + + @T("Open in folder") + diff --git a/app/MindWork AI Studio/Assistants/LogViewer/AssistantLogViewer.razor.cs b/app/MindWork AI Studio/Assistants/LogViewer/AssistantLogViewer.razor.cs index 44aa9d4e..23c7ce09 100644 --- a/app/MindWork AI Studio/Assistants/LogViewer/AssistantLogViewer.razor.cs +++ b/app/MindWork AI Studio/Assistants/LogViewer/AssistantLogViewer.razor.cs @@ -34,6 +34,9 @@ public partial class AssistantLogViewer : MSGComponentBase [Inject] private RustService RustService { get; init; } = null!; + [Inject] + private ISnackbar Snackbar { get; init; } = null!; + [Inject] private NavigationManager NavigationManager { get; init; } = null!; @@ -65,6 +68,8 @@ public partial class AssistantLogViewer : MSGComponentBase private string CurrentLogPath => this.selectedLogFile is LogFileKind.APP ? this.logPaths.LogAppPath : this.logPaths.LogStartupPath; + private bool CanOpenCurrentLogPath => !string.IsNullOrWhiteSpace(this.CurrentLogPath); + private bool HasDropdownFilter => this.selectedLogLevels.Count > 0 || this.selectedLoggers.Count > 0 || this.selectedSourceDetails.Count > 0; private bool HasActiveFilter => !string.IsNullOrWhiteSpace(this.filterText) || this.HasDropdownFilter; @@ -203,6 +208,68 @@ public partial class AssistantLogViewer : MSGComponentBase await this.RefreshLogAsync(); } + private async Task OpenCurrentLogInFileManager() + { + try + { + this.logPaths = await this.RustService.GetLogPaths(); + } + catch (Exception e) + { + this.Logger.LogWarning(e, "Could not refresh the log file paths before opening the file manager."); + this.Snackbar.Add(T("The log file path is not available yet."), Severity.Warning, config => + { + config.Icon = Icons.Material.Filled.Folder; + config.IconSize = Size.Large; + }); + return; + } + + var path = this.CurrentLogPath; + if (string.IsNullOrWhiteSpace(path)) + { + this.Snackbar.Add(T("The log file path is not available yet."), Severity.Warning, config => + { + config.Icon = Icons.Material.Filled.Folder; + config.IconSize = Size.Large; + }); + return; + } + + OpenPathResponse response; + try + { + response = await this.RustService.OpenPathInFileManager(path); + } + catch (Exception e) + { + this.Logger.LogWarning(e, "Could not open the log file location in the file manager."); + this.Snackbar.Add(T("Could not open the log file location."), Severity.Error, config => + { + config.Icon = Icons.Material.Filled.Folder; + config.IconSize = Size.Large; + }); + return; + } + + if (response.Success) + { + this.Snackbar.Add(T("Opened the log file location."), Severity.Success, config => + { + config.Icon = Icons.Material.Filled.FolderOpen; + config.IconSize = Size.Large; + }); + return; + } + + var issue = string.IsNullOrWhiteSpace(response.Issue) ? T("Unknown error") : response.Issue; + this.Snackbar.Add(string.Format(T("Could not open the log file location: {0}"), issue), Severity.Error, config => + { + config.Icon = Icons.Material.Filled.Folder; + config.IconSize = Size.Large; + }); + } + private void ClearFilters() { this.filterText = string.Empty; diff --git a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua index 228c43a9..ffed092d 100644 --- a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua @@ -1650,6 +1650,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T174782740 -- The log file does not exist: {0} UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T1807514273"] = "Die Protokolldatei existiert nicht: {0}" +-- Could not open the log file location. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T1828231197"] = "Konnte den Speicherort der Protokolldatei nicht öffnen." + -- Other UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T1849229205"] = "Andere" @@ -1668,6 +1671,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T237835357 -- No matching log lines. UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T2511997530"] = "Keine passenden Protokollzeilen." +-- Could not open the log file location: {0} +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T2533784927"] = "Konnte den Speicherort der Protokolldatei nicht öffnen: {0}" + -- Source details UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T2686813966"] = "Quellendetails" @@ -1680,6 +1686,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T315262563 -- Loading log file... UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T333036481"] = "Lade Protokolldatei..." +-- Unknown error +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T3461425987"] = "Unbekannter Fehler" + -- The log file path is not available yet. UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T3686775689"] = "Der Pfad zur Protokolldatei ist noch nicht verfügbar." @@ -1698,12 +1707,18 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T397479987 -- Usage log UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T4031747274"] = "Nutzungsprotokoll" +-- Open in folder +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T4048746540"] = "Im Ordner öffnen" + -- Log file UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T4129341821"] = "Protokolldatei" -- Log Viewer UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T4130241777"] = "Protokollanzeige" +-- Opened the log file location. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T4162897654"] = "Der Speicherort der Protokolldatei wurde geöffnet." + -- Show timestamps UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T469116133"] = "Zeitstempel anzeigen" @@ -8586,24 +8601,42 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T1110203516"] = "Fehler -- Failed to store the API key due to an API issue. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T1704298921"] = "Fehler beim Speichern des API-Schlüssels aufgrund eines API-Problems." +-- The runtime file manager endpoint returned '{0}'. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T2158262203"] = "Der Laufzeit-Dateimanager-Endpunkt hat '{0}' zurückgegeben." + -- Failed to delete the secret data due to an API issue. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T2303057928"] = "Das Löschen der geheimen Daten ist aufgrund eines API-Problems fehlgeschlagen." +-- The runtime file manager endpoint is not available. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T2486847754"] = "Der Laufzeit-Dateimanager-Endpunkt ist nicht verfügbar." + +-- The runtime file manager endpoint failed without details. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T3082220817"] = "Der Laufzeit-Dateimanager-Endpunkt ist ohne Details fehlgeschlagen." + -- Successfully copied the text to your clipboard UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T3351807428"] = "Der Text wurde erfolgreich in die Zwischenablage kopiert." +-- The path does not exist and its parent folder could not be found. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T3606079336"] = "Der Pfad existiert nicht, und der übergeordnete Ordner konnte nicht gefunden werden." + -- Failed to delete the API key due to an API issue. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T3658273365"] = "Das API-Schlüssel konnte aufgrund eines API-Problems nicht gelöscht werden." -- Failed to copy the text to your clipboard. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T3724548108"] = "Der Text konnte nicht in die Zwischenablage kopiert werden." +-- The local file manager command failed: {0} +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T381756374"] = "Der Befehl des lokalen Dateimanagers ist fehlgeschlagen: {0}" + -- Failed to get the API key due to an API issue. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T3875720022"] = "Der API-Schlüssel konnte aufgrund eines API-Problems nicht abgerufen werden." -- Failed to get the secret data due to an API issue. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T4007657575"] = "Abrufen der geheimen Daten aufgrund eines API-Problems fehlgeschlagen." +-- The local file manager command did not start. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T613631541"] = "Der Befehl des lokalen Dateimanagers wurde nicht gestartet." + -- No update found. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::UPDATESERVICE::T1015418291"] = "Kein Update gefunden." diff --git a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua index 73711e4b..6be33489 100644 --- a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua @@ -1650,6 +1650,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T174782740 -- The log file does not exist: {0} UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T1807514273"] = "The log file does not exist: {0}" +-- Could not open the log file location. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T1828231197"] = "Could not open the log file location." + -- Other UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T1849229205"] = "Other" @@ -1668,6 +1671,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T237835357 -- No matching log lines. UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T2511997530"] = "No matching log lines." +-- Could not open the log file location: {0} +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T2533784927"] = "Could not open the log file location: {0}" + -- Source details UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T2686813966"] = "Source details" @@ -1680,6 +1686,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T315262563 -- Loading log file... UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T333036481"] = "Loading log file..." +-- Unknown error +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T3461425987"] = "Unknown error" + -- The log file path is not available yet. UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T3686775689"] = "The log file path is not available yet." @@ -1698,12 +1707,18 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T397479987 -- Usage log UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T4031747274"] = "Usage log" +-- Open in folder +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T4048746540"] = "Open in folder" + -- Log file UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T4129341821"] = "Log file" -- Log Viewer UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T4130241777"] = "Log Viewer" +-- Opened the log file location. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T4162897654"] = "Opened the log file location." + -- Show timestamps UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::LOGVIEWER::ASSISTANTLOGVIEWER::T469116133"] = "Show timestamps" @@ -8586,24 +8601,42 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T1110203516"] = "Failed -- Failed to store the API key due to an API issue. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T1704298921"] = "Failed to store the API key due to an API issue." +-- The runtime file manager endpoint returned '{0}'. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T2158262203"] = "The runtime file manager endpoint returned '{0}'." + -- Failed to delete the secret data due to an API issue. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T2303057928"] = "Failed to delete the secret data due to an API issue." +-- The runtime file manager endpoint is not available. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T2486847754"] = "The runtime file manager endpoint is not available." + +-- The runtime file manager endpoint failed without details. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T3082220817"] = "The runtime file manager endpoint failed without details." + -- Successfully copied the text to your clipboard UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T3351807428"] = "Successfully copied the text to your clipboard" +-- The path does not exist and its parent folder could not be found. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T3606079336"] = "The path does not exist and its parent folder could not be found." + -- Failed to delete the API key due to an API issue. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T3658273365"] = "Failed to delete the API key due to an API issue." -- Failed to copy the text to your clipboard. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T3724548108"] = "Failed to copy the text to your clipboard." +-- The local file manager command failed: {0} +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T381756374"] = "The local file manager command failed: {0}" + -- Failed to get the API key due to an API issue. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T3875720022"] = "Failed to get the API key due to an API issue." -- Failed to get the secret data due to an API issue. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T4007657575"] = "Failed to get the secret data due to an API issue." +-- The local file manager command did not start. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T613631541"] = "The local file manager command did not start." + -- No update found. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::UPDATESERVICE::T1015418291"] = "No update found." diff --git a/app/MindWork AI Studio/Tools/Rust/OpenPathRequest.cs b/app/MindWork AI Studio/Tools/Rust/OpenPathRequest.cs new file mode 100644 index 00000000..efa51098 --- /dev/null +++ b/app/MindWork AI Studio/Tools/Rust/OpenPathRequest.cs @@ -0,0 +1,3 @@ +namespace AIStudio.Tools.Rust; + +public readonly record struct OpenPathRequest(string Path); diff --git a/app/MindWork AI Studio/Tools/Rust/OpenPathResponse.cs b/app/MindWork AI Studio/Tools/Rust/OpenPathResponse.cs new file mode 100644 index 00000000..22197783 --- /dev/null +++ b/app/MindWork AI Studio/Tools/Rust/OpenPathResponse.cs @@ -0,0 +1,3 @@ +namespace AIStudio.Tools.Rust; + +public readonly record struct OpenPathResponse(bool Success, string Issue); diff --git a/app/MindWork AI Studio/Tools/Services/RustService.FileSystem.cs b/app/MindWork AI Studio/Tools/Services/RustService.FileSystem.cs index a9c0b337..60388996 100644 --- a/app/MindWork AI Studio/Tools/Services/RustService.FileSystem.cs +++ b/app/MindWork AI Studio/Tools/Services/RustService.FileSystem.cs @@ -1,3 +1,5 @@ +using System.Diagnostics; + using AIStudio.Tools.Rust; namespace AIStudio.Tools.Services; @@ -84,4 +86,109 @@ public sealed partial class RustService return await result.Content.ReadFromJsonAsync(this.jsonRustSerializerOptions); } -} \ No newline at end of file + + public async Task OpenPathInFileManager(string path) + { + var runtimeResponse = await this.TryOpenPathInRuntimeFileManager(path); + if (runtimeResponse.Success) + return runtimeResponse; + + var localResponse = this.TryOpenPathInLocalFileManager(path); + if (localResponse.Success) + return localResponse; + + var issue = string.IsNullOrWhiteSpace(runtimeResponse.Issue) + ? localResponse.Issue + : $"{runtimeResponse.Issue} {localResponse.Issue}"; + + return new OpenPathResponse(false, issue.Trim()); + } + + private async Task TryOpenPathInRuntimeFileManager(string path) + { + try + { + var result = await this.http.PostAsJsonAsync("/open/path", new OpenPathRequest(path), this.jsonRustSerializerOptions); + if (!result.IsSuccessStatusCode) + { + this.logger!.LogWarning("Failed to open a path in the file manager through the Rust runtime: '{StatusCode}'", result.StatusCode); + return new OpenPathResponse(false, string.Format(TB("The runtime file manager endpoint returned '{0}'."), result.StatusCode)); + } + + var response = await result.Content.ReadFromJsonAsync(this.jsonRustSerializerOptions); + return response.Success + ? response + : new OpenPathResponse(false, string.IsNullOrWhiteSpace(response.Issue) ? TB("The runtime file manager endpoint failed without details.") : response.Issue); + } + catch (Exception e) + { + this.logger!.LogWarning(e, "Failed to open a path in the file manager through the Rust runtime."); + return new OpenPathResponse(false, TB("The runtime file manager endpoint is not available.")); + } + } + + private OpenPathResponse TryOpenPathInLocalFileManager(string path) + { + try + { + var target = ResolveFileManagerTarget(path); + if (target is null) + return new OpenPathResponse(false, TB("The path does not exist and its parent folder could not be found.")); + + using var process = Process.Start(CreateFileManagerStartInfo(target.Value)); + return process is null + ? new OpenPathResponse(false, TB("The local file manager command did not start.")) + : new OpenPathResponse(true, string.Empty); + } + catch (Exception e) + { + this.logger!.LogWarning(e, "Failed to open a path in the local file manager."); + return new OpenPathResponse(false, string.Format(TB("The local file manager command failed: {0}"), e.Message)); + } + } + + private static FileManagerTarget? ResolveFileManagerTarget(string path) + { + if (string.IsNullOrWhiteSpace(path)) + return null; + + var requestedPath = path.Trim(); + if (File.Exists(requestedPath)) + return new FileManagerTarget(requestedPath, true); + + if (Directory.Exists(requestedPath)) + return new FileManagerTarget(requestedPath, false); + + var parent = Directory.GetParent(requestedPath)?.FullName; + return !string.IsNullOrWhiteSpace(parent) && Directory.Exists(parent) + ? new FileManagerTarget(parent, false) + : null; + } + + private static ProcessStartInfo CreateFileManagerStartInfo(FileManagerTarget target) + { + if (OperatingSystem.IsWindows()) + { + var windowsInfo = new ProcessStartInfo("explorer.exe") { UseShellExecute = false }; + windowsInfo.ArgumentList.Add(target.RevealFile ? $"/select,{target.Path}" : target.Path); + return windowsInfo; + } + + if (OperatingSystem.IsMacOS()) + { + var macOsInfo = new ProcessStartInfo("open") { UseShellExecute = false }; + if (target.RevealFile) + macOsInfo.ArgumentList.Add("-R"); + + macOsInfo.ArgumentList.Add(target.Path); + return macOsInfo; + } + + var linuxInfo = new ProcessStartInfo("xdg-open") { UseShellExecute = false }; + var directory = target.RevealFile ? Path.GetDirectoryName(target.Path) ?? target.Path : target.Path; + linuxInfo.ArgumentList.Add(directory); + return linuxInfo; + } + + private readonly record struct FileManagerTarget(string Path, bool RevealFile); +} diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.7.3.md b/app/MindWork AI Studio/wwwroot/changelog/v26.7.3.md index 69135cd5..6744da80 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.7.3.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.7.3.md @@ -5,6 +5,7 @@ - Improved update guidance for Flatpak installations and added an enterprise option that lets organizations manage updates entirely through their IT department. - Fixed an issue that could leave AI Studio unresponsive after waking the computer from sleep. - Fixed enterprise configuration plugins from Windows-created ZIP files not loading correctly on Linux when the ZIP contained plugin files inside a folder. +- Fixed file extension handling so files are recognized correctly regardless of uppercase or lowercase letters in their extensions. - Fixed voice recording not starting on Linux. - Upgraded Rust to v1.97.0. - Upgraded Tauri to v2.11.5. diff --git a/runtime/src/file_actions.rs b/runtime/src/file_actions.rs index 3ef7d81d..9ff7376a 100644 --- a/runtime/src/file_actions.rs +++ b/runtime/src/file_actions.rs @@ -2,10 +2,18 @@ use axum::extract::Query; use axum::Json; use serde::{Deserialize, Serialize}; +use std::path::{Path, PathBuf}; +use std::process::Command; +#[cfg(windows)] +use std::os::windows::process::CommandExt; use tauri_plugin_dialog::{DialogExt, FileDialogBuilder}; use crate::api_token::APIToken; use crate::app_window::MAIN_WINDOW; +/// Microsoft documents CREATE_NO_WINDOW as a process creation flag with value 0x08000000. +#[cfg(windows)] +const CREATE_NO_WINDOW: u32 = 0x08000000; + #[derive(Clone, Deserialize)] pub struct PreviousDirectory { path: String, @@ -36,6 +44,11 @@ pub struct SaveFileOptions { filter: Option, } +#[derive(Clone, Deserialize)] +pub struct OpenPathOptions { + path: String, +} + #[derive(Serialize)] pub struct DirectorySelectionResponse { user_cancelled: bool, @@ -60,6 +73,12 @@ pub struct FileSaveResponse { save_file_path: String, } +#[derive(Serialize)] +pub struct OpenPathResponse { + success: bool, + issue: String, +} + #[derive(Clone, Deserialize)] pub struct PreviousFile { file_path: String, @@ -286,6 +305,55 @@ pub async fn save_file(_token: APIToken, payload: Json) -> Json } } +pub async fn open_path_in_file_manager( + _token: APIToken, + payload: Json, +) -> Json { + let requested_path = PathBuf::from(payload.path.trim()); + if requested_path.as_os_str().is_empty() { + return Json(OpenPathResponse { + success: false, + issue: String::from("The path is empty."), + }); + } + + let Some(target) = resolve_file_manager_target(&requested_path) else { + let issue = format!( + "The path does not exist and its parent folder could not be found: {}", + requested_path.to_string_lossy(), + ); + error!(Source = "Tauri"; "{issue}"); + return Json(OpenPathResponse { + success: false, + issue, + }); + }; + + let mut command = create_file_manager_command(&target); + + #[cfg(windows)] + command.creation_flags(CREATE_NO_WINDOW); + + match command.spawn() { + Ok(_) => { + info!("Opened file manager for path: {:?}", target.path); + Json(OpenPathResponse { + success: true, + issue: String::new(), + }) + } + + Err(error) => { + let issue = format!("Failed to open the file manager: {error}"); + error!(Source = "Tauri"; "{issue}"); + Json(OpenPathResponse { + success: false, + issue, + }) + } + } +} + /// Applies an optional file type filter to a FileDialogBuilder. fn apply_filter(file_dialog: FileDialogBuilder, filter: &Option) -> FileDialogBuilder { match filter { @@ -296,4 +364,68 @@ fn apply_filter(file_dialog: FileDialogBuilder, filter: &O None => file_dialog, } -} \ No newline at end of file +} + +struct FileManagerTarget { + path: PathBuf, + reveal_file: bool, +} + +fn resolve_file_manager_target(requested_path: &Path) -> Option { + if requested_path.is_file() { + return Some(FileManagerTarget { + path: requested_path.to_path_buf(), + reveal_file: true, + }); + } + + if requested_path.is_dir() { + return Some(FileManagerTarget { + path: requested_path.to_path_buf(), + reveal_file: false, + }); + } + + requested_path.parent() + .filter(|parent| parent.is_dir()) + .map(|parent| FileManagerTarget { + path: parent.to_path_buf(), + reveal_file: false, + }) +} + +#[cfg(target_os = "windows")] +fn create_file_manager_command(target: &FileManagerTarget) -> Command { + let mut command = Command::new("explorer.exe"); + if target.reveal_file { + command.arg(format!("/select,{}", target.path.to_string_lossy())); + } else { + command.arg(&target.path); + } + + command +} + +#[cfg(target_os = "macos")] +fn create_file_manager_command(target: &FileManagerTarget) -> Command { + let mut command = Command::new("open"); + if target.reveal_file { + command.arg("-R"); + } + + command.arg(&target.path); + command +} + +#[cfg(all(unix, not(target_os = "macos")))] +fn create_file_manager_command(target: &FileManagerTarget) -> Command { + let mut command = Command::new("xdg-open"); + let directory = if target.reveal_file { + target.path.parent().unwrap_or(&target.path) + } else { + &target.path + }; + + command.arg(directory); + command +} diff --git a/runtime/src/runtime_api.rs b/runtime/src/runtime_api.rs index 087c0ffd..5effa200 100644 --- a/runtime/src/runtime_api.rs +++ b/runtime/src/runtime_api.rs @@ -46,6 +46,7 @@ pub fn start_runtime_api() { .route("/select/file", post(crate::file_actions::select_file)) .route("/select/files", post(crate::file_actions::select_files)) .route("/save/file", post(crate::file_actions::save_file)) + .route("/open/path", post(crate::file_actions::open_path_in_file_manager)) .route("/secrets/get", post(crate::secret::get_secret)) .route("/secrets/store", post(crate::secret::store_secret)) .route("/secrets/delete", post(crate::secret::delete_secret))