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)