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
3 changes: 2 additions & 1 deletion PolyPilot.Tests/TestStubs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ internal class StubWsBridgeClient : IWsBridgeClient
public List<SessionSummary> Sessions { get; set; } = new();
public string? ActiveSessionName { get; set; }
public System.Collections.Concurrent.ConcurrentDictionary<string, List<ChatMessage>> SessionHistories { get; } = new();
public System.Collections.Concurrent.ConcurrentDictionary<string, bool> SessionHistoryHasMore { get; } = new();
public List<PersistedSessionSummary> PersistedSessions { get; set; } = new();
public string? GitHubAvatarUrl { get; set; }
public string? GitHubLogin { get; set; }
Expand Down Expand Up @@ -87,7 +88,7 @@ public Task RequestSessionsAsync(CancellationToken ct = default)
RequestSessionsCallCount++;
return Task.CompletedTask;
}
public Task RequestHistoryAsync(string sessionName, CancellationToken ct = default) => Task.CompletedTask;
public Task RequestHistoryAsync(string sessionName, int? limit = null, CancellationToken ct = default) => Task.CompletedTask;
public Task SendMessageAsync(string sessionName, string message, CancellationToken ct = default) => Task.CompletedTask;
public Task CreateSessionAsync(string name, string? model = null, string? workingDirectory = null, CancellationToken ct = default) => Task.CompletedTask;
public string? LastSwitchedSession { get; private set; }
Expand Down
28 changes: 24 additions & 4 deletions PolyPilot/Components/ExpandedSessionView.razor
Original file line number Diff line number Diff line change
Expand Up @@ -105,11 +105,27 @@

<div class="messages expanded-messages">
@{ var expandedMessages = GetWindowedMessages(); }
@if (Session.History.Count > expandedMessages.Count)
@if (IsLoadingHistory)
{
<button class="load-more-btn" @onclick="LoadMore">
⬆️ Load more (@(Session.History.Count - expandedMessages.Count) remaining)
</button>
<div class="loading-history-indicator">
<div class="loading-history-spinner"></div>
<span>Loading conversation…</span>
</div>
}
else
{
@if (HasMoreRemoteHistory)
{
<button class="load-more-btn" @onclick="LoadFullHistory">
⬆️ Load rest of conversation
</button>
}
else if (Session.History.Count > expandedMessages.Count)
{
<button class="load-more-btn" @onclick="LoadMore">
⬆️ Load more (@(Session.History.Count - expandedMessages.Count) remaining)
</button>
}
}
<ChatMessageList Messages="expandedMessages"
StreamingContent="@StreamingContent"
Expand Down Expand Up @@ -267,6 +283,8 @@
[Parameter] public string? UserAvatarUrl { get; set; }
[Parameter] public ChatLayout Layout { get; set; } = ChatLayout.Default;
[Parameter] public ChatStyle Style { get; set; } = ChatStyle.Normal;
[Parameter] public bool IsLoadingHistory { get; set; }
[Parameter] public bool HasMoreRemoteHistory { get; set; }
[Parameter] public int FontSize { get; set; } = 20;
[Parameter] public int MessageWindowSize { get; set; } = 25;
[Parameter] public bool FiestaActive { get; set; }
Expand All @@ -285,6 +303,7 @@

[Parameter] public EventCallback<int> OnFontSizeChange { get; set; }
[Parameter] public EventCallback<string> OnLoadMore { get; set; }
[Parameter] public EventCallback<string> OnLoadFullHistory { get; set; }
[Parameter] public EventCallback<(string SessionName, FiestaStartRequest Request)> OnStartFiesta { get; set; }
[Parameter] public EventCallback<string> OnStopFiesta { get; set; }
[Parameter] public EventCallback OnStopReflection { get; set; }
Expand All @@ -302,6 +321,7 @@
}

private void LoadMore() => OnLoadMore.InvokeAsync(Session.Name);
private void LoadFullHistory() => OnLoadFullHistory.InvokeAsync(Session.Name);

private static string PrettifyModel(string modelId)
{
Expand Down
24 changes: 24 additions & 0 deletions PolyPilot/Components/ExpandedSessionView.razor.css
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,30 @@
color: var(--text-primary);
}

/* Loading history indicator (remote mode) */
.loading-history-indicator {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.75rem;
padding: 2rem;
flex: 1;
color: var(--text-secondary);
font-size: var(--type-callout, 0.85rem);
}

.loading-history-spinner {
width: 24px;
height: 24px;
border: 2.5px solid var(--hover-bg);
border-top-color: var(--accent-primary);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}

@keyframes spin { to { transform: rotate(360deg); } }

/* Error bar (expanded) */
.expanded-card .error-bar {
display: flex;
Expand Down
8 changes: 8 additions & 0 deletions PolyPilot/Components/Pages/Dashboard.razor
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@
@key="session.Name">
<ExpandedSessionView Session="session"
IsCompleted="@completedSessions.Contains(session.Name)"
IsLoadingHistory="@(CopilotService.IsRemoteMode && session.MessageCount > 0 && session.History.Count == 0)"
HasMoreRemoteHistory="@CopilotService.HasMoreRemoteHistory(session.Name)"
StreamingContent="@(streamingBySession.TryGetValue(session.Name, out var s2) ? s2 : "")"
ActivityText="@(activityBySession.TryGetValue(session.Name, out var a2) ? a2 : "")"
CurrentToolName="@(currentToolBySession.TryGetValue(session.Name, out var t2) ? t2 : "")"
Expand Down Expand Up @@ -145,6 +147,7 @@
OnSetModel="(model) => SetExpandedModel(session, model)"
OnFontSizeChange="HandleFontSizeChange"
OnLoadMore="LoadMoreExpandedMessages"
OnLoadFullHistory="LoadFullRemoteHistory"
OnStartFiesta="StartFiestaForSession"
OnStopFiesta="StopFiestaForSession"
OnStopReflection="() => StopReflectionForSession(session.Name)" />
Expand Down Expand Up @@ -2595,6 +2598,11 @@
expandedMessageCounts[sessionName] = current + 25;
}

private async Task LoadFullRemoteHistory(string sessionName)
{
await CopilotService.LoadFullRemoteHistoryAsync(sessionName);
}

// === Model, plan mode, font, token helpers ===

private static readonly string[] _fallbackModels = new[]
Expand Down
8 changes: 8 additions & 0 deletions PolyPilot/Models/BridgeMessages.cs
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,10 @@ public class SessionHistoryPayload
{
public string SessionName { get; set; } = "";
public List<ChatMessage> Messages { get; set; } = new();
/// <summary>Total message count on the server (may be more than Messages.Count when limited).</summary>
public int TotalCount { get; set; }
/// <summary>True when the server has older messages not included in this response.</summary>
public bool HasMore { get; set; }
}

public class ContentDeltaPayload
Expand Down Expand Up @@ -225,6 +229,10 @@ public class ErrorPayload
public class GetHistoryPayload
{
public string SessionName { get; set; } = "";
/// <summary>
/// Max messages to return (most recent). Null = all messages.
/// </summary>
public int? Limit { get; set; }
}

public class SendMessagePayload
Expand Down
17 changes: 16 additions & 1 deletion PolyPilot/Services/CopilotService.Bridge.cs
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,7 @@ private void SyncRemoteSessions()
{
foreach (var name in sessionsNeedingHistory)
{
try { await _bridgeClient.RequestHistoryAsync(name); }
try { await _bridgeClient.RequestHistoryAsync(name, limit: 10); }
catch { }
}
});
Expand All @@ -353,6 +353,21 @@ private void SyncRemoteSessions()
private AgentSessionInfo? GetRemoteSession(string name) =>
_sessions.TryGetValue(name, out var state) ? state.Info : null;

/// <summary>
/// Whether the server has more history for this session than what's been loaded.
/// </summary>
public bool HasMoreRemoteHistory(string sessionName) =>
IsRemoteMode && _bridgeClient.SessionHistoryHasMore.TryGetValue(sessionName, out var hasMore) && hasMore;

/// <summary>
/// Request the full (unlimited) history for a session from the remote server.
/// </summary>
public async Task LoadFullRemoteHistoryAsync(string sessionName)
{
if (!IsRemoteMode) return;
await _bridgeClient.RequestHistoryAsync(sessionName, limit: null);
}

// --- Remote repo operations ---

public async Task<(string RepoId, string RepoName)?> AddRepoRemoteAsync(string url, Action<string>? onProgress = null, CancellationToken ct = default)
Expand Down
3 changes: 2 additions & 1 deletion PolyPilot/Services/IWsBridgeClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public interface IWsBridgeClient
List<SessionSummary> Sessions { get; }
string? ActiveSessionName { get; }
System.Collections.Concurrent.ConcurrentDictionary<string, List<ChatMessage>> SessionHistories { get; }
System.Collections.Concurrent.ConcurrentDictionary<string, bool> SessionHistoryHasMore { get; }
List<PersistedSessionSummary> PersistedSessions { get; }
string? GitHubAvatarUrl { get; }
string? GitHubLogin { get; }
Expand All @@ -37,7 +38,7 @@ public interface IWsBridgeClient
Task ConnectAsync(string wsUrl, string? authToken = null, CancellationToken ct = default);
void Stop();
Task RequestSessionsAsync(CancellationToken ct = default);
Task RequestHistoryAsync(string sessionName, CancellationToken ct = default);
Task RequestHistoryAsync(string sessionName, int? limit = null, CancellationToken ct = default);
Task SendMessageAsync(string sessionName, string message, CancellationToken ct = default);
Task CreateSessionAsync(string name, string? model = null, string? workingDirectory = null, CancellationToken ct = default);
Task SwitchSessionAsync(string name, CancellationToken ct = default);
Expand Down
8 changes: 5 additions & 3 deletions PolyPilot/Services/WsBridgeClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public class WsBridgeClient : IWsBridgeClient, IDisposable
public List<SessionSummary> Sessions { get; private set; } = new();
public string? ActiveSessionName { get; private set; }
public ConcurrentDictionary<string, List<ChatMessage>> SessionHistories { get; } = new();
public ConcurrentDictionary<string, bool> SessionHistoryHasMore { get; } = new();
public List<PersistedSessionSummary> PersistedSessions { get; private set; } = new();
public string? GitHubAvatarUrl { get; private set; }
public string? GitHubLogin { get; private set; }
Expand Down Expand Up @@ -168,9 +169,9 @@ public void Stop()
public async Task RequestSessionsAsync(CancellationToken ct = default) =>
await SendAsync(new BridgeMessage { Type = BridgeMessageTypes.GetSessions }, ct);

public async Task RequestHistoryAsync(string sessionName, CancellationToken ct = default) =>
public async Task RequestHistoryAsync(string sessionName, int? limit = null, CancellationToken ct = default) =>
await SendAsync(BridgeMessage.Create(BridgeMessageTypes.GetHistory,
new GetHistoryPayload { SessionName = sessionName }), ct);
new GetHistoryPayload { SessionName = sessionName, Limit = limit }), ct);

public async Task SendMessageAsync(string sessionName, string message, CancellationToken ct = default) =>
await SendAsync(BridgeMessage.Create(BridgeMessageTypes.SendMessage,
Expand Down Expand Up @@ -460,7 +461,8 @@ private void HandleServerMessage(string json)
if (history != null)
{
SessionHistories[history.SessionName] = history.Messages;
Console.WriteLine($"[WsBridgeClient] Got history for '{history.SessionName}': {history.Messages.Count} messages");
SessionHistoryHasMore[history.SessionName] = history.HasMore;
Console.WriteLine($"[WsBridgeClient] Got history for '{history.SessionName}': {history.Messages.Count} messages (total={history.TotalCount}, hasMore={history.HasMore})");
OnStateChanged?.Invoke();
}
break;
Expand Down
33 changes: 26 additions & 7 deletions PolyPilot/Services/WsBridgeServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -306,13 +306,13 @@ await SendToClientAsync(clientId, ws,
}
await SendPersistedToClient(clientId, ws, ct);

// Send history for all active sessions so mobile has full state on connect
// Send recent history for all active sessions (limited to reduce initial payload)
if (_copilot != null)
{
foreach (var session in _copilot.GetAllSessions())
{
if (session.History.Count > 0)
await SendSessionHistoryToClient(clientId, ws, session.Name, ct);
await SendSessionHistoryToClient(clientId, ws, session.Name, 10, ct);
}
}

Expand Down Expand Up @@ -371,7 +371,7 @@ await SendToClientAsync(clientId, ws,
case BridgeMessageTypes.GetHistory:
var histReq = msg.GetPayload<GetHistoryPayload>();
if (histReq != null)
await SendSessionHistoryToClient(clientId, ws, histReq.SessionName, ct);
await SendSessionHistoryToClient(clientId, ws, histReq.SessionName, histReq.Limit, ct);
break;

case BridgeMessageTypes.SendMessage:
Expand Down Expand Up @@ -415,7 +415,7 @@ await SendToClientAsync(clientId, ws,
{
_copilot.SetActiveSession(switchReq.SessionName);
BroadcastSessionsList();
await SendSessionHistoryToClient(clientId, ws, switchReq.SessionName, ct);
await SendSessionHistoryToClient(clientId, ws, switchReq.SessionName, 10, ct);
}
break;

Expand Down Expand Up @@ -748,15 +748,32 @@ private async Task SendPersistedToClient(string clientId, WebSocket ws, Cancella
await SendToClientAsync(clientId, ws, msg, ct);
}

private async Task SendSessionHistoryToClient(string clientId, WebSocket ws, string sessionName, CancellationToken ct)
private async Task SendSessionHistoryToClient(string clientId, WebSocket ws, string sessionName, int? limit, CancellationToken ct)
{
if (_copilot == null) return;

var session = _copilot.GetSession(sessionName);
if (session == null) return;

var allMessages = session.History;
var totalCount = allMessages.Count;

// Apply limit — take the most recent N messages
List<ChatMessage> messagesToSend;
bool hasMore;
if (limit.HasValue && limit.Value < totalCount)
{
messagesToSend = allMessages.Skip(totalCount - limit.Value).ToList();
hasMore = true;
}
else
{
messagesToSend = allMessages.ToList();
hasMore = false;
}

// Populate ImageDataUri for Image messages so mobile can render them
foreach (var m in session.History)
foreach (var m in messagesToSend)
{
if (m.MessageType == ChatMessageType.Image && string.IsNullOrEmpty(m.ImageDataUri) && !string.IsNullOrEmpty(m.ImagePath))
{
Expand All @@ -775,7 +792,9 @@ private async Task SendSessionHistoryToClient(string clientId, WebSocket ws, str
var payload = new SessionHistoryPayload
{
SessionName = sessionName,
Messages = session.History.ToList()
Messages = messagesToSend,
TotalCount = totalCount,
HasMore = hasMore
};
var msg = BridgeMessage.Create(BridgeMessageTypes.SessionHistory, payload);
await SendToClientAsync(clientId, ws, msg, ct);
Expand Down