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:
@@ -23,6 +23,8 @@ public abstract class StubWorkerClient : IWorkerClient
|
||||
public event Action<string>? ListUpdatedEvent;
|
||||
public event Action<string, string>? TaskMessageEvent;
|
||||
public event Action<WorkerLogEntry>? WorkerLogReceivedEvent;
|
||||
public event Action<string, string, string>? TaskQuestionAskedEvent;
|
||||
public event Action<string, string>? TaskQuestionResolvedEvent;
|
||||
public event Action? PrepStartedEvent;
|
||||
public event Action<string>? PrepLineEvent;
|
||||
public event Action<bool>? PrepFinishedEvent;
|
||||
@@ -46,6 +48,8 @@ public abstract class StubWorkerClient : IWorkerClient
|
||||
public void RaiseTaskMessage(string taskId, string line) => TaskMessageEvent?.Invoke(taskId, line);
|
||||
public void RaiseTaskUpdated(string taskId) => TaskUpdatedEvent?.Invoke(taskId);
|
||||
public void RaiseConnectionRestored() => ConnectionRestoredEvent?.Invoke();
|
||||
public void RaiseTaskQuestionAsked(string taskId, string questionId, string question) => TaskQuestionAskedEvent?.Invoke(taskId, questionId, question);
|
||||
public void RaiseTaskQuestionResolved(string taskId, string questionId) => TaskQuestionResolvedEvent?.Invoke(taskId, questionId);
|
||||
|
||||
public void RaisePrepStarted() => PrepStartedEvent?.Invoke();
|
||||
public void RaisePrepLine(string line) => PrepLineEvent?.Invoke(line);
|
||||
@@ -58,6 +62,14 @@ public abstract class StubWorkerClient : IWorkerClient
|
||||
public virtual Task WakeQueueAsync() => Task.CompletedTask;
|
||||
public virtual Task RunNowAsync(string taskId) => Task.CompletedTask;
|
||||
public virtual Task ContinueTaskAsync(string taskId, string followUpPrompt) => Task.CompletedTask;
|
||||
public (string TaskId, string QuestionId, string Answer)? LastAnswer { get; private set; }
|
||||
public virtual Task AnswerTaskQuestionAsync(string taskId, string questionId, string answer)
|
||||
{
|
||||
LastAnswer = (taskId, questionId, answer);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
public PendingQuestionDto? PendingQuestion;
|
||||
public virtual Task<PendingQuestionDto?> GetPendingQuestionAsync(string taskId) => Task.FromResult(PendingQuestion);
|
||||
public virtual Task ResetTaskAsync(string taskId) => Task.CompletedTask;
|
||||
public virtual Task CancelTaskAsync(string taskId) => Task.CompletedTask;
|
||||
public virtual Task<List<AgentInfo>> GetAgentsAsync() => Task.FromResult(new List<AgentInfo>());
|
||||
|
||||
@@ -120,4 +120,73 @@ public class TaskMonitorViewModelTests : IDisposable
|
||||
vm.DetachCommand.Execute(null);
|
||||
Assert.Same(vm, requested);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TaskQuestionAsked_SurfacesQuestion()
|
||||
{
|
||||
var worker = new FakeWorker();
|
||||
using var vm = Build(worker);
|
||||
vm.SetTaskId("t1");
|
||||
|
||||
worker.RaiseTaskQuestionAsked("t1", "q1", "DPAPI or plaintext?");
|
||||
|
||||
Assert.True(vm.HasPendingQuestion);
|
||||
Assert.Equal("DPAPI or plaintext?", vm.PendingQuestion);
|
||||
Assert.True(vm.SubmitAnswerCommand.CanExecute(null) is false); // no draft yet
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TaskQuestionAsked_ForOtherTask_IsIgnored()
|
||||
{
|
||||
var worker = new FakeWorker();
|
||||
using var vm = Build(worker);
|
||||
vm.SetTaskId("t1");
|
||||
|
||||
worker.RaiseTaskQuestionAsked("other", "q1", "not mine");
|
||||
|
||||
Assert.False(vm.HasPendingQuestion);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SubmitAnswer_InvokesClient_AndClears()
|
||||
{
|
||||
var worker = new FakeWorker();
|
||||
using var vm = Build(worker);
|
||||
vm.SetTaskId("t1");
|
||||
worker.RaiseTaskQuestionAsked("t1", "q1", "DPAPI or plaintext?");
|
||||
vm.AnswerDraft = "DPAPI please";
|
||||
|
||||
Assert.True(vm.SubmitAnswerCommand.CanExecute(null));
|
||||
await vm.SubmitAnswerCommand.ExecuteAsync(null);
|
||||
|
||||
Assert.Equal(("t1", "q1", "DPAPI please"), worker.LastAnswer);
|
||||
Assert.False(vm.HasPendingQuestion);
|
||||
Assert.Equal(string.Empty, vm.AnswerDraft);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TaskQuestionResolved_ClearsMatchingQuestion()
|
||||
{
|
||||
var worker = new FakeWorker();
|
||||
using var vm = Build(worker);
|
||||
vm.SetTaskId("t1");
|
||||
worker.RaiseTaskQuestionAsked("t1", "q1", "?");
|
||||
|
||||
worker.RaiseTaskQuestionResolved("t1", "q1");
|
||||
|
||||
Assert.False(vm.HasPendingQuestion);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TaskFinished_ClearsPendingQuestion()
|
||||
{
|
||||
var worker = new FakeWorker();
|
||||
using var vm = Build(worker);
|
||||
vm.SetTaskId("t1");
|
||||
worker.RaiseTaskQuestionAsked("t1", "q1", "?");
|
||||
|
||||
worker.RaiseTaskFinished("slot-1", "t1", "done", DateTime.UtcNow);
|
||||
|
||||
Assert.False(vm.HasPendingQuestion);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,12 +34,16 @@ sealed class FakeWorkerClient : IWorkerClient
|
||||
public event Action<string>? ListUpdatedEvent;
|
||||
public event Action<string, string>? TaskMessageEvent;
|
||||
public event Action<WorkerLogEntry>? WorkerLogReceivedEvent;
|
||||
public event Action<string, string, string>? TaskQuestionAskedEvent;
|
||||
public event Action<string, string>? TaskQuestionResolvedEvent;
|
||||
public void RaiseTaskUpdated(string taskId) => TaskUpdatedEvent?.Invoke(taskId);
|
||||
public void RaiseWorktreeUpdated(string taskId) => WorktreeUpdatedEvent?.Invoke(taskId);
|
||||
public void RaiseTaskMessage(string taskId, string line) => TaskMessageEvent?.Invoke(taskId, line);
|
||||
|
||||
public Task RunNowAsync(string taskId) => Task.CompletedTask;
|
||||
public Task ContinueTaskAsync(string taskId, string followUpPrompt) => Task.CompletedTask;
|
||||
public Task AnswerTaskQuestionAsync(string taskId, string questionId, string answer) => Task.CompletedTask;
|
||||
public Task<PendingQuestionDto?> GetPendingQuestionAsync(string taskId) => Task.FromResult<PendingQuestionDto?>(null);
|
||||
public Task ResetTaskAsync(string taskId) => Task.CompletedTask;
|
||||
public Task CancelTaskAsync(string taskId) => Task.CompletedTask;
|
||||
public Task<List<AgentInfo>> GetAgentsAsync() => Task.FromResult(new List<AgentInfo>());
|
||||
|
||||
Reference in New Issue
Block a user