Cronologia chat: salvataggio su disco, barra laterale e ripresa all'avvio
Ogni chat è salvata come JSON in %LOCALAPPDATA%\LlmRun\chats (nome file = id di sessione di claude). La barra laterale elenca le chat, permette di riaprirle con --resume e di eliminarle. Il messaggio dell'utente è salvato subito, prima della risposta, e all'avvio si riapre l'ultima conversazione. Documentazione e roadmap aggiornate. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0d1784c0f4
commit
c83d69f435
5 files changed
+166
-22
No files matched your search
@@ -11,6 +11,8 @@ using Microsoft.Web.WebView2.WinForms;
|
||||
class Gui : Form
|
||||
{
|
||||
record Model(string Label, string Id);
|
||||
record Msg(string Role, string Text); // Role: user | ai | err
|
||||
record Stored(string Id, string Title, DateTime Updated, bool Started, List<Msg> Messages);
|
||||
static readonly Model[] Models =
|
||||
[
|
||||
new("Fable 5.1", "claude-fable-5-1"),
|
||||
@@ -28,9 +30,13 @@ class Gui : Form
|
||||
DefaultBackgroundColor = Application.IsDarkModeEnabled ? Color.FromArgb(0x0F, 0x17, 0x2A) : Color.White,
|
||||
};
|
||||
|
||||
static readonly string ChatsDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "LlmRun", "chats");
|
||||
|
||||
string sessionId = Guid.NewGuid().ToString();
|
||||
bool started; // true dopo il primo messaggio riuscito: da lì in poi --resume
|
||||
bool busy;
|
||||
bool restored; // la chat più recente si riapre solo al primo avvio
|
||||
List<Msg> messages = [];
|
||||
|
||||
public Gui()
|
||||
{
|
||||
@@ -77,8 +83,18 @@ class Gui : Form
|
||||
var r = doc.RootElement;
|
||||
switch (r.GetProperty("type").GetString())
|
||||
{
|
||||
case "reset":
|
||||
sessionId = Guid.NewGuid().ToString(); started = false;
|
||||
case "reset" when !busy:
|
||||
NewSession(); PostChats();
|
||||
break;
|
||||
case "list":
|
||||
if (!restored) { restored = true; if (LatestId() is { } last) { OpenChat(last); break; } }
|
||||
PostChats();
|
||||
break;
|
||||
case "open" when !busy:
|
||||
OpenChat(r.GetProperty("id").GetString()!);
|
||||
break;
|
||||
case "delete" when !busy:
|
||||
Delete(r.GetProperty("id").GetString()!);
|
||||
break;
|
||||
case "send" when !busy:
|
||||
_ = HandleSend(r.GetProperty("text").GetString()!, r.GetProperty("model").GetString()!, r.GetProperty("effort").GetString()!);
|
||||
@@ -89,15 +105,84 @@ class Gui : Form
|
||||
async Task HandleSend(string text, string model, string effort)
|
||||
{
|
||||
busy = true;
|
||||
messages.Add(new("user", text));
|
||||
try { Save(); PostChats(); } catch (IOException) { } // il messaggio non va perso se l'app si chiude durante l'attesa
|
||||
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() });
|
||||
if (code == 0) { started = true; messages.Add(new("ai", stdout)); Post(new { type = "ai", html = Markdown.ToHtml(stdout, Md), md = stdout }); }
|
||||
else Fail($"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" }); }
|
||||
catch (System.ComponentModel.Win32Exception) { Fail("'claude' non trovato nel PATH."); }
|
||||
catch (Exception ex) { Fail(ex.Message); }
|
||||
finally
|
||||
{
|
||||
busy = false;
|
||||
try { Save(); } catch (IOException) { } // la chat resta a schermo anche se il salvataggio fallisce
|
||||
PostChats();
|
||||
Post(new { type = "done" });
|
||||
}
|
||||
}
|
||||
|
||||
void Fail(string text) { messages.Add(new("err", text)); Post(new { type = "err", text }); }
|
||||
|
||||
void NewSession() { sessionId = Guid.NewGuid().ToString(); started = false; messages = []; }
|
||||
|
||||
// L'id arriva dalla pagina e diventa un nome file: accettare solo Guid.
|
||||
static string? PathFor(string id) => Guid.TryParse(id, out var g) ? Path.Combine(ChatsDir, g.ToString() + ".json") : null;
|
||||
|
||||
void Save()
|
||||
{
|
||||
if (messages.Count == 0) return;
|
||||
Directory.CreateDirectory(ChatsDir);
|
||||
var first = messages.First(m => m.Role == "user").Text.ReplaceLineEndings(" ").Trim();
|
||||
var title = first.Length > 50 ? first[..50] + "…" : first;
|
||||
File.WriteAllText(PathFor(sessionId)!, JsonSerializer.Serialize(new Stored(sessionId, title, DateTime.Now, started, messages)));
|
||||
}
|
||||
|
||||
static Stored? Read(string file)
|
||||
{
|
||||
try { return JsonSerializer.Deserialize<Stored>(File.ReadAllText(file)); }
|
||||
catch (Exception e) when (e is IOException or JsonException) { return null; } // file corrotto: ignorato
|
||||
}
|
||||
|
||||
static string? LatestId() => Directory.Exists(ChatsDir)
|
||||
? Directory.EnumerateFiles(ChatsDir, "*.json").Select(Read).OfType<Stored>().OrderByDescending(c => c.Updated).FirstOrDefault()?.Id
|
||||
: null;
|
||||
|
||||
void PostChats()
|
||||
{
|
||||
var items = Directory.Exists(ChatsDir)
|
||||
? Directory.EnumerateFiles(ChatsDir, "*.json").Select(Read).OfType<Stored>()
|
||||
.OrderByDescending(c => c.Updated).Select(c => new { id = c.Id, title = c.Title, updated = c.Updated }).ToArray()
|
||||
: [];
|
||||
Post(new { type = "chats", current = sessionId, items });
|
||||
}
|
||||
|
||||
void OpenChat(string id)
|
||||
{
|
||||
var file = PathFor(id);
|
||||
var chat = file is not null && File.Exists(file) ? Read(file) : null;
|
||||
if (chat is null) { PostChats(); return; }
|
||||
sessionId = chat.Id; started = chat.Started; messages = chat.Messages;
|
||||
Post(new
|
||||
{
|
||||
type = "load",
|
||||
messages = messages.Select(m => new { role = m.Role, text = m.Text, html = m.Role == "ai" ? Markdown.ToHtml(m.Text, Md) : null }).ToArray(),
|
||||
});
|
||||
PostChats();
|
||||
}
|
||||
|
||||
void Delete(string id)
|
||||
{
|
||||
var file = PathFor(id);
|
||||
if (file is not null && File.Exists(file)) File.Delete(file);
|
||||
if (id == sessionId)
|
||||
{
|
||||
NewSession();
|
||||
Post(new { type = "load", messages = Array.Empty<object>() });
|
||||
}
|
||||
PostChats();
|
||||
}
|
||||
|
||||
async Task<(int Code, string Out, string Err)> RunClaude(string prompt, string model, string effort)
|
||||
|
||||
Reference in new issue
Block a user