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:
mika kuns
2026-09-10 13:18:09 +02:00
parent 1177463a82
commit 5be972564a
9 changed files with 77 additions and 9 deletions
@@ -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())