73 lines
3.3 KiB
C#
73 lines
3.3 KiB
C#
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<int> 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<StartListHandlerResult> 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<string>? 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<TaskEntity> chosen;
|
|
if (taskIds is { Count: > 0 })
|
|
{
|
|
var resolved = await TaskIdResolver.ResolveManyAsync(_tasks, taskIds, cancellationToken);
|
|
chosen = new List<TaskEntity>();
|
|
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());
|
|
}
|
|
}
|