Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions PolyPilot/Components/ExpandedSessionView.razor
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,11 @@
<span class="status-sep">·</span>
<span class="skills-trigger" @onclick="ShowSkillsPopup">@availableSkills.Count skills</span>
}
@if (availableAgents?.Count > 0)
{
<span class="status-sep">·</span>
<span class="skills-trigger" @onclick="ShowAgentsPopup">@availableAgents.Count agents</span>
}
<span class="status-sep">·</span>
<select class="inline-model-select" value="@CurrentModel" @onchange="e => OnSetModel.InvokeAsync(e.Value?.ToString())" disabled="@Session.IsProcessing" title="@(Session.IsProcessing ? "Wait for response to complete" : "Switch model")">
@if (!AvailableModels.Contains(CurrentModel))
Expand Down Expand Up @@ -258,6 +263,7 @@
name.Length <= maxLen ? name : name[..maxLen] + "…";

private List<SkillInfo>? availableSkills;
private List<AgentInfo>? availableAgents;
private string? _lastSkillSessionName;

protected override void OnParametersSet()
Expand All @@ -266,6 +272,7 @@
{
_lastSkillSessionName = Session.Name;
availableSkills = CopilotService.DiscoverAvailableSkills(Session.WorkingDirectory);
availableAgents = CopilotService.DiscoverAvailableAgents(Session.WorkingDirectory);
}
}

Expand Down Expand Up @@ -306,6 +313,44 @@
");
}

private async Task ShowAgentsPopup()
{
if (availableAgents == null || availableAgents.Count == 0) return;
var rows = string.Join("", availableAgents.Select(a =>
{
var desc = string.IsNullOrWhiteSpace(a.Description) ? "" :
$"<div style=\"font-size:13px;color:#a6adc8;margin-top:3px;line-height:1.4\">{EscapeHtml(TruncateDesc(a.Description))}</div>";
return $"<div style=\"padding:8px 14px;border-bottom:1px solid #313244\">" +
$"<div style=\"display:flex;justify-content:space-between;align-items:center;font-size:15px\">" +
$"<span style=\"color:#cdd6f4;font-weight:600\">{EscapeHtml(a.Name)}</span>" +
$"<span style=\"font-size:11px;color:#6c7086;background:rgba(255,255,255,0.05);padding:2px 8px;border-radius:3px;white-space:nowrap;margin-left:8px\">{EscapeHtml(a.Source)}</span></div>" +
desc + "</div>";
}));
var jsHtml = EscapeForJs(rows);
var headerHtml = EscapeForJs("<div style=\"padding:8px 14px;font-size:13px;color:#6c7086;border-bottom:1px solid #313244;font-weight:600\">Available Agents</div>");
await JS.InvokeVoidAsync("eval", $@"
(function(){{
var old = document.getElementById('skills-popup-overlay');
if(old) old.remove();
var trigger = document.querySelectorAll('[class*=""skills-trigger""]');
var el = trigger.length > 1 ? trigger[1] : trigger[0];
var rect = el ? el.getBoundingClientRect() : {{left:20,bottom:60}};
var ov = document.createElement('div');
ov.id = 'skills-popup-overlay';
ov.style.cssText = 'position:fixed;inset:0;z-index:9998;background:rgba(0,0,0,0.3)';
ov.onclick = function(){{ ov.remove(); }};
var popup = document.createElement('div');
var left = Math.max(8, Math.min(rect.left, window.innerWidth - 368));
var bottom = window.innerHeight - rect.top + 8;
popup.style.cssText = 'position:fixed;bottom:'+bottom+'px;left:'+left+'px;z-index:9999;background:#1e1e2e;border:1px solid #45475a;border-radius:10px;padding:6px 0;min-width:240px;max-width:360px;max-height:50vh;overflow-y:auto;box-shadow:0 -4px 20px rgba(0,0,0,0.5)';
popup.innerHTML = '{headerHtml}{jsHtml}';
popup.onclick = function(e){{ e.stopPropagation(); }};
ov.appendChild(popup);
document.body.appendChild(ov);
}})()
");
}

private static string EscapeHtml(string s) =>
s.Replace("&", "&amp;").Replace("<", "&lt;").Replace(">", "&gt;").Replace("\"", "&quot;");

Expand Down
42 changes: 42 additions & 0 deletions PolyPilot/Services/CopilotService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -848,6 +848,47 @@ private static (string? name, string? description) ParseSkillFrontmatter(string
return (name, description);
}

public List<AgentInfo> DiscoverAvailableAgents(string? workingDirectory)
{
var agents = new List<AgentInfo>();
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

try
{
if (!string.IsNullOrEmpty(workingDirectory))
{
foreach (var subdir in new[] { ".github/agents", ".claude/agents", ".copilot/agents" })
{
var agentsDir = Path.Combine(workingDirectory, subdir);
if (Directory.Exists(agentsDir))
ScanAgentDirectory(agentsDir, "project", agents, seen);
}
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[Agents] Discovery failed: {ex.Message}");
}

return agents;
}

private static void ScanAgentDirectory(string agentsDir, string source, List<AgentInfo> agents, HashSet<string> seen)
{
foreach (var file in Directory.GetFiles(agentsDir, "*.md"))
{
try
{
var content = File.ReadAllText(file);
var (name, description) = ParseSkillFrontmatter(content);
if (string.IsNullOrEmpty(name)) name = Path.GetFileNameWithoutExtension(file);
if (seen.Add(name))
agents.Add(new AgentInfo(name, description ?? "", source));
}
catch { }
}
}

/// <summary>
/// Parse a JSON element into a McpLocalServerConfig so the SDK serializes it correctly.
/// </summary>
Expand Down Expand Up @@ -1609,3 +1650,4 @@ public record QuotaInfo(
);

public record SkillInfo(string Name, string Description, string Source);
public record AgentInfo(string Name, string Description, string Source);