chore(claude-do): Mission Control: allgemeiner Session-Handoff an eine frische
## Ziel Jede Mission-Control-Session (Ad-hoc UND Task-Session) soll ihre Arbeit an eine frische Session uebergeben koennen: "gib das ab an eine neue Session". Die alte Kachel wird geschlossen, an derselben Position oeffnet eine frische Session im selben cwd, die mit einem Brief startet, den die abgebende Session selbst geschrieben hat. Motivation: eine lange Session mit vollem/verrottetem Kontex ClaudeDo-Task: 8b846deb-ad10-4243-9272-9aea2a01ec93
This commit is contained in:
@@ -26,6 +26,7 @@ public abstract class StubWorkerClient : IWorkerClient
|
||||
public event Action<string, string, string>? TaskQuestionAskedEvent;
|
||||
public event Action<string, string>? TaskQuestionResolvedEvent;
|
||||
public event Action<string, IReadOnlyList<string>, string>? HandoffRequestedEvent;
|
||||
public event Action<string, string>? SessionHandoffRequestedEvent;
|
||||
public event Action<string, IReadOnlyList<string>>? ListHandlerStartRequestedEvent;
|
||||
public event Action<string, string>? HandlerUsageWarningEvent;
|
||||
public event Action? NotesUpdatedEvent;
|
||||
@@ -59,6 +60,7 @@ public abstract class StubWorkerClient : IWorkerClient
|
||||
public void RaiseTaskQuestionResolved(string taskId, string questionId) => TaskQuestionResolvedEvent?.Invoke(taskId, questionId);
|
||||
public void RaiseListHandlerStartRequested(string listId, IReadOnlyList<string> taskIds) => ListHandlerStartRequestedEvent?.Invoke(listId, taskIds);
|
||||
public void RaiseHandoffRequested(string taskId, IReadOnlyList<string> survivingTaskIds, string nextPhase = "wait") => HandoffRequestedEvent?.Invoke(taskId, survivingTaskIds, nextPhase);
|
||||
public void RaiseSessionHandoffRequested(string paneId, string briefPath) => SessionHandoffRequestedEvent?.Invoke(paneId, briefPath);
|
||||
public void RaiseHandlerUsageWarning(string taskId, string message) => HandlerUsageWarningEvent?.Invoke(taskId, message);
|
||||
public void RaisePlanningMergeConflict(string planningTaskId, string subtaskId, IReadOnlyList<string> files, bool externallyDriven)
|
||||
=> PlanningMergeConflictEvent?.Invoke(planningTaskId, subtaskId, files, externallyDriven);
|
||||
@@ -124,8 +126,10 @@ public abstract class StubWorkerClient : IWorkerClient
|
||||
public virtual Task SubmitTaskForReviewAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask;
|
||||
public virtual Task<LaunchSpec> GetInteractiveLaunchSpecAsync(string taskId, CancellationToken ct = default)
|
||||
=> Task.FromResult(new LaunchSpec(".", "claude", Array.Empty<string>(), new Dictionary<string, string>()));
|
||||
public virtual Task<LaunchSpec> GetAdHocLaunchSpecAsync(string directory, CancellationToken ct = default)
|
||||
public virtual Task<LaunchSpec> GetAdHocLaunchSpecAsync(string directory, string paneId, CancellationToken ct = default)
|
||||
=> Task.FromResult(new LaunchSpec(directory, "claude", Array.Empty<string>(), new Dictionary<string, string>()));
|
||||
public virtual Task<LaunchSpec> GetSessionHandoffLaunchSpecAsync(string paneId, string cwd, string briefPath, string? taskId, CancellationToken ct = default)
|
||||
=> Task.FromResult(new LaunchSpec(cwd, "claude", Array.Empty<string>(), new Dictionary<string, string>()));
|
||||
public virtual Task<LaunchSpec> GetMergeHelperLaunchSpecAsync(IReadOnlyList<string> taskIds, string listId, string handlerTaskId, string? model = null, CancellationToken ct = default)
|
||||
=> Task.FromResult(new LaunchSpec(".", "claude", Array.Empty<string>(), new Dictionary<string, string>()));
|
||||
public virtual Task<string> CreateMergeHelperTaskAsync(
|
||||
|
||||
@@ -53,7 +53,7 @@ public class ConPtyPaneViewModelTests
|
||||
[Fact]
|
||||
public void CanSubmitForReview_False_ForAdHocPane_EvenWhileRunning()
|
||||
{
|
||||
using var pane = ConPtyPaneViewModel.CreateAdHoc("Ad hoc", NeverCompletes);
|
||||
using var pane = new ConPtyPaneViewModel(null, "Ad hoc", NeverCompletes);
|
||||
SetRunning(pane);
|
||||
|
||||
Assert.False(pane.IsTaskBased);
|
||||
@@ -192,7 +192,7 @@ public class ConPtyPaneViewModelTests
|
||||
[Fact]
|
||||
public void SessionDiedAfterStartup_AdHocPane_NeverRaises()
|
||||
{
|
||||
using var pane = ConPtyPaneViewModel.CreateAdHoc("Ad hoc", NeverCompletes);
|
||||
using var pane = new ConPtyPaneViewModel(null, "Ad hoc", NeverCompletes);
|
||||
var raised = false;
|
||||
pane.SessionDiedAfterStartup += (_, _) => raised = true;
|
||||
|
||||
|
||||
@@ -490,7 +490,7 @@ public class MissionControlViewModelTests : IDisposable
|
||||
|
||||
private sealed class ThrowingAdHocLaunchSpecWorker : StubWorkerClient
|
||||
{
|
||||
public override Task<LaunchSpec> GetAdHocLaunchSpecAsync(string directory, CancellationToken ct = default)
|
||||
public override Task<LaunchSpec> GetAdHocLaunchSpecAsync(string directory, string paneId, CancellationToken ct = default)
|
||||
=> throw new InvalidOperationException("bad directory");
|
||||
}
|
||||
|
||||
@@ -771,6 +771,81 @@ public class MissionControlViewModelTests : IDisposable
|
||||
Assert.Equal(color, vm.ConPtySessions[1].IdentityColorKey);
|
||||
}
|
||||
|
||||
private sealed class RecordingSessionHandoffWorker : StubWorkerClient
|
||||
{
|
||||
public string? AdHocPaneId;
|
||||
public readonly TaskCompletionSource<(string PaneId, string Cwd, string BriefPath, string? TaskId)> HandoffCall = new();
|
||||
public override Task<LaunchSpec> GetAdHocLaunchSpecAsync(string directory, string paneId, CancellationToken ct = default)
|
||||
{
|
||||
AdHocPaneId ??= paneId;
|
||||
return base.GetAdHocLaunchSpecAsync(directory, paneId, ct);
|
||||
}
|
||||
public override Task<LaunchSpec> GetSessionHandoffLaunchSpecAsync(string paneId, string cwd, string briefPath, string? taskId, CancellationToken ct = default)
|
||||
{
|
||||
HandoffCall.TrySetResult((paneId, cwd, briefPath, taskId));
|
||||
return base.GetSessionHandoffLaunchSpecAsync(paneId, cwd, briefPath, taskId, ct);
|
||||
}
|
||||
}
|
||||
|
||||
// handoff_session replaces the tile in place: same slot, same identity colour, same cwd, and the
|
||||
// outgoing pane is really closed rather than left running beside the new one.
|
||||
[Fact]
|
||||
public async Task SessionHandoffRequested_ReplacesPaneInPlace_KeepingIdentityAndCwd()
|
||||
{
|
||||
var worker = new RecordingSessionHandoffWorker();
|
||||
using var vm = BuildVm(worker);
|
||||
var dir = Path.Combine("C:", "Some", "Path");
|
||||
await vm.OpenConPtySessionAsync("t1");
|
||||
await vm.OpenAdHocConPtySessionAsync(dir);
|
||||
await vm.OpenConPtySessionAsync("t2");
|
||||
var outgoing = vm.ConPtySessions[1];
|
||||
Assert.Equal(outgoing.IdentityKey, worker.AdHocPaneId);
|
||||
var color = outgoing.IdentityColorKey;
|
||||
var opened = 0;
|
||||
vm.OpenRequested += () => opened++;
|
||||
|
||||
worker.RaiseSessionHandoffRequested(outgoing.IdentityKey, "brief.md");
|
||||
|
||||
Assert.Equal(3, vm.ConPtySessions.Count);
|
||||
var incoming = vm.ConPtySessions[1];
|
||||
Assert.NotSame(outgoing, incoming);
|
||||
Assert.Null(incoming.TaskId);
|
||||
Assert.Equal(outgoing.IdentityKey, incoming.IdentityKey);
|
||||
Assert.Equal(color, incoming.IdentityColorKey);
|
||||
Assert.Same(incoming, vm.Panes[1]);
|
||||
Assert.Equal(1, opened);
|
||||
Assert.Equal(0, SubscriberCount(outgoing, "SubmitForReviewRequested"));
|
||||
|
||||
var call = await worker.HandoffCall.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
||||
Assert.Equal((outgoing.IdentityKey, dir, "brief.md", (string?)null), call);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SessionHandoffRequested_TaskPane_KeepsTaskId()
|
||||
{
|
||||
var worker = new RecordingSessionHandoffWorker();
|
||||
using var vm = BuildVm(worker);
|
||||
await vm.OpenConPtySessionAsync("t1");
|
||||
|
||||
worker.RaiseSessionHandoffRequested("t1", "brief.md");
|
||||
|
||||
Assert.Equal("t1", Assert.Single(vm.ConPtySessions).TaskId);
|
||||
var call = await worker.HandoffCall.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
||||
Assert.Equal("t1", call.TaskId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SessionHandoffRequested_UnknownPane_DoesNothing()
|
||||
{
|
||||
var worker = new RecordingSessionHandoffWorker();
|
||||
using var vm = BuildVm(worker);
|
||||
|
||||
worker.RaiseSessionHandoffRequested("nope", "brief.md");
|
||||
|
||||
Assert.Empty(vm.ConPtySessions);
|
||||
Assert.False(worker.HandoffCall.Task.IsCompleted);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OpenMergeHelperHandoffConPtySessionAsync_SubmitForReview_ResolvesToSurvivingPane()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user