diff --git a/CLAUDE.md b/CLAUDE.md index bd538ba..324c819 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,37 +4,43 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Progetto -LlmRun: eseguibile C# (.NET 10, Windows) che invoca Claude Code (`claude -p`) da terminale. Senza argomenti apre una GUI WinForms, con argomenti funziona da CLI. Non è un repo git. +agent-wiki-ui (già `LlmRun`; il nome ha un'unica sorgente, vedi [docs/README.md](docs/README.md#nome-del-progetto)): eseguibile C# (.NET 10, Windows e Linux) che invoca Claude Code (`claude -p`) da terminale. Senza argomenti apre una GUI di chat Avalonia, con argomenti funziona da CLI. ## Workflow doc-first (obbligatorio) - Specifica e criteri di accettazione: [docs/MVP.md](docs/MVP.md). Fasi e stato: [docs/ROADMAP.md](docs/ROADMAP.md). Indice dei doc e regole (SSOT, ADR): [docs/README.md](docs/README.md); una decisione architetturale si scrive solo nel suo ADR, gli altri doc la richiamano con un link. Uso: [README.md](README.md). - Ad ogni richiesta di modifica: aggiornare prima i doc interessati (MVP.md, ROADMAP.md con le spunte, README.md se cambia l'uso; un nuovo ADR in `docs/adr/` per ogni scelta architetturale), poi il codice, poi questo file se cambiano comandi, design o vincoli. Nessuna modifica deve lasciare i doc disallineati. -> In corso: migrazione a GUI Avalonia multipiattaforma ([ADR-0001](docs/adr/0001-gui-multipiattaforma-avalonia.md), ROADMAP Fase 5). Finché non è conclusa, le sezioni sotto descrivono lo stato attuale (WinForms + WebView2). - ## Comandi -- Build: `dotnet build` (LlmRun.csproj, net10.0-windows, WinForms) +- Build: `dotnet build` (agent-wiki-ui.csproj, `net10.0`, Avalonia) - GUI: `dotnet run`; CLI: `dotnet run -- "prompt"` -- Publish single-file: `dotnet publish -c Release -r win-x64 --self-contained -p:PublishSingleFile=true` -- Test: non esistono ancora. +- Publish single-file (uno per sistema): `dotnet publish -c Release -r win-x64 --self-contained -p:PublishSingleFile=true` e `-r linux-x64` +- Test: non esistono ancora. Per verificare la GUI senza schermo si può usare `Avalonia.Headless` con `CaptureRenderedFrame()` in un progetto di prova fuori dal repo. ## Architettura -- [Program.cs](Program.cs): top-level statements. Senza argomenti avvia `Gui`; con argomenti è la CLI (`claude -p ` con `ArgumentList`, stdout/stderr ereditati, exit code propagato, 127 se `claude` non è nel PATH). -- [Gui.cs](Gui.cs): host WinForms sottile. Un solo WebView2 che carica [ui.html](ui.html) (embedded resource; `{{MODELS}}` è sostituito con le opzioni dei modelli). Pagina e C# dialogano con `WebMessage` JSON (protocollo in `docs/MVP.md`). Per ogni `send` C# lancia `claude -p` con stdout/stderr rediretti, converte la risposta da markdown a HTML con Markdig (`DisableHtml`) e la rimanda alla pagina. Contesto: `--session-id ` al primo messaggio, `--resume ` poi. Cronologia: un JSON per chat in `%LOCALAPPDATA%\LlmRun\chats\.json` (il nome file è l'id di sessione, validato come Guid prima dell'uso), salvato appena l'utente invia e di nuovo alla risposta; all'avvio (primo `list` dalla pagina) viene riaperta la chat più recente. `--model`/`--effort` omessi se "Predefinito". Lista modelli hardcoded in `Gui.Models`, da aggiornare quando escono nuovi modelli. -- [ui.html](ui.html): tutta la UI (HTML+CSS+JS, nessuna libreria). Token di colore/tipografia come variabili CSS in cima; specifiche in `docs/MVP.md` → "Design (UI/UX)". Cambiando l'aspetto aggiornare quella sezione. -- Dipendenze NuGet (solo GUI): Markdig, Microsoft.Web.WebView2. +Decisioni e motivazioni negli ADR ([docs/adr/](docs/adr/)); qui solo la mappa del codice. + +- [Program.cs](Program.cs): top-level statements. Con argomenti è la CLI (`Claude.RunInherited`); senza avvia Avalonia (`App`). +- [Core/Claude.cs](Core/Claude.cs): **unico punto di avvio di `claude`** (cartella dell'exe, stdin chiuso, `ArgumentList`, `--session-id`/`--resume`, `--model`/`--effort` omessi se vuoti). Contiene anche l'elenco modelli `Catalog.Models`, da aggiornare quando escono nuovi modelli. +- [Core/ChatStore.cs](Core/ChatStore.cs): cronologia, un JSON per chat in `//chats/.json` (nome file = id di sessione, validato come Guid prima dell'uso). +- [Core/AppInfo.cs](Core/AppInfo.cs): nome dell'app, letto dall'`AssemblyName` del csproj (unica sorgente). +- [Gui/MainViewModel.cs](Gui/MainViewModel.cs): stato e logica della chat (invio, apertura/eliminazione, salvataggio subito all'invio e di nuovo alla risposta, riapertura dell'ultima chat all'avvio). +- [Gui/MainWindow.axaml](Gui/MainWindow.axaml) (+ `.cs`): layout XAML con binding compilati. [Gui/App.axaml](Gui/App.axaml): token di colore come risorse di tema chiaro/scuro e stili; specifiche in `docs/MVP.md` → "Design (UI/UX)". Cambiando l'aspetto aggiornare quella sezione. +- [Gui/MarkdownView.cs](Gui/MarkdownView.cs): renderer markdown proprio (albero Markdig → controlli Avalonia, `DisableHtml`). +- Dipendenze NuGet: Avalonia (+ Desktop, Themes.Fluent), Markdig. ## Vincoli non ovvi - La working directory di `claude` deve essere la cartella dell'exe: usare `AppContext.BaseDirectory`, mai `Environment.CurrentDirectory` né `Assembly.Location` (vuoto in single-file). - Lo stdin di `claude` va redirezionato e chiuso subito, altrimenti aspetta 3s. -- `claude` è un .exe nativo (`~/.local/bin`), avviabile direttamente. Se fosse `claude.cmd` serve `cmd.exe /c`. -- La logica di avvio processo è duplicata tra CLI (`Program.cs`) e GUI (`Gui.cs`): se cambia (cartella di lavoro, stdin, flag), modificarla in entrambi i punti. -- Nella GUI i link `http(s)` non devono navigare nella finestra: si aprono nel browser di sistema (`NavigationStarting`/`NewWindowRequested`). -- La GUI deve girare su un thread STA (creato a mano in `Program.cs`, perché i top-level statements non hanno `[STAThread]`): senza, WebView2 fallisce con `RPC_E_CHANGED_MODE`. +- `claude` è un eseguibile nativo (`~/.local/bin`), avviabile direttamente. Se fosse `claude.cmd` (Windows) serve `cmd.exe /c`. +- Il nome dell'app non va scritto a mano nel codice: usare `AppInfo.Name`. Cambiare nome cambia la cartella dati (vedi checklist in docs/README.md; `ChatStore` migra le cartelle dei nomi precedenti). +- Avalonia 12: la clipboard si usa con `using Avalonia.Input.Platform;` (`SetTextAsync` è un'estensione); `TextBox.Watermark` è obsoleto, usare `PlaceholderText`. +- Nel renderer markdown i colori si risolvono al momento del rendering (`TryFindResource` con `ActualThemeVariant`) e la vista si ricostruisce al cambio di tema. +- I link nel markdown si aprono con hit-test sul testo (`TextLayout.HitTestPoint`) solo se `http(s)` e se non c'è una selezione in corso. +- Il publish single-file richiede `IncludeNativeLibrariesForSelfExtract` (nel csproj): Skia e HarfBuzz sono librerie native. ## Skill del repo diff --git a/Gui.cs b/Gui.cs deleted file mode 100644 index 045f399..0000000 --- a/Gui.cs +++ /dev/null @@ -1,211 +0,0 @@ -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); - record Msg(string Role, string Text); // Role: user | ai | err - record Stored(string Id, string Title, DateTime Updated, bool Started, List Messages); - 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, - }; - - 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 messages = []; - - 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 => $"")); - return html.Replace("{{MODELS}}", "" + 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" 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()!); - break; - } - } - - 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; 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) { 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(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().OrderByDescending(c => c.Updated).FirstOrDefault()?.Id - : null; - - void PostChats() - { - var items = Directory.Exists(ChatsDir) - ? Directory.EnumerateFiles(ChatsDir, "*.json").Select(Read).OfType() - .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() }); - } - PostChats(); - } - - 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()); - } -} diff --git a/README.md b/README.md index 67d60a2..85fc115 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,33 @@ -# LlmRun +# agent-wiki-ui -Eseguibile C# (.NET 10, Windows) che lancia Claude Code (`claude -p`) nella cartella in cui risiede l'eseguibile. +Eseguibile C# (.NET 10, Windows e Linux) che lancia Claude Code (`claude -p`) nella cartella in cui risiede l'eseguibile. Senza argomenti apre una GUI di chat (Avalonia), con argomenti funziona da riga di comando. ## Requisiti -- .NET SDK 10 -- `claude` (Claude Code) nel PATH -- Runtime WebView2 (incluso in Windows 11) per la GUI +- `claude` (Claude Code) installato e nel PATH, già autenticato +- Per sviluppare: .NET SDK 10 +- Per usare l'eseguibile pubblicato: nessun runtime (è self-contained). Su Linux serve un desktop con X11 o XWayland per la GUI. ## Uso -GUI (nessun argomento): finestra di chat in stile claude.ai, con la conversazione in markdown in alto e il campo messaggio in basso (`Invio` invia, `Maiusc+Invio` va a capo, "Nuova chat" azzera il contesto). +GUI (nessun argomento): finestra di chat in stile claude.ai, con la conversazione in markdown in alto e il campo messaggio in basso (`Invio` invia, `Maiusc+Invio` va a capo, "Nuova chat" azzera il contesto). La barra in alto del composer permette di scegliere **modello** ed **effort** di ragionamento ("Predefinito" = nessun flag, vale la config di `claude`). Le chat si salvano su disco e l'ultima si riapre all'avvio. ``` dotnet run ``` -Nella GUI, la barra in alto permette di scegliere **modello** (con versione) ed **effort** di ragionamento ("predefinito" = nessun flag, vale la config di `claude`). - -CLI (con argomenti): il prompt viene stampato su terminale, l'exit code è quello di `claude`. +CLI (con argomenti): il prompt viene stampato su terminale, l'exit code è quello di `claude` (127 se `claude` non è nel PATH). ``` dotnet run -- "spiega questo progetto" ``` -Eseguibile già compilato: -``` -dotnet build -bin\Debug\net10.0-windows\LlmRun.exe -``` - ## Publish single-file +Un eseguibile per sistema, senza dipendenze: ``` dotnet publish -c Release -r win-x64 --self-contained -p:PublishSingleFile=true +dotnet publish -c Release -r linux-x64 --self-contained -p:PublishSingleFile=true ``` +Il risultato è in `bin/Release/net10.0//publish/` (`agent-wiki-ui.exe` o `agent-wiki-ui`, circa 48 MB). Al primo avvio le librerie native vengono estratte in una cartella temporanea. -> **Piattaforme**: oggi solo Windows. È pianificato un eseguibile standalone per Windows e Linux con GUI in solo C# (Avalonia): vedi [ADR-0001](docs/adr/0001-gui-multipiattaforma-avalonia.md). La CLI è già portabile, la GUI attuale no. Pianificato anche l'uso di Codex CLI e Antigravity CLI già installati da terminale, oltre a Claude Code: [ADR-0002](docs/adr/0002-motori-cli-claude-codex-antigravity.md). +## Piattaforme +Windows e Linux con lo stesso codice ([ADR-0001](docs/adr/0001-gui-multipiattaforma-avalonia.md)). Pianificato l'uso di Codex CLI e Antigravity CLI già installati da terminale, oltre a Claude Code: [ADR-0002](docs/adr/0002-motori-cli-claude-codex-antigravity.md). -Documentazione: [docs/MVP.md](docs/MVP.md), [docs/ROADMAP.md](docs/ROADMAP.md), [indice e decisioni architetturali](docs/README.md). +Documentazione: [docs/README.md](docs/README.md) (indice, regole, decisioni), [docs/MVP.md](docs/MVP.md), [docs/ROADMAP.md](docs/ROADMAP.md). diff --git a/LlmRun.csproj b/agent-wiki-ui.csproj similarity index 100% rename from LlmRun.csproj rename to agent-wiki-ui.csproj diff --git a/docs/MVP.md b/docs/MVP.md index 2e0dcfb..ab2088e 100644 --- a/docs/MVP.md +++ b/docs/MVP.md @@ -17,45 +17,41 @@ Tutti gli argomenti sono uniti in un unico prompt. 5. Se `claude` non è trovato: messaggio chiaro ed exit code 127. ## GUI (chat) -> **Stato**: questa sezione descrive la GUI attuale (WinForms + WebView2 + `ui.html`, solo Windows). La destinazione è una GUI Avalonia in solo C# per Windows e Linux: vedi [ADR-0001](adr/0001-gui-multipiattaforma-avalonia.md) e [ROADMAP.md](ROADMAP.md), Fase 5. Il comportamento funzionale (chat, sessioni, cronologia, selettori) resta valido; cambiano tecnologia, protocollo pagina↔C# e percorso della cronologia. +Senza argomenti si apre una finestra Avalonia (C# + XAML, nessun HTML) in stile claude.ai/ChatGPT, identica su Windows e Linux. Con argomenti resta la CLI. Decisione e motivazioni: [ADR-0001](adr/0001-gui-multipiattaforma-avalonia.md). -Senza argomenti si apre una finestra WinForms che contiene solo un WebView2: l'intera interfaccia (header, conversazione, composer) è HTML/CSS in `ui.html`, in stile claude.ai/ChatGPT. Con argomenti resta la CLI. - -**Struttura della pagina** -- **Barra laterale** (sinistra, comprimibile): logo e nome, pulsante **Nuova chat**, elenco delle chat salvate (titolo = inizio del primo messaggio, data/ora dell'ultimo scambio; la chat aperta è evidenziata). Un clic su una chat la riapre; la ✕ (con conferma) la elimina. Elenco vuoto: "Nessuna chat salvata". -- **Conversazione**: messaggi alternati utente/Claude, aggiunti al DOM uno alla volta (nessun ricaricamento, nessuno sfarfallio). Le risposte sono renderizzate in markdown (Markdig, lato C#): titoli, liste, tabelle, citazioni, blocchi di codice. Sulle risposte il pulsante **Copia** copia il markdown; sui blocchi di codice copia il codice. +**Struttura della finestra** +- **Barra laterale** (sinistra, comprimibile con il pulsante a tre linee): logo e nome dell'app, pulsante **Nuova chat**, elenco delle chat salvate (titolo = inizio del primo messaggio, data/ora dell'ultimo scambio; la chat aperta è evidenziata). Un clic su una chat la riapre; la ✕ (con conferma) la elimina. Elenco vuoto: "Nessuna chat salvata". +- **Conversazione**: messaggi alternati utente/Claude. Le risposte sono renderizzate in markdown: titoli, liste, tabelle, citazioni, blocchi di codice, grassetto/corsivo/barrato, codice inline, link. Il rendering è un renderer proprio ([MarkdownView.cs](../Gui/MarkdownView.cs)) che trasforma l'albero di Markdig in controlli Avalonia; l'HTML grezzo è disabilitato. Il testo è selezionabile. Sulle risposte il pulsante **Copia** copia il markdown; sui blocchi di codice copia il codice. - **Schermata iniziale** (chat vuota): titolo, sottotitolo e quattro suggerimenti cliccabili che precompilano il campo. -- **Composer** (in basso, scheda arrotondata): textarea che cresce fino a ~8 righe, selettori **Modello** (Predefinito, Fable 5.1, Opus 5.5, Sonnet 5.5, Haiku 5.5, "Altro ID…" con campo libero) ed **Effort** (Predefinito, low, medium, high, xhigh, max), pulsante Invia circolare con icona. `Invio` invia, `Maiusc+Invio` va a capo. Hint sulla tastiera sotto la scheda. +- **Composer** (in basso, scheda arrotondata): campo di testo che cresce fino a ~8 righe, selettori **Modello** (Predefinito, Fable 5.1, Opus 5.5, Sonnet 5.5, Haiku 5.5, "Altro ID…" con campo libero) ed **Effort** (Predefinito, low, medium, high, xhigh, max), pulsante Invia circolare con icona. `Invio` invia, `Maiusc+Invio` va a capo. Hint sulla tastiera sotto la scheda. - Passati come `--model` / `--effort`; con "Predefinito" il flag viene omesso. Solo GUI. - **Contesto**: `claude -p --session-id ` al primo messaggio, `--resume ` ai successivi. "Nuova chat" svuota la vista e genera un nuovo guid. -- **Cronologia**: dopo ogni scambio la chat è salvata in `\\chats\.json` (oggi `%LOCALAPPDATA%`; vedi [Nome del progetto](README.md#nome-del-progetto)) (guid = id di sessione di `claude`, titolo, data, flag `started`, messaggi con ruolo e testo). Riaprendo una chat si rimostra la conversazione e si prosegue con `--resume `, quindi `claude` ricorda il contesto. Il messaggio dell'utente viene salvato subito, prima della risposta (così non si perde se l'app si chiude mentre Claude risponde); la risposta si aggiunge appena arriva. Una chat nuova e vuota non viene salvata. **All'avvio l'app riapre automaticamente l'ultima conversazione** (la più recente); se non ce ne sono parte con la schermata iniziale. "Nuova chat" resta sempre disponibile. Eliminare una chat cancella solo il file JSON, non la sessione di `claude`. Non si può aprire/eliminare una chat mentre Claude sta rispondendo. +- **Cronologia**: dopo ogni scambio la chat è salvata in `//chats/.json`, dove la cartella dati è `Environment.SpecialFolder.LocalApplicationData` (`%LOCALAPPDATA%` su Windows, `~/.local/share` su Linux; vedi [Nome del progetto](README.md#nome-del-progetto)). Il file contiene guid (= id di sessione di `claude`), titolo, data, flag `started`, messaggi con ruolo e testo. Riaprendo una chat si rimostra la conversazione e si prosegue con `--resume `, quindi `claude` ricorda il contesto. Il messaggio dell'utente viene salvato subito, prima della risposta (così non si perde se l'app si chiude mentre Claude risponde); la risposta si aggiunge appena arriva. Una chat nuova e vuota non viene salvata. **All'avvio l'app riapre automaticamente l'ultima conversazione** (la più recente); se non ce ne sono parte con la schermata iniziale. "Nuova chat" resta sempre disponibile. Eliminare una chat cancella solo il file JSON, non la sessione di `claude`. Non si può aprire/eliminare una chat mentre Claude sta rispondendo. - **Limite noto**: la sessione di `claude` è legata alla cartella in cui gira (quella dell'exe): se si sposta l'exe, le vecchie chat si leggono ancora ma `--resume` non ritrova il contesto. -- La risposta compare a fine generazione (nessuno streaming); durante l'attesa un indicatore a puntini animati e Invia disabilitato. Se `claude` esce con errore, lo stderr appare come messaggio di errore. -- I link si aprono nel browser di sistema. L'HTML grezzo nel markdown è disabilitato. -- Finestra orizzontale con proporzioni 16:10: larga il 75% dell'area utile dello schermo (max 1440px), altezza di conseguenza (max 90% dell'area utile), centrata; minimo 900x600. Su un monitor Full HD 1920x1080 risulta circa 1440x900. - -**Protocollo pagina ↔ C#** (`WebMessage` JSON). Pagina→C#: `{type:"send", text, model, effort}`, `{type:"reset"}`, `{type:"list"}`, `{type:"open", id}`, `{type:"delete", id}`. C#→pagina: `{type:"ai", html, md}`, `{type:"err", text}`, `{type:"done"}`, `{type:"chats", current, items:[{id,title,updated}]}`, `{type:"load", messages:[{role,text,html?}]}` (sostituisce la conversazione mostrata). +- La risposta compare a fine generazione (nessuno streaming); durante l'attesa un indicatore di avanzamento e Invia disabilitato. Se `claude` esce con errore, lo stderr appare come messaggio di errore. +- I link `http(s)` si aprono nel browser di sistema. +- Finestra orizzontale con proporzioni 16:10: larga il 75% dell'area utile dello schermo (max 1440 px logici), altezza di conseguenza (max 90% dell'area utile), centrata; minimo 900x600. Su un monitor Full HD 1920x1080 risulta circa 1440x900. ### Design (UI/UX) -Sorgente di verità per l'aspetto, derivato dalla skill `ui-ux-pro-max` (stile "strumento per sviluppatori": slate + accento verde). -- **Tema**: segue Windows (chiaro/scuro) via `prefers-color-scheme`; la barra del titolo segue `Application.SetColorMode(SystemColorMode.System)`; lo sfondo del WebView2 è impostato per evitare il lampo bianco all'avvio. +Sorgente di verità per l'aspetto, derivato dalla skill `ui-ux-pro-max` (stile "strumento per sviluppatori": slate + accento verde). I token vivono come risorse di tema in [Gui/App.axaml](../Gui/App.axaml). +- **Tema**: segue il sistema (chiaro/scuro) tramite `RequestedThemeVariant="Default"`. - **Token** (chiaro / scuro): sfondo `#FFFFFF` / `#0F172A`, superficie composer `#FFFFFF` / `#1E293B`, testo `#0F172A` / `#F8FAFC`, testo secondario `#475569` / `#94A3B8`, bordi `#CBD5E1` / `#334155`, accento `#15803D` / `#22C55E`, errore `#B91C1C` / `#F87171`. Contrasto testo ≥ 4.5:1. -- **Tipografia**: Segoe UI Variable (corpo 15.5px, interlinea 1.65), Cascadia Code/Consolas per il codice. Nessun font web (funziona offline). -- **Forma e movimento**: raggi 10–20px, ombra leggera sul composer, transizioni 150ms, comparsa dei messaggi 180ms; con `prefers-reduced-motion` tutto fermo. -- **Barra laterale**: sfondo `--muted`, 272px; voce attiva con barra d'accento a sinistra; ✕ visibile solo al passaggio/focus. -- **Accessibilità**: focus sempre visibile, `aria-label` sui pulsanti a sola icona, `role=log`/`aria-live` sulla conversazione, `role=status` sull'attesa, target ≥ 36px, nessuna emoji come icona. -- **Non incluso**: evidenziazione sintattica del codice (richiederebbe una libreria da incorporare). +- **Tipografia**: font di sistema (Segoe UI Variable/Segoe UI su Windows; Inter, Ubuntu, Cantarell, Noto Sans, DejaVu Sans su Linux; corpo 15.5, titoli fino a 24), Cascadia Code/Consolas/DejaVu Sans Mono per il codice. Nessun font incorporato (funziona offline). +- **Forma**: raggi 10–20, bordi sottili, pulsante d'invio circolare. +- **Barra laterale**: sfondo `Muted`, 272 px; voce attiva con barra d'accento a sinistra; ✕ per eliminare. +- **Accessibilità**: focus visibile (il composer prende il bordo d'accento), `AutomationProperties.Name` sui pulsanti a sola icona e sul campo messaggio, target ≥ 30 px, icone vettoriali (nessuna emoji). +- **Non incluso**: evidenziazione sintattica del codice. ## Scelte tecniche -Le scelte architetturali sono negli [ADR](README.md#indice-adr). Quanto segue descrive lo stato attuale. - -- .NET 10 (WinForms). `Program.cs` (CLI + avvio) e `Gui.cs` (chat). Dipendenze NuGet solo per la GUI: `Markdig` (markdown→HTML) e `Microsoft.Web.WebView2` (richiede il runtime WebView2, già presente su Windows 11). +Le scelte architetturali sono negli [ADR](README.md#indice-adr). Sintesi: +- .NET 10, un solo progetto `net10.0` per Windows e Linux. Avalonia (GUI), Markdig (parsing markdown). Il codice è diviso in `Core/` (avvio di `claude`, cronologia, nome app) e `Gui/` (finestra, ViewModel, renderer markdown); `Program.cs` sceglie CLI o GUI. - `ProcessStartInfo.ArgumentList` per passare il prompt (niente problemi di quoting). -- `claude` è un .exe nativo (`~/.local/bin`): si avvia direttamente, senza `cmd /c`. Lo stdin è chiuso subito per evitare l'attesa di 3s di claude. +- `claude` è un eseguibile nativo (`~/.local/bin`): si avvia direttamente, senza `cmd /c`. Lo stdin è chiuso subito per evitare l'attesa di 3s di claude. - Working dir: `AppContext.BaseDirectory`, non la cwd del chiamante. +- L'avvio di `claude` è in un solo punto ([Core/Claude.cs](../Core/Claude.cs)), usato da CLI e GUI. ## Fuori scope (MVP) -Streaming token-per-token nella GUI, annullamento di una risposta in corso, rinomina/ricerca delle chat, config file, altri motori oltre a Claude Code (Codex, Antigravity: vedi [ADR-0002](adr/0002-motori-cli-claude-codex-antigravity.md)), logging su file, test automatici, supporto macOS. Il supporto Linux è pianificato: [ADR-0001](adr/0001-gui-multipiattaforma-avalonia.md), Fase 5. +Streaming token-per-token nella GUI, annullamento di una risposta in corso, rinomina/ricerca delle chat, config file, altri motori oltre a Claude Code (Codex, Antigravity: vedi [ADR-0002](adr/0002-motori-cli-claude-codex-antigravity.md)), logging su file, test automatici, supporto macOS. ## Criteri di accettazione - [ ] Eseguito da un'altra cartella, `claude` parte nella cartella dell'exe (verificabile chiedendo "pwd"). @@ -64,6 +60,5 @@ Streaming token-per-token nella GUI, annullamento di una risposta in corso, rino - [ ] Un prompt con virgolette e spazi arriva intatto. ## Domande aperte -- Nome definitivo del progetto (oggi provvisorio: vedi [Nome del progetto](README.md#nome-del-progetto)). - Permessi: serve `--allowedTools` o `--permission-mode` per l'uso non interattivo? - Formato output: testo semplice o `--output-format stream-json`? diff --git a/docs/README.md b/docs/README.md index b6d7c8e..efd2d1f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -11,12 +11,12 @@ Indice e regole. Ogni informazione vive in **un solo documento**; gli altri la r | [../CLAUDE.md](../CLAUDE.md) | Guida per Claude Code: comandi, architettura del codice, vincoli | ## Nome del progetto -Il nome è **provvisorio** (oggi `LlmRun`) e non è ancora deciso. Questa sezione è l'unica sorgente: gli altri doc scrivono "l'app" o ``. +Il nome è **`agent-wiki-ui`** (prima `LlmRun`). Questa sezione è l'unica sorgente: gli altri doc scrivono "l'app" o ``. -Se il nome cambia, aggiornare solo questi punti: -- [ ] `AssemblyName`/nome del csproj e dell'eseguibile (`.exe`, ``) -- [ ] Costante del nome nel codice (titolo finestra, intestazione della barra laterale, cartella dati) -- [ ] **Cartella dati** `//chats`: cambiando nome le chat salvate non si trovano più; migrare o leggere anche la vecchia cartella +Se il nome cambia di nuovo, aggiornare solo questi punti: +- [ ] `AssemblyName` in [../agent-wiki-ui.csproj](../agent-wiki-ui.csproj) (e il nome del file csproj): da lì il codice legge il nome (`Core/AppInfo.cs`) per titolo finestra, intestazione della barra laterale e cartella dati; l'eseguibile diventa `.exe` / `` +- [ ] `RootNamespace` e namespace C# (`AgentWikiUi.*`, anche in `x:Class` e `xmlns` dei file `.axaml`): il nome del namespace non può avere trattini +- [ ] **Cartella dati** `//chats`: cambiando nome le chat salvate non si trovano più. `ChatStore` sposta una vecchia cartella (`LlmRun`) in quella nuova al primo avvio; per un nuovo cambio aggiungere il nome precedente all'elenco `LegacyNames` - [ ] Titolo di [../README.md](../README.md) e riga iniziale di [../CLAUDE.md](../CLAUDE.md) (nome del csproj nei comandi) ## Flusso (doc-first, doc-as-code) @@ -33,5 +33,5 @@ Se il nome cambia, aggiornare solo questi punti: ## Indice ADR | ADR | Titolo | Stato | |---|---|---| -| [0001](adr/0001-gui-multipiattaforma-avalonia.md) | GUI multipiattaforma in solo C# con Avalonia | Proposta | +| [0001](adr/0001-gui-multipiattaforma-avalonia.md) | GUI multipiattaforma in solo C# con Avalonia | Accettata | | [0002](adr/0002-motori-cli-claude-codex-antigravity.md) | Più motori CLI (Claude Code, Codex, Antigravity) su Windows e Linux | Proposta | diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 3fea131..5da4487 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -41,14 +41,15 @@ Decisione, tabella dei comandi e rischi: [ADR-0002](adr/0002-motori-cli-claude-c ## Fase 5 — Multipiattaforma (Windows + Linux), GUI in solo C# Decisione e motivazioni: [ADR-0001](adr/0001-gui-multipiattaforma-avalonia.md). Qui solo i passi. -- [ ] Chiudere le verifiche dell'ADR-0001 con un prototipo e portarlo a "Accettata" -- [ ] Nome dell'app in un solo punto del codice (costante usata per titolo finestra, cartella dati; `AssemblyName` nel csproj), così la rinomina segue la checklist di docs/README.md -- [ ] Target `net10.0` (non più `net10.0-windows`); rimuovere WinForms e WebView2 -- [ ] Estrarre in un solo punto la logica di avvio di `claude` (cartella dell'exe, stdin chiuso), oggi duplicata tra `Program.cs` e `Gui.cs` -- [ ] Cronologia in `Environment.SpecialFolder.LocalApplicationData` (stesso formato JSON, chat esistenti ancora leggibili) -- [ ] GUI Avalonia: finestra, barra laterale delle chat, conversazione, composer con modello ed effort -- [ ] Rendering markdown in Avalonia (titoli, liste, tabelle, citazioni, codice), pulsante Copia, link nel browser di sistema -- [ ] Tema chiaro/scuro che segue il sistema, con i token di MVP.md → "Design (UI/UX)" -- [ ] Eliminare `ui.html` e il protocollo `WebMessage` -- [ ] Publish single-file self-contained `win-x64` e `linux-x64`, provati su entrambi i sistemi -- [ ] Aggiornare README.md e CLAUDE.md (comandi, architettura, vincoli) a migrazione conclusa +- [x] Chiudere le verifiche dell'ADR-0001 con un prototipo e portarlo a "Accettata" (restano due verifiche indicate nell'ADR) +- [x] Nome dell'app in un solo punto: `AssemblyName` nel csproj, letto da `Core/AppInfo.cs`; la rinomina segue la checklist di docs/README.md +- [x] Rinomina in `agent-wiki-ui` (csproj, namespace) e migrazione automatica della cartella dati `LlmRun` → `agent-wiki-ui` +- [x] Target `net10.0` (non più `net10.0-windows`); rimossi WinForms e WebView2 +- [x] Estrarre in un solo punto la logica di avvio di `claude` (`Core/Claude.cs`) (cartella dell'exe, stdin chiuso), oggi duplicata tra `Program.cs` e `Gui.cs` +- [x] Cronologia in `Environment.SpecialFolder.LocalApplicationData` (stesso formato JSON, chat esistenti ancora leggibili) +- [x] GUI Avalonia: finestra, barra laterale delle chat, conversazione, composer con modello ed effort +- [x] Rendering markdown in Avalonia (titoli, liste, tabelle, citazioni, codice), pulsante Copia, link nel browser di sistema +- [x] Tema chiaro/scuro che segue il sistema, con i token di MVP.md → "Design (UI/UX)" (da provare su un desktop Linux reale) +- [x] Eliminare `ui.html` e il protocollo `WebMessage` +- [ ] Publish single-file self-contained `win-x64` e `linux-x64`: compilano e partono (Linux: CLI e GUI su WSLg; Windows: solo CLI, via WSL); da provare la GUI su Windows nativo e su una distribuzione Linux pulita +- [x] Aggiornare README.md e CLAUDE.md (comandi, architettura, vincoli) a migrazione conclusa diff --git a/docs/adr/0001-gui-multipiattaforma-avalonia.md b/docs/adr/0001-gui-multipiattaforma-avalonia.md index 7700fa2..73db61d 100644 --- a/docs/adr/0001-gui-multipiattaforma-avalonia.md +++ b/docs/adr/0001-gui-multipiattaforma-avalonia.md @@ -1,6 +1,6 @@ # ADR-0001 — GUI multipiattaforma in solo C# con Avalonia -- **Stato**: Proposta +- **Stato**: Accettata (implementata; restano due verifiche, vedi sotto) - **Data**: 2026-10-15 - **Sostituisce**: la parte "GUI WinForms + WebView2 + `ui.html`" di [MVP.md](../MVP.md) (le sezioni interessate rimandano a questo ADR) @@ -29,7 +29,7 @@ Oggi non è possibile: WinForms e WebView2 sono solo Windows, il csproj punta a ## Decisione La GUI viene riscritta con **Avalonia UI**: - Layout in XAML, logica in C# con `ViewModel` e binding. `ui.html`, il protocollo `WebMessage` e le dipendenze WinForms/WebView2 spariscono. -- Rendering markdown con una libreria Avalonia basata su Markdig, al posto della conversione in HTML. I link `http(s)` si aprono nel browser di sistema. +- Rendering markdown con un **renderer proprio** che trasforma l'albero di Markdig in controlli Avalonia (`Gui/MarkdownView.cs`), al posto della conversione in HTML. Si è scelto di non usare `Markdown.Avalonia` perché l'ultima versione pubblicata (11.0.x) è per Avalonia 11, mentre si usa Avalonia 12. I link `http(s)` si aprono nel browser di sistema. - Un solo progetto con target `net10.0` (non più `net10.0-windows`). Due publish single-file self-contained: `win-x64` e `linux-x64`. - La CLI (`Program.cs` con argomenti) resta com'è e funziona su entrambi i sistemi. - La cronologia usa `Environment.SpecialFolder.LocalApplicationData` (`%LOCALAPPDATA%` su Windows, `~/.local/share` su Linux), sottocartella `/chats`. Il nome viene da un'unica costante (vedi [Nome del progetto](../README.md#nome-del-progetto)). @@ -49,11 +49,12 @@ La GUI viene riscritta con **Avalonia UI**: - Su Linux serve un desktop con X11 o XWayland e le librerie di base (fontconfig ecc.). ## Da verificare (prima di passare a "Accettata") -- [ ] La libreria markdown per Avalonia scelta è compatibile con la versione di Avalonia usata e con `net10.0`, e rende tabelle, citazioni e blocchi di codice in modo accettabile. -- [ ] Il publish single-file `linux-x64` parte su una distribuzione pulita (nessuna libreria nativa mancante oltre a quelle di desktop). -- [ ] `claude` viene trovato nel PATH su Linux e la CLI mantiene exit code e codice 127. -- [ ] Il tema chiaro/scuro segue il sistema anche su Linux. -- [ ] Le chat esistenti restano leggibili (stesso formato JSON). Se il nome cambia cambia anche la cartella dati: prevedere la migrazione della vecchia cartella. +- [x] Rendering markdown: tabelle, citazioni, liste, codice, link ed enfasi verificati con uno screenshot headless in tema chiaro e scuro (libreria `Markdown.Avalonia` scartata, vedi Decisione). +- [x] Il publish single-file `linux-x64` (48 MB con compressione) parte; verificato su WSL2 con WSLg. **Da verificare** su una distribuzione desktop pulita. +- [x] Il publish single-file `win-x64` si compila e la CLI parte (provata da WSL tramite interoperabilità). **Da verificare** la GUI su Windows nativo. +- [x] `claude` viene trovato nel PATH su Linux e la CLI mantiene exit code e codice 127 (provato con un `claude` finto). +- [ ] Il tema chiaro/scuro segue il sistema anche su Linux (impostato con `RequestedThemeVariant="Default"`; non verificato su un desktop reale). +- [x] Le chat esistenti restano leggibili (stesso formato JSON). Se il nome cambia cambia anche la cartella dati: la vecchia cartella viene migrata (fatto per `LlmRun` → `agent-wiki-ui`). ## Riferimenti - Piano di lavoro: [ROADMAP.md](../ROADMAP.md), Fase 5. diff --git a/ui.html b/ui.html deleted file mode 100644 index cf3ae99..0000000 --- a/ui.html +++ /dev/null @@ -1,272 +0,0 @@ - - - - - -LlmRun - - - - -
- - -
-
- -

Come posso aiutarti?

-

Scrivi un messaggio oppure parti da uno di questi spunti.

-
- - - - -
-
- -
- -
-
- -
- - - - -
-
-
Invio per inviare · Maiusc+Invio per andare a capo
-
-
- - - -