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))