Ship Assistent 0.11.3: variants grid, EN Krea prep, and stream fence fix.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Leonid Pershin
2026-08-22 04:58:03 +03:00
co-authored by Cursor
parent 036484c7b7
commit 56e089d8da
18 changed files with 10244 additions and 9218 deletions
+77 -10
View File
@@ -21,6 +21,7 @@ public partial class SwarmAssistentExtension
"clear_prompt_images", "slot_to_prompt_image", "pack", "memories", "memory",
"memory_query", "memory_kind", "tag_query", "user_prefs",
"inventory_query", "skills", "persona_shelves", "controls",
"variants",
];
static bool HasValue(JObject obj, string key)
@@ -107,8 +108,10 @@ public partial class SwarmAssistentExtension
}
/// <summary>
/// If the reply already contains a closed fenced patch/card, cut everything after it.
/// Models often keep writing («Готово!», second aspect, …) and the stream never feels done.
/// If the reply already contains a closed fenced patch/card that is "done enough" to act on,
/// cut everything after it. Do NOT stop on weak fences (pack/creativity/notes-only) — models
/// often emit a tiny JSON first then the real prompt fence; aborting early cuts the prompt
/// and blocks skill_load / generate.
/// </summary>
static bool TryTruncateAtCompleteFence(string reply, out string truncated)
{
@@ -129,18 +132,11 @@ public partial class SwarmAssistentExtension
try
{
JObject obj = JObject.Parse(raw);
if (obj is null)
{
continue;
}
bool usable = LooksLikeCardObject(obj)
|| Array.Exists(PatchKeys, k => obj[k] is not null);
if (!usable)
if (obj is null || !FenceIsTerminalPatch(obj))
{
continue;
}
truncated = reply.Substring(0, match.Index + match.Length).TrimEnd();
// Only treat as complete if the fence actually closed (regex requires ```).
return true;
}
catch
@@ -151,6 +147,77 @@ public partial class SwarmAssistentExtension
return false;
}
/// <summary>
/// True when a closed fence is worth aborting the Ollama stream (real deliverable or tool hop).
/// </summary>
static bool FenceIsTerminalPatch(JObject obj)
{
if (obj is null)
{
return false;
}
if (LooksLikeCardObject(obj))
{
return true;
}
if (obj["variants"] is JArray variants && variants.Count > 0)
{
return true;
}
if (HasValue(obj, "look_at") || HasValue(obj, "vision_from") || HasValue(obj, "vision_slots"))
{
return true;
}
if (HasValue(obj, "search_query") || HasValue(obj, "civitai_query"))
{
return true;
}
if (HasValue(obj, "memory_query") || HasValue(obj, "tag_query") || HasValue(obj, "inventory_query"))
{
return true;
}
if (obj["actions"] is JArray acts)
{
foreach (JToken a in acts)
{
string s = a?.ToString() ?? "";
if (string.IsNullOrWhiteSpace(s))
{
continue;
}
if (s.Equals("skill_load", StringComparison.OrdinalIgnoreCase)
|| s.Equals("persona_read", StringComparison.OrdinalIgnoreCase)
|| s.Equals("memory_get", StringComparison.OrdinalIgnoreCase)
|| s.Equals("memory_search", StringComparison.OrdinalIgnoreCase)
|| s.Equals("lookup_tags", StringComparison.OrdinalIgnoreCase)
|| s.Equals("list_inventory", StringComparison.OrdinalIgnoreCase)
|| s.Equals("search_civitai", StringComparison.OrdinalIgnoreCase)
|| s.Equals("interrupt", StringComparison.OrdinalIgnoreCase)
|| s.Equals("generate", StringComparison.OrdinalIgnoreCase)
|| s.Equals("memory_upsert", StringComparison.OrdinalIgnoreCase)
|| s.Equals("user_pref_upsert", StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
}
string prompt = obj["prompt"]?.ToString()?.Trim() ?? "";
if (prompt.Length >= 48)
{
return true;
}
// Real param change without prose notes
if (HasValue(obj, "loras") || HasValue(obj, "aspect") || HasValue(obj, "steps")
|| HasValue(obj, "width") || HasValue(obj, "height") || HasValue(obj, "cfg")
|| HasValue(obj, "seed") || HasValue(obj, "controls")
|| HasValue(obj, "memories") || HasValue(obj, "user_prefs"))
{
return true;
}
// Weak: pack / creativity / intensity / empty actions / notes-only → keep streaming
return false;
}
static string ExtractSearchQuery(JObject patch)
{
if (patch is null)