mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-04 22:49:40 +00:00
Added an assistant to view the AI Studio logs (#859)
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Sync Flatpak repo (push) Blocked by required conditions
Build and Release / Collect Flatpak artifacts (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Sync Flatpak repo (push) Blocked by required conditions
Build and Release / Collect Flatpak artifacts (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
Co-authored-by: Thorsten Sommer <SommerEngineering@users.noreply.github.com>
This commit is contained in:
1 parent
79fbeb636c
commit
b1459523d9
22 files changed
+1683
-3
No files matched your search
Generated
+17
@@ -242,6 +242,20 @@ version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
|
||||
|
||||
[[package]]
|
||||
name = "ashpd"
|
||||
version = "0.13.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "281e6645758940dee594495e28807a7672ce40f11ebf4df6c22c4fcd59e2689f"
|
||||
dependencies = [
|
||||
"enumflags2",
|
||||
"futures-util",
|
||||
"getrandom 0.4.2",
|
||||
"serde",
|
||||
"tokio",
|
||||
"zbus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "asn1-rs"
|
||||
version = "0.7.1"
|
||||
@@ -4072,6 +4086,7 @@ dependencies = [
|
||||
"aes 0.9.1",
|
||||
"apple-native-keyring-store",
|
||||
"arboard",
|
||||
"ashpd",
|
||||
"async-stream",
|
||||
"axum",
|
||||
"axum-server",
|
||||
@@ -7664,6 +7679,7 @@ dependencies = [
|
||||
"signal-hook-registry",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"tracing",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
@@ -9566,6 +9582,7 @@ dependencies = [
|
||||
"rustix 1.1.4",
|
||||
"serde",
|
||||
"serde_repr",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"uds_windows",
|
||||
"uuid",
|
||||
|
||||
@@ -72,6 +72,7 @@ windows-native-keyring-store = "1.1.0"
|
||||
apple-native-keyring-store = { version = "1.0.0", features = ["keychain"] }
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
ashpd = { version = "0.13.12", default-features = false, features = ["tokio", "open_uri"] }
|
||||
dbus-secret-service-keyring-store = { version = "1.0.0", features = ["crypto-rust"] }
|
||||
webkit2gtk = { version = "2.0.2", features = ["v2_8"] }
|
||||
|
||||
|
||||
+280
-1
@@ -2,10 +2,24 @@
|
||||
use axum::extract::Query;
|
||||
use axum::Json;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::{Path, PathBuf};
|
||||
use tauri_plugin_dialog::{DialogExt, FileDialogBuilder};
|
||||
use crate::api_token::APIToken;
|
||||
use crate::app_window::MAIN_WINDOW;
|
||||
|
||||
#[cfg(any(windows, target_os = "macos"))]
|
||||
use std::process::Command;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
use ashpd::desktop::open_uri::{OpenDirectoryRequest, OpenFileRequest};
|
||||
|
||||
#[cfg(windows)]
|
||||
use std::os::windows::process::CommandExt;
|
||||
|
||||
/// 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 +50,11 @@ pub struct SaveFileOptions {
|
||||
filter: Option<FileTypeFilter>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
pub struct OpenPathOptions {
|
||||
path: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct DirectorySelectionResponse {
|
||||
user_cancelled: bool,
|
||||
@@ -60,6 +79,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 +311,79 @@ pub async fn save_file(_token: APIToken, payload: Json<SaveFileOptions>) -> Json
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn open_path_in_file_manager(
|
||||
_token: APIToken,
|
||||
payload: Json<OpenPathOptions>,
|
||||
) -> Json<OpenPathResponse> {
|
||||
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,
|
||||
});
|
||||
};
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
return match open_path_in_linux_file_manager(&target).await {
|
||||
Ok(()) => {
|
||||
info!("Opened file manager for path: {:?}", target.path);
|
||||
Json(OpenPathResponse {
|
||||
success: true,
|
||||
issue: String::new(),
|
||||
})
|
||||
}
|
||||
|
||||
Err(issue) => {
|
||||
error!(Source = "Tauri"; "{issue}");
|
||||
Json(OpenPathResponse {
|
||||
success: false,
|
||||
issue,
|
||||
})
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(any(windows, target_os = "macos"))]
|
||||
{
|
||||
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<R: tauri::Runtime>(file_dialog: FileDialogBuilder<R>, filter: &Option<FileTypeFilter>) -> FileDialogBuilder<R> {
|
||||
match filter {
|
||||
@@ -296,4 +394,185 @@ fn apply_filter<R: tauri::Runtime>(file_dialog: FileDialogBuilder<R>, filter: &O
|
||||
|
||||
None => file_dialog,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
struct FileManagerTarget {
|
||||
path: PathBuf,
|
||||
reveal_file: bool,
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", test))]
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
enum LinuxPortalOperation {
|
||||
RevealFile,
|
||||
OpenDirectory,
|
||||
}
|
||||
|
||||
fn resolve_file_manager_target(requested_path: &Path) -> Option<FileManagerTarget> {
|
||||
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(any(target_os = "linux", test))]
|
||||
fn linux_portal_operation(target: &FileManagerTarget) -> LinuxPortalOperation {
|
||||
if target.reveal_file {
|
||||
LinuxPortalOperation::RevealFile
|
||||
} else {
|
||||
LinuxPortalOperation::OpenDirectory
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", test))]
|
||||
fn xdg_open_fallback_path(target: &FileManagerTarget) -> &Path {
|
||||
if target.reveal_file {
|
||||
target.path.parent().unwrap_or(&target.path)
|
||||
} else {
|
||||
&target.path
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
enum LinuxPortalError {
|
||||
Unavailable(String),
|
||||
RequestFailed(String),
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
async fn open_path_with_linux_portal(target: &FileManagerTarget) -> Result<(), LinuxPortalError> {
|
||||
let file = std::fs::File::open(&target.path)
|
||||
.map_err(|error| LinuxPortalError::Unavailable(format!("Failed to open the path for the desktop portal: {error}")))?;
|
||||
|
||||
let request = match linux_portal_operation(target) {
|
||||
LinuxPortalOperation::RevealFile => OpenDirectoryRequest::default().send(&file).await,
|
||||
LinuxPortalOperation::OpenDirectory => OpenFileRequest::default().send_file(&file).await,
|
||||
}
|
||||
.map_err(|error| LinuxPortalError::Unavailable(format!("Desktop portal invocation failed: {error}")))?;
|
||||
|
||||
request.response()
|
||||
.map_err(|error| LinuxPortalError::RequestFailed(format!("Desktop portal request failed: {error}")))
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
async fn open_path_with_xdg_open(target: &FileManagerTarget) -> Result<(), String> {
|
||||
let fallback_path = xdg_open_fallback_path(target);
|
||||
let status = tokio::process::Command::new("xdg-open")
|
||||
.arg(fallback_path)
|
||||
.status()
|
||||
.await
|
||||
.map_err(|error| format!("xdg-open failed to start for '{}': {error}", fallback_path.to_string_lossy()))?;
|
||||
|
||||
if status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!("xdg-open failed for '{}' with exit status {status}", fallback_path.to_string_lossy()))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
async fn open_path_in_linux_file_manager(target: &FileManagerTarget) -> Result<(), String> {
|
||||
match open_path_with_linux_portal(target).await {
|
||||
Ok(()) => Ok(()),
|
||||
Err(LinuxPortalError::RequestFailed(error)) => Err(error),
|
||||
Err(LinuxPortalError::Unavailable(portal_error)) => {
|
||||
match open_path_with_xdg_open(target).await {
|
||||
Ok(()) => Ok(()),
|
||||
Err(fallback_error) => Err(format!("{portal_error} Fallback failed: {fallback_error}")),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::fs;
|
||||
|
||||
#[test]
|
||||
fn existing_file_is_revealed_and_falls_back_to_its_parent() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let file_path = temp_dir.path().join("application.log");
|
||||
fs::write(&file_path, "log").unwrap();
|
||||
|
||||
let target = resolve_file_manager_target(&file_path).unwrap();
|
||||
|
||||
assert_eq!(target.path, file_path);
|
||||
assert!(target.reveal_file);
|
||||
assert_eq!(linux_portal_operation(&target), LinuxPortalOperation::RevealFile);
|
||||
assert_eq!(xdg_open_fallback_path(&target), temp_dir.path());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn existing_directory_is_opened_directly() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
|
||||
let target = resolve_file_manager_target(temp_dir.path()).unwrap();
|
||||
|
||||
assert_eq!(target.path, temp_dir.path());
|
||||
assert!(!target.reveal_file);
|
||||
assert_eq!(linux_portal_operation(&target), LinuxPortalOperation::OpenDirectory);
|
||||
assert_eq!(xdg_open_fallback_path(&target), temp_dir.path());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_file_uses_its_existing_parent_directory() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let missing_file = temp_dir.path().join("missing.log");
|
||||
|
||||
let target = resolve_file_manager_target(&missing_file).unwrap();
|
||||
|
||||
assert_eq!(target.path, temp_dir.path());
|
||||
assert!(!target.reveal_file);
|
||||
assert_eq!(linux_portal_operation(&target), LinuxPortalOperation::OpenDirectory);
|
||||
assert_eq!(xdg_open_fallback_path(&target), temp_dir.path());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_path_without_existing_parent_is_rejected() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let invalid_path = temp_dir.path().join("missing-directory").join("missing.log");
|
||||
|
||||
assert!(resolve_file_manager_target(&invalid_path).is_none());
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
Reference in new issue
Block a user