Skip to content
Merged
2 changes: 1 addition & 1 deletion QuickShell.Core.Tests/StartupWarmupCoordinatorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,7 @@ private static QuickShellServices CreateServices(

private static void WaitForCompletion(StartupWarmupCoordinator coordinator, TimeSpan? timeout = null)
{
var deadline = DateTime.UtcNow + (timeout ?? TimeSpan.FromSeconds(5));
var deadline = DateTime.UtcNow + (timeout ?? TimeSpan.FromSeconds(15));
while (!coordinator.IsCompleted && DateTime.UtcNow < deadline)
{
Thread.Sleep(20);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,24 @@ internal sealed class AgentCliSuggestionProvider : ITaskSuggestionProvider

public IReadOnlyList<CommandSuggestionPill> GetSuggestions(TaskSuggestionContext context)
{
var usedCommands = context.ExistingLaunches.Select(e => e.Command).Where(c => !string.IsNullOrWhiteSpace(c)).ToHashSet(StringComparer.OrdinalIgnoreCase);
// Parse existing launches and track used commands.
var usedCommands = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var command in context.ExistingLaunches.Select(launch => launch.Command))
{
if (!string.IsNullOrWhiteSpace(command))
{
usedCommands.Add(command!);
}
}

var pills = new List<CommandSuggestionPill>();
foreach (var def in AgentCliCatalog.Definitions)
{
var detected = def.PathNames.FirstOrDefault(AgentCliCatalog.IsCommandOnPath);
if (detected is null && !AgentCliCatalog.HasProjectMarker(context.WorkspaceDirectory, def)) continue;
var cmd = detected ?? def.Command;
if (usedCommands.Contains(cmd) || usedCommands.Contains(def.Command)) continue;
var score = detected is not null ? AgentCliCatalog.PathDetectedScore : AgentCliCatalog.MarkerFallbackScore;
pills.Add(new CommandSuggestionPill(cmd, TaskTypeCatalog.Agent, "Agent", SuggestionPillPresentation.FormatDisplayTitle(cmd), SuggestionPillPresentation.FormatTooltip("Agent", cmd, productName: def.Title), score, detected is not null ? "agent-path" : "agent-marker"));
var pill = TryCreateSuggestionPill(def, context, usedCommands);
if (pill is not null)
{
pills.Add(pill);
}
}

// Do not Take() here: a provider-level cap hid the rest behind silent replacement
Expand All @@ -28,4 +36,29 @@ public IReadOnlyList<CommandSuggestionPill> GetSuggestions(TaskSuggestionContext
.ThenBy(p => p.DisplayTitle, StringComparer.OrdinalIgnoreCase)
.ToList();
}

private static CommandSuggestionPill? TryCreateSuggestionPill(AgentCliDefinition def, TaskSuggestionContext context, HashSet<string> usedCommands)
{
var detected = def.PathNames.FirstOrDefault(AgentCliCatalog.IsCommandOnPath);
if (detected is null && !AgentCliCatalog.HasProjectMarker(context.WorkspaceDirectory, def))
{
return null;
}

var cmd = detected ?? def.Command;
if (usedCommands.Contains(cmd) || usedCommands.Contains(def.Command))
{
return null;
}

var score = detected is not null ? AgentCliCatalog.PathDetectedScore : AgentCliCatalog.MarkerFallbackScore;
return new CommandSuggestionPill(
cmd,
TaskTypeCatalog.Agent,
"Agent",
SuggestionPillPresentation.FormatDisplayTitle(cmd),
SuggestionPillPresentation.FormatTooltip("Agent", cmd, productName: def.Title),
score,
detected is not null ? "agent-path" : "agent-marker");
}
Comment thread
tonythethompson marked this conversation as resolved.
}
18 changes: 12 additions & 6 deletions QuickShell.Core/Services/TaskTypePickContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,20 @@ internal sealed class TaskTypePickContext

public IReadOnlySet<string> UsedCommands { get; init; } = EmptyUsedCommands.Instance;

public static TaskTypePickContext FromCommands(IEnumerable<string?> commands) =>
new()
public static TaskTypePickContext FromCommands(IEnumerable<string?> commands)
{
// Bolt: Performance optimization - avoid LINQ iterator allocations for parsing existing commands
var usedCommands = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var command in commands.Where(static command => !string.IsNullOrWhiteSpace(command)))
{
UsedCommands = commands
.Where(command => !string.IsNullOrWhiteSpace(command))
.Select(command => command!)
.ToHashSet(StringComparer.OrdinalIgnoreCase),
usedCommands.Add(command!);
}
Comment thread
Copilot marked this conversation as resolved.

return new TaskTypePickContext
{
UsedCommands = usedCommands,
};
}

private sealed class EmptyUsedCommands : IReadOnlySet<string>
{
Expand Down
29 changes: 19 additions & 10 deletions QuickShell.Core/Services/WorkspaceSecurityPolicy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,22 @@ private static WorkspaceAuthorizationResult AuthorizeCore(
break;
}

AssessAdditionalRisks(content, action, risks);
ValidateDirectoryTrust(workspace, action, normalizedDirectory, issues);

var primary = GetPrimaryIssue(issues, action);
var allowed = action switch
{
WorkspaceAction.CopyPath => !issues.Any(issue => issue.Code == WorkspaceIssueCode.InvalidDirectory),
WorkspaceAction.RevokeTrust => true,
WorkspaceAction.GrantTrust => issues.Count == 0,
_ => issues.Count == 0,
};
return BuildResult(allowed, primary, issues, risks, normalizedDirectory, normalizedUrl, executablePath, arguments, content.Command, workspace.Revision);
}

private static void AssessAdditionalRisks(TerminalShortcut content, WorkspaceAction action, List<WorkspaceRisk> risks)
{
var configuredCompanionCount = CompanionAppNormalization.GetConfigured(content).Count;
if (configuredCompanionCount > 0 && action is not WorkspaceAction.StartCompanion and not WorkspaceAction.GrantTrust)
{
Expand All @@ -184,7 +200,10 @@ private static WorkspaceAuthorizationResult AuthorizeCore(
{
risks.Add(new("dev-server", "This workspace opens a configured URL after launch."));
}
}

private static void ValidateDirectoryTrust(StoredWorkspace workspace, WorkspaceAction action, string? normalizedDirectory, List<WorkspaceIssue> issues)
{
if (WorkspaceTrustFeatures.Enabled && !workspace.Security.IsTrusted && RequiresTrust(action))
{
issues.Add(new(WorkspaceIssueCode.WorkspaceUntrusted, "Trust this workspace before starting external processes or opening it."));
Expand All @@ -201,16 +220,6 @@ private static WorkspaceAuthorizationResult AuthorizeCore(
issues.Add(new(WorkspaceIssueCode.DirectoryOpenNotAllowed, "Only existing rooted local drive directories can be opened in Explorer."));
}
}

var primary = GetPrimaryIssue(issues, action);
var allowed = action switch
{
WorkspaceAction.CopyPath => !issues.Any(issue => issue.Code == WorkspaceIssueCode.InvalidDirectory),
WorkspaceAction.RevokeTrust => true,
WorkspaceAction.GrantTrust => issues.Count == 0,
_ => issues.Count == 0,
};
return BuildResult(allowed, primary, issues, risks, normalizedDirectory, normalizedUrl, executablePath, arguments, content.Command, workspace.Revision);
}

private static bool RequiresDirectory(WorkspaceAction action) =>
Expand Down
Loading