feat(ui): triage model picker (opus/fable) in the merge helper start modal
This commit is contained in:
@@ -497,6 +497,8 @@
|
|||||||
"windowTitle": "Claude machen lassen",
|
"windowTitle": "Claude machen lassen",
|
||||||
"scopeList": "Liste: {0}",
|
"scopeList": "Liste: {0}",
|
||||||
"scopeReview": "Alle Listen — Tasks im Review",
|
"scopeReview": "Alle Listen — Tasks im Review",
|
||||||
|
"modelLabel": "Modell",
|
||||||
|
"modelTip": "Modell für die Triage-Session (Dedupe & Verfeinern). Spätere Merge-Phasen laufen immer auf Opus.",
|
||||||
"confirm": "Claude machen lassen",
|
"confirm": "Claude machen lassen",
|
||||||
"cancel": "Abbrechen",
|
"cancel": "Abbrechen",
|
||||||
"selectAll": "Alle auswählen",
|
"selectAll": "Alle auswählen",
|
||||||
|
|||||||
@@ -497,6 +497,8 @@
|
|||||||
"windowTitle": "Let Claude handle it",
|
"windowTitle": "Let Claude handle it",
|
||||||
"scopeList": "List: {0}",
|
"scopeList": "List: {0}",
|
||||||
"scopeReview": "All lists — tasks waiting for review",
|
"scopeReview": "All lists — tasks waiting for review",
|
||||||
|
"modelLabel": "Model",
|
||||||
|
"modelTip": "Model for the triage session (dedupe & refine). Later merge phases always run on opus.",
|
||||||
"confirm": "Let Claude handle it",
|
"confirm": "Let Claude handle it",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"selectAll": "Select all",
|
"selectAll": "Select all",
|
||||||
|
|||||||
@@ -143,8 +143,9 @@ public interface IWorkerClient : INotifyPropertyChanged
|
|||||||
/// <summary>Launch spec for an embedded ConPTY "merge helper" session that drives the given
|
/// <summary>Launch spec for an embedded ConPTY "merge helper" session that drives the given
|
||||||
/// tasks to a merged/Done state. listId scopes the session (and cwd) to that list.
|
/// tasks to a merged/Done state. listId scopes the session (and cwd) to that list.
|
||||||
/// handlerTaskId (from CreateMergeHelperTaskAsync) is rendered into the brief so the session
|
/// handlerTaskId (from CreateMergeHelperTaskAsync) is rendered into the brief so the session
|
||||||
/// can call handoff_list_handler on its own handler task.</summary>
|
/// can call handoff_list_handler on its own handler task. model overrides the triage
|
||||||
Task<LaunchSpec> GetMergeHelperLaunchSpecAsync(IReadOnlyList<string> taskIds, string listId, string handlerTaskId, CancellationToken ct = default);
|
/// session's model (null = default).</summary>
|
||||||
|
Task<LaunchSpec> GetMergeHelperLaunchSpecAsync(IReadOnlyList<string> taskIds, string listId, string handlerTaskId, string? model = null, CancellationToken ct = default);
|
||||||
/// <summary>Creates the ClaudeDo task that owns a list-handler run (one per run, Idle/IsManual,
|
/// <summary>Creates the ClaudeDo task that owns a list-handler run (one per run, Idle/IsManual,
|
||||||
/// never queued) so the ConPTY tile can be task-based instead of ad-hoc. Returns the new task id.</summary>
|
/// never queued) so the ConPTY tile can be task-based instead of ad-hoc. Returns the new task id.</summary>
|
||||||
Task<string> CreateMergeHelperTaskAsync(
|
Task<string> CreateMergeHelperTaskAsync(
|
||||||
|
|||||||
@@ -627,8 +627,8 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
|||||||
public async Task<LaunchSpec> GetAdHocLaunchSpecAsync(string directory, CancellationToken ct = default)
|
public async Task<LaunchSpec> GetAdHocLaunchSpecAsync(string directory, CancellationToken ct = default)
|
||||||
=> await InvokeTimedAsync<LaunchSpec>("GetAdHocLaunchSpec", () => _hub.InvokeAsync<LaunchSpec>("GetAdHocLaunchSpec", directory, ct));
|
=> await InvokeTimedAsync<LaunchSpec>("GetAdHocLaunchSpec", () => _hub.InvokeAsync<LaunchSpec>("GetAdHocLaunchSpec", directory, ct));
|
||||||
|
|
||||||
public async Task<LaunchSpec> GetMergeHelperLaunchSpecAsync(IReadOnlyList<string> taskIds, string listId, string handlerTaskId, CancellationToken ct = default)
|
public async Task<LaunchSpec> GetMergeHelperLaunchSpecAsync(IReadOnlyList<string> taskIds, string listId, string handlerTaskId, string? model = null, CancellationToken ct = default)
|
||||||
=> await InvokeTimedAsync<LaunchSpec>("GetMergeHelperLaunchSpec", () => _hub.InvokeAsync<LaunchSpec>("GetMergeHelperLaunchSpec", taskIds, listId, handlerTaskId, ct));
|
=> await InvokeTimedAsync<LaunchSpec>("GetMergeHelperLaunchSpec", () => _hub.InvokeAsync<LaunchSpec>("GetMergeHelperLaunchSpec", taskIds, listId, handlerTaskId, model, ct));
|
||||||
|
|
||||||
public async Task<string> CreateMergeHelperTaskAsync(
|
public async Task<string> CreateMergeHelperTaskAsync(
|
||||||
IReadOnlyList<string> taskIds, string listId, string title, string descriptionHeader, CancellationToken ct = default)
|
IReadOnlyList<string> taskIds, string listId, string title, string descriptionHeader, CancellationToken ct = default)
|
||||||
|
|||||||
@@ -17,8 +17,9 @@ namespace ClaudeDo.Ui.ViewModels.Islands;
|
|||||||
|
|
||||||
public enum ListKind { Smart, Virtual, User }
|
public enum ListKind { Smart, Virtual, User }
|
||||||
|
|
||||||
/// <summary>Confirmed handler run: the scope list and the ordered selected task ids.</summary>
|
/// <summary>Confirmed handler run: the scope list, the ordered selected task ids, and the model
|
||||||
public sealed record MergeHelperRequest(string ListId, IReadOnlyList<string> TaskIds);
|
/// picked for the triage session (null = default; review-mode runs skip triage entirely).</summary>
|
||||||
|
public sealed record MergeHelperRequest(string ListId, IReadOnlyList<string> TaskIds, string? TriageModel = null);
|
||||||
|
|
||||||
public sealed partial class ListsIslandViewModel : ViewModelBase, IDisposable
|
public sealed partial class ListsIslandViewModel : ViewModelBase, IDisposable
|
||||||
{
|
{
|
||||||
@@ -114,7 +115,7 @@ public sealed partial class ListsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
await vm.LoadAsync();
|
await vm.LoadAsync();
|
||||||
var ids = await Dialogs.ShowMergeHelperSelectionAsync(vm);
|
var ids = await Dialogs.ShowMergeHelperSelectionAsync(vm);
|
||||||
if (ids is { Count: > 0 })
|
if (ids is { Count: > 0 })
|
||||||
LetClaudeHandleRequested?.Invoke(new MergeHelperRequest(rawId, ids));
|
LetClaudeHandleRequested?.Invoke(new MergeHelperRequest(rawId, ids, vm.SelectedModel));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Raised once per list after the review-scoped selection dialog is confirmed;
|
/// <summary>Raised once per list after the review-scoped selection dialog is confirmed;
|
||||||
|
|||||||
@@ -370,7 +370,7 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
|
|||||||
Lists.LetClaudeHandleRequested += req =>
|
Lists.LetClaudeHandleRequested += req =>
|
||||||
{
|
{
|
||||||
OpenMissionControl();
|
OpenMissionControl();
|
||||||
_ = MissionControl.OpenMergeHelperConPtySessionAsync(req.ListId, req.TaskIds);
|
_ = MissionControl.OpenMergeHelperConPtySessionAsync(req.ListId, req.TaskIds, req.TriageModel);
|
||||||
};
|
};
|
||||||
Lists.MergeReviewsRequested += req =>
|
Lists.MergeReviewsRequested += req =>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -331,10 +331,10 @@ public sealed partial class MissionControlViewModel : ViewModelBase, IDisposable
|
|||||||
// creates one new ClaudeDo task per run to own the session (title/diff/result), deduped by
|
// creates one new ClaudeDo task per run to own the session (title/diff/result), deduped by
|
||||||
// TaskId like OpenConPtySessionAsync. The handler still merges the tasks it handles itself
|
// TaskId like OpenConPtySessionAsync. The handler still merges the tasks it handles itself
|
||||||
// (no worktree of its own) — see TaskEntity.HandlerBaseCommit/HandlerHeadCommit.
|
// (no worktree of its own) — see TaskEntity.HandlerBaseCommit/HandlerHeadCommit.
|
||||||
public System.Threading.Tasks.Task OpenMergeHelperConPtySessionAsync(string listId, IReadOnlyList<string> taskIds)
|
public System.Threading.Tasks.Task OpenMergeHelperConPtySessionAsync(string listId, IReadOnlyList<string> taskIds, string? triageModel = null)
|
||||||
=> OpenListHandlerConPtySessionAsync(listId, taskIds,
|
=> OpenListHandlerConPtySessionAsync(listId, taskIds,
|
||||||
"missionControl.mergeHelperTitle", "missionControl.mergeHelperTaskTitle",
|
"missionControl.mergeHelperTitle", "missionControl.mergeHelperTaskTitle",
|
||||||
taskId => _worker.GetMergeHelperLaunchSpecAsync(taskIds, listId, taskId));
|
taskId => _worker.GetMergeHelperLaunchSpecAsync(taskIds, listId, taskId, triageModel));
|
||||||
|
|
||||||
// Merge-only list-handler session over a list's WaitingForReview tasks (the review list's
|
// Merge-only list-handler session over a list's WaitingForReview tasks (the review list's
|
||||||
// "Let Claude merge the reviews"). Same host-task shape as the full run, but the session
|
// "Let Claude merge the reviews"). Same host-task shape as the full run, but the session
|
||||||
|
|||||||
@@ -67,6 +67,15 @@ public sealed partial class MergeHelperSelectionModalViewModel : ViewModelBase
|
|||||||
|
|
||||||
public ObservableCollection<MergeHelperTaskRowViewModel> Tasks { get; } = new();
|
public ObservableCollection<MergeHelperTaskRowViewModel> Tasks { get; } = new();
|
||||||
|
|
||||||
|
/// <summary>Model choices for the triage session. Only opus/fable are offered on purpose —
|
||||||
|
/// triage is the dedupe/refine leverage point; cheaper models aren't worth a picker slot.</summary>
|
||||||
|
public IReadOnlyList<string> ModelOptions { get; } = new[] { "opus", "fable" };
|
||||||
|
|
||||||
|
[ObservableProperty] private string _selectedModel = Data.Models.ModelRegistry.HandlerTriageAlias;
|
||||||
|
|
||||||
|
/// <summary>Hidden in review mode — those runs start directly at the merge phase, no triage.</summary>
|
||||||
|
public bool ShowModelPicker => !_reviewMode;
|
||||||
|
|
||||||
[ObservableProperty] private string _scopeLabel = "";
|
[ObservableProperty] private string _scopeLabel = "";
|
||||||
|
|
||||||
public bool HasTasks => Tasks.Count > 0;
|
public bool HasTasks => Tasks.Count > 0;
|
||||||
@@ -100,6 +109,7 @@ public sealed partial class MergeHelperSelectionModalViewModel : ViewModelBase
|
|||||||
{
|
{
|
||||||
_reviewMode = true;
|
_reviewMode = true;
|
||||||
ScopeLabel = Loc.T("modals.mergeHelper.scopeReview");
|
ScopeLabel = Loc.T("modals.mergeHelper.scopeReview");
|
||||||
|
OnPropertyChanged(nameof(ShowModelPicker));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Groups a confirmed review-mode selection by owning list, preserving row order —
|
/// <summary>Groups a confirmed review-mode selection by owning list, preserving row order —
|
||||||
|
|||||||
@@ -29,11 +29,18 @@
|
|||||||
|
|
||||||
<!-- Body: scope + select-all/none toolbar, headers, checkbox rows -->
|
<!-- Body: scope + select-all/none toolbar, headers, checkbox rows -->
|
||||||
<DockPanel>
|
<DockPanel>
|
||||||
<Grid DockPanel.Dock="Top" ColumnDefinitions="*,Auto,Auto" Margin="20,12,20,6">
|
<Grid DockPanel.Dock="Top" ColumnDefinitions="*,Auto,Auto,Auto" Margin="20,12,20,6">
|
||||||
<TextBlock Grid.Column="0" Classes="meta" Text="{Binding ScopeLabel}" VerticalAlignment="Center"/>
|
<TextBlock Grid.Column="0" Classes="meta" Text="{Binding ScopeLabel}" VerticalAlignment="Center"/>
|
||||||
<Button Grid.Column="1" Classes="btn" Content="{loc:Tr modals.mergeHelper.selectAll}"
|
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="6" Margin="0,0,12,0"
|
||||||
|
IsVisible="{Binding ShowModelPicker}"
|
||||||
|
ToolTip.Tip="{loc:Tr modals.mergeHelper.modelTip}">
|
||||||
|
<TextBlock Classes="meta" Text="{loc:Tr modals.mergeHelper.modelLabel}" VerticalAlignment="Center"/>
|
||||||
|
<ComboBox MinWidth="90" ItemsSource="{Binding ModelOptions}"
|
||||||
|
SelectedItem="{Binding SelectedModel, Mode=TwoWay}"/>
|
||||||
|
</StackPanel>
|
||||||
|
<Button Grid.Column="2" Classes="btn" Content="{loc:Tr modals.mergeHelper.selectAll}"
|
||||||
Command="{Binding SelectAllCommand}" IsEnabled="{Binding HasTasks}"/>
|
Command="{Binding SelectAllCommand}" IsEnabled="{Binding HasTasks}"/>
|
||||||
<Button Grid.Column="2" Classes="btn" Content="{loc:Tr modals.mergeHelper.selectNone}"
|
<Button Grid.Column="3" Classes="btn" Content="{loc:Tr modals.mergeHelper.selectNone}"
|
||||||
Command="{Binding SelectNoneCommand}" IsEnabled="{Binding HasTasks}" Margin="8,0,0,0"/>
|
Command="{Binding SelectNoneCommand}" IsEnabled="{Binding HasTasks}" Margin="8,0,0,0"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
|||||||
@@ -770,12 +770,13 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
|||||||
// Builds the launch spec for an embedded ConPTY "merge helper" session that drives the given
|
// Builds the launch spec for an embedded ConPTY "merge helper" session that drives the given
|
||||||
// tasks to a merged/Done state via the mcp__claudedo__* tools. listId scopes the brief label
|
// tasks to a merged/Done state via the mcp__claudedo__* tools. listId scopes the brief label
|
||||||
// and cwd to that list. handlerTaskId is the id returned by CreateMergeHelperTask -- it is
|
// and cwd to that list. handlerTaskId is the id returned by CreateMergeHelperTask -- it is
|
||||||
// rendered into the brief so the session can call handoff_list_handler on itself.
|
// rendered into the brief so the session can call handoff_list_handler on itself. model
|
||||||
public Task<LaunchSpec> GetMergeHelperLaunchSpec(string[] taskIds, string listId, string handlerTaskId) => HubGuard(() =>
|
// overrides the triage session's model (null = default opus).
|
||||||
|
public Task<LaunchSpec> GetMergeHelperLaunchSpec(string[] taskIds, string listId, string handlerTaskId, string? model) => HubGuard(() =>
|
||||||
{
|
{
|
||||||
if (_interactiveLaunchSpec is null)
|
if (_interactiveLaunchSpec is null)
|
||||||
throw new InvalidOperationException("Interactive launch spec service is not configured.");
|
throw new InvalidOperationException("Interactive launch spec service is not configured.");
|
||||||
return _interactiveLaunchSpec.BuildForMergeHelperAsync(taskIds, listId, handlerTaskId, Context.ConnectionAborted);
|
return _interactiveLaunchSpec.BuildForMergeHelperAsync(taskIds, listId, handlerTaskId, model, Context.ConnectionAborted);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Creates the ClaudeDo task that owns a list-handler run, before the ConPTY tile opens --
|
// Creates the ClaudeDo task that owns a list-handler run, before the ConPTY tile opens --
|
||||||
|
|||||||
@@ -290,8 +290,10 @@ public sealed class InteractiveLaunchSpecService
|
|||||||
/// handlerTaskId (the id returned by CreateMergeHelperTaskAsync) is rendered into the brief so
|
/// handlerTaskId (the id returned by CreateMergeHelperTaskAsync) is rendered into the brief so
|
||||||
/// the session can call handoff_list_handler/submit_task_for_review on its own handler task.
|
/// the session can call handoff_list_handler/submit_task_for_review on its own handler task.
|
||||||
/// Throws KeyNotFoundException if the list doesn't exist; InvalidOperationException if
|
/// Throws KeyNotFoundException if the list doesn't exist; InvalidOperationException if
|
||||||
/// taskIds is empty or the list has no existing working directory.</summary>
|
/// taskIds is empty or the list has no existing working directory.
|
||||||
public async Task<LaunchSpec> BuildForMergeHelperAsync(IReadOnlyList<string> taskIds, string listId, string handlerTaskId, CancellationToken ct)
|
/// model overrides the triage session's model (start-modal dropdown); null/unknown falls
|
||||||
|
/// back to <see cref="ModelRegistry.HandlerTriageAlias"/>. Handoff phases are unaffected.</summary>
|
||||||
|
public async Task<LaunchSpec> BuildForMergeHelperAsync(IReadOnlyList<string> taskIds, string listId, string handlerTaskId, string? model, CancellationToken ct)
|
||||||
{
|
{
|
||||||
if (taskIds.Count == 0)
|
if (taskIds.Count == 0)
|
||||||
throw new InvalidOperationException("No tasks selected for the list handler.");
|
throw new InvalidOperationException("No tasks selected for the list handler.");
|
||||||
@@ -344,12 +346,13 @@ public sealed class InteractiveLaunchSpecService
|
|||||||
// longer influences a handler session at all, it would otherwise apply the LIST's effort
|
// longer influences a handler session at all, it would otherwise apply the LIST's effort
|
||||||
// preset to a completely different model.
|
// preset to a completely different model.
|
||||||
var settings = await new AppSettingsRepository(ctx).GetAsync(ct);
|
var settings = await new AppSettingsRepository(ctx).GetAsync(ct);
|
||||||
|
var triageModel = ModelRegistry.TryNormalizeAlias(model) ?? ModelRegistry.HandlerTriageAlias;
|
||||||
|
|
||||||
var args = new List<string>
|
var args = new List<string>
|
||||||
{
|
{
|
||||||
"--model", ModelRegistry.HandlerTriageAlias,
|
"--model", triageModel,
|
||||||
"--effort", EffortFor(settings, ModelRegistry.HandlerTriageAlias),
|
"--effort", EffortFor(settings, triageModel),
|
||||||
"--permission-mode", PermissionModeResolver.Resolve(ModelRegistry.HandlerTriageAlias, "auto"),
|
"--permission-mode", PermissionModeResolver.Resolve(triageModel, "auto"),
|
||||||
"--allowedTools", MergeHelperAllowedTools,
|
"--allowedTools", MergeHelperAllowedTools,
|
||||||
"--add-dir", sessionDir, repoDir,
|
"--add-dir", sessionDir, repoDir,
|
||||||
"--append-system-prompt-file", systemPromptPath,
|
"--append-system-prompt-file", systemPromptPath,
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ public abstract class StubWorkerClient : IWorkerClient
|
|||||||
=> Task.FromResult(new LaunchSpec(".", "claude", Array.Empty<string>(), new Dictionary<string, string>()));
|
=> Task.FromResult(new LaunchSpec(".", "claude", Array.Empty<string>(), new Dictionary<string, string>()));
|
||||||
public virtual Task<LaunchSpec> GetAdHocLaunchSpecAsync(string directory, CancellationToken ct = default)
|
public virtual Task<LaunchSpec> GetAdHocLaunchSpecAsync(string directory, CancellationToken ct = default)
|
||||||
=> Task.FromResult(new LaunchSpec(directory, "claude", Array.Empty<string>(), new Dictionary<string, string>()));
|
=> Task.FromResult(new LaunchSpec(directory, "claude", Array.Empty<string>(), new Dictionary<string, string>()));
|
||||||
public virtual Task<LaunchSpec> GetMergeHelperLaunchSpecAsync(IReadOnlyList<string> taskIds, string listId, string handlerTaskId, CancellationToken ct = default)
|
public virtual Task<LaunchSpec> GetMergeHelperLaunchSpecAsync(IReadOnlyList<string> taskIds, string listId, string handlerTaskId, string? model = null, CancellationToken ct = default)
|
||||||
=> Task.FromResult(new LaunchSpec(".", "claude", Array.Empty<string>(), new Dictionary<string, string>()));
|
=> Task.FromResult(new LaunchSpec(".", "claude", Array.Empty<string>(), new Dictionary<string, string>()));
|
||||||
public virtual Task<string> CreateMergeHelperTaskAsync(
|
public virtual Task<string> CreateMergeHelperTaskAsync(
|
||||||
IReadOnlyList<string> taskIds, string listId, string title, string descriptionHeader, CancellationToken ct = default)
|
IReadOnlyList<string> taskIds, string listId, string title, string descriptionHeader, CancellationToken ct = default)
|
||||||
|
|||||||
@@ -556,7 +556,7 @@ public class MissionControlViewModelTests : IDisposable
|
|||||||
|
|
||||||
private sealed class ThrowingMergeHelperLaunchSpecWorker : StubWorkerClient
|
private sealed class ThrowingMergeHelperLaunchSpecWorker : StubWorkerClient
|
||||||
{
|
{
|
||||||
public override Task<LaunchSpec> GetMergeHelperLaunchSpecAsync(IReadOnlyList<string> taskIds, string listId, string handlerTaskId, CancellationToken ct = default)
|
public override Task<LaunchSpec> GetMergeHelperLaunchSpecAsync(IReadOnlyList<string> taskIds, string listId, string handlerTaskId, string? model = null, CancellationToken ct = default)
|
||||||
=> throw new InvalidOperationException("spec failed");
|
=> throw new InvalidOperationException("spec failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -721,7 +721,7 @@ public sealed class InteractiveLaunchSpecServiceTests : IDisposable
|
|||||||
{
|
{
|
||||||
var svc = BuildService(now: now);
|
var svc = BuildService(now: now);
|
||||||
var spec = await svc.BuildForMergeHelperAsync(
|
var spec = await svc.BuildForMergeHelperAsync(
|
||||||
new[] { taskId }, listId, Guid.NewGuid().ToString(), CancellationToken.None);
|
new[] { taskId }, listId, Guid.NewGuid().ToString(), null, CancellationToken.None);
|
||||||
var sessionDir = TrackSessionDir(spec);
|
var sessionDir = TrackSessionDir(spec);
|
||||||
return await File.ReadAllTextAsync(Path.Combine(sessionDir, "system-prompt.md"));
|
return await File.ReadAllTextAsync(Path.Combine(sessionDir, "system-prompt.md"));
|
||||||
}
|
}
|
||||||
@@ -851,7 +851,7 @@ public sealed class InteractiveLaunchSpecServiceTests : IDisposable
|
|||||||
var listId = await SeedListAsync(workingDir: _tempDir);
|
var listId = await SeedListAsync(workingDir: _tempDir);
|
||||||
var svc = BuildService();
|
var svc = BuildService();
|
||||||
await Assert.ThrowsAsync<InvalidOperationException>(
|
await Assert.ThrowsAsync<InvalidOperationException>(
|
||||||
() => svc.BuildForMergeHelperAsync(Array.Empty<string>(), listId, Guid.NewGuid().ToString(), CancellationToken.None));
|
() => svc.BuildForMergeHelperAsync(Array.Empty<string>(), listId, Guid.NewGuid().ToString(), null, CancellationToken.None));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -863,7 +863,7 @@ public sealed class InteractiveLaunchSpecServiceTests : IDisposable
|
|||||||
|
|
||||||
var svc = BuildService();
|
var svc = BuildService();
|
||||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
|
var ex = await Assert.ThrowsAsync<InvalidOperationException>(
|
||||||
() => svc.BuildForMergeHelperAsync(new[] { taskId }, listId, Guid.NewGuid().ToString(), CancellationToken.None));
|
() => svc.BuildForMergeHelperAsync(new[] { taskId }, listId, Guid.NewGuid().ToString(), null, CancellationToken.None));
|
||||||
Assert.Contains("working directory", ex.Message);
|
Assert.Contains("working directory", ex.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -876,7 +876,7 @@ public sealed class InteractiveLaunchSpecServiceTests : IDisposable
|
|||||||
|
|
||||||
var svc = BuildService();
|
var svc = BuildService();
|
||||||
await Assert.ThrowsAsync<KeyNotFoundException>(
|
await Assert.ThrowsAsync<KeyNotFoundException>(
|
||||||
() => svc.BuildForMergeHelperAsync(new[] { taskId }, "no-such-list", Guid.NewGuid().ToString(), CancellationToken.None));
|
() => svc.BuildForMergeHelperAsync(new[] { taskId }, "no-such-list", Guid.NewGuid().ToString(), null, CancellationToken.None));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -892,7 +892,7 @@ public sealed class InteractiveLaunchSpecServiceTests : IDisposable
|
|||||||
await SeedTaskAsync(t2, listId, TaskStatus.Idle, title: "Second task");
|
await SeedTaskAsync(t2, listId, TaskStatus.Idle, title: "Second task");
|
||||||
|
|
||||||
var svc = BuildService();
|
var svc = BuildService();
|
||||||
var spec = await svc.BuildForMergeHelperAsync(new[] { t1, t2 }, listId, Guid.NewGuid().ToString(), CancellationToken.None);
|
var spec = await svc.BuildForMergeHelperAsync(new[] { t1, t2 }, listId, Guid.NewGuid().ToString(), null, CancellationToken.None);
|
||||||
var sessionDir = TrackSessionDir(spec);
|
var sessionDir = TrackSessionDir(spec);
|
||||||
|
|
||||||
Assert.Equal(repo, spec.Cwd);
|
Assert.Equal(repo, spec.Cwd);
|
||||||
@@ -934,6 +934,27 @@ public sealed class InteractiveLaunchSpecServiceTests : IDisposable
|
|||||||
Assert.Equal(InteractiveLaunchSpecService.McpToolTimeoutMs, spec.Env["MCP_TOOL_TIMEOUT"]);
|
Assert.Equal(InteractiveLaunchSpecService.McpToolTimeoutMs, spec.Env["MCP_TOOL_TIMEOUT"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("fable", "fable")]
|
||||||
|
[InlineData("opus", "opus")]
|
||||||
|
[InlineData("not-a-model", ModelRegistry.HandlerTriageAlias)] // unknown → default, never blocks the launch
|
||||||
|
public async Task BuildForMergeHelperAsync_ModelOverride_DrivesModelAndEffortArgs(string requested, string expected)
|
||||||
|
{
|
||||||
|
var repo = Path.Combine(_tempDir, $"repoModel-{requested}");
|
||||||
|
Directory.CreateDirectory(repo);
|
||||||
|
var listId = await SeedListAsync(workingDir: repo, name: "Alpha");
|
||||||
|
var t1 = Guid.NewGuid().ToString();
|
||||||
|
await SeedTaskAsync(t1, listId, TaskStatus.Idle, title: "First task");
|
||||||
|
|
||||||
|
var svc = BuildService();
|
||||||
|
var spec = await svc.BuildForMergeHelperAsync(new[] { t1 }, listId, Guid.NewGuid().ToString(), requested, CancellationToken.None);
|
||||||
|
TrackSessionDir(spec);
|
||||||
|
|
||||||
|
var args = spec.Args.ToList();
|
||||||
|
Assert.Equal(expected, args[args.IndexOf("--model") + 1]);
|
||||||
|
Assert.Equal(ModelPresets.For(ModelPresets.Defaults, expected).Effort, args[args.IndexOf("--effort") + 1]);
|
||||||
|
}
|
||||||
|
|
||||||
// A directory argument that keeps its trailing separator escapes its own closing quote once the
|
// A directory argument that keeps its trailing separator escapes its own closing quote once the
|
||||||
// ConPTY host flattens Args into a single Windows command line ("C:\repo\" -> \" is a literal
|
// ConPTY host flattens Args into a single Windows command line ("C:\repo\" -> \" is a literal
|
||||||
// quote), so --add-dir's variadic list swallows every following argument -- including the
|
// quote), so --add-dir's variadic list swallows every following argument -- including the
|
||||||
@@ -949,7 +970,7 @@ public sealed class InteractiveLaunchSpecServiceTests : IDisposable
|
|||||||
await SeedTaskAsync(t1, listId, TaskStatus.WaitingForReview, title: "First task");
|
await SeedTaskAsync(t1, listId, TaskStatus.WaitingForReview, title: "First task");
|
||||||
|
|
||||||
var svc = BuildService();
|
var svc = BuildService();
|
||||||
var spec = await svc.BuildForMergeHelperAsync(new[] { t1 }, listId, Guid.NewGuid().ToString(), CancellationToken.None);
|
var spec = await svc.BuildForMergeHelperAsync(new[] { t1 }, listId, Guid.NewGuid().ToString(), null, CancellationToken.None);
|
||||||
var sessionDir = TrackSessionDir(spec);
|
var sessionDir = TrackSessionDir(spec);
|
||||||
|
|
||||||
var args = spec.Args.ToList();
|
var args = spec.Args.ToList();
|
||||||
@@ -972,7 +993,7 @@ public sealed class InteractiveLaunchSpecServiceTests : IDisposable
|
|||||||
await SeedTaskAsync(t2, listId, TaskStatus.Idle, title: "Second task");
|
await SeedTaskAsync(t2, listId, TaskStatus.Idle, title: "Second task");
|
||||||
|
|
||||||
var svc = BuildService();
|
var svc = BuildService();
|
||||||
var spec = await svc.BuildForMergeHelperAsync(new[] { t1, t2 }, listId, Guid.NewGuid().ToString(), CancellationToken.None);
|
var spec = await svc.BuildForMergeHelperAsync(new[] { t1, t2 }, listId, Guid.NewGuid().ToString(), null, CancellationToken.None);
|
||||||
var sessionDir = TrackSessionDir(spec);
|
var sessionDir = TrackSessionDir(spec);
|
||||||
|
|
||||||
var brief = File.ReadAllText(Path.Combine(sessionDir, "brief.md"));
|
var brief = File.ReadAllText(Path.Combine(sessionDir, "brief.md"));
|
||||||
@@ -999,7 +1020,7 @@ public sealed class InteractiveLaunchSpecServiceTests : IDisposable
|
|||||||
var handlerTaskId = Guid.NewGuid().ToString();
|
var handlerTaskId = Guid.NewGuid().ToString();
|
||||||
|
|
||||||
var svc = BuildService();
|
var svc = BuildService();
|
||||||
var spec = await svc.BuildForMergeHelperAsync(new[] { t1 }, listId, handlerTaskId, CancellationToken.None);
|
var spec = await svc.BuildForMergeHelperAsync(new[] { t1 }, listId, handlerTaskId, null, CancellationToken.None);
|
||||||
var sessionDir = TrackSessionDir(spec);
|
var sessionDir = TrackSessionDir(spec);
|
||||||
|
|
||||||
var brief = File.ReadAllText(Path.Combine(sessionDir, "brief.md"));
|
var brief = File.ReadAllText(Path.Combine(sessionDir, "brief.md"));
|
||||||
@@ -1018,7 +1039,7 @@ public sealed class InteractiveLaunchSpecServiceTests : IDisposable
|
|||||||
description: "Do the thing carefully and report back.");
|
description: "Do the thing carefully and report back.");
|
||||||
|
|
||||||
var svc = BuildService();
|
var svc = BuildService();
|
||||||
var spec = await svc.BuildForMergeHelperAsync(new[] { t1 }, listId, Guid.NewGuid().ToString(), CancellationToken.None);
|
var spec = await svc.BuildForMergeHelperAsync(new[] { t1 }, listId, Guid.NewGuid().ToString(), null, CancellationToken.None);
|
||||||
var sessionDir = TrackSessionDir(spec);
|
var sessionDir = TrackSessionDir(spec);
|
||||||
|
|
||||||
var brief = File.ReadAllText(Path.Combine(sessionDir, "brief.md"));
|
var brief = File.ReadAllText(Path.Combine(sessionDir, "brief.md"));
|
||||||
@@ -1036,7 +1057,7 @@ public sealed class InteractiveLaunchSpecServiceTests : IDisposable
|
|||||||
await SeedTaskAsync(t1, listId, TaskStatus.Idle, title: "No description task", description: null);
|
await SeedTaskAsync(t1, listId, TaskStatus.Idle, title: "No description task", description: null);
|
||||||
|
|
||||||
var svc = BuildService();
|
var svc = BuildService();
|
||||||
var spec = await svc.BuildForMergeHelperAsync(new[] { t1 }, listId, Guid.NewGuid().ToString(), CancellationToken.None);
|
var spec = await svc.BuildForMergeHelperAsync(new[] { t1 }, listId, Guid.NewGuid().ToString(), null, CancellationToken.None);
|
||||||
var sessionDir = TrackSessionDir(spec);
|
var sessionDir = TrackSessionDir(spec);
|
||||||
|
|
||||||
var brief = File.ReadAllText(Path.Combine(sessionDir, "brief.md"));
|
var brief = File.ReadAllText(Path.Combine(sessionDir, "brief.md"));
|
||||||
@@ -1067,7 +1088,7 @@ public sealed class InteractiveLaunchSpecServiceTests : IDisposable
|
|||||||
await SeedTaskAsync(t2, listId, TaskStatus.WaitingForReview, title: "Second task", description: "plain description");
|
await SeedTaskAsync(t2, listId, TaskStatus.WaitingForReview, title: "Second task", description: "plain description");
|
||||||
|
|
||||||
var svc = BuildService();
|
var svc = BuildService();
|
||||||
var spec = await svc.BuildForMergeHelperAsync(new[] { t1, t2 }, listId, Guid.NewGuid().ToString(), CancellationToken.None);
|
var spec = await svc.BuildForMergeHelperAsync(new[] { t1, t2 }, listId, Guid.NewGuid().ToString(), null, CancellationToken.None);
|
||||||
var sessionDir = TrackSessionDir(spec);
|
var sessionDir = TrackSessionDir(spec);
|
||||||
|
|
||||||
var brief = File.ReadAllText(Path.Combine(sessionDir, "brief.md"));
|
var brief = File.ReadAllText(Path.Combine(sessionDir, "brief.md"));
|
||||||
@@ -1402,7 +1423,7 @@ public sealed class InteractiveLaunchSpecServiceTests : IDisposable
|
|||||||
|
|
||||||
var svc = BuildService();
|
var svc = BuildService();
|
||||||
|
|
||||||
var triageSpec = await svc.BuildForMergeHelperAsync(new[] { survivor }, listId, handlerTaskId, CancellationToken.None);
|
var triageSpec = await svc.BuildForMergeHelperAsync(new[] { survivor }, listId, handlerTaskId, null, CancellationToken.None);
|
||||||
var triageDir = TrackSessionDir(triageSpec);
|
var triageDir = TrackSessionDir(triageSpec);
|
||||||
var executeSpec = await svc.BuildForMergeHelperHandoffAsync(handlerTaskId, new[] { survivor }, "merge", CancellationToken.None);
|
var executeSpec = await svc.BuildForMergeHelperHandoffAsync(handlerTaskId, new[] { survivor }, "merge", CancellationToken.None);
|
||||||
var executeDir = TrackSessionDir(executeSpec);
|
var executeDir = TrackSessionDir(executeSpec);
|
||||||
@@ -1453,7 +1474,7 @@ public sealed class InteractiveLaunchSpecServiceTests : IDisposable
|
|||||||
ClaudeSessionId: "sess-42", Token: "tok-2", WorktreePath: _worktreeDir);
|
ClaudeSessionId: "sess-42", Token: "tok-2", WorktreePath: _worktreeDir);
|
||||||
|
|
||||||
var svc = BuildService();
|
var svc = BuildService();
|
||||||
var mergeHelperSpec = await svc.BuildForMergeHelperAsync(new[] { survivor }, listId, handlerTaskId, CancellationToken.None);
|
var mergeHelperSpec = await svc.BuildForMergeHelperAsync(new[] { survivor }, listId, handlerTaskId, null, CancellationToken.None);
|
||||||
TrackSessionDir(mergeHelperSpec);
|
TrackSessionDir(mergeHelperSpec);
|
||||||
var handoffSpec = await svc.BuildForMergeHelperHandoffAsync(handlerTaskId, new[] { survivor }, "wait", CancellationToken.None);
|
var handoffSpec = await svc.BuildForMergeHelperHandoffAsync(handlerTaskId, new[] { survivor }, "wait", CancellationToken.None);
|
||||||
TrackSessionDir(handoffSpec);
|
TrackSessionDir(handoffSpec);
|
||||||
|
|||||||
Reference in New Issue
Block a user