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) ──