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:
@@ -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
|
||||
|
||||
@@ -143,8 +143,15 @@ public sealed class WorktreeMaintenanceService
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex,
|
||||
"git worktree remove failed for {Path}; falling back to directory delete", row.Path);
|
||||
// "is not a working tree" means git already forgot this worktree (pruned, or
|
||||
// removed by hand). Deleting the directory is then the whole job, not a fallback
|
||||
// from a real failure -- warning about it floods the footer log strip when a
|
||||
// batch of stale rows is cleaned up.
|
||||
if (IsAlreadyUnregistered(ex))
|
||||
_logger.LogDebug(ex, "git no longer tracks {Path}; deleting the directory", row.Path);
|
||||
else
|
||||
_logger.LogWarning(ex,
|
||||
"git worktree remove failed for {Path}; falling back to directory delete", row.Path);
|
||||
try { if (Directory.Exists(row.Path)) Directory.Delete(row.Path, recursive: true); }
|
||||
catch (Exception delEx)
|
||||
{
|
||||
@@ -186,8 +193,12 @@ public sealed class WorktreeMaintenanceService
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to delete branch {Branch} for worktree {Path}",
|
||||
row.BranchName, row.Path);
|
||||
// Already deleted -- the normal case once the branch has been merged.
|
||||
if (IsBranchAlreadyGone(ex))
|
||||
_logger.LogDebug(ex, "branch {Branch} was already deleted", row.BranchName);
|
||||
else
|
||||
_logger.LogWarning(ex, "Failed to delete branch {Branch} for worktree {Path}",
|
||||
row.BranchName, row.Path);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -197,5 +208,12 @@ public sealed class WorktreeMaintenanceService
|
||||
return (true, branchDeleted);
|
||||
}
|
||||
|
||||
// Both are "already done" outcomes, not failures — see the call sites.
|
||||
private static bool IsAlreadyUnregistered(Exception ex) =>
|
||||
ex.Message.Contains("is not a working tree", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static bool IsBranchAlreadyGone(Exception ex) =>
|
||||
ex.Message.Contains("' not found", StringComparison.Ordinal);
|
||||
|
||||
private sealed record WorktreeRow(string TaskId, string Path, string BranchName, string? WorkingDir);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user