diff --git a/Endpoints/ChatEndpoint.cs b/Endpoints/ChatEndpoint.cs index 093b7cf..4f01a41 100644 --- a/Endpoints/ChatEndpoint.cs +++ b/Endpoints/ChatEndpoint.cs @@ -292,7 +292,7 @@ public static class ChatEndpoint if (anthropic.IsCliAvailable) { - await anthropic.StreamChatViaCli(res, ctx.ModelId, ctx.SystemPrompt, messages, ctx.ChainId, workspace: ctx.Workspace, allowUserChoice: ctx.Agent?.AllowUserChoice ?? true, planMode: body.PlanMode, webSearch: !string.IsNullOrEmpty(ctx.WebSearchVersion), sessionId: sessionId, agentId: ctx.AgentId, remoteMgr: remoteMgr, unityMcpUrl: ctx.Session?.UnityMcpUrl, unityMcpToken: ctx.Session?.UnityMcpToken, useAutoContext: ctx.Agent?.UseAutoContext ?? true, effort: ctx.Effort, seedUsage: panelUsage, workCt: workCt, sseCt: sseCt); + await anthropic.StreamChatViaCli(res, ctx.ModelId, ctx.SystemPrompt, messages, ctx.ChainId, workspace: ctx.Workspace, allowUserChoice: ctx.Agent?.AllowUserChoice ?? true, planMode: body.PlanMode, ultracode: body.Ultracode, webSearch: !string.IsNullOrEmpty(ctx.WebSearchVersion), sessionId: sessionId, agentId: ctx.AgentId, remoteMgr: remoteMgr, unityMcpUrl: ctx.Session?.UnityMcpUrl, unityMcpToken: ctx.Session?.UnityMcpToken, useAutoContext: ctx.Agent?.UseAutoContext ?? true, effort: ctx.Effort, seedUsage: panelUsage, workCt: workCt, sseCt: sseCt); } else { diff --git a/Models/ChatModels.cs b/Models/ChatModels.cs index 36e06e9..5d5c85b 100644 --- a/Models/ChatModels.cs +++ b/Models/ChatModels.cs @@ -150,4 +150,12 @@ public class ChatRequest /// [JsonPropertyName("planMode")] public bool PlanMode { get; set; } + + /// + /// Wenn true: Ultracode aktiv. Die Anfrage wird als nativer dynamischer Workflow + /// (mehrere Subagenten) gefahren – das Workflow-Tool wird freigeschaltet, Effort = xhigh. + /// Nur für den Claude-CLI-Pfad relevant (z.ai/GLM und der API-Pfad haben kein Workflow-Runtime). + /// + [JsonPropertyName("ultracode")] + public bool Ultracode { get; set; } } diff --git a/Services/AnthropicService.Cli.cs b/Services/AnthropicService.Cli.cs index ace7d37..88b7242 100644 --- a/Services/AnthropicService.Cli.cs +++ b/Services/AnthropicService.Cli.cs @@ -30,6 +30,7 @@ public partial class AnthropicService string workspace = null, bool allowUserChoice = false, bool planMode = false, + bool ultracode = false, bool webSearch = false, string sessionId = null, string agentId = null, @@ -301,6 +302,7 @@ public partial class AnthropicService workspace: workspace, hasWorkspace: !string.IsNullOrEmpty(workspace), planMode: planMode, + ultracode: ultracode, webSearch: webSearch, mcpConfigPath: mcpConfigPath, hasUnityMcp: !string.IsNullOrEmpty(unityMcpUrl), diff --git a/Services/ClaudeCliService.cs b/Services/ClaudeCliService.cs index 29e60a6..28083c0 100644 --- a/Services/ClaudeCliService.cs +++ b/Services/ClaudeCliService.cs @@ -214,6 +214,7 @@ public class ClaudeCliService string workspace = null, bool hasWorkspace = false, bool planMode = false, + bool ultracode = false, bool webSearch = false, string mcpConfigPath = null, bool hasUnityMcp = false, @@ -229,6 +230,12 @@ public class ClaudeCliService // read-only gedachter Member (oder ein regulärer Plan-Modus-User) trotzdem Dateien schreiben // könnte. 'plan' blockiert alle Schreib-/Edit-/Bash-Operationen auf Permission-Ebene. var permissionMode = planMode ? "plan" : "acceptEdits"; + + // Ultracode nutzt xhigh-Reasoning (das reale Äquivalent – "ultracode" selbst ist KEIN + // gültiger Effort-Wert der CLI, wird still ignoriert). Ein bereits gewähltes "max" wird + // nicht heruntergestuft. + var effortToUse = ultracode ? (effort == "max" ? "max" : "xhigh") : effort; + var args = new List { "-p", @@ -259,6 +266,16 @@ public class ClaudeCliService // Kein Workspace: Read für Temp-Dateien (z.B. gesendete Bilder) tools.Add("Read"); } + // Ultracode: die nativen dynamischen Workflows der Claude CLI zulassen. Ohne diese + // Freigabe lehnt die Permission-Engine das Workflow-Tool headless STILL ab (kein + // interaktiver Gate im -p-Modus) → der Workflow läuft nicht. Task = die Subagenten, + // die ein Workflow spawnt. Nur Claude (z.ai/GLM hat kein Workflow-Runtime), nicht im + // Plan-Modus (read-only). + if (ultracode && !planMode && !IsZaiModel(modelId)) + { + tools.Add("Workflow"); + tools.Add("Task"); + } if (webSearch) { // GLM/z.ai kann Anthropics server-seitige WebSearch/WebFetch nicht ausführen → @@ -337,7 +354,7 @@ public class ClaudeCliService args.Add(mcpConfigPath); } - return await RunCliStreamingAsync(args, prompt, onEvent, cancellationToken, effort, workspace, modelId); + return await RunCliStreamingAsync(args, prompt, onEvent, cancellationToken, effortToUse, workspace, modelId); } private async Task RunCliStreamingAsync( diff --git a/wwwroot/css/chat.css b/wwwroot/css/chat.css index 4b44583..27925e7 100644 --- a/wwwroot/css/chat.css +++ b/wwwroot/css/chat.css @@ -452,6 +452,11 @@ background: color-mix(in srgb, var(--primary) 28%, transparent); border-radius: var(--border-radius); } +#planModeBtn.ultracode-active { + color: var(--info); + background: color-mix(in srgb, var(--info) 18%, transparent); + border-radius: var(--border-radius); +} /* Emoji Picker */ diff --git a/wwwroot/js/api.js b/wwwroot/js/api.js index ebf2dd1..785345c 100644 --- a/wwwroot/js/api.js +++ b/wwwroot/js/api.js @@ -60,9 +60,10 @@ function apiMe() { return _get('/api/auth/me', null); } // ── Chat API ── -function apiChat(content, sessionId, signal, planMode) { +function apiChat(content, sessionId, signal, planMode, ultracode) { var payload = { content: content, sessionId: sessionId }; if (planMode) payload.planMode = true; + if (ultracode) payload.ultracode = true; return fetch('/api/chat', { method: 'POST', headers: _jsonHeaders, body: JSON.stringify(payload), signal: signal }); } diff --git a/wwwroot/js/chat.js b/wwwroot/js/chat.js index 6e0959c..68329bf 100644 --- a/wwwroot/js/chat.js +++ b/wwwroot/js/chat.js @@ -228,13 +228,14 @@ function handleStreamEvent(eventType, data) { // // _planModeActive bleibt als Compat-Variable (sendMessage + modals.js lesen sie direkt). -let _compositeMode = 'off'; // 'off' | 'plan' | 'fusion' | 'moa' +let _compositeMode = 'off'; // 'off' | 'plan' | 'ultracode' | 'fusion' | 'moa' let _compositeHasAgents = false; // Compat-Variablen – werden durch cycleCompositeMode / initCompositeMode synchron gehalten let _planModeActive = false; // true genau wenn _compositeMode === 'plan' +let _ultracodeActive = false; // true genau wenn _compositeMode === 'ultracode' (transient, pro Request) -const _COMPOSITE_ORDER = ['off', 'plan', 'fusion', 'moa']; +const _COMPOSITE_ORDER = ['off', 'plan', 'ultracode', 'fusion', 'moa']; // Serialisierte Modus-Persistenz. Schnelles Durchklicken (off→plan→fusion→moa) feuert sonst // ÜBERLAPPENDE PATCHes, deren Commit-Reihenfolge nicht garantiert ist → der zuletzt committete @@ -247,14 +248,19 @@ let _modePersist = Promise.resolve(); function _renderCompositeBtn() { var btn = $('#planModeBtn'); if (!btn) return; - btn.classList.remove('plan-mode-active', 'panel-mode-fusion', 'panel-mode-moa'); + btn.classList.remove('plan-mode-active', 'ultracode-active', 'panel-mode-fusion', 'panel-mode-moa'); var icon = btn.querySelector('i'); var input = $('#input'); if (_compositeMode === 'plan') { btn.classList.add('plan-mode-active'); if (icon) icon.className = 'fa fa-map'; - btn.title = 'Plan-Modus aktiv · klicken → Fusion'; + btn.title = 'Plan-Modus aktiv · klicken → Ultracode'; if (input) input.placeholder = 'Planen...'; + } else if (_compositeMode === 'ultracode') { + btn.classList.add('ultracode-active'); + if (icon) icon.className = 'fa fa-sitemap'; + btn.title = 'Ultracode aktiv (dynamischer Workflow) · klicken → Fusion'; + if (input) input.placeholder = 'Ultracode – als Workflow...'; } else if (_compositeMode === 'fusion') { btn.classList.add('panel-mode-fusion'); if (icon) icon.className = 'fa fa-users'; @@ -278,6 +284,7 @@ function initCompositeMode(panelMode, hasAgents) { _compositeHasAgents = !!hasAgents; _compositeMode = (panelMode === 'fusion' || panelMode === 'moa') ? panelMode : 'off'; _planModeActive = false; + _ultracodeActive = false; _renderCompositeBtn(); } @@ -287,12 +294,13 @@ async function cycleCompositeMode() { var idx = _COMPOSITE_ORDER.indexOf(_compositeMode); _compositeMode = _COMPOSITE_ORDER[(idx + 1) % _COMPOSITE_ORDER.length]; _planModeActive = (_compositeMode === 'plan'); + _ultracodeActive = (_compositeMode === 'ultracode'); _renderCompositeBtn(); - // Nur Fusion/MoA/Off brauchen einen Server-Call (Plan ist transient). Auf die serialisierte - // Kette legen, damit aufeinanderfolgende Klicks in Reihenfolge committen (letzter Klick gewinnt) - // und sendMessage das offene PATCH abwarten kann. - if (_compositeMode !== 'plan') { + // Nur Fusion/MoA/Off brauchen einen Server-Call (Plan + Ultracode sind transient pro Request). + // Auf die serialisierte Kette legen, damit aufeinanderfolgende Klicks in Reihenfolge committen + // (letzter Klick gewinnt) und sendMessage das offene PATCH abwarten kann. + if (_compositeMode !== 'plan' && _compositeMode !== 'ultracode') { var sid = getCurrentSessionId(); if (sid) { var targetMode = _compositeMode; @@ -310,6 +318,8 @@ async function cycleCompositeMode() { showToast('Modus: Aus', 'info'); } else if (_compositeMode === 'plan') { showToast('Plan-Modus aktiv', 'info'); + } else if (_compositeMode === 'ultracode') { + showToast('Ultracode aktiv – diese Anfrage läuft als dynamischer Workflow (mehr Tokens/Zeit)', 'info'); } else if (!_compositeHasAgents) { showToast('Panel: ' + (_compositeMode === 'moa' ? 'MoA' : 'Fusion') + ' aktiv – aber keine Panel-Agenten konfiguriert (im Session-Bearbeiten festlegen)', 'info'); } else { @@ -560,6 +570,10 @@ async function sendMessage() { msgText += `\nPlan-Modus ist aktiv. KEINE Dateien ändern, KEINE Shell-Befehle (außer read-only), KEINE Commits. Nur analysieren, Rückfragen stellen und Plan erstellen.`; } + if (_ultracodeActive) { + msgText += `\nUltracode ist aktiv: Bearbeite diese Anfrage als dynamischen Workflow – use a workflow. Fächere die Arbeit über mehrere Subagenten parallel auf, lass Befunde adversarial gegenprüfen und synthetisiere am Ende. Nutze dafür das Workflow-Tool.`; + } + let inlineImages = []; // Bilder für Inline-Anzeige if (pendingFiles.length > 0) { content = []; @@ -595,7 +609,7 @@ async function sendMessage() { // (z.B. MoA → nur 1 Runde, weil das Backend noch 'fusion' liest). await _modePersist; - await _runStream((signal) => apiChat(content, getCurrentSessionId(), signal, _planModeActive)); + await _runStream((signal) => apiChat(content, getCurrentSessionId(), signal, _planModeActive, _ultracodeActive)); } // ── Steering (Einwurf in die laufende Chain) ──