diff --git a/CLAUDE.md b/CLAUDE.md index 6d55180..994d532 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Progetto -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. +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`) o Codex (`codex exec`) da terminale. Senza argomenti apre una GUI di chat Avalonia, con argomenti funziona da CLI. ## Workflow doc-first (obbligatorio) @@ -23,11 +23,12 @@ agent-wiki-ui (già `LlmRun`; il nome ha un'unica sorgente, vedi [docs/README.md 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). +- [Program.cs](Program.cs): top-level statements. Con argomenti è la CLI (`EngineRunner.RunInherited`, motore scelto con `--engine ` come primo argomento, default Claude Code); senza avvia Avalonia (`App`). +- [Core/Engine.cs](Core/Engine.cs): astrazione "motore" (ADR-0002): `Engine` (id, nome, eseguibile, modelli, effort, `BuildArgs`, `Parse`), `ClaudeEngine`, `CodexEngine`, registro `Engines` e ricerca nel PATH (`PathSearch`). Gli elenchi modelli sono fissi, da aggiornare quando ne escono di nuovi. +- [Core/EngineRunner.cs](Core/EngineRunner.cs): **unico punto di avvio dei motori** (cartella dell'exe, stdin chiuso, `ArgumentList`), CLI con output ereditato e GUI con output catturato e interpretato da `Engine.Parse`. +- [Core/ChatStore.cs](Core/ChatStore.cs): cronologia, un JSON per chat in `//chats/.json` (nome file = id della chat, validato come Guid prima dell'uso; campi `Engine` e `SessionId` opzionali: i file vecchi sono chat di Claude Code). - [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/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, interruttore CLI: cambiare motore apre una nuova chat). - [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. @@ -35,8 +36,9 @@ Decisioni e motivazioni negli ADR ([docs/adr/](docs/adr/)); qui solo la mappa de ## 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. +- Lo stdin del motore va redirezionato e chiuso subito: `claude` aspetta 3 s, `codex` stampa "Reading additional input from stdin…" e attende. - `claude` è un eseguibile nativo (`~/.local/bin`), avviabile direttamente. Se fosse `claude.cmd` (Windows) serve `cmd.exe /c`. +- Codex (`CodexEngine`): l'id di sessione lo genera Codex e si legge da `thread.started` nell'output JSONL (`--json`); la risposta è l'ultimo `agent_message`; `exec resume` non accetta `-s`, la sola lettura si impone con `-c 'sandbox_mode="read-only"'`; serve `--skip-git-repo-check` (la cartella dell'exe non è un repository); `--` prima del prompt. Dettagli e prove in ADR-0002. Il modello predefinito è quello di `~/.codex/config.toml`: se richiede un Codex più recente l'errore compare nella chat (vedi docs/INSTALL.md). - 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. diff --git a/Core/Claude.cs b/Core/Claude.cs deleted file mode 100644 index 51f93dc..0000000 --- a/Core/Claude.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System.Diagnostics; - -namespace AgentWikiUi.Core; - -record ModelOption(string Label, string Id); - -static class Catalog -{ - // Da aggiornare quando escono nuovi modelli. - public static readonly ModelOption[] 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"), - ]; - - public static readonly string[] Efforts = ["low", "medium", "high", "xhigh", "max"]; -} - -record RunResult(int Code, string Out, string Err); - -// Unico punto di avvio di `claude`, condiviso da CLI e GUI. -static class Claude -{ - // sessionId null = nessun contesto (CLI). Model/effort vuoti = flag omessi. - public static ProcessStartInfo CreateStart(string prompt, bool redirectOutput, string? sessionId = null, bool resume = false, string model = "", string effort = "") - { - var psi = new ProcessStartInfo("claude") - { - WorkingDirectory = AppContext.BaseDirectory, // mai la cwd del chiamante né Assembly.Location (vuoto in single-file) - UseShellExecute = false, - RedirectStandardInput = true, // chiuso subito dal chiamante: evita l'attesa di stdin di claude - RedirectStandardOutput = redirectOutput, - RedirectStandardError = redirectOutput, - CreateNoWindow = true, - }; - psi.ArgumentList.Add("-p"); - psi.ArgumentList.Add(prompt); - if (sessionId is not null) - { - psi.ArgumentList.Add(resume ? "--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); } - return psi; - } - - // CLI: stdout/stderr ereditati (streaming), exit code propagato, 127 se `claude` manca. - public static int RunInherited(string prompt) - { - try - { - using var p = Process.Start(CreateStart(prompt, redirectOutput: false))!; - p.StandardInput.Close(); - p.WaitForExit(); - return p.ExitCode; - } - catch (System.ComponentModel.Win32Exception) - { - Console.Error.WriteLine("'claude' non trovato nel PATH."); - return 127; - } - } - - // GUI: output catturato. Win32Exception se `claude` non è nel PATH. - public static async Task RunCapturedAsync(string prompt, string sessionId, bool resume, string model, string effort) - { - using var p = Process.Start(CreateStart(prompt, redirectOutput: true, sessionId, resume, model, effort))!; - p.StandardInput.Close(); - var o = p.StandardOutput.ReadToEndAsync(); - var e = p.StandardError.ReadToEndAsync(); - await p.WaitForExitAsync(); - return new(p.ExitCode, (await o).Trim(), (await e).Trim()); - } -} diff --git a/README.md b/README.md index e4de75f..d1bc60b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # agent-wiki-ui -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. +Eseguibile C# (.NET 10, Windows e Linux) che lancia Claude Code (`claude -p`) o Codex (`codex exec`) nella cartella in cui risiede l'eseguibile. Senza argomenti apre una GUI di chat (Avalonia), con argomenti funziona da riga di comando. ## Requisiti - `claude` (Claude Code) installato e nel PATH, già autenticato; per Codex lo stesso con `codex`. Istruzioni: [docs/INSTALL.md](docs/INSTALL.md) @@ -9,14 +9,15 @@ Eseguibile C# (.NET 10, Windows e Linux) che lancia Claude Code (`claude -p`) ne ## 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). 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. +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). Il riquadro **Impostazioni** (in alto a destra) permette di scegliere la **CLI** (Claude Code o Codex), il **modello** e l'**effort** di ragionamento ("Predefinito" = nessun flag, vale la config del motore). Le chat si salvano su disco e l'ultima si riapre all'avvio. ``` dotnet run ``` -CLI (con argomenti): il prompt viene stampato su terminale, l'exit code è quello di `claude` (127 se `claude` non è nel PATH). +CLI (con argomenti): il prompt viene stampato su terminale, l'exit code è quello del motore (127 se non è nel PATH). Di default usa Claude Code; `--engine codex` usa Codex. ``` dotnet run -- "spiega questo progetto" +dotnet run -- --engine codex "spiega questo progetto" ``` ## Publish single-file @@ -28,6 +29,6 @@ dotnet publish -c Release -r linux-x64 --self-contained -p:PublishSingleFile=tru 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 -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). +Windows e Linux con lo stesso codice ([ADR-0001](docs/adr/0001-gui-multipiattaforma-avalonia.md)). Motori supportati: Claude Code e Codex CLI, già installati da terminale; Antigravity CLI è pianificato: [ADR-0002](docs/adr/0002-motori-cli-claude-codex-antigravity.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/docs/INSTALL.md b/docs/INSTALL.md index 61499c5..18ccbc4 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -71,6 +71,8 @@ codex exec --help # uso non interattivo, usato dall'app **Stato su questa macchina** (WSL, controllato il 2026-10-15): `codex` 0.144.3 in `~/.local/bin/codex`, collegamento a `~/.codex/packages/standalone/current/bin/codex`, con accesso ChatGPT già fatto. `claude` 2.1.252 in `~/.npm-global/bin/claude`, con accesso claude.ai già fatto. +**Problema noto: "requires a newer version of Codex".** Se il modello predefinito del tuo `~/.codex/config.toml` richiede un Codex più recente, `codex exec` fallisce con quell'errore (l'app lo mostra nella chat). Aggiorna con `codex update`, oppure scegli un altro modello nelle Impostazioni dell'app. Controllato su questa macchina il 2026-10-15 con Codex 0.144.3. + ## Controllo finale per l'app Dallo stesso terminale da cui avvii l'app: ``` diff --git a/docs/MVP.md b/docs/MVP.md index 6356ca3..78ba70d 100644 --- a/docs/MVP.md +++ b/docs/MVP.md @@ -7,14 +7,14 @@ Eseguibile C# a riga di comando che inoltra un prompt a Claude Code (`claude`) l ``` "spiega questo progetto" ``` -Tutti gli argomenti sono uniti in un unico prompt. +Tutti gli argomenti sono uniti in un unico prompt. Con `--engine ` come primo argomento (`claude` o `codex`) si sceglie il motore, per esempio ` --engine codex "spiega questo progetto"`; senza, si usa Claude Code. Id sconosciuto o prompt mancante: uso su stderr ed exit code 2. ## Comportamento 1. Se non ci sono argomenti: stampa l'uso ed esce con codice 2. -2. Avvia `claude -p ` (modalità non interattiva) con `WorkingDirectory = AppContext.BaseDirectory`. +2. Avvia il motore scelto in modalità non interattiva (`claude -p ` o `codex exec … -- `) con `WorkingDirectory = AppContext.BaseDirectory`. 3. Ritrasmette stdout e stderr del processo figlio in tempo reale. 4. Esce con lo stesso exit code di `claude`. -5. Se `claude` non è trovato: messaggio chiaro ed exit code 127. +5. Se l'eseguibile del motore non è trovato: messaggio chiaro ed exit code 127. ## GUI (chat) 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). @@ -24,9 +24,10 @@ Senza argomenti si apre una finestra Avalonia (C# + XAML, nessun HTML) in stile - **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): solo logo, titolo e sottotitolo (nessun suggerimento precompilato). - **Composer** (in basso, scheda arrotondata): campo di testo che cresce fino a ~8 righe e pulsante Invia circolare con icona. `Invio` invia, `Maiusc+Invio` va a capo. Hint sulla tastiera sotto la scheda. Non contiene più i selettori di modello ed effort. -- **Impostazioni** (in alto a destra, pulsante con icona a cursori, simmetrico a quello della barra laterale): aprono un riquadro a comparsa con **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). Si chiude con `Esc` o con un clic fuori; le scelte restano valide per i messaggi successivi (non sono ancora salvate su disco). Passati come `--model` / `--effort`; con "Predefinito" il flag viene omesso. Il riquadro è il punto in cui in futuro compariranno le impostazioni del motore ([ADR-0002](adr/0002-motori-cli-claude-codex-antigravity.md)). Solo GUI. +- **Impostazioni** (in alto a destra, pulsante con icona a cursori, simmetrico a quello della barra laterale): aprono un riquadro a comparsa con l'interruttore **CLI** (Claude Code, Codex; un motore non installato è mostrato "non installato" e non è selezionabile; disabilitato durante l'attesa), poi **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). Si chiude con `Esc` o con un clic fuori; le scelte restano valide per i messaggi successivi (non sono ancora salvate su disco). Passati come `--model` / `--effort`; con "Predefinito" il flag viene omesso. Solo GUI. +- **Motori** ([ADR-0002](adr/0002-motori-cli-claude-codex-antigravity.md)): una chat usa un solo motore. Cambiare **CLI** apre una nuova chat con quel motore; modello ed effort tornano a "Predefinito" e le liste dipendono dal motore (Codex: modelli GPT-5.6 Sol/Terra/Luna, stessi livelli di effort). Aprendo una chat salvata il selettore passa al motore di quella chat. L'etichetta sulle risposte, il segnaposto del composer e il messaggio d'attesa usano il nome del motore, e l'elenco chat lo indica sotto il titolo. Con Codex la sessione è letta dall'output (`thread_id`) e usata per riprendere la chat; i permessi sono di sola lettura. - **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`, 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. +- **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, motore (`Engine`) e id di sessione del motore (`SessionId`); i file senza questi campi sono chat di Claude Code. 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 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. @@ -48,7 +49,7 @@ Le scelte architetturali sono negli [ADR](README.md#indice-adr). Sintesi: - `ProcessStartInfo.ArgumentList` per passare il prompt (niente problemi di quoting). - `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. +- L'avvio dei motori è in un solo punto ([Core/EngineRunner.cs](../Core/EngineRunner.cs)), usato da CLI e GUI; ogni motore descrive argomenti e parser in [Core/Engine.cs](../Core/Engine.cs). ## 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, supporto macOS. I test automatici sono pianificati: [TESTING.md](TESTING.md). diff --git a/docs/README.md b/docs/README.md index 3552f19..57784a6 100644 --- a/docs/README.md +++ b/docs/README.md @@ -37,5 +37,5 @@ Se il nome cambia di nuovo, aggiornare solo questi punti: | ADR | Titolo | Stato | |---|---|---| | [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 | +| [0002](adr/0002-motori-cli-claude-codex-antigravity.md) | Più motori CLI (Claude Code, Codex, Antigravity) su Windows e Linux | Accettata (Claude Code, Codex) | | [0003](adr/0003-strategia-di-test.md) | Strategia di test e regola "nuova funzione = nuovi test" | Proposta | diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index c3e0c20..2625113 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -28,15 +28,17 @@ Dettagli dell'MVP in [MVP.md](MVP.md). ## Fase 4 — Più motori (Claude Code, Codex, Antigravity) Decisione, tabella dei comandi e rischi: [ADR-0002](adr/0002-motori-cli-claude-codex-antigravity.md). Qui solo i passi. Va fatta dopo (o insieme a) la Fase 5, che estrae il punto unico di avvio processo. -- [ ] Verificare con `--help` i flag reali di `codex` e `agy` su Windows e Linux (ADR-0002) e portare l'ADR a "Accettata" -- [ ] Astrazione "motore" in C#: `RunClaude` diventa `RunEngine(engine, ...)` -- [ ] Rilevamento dei motori nel PATH; motori assenti disabilitati, messaggio chiaro -- [ ] Il JSON di ogni chat memorizza il motore; cambiare motore apre una nuova chat -- [ ] GUI: schermata Impostazioni (motore, modello, effort) persistita su disco; modelli ed effort in base al motore; motore della chat indicato nell'interfaccia -- [ ] Motore Codex: lettura dell'id di sessione dall'output per la ripresa +- [x] Verificare i flag reali di `codex` (provati su Linux/WSL, ADR-0002 accettata per Claude Code e Codex); `agy` e Windows da verificare +- [x] Astrazione "motore" in C# (`Core/Engine.cs`, `Core/EngineRunner.cs`) +- [x] Rilevamento dei motori nel PATH; motori assenti disabilitati, messaggio chiaro +- [x] Il JSON di ogni chat memorizza il motore e l'id di sessione; cambiare motore apre una nuova chat +- [x] GUI: interruttore CLI nel riquadro Impostazioni; modelli ed effort in base al motore; motore della chat indicato nell'interfaccia +- [ ] Impostazioni (motore, modello, effort) persistite su disco +- [x] Motore Codex: lettura dell'id di sessione dall'output (`thread_id`) per la ripresa - [ ] Motore Antigravity: gestione del blocco con output rediretto (timeout, errore chiaro) -- [ ] Politica dei permessi: sola lettura di default, opzione esplicita per concedere di più -- [ ] CLI: opzione `--engine`, coerente con la GUI +- [x] Permessi di Codex in sola lettura (nessun flag che salta i controlli) +- [ ] Opzione esplicita per concedere permessi di scrittura +- [x] CLI: opzione `--engine`, coerente con la GUI ## Fase 5 — Multipiattaforma (Windows + Linux), GUI in solo C# Decisione e motivazioni: [ADR-0001](adr/0001-gui-multipiattaforma-avalonia.md). Qui solo i passi. @@ -45,7 +47,7 @@ Decisione e motivazioni: [ADR-0001](adr/0001-gui-multipiattaforma-avalonia.md). - [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] Estrarre in un solo punto la logica di avvio dei motori (`Core/EngineRunner.cs`: cartella dell'exe, stdin chiuso), prima 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 diff --git a/docs/TESTING.md b/docs/TESTING.md index f76b00b..1ada4d2 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -31,7 +31,7 @@ tests/FakeClaude/ eseguibile finto: registra argv/cwd/stdin; testo, ## Predisposizioni di testabilità (prerequisito dei test) - [ ] `ChatStore` come istanza con cartella iniettata (oggi statico su `LocalApplicationData`) -- [ ] `IClaudeRunner` dietro `Core/Claude.cs`, iniettato nel `MainViewModel` +- [ ] `IEngineRunner` dietro `Core/EngineRunner.cs`, iniettato nel `MainViewModel` - [ ] `InternalsVisibleTo` verso il progetto di test - [ ] Spostamento in `src/` e `tests/` con soluzione unica @@ -49,7 +49,7 @@ Esclusi dal calcolo: XAML generato, `Program.cs` (tre righe, coperte da E2E), `A ## Matrice requisito → test Fonte dei requisiti: [MVP.md](MVP.md) e gli ADR. Livello: vedi sopra. Il nome del test è indicativo. -### Avvio di `claude` — `Core/Claude.cs` +### Avvio dei motori — `Core/EngineRunner.cs` | ID | Requisito | Liv. | Test | | |---|---|---|---|---| | CL-01 | Il prompt è un solo argomento, anche con virgolette e spazi (criterio MVP 4) | U/P | `Prompt_con_virgolette_arriva_intatto` | [ ] | @@ -58,13 +58,30 @@ Fonte dei requisiti: [MVP.md](MVP.md) e gli ADR. Livello: vedi sopra. Il nome de | CL-04 | Cartella di lavoro = cartella dell'exe, non la cwd del chiamante (criterio MVP 1) | U/P | `Cartella_di_lavoro_e_quella_dell_exe` | [ ] | | CL-05 | Stdin chiuso subito: nessuna attesa di 3 s | P | `Stdin_chiuso_non_blocca` | [ ] | | CL-06 | CLI: l'exit code di `claude` è propagato (criterio MVP 3) | P/E | `ExitCode_propagato` | [ ] | -| CL-07 | CLI: `claude` assente ⇒ messaggio chiaro ed exit code 127 | P/E | `Claude_assente_127` | [ ] | +| CL-07 | CLI: eseguibile del motore assente ⇒ messaggio chiaro (con il nome dell'eseguibile) ed exit code 127 | P/E | `Claude_assente_127` | [ ] | | CL-08 | CLI: stdout/stderr ereditati e ritrasmessi (criterio MVP 2: streaming) | E | `Output_ereditato_in_tempo_reale` | [ ] | | CL-09 | GUI: output catturato e ripulito agli estremi; stderr separato | P | `Output_catturato` | [ ] | | CL-10 | GUI: `claude` assente ⇒ eccezione riconoscibile, non crash | P | `Claude_assente_in_GUI` | [ ] | | CL-11 | Il catalogo modelli contiene gli ID attesi e gli effort previsti | U | `Catalogo_modelli_ed_effort` | [ ] | | CL-12 | Senza argomenti non parte la CLI; con argomenti sì (tutti uniti in un prompt) | E | `Argomenti_uniti_in_un_prompt` | [ ] | +### Codex — `Core/Engine.cs` (parser e argomenti), `Core/EngineRunner.cs` +Le righe CL valgono per ogni motore; queste sono specifiche di Codex (ADR-0002). Il parser si prova con output JSONL reali salvati nei test. +| ID | Requisito | Liv. | Test | | +|---|---|---|---|---| +| CX-01 | Primo messaggio: `exec --json --skip-git-repo-check -s read-only … -- ` | U | `Codex_primo_messaggio` | [ ] | +| CX-02 | Ripresa: `exec resume --json … -c sandbox_mode="read-only" -- `, senza `-s` | U | `Codex_ripresa` | [ ] | +| CX-03 | Un prompt che inizia con `-` arriva intatto (grazie a `--`) | U/P | `Codex_prompt_con_trattino` | [ ] | +| CX-04 | Modello ⇒ `-m`; effort ⇒ `-c model_reasoning_effort=`; vuoti ⇒ omessi | U | `Codex_modello_ed_effort` | [ ] | +| CX-05 | Il parser legge `thread_id` da `thread.started` | U | `Codex_thread_id` | [ ] | +| CX-06 | La risposta è l'ultimo `agent_message`, anche con più messaggi | U | `Codex_ultimo_agent_message` | [ ] | +| CX-07 | `turn.failed`: messaggio interno di `error.message` estratto dal JSON annidato; exit ≠ 0 ⇒ errore | U | `Codex_errore_turno` | [ ] | +| CX-08 | Eventi `item.completed` di tipo `error` (avvisi) non fanno fallire la risposta | U | `Codex_avvisi_ignorati` | [ ] | +| CX-09 | Exit 0 senza `agent_message` ⇒ errore "nessuna risposta" | U | `Codex_nessuna_risposta` | [ ] | +| CX-10 | Righe non JSON o eventi sconosciuti sono ignorati senza eccezioni | U | `Codex_righe_non_json` | [ ] | +| CX-11 | In modalità CLI (output ereditato) non si usa `--json` | U | `Codex_cli_senza_json` | [ ] | +| CX-12 | Con un `FakeClaude` configurato come Codex il ciclo primo messaggio + ripresa funziona (id letto e riusato) | P | `Codex_ciclo_completo_con_finto` | [ ] | + ### Cronologia — `Core/ChatStore.cs`, `Core/AppInfo.cs` | ID | Requisito | Liv. | Test | | |---|---|---|---|---| @@ -78,6 +95,7 @@ Fonte dei requisiti: [MVP.md](MVP.md) e gli ADR. Livello: vedi sopra. Il nome de | ST-08 | Compatibilità del formato: un JSON scritto dalla versione WinForms si legge | U | `Formato_storico_compatibile` | [ ] | | ST-09 | Migrazione: la cartella di un nome precedente (`LlmRun`) si sposta nella nuova al primo avvio; se la nuova esiste non si tocca nulla | U | `Migrazione_da_nome_precedente` | [ ] | | ST-10 | `AppInfo.Name` = `AssemblyName` del csproj | U | `Nome_da_assembly` | [ ] | +| ST-12 | Il JSON salva `Engine` e `SessionId`; un file senza questi campi si legge come chat di Claude Code con `SessionId` = id | U | `Motore_e_sessione_salvati_e_compatibilita` | [ ] | | ST-11 | Errore di I/O nel salvataggio non perde la chat a schermo | U | vedi VM-12 | [ ] | ### Logica della chat — `Gui/MainViewModel.cs` @@ -157,15 +175,24 @@ Fonte dei requisiti: [MVP.md](MVP.md) e gli ADR. Livello: vedi sopra. Il nome de Per **ogni** motore, la stessa batteria di contratto: | ID | Requisito | Liv. | | |---|---|---|---| -| EN-01 | Argomenti del primo messaggio e della ripresa | U | [ ] | +| EN-01 | Argomenti del primo messaggio e della ripresa (Claude: CL-02; Codex: CX-01, CX-02) | U | [ ] | | EN-02 | Rilevamento nel PATH; assente ⇒ disabilitato e messaggio chiaro | U/P | [ ] | -| EN-03 | Lettura dell'id di sessione dall'output (Codex) | U | [ ] | +| EN-03 | Lettura dell'id di sessione dall'output (Codex: CX-05) | U | [ ] | | EN-04 | Parser dell'output → markdown, con output di esempio reali salvati nei test | U | [ ] | | EN-05 | Modelli ed effort mostrati secondo il motore | U/G | [ ] | | EN-06 | Il JSON della chat memorizza il motore; cambiare motore apre una nuova chat | U | [ ] | | EN-07 | Politica dei permessi: nessun flag che salta i controlli senza scelta esplicita | U | [ ] | | EN-08 | Impostazioni persistite e rilette | U | [ ] | | EN-09 | Gestione del blocco di `agy -p` con output rediretto (timeout, errore chiaro) | P | [ ] | +| EN-10 | `Engines.Find` per id (`claude`, `codex`, sconosciuto ⇒ nullo); l'id salvato nei JSON resta stabile | U | [ ] | +| EN-11 | Cambiare CLI nelle Impostazioni apre una nuova chat con quel motore; modello ed effort tornano a "Predefinito"; liste secondo il motore | U | [ ] | +| EN-12 | Motore non installato: etichetta "non installato", non selezionabile; il VM rifiuta la selezione | U/G | [ ] | +| EN-13 | L'interruttore CLI è disabilitato durante l'attesa | U/G | [ ] | +| EN-14 | Aprendo una chat salvata il selettore passa al motore della chat; sessione e `started` coerenti | U | [ ] | +| EN-15 | Etichetta autore, segnaposto, messaggio d'attesa ed elenco chat mostrano il nome del motore | U/G | [ ] | +| EN-16 | Dopo il primo invio con Codex il `SessionId` è quello letto dall'output e il secondo invio usa la ripresa; con errore al primo invio resta senza sessione | U | [ ] | +| EN-17 | CLI `--engine "prompt"`; id sconosciuto o prompt mancante ⇒ uso su stderr ed exit 2; senza `--engine` ⇒ Claude Code | E | [ ] | +| EN-18 | Rilevamento nel PATH: nome esatto su Linux, `.exe` su Windows; cartella inesistente nel PATH ignorata | U | [ ] | ## Come si eseguono (a implementazione avvenuta) Comandi previsti, da confermare con ADR-0003 → "Da verificare": diff --git a/docs/adr/0002-motori-cli-claude-codex-antigravity.md b/docs/adr/0002-motori-cli-claude-codex-antigravity.md index f3f1b4b..1782fd0 100644 --- a/docs/adr/0002-motori-cli-claude-codex-antigravity.md +++ b/docs/adr/0002-motori-cli-claude-codex-antigravity.md @@ -1,6 +1,6 @@ # ADR-0002 — Più motori CLI (Claude Code, Codex, Antigravity) su Windows e Linux -- **Stato**: Proposta +- **Stato**: Accettata per Claude Code e Codex (verificati con prove reali); Antigravity resta da verificare - **Data**: 2026-10-15 - **Collegato a**: [ADR-0001](0001-gui-multipiattaforma-avalonia.md) (GUI e piattaforme) @@ -17,31 +17,45 @@ L'app lancia solo `claude -p`. Si vuole usare dalla stessa chat anche **OpenAI C - **Astrazione "motore"** in C#. Ogni motore descrive: nome e eseguibile, argomenti per il primo messaggio e per la ripresa, elenco modelli, supporto effort, modo per ottenere l'id di sessione. `RunClaude` diventa `RunEngine(engine, …)`. - **Rilevamento** dei motori cercando l'eseguibile nel PATH (su Windows anche le estensioni di PATHEXT). I motori assenti sono disabilitati nelle Impostazioni, con messaggio chiaro; la CLI esce con 127 come oggi. - **Una chat = un motore**. Il file JSON della chat memorizza il motore; il contesto di sessione non passa da un motore all'altro, quindi cambiare motore apre una nuova chat. -- **Impostazioni** nella GUI: motore attivo, modello, effort (solo se supportato), persistite su disco accanto alla cronologia. La CLI ottiene `--engine`. +- **Impostazioni** nella GUI: interruttore **CLI** (motore attivo), modello, effort. Cambiare motore apre una nuova chat; modello ed effort tornano a "Predefinito" e le liste sono quelle del motore. La persistenza su disco delle impostazioni è rinviata. La CLI ottiene `--engine ` (`claude`, `codex`). +- **Sessione**: per Claude Code l'id lo sceglie l'app; per Codex lo genera Codex e l'app lo legge dall'output (`thread_id`). Il JSON della chat ha quindi un campo `Engine` e un campo `SessionId` separato dall'id della chat (che resta il nome del file). I file scritti prima (senza questi campi) si leggono come chat di Claude Code con `SessionId` = id della chat. +- **Autore dei messaggi**: l'etichetta sulle risposte e i segnaposto del composer usano il nome del motore della chat; l'elenco chat indica il motore. - **Permessi**: di default sola lettura / nessun bypass dei controlli; concedere di più è un'opzione esplicita. - **Output**: ogni motore ha un parser che lo normalizza in testo markdown prima del rendering. ## Comandi dei motori -Da verificare con `--help` delle versioni installate: derivano da documentazione e ricerche, non da prove su questo PC, dove `codex` e `agy` non sono installati. +Claude Code e Codex sono verificati con prove reali (Codex 0.144.3, 2026-10-15, Linux/WSL). Antigravity deriva da documentazione e ricerche: `agy` non è installato su questa macchina. | | Claude Code | Codex CLI | Antigravity CLI | |---|---|---|---| -| Comando | `claude -p ` | `codex exec ` | `agy -p ` | -| Continuare | `--session-id ` poi `--resume ` | `codex exec resume ` | da verificare | +| Comando (GUI) | `claude -p ` | `codex exec --json --skip-git-repo-check -s read-only [opzioni] -- ` | `agy -p ` (da verificare) | +| Continuare | `--session-id `, poi `--resume ` | `codex exec resume --json --skip-git-repo-check -c 'sandbox_mode="read-only"' [opzioni] -- ` | da verificare | | Modello | `--model` | `-m` / `--model` | da verificare | -| Effort | `--effort` | via `--config` (da verificare) | non noto | +| Effort | `--effort` | `-c model_reasoning_effort=` | non noto | +| Output usato | testo su stdout | JSONL su stdout (vedi sotto) | da verificare | + +**Codex: esito delle prove** +- L'id di sessione non si sceglie: compare nel primo evento JSONL `{"type":"thread.started","thread_id":""}`. Nel testo normale compare solo nell'intestazione su stderr. +- Con `--json`, stdout contiene solo eventi: `thread.started`, `turn.started`, `item.completed` (con `item.type = "agent_message"` e il testo della risposta; con `"error"` sono avvisi non fatali), `turn.completed`, e in caso di errore `error` e `turn.failed`. Il messaggio di errore è una stringa che contiene a sua volta un JSON (`error.message`): l'app estrae il messaggio interno. L'exit code è 1 se il turno fallisce. +- La risposta mostrata è l'**ultimo** `agent_message` del turno (come `--output-last-message`). +- `exec resume` **non accetta `-s/--sandbox`**: la sola lettura si ottiene con `-c 'sandbox_mode="read-only"'`, verificato (un file richiesto in scrittura non viene creato). Accetta `-m` e `-c`. +- `--skip-git-repo-check` serve perché la cartella dell'exe non è un repository Git. +- `--` prima del prompt evita che un prompt che inizia con `-` sia letto come opzione (verificato). +- Lo stdin va chiuso subito, come per Claude Code: altrimenti Codex stampa "Reading additional input from stdin…" e attende. +- Il modello predefinito è quello di `~/.codex/config.toml`. Se richiede una versione più recente di Codex l'errore è chiaro (`requires a newer version of Codex`): l'app lo mostra e [INSTALL.md](../INSTALL.md) indica `codex update`. +- Il catalogo modelli di Codex è dinamico (`~/.codex/models_cache.json`); l'app ne usa uno fisso, da aggiornare, con "Predefinito" e "Altro ID…" sempre disponibili. ## Conseguenze **Positive**: un solo flusso di avvio; nuovi motori aggiungibili con una definizione; elimina la duplicazione CLI/GUI. **Negative**: i flag delle CLI cambiano nel tempo e vanno mantenuti; le sessioni non si trasferiscono tra motori; più superficie da testare su due sistemi operativi. ## Rischi e da verificare -- [ ] **Codex**: l'id di sessione lo genera Codex e non l'app; va letto dall'output (probabilmente con output JSON) per riprendere la chat. +- [x] **Codex**: l'id di sessione lo genera Codex; letto dall'evento `thread.started` (vedi sopra). - [ ] **Antigravity**: segnalato un blocco di `agy -p` da sottoprocesso con output rediretto su Windows ([issue #318](https://github.com/google-antigravity/antigravity-cli/issues/318)). Provare con `--print-timeout` e prevedere un messaggio d'errore. I flag vanno messi prima di `-p`. Da provare anche su Linux. - [ ] **Forma dell'eseguibile**: `claude` è un .exe nativo avviabile direttamente; Codex e Antigravity, se installati con un gestore di pacchetti (es. npm), su Windows possono essere script `.cmd` che richiedono `cmd.exe /c`. Su Linux sono normali eseguibili o script con shebang. Verificare e gestire per motore. - [ ] Autenticazione: ogni CLI deve essere già autenticata; l'app mostra l'errore del motore senza gestire login. -- [ ] Formato dell'output (testo o JSON) e normalizzazione per ciascun motore. -- [ ] Mappatura effort e modelli per motore, con elenco modelli aggiornabile. +- [x] Formato dell'output di Codex: JSONL, normalizzato in testo markdown. Antigravity ancora da verificare. +- [x] Mappatura effort e modelli per Codex (liste fisse, da aggiornare). Antigravity da verificare. ## Riferimenti - Passi: [ROADMAP.md](../ROADMAP.md), Fase 4. diff --git a/docs/adr/0003-strategia-di-test.md b/docs/adr/0003-strategia-di-test.md index 93151f1..fd52e41 100644 --- a/docs/adr/0003-strategia-di-test.md +++ b/docs/adr/0003-strategia-di-test.md @@ -22,7 +22,7 @@ Oggi non esistono test. L'app gira su due sistemi operativi, ha una CLI, una GUI - **Livelli**: unitari (Core, ViewModel, renderer markdown), componenti GUI headless (finestra, template, tastiera), end-to-end sull'eseguibile pubblicato (CLI e codice 127) e smoke di publish per `win-x64` e `linux-x64`. - **Predisposizioni di testabilità** nel codice (da fare insieme ai primi test): - `ChatStore` diventa un'istanza con cartella iniettata, non uno statico legato a `LocalApplicationData`. - - L'esecuzione di `claude` sta dietro un'interfaccia (`IClaudeRunner`) iniettata nel ViewModel; l'implementazione reale resta in `Core/Claude.cs`. I test del ViewModel usano un finto runner; quelli di `Claude` usano `FakeClaude`. + - L'esecuzione dei motori sta dietro un'interfaccia (`IEngineRunner`) iniettata nel ViewModel; l'implementazione reale resta in `Core/EngineRunner.cs`. I test del ViewModel usano un finto runner; quelli di `EngineRunner` usano `FakeClaude` (che con la configurazione giusta imita anche Codex). - La conferma di eliminazione, gli appunti e il focus sono già delegati (`ConfirmDelete`, `CopyText`, `FocusInput`) e restano sostituibili. - Gli ADR-0002 (motori) richiedono che ogni motore abbia la propria batteria di test sugli argomenti e sul parser dell'output. - **Struttura del repo**: codice dell'app in `src/agent-wiki-ui/`, test in `tests/`, soluzione unica alla radice. Così il globbing del csproj dell'app non include i sorgenti dei test. Il progetto dell'app espone gli `internal` ai test con `InternalsVisibleTo`.