feat(ui): mission-control pane title shows usage limit reset time
When a list-handler handoff is deferred waiting for the 5h usage limit to
reset, the Mission Control pane title is augmented with ⏳ waiting for
limit reset (HH:mm) so the wait is visible at a glance. Extends
HubBroadcaster.HandoffRequested with an optional deferredUntil DateTime
parameter, wired through SignalR to MissionControlViewModel, which formats
it into the pane title via Loc.T(missionControl.waitingForLimitReset).
This commit is contained in:
@@ -349,6 +349,7 @@
|
||||
"conptyLaunchFailed": "Interaktive Sitzung konnte nicht geöffnet werden: {0}",
|
||||
"conptyStarting": "Sitzung wird gestartet…",
|
||||
"usageWarningNotDelivered": "Die Listen-Handler-Sitzung konnte nicht vor dem Usage-Limit gewarnt werden.",
|
||||
"deferredHandoffTitleSuffix": "warte auf Limit-Zurücksetzen ({0})",
|
||||
"mergeHelperTitle": "Merge-Helfer",
|
||||
"mergeHelperHandoffTitleSuffix": " (Übergabe)",
|
||||
"mergeHelperTaskTitle": "Listen-Handler: {0}",
|
||||
|
||||
@@ -349,6 +349,7 @@
|
||||
"conptyLaunchFailed": "Couldn't open interactive session: {0}",
|
||||
"conptyStarting": "Starting session…",
|
||||
"usageWarningNotDelivered": "Couldn't warn the list-handler session about the usage limit.",
|
||||
"deferredHandoffTitleSuffix": "waiting for limit reset ({0})",
|
||||
"mergeHelperTitle": "Merge Helper",
|
||||
"mergeHelperHandoffTitleSuffix": " (Handoff)",
|
||||
"mergeHelperTaskTitle": "List handler: {0}",
|
||||
|
||||
@@ -26,9 +26,10 @@ public interface IWorkerClient : INotifyPropertyChanged
|
||||
event Action<string, string>? TaskQuestionResolvedEvent;
|
||||
|
||||
/// <summary>A running list-handler session called handoff_list_handler at the end of a phase:
|
||||
/// (handlerTaskId, survivingTaskIds, nextPhase). The UI opens a second ConPTY tile for the same
|
||||
/// task to carry out nextPhase ("wait" | "merge" | "wait_final" | "merge_final").</summary>
|
||||
event Action<string, IReadOnlyList<string>, string>? HandoffRequestedEvent;
|
||||
/// (handlerTaskId, survivingTaskIds, nextPhase, deferredUntil). The UI opens a second ConPTY tile
|
||||
/// for the same task to carry out nextPhase ("wait" | "merge" | "wait_final" | "merge_final").
|
||||
/// deferredUntil hints when the session is suspended waiting for the usage limit to reset.</summary>
|
||||
event Action<string, IReadOnlyList<string>, string, DateTime?>? HandoffRequestedEvent;
|
||||
|
||||
/// <summary>start_list_handler was called through the external MCP: (listId, taskIds). The UI
|
||||
/// starts a list-handler run for them exactly like the "Let Claude handle it" button.</summary>
|
||||
@@ -37,6 +38,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;
|
||||
|
||||
@@ -48,9 +48,10 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
||||
public event Action<string>? TaskUpdatedEvent;
|
||||
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, IReadOnlyList<string>, string, DateTime?>? HandoffRequestedEvent;
|
||||
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;
|
||||
@@ -156,9 +157,9 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
||||
Dispatcher.UIThread.Post(() => TaskQuestionResolvedEvent?.Invoke(taskId, questionId));
|
||||
});
|
||||
|
||||
_hub.On<string, IReadOnlyList<string>, string>("HandoffRequested", (taskId, survivingTaskIds, nextPhase) =>
|
||||
_hub.On<string, IReadOnlyList<string>, string, DateTime?>("HandoffRequested", (taskId, survivingTaskIds, nextPhase, deferredUntil) =>
|
||||
{
|
||||
Dispatcher.UIThread.Post(() => HandoffRequestedEvent?.Invoke(taskId, survivingTaskIds, nextPhase));
|
||||
Dispatcher.UIThread.Post(() => HandoffRequestedEvent?.Invoke(taskId, survivingTaskIds, nextPhase, deferredUntil));
|
||||
});
|
||||
|
||||
_hub.On<string, IReadOnlyList<string>>("ListHandlerStartRequested", (listId, taskIds) =>
|
||||
@@ -171,6 +172,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));
|
||||
|
||||
@@ -20,9 +20,10 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
|
||||
private readonly Action<string, string, string, DateTime> _onTaskFinished;
|
||||
private readonly Action<string> _onTaskUpdated;
|
||||
private readonly Action _onConnectionRestored;
|
||||
private readonly Action<string, IReadOnlyList<string>, string> _onHandoffRequested;
|
||||
private readonly Action<string, IReadOnlyList<string>, string, DateTime?> _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.
|
||||
@@ -106,13 +107,15 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
|
||||
_onConnectionRestored = () => { _ = RefreshQueueAsync(); };
|
||||
_worker.ConnectionRestoredEvent += _onConnectionRestored;
|
||||
|
||||
_onHandoffRequested = (taskId, survivingTaskIds, nextPhase) => { _ = OpenMergeHelperHandoffConPtySessionAsync(taskId, survivingTaskIds, nextPhase); };
|
||||
_onHandoffRequested = (taskId, survivingTaskIds, nextPhase, deferredUntil) => { _ = OpenMergeHelperHandoffConPtySessionAsync(taskId, survivingTaskIds, nextPhase, deferredUntil); };
|
||||
_worker.HandoffRequestedEvent += _onHandoffRequested;
|
||||
|
||||
_onListHandlerStartRequested = (listId, taskIds) => { OpenRequested?.Invoke(); _ = OpenMergeHelperConPtySessionAsync(listId, taskIds); };
|
||||
_worker.ListHandlerStartRequestedEvent += _onListHandlerStartRequested;
|
||||
_onHandlerUsageWarning = (taskId, message) => { _ = WarnHandlerSessionAsync(taskId, message); };
|
||||
_worker.HandlerUsageWarningEvent += _onHandlerUsageWarning;
|
||||
_onHandlerHandoffDeferred = (taskId, dueAt) => UpdateHandlerSessionTitleAsync(taskId, dueAt);
|
||||
_worker.HandlerHandoffDeferredEvent += _onHandlerHandoffDeferred;
|
||||
|
||||
_ = RefreshQueueAsync();
|
||||
_ = RefreshSessionTargetsAsync();
|
||||
@@ -403,7 +406,7 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
|
||||
// FirstOrDefault. No new task is created here; see
|
||||
// InteractiveLaunchSpecService.BuildForMergeHelperHandoffAsync.
|
||||
public async System.Threading.Tasks.Task OpenMergeHelperHandoffConPtySessionAsync(
|
||||
string taskId, IReadOnlyList<string> survivingTaskIds, string nextPhase)
|
||||
string taskId, IReadOnlyList<string> survivingTaskIds, string nextPhase, DateTime? deferredUntil = null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(taskId) || survivingTaskIds is not { Count: > 0 }) return;
|
||||
OpenRequested?.Invoke();
|
||||
@@ -444,6 +447,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)
|
||||
@@ -596,6 +610,7 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
|
||||
_worker.HandoffRequestedEvent -= _onHandoffRequested;
|
||||
_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);
|
||||
|
||||
@@ -47,6 +47,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");
|
||||
|
||||
Reference in New Issue
Block a user