diff --git a/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs index 54134ed..53ca6ce 100644 --- a/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs @@ -1,9 +1,135 @@ +using System.Collections.ObjectModel; using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using ClaudeDo.Data; +using ClaudeDo.Data.Repositories; +using ClaudeDo.Ui.Services; +using Microsoft.EntityFrameworkCore; namespace ClaudeDo.Ui.ViewModels.Islands; public sealed partial class DetailsIslandViewModel : ViewModelBase { + private readonly IDbContextFactory _dbFactory; + private readonly WorkerClient _worker; + + // Current task row (set by IslandsShellViewModel via Bind) [ObservableProperty] private TaskRowViewModel? _task; - public void Bind(TaskRowViewModel? task) => Task = task; + + // Editable fields + [ObservableProperty] private string _editableTitle = ""; + [ObservableProperty] private string _notes = ""; + [ObservableProperty] private string _promptInput = ""; + + // Agent strip fields + [ObservableProperty] private string _agentStatusLabel = "Idle"; + [ObservableProperty] private string? _model; + [ObservableProperty] private string? _worktreePath; + [ObservableProperty] private string? _branchLine; + [ObservableProperty] private int _turns; + [ObservableProperty] private int _tokens; + + public ObservableCollection Log { get; } = new(); + public ObservableCollection Subtasks { get; } = new(); + + // The task ID we are currently subscribed to for live log messages + private string? _subscribedTaskId; + + public DetailsIslandViewModel(IDbContextFactory dbFactory, WorkerClient worker) + { + _dbFactory = dbFactory; + _worker = worker; + + // Subscribe once; filter by current task id inside the handler + _worker.TaskMessageEvent += OnTaskMessage; + } + + private void OnTaskMessage(string taskId, string line) + { + if (taskId != _subscribedTaskId) return; + // Parse a simple prefix convention: "[sys]", "[tool]", "[claude]", etc. + // Fall back to Msg for unrecognised lines. + var kind = line.StartsWith("[sys]", StringComparison.OrdinalIgnoreCase) ? LogKind.Sys + : line.StartsWith("[tool]", StringComparison.OrdinalIgnoreCase) ? LogKind.Tool + : line.StartsWith("[claude]", StringComparison.OrdinalIgnoreCase) ? LogKind.Claude + : line.StartsWith("[stdout]", StringComparison.OrdinalIgnoreCase) ? LogKind.Stdout + : line.StartsWith("[stderr]", StringComparison.OrdinalIgnoreCase) ? LogKind.Stderr + : line.StartsWith("[done]", StringComparison.OrdinalIgnoreCase) ? LogKind.Done + : LogKind.Msg; + Log.Add(new LogLineViewModel { Kind = kind, Text = line }); + } + + public async void Bind(TaskRowViewModel? row) + { + Task = row; + Log.Clear(); + Subtasks.Clear(); + + if (row == null) + { + _subscribedTaskId = null; + EditableTitle = ""; + Notes = ""; + Model = null; + WorktreePath = null; + BranchLine = null; + AgentStatusLabel = "Idle"; + return; + } + + // Wire live-log subscription to new task + _subscribedTaskId = row.Id; + + await using var ctx = _dbFactory.CreateDbContext(); + var taskRepo = new TaskRepository(ctx); + var subtaskRepo = new SubtaskRepository(ctx); + + var entity = await taskRepo.GetByIdAsync(row.Id); + if (entity == null) return; + + EditableTitle = entity.Title; + Notes = entity.Notes ?? ""; + Model = entity.Model; + WorktreePath = entity.Worktree?.Path; + BranchLine = entity.Worktree is { } w ? $"{w.BranchName} \u2190 main" : null; + AgentStatusLabel = entity.Status.ToString(); + + var subs = await subtaskRepo.GetByTaskIdAsync(row.Id); + foreach (var s in subs) + Subtasks.Add(new SubtaskRowViewModel { Id = s.Id, Title = s.Title, Done = s.Completed }); + } + + [RelayCommand] + private async System.Threading.Tasks.Task SendPromptAsync() + { + if (string.IsNullOrWhiteSpace(PromptInput) || Task == null) return; + Log.Add(new LogLineViewModel { Kind = LogKind.Msg, Text = $"[you] {PromptInput}" }); + // TODO: WorkerClient has no SendPromptAsync — no matching hub method found. + // When the worker gains a "SendPrompt" hub method, call: + // await _worker.SendPromptAsync(Task.Id, PromptInput); + PromptInput = ""; + await System.Threading.Tasks.Task.CompletedTask; + } + + [RelayCommand] + private async System.Threading.Tasks.Task ApproveMergeAsync() + { + if (Task == null) return; + // TODO: call worker merge hub method when available + await System.Threading.Tasks.Task.CompletedTask; + } + + [RelayCommand] + private async System.Threading.Tasks.Task StopAsync() + { + if (Task == null) return; + await _worker.CancelTaskAsync(Task.Id); + } +} + +public sealed partial class SubtaskRowViewModel : ViewModelBase +{ + public required string Id { get; init; } + [ObservableProperty] private string _title = ""; + [ObservableProperty] private bool _done; } diff --git a/src/ClaudeDo.Ui/ViewModels/Islands/LogLineViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Islands/LogLineViewModel.cs new file mode 100644 index 0000000..052a6a0 --- /dev/null +++ b/src/ClaudeDo.Ui/ViewModels/Islands/LogLineViewModel.cs @@ -0,0 +1,20 @@ +namespace ClaudeDo.Ui.ViewModels.Islands; + +public enum LogKind { Sys, Tool, Claude, Stdout, Stderr, Done, Msg } + +public sealed class LogLineViewModel +{ + public required LogKind Kind { get; init; } + public required string Text { get; init; } + public string ClassName => Kind switch + { + LogKind.Sys => "log-sys", + LogKind.Tool => "log-tool", + LogKind.Claude => "log-claude", + LogKind.Stdout => "log-stdout", + LogKind.Stderr => "log-stderr", + LogKind.Done => "log-done", + LogKind.Msg => "log-msg", + _ => "", + }; +}