diff --git a/Endpoints/ChatEndpoint.cs b/Endpoints/ChatEndpoint.cs index 35543d3..04e2edb 100644 --- a/Endpoints/ChatEndpoint.cs +++ b/Endpoints/ChatEndpoint.cs @@ -52,7 +52,19 @@ public static class ChatEndpoint var userId = req.HttpContext.GetUserId(); var maxIterations = config.GetValue("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 { diff --git a/Models/ChainRecord.cs b/Models/ChainRecord.cs index 9785a3c..b8a15e5 100644 --- a/Models/ChainRecord.cs +++ b/Models/ChainRecord.cs @@ -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; } } diff --git a/Models/ChatContext.cs b/Models/ChatContext.cs index 2372a33..a87c406 100644 --- a/Models/ChatContext.cs +++ b/Models/ChatContext.cs @@ -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; } diff --git a/Models/ChatModels.cs b/Models/ChatModels.cs index a13526b..36e06e9 100644 --- a/Models/ChatModels.cs +++ b/Models/ChatModels.cs @@ -144,4 +144,10 @@ public class ChatRequest /// [JsonPropertyName("choiceResult")] public string ChoiceResult { get; set; } + + /// + /// Wenn true: Plan-Modus aktiv. Nur Read-only Tools, keine Datei-Änderungen. + /// + [JsonPropertyName("planMode")] + public bool PlanMode { get; set; } } diff --git a/Services/AnthropicService.cs b/Services/AnthropicService.cs index 22dffe4..c84afb0 100644 --- a/Services/AnthropicService.cs +++ b/Services/AnthropicService.cs @@ -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(); 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; } diff --git a/Services/ChatService.cs b/Services/ChatService.cs index 6cf44a1..39d64de 100644 --- a/Services/ChatService.cs +++ b/Services/ChatService.cs @@ -42,7 +42,7 @@ public class ChatService /// /// Baut den vollständigen ChatContext auf: Agent, Model, SystemPrompt, Chain, Workspace etc. /// - public async Task PrepareContext(string sessionId, string userId, int maxIterations = 100) + public async Task 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 }; } diff --git a/Services/DatabaseService.cs b/Services/DatabaseService.cs index 890f628..cff2cec 100644 --- a/Services/DatabaseService.cs +++ b/Services/DatabaseService.cs @@ -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); } diff --git a/Services/ToolService.cs b/Services/ToolService.cs index e138d53..6b17029 100644 --- a/Services/ToolService.cs +++ b/Services/ToolService.cs @@ -17,6 +17,34 @@ public class ToolService { public const string AskUserChoiceToolName = "ask_user_choice"; + /// + /// Read-only Tools die im Plan-Modus verfügbar bleiben. + /// Write-Tools (write_file, edit_file, delete_file, execute) werden entfernt. + /// + private static readonly HashSet ReadOnlyToolNames = new HashSet + { + "read_file", "list_files", "glob", "grep", "todo_write", + "memory_read", "memory_write", "memory_list", + AskUserChoiceToolName + }; + + public object[] FilterReadOnlyTools(object[] tools) + { + var filtered = new List(); + foreach (var tool in tools) + { + var json = JsonSerializer.Serialize(tool); + var el = JsonSerializer.Deserialize(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(); diff --git a/wwwroot/css/chat.css b/wwwroot/css/chat.css index 5269afa..41dfa2b 100644 --- a/wwwroot/css/chat.css +++ b/wwwroot/css/chat.css @@ -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 { diff --git a/wwwroot/index.html b/wwwroot/index.html index 8753801..ef5e69d 100644 --- a/wwwroot/index.html +++ b/wwwroot/index.html @@ -322,6 +322,7 @@ + diff --git a/wwwroot/js/api.js b/wwwroot/js/api.js index 437b242..6c82ed0 100644 --- a/wwwroot/js/api.js +++ b/wwwroot/js/api.js @@ -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 }); } diff --git a/wwwroot/js/chat.js b/wwwroot/js/chat.js index e6a9764..1f5a01b 100644 --- a/wwwroot/js/chat.js +++ b/wwwroot/js/chat.js @@ -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 = ''; diff --git a/wwwroot/js/pages/chat-page.js b/wwwroot/js/pages/chat-page.js index 5f73be7..65b26a0 100644 --- a/wwwroot/js/pages/chat-page.js +++ b/wwwroot/js/pages/chat-page.js @@ -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');