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:
mika kuns
2026-09-29 08:54:14 +02:00
9 changed files with 67 additions and 0 deletions
@@ -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;
+6
View File
@@ -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())
+2
View File
@@ -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);