Files
agent-wiki-ui/Gui.cs
T

126 lines
5.4 KiB
C#
Raw Normal View History

using System.Diagnostics;
using System.Net;
using System.Reflection;
using System.Text.Json;
using System.Windows.Forms;
using Markdig;
using Microsoft.Web.WebView2.Core;
using Microsoft.Web.WebView2.WinForms;
// Host sottile: l'interfaccia vive in ui.html (embedded). Protocollo messaggi: docs/MVP.md.
class Gui : Form
{
record Model(string Label, string Id);
static readonly Model[] Models =
[
new("Fable 5.1", "claude-fable-5-1"),
new("Opus 5.5", "claude-opus-5-5"),
new("Sonnet 5.5", "claude-sonnet-5-5"),
new("Haiku 5.5", "claude-haiku-5-5"),
];
static readonly MarkdownPipeline Md = new MarkdownPipelineBuilder().UseAdvancedExtensions().DisableHtml().Build();
readonly WebView2 view = new()
{
Dock = DockStyle.Fill,
// evita il lampo bianco all'avvio in tema scuro (stesso --bg di ui.html)
DefaultBackgroundColor = Application.IsDarkModeEnabled ? Color.FromArgb(0x0F, 0x17, 0x2A) : Color.White,
};
string sessionId = Guid.NewGuid().ToString();
bool started; // true dopo il primo messaggio riuscito: da lì in poi --resume
bool busy;
public Gui()
{
Text = "LlmRun";
var area = Screen.PrimaryScreen!.WorkingArea;
StartPosition = FormStartPosition.CenterScreen;
// proporzioni 16:10 orizzontali: 75% della larghezza utile (max 1440), altezza max 90%
var w = Math.Min(1440, area.Width * 3 / 4);
var h = Math.Min(w * 10 / 16, area.Height * 9 / 10);
Size = new Size(h * 16 / 10, h);
MinimumSize = new Size(900, 600);
Controls.Add(view);
Load += async (_, _) =>
{
var dir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "LlmRun");
await view.EnsureCoreWebView2Async(await CoreWebView2Environment.CreateAsync(null, dir));
var web = view.CoreWebView2;
web.Settings.AreDevToolsEnabled = Debugger.IsAttached;
web.NavigationStarting += (_, e) => { if (IsWeb(e.Uri)) { e.Cancel = true; Open(e.Uri); } };
web.NewWindowRequested += (_, e) => { e.Handled = true; if (IsWeb(e.Uri)) Open(e.Uri); };
web.WebMessageReceived += (_, e) => OnMessage(e.WebMessageAsJson);
view.NavigateToString(LoadPage());
};
}
static bool IsWeb(string uri) => uri.StartsWith("http", StringComparison.OrdinalIgnoreCase);
static void Open(string uri) => Process.Start(new ProcessStartInfo(uri) { UseShellExecute = true });
static string LoadPage()
{
var asm = Assembly.GetExecutingAssembly();
using var s = asm.GetManifestResourceStream(asm.GetManifestResourceNames().First(n => n.EndsWith("ui.html")))!;
var html = new StreamReader(s).ReadToEnd();
var options = string.Concat(Models.Select(m => $"<option value=\"{m.Id}\">{WebUtility.HtmlEncode(m.Label)}</option>"));
return html.Replace("{{MODELS}}", "<option value=\"\">Predefinito</option>" + options);
}
void Post(object message) => view.CoreWebView2.PostWebMessageAsJson(JsonSerializer.Serialize(message));
void OnMessage(string json)
{
using var doc = JsonDocument.Parse(json);
var r = doc.RootElement;
switch (r.GetProperty("type").GetString())
{
case "reset":
sessionId = Guid.NewGuid().ToString(); started = false;
break;
case "send" when !busy:
_ = HandleSend(r.GetProperty("text").GetString()!, r.GetProperty("model").GetString()!, r.GetProperty("effort").GetString()!);
break;
}
}
async Task HandleSend(string text, string model, string effort)
{
busy = true;
try
{
var (code, stdout, stderr) = await RunClaude(text, model, effort);
if (code == 0) { started = true; Post(new { type = "ai", html = Markdown.ToHtml(stdout, Md), md = stdout }); }
else Post(new { type = "err", text = $"claude è uscito con codice {code}\n{stderr}".Trim() });
}
catch (System.ComponentModel.Win32Exception) { Post(new { type = "err", text = "'claude' non trovato nel PATH." }); }
catch (Exception ex) { Post(new { type = "err", text = ex.Message }); }
finally { busy = false; Post(new { type = "done" }); }
}
async Task<(int Code, string Out, string Err)> RunClaude(string prompt, string model, string effort)
{
var psi = new ProcessStartInfo("claude")
{
WorkingDirectory = AppContext.BaseDirectory,
UseShellExecute = false,
RedirectStandardInput = true, RedirectStandardOutput = true, RedirectStandardError = true,
CreateNoWindow = true,
};
psi.ArgumentList.Add("-p");
psi.ArgumentList.Add(prompt);
psi.ArgumentList.Add(started ? "--resume" : "--session-id");
psi.ArgumentList.Add(sessionId);
foreach (var (flag, value) in new[] { ("--model", model), ("--effort", effort) })
if (value != "") { psi.ArgumentList.Add(flag); psi.ArgumentList.Add(value); }
using var p = Process.Start(psi)!;
p.StandardInput.Close(); // evita l'attesa di stdin di claude
var o = p.StandardOutput.ReadToEndAsync();
var e = p.StandardError.ReadToEndAsync();
await p.WaitForExitAsync();
return (p.ExitCode, (await o).Trim(), (await e).Trim());
}
}