feat: Plan-Modus – Toggle im Chat zum Umschalten zwischen Plan/Ausführung

- Toggle-Button (🗺️) im Chat-Input-Bereich neben Explorer
- Plan-Modus wird per ChatRequest.planMode an Backend gesendet
- Backend: Write-Tools gefiltert (nur read_file, list_files, glob, grep,
  todo_write, memory_*, ask_user_choice bleiben), System-Prompt-Block
  ergänzt der Änderungen verbietet, tool_choice nie 'any' erzwungen
- ChainRecord.PlanMode: pro Chain gespeichert (informativ für History)
- Chain-Meta-Badge '📋 Plan-Modus' bei Plan-Chains
- Choice-Response erbt planMode vom vorherigen Chain
- CSS: .plan-mode-active Styling (Primary-Color Highlight)
- Placeholder wechselt zu 'Plannen...' wenn aktiv
- Plan-Modus wird bei Session-Wechsel zurückgesetzt
This commit is contained in:
Meleki committed 2026-03-07 12:07:39 +01:00
1 parent 1c11ffa3cd
commit 6d95f9aec2
13 files changed
+123 -13

No files matched your search

+25 -3
View File
@@ -50,6 +50,7 @@ public class AnthropicService
ReviewContext review = null,
FinalizerContext finalizer = null,
bool allowUserChoice = true,
bool planMode = false,
CancellationToken cancellationToken = default)
{
var sw = System.Diagnostics.Stopwatch.StartNew();
@@ -58,6 +59,20 @@ public class AnthropicService
int reviewRound = 0;
string lastStopReason = "";
// Plan-Modus: System-Prompt-Ergänzung
if (planMode && systemPrompt != null)
{
systemPrompt.Add(new { type = "text", text = @"## Plan-Modus aktiv
Der User hat den Plan-Modus aktiviert. Du DARFST KEINE Änderungen vornehmen:
- KEINE Dateien erstellen, bearbeiten oder löschen
- KEINE Shell-Befehle ausführen die Änderungen vornehmen (Commits, Installationen etc.)
- NUR lesen, analysieren, suchen und planen
- Nutze read_file, list_files, glob, grep und web_search um den Codebase zu verstehen
- Nutze ask_user_choice um Rückfragen zu stellen und Anforderungen zu klären
- Erstelle einen strukturierten Plan mit Aufgaben, Abhängigkeiten und Reihenfolge
- Der User wird den Plan-Modus deaktivieren wenn du mit der Umsetzung beginnen sollst" });
}
var chain = new ChainRecord
{
ChainId = chainId,
@@ -93,13 +108,19 @@ public class AnthropicService
{
var tools = new List<object>();
if (hasTools)
tools.AddRange(_toolService.GetToolDefinitions(includeFileTools: hasWorkspace, includeMemory: includeMemory, includeUserChoice: allowUserChoice));
{
var allTools = _toolService.GetToolDefinitions(includeFileTools: hasWorkspace, includeMemory: includeMemory, includeUserChoice: allowUserChoice);
if (planMode)
allTools = _toolService.FilterReadOnlyTools(allTools);
tools.AddRange(allTools);
}
if (hasWebSearch)
tools.Add(new { type = webSearchVersion, name = "web_search", max_uses = 5 });
payload["tools"] = tools;
// "any" erzwingt Tool-Nutzung, aber nur in der ersten Iteration,
// damit Claude nicht endlos Tools aufruft wenn die Aufgabe erledigt ist.
var effectiveChoice = (toolChoice == "any" && iterations == 1) ? "any" : "auto";
// Im Plan-Modus nie "any" erzwingen – Claude soll frei antworten können.
var effectiveChoice = (!planMode && toolChoice == "any" && iterations == 1) ? "any" : "auto";
payload["tool_choice"] = new { type = effectiveChoice };
}
@@ -252,7 +273,8 @@ public class AnthropicService
chainId,
durationMs = sw.ElapsedMilliseconds,
stopReason = finalStopReason,
streamErrorInfo
streamErrorInfo,
planMode
}, cancellationToken);
return;
}
+5 -3
View File
@@ -42,7 +42,7 @@ public class ChatService
/// <summary>
/// Baut den vollständigen ChatContext auf: Agent, Model, SystemPrompt, Chain, Workspace etc.
/// </summary>
public async Task<ChatContext> PrepareContext(string sessionId, string userId, int maxIterations = 100)
public async Task<ChatContext> PrepareContext(string sessionId, string userId, int maxIterations = 100, bool planMode = false)
{
var session = await _db.GetSession(sessionId);
if (session == null || (userId != null && session.UserId != userId) || string.IsNullOrEmpty(session.AgentId))
@@ -94,7 +94,8 @@ public class ChatService
Model = modelId,
SystemPrompt = systemPromptText,
ToolChoice = (hasWorkspace || includeMemory) ? toolChoice : null,
ToolNames = toolNames
ToolNames = toolNames,
PlanMode = planMode
});
await _db.UpdateSessionTimestamp(sessionId);
@@ -193,7 +194,8 @@ public class ChatService
Agent = agent,
Session = session,
Review = reviewContext,
Finalizer = finalizerContext
Finalizer = finalizerContext,
PlanMode = planMode
};
}
+3 -2
View File
@@ -179,6 +179,7 @@ public class DatabaseService
TryAddColumn(conn, "agents", "avatar_offset_y", "REAL NOT NULL DEFAULT 0");
TryAddColumn(conn, "sessions", "finalizer_agent_id", "TEXT");
TryAddColumn(conn, "agents", "allow_user_choice", "INTEGER NOT NULL DEFAULT 1");
TryAddColumn(conn, "chains", "plan_mode", "INTEGER NOT NULL DEFAULT 0");
conn.Execute(@"
CREATE TABLE IF NOT EXISTS agent_skills (
@@ -579,8 +580,8 @@ public class DatabaseService
using var conn = CreateConnection();
chain.CreatedAt = DateTime.UtcNow.ToString("o");
await conn.ExecuteAsync(@"
INSERT INTO chains (chain_id, session_id, archive_id, agent_id, model, system_prompt, tool_choice, tool_names, input_tokens, output_tokens, iterations, stop_reason, duration_ms, created_at)
VALUES (@ChainId, @SessionId, @ArchiveId, @AgentId, @Model, @SystemPrompt, @ToolChoice, @ToolNames, @InputTokens, @OutputTokens, @Iterations, @StopReason, @DurationMs, @CreatedAt)
INSERT INTO chains (chain_id, session_id, archive_id, agent_id, model, system_prompt, tool_choice, tool_names, input_tokens, output_tokens, iterations, stop_reason, duration_ms, plan_mode, created_at)
VALUES (@ChainId, @SessionId, @ArchiveId, @AgentId, @Model, @SystemPrompt, @ToolChoice, @ToolNames, @InputTokens, @OutputTokens, @Iterations, @StopReason, @DurationMs, @PlanMode, @CreatedAt)
", chain);
}
+28
View File
@@ -17,6 +17,34 @@ public class ToolService
{
public const string AskUserChoiceToolName = "ask_user_choice";
/// <summary>
/// Read-only Tools die im Plan-Modus verfügbar bleiben.
/// Write-Tools (write_file, edit_file, delete_file, execute) werden entfernt.
/// </summary>
private static readonly HashSet<string> ReadOnlyToolNames = new HashSet<string>
{
"read_file", "list_files", "glob", "grep", "todo_write",
"memory_read", "memory_write", "memory_list",
AskUserChoiceToolName
};
public object[] FilterReadOnlyTools(object[] tools)
{
var filtered = new List<object>();
foreach (var tool in tools)
{
var json = JsonSerializer.Serialize(tool);
var el = JsonSerializer.Deserialize<JsonElement>(json);
if (el.TryGetProperty("name", out var nameProp))
{
var name = nameProp.GetString() ?? "";
if (ReadOnlyToolNames.Contains(name))
filtered.Add(tool);
}
}
return filtered.ToArray();
}
public object[] GetToolDefinitions(bool includeFileTools = false, bool includeMemory = false, bool includeUserChoice = true)
{
var allTools = new List<object>();