Merge branch 'claudedo/f518b71f990643cfaf73174e8617b2b5'
Drop the unused deferredUntil parameter the branch added to HandoffRequested: the worker still sends three args, so the four-arg client handler would never bind. The reset time already arrives via HandlerHandoffDeferred.
This commit is contained in:
@@ -393,6 +393,7 @@
|
||||
"conptyExited": "Sitzung beendet (Code {0}).",
|
||||
"usageWarningNotDelivered": "Die Listen-Handler-Sitzung konnte nicht vor dem Usage-Limit gewarnt werden.",
|
||||
"terminalSendFailed": "Konnte nicht an die Terminal-Sitzung gesendet werden.",
|
||||
"deferredHandoffTitleSuffix": "warte auf Limit-Zurücksetzen ({0})",
|
||||
"mergeHelperTitle": "Merge-Helfer",
|
||||
"mergeHelperHandoffTitleSuffix": " (Übergabe)",
|
||||
"mergeHelperTaskTitle": "Listen-Handler: {0}",
|
||||
|
||||
@@ -393,6 +393,7 @@
|
||||
"conptyExited": "Session ended (code {0}).",
|
||||
"usageWarningNotDelivered": "Couldn't warn the list-handler session about the usage limit.",
|
||||
"terminalSendFailed": "Couldn't send that to the terminal session.",
|
||||
"deferredHandoffTitleSuffix": "waiting for limit reset ({0})",
|
||||
"mergeHelperTitle": "Merge Helper",
|
||||
"mergeHelperHandoffTitleSuffix": " (Handoff)",
|
||||
"mergeHelperTaskTitle": "List handler: {0}",
|
||||
|
||||
@@ -41,6 +41,9 @@ public interface IWorkerClient : INotifyPropertyChanged
|
||||
/// (handlerTaskId, message). Mission Control types the message into that task's ConPTY pane so
|
||||
/// the session can hand off cleanly; no pane for the task means nothing happens.</summary>
|
||||
event Action<string, string>? HandlerUsageWarningEvent;
|
||||
/// <summary>A list-handler handoff is deferred because the 5h usage window is exhausted:
|
||||
/// (handlerTaskId, dueAt). Mission Control updates the pane title to show the reset time.</summary>
|
||||
event Action<string, DateTimeOffset>? HandlerHandoffDeferredEvent;
|
||||
|
||||
/// <summary>An agent added a note through the MCP tool — an open notes editor reloads.</summary>
|
||||
event Action? NotesUpdatedEvent;
|
||||
|
||||
@@ -52,6 +52,7 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
||||
public event Action<string, string>? SessionHandoffRequestedEvent;
|
||||
public event Action<string, IReadOnlyList<string>>? ListHandlerStartRequestedEvent;
|
||||
public event Action<string, string>? HandlerUsageWarningEvent;
|
||||
public event Action<string, DateTimeOffset>? HandlerHandoffDeferredEvent;
|
||||
public event Action? ConnectionRestoredEvent;
|
||||
public event Action<string>? WorktreeUpdatedEvent;
|
||||
public event Action<string>? ListUpdatedEvent;
|
||||
@@ -176,6 +177,11 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
||||
Dispatcher.UIThread.Post(() => HandlerUsageWarningEvent?.Invoke(taskId, message));
|
||||
});
|
||||
|
||||
_hub.On<string, DateTimeOffset>("HandlerHandoffDeferred", (taskId, dueAt) =>
|
||||
{
|
||||
Dispatcher.UIThread.Post(() => HandlerHandoffDeferredEvent?.Invoke(taskId, dueAt));
|
||||
});
|
||||
|
||||
_hub.On<string>("WorktreeUpdated", taskId =>
|
||||
{
|
||||
Dispatcher.UIThread.Post(() => WorktreeUpdatedEvent?.Invoke(taskId));
|
||||
|
||||
@@ -25,6 +25,7 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
|
||||
private readonly Action<string, IReadOnlyList<string>, string> _onHandoffRequested;
|
||||
private readonly Action<string, IReadOnlyList<string>> _onListHandlerStartRequested;
|
||||
private readonly Action<string, string> _onHandlerUsageWarning;
|
||||
private readonly Action<string, DateTimeOffset> _onHandlerHandoffDeferred;
|
||||
|
||||
// Embedded ConPTY sessions (task-based only) — a manual cockpit detached from the
|
||||
// review/merge/status machinery.
|
||||
@@ -122,6 +123,8 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
|
||||
_worker.ListHandlerStartRequestedEvent += _onListHandlerStartRequested;
|
||||
_onHandlerUsageWarning = (taskId, message) => { _ = WarnHandlerSessionAsync(taskId, message); };
|
||||
_worker.HandlerUsageWarningEvent += _onHandlerUsageWarning;
|
||||
_onHandlerHandoffDeferred = (taskId, dueAt) => UpdateHandlerSessionTitleAsync(taskId, dueAt);
|
||||
_worker.HandlerHandoffDeferredEvent += _onHandlerHandoffDeferred;
|
||||
|
||||
_ = RefreshQueueAsync();
|
||||
_ = RefreshSessionTargetsAsync();
|
||||
@@ -556,6 +559,17 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
|
||||
ErrorReported?.Invoke(Loc.T("missionControl.usageWarningNotDelivered"));
|
||||
}
|
||||
|
||||
// A handler session's handoff is deferred due to usage limits: update the pane title to show
|
||||
// the reset time. No pane = nothing to update, not an error.
|
||||
private void UpdateHandlerSessionTitleAsync(string taskId, DateTimeOffset dueAt)
|
||||
{
|
||||
if (string.IsNullOrEmpty(taskId)) return;
|
||||
if (ConPtySessions.FirstOrDefault(s => s.TaskId == taskId) is not { } pane) return;
|
||||
|
||||
var suffix = Loc.T("missionControl.deferredHandoffTitleSuffix", dueAt.LocalDateTime.ToString("HH:mm"));
|
||||
pane.DisplayTitle += $" — {suffix}";
|
||||
}
|
||||
|
||||
// Wires a freshly built pane and shows it immediately — the pane resolves its own launch spec,
|
||||
// so the tile is on screen (spinner running) while the worker is still preparing the worktree.
|
||||
private void AddConPtyPane(ConPtyPaneViewModel pane, int? index = null)
|
||||
@@ -725,6 +739,7 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
|
||||
_worker.SessionHandoffRequestedEvent -= OpenSessionHandoff;
|
||||
_worker.ListHandlerStartRequestedEvent -= _onListHandlerStartRequested;
|
||||
_worker.HandlerUsageWarningEvent -= _onHandlerUsageWarning;
|
||||
_worker.HandlerHandoffDeferredEvent -= _onHandlerHandoffDeferred;
|
||||
ConPtySessions.CollectionChanged -= OnConPtySessionsChanged;
|
||||
Panes.CollectionChanged -= OnPanesChanged;
|
||||
foreach (var c in ConPtySessions.ToList())
|
||||
|
||||
@@ -92,6 +92,8 @@ public sealed class HandoffMcpTools
|
||||
taskId, survivingTaskIds, round.EffectivePhase, cancellationToken);
|
||||
if (deferredUntil is null)
|
||||
await _broadcaster.HandoffRequested(taskId, survivingTaskIds, round.EffectivePhase);
|
||||
else
|
||||
await _broadcaster.HandlerHandoffDeferred(taskId, deferredUntil.Value);
|
||||
|
||||
return new HandoffListHandlerResult(
|
||||
true, taskId, task.Number, survivingTaskIds.Count, round.EffectivePhase, round.Coerced, deferredUntil);
|
||||
|
||||
@@ -52,6 +52,11 @@ public sealed class HubBroadcaster : IPrimeBroadcaster, IRefineBroadcaster
|
||||
public Task HandlerUsageWarning(string handlerTaskId, string message) =>
|
||||
_hub.Clients.All.SendAsync("HandlerUsageWarning", handlerTaskId, message);
|
||||
|
||||
// A list-handler handoff is deferred because the 5h usage window is exhausted. Mission Control
|
||||
// updates the pane title to show the reset time, so the user sees when the session will resume.
|
||||
public Task HandlerHandoffDeferred(string handlerTaskId, DateTimeOffset dueAt) =>
|
||||
_hub.Clients.All.SendAsync("HandlerHandoffDeferred", handlerTaskId, dueAt);
|
||||
|
||||
/// <summary>An agent wrote a note through MCP — the open notes editor reloads.</summary>
|
||||
public Task NotesUpdated() =>
|
||||
_hub.Clients.All.SendAsync("NotesUpdated");
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using ClaudeDo.Ui.Services;
|
||||
using ClaudeDo.Ui.ViewModels.MissionControl;
|
||||
using Xunit;
|
||||
|
||||
namespace ClaudeDo.Ui.Tests;
|
||||
|
||||
public class MissionControlViewModelTests
|
||||
{
|
||||
[Fact]
|
||||
public void DeferredHandoffUpdatesConPtyPaneTitleWithResetTime()
|
||||
{
|
||||
var taskId = Guid.NewGuid().ToString();
|
||||
var title = "Test Handler";
|
||||
var dueAt = new DateTimeOffset(2026, 9, 10, 14, 30, 0, TimeSpan.Zero);
|
||||
|
||||
var pane = new ConPtyPaneViewModel(taskId, title, () => Task.FromResult(
|
||||
new TerminalLaunchDescriptor(".", "claude", Array.Empty<string>(), new Dictionary<string, string>())));
|
||||
|
||||
var originalTitle = pane.DisplayTitle;
|
||||
|
||||
// Simulate the update that would happen on HandlerHandoffDeferredEvent
|
||||
var suffix = $"waiting for limit reset ({dueAt.LocalDateTime:HH:mm})";
|
||||
pane.DisplayTitle += $" — {suffix}";
|
||||
|
||||
Assert.NotEqual(originalTitle, pane.DisplayTitle);
|
||||
Assert.Contains("waiting for limit reset", pane.DisplayTitle);
|
||||
Assert.Contains(dueAt.LocalDateTime.ToString("HH:mm"), pane.DisplayTitle);
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,7 @@ public abstract class StubWorkerClient : IWorkerClient
|
||||
public event Action<string, string>? SessionHandoffRequestedEvent;
|
||||
public event Action<string, IReadOnlyList<string>>? ListHandlerStartRequestedEvent;
|
||||
public event Action<string, string>? HandlerUsageWarningEvent;
|
||||
public event Action<string, DateTimeOffset>? HandlerHandoffDeferredEvent;
|
||||
public event Action? NotesUpdatedEvent;
|
||||
public event Action? PrepStartedEvent;
|
||||
public event Action<string>? PrepLineEvent;
|
||||
@@ -62,6 +63,7 @@ public abstract class StubWorkerClient : IWorkerClient
|
||||
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 RaiseHandlerHandoffDeferred(string taskId, DateTimeOffset dueAt) => HandlerHandoffDeferredEvent?.Invoke(taskId, dueAt);
|
||||
public void RaisePlanningMergeConflict(string planningTaskId, string subtaskId, IReadOnlyList<string> files, bool externallyDriven)
|
||||
=> PlanningMergeConflictEvent?.Invoke(planningTaskId, subtaskId, files, externallyDriven);
|
||||
public void RaiseOperationProgress(string opKey, string phase, int current, int total) => OperationProgressEvent?.Invoke(opKey, phase, current, total);
|
||||
|
||||
Reference in New Issue
Block a user