Skip to content
Closed
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
152 changes: 152 additions & 0 deletions PolyPilot.Tests/ImportCliSessionsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
using PolyPilot.Services;

namespace PolyPilot.Tests;

/// <summary>
/// Tests for the CLI session import feature (ImportCliSessionsAsync).
/// </summary>
public class ImportCliSessionsTests
{
// --- GenerateImportDisplayName ---

[Fact]
public void GenerateImportDisplayName_PrefersSummary()
{
var name = CopilotService.GenerateImportDisplayName(
"Fix the login bug in auth module",
"/Users/test/projects/myapp",
"abc12345-1234-1234-1234-123456789abc");

Assert.Equal("Fix the login bug in auth module", name);
}

[Fact]
public void GenerateImportDisplayName_TruncatesLongSummary()
{
var longSummary = new string('x', 80);
var name = CopilotService.GenerateImportDisplayName(
longSummary, "/some/path", "abc12345-dead-beef-1234-123456789abc");

Assert.Equal(50, name.Length);
Assert.EndsWith("...", name);
}

[Fact]
public void GenerateImportDisplayName_FallsToCwdBasename()
{
var name = CopilotService.GenerateImportDisplayName(
null, "/Users/test/projects/my-cool-project", "abc12345-dead-beef-1234-123456789abc");

Assert.Equal("my-cool-project", name);
}

[Fact]
public void GenerateImportDisplayName_FallsToCwdBasename_TrailingSlash()
{
var name = CopilotService.GenerateImportDisplayName(
null, "/Users/test/projects/my-cool-project/", "abc12345-dead-beef-1234-123456789abc");

Assert.Equal("my-cool-project", name);
}

[Fact]
public void GenerateImportDisplayName_FallsToShortGuid()
{
var name = CopilotService.GenerateImportDisplayName(
null, null, "abc12345-dead-beef-1234-123456789abc");

Assert.Equal("abc12345", name);
}

[Fact]
public void GenerateImportDisplayName_EmptySummaryFallsToCwd()
{
var name = CopilotService.GenerateImportDisplayName(
"", "/some/path/coolapp", "abc12345-dead-beef-1234-123456789abc");

Assert.Equal("coolapp", name);
}

[Fact]
public void GenerateImportDisplayName_WhitespaceSummaryFallsToCwd()
{
var name = CopilotService.GenerateImportDisplayName(
" ", "/some/path/coolapp", "abc12345-dead-beef-1234-123456789abc");

Assert.Equal("coolapp", name);
}

[Fact]
public void GenerateImportDisplayName_CleansNewlines()
{
var name = CopilotService.GenerateImportDisplayName(
"Fix the\nbug\r\nin module", null, "abc12345-dead-beef-1234-123456789abc");

Assert.Equal("Fix the bug in module", name);
}

// --- ActiveSessionEntry.Imported flag ---

[Fact]
public void ActiveSessionEntry_ImportedFlag_DefaultsFalse()
{
var entry = new ActiveSessionEntry { SessionId = "test", DisplayName = "Test" };
Assert.False(entry.Imported);
}

[Fact]
public void ActiveSessionEntry_ImportedFlag_RoundTripsViaJson()
{
var entry = new ActiveSessionEntry
{
SessionId = "test-id",
DisplayName = "Test Session",
Model = "claude-opus-4.6",
Imported = true
};

var json = System.Text.Json.JsonSerializer.Serialize(entry);
var deserialized = System.Text.Json.JsonSerializer.Deserialize<ActiveSessionEntry>(json);

Assert.NotNull(deserialized);
Assert.True(deserialized!.Imported);
Assert.Equal("test-id", deserialized.SessionId);
}

[Fact]
public void ActiveSessionEntry_ImportedFlag_FalseNotIncludedInJson()
{
var entry = new ActiveSessionEntry
{
SessionId = "test-id",
DisplayName = "Test Session",
Imported = false
};

var json = System.Text.Json.JsonSerializer.Serialize(entry);
// When false (default), the value should still serialize correctly
var deserialized = System.Text.Json.JsonSerializer.Deserialize<ActiveSessionEntry>(json);
Assert.NotNull(deserialized);
Assert.False(deserialized!.Imported);
}

// --- Merge preserves Imported flag ---

[Fact]
public void Merge_PreservesImportedFlag_FromPersistedEntries()
{
var active = new List<ActiveSessionEntry>();
var persisted = new List<ActiveSessionEntry>
{
new() { SessionId = "imp-1", DisplayName = "Imported One", Model = "m", Imported = true }
};
var closed = new HashSet<string>();

var result = CopilotService.MergeSessionEntries(
active, persisted, closed, new HashSet<string>(), _ => true);

var importedEntry = result.FirstOrDefault(e => e.SessionId == "imp-1");
Assert.NotNull(importedEntry);
Assert.True(importedEntry!.Imported);
}
}
51 changes: 51 additions & 0 deletions PolyPilot/Components/Layout/SessionSidebar.razor
Original file line number Diff line number Diff line change
Expand Up @@ -1166,6 +1166,23 @@ else
<input type="text" @bind="sessionFilter" @bind:event="oninput"
placeholder="Filter sessions..." class="filter-input" />
</div>

@if (!isImporting)
{
<div style="padding: 4px 8px;">
<button class="btn-import-all" @onclick="ImportAllCliSessions"
title="Import all CLI sessions into PolyPilot"
style="width: 100%; padding: 6px 10px; font-size: var(--type-caption1); cursor: pointer; background: var(--color-accent-subtle, #1a1b26); border: 1px solid var(--color-border-default, #444); border-radius: 6px; color: var(--color-fg-default, #c9d1d9);">
📥 Import All CLI Sessions
</button>
</div>
}
else
{
<div style="padding: 4px 8px; font-size: var(--type-caption1); color: var(--color-fg-muted, #8b949e);">
📥 Importing... @importProgress
</div>
}

@foreach (var persisted in filteredPersistedSessions.Take(30))
{
Expand Down Expand Up @@ -1338,6 +1355,8 @@ else
private bool showDirectoryPicker;
private List<AgentSessionInfo> sessions = new();
private List<PersistedSessionInfo> persistedSessions = new();
private bool isImporting = false;
private string importProgress = "";

// --- Status filter ---
private enum SessionStatusFilter { All, Processing, NeedsAttention, Stuck, Idle }
Expand Down Expand Up @@ -1765,6 +1784,38 @@ else
if (showPersistedSessions) LoadPersistedSessions();
}

private async Task ImportAllCliSessions()
{
if (isImporting) return;
isImporting = true;
importProgress = "Scanning...";
StateHasChanged();

try
{
var progress = new Progress<(int scanned, int imported, int total)>(p =>
{
importProgress = $"{p.imported} imported ({p.scanned}/{p.total} scanned)";
InvokeAsync(StateHasChanged);
});

var count = await Task.Run(() => CopilotService.ImportCliSessionsAsync(progress));
importProgress = $"Done! {count} sessions imported.";
}
catch (Exception ex)
{
importProgress = $"Error: {ex.Message}";
Console.WriteLine($"Import error: {ex}");
}
finally
{
isImporting = false;
StateHasChanged();
// Refresh the persisted session list since many are now "open"
LoadPersistedSessions();
}
}

private void ToggleExternalSessions() => showExternalSessions = !showExternalSessions;

private string? confirmExternalResumeId; // reused for error display only
Expand Down
2 changes: 2 additions & 0 deletions PolyPilot/Platforms/MacCatalyst/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,7 @@
<string>PolyPilot uses speech recognition to convert your voice into text messages.</string>
<key>NSMicrophoneUsageDescription</key>
<string>PolyPilot needs microphone access for speech-to-text input.</string>
<key>NSBluetoothAlwaysUsageDescription</key>
<string>PolyPilot does not use Bluetooth.</string>
</dict>
</plist>
2 changes: 2 additions & 0 deletions PolyPilot/Platforms/iOS/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
<string>PolyPilot uses speech recognition to convert your voice into text messages.</string>
<key>NSMicrophoneUsageDescription</key>
<string>PolyPilot needs microphone access for speech-to-text input.</string>
<key>NSBluetoothAlwaysUsageDescription</key>
<string>PolyPilot does not use Bluetooth.</string>
<key>UIViewControllerBasedStatusBarAppearance</key>
<true/>
<!-- Required by App Store validation (ITMS-90683) — referenced by linked frameworks -->
Expand Down
1 change: 1 addition & 0 deletions PolyPilot/PolyPilot.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
<MauiXaml Update="MyPage.xaml" Inflator="Default" /> (reverts to defaults: Runtime for Debug, XamlC for Release)
<MauiXaml Update="MyPage.xaml" Inflator="Runtime" /> (force runtime inflation) -->
<MauiXamlInflator>SourceGen</MauiXamlInflator>
<ValidateXcodeVersion>false</ValidateXcodeVersion>

<!-- Display name -->
<ApplicationTitle>PolyPilot</ApplicationTitle>
Expand Down
Loading
Loading