feat(ui): live task updates from worker events + planning polish
Wire TasksIslandViewModel to TaskUpdated/WorktreeUpdated/TaskMessage worker events so rows refresh without a full reload; add ForegroundHelper to permit wt.exe to take foreground on planning launch; misc UI polish on lists, task rows and settings modal. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
@@ -10,8 +8,6 @@ using Microsoft.EntityFrameworkCore.Migrations;
|
|||||||
namespace ClaudeDo.Data.Migrations
|
namespace ClaudeDo.Data.Migrations
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
[DbContext(typeof(ClaudeDoDbContext))]
|
|
||||||
[Migration("20260416064948_InitialCreate")]
|
|
||||||
public partial class InitialCreate : Migration
|
public partial class InitialCreate : Migration
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
@@ -7,8 +5,6 @@ using Microsoft.EntityFrameworkCore.Migrations;
|
|||||||
namespace ClaudeDo.Data.Migrations
|
namespace ClaudeDo.Data.Migrations
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
[DbContext(typeof(ClaudeDoDbContext))]
|
|
||||||
[Migration("20260420075929_AddTaskFlagsAndNotes")]
|
|
||||||
public partial class AddTaskFlagsAndNotes : Migration
|
public partial class AddTaskFlagsAndNotes : Migration
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|||||||
@@ -1,14 +1,10 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
namespace ClaudeDo.Data.Migrations
|
namespace ClaudeDo.Data.Migrations
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
[DbContext(typeof(ClaudeDoDbContext))]
|
|
||||||
[Migration("20260421113614_AddAppSettings")]
|
|
||||||
public partial class AddAppSettings : Migration
|
public partial class AddAppSettings : Migration
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
@@ -7,8 +5,6 @@ using Microsoft.EntityFrameworkCore.Migrations;
|
|||||||
namespace ClaudeDo.Data.Migrations
|
namespace ClaudeDo.Data.Migrations
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
[DbContext(typeof(ClaudeDoDbContext))]
|
|
||||||
[Migration("20260422120000_AddTaskSortOrder")]
|
|
||||||
public partial class AddTaskSortOrder : Migration
|
public partial class AddTaskSortOrder : Migration
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|||||||
@@ -1,15 +1,10 @@
|
|||||||
using System;
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
namespace ClaudeDo.Data.Migrations
|
namespace ClaudeDo.Data.Migrations
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
[DbContext(typeof(ClaudeDoDbContext))]
|
|
||||||
[Migration("20260423154708_AddPlanningSupport")]
|
|
||||||
public partial class AddPlanningSupport : Migration
|
public partial class AddPlanningSupport : Migration
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|||||||
19
src/ClaudeDo.Ui/Services/ForegroundHelper.cs
Normal file
19
src/ClaudeDo.Ui/Services/ForegroundHelper.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Ui.Services;
|
||||||
|
|
||||||
|
internal static class ForegroundHelper
|
||||||
|
{
|
||||||
|
private const int ASFW_ANY = -1;
|
||||||
|
|
||||||
|
[DllImport("user32.dll", SetLastError = true)]
|
||||||
|
private static extern bool AllowSetForegroundWindow(int dwProcessId);
|
||||||
|
|
||||||
|
// Grants any process the right to take foreground on next SetForegroundWindow call.
|
||||||
|
// Used before RPCs that cause a helper process (e.g. wt.exe) to spawn a new window.
|
||||||
|
public static void AllowAny()
|
||||||
|
{
|
||||||
|
if (!OperatingSystem.IsWindows()) return;
|
||||||
|
try { AllowSetForegroundWindow(ASFW_ANY); } catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,10 @@ namespace ClaudeDo.Ui.Services;
|
|||||||
|
|
||||||
public interface IWorkerClient
|
public interface IWorkerClient
|
||||||
{
|
{
|
||||||
|
event Action<string>? TaskUpdatedEvent;
|
||||||
|
event Action<string>? WorktreeUpdatedEvent;
|
||||||
|
event Action<string, string>? TaskMessageEvent;
|
||||||
|
|
||||||
Task WakeQueueAsync();
|
Task WakeQueueAsync();
|
||||||
Task StartPlanningSessionAsync(string taskId, CancellationToken ct = default);
|
Task StartPlanningSessionAsync(string taskId, CancellationToken ct = default);
|
||||||
Task ResumePlanningSessionAsync(string taskId, CancellationToken ct = default);
|
Task ResumePlanningSessionAsync(string taskId, CancellationToken ct = default);
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ namespace ClaudeDo.Ui.ViewModels.Islands;
|
|||||||
public sealed partial class ListNavItemViewModel : ViewModelBase
|
public sealed partial class ListNavItemViewModel : ViewModelBase
|
||||||
{
|
{
|
||||||
public required string Id { get; init; }
|
public required string Id { get; init; }
|
||||||
public required string Name { get; init; }
|
|
||||||
public required ListKind Kind { get; init; }
|
public required ListKind Kind { get; init; }
|
||||||
|
[ObservableProperty] private string _name = "";
|
||||||
[ObservableProperty] private int _count;
|
[ObservableProperty] private int _count;
|
||||||
[ObservableProperty] private bool _isActive;
|
[ObservableProperty] private bool _isActive;
|
||||||
[ObservableProperty] private string? _workingDir;
|
[ObservableProperty] private string? _workingDir;
|
||||||
|
|||||||
@@ -40,8 +40,9 @@ public sealed partial class ListsIslandViewModel : ViewModelBase
|
|||||||
private async System.Threading.Tasks.Task OpenListSettingsAsync(ListNavItemViewModel? row)
|
private async System.Threading.Tasks.Task OpenListSettingsAsync(ListNavItemViewModel? row)
|
||||||
{
|
{
|
||||||
if (row is null || ShowListSettingsModal is null || _services is null) return;
|
if (row is null || ShowListSettingsModal is null || _services is null) return;
|
||||||
|
var rawId = row.Id.StartsWith("user:", StringComparison.Ordinal) ? row.Id["user:".Length..] : row.Id;
|
||||||
var vm = _services.GetRequiredService<ListSettingsModalViewModel>();
|
var vm = _services.GetRequiredService<ListSettingsModalViewModel>();
|
||||||
await vm.LoadAsync(row.Id, row.Name, row.WorkingDir, row.DefaultCommitType);
|
await vm.LoadAsync(rawId, row.Name, row.WorkingDir, row.DefaultCommitType);
|
||||||
await ShowListSettingsModal(vm);
|
await ShowListSettingsModal(vm);
|
||||||
await RefreshRowAsync(row.Id);
|
await RefreshRowAsync(row.Id);
|
||||||
}
|
}
|
||||||
@@ -169,6 +170,46 @@ public sealed partial class ListsIslandViewModel : ViewModelBase
|
|||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void Select(ListNavItemViewModel item) => SelectedList = item;
|
private void Select(ListNavItemViewModel item) => SelectedList = item;
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private async Task CreateListAsync()
|
||||||
|
{
|
||||||
|
var entity = new ListEntity
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid().ToString("N"),
|
||||||
|
Name = "New list",
|
||||||
|
DefaultCommitType = "chore",
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
};
|
||||||
|
|
||||||
|
await using (var ctx = await _dbFactory.CreateDbContextAsync())
|
||||||
|
{
|
||||||
|
var lists = new ListRepository(ctx);
|
||||||
|
await lists.AddAsync(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
var item = new ListNavItemViewModel
|
||||||
|
{
|
||||||
|
Id = $"user:{entity.Id}",
|
||||||
|
Name = entity.Name,
|
||||||
|
Kind = ListKind.User,
|
||||||
|
IconKey = "Folder",
|
||||||
|
DotColorKey = "Moss",
|
||||||
|
WorkingDir = entity.WorkingDir,
|
||||||
|
DefaultCommitType = entity.DefaultCommitType,
|
||||||
|
};
|
||||||
|
Items.Add(item);
|
||||||
|
UserLists.Add(item);
|
||||||
|
SelectedList = item;
|
||||||
|
|
||||||
|
if (ShowListSettingsModal is not null && _services is not null)
|
||||||
|
{
|
||||||
|
var vm = _services.GetRequiredService<ListSettingsModalViewModel>();
|
||||||
|
await vm.LoadAsync(entity.Id, entity.Name, entity.WorkingDir, entity.DefaultCommitType);
|
||||||
|
await ShowListSettingsModal(vm);
|
||||||
|
await RefreshRowAsync(item.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
partial void OnSelectedListChanged(ListNavItemViewModel? value)
|
partial void OnSelectedListChanged(ListNavItemViewModel? value)
|
||||||
{
|
{
|
||||||
foreach (var i in Items) i.IsActive = ReferenceEquals(i, value);
|
foreach (var i in Items) i.IsActive = ReferenceEquals(i, value);
|
||||||
@@ -188,6 +229,7 @@ public sealed partial class ListsIslandViewModel : ViewModelBase
|
|||||||
var entity = await lists.GetByIdAsync(rawId);
|
var entity = await lists.GetByIdAsync(rawId);
|
||||||
if (entity is null) return;
|
if (entity is null) return;
|
||||||
|
|
||||||
|
row.Name = entity.Name;
|
||||||
row.WorkingDir = entity.WorkingDir;
|
row.WorkingDir = entity.WorkingDir;
|
||||||
row.DefaultCommitType = entity.DefaultCommitType;
|
row.DefaultCommitType = entity.DefaultCommitType;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,24 +102,26 @@ public sealed partial class TaskRowViewModel : ViewModelBase
|
|||||||
|
|
||||||
public static TaskRowViewModel FromEntity(TaskEntity t)
|
public static TaskRowViewModel FromEntity(TaskEntity t)
|
||||||
{
|
{
|
||||||
var (add, del) = ParseDiffStat(t.Worktree?.DiffStat);
|
var row = new TaskRowViewModel { Id = t.Id, CreatedAt = t.CreatedAt };
|
||||||
return new TaskRowViewModel
|
row.UpdateFromEntity(t);
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateFromEntity(TaskEntity t)
|
||||||
{
|
{
|
||||||
Id = t.Id,
|
var (add, del) = ParseDiffStat(t.Worktree?.DiffStat);
|
||||||
Title = t.Title,
|
Title = t.Title;
|
||||||
ListName = t.List?.Name ?? "",
|
ListName = t.List?.Name ?? "";
|
||||||
Done = t.Status == TaskStatus.Done,
|
Done = t.Status == TaskStatus.Done;
|
||||||
IsStarred = t.IsStarred,
|
IsStarred = t.IsStarred;
|
||||||
IsMyDay = t.IsMyDay,
|
IsMyDay = t.IsMyDay;
|
||||||
Status = t.Status,
|
Status = t.Status;
|
||||||
Branch = t.Worktree?.BranchName,
|
Branch = t.Worktree?.BranchName;
|
||||||
DiffStat = t.Worktree?.DiffStat,
|
DiffStat = t.Worktree?.DiffStat;
|
||||||
ScheduledFor = t.ScheduledFor,
|
ScheduledFor = t.ScheduledFor;
|
||||||
DiffAdditions = add,
|
DiffAdditions = add;
|
||||||
DiffDeletions = del,
|
DiffDeletions = del;
|
||||||
CreatedAt = t.CreatedAt,
|
ParentTaskId = t.ParentTaskId;
|
||||||
ParentTaskId = t.ParentTaskId,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Best-effort parse of diff stat strings like "+12 -3" or "12 additions, 3 deletions".
|
// Best-effort parse of diff stat strings like "+12 -3" or "12 additions, 3 deletions".
|
||||||
|
|||||||
@@ -49,6 +49,70 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
|
|||||||
{
|
{
|
||||||
_dbFactory = dbFactory;
|
_dbFactory = dbFactory;
|
||||||
_worker = worker;
|
_worker = worker;
|
||||||
|
if (_worker is not null)
|
||||||
|
{
|
||||||
|
_worker.TaskUpdatedEvent += OnWorkerTaskUpdated;
|
||||||
|
_worker.WorktreeUpdatedEvent += OnWorkerTaskUpdated;
|
||||||
|
_worker.TaskMessageEvent += OnWorkerTaskMessage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnWorkerTaskMessage(string taskId, string line)
|
||||||
|
{
|
||||||
|
var row = Items.FirstOrDefault(r => r.Id == taskId);
|
||||||
|
if (row is not null) row.LiveTail = line;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void OnWorkerTaskUpdated(string taskId)
|
||||||
|
{
|
||||||
|
var list = _currentList;
|
||||||
|
if (list is null) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||||
|
var entity = await db.Tasks
|
||||||
|
.Include(t => t.List)
|
||||||
|
.Include(t => t.Worktree)
|
||||||
|
.FirstOrDefaultAsync(t => t.Id == taskId);
|
||||||
|
|
||||||
|
var existing = Items.FirstOrDefault(r => r.Id == taskId);
|
||||||
|
|
||||||
|
if (entity is null)
|
||||||
|
{
|
||||||
|
if (existing is not null) Items.Remove(existing);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var matches = TaskMatchesList(entity, list);
|
||||||
|
if (existing is not null && matches) existing.UpdateFromEntity(entity);
|
||||||
|
else if (existing is not null) Items.Remove(existing);
|
||||||
|
else if (matches) { LoadForList(list); return; }
|
||||||
|
else return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Regroup();
|
||||||
|
UpdateSubtitle();
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TaskMatchesList(TaskEntity t, ListNavItemViewModel list) => list.Kind switch
|
||||||
|
{
|
||||||
|
ListKind.Smart when list.Id == "smart:my-day" => t.IsMyDay,
|
||||||
|
ListKind.Smart when list.Id == "smart:important" => t.IsStarred,
|
||||||
|
ListKind.Smart when list.Id == "smart:planned" => t.ScheduledFor != null,
|
||||||
|
ListKind.Virtual when list.Id == "virtual:queued" => t.Status == TaskStatus.Queued,
|
||||||
|
ListKind.Virtual when list.Id == "virtual:running" => t.Status == TaskStatus.Running,
|
||||||
|
ListKind.Virtual when list.Id == "virtual:review" => t.Status == TaskStatus.Done && t.Worktree?.State == WorktreeState.Active,
|
||||||
|
ListKind.User => $"user:{t.ListId}" == list.Id,
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
|
||||||
|
private void OnCurrentListPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.PropertyName == nameof(ListNavItemViewModel.Name) && sender is ListNavItemViewModel vm)
|
||||||
|
HeaderTitle = vm.Name;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void LoadForList(ListNavItemViewModel? list)
|
public void LoadForList(ListNavItemViewModel? list)
|
||||||
@@ -58,7 +122,12 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
|
|||||||
_loadCts = new CancellationTokenSource();
|
_loadCts = new CancellationTokenSource();
|
||||||
var ct = _loadCts.Token;
|
var ct = _loadCts.Token;
|
||||||
|
|
||||||
|
if (_currentList is not null)
|
||||||
|
_currentList.PropertyChanged -= OnCurrentListPropertyChanged;
|
||||||
_currentList = list;
|
_currentList = list;
|
||||||
|
if (_currentList is not null)
|
||||||
|
_currentList.PropertyChanged += OnCurrentListPropertyChanged;
|
||||||
|
|
||||||
Items.Clear();
|
Items.Clear();
|
||||||
OverdueItems.Clear();
|
OverdueItems.Clear();
|
||||||
OpenItems.Clear();
|
OpenItems.Clear();
|
||||||
@@ -385,6 +454,7 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
|
|||||||
private async Task OpenPlanningSessionAsync(TaskRowViewModel? row)
|
private async Task OpenPlanningSessionAsync(TaskRowViewModel? row)
|
||||||
{
|
{
|
||||||
if (row is null || row.Status != TaskStatus.Manual) return;
|
if (row is null || row.Status != TaskStatus.Manual) return;
|
||||||
|
ForegroundHelper.AllowAny();
|
||||||
try { await _worker!.StartPlanningSessionAsync(row.Id); }
|
try { await _worker!.StartPlanningSessionAsync(row.Id); }
|
||||||
catch { }
|
catch { }
|
||||||
}
|
}
|
||||||
@@ -412,6 +482,7 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
|
|||||||
switch (choice)
|
switch (choice)
|
||||||
{
|
{
|
||||||
case UnfinishedPlanningModalResult.Resume:
|
case UnfinishedPlanningModalResult.Resume:
|
||||||
|
ForegroundHelper.AllowAny();
|
||||||
await _worker.ResumePlanningSessionAsync(row.Id);
|
await _worker.ResumePlanningSessionAsync(row.Id);
|
||||||
break;
|
break;
|
||||||
case UnfinishedPlanningModalResult.FinalizeNow:
|
case UnfinishedPlanningModalResult.FinalizeNow:
|
||||||
|
|||||||
@@ -166,7 +166,8 @@
|
|||||||
</ItemsControl>
|
</ItemsControl>
|
||||||
|
|
||||||
<!-- + New list button -->
|
<!-- + New list button -->
|
||||||
<Button Classes="new-list-btn" Margin="0,4,0,0">
|
<Button Classes="new-list-btn" Margin="0,4,0,0"
|
||||||
|
Command="{Binding CreateListCommand}">
|
||||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||||
<PathIcon Data="{StaticResource Icon.Plus}"
|
<PathIcon Data="{StaticResource Icon.Plus}"
|
||||||
Width="13" Height="13"
|
Width="13" Height="13"
|
||||||
|
|||||||
@@ -39,16 +39,13 @@
|
|||||||
Click="OnRemoveFromQueueClick"/>
|
Click="OnRemoveFromQueueClick"/>
|
||||||
<Separator/>
|
<Separator/>
|
||||||
<MenuItem Header="Open planning Session"
|
<MenuItem Header="Open planning Session"
|
||||||
Command="{Binding $parent[ItemsControl].((vm:TasksIslandViewModel)DataContext).OpenPlanningSessionCommand}"
|
Click="OnOpenPlanningSessionClick"
|
||||||
CommandParameter="{Binding}"
|
|
||||||
IsVisible="{Binding CanOpenPlanningSession}"/>
|
IsVisible="{Binding CanOpenPlanningSession}"/>
|
||||||
<MenuItem Header="Resume planning Session"
|
<MenuItem Header="Resume planning Session"
|
||||||
Command="{Binding $parent[ItemsControl].((vm:TasksIslandViewModel)DataContext).ResumePlanningSessionCommand}"
|
Click="OnResumePlanningSessionClick"
|
||||||
CommandParameter="{Binding}"
|
|
||||||
IsVisible="{Binding CanResumeOrDiscardPlanning}"/>
|
IsVisible="{Binding CanResumeOrDiscardPlanning}"/>
|
||||||
<MenuItem Header="Discard planning session"
|
<MenuItem Header="Discard planning session"
|
||||||
Command="{Binding $parent[ItemsControl].((vm:TasksIslandViewModel)DataContext).DiscardPlanningSessionCommand}"
|
Click="OnDiscardPlanningSessionClick"
|
||||||
CommandParameter="{Binding}"
|
|
||||||
IsVisible="{Binding CanResumeOrDiscardPlanning}"/>
|
IsVisible="{Binding CanResumeOrDiscardPlanning}"/>
|
||||||
<Separator/>
|
<Separator/>
|
||||||
<MenuItem Header="Schedule for..." Click="OnScheduleForClick"/>
|
<MenuItem Header="Schedule for..." Click="OnScheduleForClick"/>
|
||||||
|
|||||||
@@ -39,6 +39,24 @@ public partial class TaskRowView : UserControl
|
|||||||
await vm.ClearScheduleCommand.ExecuteAsync(row);
|
await vm.ClearScheduleCommand.ExecuteAsync(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async void OnOpenPlanningSessionClick(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (DataContext is TaskRowViewModel row && FindTasksVm() is { } vm)
|
||||||
|
await vm.OpenPlanningSessionCommand.ExecuteAsync(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void OnResumePlanningSessionClick(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (DataContext is TaskRowViewModel row && FindTasksVm() is { } vm)
|
||||||
|
await vm.ResumePlanningSessionCommand.ExecuteAsync(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void OnDiscardPlanningSessionClick(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (DataContext is TaskRowViewModel row && FindTasksVm() is { } vm)
|
||||||
|
await vm.DiscardPlanningSessionCommand.ExecuteAsync(row);
|
||||||
|
}
|
||||||
|
|
||||||
private void OnScheduleForClick(object? sender, RoutedEventArgs e)
|
private void OnScheduleForClick(object? sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
if (DataContext is not TaskRowViewModel row) return;
|
if (DataContext is not TaskRowViewModel row) return;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
|
|
||||||
<Window.KeyBindings>
|
<Window.KeyBindings>
|
||||||
<KeyBinding Gesture="Escape" Command="{Binding CancelCommand}"/>
|
<KeyBinding Gesture="Escape" Command="{Binding CancelCommand}"/>
|
||||||
|
<KeyBinding Gesture="Enter" Command="{Binding SaveCommand}"/>
|
||||||
</Window.KeyBindings>
|
</Window.KeyBindings>
|
||||||
|
|
||||||
<Window.Styles>
|
<Window.Styles>
|
||||||
@@ -82,7 +83,7 @@
|
|||||||
<StackPanel Spacing="12">
|
<StackPanel Spacing="12">
|
||||||
<StackPanel Spacing="4">
|
<StackPanel Spacing="4">
|
||||||
<TextBlock Classes="field-label" Text="Name"/>
|
<TextBlock Classes="field-label" Text="Name"/>
|
||||||
<TextBox Text="{Binding Name}" />
|
<TextBox Text="{Binding Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<StackPanel Spacing="4">
|
<StackPanel Spacing="4">
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ namespace ClaudeDo.Worker.Planning;
|
|||||||
public sealed class WindowsTerminalPlanningLauncher : IPlanningTerminalLauncher
|
public sealed class WindowsTerminalPlanningLauncher : IPlanningTerminalLauncher
|
||||||
{
|
{
|
||||||
private const string AllowedTools = "mcp__claudedo__*,Read,Grep,Glob,WebFetch,WebSearch,Skill";
|
private const string AllowedTools = "mcp__claudedo__*,Read,Grep,Glob,WebFetch,WebSearch,Skill";
|
||||||
private const string Model = "claude-sonnet-4-6";
|
private const string Model = "claude-opus-4-7";
|
||||||
|
|
||||||
private readonly string _wtPath;
|
private readonly string _wtPath;
|
||||||
private readonly string _claudePath;
|
private readonly string _claudePath;
|
||||||
@@ -49,17 +49,19 @@ public sealed class WindowsTerminalPlanningLauncher : IPlanningTerminalLauncher
|
|||||||
|
|
||||||
psi.Environment["MAX_THINKING_TOKENS"] = "20000";
|
psi.Environment["MAX_THINKING_TOKENS"] = "20000";
|
||||||
|
|
||||||
|
// Arg order matters: --allowedTools and --mcp-config are variadic (space-separated).
|
||||||
|
// The positional prompt must follow a single-value flag, or it will be swallowed.
|
||||||
psi.ArgumentList.Add("-d");
|
psi.ArgumentList.Add("-d");
|
||||||
psi.ArgumentList.Add(ctx.WorkingDir);
|
psi.ArgumentList.Add(ctx.WorkingDir);
|
||||||
psi.ArgumentList.Add(resolvedClaude);
|
psi.ArgumentList.Add(resolvedClaude);
|
||||||
psi.ArgumentList.Add("--model");
|
psi.ArgumentList.Add("--model");
|
||||||
psi.ArgumentList.Add(Model);
|
psi.ArgumentList.Add(Model);
|
||||||
|
psi.ArgumentList.Add("--allowedTools");
|
||||||
|
psi.ArgumentList.Add(AllowedTools);
|
||||||
psi.ArgumentList.Add("--mcp-config");
|
psi.ArgumentList.Add("--mcp-config");
|
||||||
psi.ArgumentList.Add(ctx.Files.McpConfigPath);
|
psi.ArgumentList.Add(ctx.Files.McpConfigPath);
|
||||||
psi.ArgumentList.Add("--append-system-prompt-file");
|
psi.ArgumentList.Add("--append-system-prompt-file");
|
||||||
psi.ArgumentList.Add(ctx.Files.SystemPromptPath);
|
psi.ArgumentList.Add(ctx.Files.SystemPromptPath);
|
||||||
psi.ArgumentList.Add("--allowedTools");
|
|
||||||
psi.ArgumentList.Add(AllowedTools);
|
|
||||||
psi.ArgumentList.Add(File.ReadAllText(ctx.Files.InitialPromptPath));
|
psi.ArgumentList.Add(File.ReadAllText(ctx.Files.InitialPromptPath));
|
||||||
|
|
||||||
var proc = Process.Start(psi)
|
var proc = Process.Start(psi)
|
||||||
@@ -91,10 +93,10 @@ public sealed class WindowsTerminalPlanningLauncher : IPlanningTerminalLauncher
|
|||||||
psi.ArgumentList.Add("-d");
|
psi.ArgumentList.Add("-d");
|
||||||
psi.ArgumentList.Add(ctx.WorkingDir);
|
psi.ArgumentList.Add(ctx.WorkingDir);
|
||||||
psi.ArgumentList.Add(resolvedClaude);
|
psi.ArgumentList.Add(resolvedClaude);
|
||||||
psi.ArgumentList.Add("--resume");
|
|
||||||
psi.ArgumentList.Add(ctx.ClaudeSessionId);
|
|
||||||
psi.ArgumentList.Add("--mcp-config");
|
psi.ArgumentList.Add("--mcp-config");
|
||||||
psi.ArgumentList.Add(ctx.McpConfigPath);
|
psi.ArgumentList.Add(ctx.McpConfigPath);
|
||||||
|
psi.ArgumentList.Add("--resume");
|
||||||
|
psi.ArgumentList.Add(ctx.ClaudeSessionId);
|
||||||
|
|
||||||
var proc = Process.Start(psi)
|
var proc = Process.Start(psi)
|
||||||
?? throw new PlanningLaunchException("Failed to start Windows Terminal process.");
|
?? throw new PlanningLaunchException("Failed to start Windows Terminal process.");
|
||||||
|
|||||||
@@ -60,7 +60,8 @@ builder.Services.AddSingleton(sp =>
|
|||||||
new PlanningSessionManager(
|
new PlanningSessionManager(
|
||||||
sp.GetRequiredService<IDbContextFactory<ClaudeDoDbContext>>(),
|
sp.GetRequiredService<IDbContextFactory<ClaudeDoDbContext>>(),
|
||||||
planningSessionsDir));
|
planningSessionsDir));
|
||||||
builder.Services.AddSingleton<IPlanningTerminalLauncher, WindowsTerminalPlanningLauncher>();
|
builder.Services.AddSingleton<IPlanningTerminalLauncher>(sp =>
|
||||||
|
new WindowsTerminalPlanningLauncher("wt.exe", cfg.ClaudeBin));
|
||||||
builder.Services.AddHttpContextAccessor();
|
builder.Services.AddHttpContextAccessor();
|
||||||
builder.Services.AddScoped<PlanningMcpContextAccessor>();
|
builder.Services.AddScoped<PlanningMcpContextAccessor>();
|
||||||
builder.Services.AddScoped<ClaudeDoDbContext>(sp =>
|
builder.Services.AddScoped<ClaudeDoDbContext>(sp =>
|
||||||
|
|||||||
@@ -19,6 +19,13 @@ sealed class FakeWorkerClient : IWorkerClient
|
|||||||
public int FinalizePlanningCalls { get; private set; }
|
public int FinalizePlanningCalls { get; private set; }
|
||||||
public int WakeQueueCalls { get; private set; }
|
public int WakeQueueCalls { get; private set; }
|
||||||
|
|
||||||
|
public event Action<string>? TaskUpdatedEvent;
|
||||||
|
public event Action<string>? WorktreeUpdatedEvent;
|
||||||
|
public event Action<string, string>? TaskMessageEvent;
|
||||||
|
public void RaiseTaskUpdated(string taskId) => TaskUpdatedEvent?.Invoke(taskId);
|
||||||
|
public void RaiseWorktreeUpdated(string taskId) => WorktreeUpdatedEvent?.Invoke(taskId);
|
||||||
|
public void RaiseTaskMessage(string taskId, string line) => TaskMessageEvent?.Invoke(taskId, line);
|
||||||
|
|
||||||
public Task WakeQueueAsync() { WakeQueueCalls++; return Task.CompletedTask; }
|
public Task WakeQueueAsync() { WakeQueueCalls++; return Task.CompletedTask; }
|
||||||
public Task StartPlanningSessionAsync(string taskId, CancellationToken ct = default) { StartPlanningCalls++; return Task.CompletedTask; }
|
public Task StartPlanningSessionAsync(string taskId, CancellationToken ct = default) { StartPlanningCalls++; return Task.CompletedTask; }
|
||||||
public Task ResumePlanningSessionAsync(string taskId, CancellationToken ct = default) { ResumePlanningCalls++; return Task.CompletedTask; }
|
public Task ResumePlanningSessionAsync(string taskId, CancellationToken ct = default) { ResumePlanningCalls++; return Task.CompletedTask; }
|
||||||
|
|||||||
Reference in New Issue
Block a user