diff --git a/PolyPilot.Tests/SessionOrganizationTests.cs b/PolyPilot.Tests/SessionOrganizationTests.cs index 5200c71e40..43bd6c610e 100644 --- a/PolyPilot.Tests/SessionOrganizationTests.cs +++ b/PolyPilot.Tests/SessionOrganizationTests.cs @@ -1,5 +1,7 @@ using System.Text.Json; +using Microsoft.Extensions.DependencyInjection; using PolyPilot.Models; +using PolyPilot.Services; namespace PolyPilot.Tests; @@ -116,3 +118,138 @@ public void OrganizationCommandPayload_Serializes() Assert.Equal("test-session", deserialized.SessionName); } } + +/// +/// Tests for CopilotService.MoveSession behaviour including the auto-create-meta fix. +/// +public class MoveSessionTests +{ + private readonly StubChatDatabase _chatDb = new(); + private readonly StubServerManager _serverManager = new(); + private readonly StubWsBridgeClient _bridgeClient = new(); + private readonly StubDemoService _demoService = new(); + private readonly RepoManager _repoManager = new(); + private readonly IServiceProvider _serviceProvider; + + public MoveSessionTests() + { + var services = new ServiceCollection(); + _serviceProvider = services.BuildServiceProvider(); + } + + private CopilotService CreateService() => + new CopilotService(_chatDb, _serverManager, _bridgeClient, _repoManager, _serviceProvider, _demoService); + + [Fact] + public void MoveSession_WithExistingMeta_UpdatesGroupId() + { + var svc = CreateService(); + + // Set up a group and a session meta + var group = svc.CreateGroup("Work"); + svc.Organization.Sessions.Add(new SessionMeta + { + SessionName = "my-session", + GroupId = SessionGroup.DefaultId + }); + + svc.MoveSession("my-session", group.Id); + + var meta = svc.Organization.Sessions.FirstOrDefault(m => m.SessionName == "my-session"); + Assert.NotNull(meta); + Assert.Equal(group.Id, meta!.GroupId); + } + + [Fact] + public void MoveSession_WithoutExistingMeta_CreatesMetaInTargetGroup() + { + var svc = CreateService(); + + // Create a group but do NOT add a SessionMeta for the session + var group = svc.CreateGroup("Work"); + + svc.MoveSession("orphan-session", group.Id); + + var meta = svc.Organization.Sessions.FirstOrDefault(m => m.SessionName == "orphan-session"); + Assert.NotNull(meta); + Assert.Equal(group.Id, meta!.GroupId); + } + + [Fact] + public void MoveSession_ToNonExistentGroup_DoesNothing() + { + var svc = CreateService(); + + svc.Organization.Sessions.Add(new SessionMeta + { + SessionName = "my-session", + GroupId = SessionGroup.DefaultId + }); + + svc.MoveSession("my-session", "non-existent-group"); + + var meta = svc.Organization.Sessions.FirstOrDefault(m => m.SessionName == "my-session"); + Assert.NotNull(meta); + Assert.Equal(SessionGroup.DefaultId, meta!.GroupId); + } + + [Fact] + public void MoveSession_BetweenGroups_UpdatesCorrectly() + { + var svc = CreateService(); + + var groupA = svc.CreateGroup("Group A"); + var groupB = svc.CreateGroup("Group B"); + svc.Organization.Sessions.Add(new SessionMeta + { + SessionName = "my-session", + GroupId = groupA.Id + }); + + // Move from A to B + svc.MoveSession("my-session", groupB.Id); + + var meta = svc.Organization.Sessions.FirstOrDefault(m => m.SessionName == "my-session"); + Assert.NotNull(meta); + Assert.Equal(groupB.Id, meta!.GroupId); + } + + [Fact] + public void MoveSession_BackToDefaultGroup_Works() + { + var svc = CreateService(); + + var group = svc.CreateGroup("Custom"); + svc.Organization.Sessions.Add(new SessionMeta + { + SessionName = "my-session", + GroupId = group.Id + }); + + svc.MoveSession("my-session", SessionGroup.DefaultId); + + var meta = svc.Organization.Sessions.FirstOrDefault(m => m.SessionName == "my-session"); + Assert.NotNull(meta); + Assert.Equal(SessionGroup.DefaultId, meta!.GroupId); + } + + [Fact] + public void MoveSession_FiresStateChanged() + { + var svc = CreateService(); + + var group = svc.CreateGroup("Work"); + svc.Organization.Sessions.Add(new SessionMeta + { + SessionName = "my-session", + GroupId = SessionGroup.DefaultId + }); + + bool stateChanged = false; + svc.OnStateChanged += () => stateChanged = true; + + svc.MoveSession("my-session", group.Id); + + Assert.True(stateChanged); + } +} diff --git a/PolyPilot/Components/Layout/SessionListItem.razor b/PolyPilot/Components/Layout/SessionListItem.razor index fa453704bd..3dd8f8c3ba 100644 --- a/PolyPilot/Components/Layout/SessionListItem.razor +++ b/PolyPilot/Components/Layout/SessionListItem.razor @@ -103,6 +103,21 @@ πŸ“Œ Pin } + @if (Groups != null && Groups.Count > 1) + { + + + } @if (!string.IsNullOrEmpty(sessionDir)) { diff --git a/PolyPilot/Components/Layout/SessionListItem.razor.css b/PolyPilot/Components/Layout/SessionListItem.razor.css index 601f620b4e..92eb6a4ba6 100644 --- a/PolyPilot/Components/Layout/SessionListItem.razor.css +++ b/PolyPilot/Components/Layout/SessionListItem.razor.css @@ -187,6 +187,22 @@ .menu-item.destructive { color: var(--accent-primary); } .menu-item.destructive:hover { background: var(--control-border); color: var(--accent-primary); } +.menu-submenu { + display: flex; + flex-direction: column; +} + +.submenu-label { + cursor: default; + opacity: 0.7; + font-size: calc(var(--type-callout) - 1px); +} +.submenu-label:hover { background: transparent; color: var(--text-on-surface); } + +.submenu-item { + padding-left: 1.2rem; +} + .rename-input { width: 100%; padding: 0.2rem 0.4rem; diff --git a/PolyPilot/Services/CopilotService.Organization.cs b/PolyPilot/Services/CopilotService.Organization.cs index 82b0068b41..9b7958bcd0 100644 --- a/PolyPilot/Services/CopilotService.Organization.cs +++ b/PolyPilot/Services/CopilotService.Organization.cs @@ -167,13 +167,23 @@ public void PinSession(string sessionName, bool pinned) public void MoveSession(string sessionName, string groupId) { + if (!Organization.Groups.Any(g => g.Id == groupId)) + return; + var meta = Organization.Sessions.FirstOrDefault(m => m.SessionName == sessionName); - if (meta != null && Organization.Groups.Any(g => g.Id == groupId)) + if (meta == null) + { + // Session exists but wasn't reconciled yet β€” create meta on the fly + meta = new SessionMeta { SessionName = sessionName, GroupId = groupId }; + Organization.Sessions.Add(meta); + } + else { meta.GroupId = groupId; - SaveOrganization(); - OnStateChanged?.Invoke(); } + + SaveOrganization(); + OnStateChanged?.Invoke(); } public SessionGroup CreateGroup(string name)