Merge claudedo/5e2a441113a34c2a9257b56abb0a605c
This commit is contained in:
@@ -25,9 +25,10 @@ public interface IWorkerClient : INotifyPropertyChanged
|
|||||||
/// <summary>A pending question was answered, timed out, or the run ended: (taskId, questionId).</summary>
|
/// <summary>A pending question was answered, timed out, or the run ended: (taskId, questionId).</summary>
|
||||||
event Action<string, string>? TaskQuestionResolvedEvent;
|
event Action<string, string>? TaskQuestionResolvedEvent;
|
||||||
|
|
||||||
/// <summary>A running list-handler session called handoff_list_handler at the end of Phase 2:
|
/// <summary>A running list-handler session called handoff_list_handler at the end of a phase:
|
||||||
/// (handlerTaskId, survivingTaskIds). The UI opens a second ConPTY tile for the same task.</summary>
|
/// (handlerTaskId, survivingTaskIds, nextPhase). The UI opens a second ConPTY tile for the same
|
||||||
event Action<string, IReadOnlyList<string>>? HandoffRequestedEvent;
|
/// task to carry out nextPhase ("wait" | "merge" | "wait_final" | "merge_final").</summary>
|
||||||
|
event Action<string, IReadOnlyList<string>, string>? HandoffRequestedEvent;
|
||||||
|
|
||||||
event Action? PrepStartedEvent;
|
event Action? PrepStartedEvent;
|
||||||
event Action<string>? PrepLineEvent;
|
event Action<string>? PrepLineEvent;
|
||||||
@@ -109,10 +110,11 @@ public interface IWorkerClient : INotifyPropertyChanged
|
|||||||
/// never queued) so the ConPTY tile can be task-based instead of ad-hoc. Returns the new task id.</summary>
|
/// never queued) so the ConPTY tile can be task-based instead of ad-hoc. Returns the new task id.</summary>
|
||||||
Task<string> CreateMergeHelperTaskAsync(
|
Task<string> CreateMergeHelperTaskAsync(
|
||||||
IReadOnlyList<string> taskIds, string listId, string title, string descriptionHeader, CancellationToken ct = default);
|
IReadOnlyList<string> taskIds, string listId, string title, string descriptionHeader, CancellationToken ct = default);
|
||||||
/// <summary>Launch spec for the fresh ConPTY session a merge-helper run hands off to once Phase 2
|
/// <summary>Launch spec for the fresh ConPTY session a merge-helper run hands off to once a phase
|
||||||
/// is done -- reuses the SAME handler task id (no new task created).</summary>
|
/// is done -- reuses the SAME handler task id (no new task created). nextPhase picks the next
|
||||||
|
/// session's role: "wait" | "merge" | "wait_final" | "merge_final".</summary>
|
||||||
Task<LaunchSpec> GetMergeHelperHandoffLaunchSpecAsync(
|
Task<LaunchSpec> GetMergeHelperHandoffLaunchSpecAsync(
|
||||||
string taskId, IReadOnlyList<string> survivingTaskIds, CancellationToken ct = default);
|
string taskId, IReadOnlyList<string> survivingTaskIds, string nextPhase, CancellationToken ct = default);
|
||||||
/// <summary>Starts a planning session and returns the launch spec for an embedded ConPTY
|
/// <summary>Starts a planning session and returns the launch spec for an embedded ConPTY
|
||||||
/// planning terminal (replaces StartPlanningSessionAsync's external wt window).</summary>
|
/// planning terminal (replaces StartPlanningSessionAsync's external wt window).</summary>
|
||||||
Task<LaunchSpec> GetPlanningStartLaunchSpecAsync(string taskId, CancellationToken ct = default);
|
Task<LaunchSpec> GetPlanningStartLaunchSpecAsync(string taskId, CancellationToken ct = default);
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
|||||||
public event Action<string>? TaskUpdatedEvent;
|
public event Action<string>? TaskUpdatedEvent;
|
||||||
public event Action<string, string, string>? TaskQuestionAskedEvent;
|
public event Action<string, string, string>? TaskQuestionAskedEvent;
|
||||||
public event Action<string, string>? TaskQuestionResolvedEvent;
|
public event Action<string, string>? TaskQuestionResolvedEvent;
|
||||||
public event Action<string, IReadOnlyList<string>>? HandoffRequestedEvent;
|
public event Action<string, IReadOnlyList<string>, string>? HandoffRequestedEvent;
|
||||||
public event Action? ConnectionRestoredEvent;
|
public event Action? ConnectionRestoredEvent;
|
||||||
public event Action<string>? WorktreeUpdatedEvent;
|
public event Action<string>? WorktreeUpdatedEvent;
|
||||||
public event Action<string>? ListUpdatedEvent;
|
public event Action<string>? ListUpdatedEvent;
|
||||||
@@ -151,9 +151,9 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
|||||||
Dispatcher.UIThread.Post(() => TaskQuestionResolvedEvent?.Invoke(taskId, questionId));
|
Dispatcher.UIThread.Post(() => TaskQuestionResolvedEvent?.Invoke(taskId, questionId));
|
||||||
});
|
});
|
||||||
|
|
||||||
_hub.On<string, IReadOnlyList<string>>("HandoffRequested", (taskId, survivingTaskIds) =>
|
_hub.On<string, IReadOnlyList<string>, string>("HandoffRequested", (taskId, survivingTaskIds, nextPhase) =>
|
||||||
{
|
{
|
||||||
Dispatcher.UIThread.Post(() => HandoffRequestedEvent?.Invoke(taskId, survivingTaskIds));
|
Dispatcher.UIThread.Post(() => HandoffRequestedEvent?.Invoke(taskId, survivingTaskIds, nextPhase));
|
||||||
});
|
});
|
||||||
|
|
||||||
_hub.On<string>("WorktreeUpdated", taskId =>
|
_hub.On<string>("WorktreeUpdated", taskId =>
|
||||||
@@ -566,8 +566,8 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
|||||||
=> await _hub.InvokeAsync<string>("CreateMergeHelperTask", taskIds, listId, title, descriptionHeader, ct);
|
=> await _hub.InvokeAsync<string>("CreateMergeHelperTask", taskIds, listId, title, descriptionHeader, ct);
|
||||||
|
|
||||||
public async Task<LaunchSpec> GetMergeHelperHandoffLaunchSpecAsync(
|
public async Task<LaunchSpec> GetMergeHelperHandoffLaunchSpecAsync(
|
||||||
string taskId, IReadOnlyList<string> survivingTaskIds, CancellationToken ct = default)
|
string taskId, IReadOnlyList<string> survivingTaskIds, string nextPhase, CancellationToken ct = default)
|
||||||
=> await _hub.InvokeAsync<LaunchSpec>("GetMergeHelperHandoffLaunchSpec", taskId, survivingTaskIds, ct);
|
=> await _hub.InvokeAsync<LaunchSpec>("GetMergeHelperHandoffLaunchSpec", taskId, survivingTaskIds, nextPhase, ct);
|
||||||
|
|
||||||
public async Task<LaunchSpec> GetPlanningStartLaunchSpecAsync(string taskId, CancellationToken ct = default)
|
public async Task<LaunchSpec> GetPlanningStartLaunchSpecAsync(string taskId, CancellationToken ct = default)
|
||||||
=> await _hub.InvokeAsync<LaunchSpec>("GetPlanningStartLaunchSpec", taskId, ct);
|
=> await _hub.InvokeAsync<LaunchSpec>("GetPlanningStartLaunchSpec", taskId, ct);
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
|
|||||||
private readonly Action<string, string, string, DateTime> _onTaskFinished;
|
private readonly Action<string, string, string, DateTime> _onTaskFinished;
|
||||||
private readonly Action<string> _onTaskUpdated;
|
private readonly Action<string> _onTaskUpdated;
|
||||||
private readonly Action _onConnectionRestored;
|
private readonly Action _onConnectionRestored;
|
||||||
private readonly Action<string, IReadOnlyList<string>> _onHandoffRequested;
|
private readonly Action<string, IReadOnlyList<string>, string> _onHandoffRequested;
|
||||||
|
|
||||||
// Embedded ConPTY sessions (task-based only) — a manual cockpit detached from the
|
// Embedded ConPTY sessions (task-based only) — a manual cockpit detached from the
|
||||||
// review/merge/status machinery.
|
// review/merge/status machinery.
|
||||||
@@ -85,7 +85,7 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
|
|||||||
_onConnectionRestored = () => { _ = RefreshQueueAsync(); };
|
_onConnectionRestored = () => { _ = RefreshQueueAsync(); };
|
||||||
_worker.ConnectionRestoredEvent += _onConnectionRestored;
|
_worker.ConnectionRestoredEvent += _onConnectionRestored;
|
||||||
|
|
||||||
_onHandoffRequested = (taskId, survivingTaskIds) => { _ = OpenMergeHelperHandoffConPtySessionAsync(taskId, survivingTaskIds); };
|
_onHandoffRequested = (taskId, survivingTaskIds, nextPhase) => { _ = OpenMergeHelperHandoffConPtySessionAsync(taskId, survivingTaskIds, nextPhase); };
|
||||||
_worker.HandoffRequestedEvent += _onHandoffRequested;
|
_worker.HandoffRequestedEvent += _onHandoffRequested;
|
||||||
|
|
||||||
_ = RefreshQueueAsync();
|
_ = RefreshQueueAsync();
|
||||||
@@ -153,7 +153,9 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
|
|||||||
public async System.Threading.Tasks.Task OpenConPtySessionAsync(string taskId)
|
public async System.Threading.Tasks.Task OpenConPtySessionAsync(string taskId)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(taskId)) return;
|
if (string.IsNullOrEmpty(taskId)) return;
|
||||||
if (ConPtySessions.FirstOrDefault(s => s.TaskId == taskId) is { } existing)
|
// A merge-helper handoff can leave more than one pane for this TaskId (the outgoing phase's
|
||||||
|
// frozen tile plus the active one) -- LastOrDefault resolves to the newest/active pane.
|
||||||
|
if (ConPtySessions.LastOrDefault(s => s.TaskId == taskId) is { } existing)
|
||||||
{
|
{
|
||||||
FocusedPane = existing;
|
FocusedPane = existing;
|
||||||
return;
|
return;
|
||||||
@@ -281,19 +283,19 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// List-handler handoff: the running session called handoff_list_handler at the end of Phase 2.
|
// List-handler handoff: the running session called handoff_list_handler at the end of a phase.
|
||||||
// Replaces the Phase 1-2 tile with a fresh one for the SAME handler task id, which carries out
|
// Opens a NEW tile for the SAME handler task id to carry out nextPhase, and deliberately leaves
|
||||||
// Phases 3-5. The old tile used to be left open so its last message could still be read, but
|
// the outgoing tile open -- it still shows that phase's finished output, which the wait/merge
|
||||||
// its process is gone by then and the terminal renders empty — it was only ever a dead
|
// chain can run through several times for one handler task. ConPtySessions can therefore hold
|
||||||
// placeholder to close by hand. No new task is created here; see
|
// more than one pane per TaskId from here on; lookups that mean "the current/active session for
|
||||||
// InteractiveLaunchSpecService.BuildForMergeHelperHandoffAsync.
|
// this task" (OpenConPtySessionAsync's dedupe, OnPaneSubmitForReview) use LastOrDefault so they
|
||||||
public async System.Threading.Tasks.Task OpenMergeHelperHandoffConPtySessionAsync(string taskId, IReadOnlyList<string> survivingTaskIds)
|
// resolve to the newest tile rather than a frozen earlier phase. No new task is created here;
|
||||||
|
// see InteractiveLaunchSpecService.BuildForMergeHelperHandoffAsync.
|
||||||
|
public async System.Threading.Tasks.Task OpenMergeHelperHandoffConPtySessionAsync(
|
||||||
|
string taskId, IReadOnlyList<string> survivingTaskIds, string nextPhase)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(taskId) || survivingTaskIds is not { Count: > 0 }) return;
|
if (string.IsNullOrEmpty(taskId) || survivingTaskIds is not { Count: > 0 }) return;
|
||||||
|
|
||||||
foreach (var stale in ConPtySessions.Where(s => s.TaskId == taskId).ToList())
|
|
||||||
CloseConPtySession(stale);
|
|
||||||
|
|
||||||
var baseTitle = Loc.T("missionControl.mergeHelperTitle");
|
var baseTitle = Loc.T("missionControl.mergeHelperTitle");
|
||||||
var title = baseTitle + Loc.T("missionControl.mergeHelperHandoffTitleSuffix");
|
var title = baseTitle + Loc.T("missionControl.mergeHelperHandoffTitleSuffix");
|
||||||
try
|
try
|
||||||
@@ -310,7 +312,7 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
|
|||||||
catch { /* best-effort title lookup */ }
|
catch { /* best-effort title lookup */ }
|
||||||
|
|
||||||
AddConPtyPane(new ConPtyPaneViewModel(taskId, title,
|
AddConPtyPane(new ConPtyPaneViewModel(taskId, title,
|
||||||
() => DescribeAsync(() => _worker.GetMergeHelperHandoffLaunchSpecAsync(taskId, survivingTaskIds))));
|
() => DescribeAsync(() => _worker.GetMergeHelperHandoffLaunchSpecAsync(taskId, survivingTaskIds, nextPhase))));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wires a freshly built pane and shows it immediately — the pane resolves its own launch spec,
|
// Wires a freshly built pane and shows it immediately — the pane resolves its own launch spec,
|
||||||
@@ -340,7 +342,9 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
|
|||||||
// SubmitTaskForReviewAsync calls, with the loser flashing a spurious footer error.
|
// SubmitTaskForReviewAsync calls, with the loser flashing a spurious footer error.
|
||||||
private async void OnPaneSubmitForReview(string taskId)
|
private async void OnPaneSubmitForReview(string taskId)
|
||||||
{
|
{
|
||||||
if (ConPtySessions.FirstOrDefault(s => s.TaskId == taskId) is not { } pane || pane.IsSubmitPending)
|
// See OpenConPtySessionAsync -- LastOrDefault resolves to the newest/active pane when a
|
||||||
|
// merge-helper handoff has left an earlier phase's frozen tile in place for this TaskId.
|
||||||
|
if (ConPtySessions.LastOrDefault(s => s.TaskId == taskId) is not { } pane || pane.IsSubmitPending)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
pane.IsSubmitPending = true;
|
pane.IsSubmitPending = true;
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ public abstract class StubWorkerClient : IWorkerClient
|
|||||||
public event Action<WorkerLogEntry>? WorkerLogReceivedEvent;
|
public event Action<WorkerLogEntry>? WorkerLogReceivedEvent;
|
||||||
public event Action<string, string, string>? TaskQuestionAskedEvent;
|
public event Action<string, string, string>? TaskQuestionAskedEvent;
|
||||||
public event Action<string, string>? TaskQuestionResolvedEvent;
|
public event Action<string, string>? TaskQuestionResolvedEvent;
|
||||||
public event Action<string, IReadOnlyList<string>>? HandoffRequestedEvent;
|
public event Action<string, IReadOnlyList<string>, string>? HandoffRequestedEvent;
|
||||||
public event Action? PrepStartedEvent;
|
public event Action? PrepStartedEvent;
|
||||||
public event Action<string>? PrepLineEvent;
|
public event Action<string>? PrepLineEvent;
|
||||||
public event Action<bool>? PrepFinishedEvent;
|
public event Action<bool>? PrepFinishedEvent;
|
||||||
@@ -52,7 +52,7 @@ public abstract class StubWorkerClient : IWorkerClient
|
|||||||
public void RaiseConnectionRestored() => ConnectionRestoredEvent?.Invoke();
|
public void RaiseConnectionRestored() => ConnectionRestoredEvent?.Invoke();
|
||||||
public void RaiseTaskQuestionAsked(string taskId, string questionId, string question) => TaskQuestionAskedEvent?.Invoke(taskId, questionId, question);
|
public void RaiseTaskQuestionAsked(string taskId, string questionId, string question) => TaskQuestionAskedEvent?.Invoke(taskId, questionId, question);
|
||||||
public void RaiseTaskQuestionResolved(string taskId, string questionId) => TaskQuestionResolvedEvent?.Invoke(taskId, questionId);
|
public void RaiseTaskQuestionResolved(string taskId, string questionId) => TaskQuestionResolvedEvent?.Invoke(taskId, questionId);
|
||||||
public void RaiseHandoffRequested(string taskId, IReadOnlyList<string> survivingTaskIds) => HandoffRequestedEvent?.Invoke(taskId, survivingTaskIds);
|
public void RaiseHandoffRequested(string taskId, IReadOnlyList<string> survivingTaskIds, string nextPhase = "wait") => HandoffRequestedEvent?.Invoke(taskId, survivingTaskIds, nextPhase);
|
||||||
public void RaisePlanningMergeConflict(string planningTaskId, string subtaskId, IReadOnlyList<string> files, bool externallyDriven)
|
public void RaisePlanningMergeConflict(string planningTaskId, string subtaskId, IReadOnlyList<string> files, bool externallyDriven)
|
||||||
=> PlanningMergeConflictEvent?.Invoke(planningTaskId, subtaskId, files, externallyDriven);
|
=> PlanningMergeConflictEvent?.Invoke(planningTaskId, subtaskId, files, externallyDriven);
|
||||||
public void RaisePlanningMergeStarted(string planningTaskId, string targetBranch) => PlanningMergeStartedEvent?.Invoke(planningTaskId, targetBranch);
|
public void RaisePlanningMergeStarted(string planningTaskId, string targetBranch) => PlanningMergeStartedEvent?.Invoke(planningTaskId, targetBranch);
|
||||||
@@ -116,7 +116,7 @@ public abstract class StubWorkerClient : IWorkerClient
|
|||||||
IReadOnlyList<string> taskIds, string listId, string title, string descriptionHeader, CancellationToken ct = default)
|
IReadOnlyList<string> taskIds, string listId, string title, string descriptionHeader, CancellationToken ct = default)
|
||||||
=> Task.FromResult(Guid.NewGuid().ToString());
|
=> Task.FromResult(Guid.NewGuid().ToString());
|
||||||
public virtual Task<LaunchSpec> GetMergeHelperHandoffLaunchSpecAsync(
|
public virtual Task<LaunchSpec> GetMergeHelperHandoffLaunchSpecAsync(
|
||||||
string taskId, IReadOnlyList<string> survivingTaskIds, CancellationToken ct = default)
|
string taskId, IReadOnlyList<string> survivingTaskIds, string nextPhase, CancellationToken ct = default)
|
||||||
=> Task.FromResult(new LaunchSpec(".", "claude", Array.Empty<string>(), new Dictionary<string, string>()));
|
=> Task.FromResult(new LaunchSpec(".", "claude", Array.Empty<string>(), new Dictionary<string, string>()));
|
||||||
public virtual Task<LaunchSpec> GetPlanningStartLaunchSpecAsync(string taskId, CancellationToken ct = default)
|
public virtual Task<LaunchSpec> GetPlanningStartLaunchSpecAsync(string taskId, CancellationToken ct = default)
|
||||||
=> Task.FromResult(new LaunchSpec(".", "claude", Array.Empty<string>(), new Dictionary<string, string>()));
|
=> Task.FromResult(new LaunchSpec(".", "claude", Array.Empty<string>(), new Dictionary<string, string>()));
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ sealed class FakeWorkerClient : IWorkerClient
|
|||||||
public event Action<WorkerLogEntry>? WorkerLogReceivedEvent;
|
public event Action<WorkerLogEntry>? WorkerLogReceivedEvent;
|
||||||
public event Action<string, string, string>? TaskQuestionAskedEvent;
|
public event Action<string, string, string>? TaskQuestionAskedEvent;
|
||||||
public event Action<string, string>? TaskQuestionResolvedEvent;
|
public event Action<string, string>? TaskQuestionResolvedEvent;
|
||||||
public event Action<string, IReadOnlyList<string>>? HandoffRequestedEvent;
|
public event Action<string, IReadOnlyList<string>, string>? HandoffRequestedEvent;
|
||||||
public void RaiseTaskUpdated(string taskId) => TaskUpdatedEvent?.Invoke(taskId);
|
public void RaiseTaskUpdated(string taskId) => TaskUpdatedEvent?.Invoke(taskId);
|
||||||
public void RaiseWorktreeUpdated(string taskId) => WorktreeUpdatedEvent?.Invoke(taskId);
|
public void RaiseWorktreeUpdated(string taskId) => WorktreeUpdatedEvent?.Invoke(taskId);
|
||||||
public void RaiseTaskMessage(string taskId, string line) => TaskMessageEvent?.Invoke(taskId, line);
|
public void RaiseTaskMessage(string taskId, string line) => TaskMessageEvent?.Invoke(taskId, line);
|
||||||
@@ -87,7 +87,7 @@ sealed class FakeWorkerClient : IWorkerClient
|
|||||||
IReadOnlyList<string> taskIds, string listId, string title, string descriptionHeader, CancellationToken ct = default)
|
IReadOnlyList<string> taskIds, string listId, string title, string descriptionHeader, CancellationToken ct = default)
|
||||||
=> Task.FromResult(Guid.NewGuid().ToString());
|
=> Task.FromResult(Guid.NewGuid().ToString());
|
||||||
public Task<LaunchSpec> GetMergeHelperHandoffLaunchSpecAsync(
|
public Task<LaunchSpec> GetMergeHelperHandoffLaunchSpecAsync(
|
||||||
string taskId, IReadOnlyList<string> survivingTaskIds, CancellationToken ct = default)
|
string taskId, IReadOnlyList<string> survivingTaskIds, string nextPhase, CancellationToken ct = default)
|
||||||
=> Task.FromResult(new LaunchSpec(".", "claude", Array.Empty<string>(), new Dictionary<string, string>()));
|
=> Task.FromResult(new LaunchSpec(".", "claude", Array.Empty<string>(), new Dictionary<string, string>()));
|
||||||
public Task<LaunchSpec> GetAdHocLaunchSpecAsync(string directory, CancellationToken ct = default)
|
public Task<LaunchSpec> GetAdHocLaunchSpecAsync(string directory, CancellationToken ct = default)
|
||||||
=> Task.FromResult(new LaunchSpec(directory, "claude", Array.Empty<string>(), new Dictionary<string, string>()));
|
=> Task.FromResult(new LaunchSpec(directory, "claude", Array.Empty<string>(), new Dictionary<string, string>()));
|
||||||
|
|||||||
Reference in New Issue
Block a user