Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/MandoCode.Desktop/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,12 @@ private static ServiceProvider BuildServices()
// ---- Coordinators + session registry ----
services.AddSingleton(provider => new ConfigCoordinator(provider.GetRequiredService<MandoCodeConfig>()));
services.AddSingleton(provider => new McpCoordinator(provider.GetRequiredService<MandoCodeConfig>()));
services.AddSingleton(provider => new SkillCoordinator(provider.GetRequiredService<MandoCodeConfig>()));
services.AddSingleton(provider => new SessionManager(
provider,
provider.GetRequiredService<ConfigCoordinator>(),
provider.GetRequiredService<McpCoordinator>(),
provider.GetRequiredService<SkillCoordinator>(),
projectRoot));

return services.BuildServiceProvider();
Expand Down
439 changes: 418 additions & 21 deletions src/MandoCode.Desktop/MainWindow.xaml

Large diffs are not rendered by default.

588 changes: 578 additions & 10 deletions src/MandoCode.Desktop/MainWindow.xaml.cs

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions src/MandoCode.Desktop/Services/SessionManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public sealed class SessionManager
private readonly IServiceProvider _globals;
private readonly ConfigCoordinator _configs;
private readonly McpCoordinator _mcp;
private readonly SkillCoordinator _skills;
private readonly string _initialProjectRoot;
private readonly List<AgentSession> _sessions = new();

Expand All @@ -28,15 +29,18 @@ public SessionManager(
IServiceProvider globals,
ConfigCoordinator configs,
McpCoordinator mcp,
SkillCoordinator skills,
string initialProjectRoot)
{
_globals = globals;
_configs = configs;
_mcp = mcp;
_skills = skills;
_initialProjectRoot = initialProjectRoot;

_configs.SessionsAccessor = () => _sessions;
_mcp.SessionsAccessor = () => _sessions;
_skills.SessionsAccessor = () => _sessions;
}

/// <summary>
Expand Down
121 changes: 121 additions & 0 deletions src/MandoCode.Desktop/Services/SkillAuthor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;
using Microsoft.SemanticKernel.Connectors.Ollama;

namespace MandoCode.Desktop.Services;

/// <summary>A model-drafted skill, split into the three fields the editor binds to.</summary>
public sealed record DraftSkill(string Name, string Description, string Body);

/// <summary>
/// One-shot LLM helper that drafts and refines skills for the Skills editor. Like
/// <see cref="SnapshotEnhancer"/>, it builds a bare Ollama kernel — no plugins, tools, filters, or
/// shared history — so authoring can never touch a live agent's conversation or fire a tool call.
/// The draft is returned to the editor for the user to review and edit before anything is saved;
/// nothing here writes to disk.
///
/// Generation asks the model for a complete SKILL.md (frontmatter + body) and splits it back into
/// name / description / body, so a generated skill is well-formed by construction.
/// </summary>
public static class SkillAuthor
{
private const string GenerateSystem =
"You write SKILL.md files for an AI coding assistant. A skill is a named, reusable set of " +
"instructions the assistant loads on demand when a user's request matches the skill's " +
"description. Given the user's intent, output ONE complete SKILL.md and NOTHING else: no " +
"commentary, no code fences. Format EXACTLY:\n" +
"---\n" +
"name: <short-kebab-case-name>\n" +
"description: <one sentence, written so the assistant can match it to a request>\n" +
"---\n" +
"<markdown body: the concrete steps/rules the assistant should follow when this skill loads>\n\n" +
"Keep the body focused and actionable — numbered steps or short sections. Don't restate the " +
"description. Don't invent tools or file paths that weren't implied by the intent.";

private const string RefineSystem =
"You improve the instructions body of a SKILL.md for an AI coding assistant. Apply the user's " +
"requested change to the instructions below and return ONLY the revised markdown body — no " +
"frontmatter, no commentary, no code fences. Keep what already works; change what was asked. " +
"Keep it concrete and actionable.";

/// <summary>Drafts a new skill from a one-line intent. Throws on connection/model failure.</summary>
public static async Task<DraftSkill> GenerateAsync(
string endpoint, string model, string intent, CancellationToken ct = default)
{
var raw = await CompleteAsync(endpoint, model, GenerateSystem, intent.Trim(), ct);
return ParseDraft(StripFences(raw));
}

/// <summary>Rewrites an existing instructions body against an instruction. Returns the new body.</summary>
public static async Task<string> RefineAsync(
string endpoint, string model, string currentBody, string instruction, CancellationToken ct = default)
{
var user = $"Requested change:\n{instruction.Trim()}\n\nCurrent instructions:\n{currentBody.Trim()}";
var raw = await CompleteAsync(endpoint, model, RefineSystem, user, ct);
return StripFences(raw).Trim();
}

private static async Task<string> CompleteAsync(
string endpoint, string model, string system, string user, CancellationToken ct)
{
var kernel = Kernel.CreateBuilder()
.AddOllamaChatCompletion(modelId: model, endpoint: new Uri(endpoint))
.Build();
var chat = kernel.GetRequiredService<IChatCompletionService>();

var history = new ChatHistory();
history.AddSystemMessage(system);
history.AddUserMessage(user);

// A touch of latitude helps phrasing without drifting off-spec.
var settings = new OllamaPromptExecutionSettings { Temperature = 0.4f };
var result = await chat.GetChatMessageContentAsync(history, settings, kernel, ct);
return result.Content?.Trim() ?? "";
}

/// <summary>Strips a wrapping ```fenced block if the model added one despite instructions.</summary>
private static string StripFences(string text)
{
var t = text.Trim();
if (!t.StartsWith("```")) return t;

var firstNewline = t.IndexOf('\n');
if (firstNewline < 0) return t;
t = t.Substring(firstNewline + 1); // drop the opening ```lang line
var lastFence = t.LastIndexOf("```", StringComparison.Ordinal);
if (lastFence >= 0) t = t.Substring(0, lastFence);
return t.Trim();
}

/// <summary>Splits a generated SKILL.md into name/description/body. Mirrors the engine's
/// frontmatter shape; on anything unexpected it degrades to "whole thing is the body" so the
/// user still gets something editable rather than an error.</summary>
private static DraftSkill ParseDraft(string raw)
{
var norm = raw.Replace("\r\n", "\n").Trim();
string name = "", description = "", body = norm;

if (norm.StartsWith("---\n"))
{
var after = norm.Substring(4);
var close = after.IndexOf("\n---", StringComparison.Ordinal);
if (close >= 0)
{
var frontmatter = after.Substring(0, close);
body = after.Substring(close + 4).TrimStart('\n');

foreach (var line in frontmatter.Split('\n'))
{
var idx = line.IndexOf(':');
if (idx <= 0) continue;
var key = line.Substring(0, idx).Trim().ToLowerInvariant();
var val = line.Substring(idx + 1).Trim().Trim('"', '\'');
if (key == "name") name = val;
else if (key == "description") description = val;
}
}
}

return new DraftSkill(name, description, body.Trim());
}
}
Loading
Loading