feat(claude-do): merge Stream Claude CLI update progress into the footer strip

ClaudeDo-Task: 92130a5d447b4b0da301ce72e42c041a
This commit is contained in:
mika kuns
2026-10-01 10:29:48 +02:00
3 changed files with 21 additions and 5 deletions
+1 -1
View File
@@ -866,7 +866,7 @@
}, },
"vm": { "vm": {
"connection": { "online": "Online", "connecting": "Verbinden…", "offline": "Offline" }, "connection": { "online": "Online", "connecting": "Verbinden…", "offline": "Offline" },
"shell": { "unexpectedError": "Unerwarteter Fehler: {0}", "shutdownConfirm": "Dadurch wird der ClaudeDo-Worker beendet (laufende Task-Runs und interaktive Sitzungen brechen ab) und die App geschlossen. Fortfahren?" }, "shell": { "unexpectedError": "Unerwarteter Fehler: {0}", "updatingClaudeCli": "Claude CLI wird aktualisiert…", "shutdownConfirm": "Dadurch wird der ClaudeDo-Worker beendet (laufende Task-Runs und interaktive Sitzungen brechen ab) und die App geschlossen. Fortfahren?" },
"mainWindow": { "closeConfirm": { "message": "{0} Mission-Control-Sitzung(en) laufen noch. Beim Beenden werden sie abgebrochen. Trotzdem schließen?" } }, "mainWindow": { "closeConfirm": { "message": "{0} Mission-Control-Sitzung(en) laufen noch. Beim Beenden werden sie abgebrochen. Trotzdem schließen?" } },
"agentStatus": { "idle": "Leerlauf", "queued": "In Warteschlange", "running": "Läuft", "review": "Prüfung", "children": "Wartet auf Teilaufgaben", "done": "Fertig", "failed": "Fehlgeschlagen", "cancelled": "Abgebrochen" }, "agentStatus": { "idle": "Leerlauf", "queued": "In Warteschlange", "running": "Läuft", "review": "Prüfung", "children": "Wartet auf Teilaufgaben", "done": "Fertig", "failed": "Fehlgeschlagen", "cancelled": "Abgebrochen" },
"taskStatus": { "idle": "Leerlauf", "queued": "In Warteschlange", "running": "Läuft", "waitingForReview": "Wartet auf Prüfung", "waitingForChildren": "Wartet auf Teilaufgaben", "done": "Fertig", "failed": "Fehlgeschlagen", "cancelled": "Abgebrochen", "parked": "Geparkt", "interactive": "Interaktiv", "waiting": "Wartet" }, "taskStatus": { "idle": "Leerlauf", "queued": "In Warteschlange", "running": "Läuft", "waitingForReview": "Wartet auf Prüfung", "waitingForChildren": "Wartet auf Teilaufgaben", "done": "Fertig", "failed": "Fehlgeschlagen", "cancelled": "Abgebrochen", "parked": "Geparkt", "interactive": "Interaktiv", "waiting": "Wartet" },
+1 -1
View File
@@ -866,7 +866,7 @@
}, },
"vm": { "vm": {
"connection": { "online": "Online", "connecting": "Connecting…", "offline": "Offline" }, "connection": { "online": "Online", "connecting": "Connecting…", "offline": "Offline" },
"shell": { "unexpectedError": "Unexpected error: {0}", "shutdownConfirm": "This stops the ClaudeDo worker (any running task or interactive session is interrupted) and closes the app. Continue?" }, "shell": { "unexpectedError": "Unexpected error: {0}", "updatingClaudeCli": "Updating Claude CLI…", "shutdownConfirm": "This stops the ClaudeDo worker (any running task or interactive session is interrupted) and closes the app. Continue?" },
"mainWindow": { "closeConfirm": { "message": "{0} Mission Control session(s) are still running. Closing the app will end them. Close anyway?" } }, "mainWindow": { "closeConfirm": { "message": "{0} Mission Control session(s) are still running. Closing the app will end them. Close anyway?" } },
"agentStatus": { "idle": "Idle", "queued": "Queued", "running": "Running", "review": "Review", "children": "Waiting for Subtasks", "done": "Done", "failed": "Failed", "cancelled": "Cancelled" }, "agentStatus": { "idle": "Idle", "queued": "Queued", "running": "Running", "review": "Review", "children": "Waiting for Subtasks", "done": "Done", "failed": "Failed", "cancelled": "Cancelled" },
"taskStatus": { "idle": "Idle", "queued": "Queued", "running": "Running", "waitingForReview": "Waiting for Review", "waitingForChildren": "Waiting for Subtasks", "done": "Done", "failed": "Failed", "cancelled": "Cancelled", "parked": "Parked", "interactive": "Interactive", "waiting": "Waiting" }, "taskStatus": { "idle": "Idle", "queued": "Queued", "running": "Running", "waitingForReview": "Waiting for Review", "waitingForChildren": "Waiting for Subtasks", "done": "Done", "failed": "Failed", "cancelled": "Cancelled", "parked": "Parked", "interactive": "Interactive", "waiting": "Waiting" },
@@ -206,6 +206,9 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
// paint a good outcome red, which reads as a failure. // paint a good outcome red, which reads as a failure.
public void FlashFooterSuccess(string message) => FlashFooter(message, WorkerLogLevel.Success); public void FlashFooterSuccess(string message) => FlashFooter(message, WorkerLogLevel.Success);
// Same strip, for neutral in-progress text (e.g. a CLI's progress lines).
public void FlashFooterInfo(string message) => FlashFooter(message, WorkerLogLevel.Info);
private void FlashFooter(string message, WorkerLogLevel level) private void FlashFooter(string message, WorkerLogLevel level)
{ {
if (level == WorkerLogLevel.Error) if (level == WorkerLogLevel.Error)
@@ -749,8 +752,10 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
{ {
_updatingClaudeCli = true; _updatingClaudeCli = true;
UpdateClaudeCliCommand.NotifyCanExecuteChanged(); UpdateClaudeCliCommand.NotifyCanExecuteChanged();
var finished = false;
try try
{ {
FlashFooterInfo(Loc.T("vm.shell.updatingClaudeCli"));
var dto = Worker is null ? null : await Worker.GetClaudeBinAsync(); var dto = Worker is null ? null : await Worker.GetClaudeBinAsync();
if (dto is null) if (dto is null)
{ {
@@ -758,18 +763,24 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
return; return;
} }
var (success, message) = await RunClaudeUpdateAsync(dto); // `finished` is only touched on the UI thread: a queued line can't overwrite the final flash.
var (success, message) = await RunClaudeUpdateAsync(dto, line => Dispatcher.UIThread.Post(() =>
{
if (!finished) FlashFooterInfo(line);
}));
finished = true;
if (success) FlashFooterSuccess(message); if (success) FlashFooterSuccess(message);
else FlashFooterError(message); else FlashFooterError(message);
} }
finally finally
{ {
finished = true;
_updatingClaudeCli = false; _updatingClaudeCli = false;
UpdateClaudeCliCommand.NotifyCanExecuteChanged(); UpdateClaudeCliCommand.NotifyCanExecuteChanged();
} }
} }
private static async Task<(bool Success, string Message)> RunClaudeUpdateAsync(ClaudeBinDto dto) private static async Task<(bool Success, string Message)> RunClaudeUpdateAsync(ClaudeBinDto dto, Action<string> onStdoutLine)
{ {
var resolved = ClaudeDo.Data.Environment.ExecutableResolver.Resolve(dto.Value); var resolved = ClaudeDo.Data.Environment.ExecutableResolver.Resolve(dto.Value);
if (resolved is null) return (false, $"Update failed: '{dto.Value}' not found."); if (resolved is null) return (false, $"Update failed: '{dto.Value}' not found.");
@@ -785,7 +796,12 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
using var process = new System.Diagnostics.Process { StartInfo = psi }; using var process = new System.Diagnostics.Process { StartInfo = psi };
var stdout = new System.Text.StringBuilder(); var stdout = new System.Text.StringBuilder();
var stderr = new System.Text.StringBuilder(); var stderr = new System.Text.StringBuilder();
process.OutputDataReceived += (_, e) => { if (e.Data is not null) stdout.AppendLine(e.Data); }; process.OutputDataReceived += (_, e) =>
{
if (e.Data is null) return;
stdout.AppendLine(e.Data);
if (!string.IsNullOrWhiteSpace(e.Data)) onStdoutLine(e.Data.Trim());
};
process.ErrorDataReceived += (_, e) => { if (e.Data is not null) stderr.AppendLine(e.Data); }; process.ErrorDataReceived += (_, e) => { if (e.Data is not null) stderr.AppendLine(e.Data); };
try { process.Start(); } try { process.Start(); }