using System.ComponentModel; using ClaudeDo.Data.Models; using ClaudeDo.Data.Repositories; using ClaudeDo.Worker.Hub; using ModelContextProtocol.Server; using TaskStatus = ClaudeDo.Data.Models.TaskStatus; namespace ClaudeDo.Worker.External; public sealed record StartListHandlerResult(bool Requested, string ListId, string ListName, int TaskCount, IReadOnlyList TaskNumbers); // The "Let Claude handle it" button, reachable from outside the UI. The worker only broadcasts; // the App owns the ConPTY tile and creates the handler task exactly as the button does // (MissionControlViewModel.OpenMergeHelperConPtySessionAsync), so there is one code path for // starting a run, not two. Without a connected App nothing happens -- the result says so. [McpServerToolType] public sealed class ListHandlerMcpTools { private readonly ListRepository _lists; private readonly TaskRepository _tasks; private readonly HubBroadcaster _broadcaster; public ListHandlerMcpTools(ListRepository lists, TaskRepository tasks, HubBroadcaster broadcaster) { _lists = lists; _tasks = tasks; _broadcaster = broadcaster; } [McpServerTool, Description( "Start a list-handler run (\"Let Claude handle it\") for a list, exactly like the button in " + "Mission Control: the connected ClaudeDo App creates the handler task and opens the triage " + "ConPTY tile for the given tasks. taskIds defaults to every Idle, non-manual task of the list. " + "Requires the App to be running and connected to this worker -- the worker only broadcasts " + "the request. taskIds accept task numbers (#123 or bare 123).")] public async Task StartListHandler( [Description("The list to run the handler on.")] string listId, [Description("Tasks to hand to the run, in the order to triage them. Omit for all Idle, non-manual tasks of the list.")] IReadOnlyList? taskIds = null, CancellationToken cancellationToken = default) { var list = await _lists.GetByIdAsync(listId, cancellationToken) ?? throw new InvalidOperationException($"List {listId} not found."); var listTasks = await _tasks.GetByListIdAsync(listId, cancellationToken); List chosen; if (taskIds is { Count: > 0 }) { var resolved = await TaskIdResolver.ResolveManyAsync(_tasks, taskIds, cancellationToken); chosen = new List(); foreach (var id in resolved) { var t = listTasks.FirstOrDefault(x => x.Id == id) ?? throw new InvalidOperationException($"Task {id} not found in list {list.Name}."); chosen.Add(t); } } else { chosen = listTasks .Where(t => t.Status == TaskStatus.Idle && !t.IsManual && t.ParentTaskId is null) .OrderBy(t => t.SortOrder) .ToList(); } if (chosen.Count == 0) throw new InvalidOperationException($"No tasks to hand to the list handler for {list.Name}."); var ids = chosen.Select(t => t.Id).ToList(); await _broadcaster.ListHandlerStartRequested(listId, ids); return new StartListHandlerResult(true, listId, list.Name, ids.Count, chosen.Select(t => t.Number).ToList()); } }