feat(prompts): add editable system/planning/agent prompt files
Introduces ~/.todo-app/prompts/{system,planning,agent}.md as the canonical
location for prompt content. The settings modal exposes "Open in editor"
shortcuts for each, and TaskRunner merges system.md (always) and agent.md
(for "agent"-tagged tasks) into the effective system prompt alongside the
existing global/list/task layers.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
58
src/ClaudeDo.Data/PromptFiles.cs
Normal file
58
src/ClaudeDo.Data/PromptFiles.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
namespace ClaudeDo.Data;
|
||||
|
||||
public enum PromptKind { System, Planning, Agent }
|
||||
|
||||
public static class PromptFiles
|
||||
{
|
||||
public static string Root => Path.Combine(Paths.AppDataRoot(), "prompts");
|
||||
|
||||
public static string PathFor(PromptKind kind) => kind switch
|
||||
{
|
||||
PromptKind.System => Path.Combine(Root, "system.md"),
|
||||
PromptKind.Planning => Path.Combine(Root, "planning.md"),
|
||||
PromptKind.Agent => Path.Combine(Root, "agent.md"),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(kind))
|
||||
};
|
||||
|
||||
public static void EnsureExists(PromptKind kind)
|
||||
{
|
||||
Directory.CreateDirectory(Root);
|
||||
var path = PathFor(kind);
|
||||
if (File.Exists(path)) return;
|
||||
File.WriteAllText(path, DefaultFor(kind));
|
||||
}
|
||||
|
||||
public static string? ReadOrNull(PromptKind kind)
|
||||
{
|
||||
var path = PathFor(kind);
|
||||
if (!File.Exists(path)) return null;
|
||||
var content = File.ReadAllText(path).Trim();
|
||||
return string.IsNullOrEmpty(content) ? null : content;
|
||||
}
|
||||
|
||||
private static string DefaultFor(PromptKind kind) => kind switch
|
||||
{
|
||||
PromptKind.System =>
|
||||
"# System Prompt\n\n" +
|
||||
"Baseline instructions appended to every task run.\n" +
|
||||
"Edit this file to inject project-wide rules (style, conventions, hard constraints).\n",
|
||||
PromptKind.Planning =>
|
||||
"You are a planning assistant for ClaudeDo.\n" +
|
||||
"Your role is to help break down a task into smaller, actionable subtasks.\n" +
|
||||
"Your final goal WILL ALWAYS be the creation of Subtasks.\n\n" +
|
||||
"ALWAYS invoke the `superpowers:brainstorming` skill via the Skill tool at the\n" +
|
||||
"start of every planning session, and follow its process end-to-end. It guides\n" +
|
||||
"you through clarifying questions, approach exploration, and design approval\n" +
|
||||
"BEFORE any subtasks are created. Do not create child tasks until the user has\n" +
|
||||
"approved a design.\n\n" +
|
||||
"NEVER change files yourself.\n\n" +
|
||||
"ALWAYS use the available MCP tools (mcp__claudedo__*) to create child tasks once\n" +
|
||||
"the design is approved. When you are done planning, finalize the session.\n\n" +
|
||||
"Be concise and focused. Each subtask should be independently executable.\n",
|
||||
PromptKind.Agent =>
|
||||
"# Agent Prompt\n\n" +
|
||||
"Appended to the system prompt for tasks tagged \"agent\" (auto-queued runs).\n" +
|
||||
"Use this for autonomous-execution rules that don't apply to manual runs.\n",
|
||||
_ => ""
|
||||
};
|
||||
}
|
||||
@@ -38,6 +38,10 @@ public sealed partial class SettingsModalViewModel : ViewModelBase
|
||||
public string LogsFolderPath { get; } = Path.Combine(Paths.AppDataRoot(), "logs");
|
||||
public string WorkerConfigPath { get; } = Path.Combine(Paths.AppDataRoot(), "worker.config.json");
|
||||
|
||||
public string SystemPromptPath { get; } = PromptFiles.PathFor(PromptKind.System);
|
||||
public string PlanningPromptPath { get; } = PromptFiles.PathFor(PromptKind.Planning);
|
||||
public string AgentPromptPath { get; } = PromptFiles.PathFor(PromptKind.Agent);
|
||||
|
||||
public Action? CloseAction { get; set; }
|
||||
|
||||
public SettingsModalViewModel(WorkerClient worker)
|
||||
@@ -199,4 +203,20 @@ public sealed partial class SettingsModalViewModel : ViewModelBase
|
||||
}
|
||||
catch { /* ignore */ }
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void OpenPrompt(string? kindName)
|
||||
{
|
||||
if (!Enum.TryParse<PromptKind>(kindName, ignoreCase: true, out var kind)) return;
|
||||
try
|
||||
{
|
||||
PromptFiles.EnsureExists(kind);
|
||||
var path = PromptFiles.PathFor(kind);
|
||||
Process.Start(new ProcessStartInfo(path) { UseShellExecute = true });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = $"Open failed: {ex.Message}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,6 +201,38 @@
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
<!-- PROMPTS -->
|
||||
<StackPanel Spacing="0">
|
||||
<TextBlock Classes="section-label" Text="PROMPTS"/>
|
||||
<Border Classes="section">
|
||||
<Grid RowDefinitions="Auto,Auto,Auto" ColumnDefinitions="90,*,Auto" RowSpacing="8">
|
||||
<TextBlock Grid.Row="0" Grid.Column="0" Classes="field-label" Text="System"
|
||||
VerticalAlignment="Center"/>
|
||||
<TextBlock Grid.Row="0" Grid.Column="1" Classes="path-mono"
|
||||
Text="{Binding SystemPromptPath}" VerticalAlignment="Center"/>
|
||||
<Button Grid.Row="0" Grid.Column="2" Content="Open in editor"
|
||||
Command="{Binding OpenPromptCommand}"
|
||||
CommandParameter="System"/>
|
||||
|
||||
<TextBlock Grid.Row="1" Grid.Column="0" Classes="field-label" Text="Planning"
|
||||
VerticalAlignment="Center"/>
|
||||
<TextBlock Grid.Row="1" Grid.Column="1" Classes="path-mono"
|
||||
Text="{Binding PlanningPromptPath}" VerticalAlignment="Center"/>
|
||||
<Button Grid.Row="1" Grid.Column="2" Content="Open in editor"
|
||||
Command="{Binding OpenPromptCommand}"
|
||||
CommandParameter="Planning"/>
|
||||
|
||||
<TextBlock Grid.Row="2" Grid.Column="0" Classes="field-label" Text="Agent"
|
||||
VerticalAlignment="Center"/>
|
||||
<TextBlock Grid.Row="2" Grid.Column="1" Classes="path-mono"
|
||||
Text="{Binding AgentPromptPath}" VerticalAlignment="Center"/>
|
||||
<Button Grid.Row="2" Grid.Column="2" Content="Open in editor"
|
||||
Command="{Binding OpenPromptCommand}"
|
||||
CommandParameter="Agent"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
<!-- ABOUT -->
|
||||
<StackPanel Spacing="0">
|
||||
<TextBlock Classes="section-label" Text="ABOUT"/>
|
||||
|
||||
@@ -404,14 +404,22 @@ public sealed class TaskRunner
|
||||
TaskEntity task, ListConfigEntity? listConfig, string? resumeSessionId, CancellationToken ct)
|
||||
{
|
||||
AppSettingsEntity global;
|
||||
bool isAgentTask;
|
||||
using (var ctx = _dbFactory.CreateDbContext())
|
||||
{
|
||||
var settingsRepo = new AppSettingsRepository(ctx);
|
||||
global = await settingsRepo.GetAsync(ct);
|
||||
|
||||
var taskRepo = new TaskRepository(ctx);
|
||||
var tags = await taskRepo.GetEffectiveTagsAsync(task.Id, ct);
|
||||
isAgentTask = tags.Any(t => string.Equals(t.Name, "agent", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
var systemFile = PromptFiles.ReadOrNull(PromptKind.System);
|
||||
var agentFile = isAgentTask ? PromptFiles.ReadOrNull(PromptKind.Agent) : null;
|
||||
|
||||
var instructions = MergeInstructions(
|
||||
global.DefaultClaudeInstructions, listConfig?.SystemPrompt, task.SystemPrompt);
|
||||
systemFile, global.DefaultClaudeInstructions, listConfig?.SystemPrompt, task.SystemPrompt, agentFile);
|
||||
|
||||
return new ClaudeRunConfig(
|
||||
Model: task.Model ?? listConfig?.Model ?? global.DefaultModel,
|
||||
@@ -422,12 +430,11 @@ public sealed class TaskRunner
|
||||
PermissionMode: global.DefaultPermissionMode);
|
||||
}
|
||||
|
||||
public static string MergeInstructions(string? global, string? list, string? task)
|
||||
public static string MergeInstructions(params string?[] parts)
|
||||
{
|
||||
var parts = new List<string>(3);
|
||||
if (!string.IsNullOrWhiteSpace(global)) parts.Add(global.Trim());
|
||||
if (!string.IsNullOrWhiteSpace(list)) parts.Add(list.Trim());
|
||||
if (!string.IsNullOrWhiteSpace(task)) parts.Add(task.Trim());
|
||||
return string.Join("\n\n", parts);
|
||||
var trimmed = parts
|
||||
.Where(p => !string.IsNullOrWhiteSpace(p))
|
||||
.Select(p => p!.Trim());
|
||||
return string.Join("\n\n", trimmed);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user