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
7 changes: 5 additions & 2 deletions PolyPilot/App.xaml.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
namespace PolyPilot;
using PolyPilot.Services;

namespace PolyPilot;

public partial class App : Application
{
public App()
public App(INotificationManagerService notificationService)
{
InitializeComponent();
_ = notificationService.InitializeAsync();
}

protected override Window CreateWindow(IActivationState? activationState)
Expand Down
9 changes: 8 additions & 1 deletion PolyPilot/Components/ChatMessageList.razor
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

@* Shared chat message list — used by Dashboard.razor (grid=compact, expanded=full) *@

<div class="chat-message-list @(Compact ? "compact" : "full") @GetLayoutClass()">
<div class="chat-message-list @(Compact ? "compact" : "full") @GetLayoutClass() @GetStyleClass()">
@if (!Messages.Any() && string.IsNullOrEmpty(StreamingContent))
{
<p class="chat-empty">@(Compact ? "No messages yet" : "Start a conversation with Copilot!")</p>
Expand Down Expand Up @@ -108,6 +108,7 @@
[Parameter] public bool Compact { get; set; }
[Parameter] public string? UserAvatarUrl { get; set; }
[Parameter] public ChatLayout Layout { get; set; } = ChatLayout.Default;
[Parameter] public ChatStyle Style { get; set; } = ChatStyle.Normal;

private string GetLayoutClass() => Layout switch
{
Expand All @@ -116,6 +117,12 @@
_ => ""
};

private string GetStyleClass() => Style switch
{
ChatStyle.Minimal => "style-minimal",
_ => ""
};

private IEnumerable<ToolActivity> GetNewActivities()
{
var historyCallIds = Messages.ToList().Where(m => m.ToolCallId != null).Select(m => m.ToolCallId).ToHashSet();
Expand Down
89 changes: 81 additions & 8 deletions PolyPilot/Components/ChatMessageList.razor.css
Original file line number Diff line number Diff line change
Expand Up @@ -12,37 +12,62 @@
}

/* === Compact mode (dashboard cards) === */
.chat-message-list.compact {
gap: 0.15rem;
}

.chat-message-list.compact ::deep .chat-msg {
display: flex;
gap: 0.5rem;
font-size: var(--type-body);
gap: 0.4rem;
font-size: var(--type-callout);
line-height: 1.4;
padding: 0.2rem 0.4rem;
border-radius: 6px;
}

.chat-message-list.compact ::deep .chat-msg.user {
background: rgba(255, 255, 255, 0.04);
}

.chat-message-list.compact ::deep .chat-msg-role {
font-weight: 600;
flex-shrink: 0;
font-size: var(--type-callout);
padding: 0.1rem 0.3rem;
font-size: 10px;
padding: 0.05rem 0.25rem;
border-radius: 3px;
align-self: flex-start;
margin-top: 1px;
letter-spacing: 0.02em;
}

.chat-message-list.compact ::deep .chat-msg.user .chat-msg-role {
background: rgba(78, 168, 209, 0.3);
color: #7fc4e8;
background: rgba(78, 168, 209, 0.15);
color: rgba(127, 196, 232, 0.8);
}

.chat-message-list.compact ::deep .chat-msg.assistant .chat-msg-role {
background: rgba(0, 195, 91, 0.2);
color: #00c35b;
background: rgba(0, 195, 91, 0.1);
color: rgba(0, 195, 91, 0.7);
}

.chat-message-list.compact ::deep .chat-msg.tool .chat-msg-role,
.chat-message-list.compact ::deep .chat-msg.reasoning .chat-msg-role {
background: none;
padding: 0;
font-size: var(--type-callout);
}

.chat-message-list.compact ::deep .chat-msg-text {
color: var(--text-primary);
word-break: break-word;
}

.chat-message-list.compact ::deep .chat-msg.tool .chat-msg-text,
.chat-message-list.compact ::deep .chat-msg.reasoning .chat-msg-text {
color: var(--text-muted);
font-size: var(--type-subhead);
}

.chat-message-list.compact ::deep .chat-msg-text.thinking {
color: var(--text-muted);
font-style: italic;
Expand Down Expand Up @@ -706,3 +731,51 @@
.chat-message-list.full.layout-both-left ::deep .chat-msg.user .chat-msg-content {
border-radius: 12px 12px 12px 0;
}

/* Style: Minimal — full-width, no bubbles (Claude.ai style) */
.chat-message-list.full.style-minimal ::deep .chat-msg {
align-items: flex-start;
gap: 0.6rem;
padding: 0.8rem 1rem;
}
.chat-message-list.full.style-minimal ::deep .chat-msg.user {
background: rgba(255, 255, 255, 0.03);
}
.chat-message-list.full.style-minimal ::deep .chat-msg.user .chat-msg-avatar {
width: 32px;
height: 32px;
}
.chat-message-list.full.style-minimal ::deep .chat-msg.user .chat-msg-content {
background: none;
border: none;
border-radius: 0;
padding: 0;
max-width: 100%;
}
.chat-message-list.full.style-minimal ::deep .chat-msg.assistant .chat-msg-avatar {
width: 32px;
height: 32px;
}
.chat-message-list.full.style-minimal ::deep .chat-msg.assistant .chat-msg-content {
background: none;
border: none;
border-radius: 0;
padding: 0;
max-width: 100%;
}
.chat-message-list.full.style-minimal ::deep .reasoning-block,
.chat-message-list.full.style-minimal ::deep .action-box,
.chat-message-list.full.style-minimal ::deep .task-complete-card,
.chat-message-list.full.style-minimal ::deep .error-card {
margin-left: 0;
margin-right: 0;
}
.chat-message-list.full.style-minimal ::deep .chat-msg.system {
justify-content: center;
}
.chat-message-list.full.style-minimal ::deep .system-text {
font-size: 12px;
text-align: center;
padding: 0.2rem 0.5rem;
opacity: 0.7;
}
51 changes: 49 additions & 2 deletions PolyPilot/Components/ExpandedSessionView.razor
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@
IsProcessing="Session.IsProcessing"
Compact="false"
UserAvatarUrl="@UserAvatarUrl"
Layout="@Layout" />
Layout="@Layout"
Style="@Style" />
</div>

@if (!string.IsNullOrEmpty(Error))
Expand Down Expand Up @@ -141,6 +142,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 All @@ -152,12 +158,12 @@
<option value="@m">@PrettifyModel(m)</option>
}
</select>
<div class="status-spacer"></div>
<span class="status-extra">
@if (UsageInfo != null)
{
@if (UsageInfo.InputTokens.HasValue || UsageInfo.OutputTokens.HasValue)
{
<span class="status-sep">·</span>
<span class="status-tokens">↑@FormatTokenCount(UsageInfo.InputTokens ?? 0) ↓@FormatTokenCount(UsageInfo.OutputTokens ?? 0)</span>
}
@if (UsageInfo.CurrentTokens.HasValue && UsageInfo.TokenLimit.HasValue)
Expand Down Expand Up @@ -187,6 +193,7 @@
[Parameter] public List<PendingImage>? PendingImages { get; set; }
[Parameter] public string? UserAvatarUrl { get; set; }
[Parameter] public ChatLayout Layout { get; set; } = ChatLayout.Default;
[Parameter] public ChatStyle Style { get; set; } = ChatStyle.Normal;
[Parameter] public int FontSize { get; set; } = 20;
[Parameter] public int MessageWindowSize { get; set; } = 25;

Expand Down Expand Up @@ -258,6 +265,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 +274,7 @@
{
_lastSkillSessionName = Session.Name;
availableSkills = CopilotService.DiscoverAvailableSkills(Session.WorkingDirectory);
availableAgents = CopilotService.DiscoverAvailableAgents(Session.WorkingDirectory);
}
}

Expand Down Expand Up @@ -306,6 +315,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
1 change: 1 addition & 0 deletions PolyPilot/Components/ExpandedSessionView.razor.css
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,7 @@
.inline-model-select:hover { color: var(--accent-primary); }
.inline-model-select option { background: var(--bg-tertiary); color: var(--text-primary); }
.status-extra { display: contents; }
.status-spacer { flex: 1; }

.font-size-controls { display: inline-flex; align-items: center; gap: 0.15rem; }
.font-size-btn { background: var(--hover-bg); border: 1px solid var(--hover-bg); border-radius: 4px; color: var(--text-secondary); cursor: pointer; font-size: 0.65rem; font-weight: 600; padding: 0.1rem 0.3rem; line-height: 1; }
Expand Down
4 changes: 4 additions & 0 deletions PolyPilot/Components/Layout/SessionListItem.razor
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@
else
{
<span class="session-name-text" @ondblclick="() => OnStartRename.InvokeAsync()" @ondblclick:stopPropagation="true">@Session.Name</span>
@if (Session.UnreadCount > 0)
{
<span class="unread-badge">@Session.UnreadCount</span>
}
}
</span>
<div class="session-meta-row">
Expand Down
17 changes: 17 additions & 0 deletions PolyPilot/Components/Layout/SessionListItem.razor.css
Original file line number Diff line number Diff line change
Expand Up @@ -239,3 +239,20 @@
font-size: var(--type-callout);
}
}


.unread-badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 18px;
height: 18px;
padding: 0 5px;
border-radius: 9px;
background: var(--accent-primary, #7c5cfc);
color: #fff;
font-size: 11px;
font-weight: 600;
margin-left: 6px;
flex-shrink: 0;
}
2 changes: 2 additions & 0 deletions PolyPilot/Components/Layout/SessionSidebar.razor
Original file line number Diff line number Diff line change
Expand Up @@ -700,6 +700,8 @@ else
{
completedSessions.Remove(name);
CopilotService.SwitchSession(name);
var session = CopilotService.GetSession(name);
if (session != null) session.LastReadMessageCount = session.History.Count;
CopilotService.SaveUiState("/", name);
await OnSessionSelected.InvokeAsync();
if (currentPage != "/")
Expand Down
14 changes: 12 additions & 2 deletions PolyPilot/Components/Pages/Dashboard.razor
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
PendingImages="@(pendingImagesBySession.TryGetValue(session.Name, out var pi2) ? pi2 : null)"
UserAvatarUrl="@CopilotService.GitHubAvatarUrl"
Layout="@CopilotService.ChatLayout"
Style="@CopilotService.ChatStyle"
FontSize="fontSize"
MessageWindowSize="@(expandedMessageCounts.TryGetValue(session.Name, out var emc) ? emc : 25)"
OnSend="() => SendFromCard(session.Name)"
Expand Down Expand Up @@ -163,6 +164,7 @@
<SessionCard Session="session"
Meta="meta"
IsCompleted="isCompleted"
MessageCount="@(cardMessageCounts.TryGetValue(session.Name, out var mc) ? mc : DefaultCardMessageWindow)"
StreamingContent="@(streamingBySession.TryGetValue(session.Name, out var s) ? s : "")"
ActivityText="@(activityBySession.TryGetValue(session.Name, out var a) ? a : "")"
CurrentToolName="@(currentToolBySession.TryGetValue(session.Name, out var t) ? t : "")"
Expand All @@ -172,6 +174,7 @@
PendingImages="@(pendingImagesBySession.TryGetValue(session.Name, out var pi) ? pi : new())"
UserAvatarUrl="@CopilotService.GitHubAvatarUrl"
Layout="@CopilotService.ChatLayout"
Style="@CopilotService.ChatStyle"
IsRenaming="@(cardRenamingSession == session.Name)"
IsMenuOpen="@(cardMenuSession == session.Name)"
OnGoTo="() => GoToSession(session.Name)"
Expand Down Expand Up @@ -579,7 +582,7 @@

await JS.InvokeVoidAsync("restoreDraftsAndFocus", draftsJson, focusId, selStart, selEnd, forceScroll);

// Auto-load more messages when scrolled to top (IntersectionObserver on load-more buttons)
// Auto-load more messages when scrolled to top (expanded view only, not grid cards)
await JS.InvokeVoidAsync("eval", @"
if (!window.__loadMoreObserver) {
window.__loadMoreObserver = new IntersectionObserver(function(entries) {
Expand All @@ -588,7 +591,7 @@
});
}, { threshold: 0.1 });
}
document.querySelectorAll('.load-more-btn').forEach(function(btn) {
document.querySelectorAll('.messages .load-more-btn').forEach(function(btn) {
if (!btn.__observed) { btn.__observed = true; window.__loadMoreObserver.observe(btn); }
});
");
Expand Down Expand Up @@ -1553,6 +1556,9 @@
expandedSession = sessionName;
_needsScrollToBottom = true;
CopilotService.SwitchSession(sessionName);
// Mark messages as read
var session = CopilotService.GetSession(sessionName);
if (session != null) session.LastReadMessageCount = session.History.Count;
CopilotService.SaveUiState("/dashboard", activeSession: sessionName, expandedSession: sessionName, expandedGrid: !isCompactGrid);
}

Expand Down Expand Up @@ -1801,6 +1807,8 @@
await SaveDraftsAndCursor();
expandedSession = sessionName;
CopilotService.SwitchSession(sessionName);
var s = CopilotService.GetSession(sessionName);
if (s != null) s.LastReadMessageCount = s.History.Count;
StateHasChanged();
}

Expand All @@ -1826,6 +1834,8 @@
idx = reverse ? (idx - 1 + sessions.Count) % sessions.Count : (idx + 1) % sessions.Count;
expandedSession = sessions[idx].Name;
CopilotService.SwitchSession(expandedSession);
var cs = CopilotService.GetSession(expandedSession);
if (cs != null) cs.LastReadMessageCount = cs.History.Count;
_focusedInputId = $"input-{expandedSession.Replace(" ", "-")}";
_cursorStart = 0;
_cursorEnd = 0;
Expand Down
Loading