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:
Mika Kuns
2026-06-25 22:53:46 +02:00
parent c7f8280106
commit 917301d61c
10 changed files with 224 additions and 2 deletions

View File

@@ -47,6 +47,8 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
public event Action<string, string, string, DateTime>? TaskFinishedEvent;
public event Action<string, string>? TaskMessageEvent;
public event Action<string>? TaskUpdatedEvent;
public event Action<string, string, string>? TaskQuestionAskedEvent;
public event Action<string, string>? TaskQuestionResolvedEvent;
public event Action? ConnectionRestoredEvent;
public event Action<string>? WorktreeUpdatedEvent;
public event Action<string>? ListUpdatedEvent;
@@ -136,6 +138,16 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
Dispatcher.UIThread.Post(() => TaskUpdatedEvent?.Invoke(taskId));
});
_hub.On<string, string, string>("TaskQuestionAsked", (taskId, questionId, question) =>
{
Dispatcher.UIThread.Post(() => TaskQuestionAskedEvent?.Invoke(taskId, questionId, question));
});
_hub.On<string, string>("TaskQuestionResolved", (taskId, questionId) =>
{
Dispatcher.UIThread.Post(() => TaskQuestionResolvedEvent?.Invoke(taskId, questionId));
});
_hub.On<string>("WorktreeUpdated", taskId =>
{
Dispatcher.UIThread.Post(() => WorktreeUpdatedEvent?.Invoke(taskId));
@@ -261,6 +273,15 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
await _hub.InvokeAsync("ContinueTask", taskId, followUpPrompt);
}
public async Task AnswerTaskQuestionAsync(string taskId, string questionId, string answer)
{
try { await _hub.InvokeAsync<bool>("AnswerTaskQuestion", taskId, questionId, answer); }
catch { /* offline or already resolved — the UI clears optimistically */ }
}
public Task<PendingQuestionDto?> GetPendingQuestionAsync(string taskId)
=> TryInvokeAsync<PendingQuestionDto>("GetPendingQuestion", taskId);
public async Task ResetTaskAsync(string taskId)
{
await _hub.InvokeAsync("ResetTask", taskId);
@@ -586,6 +607,7 @@ public sealed record WorktreeOverviewDto(
bool PathExistsOnDisk);
public sealed record ForceRemoveResultDto(bool Removed, string? Reason);
public sealed record PendingQuestionDto(string TaskId, string QuestionId, string Question);
public sealed record OnlineInboxStateDto(
bool Enabled,