feat(prime): wire prompt_override into the daily prep prompt
The prime_schedules.prompt_override column existed end-to-end but was never populated or read. DailyPrepPrompt.BuildPrompt now takes an optional override and appends it as an extra paragraph after the fixed prompt (additive, never a replacement, so a user can't disable the required get_daily_prep_candidates/set_my_day flow). PrimeRunner passes schedule.PromptOverride through. The Prime tab in Settings now has a multiline field per schedule wired to a new PromptOverride property on PrimeScheduleRowViewModel (blank persists as null).
This commit is contained in:
@@ -75,6 +75,8 @@
|
|||||||
"addSchedule": "+ Zeitplan hinzufügen",
|
"addSchedule": "+ Zeitplan hinzufügen",
|
||||||
"removeScheduleTip": "Zeitplan entfernen",
|
"removeScheduleTip": "Zeitplan entfernen",
|
||||||
"dailyPrepMaxTasks": "Max. Aufgaben pro Tag",
|
"dailyPrepMaxTasks": "Max. Aufgaben pro Tag",
|
||||||
|
"promptOverrideLabel": "Prompt-Zusatz",
|
||||||
|
"promptOverridePlaceholder": "Zusätzliche Anweisungen, die für diesen Zeitplan an den Prime-Prompt angehängt werden (optional)",
|
||||||
"dayMo": "Mo",
|
"dayMo": "Mo",
|
||||||
"dayTu": "Di",
|
"dayTu": "Di",
|
||||||
"dayWe": "Mi",
|
"dayWe": "Mi",
|
||||||
|
|||||||
@@ -75,6 +75,8 @@
|
|||||||
"addSchedule": "+ Add schedule",
|
"addSchedule": "+ Add schedule",
|
||||||
"removeScheduleTip": "Remove schedule",
|
"removeScheduleTip": "Remove schedule",
|
||||||
"dailyPrepMaxTasks": "Max tasks per day",
|
"dailyPrepMaxTasks": "Max tasks per day",
|
||||||
|
"promptOverrideLabel": "Prompt addition",
|
||||||
|
"promptOverridePlaceholder": "Extra instructions appended to the daily prep prompt for this schedule (optional)",
|
||||||
"dayMo": "Mo",
|
"dayMo": "Mo",
|
||||||
"dayTu": "Tu",
|
"dayTu": "Tu",
|
||||||
"dayWe": "We",
|
"dayWe": "We",
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ public sealed partial class PrimeScheduleRowViewModel : ViewModelBase
|
|||||||
[ObservableProperty] private bool _sunday;
|
[ObservableProperty] private bool _sunday;
|
||||||
[ObservableProperty] private TimeSpan _timeOfDay;
|
[ObservableProperty] private TimeSpan _timeOfDay;
|
||||||
[ObservableProperty] private DateTimeOffset? _lastRunAt;
|
[ObservableProperty] private DateTimeOffset? _lastRunAt;
|
||||||
|
[ObservableProperty] private string? _promptOverride;
|
||||||
|
|
||||||
public string LastRunLabel => LastRunAt is { } v ? v.LocalDateTime.ToString("g") : "—";
|
public string LastRunLabel => LastRunAt is { } v ? v.LocalDateTime.ToString("g") : "—";
|
||||||
|
|
||||||
@@ -58,6 +59,7 @@ public sealed partial class PrimeScheduleRowViewModel : ViewModelBase
|
|||||||
Sunday = (dto.Days & Sun) != 0;
|
Sunday = (dto.Days & Sun) != 0;
|
||||||
TimeOfDay = dto.TimeOfDay;
|
TimeOfDay = dto.TimeOfDay;
|
||||||
LastRunAt = dto.LastRunAt;
|
LastRunAt = dto.LastRunAt;
|
||||||
|
PromptOverride = dto.PromptOverride;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int DaysMask()
|
public int DaysMask()
|
||||||
@@ -74,5 +76,6 @@ public sealed partial class PrimeScheduleRowViewModel : ViewModelBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
public PrimeScheduleDto ToDto() =>
|
public PrimeScheduleDto ToDto() =>
|
||||||
new(Id, DaysMask(), TimeOfDay, Enabled, LastRunAt, null);
|
new(Id, DaysMask(), TimeOfDay, Enabled, LastRunAt,
|
||||||
|
string.IsNullOrWhiteSpace(PromptOverride) ? null : PromptOverride);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -354,28 +354,35 @@
|
|||||||
<Border BorderBrush="{DynamicResource LineBrush}" BorderThickness="1"
|
<Border BorderBrush="{DynamicResource LineBrush}" BorderThickness="1"
|
||||||
CornerRadius="6" Padding="10,8" Margin="0,0,0,8"
|
CornerRadius="6" Padding="10,8" Margin="0,0,0,8"
|
||||||
Background="{DynamicResource DeepBrush}">
|
Background="{DynamicResource DeepBrush}">
|
||||||
<Grid ColumnDefinitions="Auto,*,Auto,Auto,Auto" ColumnSpacing="8">
|
<StackPanel Spacing="6">
|
||||||
<CheckBox Grid.Column="0" IsChecked="{Binding Enabled, Mode=TwoWay}" VerticalAlignment="Center"/>
|
<Grid ColumnDefinitions="Auto,*,Auto,Auto,Auto" ColumnSpacing="8">
|
||||||
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="4" VerticalAlignment="Center">
|
<CheckBox Grid.Column="0" IsChecked="{Binding Enabled, Mode=TwoWay}" VerticalAlignment="Center"/>
|
||||||
<ToggleButton Classes="day-toggle" Content="{loc:Tr settings.prime.dayMo}" IsChecked="{Binding Monday, Mode=TwoWay}"/>
|
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="4" VerticalAlignment="Center">
|
||||||
<ToggleButton Classes="day-toggle" Content="{loc:Tr settings.prime.dayTu}" IsChecked="{Binding Tuesday, Mode=TwoWay}"/>
|
<ToggleButton Classes="day-toggle" Content="{loc:Tr settings.prime.dayMo}" IsChecked="{Binding Monday, Mode=TwoWay}"/>
|
||||||
<ToggleButton Classes="day-toggle" Content="{loc:Tr settings.prime.dayWe}" IsChecked="{Binding Wednesday, Mode=TwoWay}"/>
|
<ToggleButton Classes="day-toggle" Content="{loc:Tr settings.prime.dayTu}" IsChecked="{Binding Tuesday, Mode=TwoWay}"/>
|
||||||
<ToggleButton Classes="day-toggle" Content="{loc:Tr settings.prime.dayTh}" IsChecked="{Binding Thursday, Mode=TwoWay}"/>
|
<ToggleButton Classes="day-toggle" Content="{loc:Tr settings.prime.dayWe}" IsChecked="{Binding Wednesday, Mode=TwoWay}"/>
|
||||||
<ToggleButton Classes="day-toggle" Content="{loc:Tr settings.prime.dayFr}" IsChecked="{Binding Friday, Mode=TwoWay}"/>
|
<ToggleButton Classes="day-toggle" Content="{loc:Tr settings.prime.dayTh}" IsChecked="{Binding Thursday, Mode=TwoWay}"/>
|
||||||
<ToggleButton Classes="day-toggle" Content="{loc:Tr settings.prime.daySa}" IsChecked="{Binding Saturday, Mode=TwoWay}"/>
|
<ToggleButton Classes="day-toggle" Content="{loc:Tr settings.prime.dayFr}" IsChecked="{Binding Friday, Mode=TwoWay}"/>
|
||||||
<ToggleButton Classes="day-toggle" Content="{loc:Tr settings.prime.daySu}" IsChecked="{Binding Sunday, Mode=TwoWay}"/>
|
<ToggleButton Classes="day-toggle" Content="{loc:Tr settings.prime.daySa}" IsChecked="{Binding Saturday, Mode=TwoWay}"/>
|
||||||
</StackPanel>
|
<ToggleButton Classes="day-toggle" Content="{loc:Tr settings.prime.daySu}" IsChecked="{Binding Sunday, Mode=TwoWay}"/>
|
||||||
<TextBox Grid.Column="2"
|
</StackPanel>
|
||||||
Text="{Binding TimeText, Mode=TwoWay}"
|
<TextBox Grid.Column="2"
|
||||||
PlaceholderText="HH:mm" MaxLength="5"
|
Text="{Binding TimeText, Mode=TwoWay}"
|
||||||
Width="68" VerticalAlignment="Center"/>
|
PlaceholderText="HH:mm" MaxLength="5"
|
||||||
<TextBlock Classes="meta" Grid.Column="3" Text="{Binding LastRunLabel}" VerticalAlignment="Center"
|
Width="68" VerticalAlignment="Center"/>
|
||||||
MinWidth="80"/>
|
<TextBlock Classes="meta" Grid.Column="3" Text="{Binding LastRunLabel}" VerticalAlignment="Center"
|
||||||
<Button Classes="icon-btn" Grid.Column="4" Content="✕"
|
MinWidth="80"/>
|
||||||
ToolTip.Tip="{loc:Tr settings.prime.removeScheduleTip}"
|
<Button Classes="icon-btn" Grid.Column="4" Content="✕"
|
||||||
Command="{Binding $parent[ItemsControl].((vm:SettingsModalViewModel)DataContext).Prime.RemoveScheduleCommand}"
|
ToolTip.Tip="{loc:Tr settings.prime.removeScheduleTip}"
|
||||||
CommandParameter="{Binding}"/>
|
Command="{Binding $parent[ItemsControl].((vm:SettingsModalViewModel)DataContext).Prime.RemoveScheduleCommand}"
|
||||||
</Grid>
|
CommandParameter="{Binding}"/>
|
||||||
|
</Grid>
|
||||||
|
<TextBlock Classes="field-label" Text="{loc:Tr settings.prime.promptOverrideLabel}"/>
|
||||||
|
<TextBox Text="{Binding PromptOverride, Mode=TwoWay}"
|
||||||
|
PlaceholderText="{loc:Tr settings.prime.promptOverridePlaceholder}"
|
||||||
|
AcceptsReturn="True" TextWrapping="Wrap"
|
||||||
|
MinHeight="48" MaxHeight="120"/>
|
||||||
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ItemsControl.ItemTemplate>
|
</ItemsControl.ItemTemplate>
|
||||||
|
|||||||
@@ -16,12 +16,18 @@ public static class DailyPrepPrompt
|
|||||||
"--allowedTools", CandidatesTool, SetMyDayTool,
|
"--allowedTools", CandidatesTool, SetMyDayTool,
|
||||||
];
|
];
|
||||||
|
|
||||||
public static string BuildPrompt(int maxTasks, DateOnly today) =>
|
public static string BuildPrompt(int maxTasks, DateOnly today, string? promptOverride = null)
|
||||||
ClaudeDo.Data.PromptFiles.Render(
|
{
|
||||||
|
var prompt = ClaudeDo.Data.PromptFiles.Render(
|
||||||
ClaudeDo.Data.PromptKind.DailyPrep,
|
ClaudeDo.Data.PromptKind.DailyPrep,
|
||||||
new Dictionary<string, string>
|
new Dictionary<string, string>
|
||||||
{
|
{
|
||||||
["date"] = today.ToString("yyyy-MM-dd"),
|
["date"] = today.ToString("yyyy-MM-dd"),
|
||||||
["maxTasks"] = maxTasks.ToString(),
|
["maxTasks"] = maxTasks.ToString(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Additive by design: a schedule's override is appended, never substituted, so a user
|
||||||
|
// can't accidentally break the mcp__claudedo__get_daily_prep_candidates/set_my_day flow.
|
||||||
|
return string.IsNullOrWhiteSpace(promptOverride) ? prompt : $"{prompt}\n\n{promptOverride}";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ public sealed class PrimeRunner : IPrimeRunner
|
|||||||
}
|
}
|
||||||
|
|
||||||
var today = DateOnly.FromDateTime(_clock.Now.LocalDateTime);
|
var today = DateOnly.FromDateTime(_clock.Now.LocalDateTime);
|
||||||
var prompt = DailyPrepPrompt.BuildPrompt(maxTasks, today);
|
var prompt = DailyPrepPrompt.BuildPrompt(maxTasks, today, schedule.PromptOverride);
|
||||||
var args = DailyPrepPrompt.BuildArgs(MaxTurns);
|
var args = DailyPrepPrompt.BuildArgs(MaxTurns);
|
||||||
|
|
||||||
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
using ClaudeDo.Data.Models;
|
||||||
|
using ClaudeDo.Data.Repositories;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Data.Tests;
|
||||||
|
|
||||||
|
public sealed class PrimeScheduleRepositoryTests : IDisposable
|
||||||
|
{
|
||||||
|
private readonly string _dbPath;
|
||||||
|
private readonly ClaudeDoDbContext _ctx;
|
||||||
|
private readonly PrimeScheduleRepository _repo;
|
||||||
|
|
||||||
|
public PrimeScheduleRepositoryTests()
|
||||||
|
{
|
||||||
|
_dbPath = Path.Combine(Path.GetTempPath(), $"claudedo_prime_{Guid.NewGuid():N}.db");
|
||||||
|
var options = new DbContextOptionsBuilder<ClaudeDoDbContext>()
|
||||||
|
.UseSqlite($"Data Source={_dbPath}")
|
||||||
|
.Options;
|
||||||
|
_ctx = new ClaudeDoDbContext(options);
|
||||||
|
_ctx.Database.EnsureCreated();
|
||||||
|
_repo = new PrimeScheduleRepository(_ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_ctx.Dispose();
|
||||||
|
foreach (var suffix in new[] { "", "-wal", "-shm" })
|
||||||
|
try { File.Delete(_dbPath + suffix); } catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpsertAsync_then_GetAsync_roundtrips_the_prompt_override()
|
||||||
|
{
|
||||||
|
var entity = new PrimeScheduleEntity { PromptOverride = "Also prioritize anything tagged #urgent." };
|
||||||
|
|
||||||
|
await _repo.UpsertAsync(entity);
|
||||||
|
var found = await _repo.GetAsync(entity.Id);
|
||||||
|
|
||||||
|
Assert.NotNull(found);
|
||||||
|
Assert.Equal("Also prioritize anything tagged #urgent.", found!.PromptOverride);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpsertAsync_with_null_override_persists_as_null()
|
||||||
|
{
|
||||||
|
var entity = new PrimeScheduleEntity { PromptOverride = null };
|
||||||
|
|
||||||
|
await _repo.UpsertAsync(entity);
|
||||||
|
var found = await _repo.GetAsync(entity.Id);
|
||||||
|
|
||||||
|
Assert.NotNull(found);
|
||||||
|
Assert.Null(found!.PromptOverride);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpsertAsync_updates_the_override_on_an_existing_row()
|
||||||
|
{
|
||||||
|
var entity = new PrimeScheduleEntity { PromptOverride = "old override" };
|
||||||
|
await _repo.UpsertAsync(entity);
|
||||||
|
|
||||||
|
entity.PromptOverride = "new override";
|
||||||
|
await _repo.UpsertAsync(entity);
|
||||||
|
|
||||||
|
var found = await _repo.GetAsync(entity.Id);
|
||||||
|
Assert.Equal("new override", found!.PromptOverride);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,43 @@ public class DailyPrepPromptTests
|
|||||||
Assert.Contains("preparing my workday", prompt);
|
Assert.Contains("preparing my workday", prompt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Build_prompt_without_override_matches_the_plain_render()
|
||||||
|
{
|
||||||
|
var today = new DateOnly(2026, 6, 3);
|
||||||
|
var expected = ClaudeDo.Data.PromptFiles.Render(
|
||||||
|
ClaudeDo.Data.PromptKind.DailyPrep,
|
||||||
|
new Dictionary<string, string> { ["date"] = "2026-06-03", ["maxTasks"] = "5" });
|
||||||
|
|
||||||
|
var prompt = DailyPrepPrompt.BuildPrompt(maxTasks: 5, today: today);
|
||||||
|
|
||||||
|
Assert.Equal(expected, prompt);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Build_prompt_appends_override_as_an_additional_paragraph()
|
||||||
|
{
|
||||||
|
var today = new DateOnly(2026, 6, 3);
|
||||||
|
var baseline = DailyPrepPrompt.BuildPrompt(maxTasks: 5, today: today);
|
||||||
|
|
||||||
|
var prompt = DailyPrepPrompt.BuildPrompt(maxTasks: 5, today: today,
|
||||||
|
promptOverride: "Also prioritize anything tagged #urgent.");
|
||||||
|
|
||||||
|
Assert.StartsWith(baseline, prompt);
|
||||||
|
Assert.Contains("Also prioritize anything tagged #urgent.", prompt);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Build_prompt_treats_whitespace_only_override_as_absent()
|
||||||
|
{
|
||||||
|
var today = new DateOnly(2026, 6, 3);
|
||||||
|
var baseline = DailyPrepPrompt.BuildPrompt(maxTasks: 5, today: today);
|
||||||
|
|
||||||
|
var prompt = DailyPrepPrompt.BuildPrompt(maxTasks: 5, today: today, promptOverride: " ");
|
||||||
|
|
||||||
|
Assert.Equal(baseline, prompt);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Build_args_allows_only_the_two_tools()
|
public void Build_args_allows_only_the_two_tools()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user