fix(ui): refresh the run session id live, close the stale handoff pane

A task selected before its run started kept LatestRunSessionId null, so the
roadblock reply box and Continue stayed dead until it was re-selected.

The handoff left the Phase 1-2 tile open so its last message could be read, but
its process is gone by then and the terminal renders empty -- a dead
placeholder. It is closed on handoff now.

Also drops the WARN flood from worktree cleanup (already-unregistered worktree
and already-deleted branch are normal outcomes, not failures) and replaces the
fixed sleep in UsageGate_TransitionLogging_FiresOncePerChange with the polling
helper that already sits three lines below it in the same file.
This commit is contained in:
mika kuns
2026-08-06 10:20:54 +02:00
parent 56f7d64f07
commit f106c890b3
5 changed files with 79 additions and 22 deletions
@@ -366,6 +366,10 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
if (Task?.Id != taskId) return;
_ = RefreshWorktreeAsync(taskId);
_ = RefreshChildOutcomeAsync(taskId);
// The run only gets a session id once it has started, so a task that was already
// selected while it ran still holds the null from selection time. Without this the
// roadblock reply box and Continue stay dead until the task is re-selected.
_ = RefreshLatestRunSessionIdAsync(taskId);
};
_worker.TaskFinishedEvent += _workerTaskFinishedHandler;
@@ -801,6 +805,18 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
// Refreshes the bound row itself (status, planning phase, worktree/branch mirrors, etc.) from
// the DB. Kept independent of TasksIslandViewModel's own handler: the row instance backing
// Task may have been replaced by a full list reload, so this must not assume it stayed live.
private async System.Threading.Tasks.Task RefreshLatestRunSessionIdAsync(string taskId)
{
try
{
await using var ctx = await _dbFactory.CreateDbContextAsync();
var latestRun = await new TaskRunRepository(ctx).GetLatestByTaskIdAsync(taskId);
if (Task?.Id != taskId) return;
LatestRunSessionId = latestRun?.SessionId;
}
catch { /* best-effort */ }
}
private async System.Threading.Tasks.Task RefreshBoundTaskAsync(string taskId)
{
try
@@ -248,14 +248,18 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
}
// List-handler handoff: the running session called handoff_list_handler at the end of Phase 2.
// Opens a SECOND ConPTY tile for the SAME handler task id to carry out Phases 3-5 — deliberately
// NOT deduped by TaskId like OpenMergeHelperConPtySessionAsync above, since the original tile is
// meant to stay open (Mika closes it by hand once he has seen its last message). No new task is
// created here; see InteractiveLaunchSpecService.BuildForMergeHelperHandoffAsync.
// Replaces the Phase 1-2 tile with a fresh one for the SAME handler task id, which carries out
// Phases 3-5. The old tile used to be left open so its last message could still be read, but
// its process is gone by then and the terminal renders empty — it was only ever a dead
// placeholder to close by hand. No new task is created here; see
// InteractiveLaunchSpecService.BuildForMergeHelperHandoffAsync.
public async System.Threading.Tasks.Task OpenMergeHelperHandoffConPtySessionAsync(string taskId, IReadOnlyList<string> survivingTaskIds)
{
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 title = baseTitle + Loc.T("missionControl.mergeHelperHandoffTitleSuffix");
try