Merge branch 'main' into fix-invisible-file-attachments-in-dynamic-assistants

This commit is contained in:
Thorsten Sommer 2026-07-21 10:04:50 +02:00 committed by GitHub
commit 69a924f237
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 74 additions and 40 deletions

View File

@ -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."))));

View File

@ -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."));

View File

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

View File

@ -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,

View File

@ -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.

View File

@ -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"));