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:
1 parent
1c11ffa3cd
commit
6d95f9aec2
13 files changed
+123
-13
No files matched your search
@@ -52,7 +52,19 @@ public static class ChatEndpoint
|
||||
var userId = req.HttpContext.GetUserId();
|
||||
var maxIterations = config.GetValue<int>("MaxIterations", 100);
|
||||
|
||||
var ctx = await chatService.PrepareContext(sessionId, userId, maxIterations);
|
||||
// Bei Choice-Response: planMode vom vorherigen Chain übernehmen
|
||||
var effectivePlanMode = body.PlanMode;
|
||||
if (isChoiceResponse && !effectivePlanMode)
|
||||
{
|
||||
var prevChainId = await chatService.FindChainWithToolCall(sessionId, body.ChoiceToolCallId);
|
||||
if (prevChainId != null)
|
||||
{
|
||||
var prevChain = await db.GetChain(prevChainId);
|
||||
if (prevChain != null) effectivePlanMode = prevChain.PlanMode;
|
||||
}
|
||||
}
|
||||
|
||||
var ctx = await chatService.PrepareContext(sessionId, userId, maxIterations, planMode: effectivePlanMode);
|
||||
if (ctx == null)
|
||||
{
|
||||
res.StatusCode = 400;
|
||||
@@ -132,7 +144,7 @@ public static class ChatEndpoint
|
||||
|
||||
try
|
||||
{
|
||||
await anthropic.StreamChat(res, ctx.ModelId, ctx.SystemPrompt, messages, ctx.ChainId, ctx.Workspace, sessionId, agentId: ctx.AgentId, useMemory: ctx.UseMemory, webSearchVersion: ctx.WebSearchVersion, maxIterations: ctx.MaxIterations, toolChoice: effectiveToolChoice, review: ctx.Review, finalizer: ctx.Finalizer, allowUserChoice: ctx.Agent?.AllowUserChoice ?? true, cancellationToken: linkedCts.Token);
|
||||
await anthropic.StreamChat(res, ctx.ModelId, ctx.SystemPrompt, messages, ctx.ChainId, ctx.Workspace, sessionId, agentId: ctx.AgentId, useMemory: ctx.UseMemory, webSearchVersion: ctx.WebSearchVersion, maxIterations: ctx.MaxIterations, toolChoice: effectiveToolChoice, review: ctx.Review, finalizer: ctx.Finalizer, allowUserChoice: ctx.Agent?.AllowUserChoice ?? true, planMode: ctx.PlanMode, cancellationToken: linkedCts.Token);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
@@ -19,5 +19,6 @@ public class ChainRecord
|
||||
public string ToolNames { get; set; }
|
||||
public string ArchiveId { get; set; }
|
||||
public string StreamErrorInfo { get; set; }
|
||||
public bool PlanMode { get; set; }
|
||||
public string CreatedAt { get; set; }
|
||||
}
|
||||
@@ -17,6 +17,7 @@ public class ChatContext
|
||||
public int MaxIterations { get; set; }
|
||||
public AgentRecord Agent { get; set; }
|
||||
public SessionRecord Session { get; set; }
|
||||
public bool PlanMode { get; set; }
|
||||
|
||||
// Review-Agent
|
||||
public ReviewContext Review { get; set; }
|
||||
|
||||
@@ -144,4 +144,10 @@ public class ChatRequest
|
||||
/// </summary>
|
||||
[JsonPropertyName("choiceResult")]
|
||||
public string ChoiceResult { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Wenn true: Plan-Modus aktiv. Nur Read-only Tools, keine Datei-Änderungen.
|
||||
/// </summary>
|
||||
[JsonPropertyName("planMode")]
|
||||
public bool PlanMode { get; set; }
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>();
|
||||
|
||||
@@ -378,6 +378,13 @@
|
||||
color: var(--dangerous);
|
||||
}
|
||||
|
||||
/* Plan Mode Toggle */
|
||||
#planModeBtn.plan-mode-active {
|
||||
color: var(--primary);
|
||||
background: color-mix(in srgb, var(--primary) 15%, transparent);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
/* Emoji Picker */
|
||||
|
||||
.emoji-picker {
|
||||
|
||||
@@ -322,6 +322,7 @@
|
||||
<input type="file" id="fileInput" accept="image/jpeg,image/png,image/gif,image/webp,application/pdf" multiple hidden>
|
||||
<button id="fileBtn" class="icon-btn" title="Datei anhängen"><i class="fa fa-paperclip"></i></button>
|
||||
<button id="explorerBtn" class="icon-btn" title="Workspace durchsuchen"><i class="fa fa-folder"></i></button>
|
||||
<button id="planModeBtn" class="icon-btn" title="Plan-Modus umschalten"><i class="fa fa-map"></i></button>
|
||||
<textarea id="input" placeholder="Nachricht..." rows="1"></textarea>
|
||||
<button id="emojiBtn" class="icon-btn" title="Emoji einfügen"><i class="fa fa-smile-o"></i></button>
|
||||
<button id="send" class="icon-btn"><i class="fa fa-paper-plane"></i></button>
|
||||
|
||||
+4
-2
@@ -41,11 +41,13 @@ async function apiMe() {
|
||||
|
||||
// ── API Calls ──
|
||||
|
||||
async function apiChat(content, sessionId, signal) {
|
||||
async function apiChat(content, sessionId, signal, planMode) {
|
||||
var payload = { content, sessionId };
|
||||
if (planMode) payload.planMode = true;
|
||||
return fetch('/api/chat', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ content, sessionId }),
|
||||
body: JSON.stringify(payload),
|
||||
signal
|
||||
});
|
||||
}
|
||||
|
||||
+19
-1
@@ -2,6 +2,7 @@
|
||||
|
||||
function buildMetaBadges(d) {
|
||||
const badges = [];
|
||||
if (d.planMode) badges.push('📋 Plan-Modus');
|
||||
badges.push(prettyModelName(d.model));
|
||||
badges.push('Tokens: ' + (d.inputTokens || 0) + ' / ' + (d.outputTokens || 0));
|
||||
if (d.cacheCreationTokens > 0 || d.cacheReadTokens > 0) {
|
||||
@@ -137,6 +138,23 @@ function handleStreamEvent(eventType, data) {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Plan Mode ──
|
||||
|
||||
let _planModeActive = false;
|
||||
|
||||
function togglePlanMode() {
|
||||
_planModeActive = !_planModeActive;
|
||||
var btn = $('#planModeBtn');
|
||||
if (btn) {
|
||||
btn.classList.toggle('plan-mode-active', _planModeActive);
|
||||
btn.title = _planModeActive ? 'Plan-Modus deaktivieren' : 'Plan-Modus aktivieren';
|
||||
}
|
||||
var input = $('#input');
|
||||
if (input) {
|
||||
input.placeholder = _planModeActive ? 'Plannen...' : 'Nachricht...';
|
||||
}
|
||||
}
|
||||
|
||||
// ── Chat State ──
|
||||
|
||||
let isStreaming = false;
|
||||
@@ -254,7 +272,7 @@ async function sendMessage() {
|
||||
_chatAbandoned = false;
|
||||
|
||||
try {
|
||||
const resp = await apiChat(content, getCurrentSessionId(), _abortController.signal);
|
||||
const resp = await apiChat(content, getCurrentSessionId(), _abortController.signal, _planModeActive);
|
||||
const reader = resp.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
|
||||
@@ -304,6 +304,15 @@ registerPage('chat', {
|
||||
});
|
||||
}
|
||||
|
||||
// Plan Mode Toggle
|
||||
const planModeBtn = document.getElementById('planModeBtn');
|
||||
if (planModeBtn) {
|
||||
// Plan-Modus bei Session-Wechsel zurücksetzen
|
||||
_planModeActive = false;
|
||||
planModeBtn.classList.remove('plan-mode-active');
|
||||
planModeBtn.addEventListener('click', togglePlanMode);
|
||||
}
|
||||
|
||||
// File Upload
|
||||
const fileBtn = document.getElementById('fileBtn');
|
||||
const fileInput = document.getElementById('fileInput');
|
||||
|
||||
Reference in new issue
Block a user