feat(ui): answer a running task's question inline in Mission Control
TaskMonitorViewModel surfaces a pending AskUser question (TaskQuestionAsked / TaskQuestionResolved events) with an AnswerDraft + SubmitAnswerCommand that calls the new IWorkerClient.AnswerTaskQuestionAsync; MonitorPaneView shows an accent question banner with an input box above the terminal. Pending question is cleared on answer/resolve/finish and re-hydrated on attach via GetPendingQuestionAsync. en/de localization for missionControl.question.*; test fakes updated.
This commit is contained in:
@@ -75,6 +75,23 @@ public sealed partial class TaskMonitorViewModel : ViewModelBase, IDisposable
|
||||
private readonly Action<string, string, DateTime> _onTaskStarted;
|
||||
private readonly Action<string, string, string, DateTime> _onTaskFinished;
|
||||
private readonly Action<string> _onTaskUpdated;
|
||||
private readonly Action<string, string, string> _onTaskQuestionAsked;
|
||||
private readonly Action<string, string> _onTaskQuestionResolved;
|
||||
|
||||
// A question the running task raised via AskUser and is blocking on, plus the answer
|
||||
// the user is typing. Ephemeral (in-memory + live events) — the task is still Running.
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(HasPendingQuestion))]
|
||||
[NotifyCanExecuteChangedFor(nameof(SubmitAnswerCommand))]
|
||||
private string? _pendingQuestionId;
|
||||
|
||||
[ObservableProperty] private string? _pendingQuestion;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyCanExecuteChangedFor(nameof(SubmitAnswerCommand))]
|
||||
private string _answerDraft = string.Empty;
|
||||
|
||||
public bool HasPendingQuestion => PendingQuestionId is not null;
|
||||
|
||||
public TaskMonitorViewModel(IDbContextFactory<ClaudeDoDbContext> dbFactory, IWorkerClient worker)
|
||||
{
|
||||
@@ -101,6 +118,7 @@ public sealed partial class TaskMonitorViewModel : ViewModelBase, IDisposable
|
||||
Text = $"── {status.ToUpperInvariant()} · {finishedAt.ToLocalTime():HH:mm:ss} ──",
|
||||
});
|
||||
AgentState = FinishedStatusToStateKey(status);
|
||||
ClearPendingQuestion();
|
||||
_ = RefreshOutcomeAsync(taskId);
|
||||
};
|
||||
_worker.TaskFinishedEvent += _onTaskFinished;
|
||||
@@ -111,8 +129,50 @@ public sealed partial class TaskMonitorViewModel : ViewModelBase, IDisposable
|
||||
_ = RefreshStatusAsync(taskId);
|
||||
};
|
||||
_worker.TaskUpdatedEvent += _onTaskUpdated;
|
||||
|
||||
_onTaskQuestionAsked = (taskId, questionId, question) =>
|
||||
{
|
||||
if (taskId != _subscribedTaskId) return;
|
||||
PendingQuestionId = questionId;
|
||||
PendingQuestion = question;
|
||||
};
|
||||
_worker.TaskQuestionAskedEvent += _onTaskQuestionAsked;
|
||||
|
||||
_onTaskQuestionResolved = (taskId, questionId) =>
|
||||
{
|
||||
if (taskId == _subscribedTaskId && PendingQuestionId == questionId)
|
||||
ClearPendingQuestion();
|
||||
};
|
||||
_worker.TaskQuestionResolvedEvent += _onTaskQuestionResolved;
|
||||
}
|
||||
|
||||
// Surface a pending question (used by live event + re-attach hydration).
|
||||
public void SetPendingQuestion(string questionId, string question)
|
||||
{
|
||||
PendingQuestionId = questionId;
|
||||
PendingQuestion = question;
|
||||
}
|
||||
|
||||
private void ClearPendingQuestion()
|
||||
{
|
||||
PendingQuestionId = null;
|
||||
PendingQuestion = null;
|
||||
AnswerDraft = string.Empty;
|
||||
}
|
||||
|
||||
[RelayCommand(CanExecute = nameof(CanSubmitAnswer))]
|
||||
private async System.Threading.Tasks.Task SubmitAnswer()
|
||||
{
|
||||
var questionId = PendingQuestionId;
|
||||
if (questionId is null || string.IsNullOrEmpty(_subscribedTaskId)) return;
|
||||
var answer = AnswerDraft;
|
||||
if (string.IsNullOrWhiteSpace(answer)) return;
|
||||
ClearPendingQuestion(); // optimistic; the resolved event also clears
|
||||
await _worker.AnswerTaskQuestionAsync(_subscribedTaskId, questionId, answer);
|
||||
}
|
||||
|
||||
private bool CanSubmitAnswer() => HasPendingQuestion && !string.IsNullOrWhiteSpace(AnswerDraft);
|
||||
|
||||
partial void OnAgentStateChanged(string value)
|
||||
{
|
||||
OnPropertyChanged(nameof(AgentStatusLabel));
|
||||
@@ -140,6 +200,7 @@ public sealed partial class TaskMonitorViewModel : ViewModelBase, IDisposable
|
||||
AgentState = "idle";
|
||||
SessionOutcome = null;
|
||||
Roadblocks = null;
|
||||
ClearPendingQuestion();
|
||||
}
|
||||
|
||||
[ObservableProperty]
|
||||
@@ -361,5 +422,7 @@ public sealed partial class TaskMonitorViewModel : ViewModelBase, IDisposable
|
||||
_worker.TaskStartedEvent -= _onTaskStarted;
|
||||
_worker.TaskFinishedEvent -= _onTaskFinished;
|
||||
_worker.TaskUpdatedEvent -= _onTaskUpdated;
|
||||
_worker.TaskQuestionAskedEvent -= _onTaskQuestionAsked;
|
||||
_worker.TaskQuestionResolvedEvent -= _onTaskQuestionResolved;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user