mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-08-11 20:12:12 +00:00
Merge branch 'main' into fix-invisible-file-attachments-in-dynamic-assistants
This commit is contained in:
commit
69a924f237
@ -117,10 +117,14 @@ public sealed class AssistantAuditAgent(ILogger<AssistantAuditAgent> logger, ILo
|
||||
/// <summary>
|
||||
/// Resolves and stores the provider configuration used for assistant plugin audits.
|
||||
/// </summary>
|
||||
/// <param name="fallbackProvider">The provider to use when no provider is configured for the audit agent.</param>
|
||||
/// <returns>The configured provider, or <see cref="AIStudio.Settings.Provider.NONE"/> when no audit provider is configured.</returns>
|
||||
public AIStudio.Settings.Provider ResolveProvider()
|
||||
public AIStudio.Settings.Provider ResolveProvider(AIStudio.Settings.Provider? fallbackProvider = null)
|
||||
{
|
||||
var provider = this.SettingsManager.GetPreselectedProvider(Tools.Components.AGENT_ASSISTANT_PLUGIN_AUDIT, null, true);
|
||||
if (provider == AIStudio.Settings.Provider.NONE && fallbackProvider is not null)
|
||||
provider = fallbackProvider;
|
||||
|
||||
this.ProviderSettings = provider;
|
||||
return provider;
|
||||
}
|
||||
@ -130,12 +134,13 @@ public sealed class AssistantAuditAgent(ILogger<AssistantAuditAgent> logger, ILo
|
||||
/// </summary>
|
||||
/// <param name="plugin">The assistant plugin to audit.</param>
|
||||
/// <param name="token">A cancellation token for prompt generation and the audit request.</param>
|
||||
/// <param name="fallbackProvider">The provider to use when no provider is configured for the audit agent.</param>
|
||||
/// <returns>
|
||||
/// The parsed audit result, or an <c>UNKNOWN</c> result when no provider is configured or the model response cannot be used.
|
||||
/// </returns>
|
||||
public async Task<AssistantAuditResult> AuditAsync(PluginAssistants plugin, CancellationToken token = default)
|
||||
public async Task<AssistantAuditResult> AuditAsync(PluginAssistants plugin, CancellationToken token = default, AIStudio.Settings.Provider? fallbackProvider = null)
|
||||
{
|
||||
var provider = this.ResolveProvider();
|
||||
var provider = this.ResolveProvider(fallbackProvider);
|
||||
if (provider == AIStudio.Settings.Provider.NONE)
|
||||
{
|
||||
await MessageBus.INSTANCE.SendError(new (Icons.Material.Filled.SettingsSuggest, string.Format(TB("No provider is configured for the Security Audit Agent."))));
|
||||
|
||||
@ -572,7 +572,7 @@ public partial class AssistantBuilder : AssistantBaseCore<NoSettingsPanel>
|
||||
this.isAuditingPlugin = true;
|
||||
try
|
||||
{
|
||||
this.pluginAudit = await this.AssistantPluginAuditService.RunAuditAsync(this.installedAssistantPlugin);
|
||||
this.pluginAudit = await this.AssistantPluginAuditService.RunAuditAsync(this.installedAssistantPlugin, fallbackProvider: this.ProviderSettings);
|
||||
if (this.pluginAudit.Level is AssistantAuditLevel.UNKNOWN)
|
||||
{
|
||||
this.FailInstallStep(BuilderInstallStep.SECURITY_CHECK, T("The security check could not determine a result."));
|
||||
|
||||
@ -7,9 +7,12 @@ namespace AIStudio.Tools.PluginSystem.Assistants;
|
||||
/// </summary>
|
||||
public sealed class AssistantPluginAuditService(AssistantAuditAgent auditAgent)
|
||||
{
|
||||
public async Task<PluginAssistantAudit> RunAuditAsync(PluginAssistants plugin, CancellationToken token = default)
|
||||
/// <summary>
|
||||
/// Runs an assistant plugin audit, optionally falling back to the supplied provider when no audit provider is configured.
|
||||
/// </summary>
|
||||
public async Task<PluginAssistantAudit> RunAuditAsync(PluginAssistants plugin, CancellationToken token = default, Settings.Provider? fallbackProvider = null)
|
||||
{
|
||||
var result = await auditAgent.AuditAsync(plugin, token);
|
||||
var result = await auditAgent.AuditAsync(plugin, token, fallbackProvider);
|
||||
var provider = auditAgent.ProviderSettings;
|
||||
var promptPreview = await plugin.BuildAuditPromptPreviewAsync(token);
|
||||
|
||||
|
||||
@ -149,12 +149,12 @@ public sealed class GlobalShortcutService : BackgroundService, IMessageBusReceiv
|
||||
&& !string.Equals(lastNonEmptyShortcut, requestedState.Shortcut, StringComparison.Ordinal);
|
||||
|
||||
var result = await this.rustService.UpdateGlobalShortcut(shortcutId, requestedState.Shortcut, description, reconfigure);
|
||||
this.lastSentStates[shortcutId] = requestedState;
|
||||
if (!string.IsNullOrWhiteSpace(requestedState.Shortcut))
|
||||
this.lastNonEmptyShortcuts[shortcutId] = requestedState.Shortcut;
|
||||
|
||||
if (result.Success)
|
||||
{
|
||||
this.lastSentStates[shortcutId] = requestedState;
|
||||
if (!string.IsNullOrWhiteSpace(requestedState.Shortcut))
|
||||
this.lastNonEmptyShortcuts[shortcutId] = requestedState.Shortcut;
|
||||
|
||||
this.logger.LogInformation(
|
||||
"Global shortcut '{ShortcutId}' ({Shortcut}) synchronized through {Backend}.",
|
||||
shortcutId,
|
||||
|
||||
@ -10,6 +10,7 @@
|
||||
- Improved secure API-key storage diagnostics on Linux. AI Studio now provides specific guidance when the default password collection is missing or locked, a password-manager prompt is dismissed, or no compatible Secret Service is available.
|
||||
- Improved the file dialogs to prevent opening multiple times when you click "Open" or "Save" multiple times in a row.
|
||||
- Improved assistant plugins so they clearly indicate when content from a document has been loaded and clear the indicator when the assistant is reset.
|
||||
- Improved the Assistant Builder security check so it can use the selected provider when no dedicated security audit agent provider is configured.
|
||||
- Fixed an issue that could leave AI Studio unresponsive after waking the computer from sleep. Yes, we know this was an annoying bug, and we apologize for the inconvenience.
|
||||
- Fixed connections to internal HTTPS services and enterprise configuration servers that use organization-provided root certificates on Linux.
|
||||
- Fixed enterprise configuration plugins from Windows-created ZIP files may not load correctly on Linux when the ZIP contained plugin files inside a folder.
|
||||
|
||||
@ -11,6 +11,7 @@ use once_cell::sync::Lazy;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum_macros::Display;
|
||||
use tauri_plugin_global_shortcut::GlobalShortcutExt;
|
||||
use tauri_plugin_global_shortcut::ShortcutState;
|
||||
use tokio::sync::{Mutex, broadcast};
|
||||
|
||||
use crate::app_window::{Event, TauriEventType};
|
||||
@ -217,6 +218,7 @@ pub async fn register(
|
||||
let Some(app_handle) = app_handle else {
|
||||
return ShortcutResponse::error("Main window not available", ShortcutBackend::None, false);
|
||||
};
|
||||
|
||||
let Some(event_sender) = event_sender else {
|
||||
return ShortcutResponse::error("Event broadcast not initialized", ShortcutBackend::None, false);
|
||||
};
|
||||
@ -242,24 +244,22 @@ pub async fn register(
|
||||
return ShortcutResponse::success(ShortcutBackend::Portal, effective_display_name);
|
||||
},
|
||||
|
||||
Err(error) if may_fallback_to_tauri(
|
||||
error.kind,
|
||||
manager.bindings.get(&request.id).map(ActiveBinding::backend),
|
||||
) => {
|
||||
warn!(Source = "XDG portal"; "Global shortcuts portal is unavailable; using the Tauri X11 backend: {}", error.message);
|
||||
},
|
||||
|
||||
Err(error) => {
|
||||
let cancelled = error.kind == PortalFailureKind::Cancelled;
|
||||
if cancelled {
|
||||
warn!(Source = "XDG portal"; "Global shortcut configuration was cancelled by the user.");
|
||||
} else if error.kind == PortalFailureKind::Denied {
|
||||
warn!(Source = "XDG portal"; "Global shortcut permission was denied: {}", error.message);
|
||||
let current_backend = manager.bindings.get(&request.id).map(ActiveBinding::backend);
|
||||
if may_fallback_to_tauri(error.kind, current_backend) {
|
||||
warn!(Source = "XDG portal"; "Global shortcut registration failed; using the Tauri X11 backend: {}", error.message);
|
||||
} else {
|
||||
error!(Source = "XDG portal"; "Global shortcut registration failed: {}", error.message);
|
||||
}
|
||||
let cancelled = error.kind == PortalFailureKind::Cancelled;
|
||||
if cancelled {
|
||||
warn!(Source = "XDG portal"; "Global shortcut configuration was cancelled by the user; preserving the active portal binding.");
|
||||
} else if error.kind == PortalFailureKind::Denied {
|
||||
warn!(Source = "XDG portal"; "Global shortcut permission was denied; preserving the active portal binding: {}", error.message);
|
||||
} else {
|
||||
error!(Source = "XDG portal"; "Global shortcut registration failed; preserving the active portal binding: {}", error.message);
|
||||
}
|
||||
|
||||
return ShortcutResponse::error(error.message, ShortcutBackend::Portal, cancelled);
|
||||
return ShortcutResponse::error(error.message, ShortcutBackend::Portal, cancelled);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -349,15 +349,24 @@ fn register_tauri_binding(
|
||||
shortcut_id: Shortcut,
|
||||
event_sender: broadcast::Sender<Event>,
|
||||
) -> Result<(), tauri_plugin_global_shortcut::Error> {
|
||||
app_handle.global_shortcut().on_shortcut(shortcut, move |_app, _shortcut, _event| {
|
||||
if PROCESSING_SUSPENDED.load(Ordering::Relaxed) {
|
||||
app_handle.global_shortcut().on_shortcut(shortcut, move |_app, _shortcut, event| {
|
||||
if !should_forward_tauri_event(event.state) || PROCESSING_SUSPENDED.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
|
||||
send_shortcut_pressed(&event_sender, shortcut_id, "Tauri");
|
||||
info!(Source = "Tauri"; "Tauri shortcut callback received for '{}'.", shortcut_id);
|
||||
let sender = event_sender.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
send_shortcut_pressed(&sender, shortcut_id, "Tauri");
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns whether a native shortcut event represents the single actionable key press.
|
||||
fn should_forward_tauri_event(state: ShortcutState) -> bool {
|
||||
state == ShortcutState::Pressed
|
||||
}
|
||||
|
||||
/// Publishes a shortcut activation using the existing runtime event format.
|
||||
fn send_shortcut_pressed(event_sender: &broadcast::Sender<Event>, shortcut_id: Shortcut, source: &str) {
|
||||
info!(Source = "Global shortcuts"; "Global shortcut triggered through {source} for '{}'.", shortcut_id);
|
||||
@ -438,9 +447,9 @@ enum PortalFailureKind {
|
||||
Technical,
|
||||
}
|
||||
|
||||
/// Determines whether an unavailable portal may safely fall back to Tauri.
|
||||
fn may_fallback_to_tauri(failure: PortalFailureKind, current_backend: Option<ShortcutBackend>) -> bool {
|
||||
failure == PortalFailureKind::Unavailable && current_backend.is_none_or(|backend| backend == ShortcutBackend::Tauri)
|
||||
/// Determines whether a failed portal attempt may safely fall back to Tauri.
|
||||
fn may_fallback_to_tauri(_failure: PortalFailureKind, current_backend: Option<ShortcutBackend>) -> bool {
|
||||
current_backend != Some(ShortcutBackend::Portal)
|
||||
}
|
||||
|
||||
/// Determines whether a backend must unregister its shortcut during suspension.
|
||||
@ -937,14 +946,23 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Verifies that fallback is restricted to unavailable portals and safe active states.
|
||||
fn fallback_is_limited_to_an_unavailable_portal() {
|
||||
assert!(may_fallback_to_tauri(PortalFailureKind::Unavailable, None));
|
||||
assert!(may_fallback_to_tauri(PortalFailureKind::Unavailable, Some(ShortcutBackend::Tauri)));
|
||||
assert!(!may_fallback_to_tauri(PortalFailureKind::Unavailable, Some(ShortcutBackend::Portal)));
|
||||
assert!(!may_fallback_to_tauri(PortalFailureKind::Cancelled, None));
|
||||
assert!(!may_fallback_to_tauri(PortalFailureKind::Denied, None));
|
||||
assert!(!may_fallback_to_tauri(PortalFailureKind::Technical, None));
|
||||
/// Verifies that all initial portal failures use the Tauri fallback.
|
||||
fn all_initial_portal_failures_use_tauri_fallback() {
|
||||
for failure in [
|
||||
PortalFailureKind::Unavailable,
|
||||
PortalFailureKind::Cancelled,
|
||||
PortalFailureKind::Denied,
|
||||
PortalFailureKind::Technical,
|
||||
] {
|
||||
assert!(may_fallback_to_tauri(failure, None));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Verifies that a failed reconfiguration never replaces an active portal binding.
|
||||
fn failed_reconfiguration_preserves_portal_binding() {
|
||||
assert!(!may_fallback_to_tauri(PortalFailureKind::Cancelled, Some(ShortcutBackend::Portal)));
|
||||
assert!(!may_fallback_to_tauri(PortalFailureKind::Denied, Some(ShortcutBackend::Portal)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -954,10 +972,17 @@ mod tests {
|
||||
assert!(unregister_backend_during_suspend(ShortcutBackend::Tauri));
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Verifies that Tauri key releases cannot trigger a second shortcut event.
|
||||
fn tauri_only_forwards_pressed_events() {
|
||||
assert!(should_forward_tauri_event(ShortcutState::Pressed));
|
||||
assert!(!should_forward_tauri_event(ShortcutState::Released));
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[test]
|
||||
/// Verifies recognition of unavailable-portal D-Bus errors without misclassifying rejection.
|
||||
fn only_unavailable_portal_errors_allow_fallback() {
|
||||
fn recognizes_unavailable_portal_errors() {
|
||||
assert!(portal_error_is_unavailable("org.freedesktop.DBus.Error.UnknownMethod"));
|
||||
assert!(portal_error_is_unavailable("ServiceUnknown"));
|
||||
assert!(!portal_error_is_unavailable("Portal request was cancelled"));
|
||||
|
||||
Loading…
Reference in New Issue
Block a user