Sostituisce WinForms, WebView2 e ui.html con una GUI Avalonia (XAML, ViewModel, tema chiaro/scuro) e un renderer markdown che trasforma l'albero di Markdig in controlli. Un solo progetto net10.0 per Windows e Linux, con publish single-file compresso. L'avvio di claude è in un solo punto (Core/Claude.cs) condiviso da CLI e GUI. Il nome dell'app viene dall'AssemblyName; ChatStore migra la cartella dati LlmRun alla nuova. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
67 lines
2.6 KiB
C#
67 lines
2.6 KiB
C#
using System.Text.Json;
|
|
|
|
namespace AgentWikiUi.Core;
|
|
|
|
record Msg(string Role, string Text); // Role: user | ai | err
|
|
record Stored(string Id, string Title, DateTime Updated, bool Started, List<Msg> Messages);
|
|
|
|
// Cronologia: un JSON per chat, il nome file è l'id di sessione di `claude`.
|
|
static class ChatStore
|
|
{
|
|
// Nomi precedenti dell'app: le loro chat vengono spostate nella cartella attuale al primo avvio.
|
|
static readonly string[] LegacyNames = ["LlmRun"];
|
|
|
|
static readonly string Dir = Prepare();
|
|
|
|
static string Prepare()
|
|
{
|
|
var data = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
|
|
var dir = Path.Combine(data, AppInfo.Name, "chats");
|
|
if (Directory.Exists(dir)) return dir;
|
|
foreach (var name in LegacyNames)
|
|
{
|
|
var old = Path.Combine(data, name, "chats");
|
|
if (!Directory.Exists(old)) continue;
|
|
try { Directory.CreateDirectory(Path.GetDirectoryName(dir)!); Directory.Move(old, dir); }
|
|
catch (IOException) { } // se non si riesce a spostare, si riparte da una cartella vuota
|
|
break;
|
|
}
|
|
return dir;
|
|
}
|
|
|
|
// L'id può arrivare dalla UI e diventa un nome file: accettare solo Guid.
|
|
static string? PathFor(string id) => Guid.TryParse(id, out var g) ? Path.Combine(Dir, g.ToString() + ".json") : null;
|
|
|
|
public static void Save(string id, bool started, List<Msg> messages)
|
|
{
|
|
if (messages.Count == 0) return;
|
|
Directory.CreateDirectory(Dir);
|
|
var first = messages.First(m => m.Role == "user").Text.ReplaceLineEndings(" ").Trim();
|
|
var title = first.Length > 50 ? first[..50] + "…" : first;
|
|
File.WriteAllText(PathFor(id)!, JsonSerializer.Serialize(new Stored(id, title, DateTime.Now, started, messages)));
|
|
}
|
|
|
|
public static Stored? Load(string id)
|
|
{
|
|
var file = PathFor(id);
|
|
return file is not null && File.Exists(file) ? Read(file) : null;
|
|
}
|
|
|
|
// Dalla più recente; i file corrotti sono ignorati.
|
|
public static List<Stored> List() => Directory.Exists(Dir)
|
|
? Directory.EnumerateFiles(Dir, "*.json").Select(Read).OfType<Stored>().OrderByDescending(c => c.Updated).ToList()
|
|
: [];
|
|
|
|
public static void Delete(string id)
|
|
{
|
|
var file = PathFor(id);
|
|
if (file is not null && File.Exists(file)) File.Delete(file);
|
|
}
|
|
|
|
static Stored? Read(string file)
|
|
{
|
|
try { return JsonSerializer.Deserialize<Stored>(File.ReadAllText(file)); }
|
|
catch (Exception e) when (e is IOException or JsonException) { return null; }
|
|
}
|
|
}
|