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
463 changes: 458 additions & 5 deletions PolyPilot.Tests/ProcessingWatchdogTests.cs

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions PolyPilot.Tests/ScenarioReferenceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
{
var json = File.ReadAllText(file);
var doc = JsonDocument.Parse(json); // throws on invalid JSON
Assert.NotNull(doc.RootElement.GetProperty("scenarios"));

Check warning on line 31 in PolyPilot.Tests/ScenarioReferenceTests.cs

View workflow job for this annotation

GitHub Actions / Build & Test

Do not use Assert.NotNull() on value type 'JsonElement'. Remove this assert. (https://xunit.net/xunit.analyzers/rules/xUnit2002)

Check warning on line 31 in PolyPilot.Tests/ScenarioReferenceTests.cs

View workflow job for this annotation

GitHub Actions / Build & Test

Do not use Assert.NotNull() on value type 'JsonElement'. Remove this assert. (https://xunit.net/xunit.analyzers/rules/xUnit2002)
}
}

Expand Down Expand Up @@ -63,7 +63,7 @@
{
Assert.True(step.TryGetProperty("action", out var action),
$"Step in '{id}' missing 'action'");
Assert.Contains(action.GetString(), validActions);

Check warning on line 66 in PolyPilot.Tests/ScenarioReferenceTests.cs

View workflow job for this annotation

GitHub Actions / Build & Test

Argument of type 'HashSet<string>' cannot be used for parameter 'set' of type 'HashSet<string?>' in 'void Assert.Contains<string?>(string? expected, HashSet<string?> set)' due to differences in the nullability of reference types.

Check warning on line 66 in PolyPilot.Tests/ScenarioReferenceTests.cs

View workflow job for this annotation

GitHub Actions / Build & Test

Argument of type 'HashSet<string>' cannot be used for parameter 'set' of type 'HashSet<string?>' in 'void Assert.Contains<string?>(string? expected, HashSet<string?> set)' due to differences in the nullability of reference types.
}
}
}
Expand Down Expand Up @@ -155,6 +155,7 @@
/// Scenario: "stuck-session-recovery-after-server-disconnect"
/// Unit test equivalents: ProcessingWatchdogTests.WatchdogCheckInterval_IsReasonable,
/// ProcessingWatchdogTests.WatchdogInactivityTimeout_IsReasonable,
/// ProcessingWatchdogTests.WatchdogToolExecutionTimeout_IsReasonable,
/// ProcessingWatchdogTests.SystemMessage_ConnectionLost_HasExpectedContent,
/// ProcessingWatchdogTests.SystemMessage_AddedToHistory_IsVisible
/// </summary>
Expand Down
9 changes: 5 additions & 4 deletions PolyPilot.Tests/Scenarios/mode-switch-scenarios.json
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,7 @@
"unitTestCoverage": [
"ProcessingWatchdogTests.WatchdogCheckInterval_IsReasonable",
"ProcessingWatchdogTests.WatchdogInactivityTimeout_IsReasonable",
"ProcessingWatchdogTests.WatchdogToolExecutionTimeout_IsReasonable",
"ProcessingWatchdogTests.WatchdogTimeout_IsGreaterThanCheckInterval",
"ProcessingWatchdogTests.SystemMessage_ConnectionLost_HasExpectedContent",
"ProcessingWatchdogTests.SystemMessage_AddedToHistory_IsVisible"
Expand Down Expand Up @@ -469,12 +470,12 @@
},
{
"action": "note",
"text": "To fully test: kill the persistent server process while session is processing, then wait up to 2 minutes for the watchdog to detect inactivity and clear the stuck state. The session should show a system message: 'Connection lost — no response received.'"
"text": "To fully test: kill the persistent server process while session is processing, then wait up to 2 minutes for the watchdog to detect inactivity and clear the stuck state (10 min if a tool is running). The session should show a system message: 'Session appears stuck — no response received.'"
},
{
"action": "wait",
"duration": 130000,
"note": "Wait for watchdog timeout (120s) + buffer. In manual testing, kill server during this wait."
"note": "Wait for watchdog timeout (120s when no tool running) + buffer. In manual testing, kill server during this wait."
},
{
"action": "evaluate",
Expand All @@ -484,9 +485,9 @@
},
{
"action": "evaluate",
"script": "Array.from(document.querySelectorAll('.chat-msg')).some(el => el.textContent.includes('Connection lost'))",
"script": "Array.from(document.querySelectorAll('.chat-msg')).some(el => el.textContent.includes('appears stuck'))",
"expect": { "equals": "true" },
"note": "System message about connection loss should appear in chat"
"note": "System message about stuck session should appear in chat"
}
]
},
Expand Down
9 changes: 9 additions & 0 deletions PolyPilot/Components/Layout/SessionListItem.razor
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,13 @@
</div>
}
<div class="menu-separator"></div>
<button class="menu-item" @onclick="() => { OnCloseMenu.InvokeAsync(); OnReportBug.InvokeAsync(); }">
🐛 Report Bug
</button>
<button class="menu-item" @onclick="() => { OnCloseMenu.InvokeAsync(); OnFixWithCopilot.InvokeAsync(); }">
🔧 Fix with Copilot
</button>
<div class="menu-separator"></div>
@if (!string.IsNullOrEmpty(sessionDir))
{
<button class="menu-item" @onclick="() => { OnCloseMenu.InvokeAsync(); OpenInTerminal(); }">
Expand Down Expand Up @@ -157,6 +164,8 @@
[Parameter] public EventCallback OnCommitRename { get; set; }
[Parameter] public EventCallback OnToggleMenu { get; set; }
[Parameter] public EventCallback OnCloseMenu { get; set; }
[Parameter] public EventCallback OnReportBug { get; set; }
[Parameter] public EventCallback OnFixWithCopilot { get; set; }

private async Task HandleRenameKeyDown(KeyboardEventArgs e)
{
Expand Down
62 changes: 56 additions & 6 deletions PolyPilot/Components/Layout/SessionSidebar.razor
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,16 @@ else
<div class="bug-report-inline">
<div class="bug-report-header">
<span>Report a Bug</span>
<button class="flyout-close-btn" @onclick="CloseFooterPanel">✕</button>
<button class="flyout-close-btn" @onclick="CloseFooterPanel" aria-label="Close panel">✕</button>
</div>
<textarea class="bug-report-textarea" @bind="bugDescription" placeholder="Describe the issue you're seeing…"></textarea>
<select class="bug-report-select" @bind="selectedBugSession" aria-label="Select session to report">
<option value="">No specific session</option>
@foreach (var s in CopilotService.GetAllSessions())
{
<option value="@s.Name">@s.Name@(s.IsProcessing ? " (Thinking)" : "")</option>
}
</select>
<textarea class="bug-report-textarea" @bind="bugDescription" placeholder="Describe the issue you're seeing…" aria-label="Bug description"></textarea>
<details class="bug-report-details">
<summary>Debug info included</summary>
<pre class="bug-report-debug">@GetBugReportDebugInfo()</pre>
Expand All @@ -110,9 +117,16 @@ else
<div class="bug-report-inline">
<div class="bug-report-header">
<span>Fix a Bug / Add Feature</span>
<button class="flyout-close-btn" @onclick="CloseFooterPanel">✕</button>
<button class="flyout-close-btn" @onclick="CloseFooterPanel" aria-label="Close panel">✕</button>
</div>
<textarea class="bug-report-textarea" @bind="bugDescription" placeholder="Describe what to fix or build…"></textarea>
<select class="bug-report-select" @bind="selectedBugSession" aria-label="Select session">
<option value="">No specific session</option>
@foreach (var s in CopilotService.GetAllSessions())
{
<option value="@s.Name">@s.Name@(s.IsProcessing ? " (Thinking)" : "")</option>
}
</select>
<textarea class="bug-report-textarea" @bind="bugDescription" placeholder="Describe what to fix or build…" aria-label="Description"></textarea>
<details class="bug-report-details">
<summary>Debug info included</summary>
<pre class="bug-report-debug">@GetBugReportDebugInfo()</pre>
Expand Down Expand Up @@ -345,7 +359,9 @@ else
OnStartRename="() => StartRename(sName)"
OnCommitRename="CommitRename"
OnToggleMenu="() => ToggleSessionMenu(sName)"
OnCloseMenu="() => { openMenuSession = null; }" />
OnCloseMenu="() => { openMenuSession = null; }"
OnReportBug="() => OpenBugReportForSession(sName)"
OnFixWithCopilot="() => OpenFixItForSession(sName)" />
}
}
}
Expand Down Expand Up @@ -938,6 +954,7 @@ else
private bool showBugReport;
private bool showFixIt;
private string bugDescription = "";
private string selectedBugSession = "";
private bool footerSubmitting;
private string? footerStatus;

Expand All @@ -946,6 +963,7 @@ else
showBugReport = true;
showFixIt = false;
bugDescription = "";
selectedBugSession = "";
footerStatus = null;
StateHasChanged();
}
Expand All @@ -955,10 +973,25 @@ else
showFixIt = true;
showBugReport = false;
bugDescription = "";
selectedBugSession = "";
footerStatus = null;
StateHasChanged();
}

private void OpenBugReportForSession(string sessionName)
{
OpenBugReport();
selectedBugSession = sessionName;
StateHasChanged();
}

private void OpenFixItForSession(string sessionName)
{
OpenFixIt();
selectedBugSession = sessionName;
StateHasChanged();
}

private void CloseFooterPanel()
{
showBugReport = false;
Expand All @@ -983,6 +1016,20 @@ else
sb.AppendLine($"FallbackNotice: {CopilotService.FallbackNotice}");
sb.AppendLine($"LastDebug: {CopilotService.LastDebugMessage}");

if (!string.IsNullOrEmpty(selectedBugSession))
{
var session = CopilotService.GetAllSessions().FirstOrDefault(s => s.Name == selectedBugSession);
if (session != null)
{
sb.AppendLine($"--- Selected Session: {session.Name} ---");
sb.AppendLine($"IsProcessing: {session.IsProcessing}");
sb.AppendLine($"Model: {session.Model}");
sb.AppendLine($"MessageCount: {session.MessageCount}");
sb.AppendLine($"LastUpdatedAt: {session.LastUpdatedAt:o}");
sb.AppendLine($"WorkingDirectory: {session.WorkingDirectory}");
}
}

try
{
var crashPath = Path.Combine(
Expand Down Expand Up @@ -1018,12 +1065,13 @@ else
try
{
var debugInfo = GetBugReportDebugInfo();
var sessionLabel = !string.IsNullOrEmpty(selectedBugSession) ? $" [{selectedBugSession}]" : "";
var body = $"## Description\n{bugDescription.Trim()}\n\n## Debug Info\n```\n{debugInfo}\n```";
var title = bugDescription.Trim().Split('\n')[0];
if (title.Length > 80) title = title[..80];

var psi = new System.Diagnostics.ProcessStartInfo("gh",
$"issue create --repo PureWeen/PolyPilot --title \"[Bug Report] {EscapeForShell(title)}\" --body \"{EscapeForShell(body)}\" --label bug")
$"issue create --repo PureWeen/PolyPilot --title \"[Bug Report]{EscapeForShell(sessionLabel)} {EscapeForShell(title)}\" --body \"{EscapeForShell(body)}\" --label bug")
{
RedirectStandardOutput = true,
RedirectStandardError = true,
Expand Down Expand Up @@ -1081,6 +1129,8 @@ else
{
var debugInfo = GetBugReportDebugInfo();
var desc = bugDescription.Trim();
if (!string.IsNullOrEmpty(selectedBugSession))
desc = $"[Session: {selectedBugSession}] {desc}";
var slugTitle = new string(desc.Split('\n')[0]
.Where(c => char.IsLetterOrDigit(c) || c == ' ' || c == '-')
.ToArray())
Expand Down
25 changes: 25 additions & 0 deletions PolyPilot/Components/Layout/SessionSidebar.razor.css
Original file line number Diff line number Diff line change
Expand Up @@ -1212,6 +1212,31 @@
font-family: inherit;
box-sizing: border-box;
}

.bug-report-select {
width: 100%;
padding: 0.4rem;
background: var(--bg-primary);
border: 1px solid var(--control-border);
border-radius: 6px;
color: var(--text-primary);
font-size: 0.75rem;
font-family: inherit;
box-sizing: border-box;
-webkit-appearance: none;
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23a0b4cc' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 0.5rem center;
}
.bug-report-select option {
background: var(--bg-primary);
color: var(--text-primary);
}
.bug-report-select:focus {
outline: none;
border-color: var(--accent-primary);
}
.bug-report-textarea:focus {
outline: none;
border-color: var(--accent-primary);
Expand Down
Loading
Loading