Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6080a65edc | ||
|
|
dc7dbbd3bb | ||
|
|
28201fbbda | ||
|
|
3587f35c2d | ||
|
|
b6574776b6 | ||
|
|
6bbaf7192f | ||
|
|
f2086de31d | ||
|
|
5870de6fda | ||
|
|
51993dd0c8 | ||
|
|
b3223fb579 | ||
|
|
15c664007d | ||
|
|
af171201b5 |
No files matched your search
@@ -4,7 +4,7 @@ Guida per gli agenti di codice (Claude Code, Gemini CLI, altri) che lavorano in
|
||||
|
||||
## 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`) o Codex (`codex exec`) 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`), Codex (`codex exec`) o Antigravity (`agy … -p`) da terminale. Senza argomenti apre una GUI di chat Avalonia, con argomenti funziona da CLI.
|
||||
|
||||
## Workflow doc-first (obbligatorio)
|
||||
|
||||
@@ -14,33 +14,34 @@ agent-wiki-ui (già `LlmRun`; il nome ha un'unica sorgente, vedi [docs/README.md
|
||||
|
||||
## Comandi
|
||||
|
||||
- Build: `dotnet build` (agent-wiki-ui.csproj, `net10.0`, Avalonia)
|
||||
- GUI: `dotnet run`; CLI: `dotnet run -- "prompt"`
|
||||
- Publish single-file (uno per sistema): `scripts/publish.sh` (Linux/WSL/macOS) o `scripts/publish.ps1` (Windows PowerShell): creano `dist/win-x64/` e `dist/linux-x64/` (`dist/` è ignorata da git); i due script vanno tenuti allineati, oppure a mano `dotnet publish -c Release -r win-x64 --self-contained -p:PublishSingleFile=true` e `-r linux-x64`
|
||||
- Test: non esistono ancora (pianificati, Fase 6: `dotnet test`; il piano è in docs/TESTING.md). Per verificare la GUI senza schermo si può usare `Avalonia.Headless` con `CaptureRenderedFrame()` in un progetto di prova fuori dal repo.
|
||||
- Build: `dotnet build` dalla radice (soluzione `agent-wiki-ui.slnx`: app in `src/agent-wiki-ui/`, test in `tests/`; `net10.0`, Avalonia)
|
||||
- GUI: `dotnet run --project src/agent-wiki-ui`; CLI: `dotnet run --project src/agent-wiki-ui -- "prompt"`
|
||||
- Publish single-file (uno per sistema): `scripts/publish.sh` (Linux/WSL/macOS) o `scripts/publish.ps1` (Windows PowerShell): creano `dist/win-x64/` e `dist/linux-x64/` (`dist/` è ignorata da git); i due script vanno tenuti allineati, oppure a mano `dotnet publish src/agent-wiki-ui -c Release -r win-x64 --self-contained -p:PublishSingleFile=true` e `-r linux-x64`
|
||||
- Test: `dotnet test` dalla radice (xUnit v3 + Avalonia.Headless con Skia reale; `tests/FakeClaude` è il `claude` finto per i test di processo). Per ora coprono le immagini e le formule del renderer, il `FakeClaude` e il motore Antigravity (AG-01…AG-12, UP-21); il resto è pianificato in docs/TESTING.md (Fase 6). Per verificare la GUI senza schermo si può usare `Avalonia.Headless` con `CaptureRenderedFrame()` in un progetto di prova fuori dal repo.
|
||||
|
||||
## Architettura
|
||||
|
||||
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 (`EngineRunner.RunInherited`, motore scelto con `--engine <id>` 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`, `FetchLatestVersionAsync` per il controllo delle versioni), `ClaudeEngine`, `CodexEngine`, registro `Engines` e ricerca nel PATH (`PathSearch`). Gli elenchi modelli sono fissi, da aggiornare quando ne escono di nuovi; `DefaultModelId` indica il modello più piccolo, scelto all'avvio insieme a effort `low` (se esce un modello più piccolo, aggiornarlo).
|
||||
- [Core/EngineRunner.cs](Core/EngineRunner.cs): **unico punto di avvio dei motori** (cartella dell'exe, stdin chiuso, `ArgumentList`): CLI con output ereditato, GUI con output catturato e interpretato da `Engine.Parse`, e `RunToolAsync` per i comandi di servizio (`--version`, `update`) con tempo massimo.
|
||||
- [Core/EngineUpdater.cs](Core/EngineUpdater.cs): controllo delle versioni in sola lettura (`CheckAsync`, `IsNewer`) e aggiornamento di un CLI con il suo `update` ufficiale (ADR-0004): versione prima/dopo, esito leggibile, metodo di installazione dedotto dal percorso reale e comando manuale da suggerire (l'app lo mostra, non lo esegue).
|
||||
- [Core/ChatStore.cs](Core/ChatStore.cs): cronologia, un JSON per chat in `<LocalApplicationData>/<nome-app>/chats/<guid>.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, interruttore CLI: cambiare motore apre una nuova chat; aggiornamento dei CLI con un solo aggiornamento alla volta, bloccando invio e cambio motore).
|
||||
- [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`).
|
||||
- [Program.cs](src/agent-wiki-ui/Program.cs): top-level statements. Con argomenti è la CLI (`EngineRunner.RunInherited`, motore scelto con `--engine <id>` come primo argomento, default Claude Code); senza avvia Avalonia (`App`).
|
||||
- [Core/Engine.cs](src/agent-wiki-ui/Core/Engine.cs): astrazione "motore" (ADR-0002): `Engine` (id, nome, eseguibile, modelli, effort, `BuildArgs`, `Parse`, `FetchLatestVersionAsync` per il controllo delle versioni), `ClaudeEngine`, `CodexEngine`, `AgyEngine`, registro `Engines` e ricerca nel PATH (`PathSearch`). Gli elenchi modelli sono fissi, da aggiornare quando ne escono di nuovi; `DefaultModelId` indica il modello più piccolo, scelto all'avvio insieme a effort `low` (se esce un modello più piccolo, aggiornarlo).
|
||||
- [Core/EngineRunner.cs](src/agent-wiki-ui/Core/EngineRunner.cs): **unico punto di avvio dei motori** (cartella dell'exe, stdin chiuso, `ArgumentList`): CLI con output ereditato, GUI con output catturato e interpretato da `Engine.Parse`, e `RunToolAsync` per i comandi di servizio (`--version`, `update`) con tempo massimo.
|
||||
- [Core/EngineUpdater.cs](src/agent-wiki-ui/Core/EngineUpdater.cs): controllo delle versioni in sola lettura (`CheckAsync`, `IsNewer`) e aggiornamento di un CLI con il suo `update` ufficiale (ADR-0004): versione prima/dopo, esito leggibile, metodo di installazione dedotto dal percorso reale e comando manuale da suggerire (l'app lo mostra, non lo esegue).
|
||||
- [Core/ChatStore.cs](src/agent-wiki-ui/Core/ChatStore.cs): cronologia, un JSON per chat in `<LocalApplicationData>/<nome-app>/chats/<guid>.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](src/agent-wiki-ui/Core/AppInfo.cs): nome dell'app, letto dall'`AssemblyName` del csproj (unica sorgente).
|
||||
- [Gui/MainViewModel.cs](src/agent-wiki-ui/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; aggiornamento dei CLI con un solo aggiornamento alla volta, bloccando invio e cambio motore).
|
||||
- [Gui/MainWindow.axaml](src/agent-wiki-ui/Gui/MainWindow.axaml) (+ `.cs`): layout XAML con binding compilati. [Gui/App.axaml](src/agent-wiki-ui/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](src/agent-wiki-ui/Gui/MarkdownView.cs): renderer markdown proprio (albero Markdig → controlli Avalonia, `DisableHtml`); la parte sulle immagini è in [Gui/MarkdownView.Images.cs](src/agent-wiki-ui/Gui/MarkdownView.Images.cs) (`partial`); le formule LaTeX (ADR-0008) sono in [Gui/MarkdownView.Math.cs](src/agent-wiki-ui/Gui/MarkdownView.Math.cs) (`partial`, libreria `Sylinko.CSharpMath.Avalonia`). Le immagini si mostrano solo da file locale (ADR-0006), mai da URL; i percorsi relativi si cercano nella cartella dell'exe e poi nelle superiori (radice della wiki).
|
||||
- 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 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`.
|
||||
- Lo stdin del motore va redirezionato e chiuso subito: `claude` aspetta 3 s, `codex` stampa "Reading additional input from stdin…" e attende, `agy` può bloccarsi su output rediretto (issue #318).
|
||||
- `claude` e `agy` sono eseguibili nativi (`~/.local/bin`, `~/.gemini/bin`), avviabili direttamente. Se fossero script `.cmd` (Windows) servirebbe `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 controllo delle versioni contatta solo `downloads.claude.ai` (Claude Code) e `api.github.com` (Codex, serve lo User-Agent), al massimo ogni 5 minuti e in background; un errore di rete non è un errore dell'app (stato "sconosciuto").
|
||||
- Antigravity (`AgyEngine`): eseguibile `agy`; tutti i flag di configurazione vanno passati prima di `-p <prompt>`; per usare `agy` in modalità non interattiva (`-p`) in sola lettura, nel file `~/.gemini/antigravity-cli/settings.json` deve essere presente la regola `'read_file(*)'` sotto `'permissions.allow'` (altrimenti headless mode nega `read_file` con errore jetski: 'jetski: no output produced - a tool required the read_file permission that headless mode cannot prompt for'), mentre scritture e comandi restano negati di default; non passare `--mode plan` né `--mode auto` e mai usare `--dangerously-skip-permissions`; per la GUI si usa `--output-format json` e l'id di sessione si legge da `conversation_id` (`OwnsSessionId => true`); la ripresa della conversazione usa `--conversation <id>`; per evitare il blocco su sottoprocesso con output rediretto (issue #318) si passa `--print-timeout 120s` e si chiude immediatamente lo stdin.
|
||||
- Il controllo delle versioni contatta solo `downloads.claude.ai` (Claude Code), `api.github.com` (Codex, serve lo User-Agent) e `antigravity-cli-auto-updater-974169037036.us-central1.run.app` (Antigravity), al massimo ogni 5 minuti e in background; un errore di rete non è un errore dell'app (stato "sconosciuto").
|
||||
- L'aggiornamento di un CLI è sempre un gesto esplicito dell'utente (pulsante Aggiorna): nessun aggiornamento automatico e nessun fallback che esegua npm/brew/winget al posto suo.
|
||||
- Windows: l'eseguibile è `WinExe` (nessuna console al doppio clic); in modalità CLI `Core/ConsoleHost.cs` riaggancia il terminale padre con `AttachConsole` e collega stdout/stderr a `CONOUT$` solo se non sono già reindirizzati (ADR-0005). Il codice deve poter fallire in silenzio senza terminale. Conseguenza: su Windows l'exit code della CLI arriva solo se il chiamante attende il processo.
|
||||
- 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).
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
# agent-wiki-ui
|
||||
|
||||
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.
|
||||
Eseguibile C# (.NET 10, Windows e Linux) che lancia Claude Code (`claude -p`), Codex (`codex exec`) o Antigravity (`agy … -p`) 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)
|
||||
- `claude` (Claude Code), `codex` (Codex) o `agy` (Antigravity) installati e nel PATH, già autenticati. Istruzioni: [docs/INSTALL.md](docs/INSTALL.md)
|
||||
- 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). Il riquadro **Impostazioni** (in alto a destra) permette di scegliere la **CLI** (Claude Code o Codex), il **modello** e l'**effort** di ragionamento. All'avvio, e a ogni cambio di CLI, sono scelti il modello più piccolo (Haiku 5.5 per Claude Code, GPT-5.6 Luna per Codex) ed effort `low`, per risposte rapide e economiche; "Predefinito" = nessun flag, vale la config del motore. Le chat si salvano su disco e l'ultima si riapre all'avvio. Sempre nel riquadro, la sezione **Aggiornamenti** dice se Claude Code e Codex hanno una versione nuova (pulsante evidenziato) e li aggiorna uno alla volta.
|
||||
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, Codex o Antigravity), il **modello** e l'**effort** di ragionamento. All'avvio, e a ogni cambio di CLI, sono scelti il modello più piccolo (Haiku 5.5 per Claude Code, GPT-5.6 Luna per Codex, Gemini 3.0 Flash Low per Antigravity) ed effort `low`, per risposte rapide e economiche; "Predefinito" = nessun flag, vale la config del motore. Le chat si salvano su disco e l'ultima si riapre all'avvio. Sempre nel riquadro, la sezione **Aggiornamenti** dice se Claude Code, Codex e Antigravity hanno una versione nuova (pulsante evidenziato) e li aggiorna uno alla volta.
|
||||
```
|
||||
dotnet run
|
||||
dotnet run --project src/agent-wiki-ui
|
||||
```
|
||||
|
||||
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.
|
||||
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; `--engine agy` usa Antigravity.
|
||||
```
|
||||
dotnet run -- "spiega questo progetto"
|
||||
dotnet run -- --engine codex "spiega questo progetto"
|
||||
dotnet run --project src/agent-wiki-ui -- "spiega questo progetto"
|
||||
dotnet run --project src/agent-wiki-ui -- --engine codex "spiega questo progetto"
|
||||
dotnet run --project src/agent-wiki-ui -- --engine agy "spiega questo progetto"
|
||||
```
|
||||
|
||||
## Creare gli eseguibili (Windows e Linux)
|
||||
@@ -25,7 +26,7 @@ Ogni eseguibile è un **unico file** che include il runtime .NET: chi lo usa non
|
||||
|
||||
### 1. Installare il .NET SDK 10
|
||||
- **Windows** (PowerShell o Prompt dei comandi): `winget install Microsoft.DotNet.SDK.10`, oppure il programma di installazione da [dotnet.microsoft.com/download](https://dotnet.microsoft.com/download).
|
||||
- **Linux / WSL / macOS**: pacchetto della distribuzione o script ufficiale da [dotnet.microsoft.com/download](https://dotnet.microsoft.com/download).
|
||||
- **Linux / WSL / macOS**: pacchetto della distribuzione o script ufficiale da [dotnet.microsoft.com/download](https://dotnet.microsoft.com/download). Funziona anche su ARM64 (Raspberry Pi): `curl -sSL https://dot.net/v1/dotnet-install.sh -o /tmp/dotnet-install.sh && bash /tmp/dotnet-install.sh --channel 10.0`, poi `export DOTNET_ROOT=$HOME/.dotnet PATH=$PATH:$HOME/.dotnet` (da mettere in `~/.bashrc`).
|
||||
- Controllo: `dotnet --version` deve stampare `10.x`.
|
||||
|
||||
### 2. Creare gli eseguibili
|
||||
@@ -45,11 +46,11 @@ Risultato (circa 47 MB per file), in `dist/`, ignorata da git:
|
||||
| Windows x64 | `dist/win-x64/agent-wiki-ui.exe` |
|
||||
| Linux x64 | `dist/linux-x64/agent-wiki-ui` |
|
||||
|
||||
Altri identificatori (per esempio `win-arm64`, `linux-arm64`) si possono provare allo stesso modo, ma non sono verificati. Lo script è solo una comodità: il comando sotto fa la stessa cosa.
|
||||
Altri identificatori (per esempio `win-arm64`, `linux-arm64`) si passano allo script allo stesso modo (`scripts/publish.sh linux-arm64`, risultato in `dist/linux-arm64/`), ma non sono verificati. Su un Raspberry Pi servono anche i motori: per Claude Code vedi [docs/INSTALL.md](docs/INSTALL.md). Lo script è solo una comodità: il comando sotto fa la stessa cosa.
|
||||
|
||||
**Senza script** (identico su qualsiasi sistema; sostituisci `<rid>` con `win-x64` o `linux-x64`):
|
||||
```
|
||||
dotnet publish -c Release -r <rid> --self-contained -p:PublishSingleFile=true -o dist/<rid>
|
||||
dotnet publish src/agent-wiki-ui -c Release -r <rid> --self-contained -p:PublishSingleFile=true -o dist/<rid>
|
||||
```
|
||||
Senza `-o` il risultato è in `bin/Release/net10.0/<rid>/publish/`.
|
||||
|
||||
@@ -59,10 +60,16 @@ Verificato: da Linux/WSL entrambi gli eseguibili; da Windows nativo (PowerShell
|
||||
- Windows: doppio clic su `agent-wiki-ui.exe` per la GUI (non si apre nessuna finestra di console), oppure da terminale `agent-wiki-ui.exe "prompt"` per la CLI. Da `cmd` o PowerShell il prompt torna subito e l'output compare dopo; per aspettare la fine e leggere l'exit code usa `start /wait agent-wiki-ui.exe "prompt"` (cmd) o `Start-Process -Wait -NoNewWindow agent-wiki-ui.exe "prompt"` (PowerShell); `& .\agent-wiki-ui.exe "prompt" | Out-Host` aspetta ma non dà l'exit code. Spiegazione in [ADR-0005](docs/adr/0005-windows-senza-console.md). Il file non è firmato: Windows può mostrare un avviso di sicurezza al primo avvio.
|
||||
- Linux: `chmod +x agent-wiki-ui` (se serve) e poi `./agent-wiki-ui`. La GUI richiede un desktop con X11 o XWayland.
|
||||
- Al primo avvio le librerie native (Skia, HarfBuzz) vengono estratte in una cartella temporanea: il primo avvio è un po' più lento.
|
||||
- I motori (`claude`, `codex`) devono essere installati e nel PATH dello stesso sistema: vedi [docs/INSTALL.md](docs/INSTALL.md).
|
||||
- I motori (`claude`, `codex`, `agy`) devono essere installati e nel PATH dello stesso sistema: vedi [docs/INSTALL.md](docs/INSTALL.md).
|
||||
|
||||
## Formule nelle risposte
|
||||
Le formule LaTeX si vedono composte: `$ ... $` nel testo e `$$ ... $$` a blocco ([ADR-0008](docs/adr/0008-formule-latex-nel-markdown.md)). Se il LaTeX non è valido compare il sorgente come testo; i `$` isolati (per esempio «10 $ e 20 $») restano testo. Le formule non sono selezionabili come testo, ma il pulsante **Copia** della risposta copia il markdown con il sorgente.
|
||||
|
||||
## Immagini nelle risposte
|
||||
L'app mostra il file indicato dal percorso scritto nella risposta, ovunque stia; non conosce né cartelle né cataloghi di immagini. Se la risposta contiene `` e il file esiste, la chat mostra l'immagine; altrimenti compare il testo `[didascalia]`. Gli URL `http(s)` non si scaricano. Un percorso relativo si cerca nella cartella dell'eseguibile e poi in quelle superiori, quindi funziona anche se l'eseguibile sta in una sottocartella del progetto. Dettagli: [ADR-0006](docs/adr/0006-immagini-nel-markdown.md); per una convenzione di progetto interrogabile con immagini vedi [ADR-0007](docs/adr/0007-schema-wiki-per-query-con-immagini.md).
|
||||
|
||||
## Piattaforme
|
||||
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).
|
||||
Windows e Linux con lo stesso codice ([ADR-0001](docs/adr/0001-gui-multipiattaforma-avalonia.md)). Motori supportati: Claude Code, Codex CLI e Antigravity CLI, già installati da terminale: [ADR-0002](docs/adr/0002-motori-cli-claude-codex-antigravity.md).
|
||||
|
||||
> **Stato: MVP** (prima della 1.0): la parte funzionale c'è, i test automatici e alcune verifiche su macchine reali mancano. Dettagli in [docs/PROJECT_STATE.md](docs/PROJECT_STATE.md).
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<Solution>
|
||||
<Folder Name="/src/">
|
||||
<Project Path="src/agent-wiki-ui/agent-wiki-ui.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/tests/">
|
||||
<Project Path="tests/agent-wiki-ui.Tests/agent-wiki-ui.Tests.csproj" />
|
||||
<Project Path="tests/FakeClaude/FakeClaude.csproj" />
|
||||
</Folder>
|
||||
</Solution>
|
||||
+56
-17
@@ -1,4 +1,4 @@
|
||||
# Installare i motori (Claude Code e Codex)
|
||||
# Installare i motori (Claude Code, Codex, Antigravity)
|
||||
|
||||
L'app non installa né autentica i motori: lancia quelli che trova nel PATH, già installati e con l'accesso fatto ([ADR-0002](adr/0002-motori-cli-claude-codex-antigravity.md)). Questa pagina spiega come prepararli. Fonti e data di consultazione in fondo.
|
||||
|
||||
@@ -8,10 +8,10 @@ Il motore va installato **nello stesso ambiente in cui gira l'app**:
|
||||
| Dove avvii l'app | Dove installare il motore |
|
||||
|---|---|
|
||||
| Linux nativo | nel terminale Linux |
|
||||
| WSL (anche con `dotnet run`) | nel terminale della distribuzione WSL |
|
||||
| WSL (anche con `dotnet run --project src/agent-wiki-ui`) | nel terminale della distribuzione WSL |
|
||||
| Windows (`.exe`) | in Windows (PowerShell), non in WSL |
|
||||
|
||||
Dentro WSL il PATH di Windows è visibile (compare `/mnt/c/…`): se in WSL non c'è un `claude` o un `codex`, il comando può risolvere nella versione Windows. Controllalo con `command -v claude` e `command -v codex`.
|
||||
Dentro WSL il PATH di Windows è visibile (compare `/mnt/c/…`): se in WSL non c'è un `claude`, `codex` o `agy`, il comando può risolvere nella versione Windows. Controllalo con `command -v claude`, `command -v codex` e `command -v agy`.
|
||||
|
||||
## Claude Code
|
||||
|
||||
@@ -31,6 +31,17 @@ Dentro WSL il PATH di Windows è visibile (compare `/mnt/c/…`): se in WSL non
|
||||
|
||||
L'installazione nativa non mostra avanzamento durante il download. A fine installazione apri un **nuovo** terminale.
|
||||
|
||||
**Da zero su Linux, anche ARM64 (per esempio Raspberry Pi 5 con Raspberry Pi OS a 64 bit).** Il metodo nativo riconosce da solo l'architettura (`uname -m` deve dare `aarch64` o `x86_64`):
|
||||
```
|
||||
sudo apt install -y curl # se manca
|
||||
curl -fsSL https://claude.ai/install.sh | bash
|
||||
# apri un nuovo terminale, poi:
|
||||
command -v claude # atteso: ~/.local/bin/claude
|
||||
claude --version
|
||||
claude # primo avvio: accesso nel browser
|
||||
```
|
||||
Su un Raspberry senza schermo l'accesso nel browser si fa da un altro dispositivo, seguendo l'indirizzo che `claude` stampa nel terminale (per esempio via SSH); in alternativa usa una API key o un provider cloud, vedi "Accesso". Se `~/.local/bin` non è nel PATH aggiungi `export PATH="$HOME/.local/bin:$PATH"` a `~/.bashrc`. Con la sola architettura ARM64 e Debian 10+ i requisiti sono soddisfatti; il metodo npm è l'alternativa se lo script nativo non funziona. Non provato su un Raspberry reale: le istruzioni vengono dalla guida ufficiale.
|
||||
|
||||
**Accesso**: esegui `claude` e segui le istruzioni nel browser. Serve un account Pro, Max, Team, Enterprise o Console (il piano gratuito di claude.ai non include Claude Code). In alternativa Amazon Bedrock, Google Cloud o Microsoft Foundry.
|
||||
|
||||
**Verifica**
|
||||
@@ -73,32 +84,60 @@ codex exec --help # uso non interattivo, usato dall'app
|
||||
|
||||
**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.
|
||||
|
||||
## Aggiornare un CLI dall'app
|
||||
Nel riquadro Impostazioni, sezione **Aggiornamenti**, l'app controlla da sola, all'avvio e all'apertura del riquadro, se c'è una versione più recente (Claude Code: `downloads.claude.ai`; Codex: release di GitHub): se c'è, la riga dice "Aggiornamento disponibile", il pulsante **Aggiorna** si evidenzia e compare un puntino sul pulsante Impostazioni. Il pulsante esegue `claude update` o `codex update` (controllano e installano solo se c'è una versione nuova). Nulla si aggiorna senza un clic. Se non va a buon fine l'app mostra l'errore e il comando manuale, che dipende da come hai installato il CLI:
|
||||
## Antigravity CLI (`agy`)
|
||||
|
||||
| Installazione | Claude Code | Codex |
|
||||
|---|---|---|
|
||||
| npm | `npm install -g @anthropic-ai/claude-code@latest` | `npm install -g @openai/codex@latest` |
|
||||
| Homebrew | `brew upgrade claude-code` | `brew upgrade --cask codex` |
|
||||
| WinGet | `winget upgrade Anthropic.ClaudeCode` | reinstalla con lo script PowerShell |
|
||||
| Nativo / script | `claude update` | `codex update` (o rilancia lo script di installazione) |
|
||||
**Requisiti**: Linux x64 (binario nativo ELF) o Windows x64 (eseguibile nativo `.exe`).
|
||||
|
||||
**Installazione**: binario nativo distribuito ufficialmente (su Linux collocato in `~/.gemini/bin/agy` o in una cartella inclusa nel PATH; su Windows `agy.exe`).
|
||||
|
||||
**Accesso**: autenticazione tramite account Google/Antigravity (`agy` o configurazione di ambiente).
|
||||
|
||||
**Verifica**
|
||||
```
|
||||
agy --version # stampa la versione, es. 1.4.2
|
||||
agy models # catalogo modelli disponibili
|
||||
agy update # controlla e aggiorna alla versione più recente
|
||||
```
|
||||
|
||||
**Permessi per la sola lettura non interattiva**: per usare `agy` in modalità non interattiva (`-p`) in sola lettura, nel file `~/.gemini/antigravity-cli/settings.json` deve essere presente la regola `'read_file(*)'` sotto `'permissions.allow'` (altrimenti headless mode nega `read_file` con errore jetski: `a tool required the read_file permission that headless mode cannot prompt for`), mentre scritture e comandi restano negati di default:
|
||||
|
||||
```json
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"read_file(*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Aggiornare un CLI dall'app
|
||||
Nel riquadro Impostazioni, sezione **Aggiornamenti**, l'app controlla da sola, all'avvio e all'apertura del riquadro, se c'è una versione più recente (Claude Code: `downloads.claude.ai`; Codex: release di GitHub; Antigravity: endpoint ufficiale di auto-update): se c'è, la riga dice "Aggiornamento disponibile", il pulsante **Aggiorna** si evidenzia e compare un puntino sul pulsante Impostazioni. Il pulsante esegue `claude update`, `codex update` o `agy update` (controllano e installano solo se c'è una versione nuova). Nulla si aggiorna senza un clic. Se non va a buon fine l'app mostra l'errore e il comando manuale, che dipende da come hai installato il CLI:
|
||||
|
||||
| Installazione | Claude Code | Codex | Antigravity |
|
||||
|---|---|---|---|
|
||||
| npm | `npm install -g @anthropic-ai/claude-code@latest` | `npm install -g @openai/codex@latest` | `agy update` |
|
||||
| Homebrew | `brew upgrade claude-code` | `brew upgrade --cask codex` | `brew upgrade agy` |
|
||||
| WinGet | `winget upgrade Anthropic.ClaudeCode` | reinstalla con lo script PowerShell | `agy update` |
|
||||
| Nativo / script | `claude update` | `codex update` (o rilancia lo script di installazione) | `agy update` |
|
||||
|
||||
Dettagli e decisioni: [ADR-0004](adr/0004-aggiornamento-dei-cli.md).
|
||||
|
||||
## Controllo finale per l'app
|
||||
Dallo stesso terminale da cui avvii l'app:
|
||||
```
|
||||
command -v claude codex # Linux/WSL; in PowerShell: Get-Command claude, codex
|
||||
command -v claude codex agy # Linux/WSL; in PowerShell: Get-Command claude, codex, agy
|
||||
claude -p "rispondi solo: ok"
|
||||
codex exec "rispondi solo: ok"
|
||||
agy -p "rispondi solo: ok"
|
||||
```
|
||||
Se i comandi rispondono, l'app li trova. Se l'app dice "'claude' non trovato nel PATH":
|
||||
Se i comandi rispondono, l'app li trova. Se l'app dice che un motore non è trovato nel PATH:
|
||||
- apri un nuovo terminale (il PATH si aggiorna solo ai nuovi);
|
||||
- controlla che la cartella d'installazione sia nel PATH: per Claude Code nativo e per Codex con script è `~/.local/bin` su Linux/macOS/WSL, `%USERPROFILE%\.local\bin` su Windows (per Claude Code);
|
||||
- controlla che la cartella d'installazione sia nel PATH (`~/.local/bin`, `~/.gemini/bin`, o `%USERPROFILE%\.local\bin` su Windows);
|
||||
- verifica di non aver installato in un ambiente diverso da quello dell'app (tabella iniziale).
|
||||
|
||||
## Fonti
|
||||
- Claude Code: documentazione ufficiale "Advanced setup", consultata il 2026-10-15 (`code.claude.com/docs/en/setup`).
|
||||
- Codex: README del repository `github.com/openai/codex`, consultato il 2026-10-15. La pagina openai.com/it-IT/codex/ non era raggiungibile (HTTP 403).
|
||||
- Comandi `--version`, `login status`, `auth status` e `exec --help`: provati sulla macchina descritta sopra.
|
||||
- Antigravity CLI (`agy`): non ancora trattato, da aggiungere quando verrà integrato.
|
||||
- Codex: README del repository `github.com/openai/codex`, consultato il 2026-10-15.
|
||||
- Antigravity CLI: binario `agy` verificato in ambiente reale il 2026-10-18.
|
||||
- Comandi `--version`, `agy models`, `agy update`: provati sulla macchina descritta sopra.
|
||||
+9
-9
@@ -7,11 +7,11 @@ Eseguibile C# a riga di comando che inoltra un prompt a Claude Code (`claude`) l
|
||||
```
|
||||
<nome-app> "spiega questo progetto"
|
||||
```
|
||||
Tutti gli argomenti sono uniti in un unico prompt. Con `--engine <id>` come primo argomento (`claude` o `codex`) si sceglie il motore, per esempio `<nome-app> --engine codex "spiega questo progetto"`; senza, si usa Claude Code. Id sconosciuto o prompt mancante: uso su stderr ed exit code 2.
|
||||
Tutti gli argomenti sono uniti in un unico prompt. Con `--engine <id>` come primo argomento (`claude`, `codex` o `agy`) si sceglie il motore, per esempio `<nome-app> --engine agy "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 il motore scelto in modalità non interattiva (`claude -p <prompt>` o `codex exec … -- <prompt>`) con `WorkingDirectory = AppContext.BaseDirectory`.
|
||||
2. Avvia il motore scelto in modalità non interattiva (`claude -p <prompt>`, `codex exec … -- <prompt>` o `agy … -p <prompt>`) con `WorkingDirectory = AppContext.BaseDirectory`.
|
||||
3. Ritrasmette stdout e stderr del processo figlio in tempo reale.
|
||||
4. Esce con lo stesso exit code del motore. **Su Windows** l'eseguibile non ha finestra di console ([ADR-0005](adr/0005-windows-senza-console.md)): `cmd` e PowerShell non aspettano la fine del programma, quindi l'exit code è disponibile solo se il chiamante attende il processo (`start /wait`, `Start-Process -Wait`, `| Out-Host`, script).
|
||||
5. Se l'eseguibile del motore non è trovato: messaggio chiaro ed exit code 127.
|
||||
@@ -21,12 +21,12 @@ Senza argomenti si apre una finestra Avalonia (C# + XAML, nessun HTML) in stile
|
||||
|
||||
**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.
|
||||
- **Conversazione**: messaggi alternati utente/Claude. Le risposte sono renderizzate in markdown: titoli, liste, tabelle, citazioni, blocchi di codice, grassetto/corsivo/barrato, codice inline, link, immagini, formule LaTeX (`$…$` nel testo, `$$…$$` a blocco; [ADR-0008](adr/0008-formule-latex-nel-markdown.md); se il LaTeX non è valido compare il sorgente). Le immagini `` si mostrano se il percorso è locale (assoluto, `file://` o relativo: cercato nella cartella dell'exe e poi nelle cartelle superiori, cioè nella radice del progetto del motore) e il file è un'immagine leggibile entro i limiti (caricata in background: prima compare `[alt]`, poi l'immagine); altrimenti, e sempre per gli URL `http(s)`, compare il testo alternativo `[alt]` e non si scarica nulla ([ADR-0006](adr/0006-immagini-nel-markdown.md)). Il rendering è un renderer proprio ([MarkdownView.cs](../src/agent-wiki-ui/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 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). **Valori iniziali**: il modello più piccolo del motore (Claude Code: Haiku 5.5; Codex: GPT-5.6 Luna) ed effort `low`, per risposte rapide e poco costose; sono quelli che si ritrovano all'avvio e a ogni cambio di CLI. 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.
|
||||
- **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, Antigravity; un motore non installato è mostrato "non installato" e non è selezionabile; disabilitato durante l'attesa), poi **Modello** (Predefinito, catalogo del motore, "Altro ID…" con campo libero) ed **Effort** (Predefinito, low, medium, high, xhigh, max). **Valori iniziali**: il modello più piccolo del motore (Claude Code: Haiku 5.5; Codex: GPT-5.6 Luna; Antigravity: Gemini 3.0 Flash Low) ed effort `low`, per risposte rapide e poco costose; sono quelli che si ritrovano all'avvio e a ogni cambio di CLI. 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.
|
||||
- **Aggiornamenti** (nello stesso riquadro, sotto le impostazioni): una riga per ogni CLI installato con nome, versione e pulsante **Aggiorna**. L'app **controlla prima** se esiste una versione più recente (all'avvio, all'apertura del riquadro se l'ultimo controllo ha più di 5 minuti, e dopo ogni aggiornamento): se c'è, la riga dice "Aggiornamento disponibile: X", il pulsante è evidenziato e un puntino compare sul pulsante Impostazioni; se no, la riga dice "Aggiornato" e nulla è evidenziato; senza rete "Controllo non riuscito". Il pulsante esegue il comando ufficiale `<cli> update` e mostra l'esito sulla riga: "Aggiornato da X a Y", "Già aggiornato (X)" o l'errore con il comando manuale adatto al metodo di installazione (npm, Homebrew, WinGet, nativo). Un solo aggiornamento alla volta; i pulsanti sono disabilitati durante una risposta. Nessun aggiornamento parte da solo. Vedi [ADR-0004](adr/0004-aggiornamento-dei-cli.md).
|
||||
- **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 ai valori iniziali del nuovo motore (il modello più piccolo ed effort `low`) 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.
|
||||
- **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 ai valori iniziali del nuovo motore (il modello più piccolo ed effort `low`) e le liste dipendono dal motore (Antigravity: modelli Gemini Flash / Claude / GPT-OSS, effort fino a max). 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 e Antigravity la sessione è letta dall'output (`thread_id` per Codex, `conversation_id` per Antigravity) e usata per riprendere la chat (`--conversation` su agy); i permessi sono di sola lettura (`sandbox_mode="read-only"` su Codex; su Antigravity in headless mode `-p` la sola lettura è garantita dalla modalità predefinita senza flag `--mode`).
|
||||
- **Contesto**: `claude -p --session-id <guid>` al primo messaggio, `--resume <guid>` ai successivi. "Nuova chat" svuota la vista e genera un nuovo guid.
|
||||
- **Cronologia**: dopo ogni scambio la chat è salvata in `<cartella dati>/<nome-app>/chats/<guid>.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 <guid>`, 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.
|
||||
@@ -35,7 +35,7 @@ Senza argomenti si apre una finestra Avalonia (C# + XAML, nessun HTML) in stile
|
||||
- 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). I token vivono come risorse di tema in [Gui/App.axaml](../Gui/App.axaml).
|
||||
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](../src/agent-wiki-ui/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**: 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).
|
||||
@@ -46,14 +46,14 @@ Sorgente di verità per l'aspetto, derivato dalla skill `ui-ux-pro-max` (stile "
|
||||
|
||||
## Scelte tecniche
|
||||
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.
|
||||
- .NET 10, un solo progetto `net10.0` per Windows e Linux. Avalonia (GUI), Markdig (parsing markdown), CSharpMath (formule LaTeX). 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 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 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).
|
||||
- L'avvio dei motori è in un solo punto ([Core/EngineRunner.cs](../src/agent-wiki-ui/Core/EngineRunner.cs)), usato da CLI e GUI; ogni motore descrive argomenti e parser in [Core/Engine.cs](../src/agent-wiki-ui/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).
|
||||
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 e Antigravity ([ADR-0002](adr/0002-motori-cli-claude-codex-antigravity.md)), logging su file, supporto macOS. I test automatici sono pianificati: [TESTING.md](TESTING.md).
|
||||
|
||||
## Criteri di accettazione
|
||||
Ogni criterio ha test automatici nella matrice di [TESTING.md](TESTING.md) (CL-04, CL-08, CL-06, CL-01).
|
||||
|
||||
+13
-9
@@ -1,20 +1,20 @@
|
||||
# Stato del progetto
|
||||
|
||||
> **Stadio: MVP** (prima di una versione 1.0). Ultimo aggiornamento: 2026-10-15.
|
||||
> **Stadio: MVP** (prima di una versione 1.0). Ultimo aggiornamento: 2026-10-18.
|
||||
> Questo documento è la fotografia dello stato, non la fonte di dettagli: i passi sono in [ROADMAP.md](ROADMAP.md), i requisiti in [MVP.md](MVP.md), le decisioni negli ADR (indice in [README.md](README.md)), i test in [TESTING.md](TESTING.md). Qui si legge a colpo d'occhio dove siamo; per il resto si segue il collegamento.
|
||||
|
||||
## In una frase
|
||||
`agent-wiki-ui` è un eseguibile C# (.NET 10) per Windows e Linux che inoltra prompt a Claude Code e Codex già installati, da riga di comando o da una GUI di chat. La parte funzionale dell'MVP c'è; mancano i test automatici e alcune verifiche su macchine reali.
|
||||
`agent-wiki-ui` è un eseguibile C# (.NET 10) per Windows e Linux che inoltra prompt a Claude Code, Codex e Antigravity già installati, da riga di comando o da una GUI di chat. La parte funzionale dell'MVP c'è; mancano i test automatici e alcune verifiche su macchine reali.
|
||||
|
||||
## Stato per area
|
||||
| Area | Stato | Note |
|
||||
|---|---|---|
|
||||
| CLI (`agent-wiki-ui "prompt"`, `--engine`) | Funzionante | Exit code e 127 provati su Linux; su Windows l'exit code arriva solo se il chiamante attende il processo ([ADR-0005](adr/0005-windows-senza-console.md)). |
|
||||
| GUI di chat (Avalonia) | Funzionante | Barra laterale, cronologia su disco, markdown, tema chiaro/scuro, Impostazioni. Provata con screenshot headless e su WSLg; non su Windows nativo. |
|
||||
| Motori | Claude Code e Codex | Una chat = un motore; sessione di Codex letta dall'output. Antigravity non iniziato ([ADR-0002](adr/0002-motori-cli-claude-codex-antigravity.md)). |
|
||||
| Motori | Claude Code, Codex, Antigravity | Una chat = un motore; sessione di Codex (`thread_id`) e Antigravity (`conversation_id`) lette dall'output. Sola lettura di default ([ADR-0002](adr/0002-motori-cli-claude-codex-antigravity.md)). |
|
||||
| Aggiornamento dei CLI | Funzionante, non provato in reale | Controllo preventivo e pulsante evidenziato; l'`update` vero non è mai stato lanciato dall'app ([ADR-0004](adr/0004-aggiornamento-dei-cli.md)). |
|
||||
| Eseguibili standalone | Funzionanti | `win-x64` e `linux-x64` creati da entrambi i sistemi con `scripts/publish.sh` / `.ps1` (circa 47 MB). Windows senza console al doppio clic. |
|
||||
| Test automatici | **Non iniziati** | Piano e matrice pronti ([ADR-0003](adr/0003-strategia-di-test.md)); nessun test scritto. Vedi debiti. |
|
||||
| Test automatici | **Avviati** | Piano e matrice pronti ([ADR-0003](adr/0003-strategia-di-test.md)); scritti i test di immagini e formule del renderer, `FakeClaude` e motore Antigravity (AG-01…AG-12, UP-21). Il resto della matrice manca. Vedi debiti. |
|
||||
| Documentazione | Allineata al codice | Doc-first; ogni funzione ha le righe della matrice in TESTING.md. |
|
||||
|
||||
## Criteri di accettazione dell'MVP
|
||||
@@ -28,29 +28,33 @@ Da [MVP.md](MVP.md). Restano non spuntati finché non hanno test automatici (reg
|
||||
| Prompt con virgolette e spazi intatto | Sì, con un `claude` finto su Linux | No (CL-01) |
|
||||
|
||||
## Debiti noti
|
||||
- **Test automatici assenti** (Fase 6): contraddice la regola "nuova funzione = nuovi test" per tutto il codice scritto finora. È il debito principale.
|
||||
- **Antigravity mai provato con `agy` reale**: flag, forma del JSON, sola lettura in headless, nomi dei modelli e URL dell'auto-updater sono dedotti dal lavoro di Benedetta e coperti solo da test con output finto.
|
||||
- **Test automatici incompleti** (Fase 6): restano scoperti `Core/` (tranne Antigravity), `MainViewModel`, finestra e criteri di accettazione; contraddice la regola "nuova funzione = nuovi test" per tutto il codice scritto finora. È il debito principale.
|
||||
- **Verifiche su macchine reali**: GUI su Windows nativo e su una distribuzione Linux pulita; doppio clic senza console e riaggancio al terminale (ADR-0005); tema che segue il sistema su Linux; `update` reale dei CLI.
|
||||
- **Impostazioni non salvate su disco**: modello, effort e motore tornano ai valori iniziali a ogni avvio (si parte dal modello più piccolo con effort `low`).
|
||||
- **Nessuno streaming nella GUI**: la risposta compare a fine generazione.
|
||||
- **Elenchi dei modelli fissi** (Claude Code e Codex): da aggiornare a mano; il "modello più piccolo" di Codex è un'assunzione (GPT-5.6 Luna).
|
||||
- **Elenchi dei modelli fissi** (Claude Code, Codex, Antigravity): da aggiornare a mano; il "modello più piccolo" di Codex è un'assunzione (GPT-5.6 Luna), per Antigravity è `gemini-3.0-flash-low`.
|
||||
|
||||
## Decisioni
|
||||
| ADR | Stato |
|
||||
|---|---|
|
||||
| [0001](adr/0001-gui-multipiattaforma-avalonia.md) GUI Avalonia multipiattaforma | Accettata |
|
||||
| [0002](adr/0002-motori-cli-claude-codex-antigravity.md) Più motori CLI | Accettata (Claude Code, Codex) |
|
||||
| [0002](adr/0002-motori-cli-claude-codex-antigravity.md) Più motori CLI | Accettata (Claude Code, Codex, Antigravity) |
|
||||
| [0003](adr/0003-strategia-di-test.md) Strategia di test | Proposta |
|
||||
| [0004](adr/0004-aggiornamento-dei-cli.md) Aggiornamento dei CLI | Proposta |
|
||||
| [0005](adr/0005-windows-senza-console.md) Windows senza console | Proposta |
|
||||
| [0006](adr/0006-immagini-nel-markdown.md) Immagini nel markdown (solo file locali) | Proposta (implementata, test verdi) |
|
||||
| [0007](adr/0007-schema-wiki-per-query-con-immagini.md) Schema di una wiki interrogabile, con immagini | Proposta (solo documentazione; nessun codice) |
|
||||
| [0008](adr/0008-formule-latex-nel-markdown.md) Formule LaTeX nel markdown | Proposta (implementata) |
|
||||
|
||||
## Per chiudere l'MVP
|
||||
Proposta, derivata dai punti aperti sopra; da confermare.
|
||||
1. Test automatici per `Core/` e `MainViewModel` e per i quattro criteri di accettazione (Fase 6, prime righe di CL, ST, VM, CX).
|
||||
1. Test automatici per `Core/` e `MainViewModel` e per i quattro criteri di accettazione (Fase 6, prime righe di CL, ST, VM, CX, AG).
|
||||
2. Gli ADR 0003, 0004 e 0005 passati ad "Accettata" dopo le loro verifiche.
|
||||
3. GUI provata su Windows nativo e su Linux pulito.
|
||||
4. Criteri di accettazione di MVP.md spuntati.
|
||||
|
||||
Oltre l'MVP (non bloccanti): Antigravity, impostazioni persistite, streaming nella GUI, rinomina e ricerca delle chat, CI con matrice Windows e Linux, firma dell'eseguibile Windows.
|
||||
Oltre l'MVP (non bloccanti): impostazioni persistite, streaming nella GUI, rinomina e ricerca delle chat, CI con matrice Windows e Linux, firma dell'eseguibile Windows.
|
||||
|
||||
## Come tenere aggiornato questo file
|
||||
Si aggiorna nello stesso commit di ogni cambio di stadio, di funzione, di ADR o di debito: stato per area, criteri, debiti, tabella delle decisioni, data. Se una riga qui contraddice ROADMAP.md o un ADR, vale quello e questa pagina va corretta.
|
||||
+5
-2
@@ -17,7 +17,7 @@ Indice e regole. Ogni informazione vive in **un solo documento**; gli altri la r
|
||||
Il nome è **`agent-wiki-ui`** (prima `LlmRun`). Questa sezione è l'unica sorgente: gli altri doc scrivono "l'app" o `<nome-app>`.
|
||||
|
||||
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 `<nome-app>.exe` / `<nome-app>`
|
||||
- [ ] `AssemblyName` in [../src/agent-wiki-ui/agent-wiki-ui.csproj](../src/agent-wiki-ui/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 `<nome-app>.exe` / `<nome-app>`
|
||||
- [ ] `RootNamespace` e namespace C# (`AgentWikiUi.*`, anche in `x:Class` e `xmlns` dei file `.axaml`): il nome del namespace non può avere trattini
|
||||
- [ ] **Cartella dati** `<LocalApplicationData>/<nome-app>/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 [../AGENT.md](../AGENT.md) (nome del csproj nei comandi)
|
||||
@@ -48,7 +48,10 @@ Non si scrive nulla nei puntatori: ogni modifica va in AGENT.md. Per un altro st
|
||||
| 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 | Accettata (Claude Code, Codex) |
|
||||
| [0002](adr/0002-motori-cli-claude-codex-antigravity.md) | Più motori CLI (Claude Code, Codex, Antigravity) su Windows e Linux | Accettata (Claude Code, Codex, Antigravity) |
|
||||
| [0003](adr/0003-strategia-di-test.md) | Strategia di test e regola "nuova funzione = nuovi test" | Proposta |
|
||||
| [0004](adr/0004-aggiornamento-dei-cli.md) | Aggiornamento dei CLI dalle Impostazioni | Proposta |
|
||||
| [0005](adr/0005-windows-senza-console.md) | Windows: eseguibile senza finestra di console | Proposta |
|
||||
| [0006](adr/0006-immagini-nel-markdown.md) | Immagini nel markdown: solo file locali | Proposta |
|
||||
| [0007](adr/0007-schema-wiki-per-query-con-immagini.md) | Schema di una wiki interrogabile, con immagini | Proposta |
|
||||
| [0008](adr/0008-formule-latex-nel-markdown.md) | Formule LaTeX nel markdown | Proposta |
|
||||
+12
-8
@@ -22,26 +22,30 @@ Dettagli dell'MVP in [MVP.md](MVP.md). Stadio attuale e debiti: [PROJECT_STATE.m
|
||||
## Fase 3 — Oltre
|
||||
- [x] GUI: cronologia chat (salvataggio JSON, barra laterale, riapertura con `--resume`, eliminazione)
|
||||
- [ ] GUI: streaming della risposta (`--output-format stream-json`), stop, rinomina/ricerca chat
|
||||
- [x] GUI: immagini nel markdown da percorso locale, con ripiego sul testo alternativo ([ADR-0006](adr/0006-immagini-nel-markdown.md); test MD-14, MD-19…MD-30); immagini remote rimandate
|
||||
- [x] Schema di una wiki interrogabile con immagini: convenzione per le istruzioni solo-query, l'indice e il catalogo ([ADR-0007](adr/0007-schema-wiki-per-query-con-immagini.md)); da provare con la chat reale su una wiki di esempio
|
||||
- [x] GUI: formule LaTeX nel markdown, inline `$…$` e a blocco `$$…$$`, con ripiego sul sorgente ([ADR-0008](adr/0008-formule-latex-nel-markdown.md); test MD-31…MD-38)
|
||||
- [ ] Config file accanto all'exe
|
||||
- [ ] Test automatici e CI (vedi Fase 6)
|
||||
|
||||
## 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. Il punto unico di avvio processo l'ha estratto la Fase 5.
|
||||
|
||||
- [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] Verificare i flag reali di `codex` e `agy` (provati su Linux/WSL, ADR-0002 accettata per Claude Code, Codex e Antigravity); Windows da verificare
|
||||
- [x] Astrazione "motore" in C# (`src/agent-wiki-ui/Core/Engine.cs`, `src/agent-wiki-ui/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)
|
||||
- [x] Permessi di Codex in sola lettura (nessun flag che salta i controlli)
|
||||
- [x] Motore Antigravity: gestione del blocco con output rediretto (`--print-timeout 120s` prima di `-p`, chiusura immediata di stdin)
|
||||
- [x] Motore Antigravity: lettura dell'id di sessione dall'output (`conversation_id`) e ripresa con `--conversation`
|
||||
- [x] Permessi di Codex e Antigravity in sola lettura (`sandbox_mode="read-only"` per Codex; per Antigravity in headless mode `-p` sola lettura garantita dalla modalità di default senza `--mode`; nessun flag che salta i controlli)
|
||||
- [ ] Opzione esplicita per concedere permessi di scrittura
|
||||
- [x] CLI: opzione `--engine`, coerente con la GUI
|
||||
- [x] CLI: opzione `--engine`, coerente con la GUI (supporta `claude`, `codex`, `agy`)
|
||||
- [x] Aggiornamento dei CLI dalle Impostazioni, un pulsante per CLI con `update` ufficiale ed esito leggibile ([ADR-0004](adr/0004-aggiornamento-dei-cli.md))
|
||||
- [x] Controllo preventivo della versione più recente (sorgenti ufficiali, cache di 5 minuti); pulsante Aggiorna evidenziato e puntino sul pulsante Impostazioni solo se c'è una versione nuova
|
||||
- [ ] Provare gli aggiornamenti reali (finora provati solo con CLI finti, e rilevamento del metodo di installazione sui percorsi reali) su Linux e Windows (installazione nativa e npm) e portare l'ADR-0004 ad "Accettata"
|
||||
- [ ] Provare gli aggiornamenti reali (finora provati solo con CLI finti, e rilevamento del metodo di installazione sui percorsi reali; provato `agy update`) su Linux e Windows (installazione nativa e npm) e portare l'ADR-0004 ad "Accettata"
|
||||
|
||||
## Fase 5 — Multipiattaforma (Windows + Linux), GUI in solo C#
|
||||
Decisione e motivazioni: [ADR-0001](adr/0001-gui-multipiattaforma-avalonia.md). Qui solo i passi.
|
||||
@@ -64,9 +68,9 @@ Decisione e motivazioni: [ADR-0001](adr/0001-gui-multipiattaforma-avalonia.md).
|
||||
Decisione: [ADR-0003](adr/0003-strategia-di-test.md). Piano, soglie e matrice: [TESTING.md](TESTING.md). Da questa fase in poi vale la regola: ogni nuova funzione arriva con i suoi test.
|
||||
|
||||
- [ ] Verificare le compatibilità dell'ADR-0003 (xUnit, Avalonia.Headless, coverlet, `dotnet test`) e portarlo ad "Accettata"
|
||||
- [ ] Ristrutturare in `src/` e `tests/` con soluzione unica e `InternalsVisibleTo`
|
||||
- [x] Ristrutturare in `src/` e `tests/` con soluzione unica e `InternalsVisibleTo`
|
||||
- [ ] Predisposizioni di testabilità: `ChatStore` a istanza, `IClaudeRunner`
|
||||
- [ ] `FakeClaude`: eseguibile finto per i test di processo
|
||||
- [x] `FakeClaude`: eseguibile finto per i test di processo (`tests/FakeClaude`, prove FK-01…FK-05)
|
||||
- [ ] Test `Core/` (CL, ST) e `MainViewModel` (VM)
|
||||
- [ ] Test del renderer markdown (MD) e della GUI headless (UI)
|
||||
- [ ] Test end-to-end e smoke di publish per `win-x64` e `linux-x64` (PUB)
|
||||
|
||||
+68
-6
@@ -21,20 +21,35 @@ ADR (se serve) → MVP.md (requisito e criterio) → riga in questa matrice →
|
||||
| **P** — processo | avvio reale di un processo (`Claude`) | `FakeClaude` nel PATH |
|
||||
| **E** — end-to-end | l'eseguibile pubblicato (CLI, 127, exit code) | publish + processo figlio + `FakeClaude` |
|
||||
|
||||
## Struttura prevista
|
||||
## Struttura
|
||||
```
|
||||
agent-wiki-ui.slnx
|
||||
src/agent-wiki-ui/ app (Core/, Gui/, Program.cs)
|
||||
tests/agent-wiki-ui.Tests/ U, R, G, P, E (Trait "Category": Unit | Gui | Process | E2E)
|
||||
tests/FakeClaude/ eseguibile finto: registra argv/cwd/stdin; testo, stderr, exit code e ritardo da variabili d'ambiente
|
||||
tests/agent-wiki-ui.Tests/ Support/ (TestApp, PngFactory), Gui/ (R, G), Process/ (P), Core/ (U), E2E/ (E)
|
||||
tests/FakeClaude/ eseguibile finto `claude` (vedi sotto)
|
||||
```
|
||||
I percorsi `Core/` e `Gui/` citati in questo documento sono relativi a `src/agent-wiki-ui/`.
|
||||
Una cartella per livello, creata quando c'è il primo test; il livello si dichiara anche con `[Trait("Category", "Unit" | "Gui" | "Process" | "E2E")]`.
|
||||
|
||||
### FakeClaude
|
||||
Piccolo eseguibile (`AssemblyName` `claude`, nel PATH dei test) che prende il posto del vero motore. Un test lo configura con variabili d'ambiente del processo che avvia:
|
||||
|
||||
| Variabile | Effetto |
|
||||
|---|---|
|
||||
| `FAKE_CLAUDE_LOG` | file in cui accoda una riga JSON per invocazione: `{"args": [...], "cwd": "...", "stdin": "..."}` |
|
||||
| `FAKE_CLAUDE_OUT` | testo scritto su stdout (anche per `--version`) |
|
||||
| `FAKE_CLAUDE_ERR` | testo scritto su stderr |
|
||||
| `FAKE_CLAUDE_EXIT` | exit code (predefinito 0) |
|
||||
| `FAKE_CLAUDE_DELAY_MS` | attesa prima di rispondere |
|
||||
|
||||
Legge stdin fino alla fine e lo registra: se il chiamante non lo chiude, il processo non termina (così CL-05 è verificabile). Il percorso del file compilato lo fornisce l'helper di test `FakeClaudePath`.
|
||||
|
||||
## Predisposizioni di testabilità (prerequisito dei test)
|
||||
- [ ] `ChatStore` come istanza con cartella iniettata (oggi statico su `LocalApplicationData`)
|
||||
- [ ] `IEngineRunner` dietro `Core/EngineRunner.cs`, iniettato nel `MainViewModel`
|
||||
- [x] `EngineUpdater.CheckAsync` accetta un `HttpClient`: i test usano un gestore finto (rete assente, risposta non valida, versioni di prova)
|
||||
- [ ] `InternalsVisibleTo` verso il progetto di test
|
||||
- [ ] Spostamento in `src/` e `tests/` con soluzione unica
|
||||
- [x] `InternalsVisibleTo` verso il progetto di test
|
||||
- [x] Spostamento in `src/agent-wiki-ui/` e `tests/` con soluzione unica `agent-wiki-ui.slnx`
|
||||
|
||||
## Soglie di copertura (la build fallisce sotto soglia)
|
||||
| Area | Righe | Rami |
|
||||
@@ -83,6 +98,23 @@ Le righe CL valgono per ogni motore; queste sono specifiche di Codex (ADR-0002).
|
||||
| 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` | [ ] |
|
||||
|
||||
### Antigravity — `Core/Engine.cs` (parser e argomenti), `Core/EngineRunner.cs`
|
||||
Le righe CL valgono per ogni motore; queste sono specifiche di Antigravity CLI (`agy`, ADR-0002).
|
||||
| ID | Requisito | Liv. | Test | |
|
||||
|---|---|---|---|---|
|
||||
| AG-01 | Primo messaggio: `--output-format json --print-timeout 120s … -p <prompt>` (senza `--mode`, sola lettura garantita dal default in headless) | U | `Agy_primo_messaggio` | [x] |
|
||||
| AG-02 | Ripresa: `--output-format json --conversation <sessionId> … -p <prompt>` (senza `--mode`) | U | `Agy_ripresa` | [x] |
|
||||
| AG-03 | Tutti i flag precedono rigorosamente `-p <prompt>` | U | `Agy_flag_prima_di_p` | [x] |
|
||||
| AG-04 | Modello ⇒ `--model`; effort ⇒ `--effort`; vuoti ⇒ omessi | U | `Agy_modello_ed_effort` | [x] |
|
||||
| AG-05 | Il parser legge `conversation_id` dall'output JSON (`OwnsSessionId => true`) | U | `Agy_conversation_id` | [x] |
|
||||
| AG-06 | La risposta è estratta dal campo `response` del JSON | U | `Agy_estrazione_risposta` | [x] |
|
||||
| AG-07 | `status` diverso da SUCCESS o campo `error` ⇒ errore; exit ≠ 0 ⇒ Failed(code, stderr) | U | `Agy_errore_status` | [x] |
|
||||
| AG-08 | Avvisi o testo spurio prima/dopo il JSON sono isolati correttamente | U | `Agy_estrazione_json_robusta` | [x] |
|
||||
| AG-09 | Exit 0 con `response` vuoto ⇒ errore "agy non ha restituito nessuna risposta" | U | `Agy_nessuna_risposta` | [x] |
|
||||
| AG-10 | In modalità CLI (`structured = false`) non si usa `--output-format json` | U | `Agy_cli_senza_json` | [x] |
|
||||
| AG-11 | Stdin chiuso subito e `--print-timeout 120s` prevengono il blocco con output rediretto | P | `Agy_timeout_e_stdin` | [x] |
|
||||
| AG-12 | Catalogo modelli con i 6 modelli attesi ed effort fino a max; default `gemini-3.0-flash-low`, `low` | U | `Agy_catalogo_modelli` | [x] |
|
||||
|
||||
### Aggiornamento dei CLI — `Core/EngineUpdater.cs`, `Gui/MainViewModel.cs` (ADR-0004)
|
||||
Si provano con `FakeClaude`/`FakeCodex` che rispondono a `--version` e `update`; nessun test aggiorna un CLI reale.
|
||||
| ID | Requisito | Liv. | Test | |
|
||||
@@ -107,6 +139,7 @@ Si provano con `FakeClaude`/`FakeCodex` che rispondono a `--version` e `update`;
|
||||
| UP-18 | Disponibile ⇒ riga con "Aggiornamento disponibile: X", pulsante evidenziato e puntino sul pulsante Impostazioni; aggiornato ⇒ nessuno dei tre | U/G | `Evidenziazione_aggiornamento` | [ ] |
|
||||
| UP-19 | Il controllo parte all'avvio e all'apertura del riquadro solo se l'ultimo ha più di 5 minuti; dopo un aggiornamento riuscito si ripete; non blocca invio né interfaccia | U | `Quando_si_controlla` | [ ] |
|
||||
| UP-20 | Il controllo non installa nulla e non modifica i CLI | P | `Controllo_in_sola_lettura` | [ ] |
|
||||
| UP-21 | Antigravity: l'ultima versione si legge dal servizio auto-updater ufficiale (stringa "Stable Version: X.Y.Z") | U | `Ultima_versione_agy` | [x] |
|
||||
|
||||
### Cronologia — `Core/ChatStore.cs`, `Core/AppInfo.cs`
|
||||
| ID | Requisito | Liv. | Test | |
|
||||
@@ -163,7 +196,27 @@ Si provano con `FakeClaude`/`FakeCodex` che rispondono a `--version` e `update`;
|
||||
| MD-11 | Link: colore d'accento e sottolineatura; il clic apre solo `http(s)` | R | `Link_http_https` | [ ] |
|
||||
| MD-12 | Link: nessuna apertura se c'è una selezione in corso; cursore a mano sopra il link | R | `Link_con_selezione` | [ ] |
|
||||
| MD-13 | Posizioni dei link corrette con enfasi, codice inline e a capo prima del link | R | `Intervalli_link` | [ ] |
|
||||
| MD-14 | Immagini: si mostra il testo alternativo, non si scarica nulla | R | `Immagini_come_testo` | [ ] |
|
||||
| MD-14 | Immagini con URL `http(s)` o altro schema: si mostra il testo alternativo, nessuna richiesta di rete | R | `Immagini_remote_come_testo` | [x] |
|
||||
| MD-19 | Immagine con percorso assoluto di un file png valido ⇒ controllo immagine con le dimensioni attese (ADR-0006) | R | `Immagine_percorso_assoluto` | [x] |
|
||||
| MD-20 | Percorso relativo risolto rispetto a `AppContext.BaseDirectory`; anche URI `file://` | R | `Immagine_percorso_relativo_e_file_uri` | [x] |
|
||||
| MD-21 | File mancante, illeggibile, corrotto o con estensione non ammessa ⇒ testo alternativo, nessuna eccezione | R | `Immagine_non_valida_ripiego` | [x] |
|
||||
| MD-22 | File oltre il limite di 20 MB ⇒ testo alternativo, file non decodificato | R | `Immagine_oltre_limite` | [x] |
|
||||
| MD-23 | Immagine più larga della colonna ⇒ ridotta in larghezza mantenendo le proporzioni; mai ingrandita oltre l'originale | R | `Immagine_larghezza_massima` | [x] |
|
||||
| MD-24 | Il testo alternativo è nome accessibile e suggerimento dell'immagine | R | `Immagine_testo_alternativo_accessibile` | [x] |
|
||||
| MD-25 | Immagine dentro un link, in una lista o in una tabella ⇒ resa senza rompere intervalli dei link (MD-13) né il layout | R | `Immagine_in_contesti_annidati` | [x] |
|
||||
| MD-26 | Cambio di tema con immagini presenti ⇒ vista ricostruita, immagini ancora mostrate | R | `Immagine_cambio_tema` | [x] |
|
||||
| MD-27 | Il caricamento è in background: subito dopo il rendering c'è il segnaposto `[alt]` e un'immagine in attesa; a fine decodifica compare l'immagine; un file corrotto lascia `[alt]` | R | `Immagine_caricamento_in_background` | [x] |
|
||||
| MD-28 | Percorso relativo non presente nella cartella dell'exe ma in una cartella superiore (radice del progetto, es. `<cartella>/<file>` con l'exe in una sottocartella) ⇒ immagine mostrata | R | `Immagine_percorso_relativo_alla_radice` | [x] |
|
||||
| MD-29 | Se il file esiste in più cartelle, vince la più vicina all'exe; se non esiste in nessuna ⇒ testo alternativo | R | `Immagine_cartella_piu_vicina` | [x] |
|
||||
| MD-30 | Risposta con immagine e, sotto, didascalia e link in markdown: l'immagine compare e la didascalia resta testo; l'app non legge nessun catalogo | R | `Immagine_risposta_con_didascalia` | [x] |
|
||||
| MD-31 | Formula inline `$…$` ⇒ controllo formula inserito nel testo; il testo prima e dopo resta intatto e non sparisce più (ADR-0008) | R | `Formula_inline` | [x] |
|
||||
| MD-32 | Formula a blocco `$$…$$` ⇒ controllo formula (non un blocco di codice col sorgente) | R | `Formula_a_blocco` | [x] |
|
||||
| MD-33 | LaTeX non valido (inline e a blocco) ⇒ sorgente come testo, nessuna eccezione | R | `Formula_non_valida_ripiego` | [x] |
|
||||
| MD-34 | Costrutti LaTeX comuni (frazioni, apici e pedici, `\mathbf`, `\overline`, `\times`, lettere greche, `\left…\right`, `\sum`) ⇒ nessun errore | R | `Costrutti_latex_comuni` | [x] |
|
||||
| MD-35 | Cambio di tema con formule presenti ⇒ vista ricostruita, formule ancora mostrate | R | `Formula_cambio_tema` | [x] |
|
||||
| MD-36 | Formula dentro una lista, una tabella o un link ⇒ resa senza rompere il layout né gli intervalli dei link (MD-13) | R | `Formula_in_contesti_annidati` | [x] |
|
||||
| MD-37 | Il nome accessibile della formula è il suo sorgente LaTeX | R | `Formula_nome_accessibile` | [x] |
|
||||
| MD-38 | Simboli `$` isolati (es. prezzi «10 $ e 20 $») restano testo, nessuna formula | R | `Dollari_isolati_restano_testo` | [x] |
|
||||
| MD-15 | HTML grezzo disabilitato: i tag compaiono come testo, non come elementi | R | `Html_disabilitato` | [ ] |
|
||||
| MD-16 | Cambio di tema (chiaro/scuro): i colori si aggiornano ricostruendo la vista | R | `Cambio_tema` | [ ] |
|
||||
| MD-17 | Markdown malformato o blocchi sconosciuti non fanno crashare | R | `Markdown_malformato` | [ ] |
|
||||
@@ -230,6 +283,15 @@ Per **ogni** motore, la stessa batteria di contratto:
|
||||
| EN-17 | CLI `--engine <id> "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 | [ ] |
|
||||
|
||||
### FakeClaude — `tests/FakeClaude`
|
||||
| ID | Requisito | Liv. | Test | |
|
||||
|---|---|---|---|---|
|
||||
| FK-01 | Registra argomenti (anche con virgolette e spazi, uno per elemento), cartella di lavoro e stdin | P | `Registra_argomenti_cwd_e_stdin` | [x] |
|
||||
| FK-02 | Scrive su stdout e stderr il testo configurato | P | `Output_e_stderr_configurati` | [x] |
|
||||
| FK-03 | L'exit code configurato è quello del processo | P | `ExitCode_configurato` | [x] |
|
||||
| FK-04 | Il ritardo configurato è rispettato | P | `Ritardo_configurato` | [x] |
|
||||
| FK-05 | Termina solo dopo la chiusura di stdin | P | `Attende_la_chiusura_di_stdin` | [x] |
|
||||
|
||||
## Come si eseguono (a implementazione avvenuta)
|
||||
Comandi previsti, da confermare con ADR-0003 → "Da verificare":
|
||||
- Tutti i test: `dotnet test`
|
||||
|
||||
@@ -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 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.
|
||||
- Rendering markdown con un **renderer proprio** che trasforma l'albero di Markdig in controlli Avalonia (`src/agent-wiki-ui/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 `<nome-app>/chats`. Il nome viene da un'unica costante (vedi [Nome del progetto](../README.md#nome-del-progetto)).
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# ADR-0002 — Più motori CLI (Claude Code, Codex, Antigravity) su Windows e Linux
|
||||
|
||||
- **Stato**: Accettata per Claude Code e Codex (verificati con prove reali); Antigravity resta da verificare
|
||||
- **Data**: 2026-10-15
|
||||
- **Stato**: Accettata (Claude Code, Codex, Antigravity)
|
||||
- **Data**: 2026-10-15 (aggiornato 2026-10-18 per Antigravity)
|
||||
- **Collegato a**: [ADR-0001](0001-gui-multipiattaforma-avalonia.md) (GUI e piattaforme)
|
||||
|
||||
## Contesto
|
||||
L'app lancia solo `claude -p`. Si vuole usare dalla stessa chat anche **OpenAI Codex CLI** e **Antigravity CLI** (`agy`), così come `claude` già installati da terminale dall'utente, su Windows e Linux. L'app non li installa e non gestisce login: sono programmi esterni, autenticati a parte.
|
||||
L'app lanciava solo `claude -p`. Si vuole usare dalla stessa chat anche **OpenAI Codex CLI** e **Antigravity CLI** (`agy`), così come `claude` già installati da terminale dall'utente, su Windows e Linux. L'app non li installa e non gestisce login: sono programmi esterni, autenticati a parte.
|
||||
|
||||
## Criteri di decisione
|
||||
1. Aggiungere un motore costa una definizione dichiarativa, non nuova logica sparsa.
|
||||
@@ -17,22 +17,22 @@ 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` è diventato `EngineRunner` con il motore come parametro.
|
||||
- **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: interruttore **CLI** (motore attivo), modello, effort. Cambiare motore apre una nuova chat; modello ed effort tornano ai valori iniziali del motore (il modello più piccolo ed effort `low`; "Predefinito" resta selezionabile) e le liste sono quelle del motore. La persistenza su disco delle impostazioni è rinviata. La CLI ottiene `--engine <id>` (`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.
|
||||
- **Impostazioni** nella GUI: interruttore **CLI** (motore attivo), modello, effort. Cambiare motore apre una nuova chat; modello ed effort tornano ai valori iniziali del motore (il modello più piccolo ed effort `low`; "Predefinito" resta selezionabile) e le liste sono quelle del motore. La persistenza su disco delle impostazioni è rinviata. La CLI ottiene `--engine <id>` (`claude`, `codex`, `agy`).
|
||||
- **Sessione**: per Claude Code l'id lo sceglie l'app; per Codex e Antigravity lo genera il motore e l'app lo legge dall'output (`thread_id` per Codex, `conversation_id` per Antigravity). 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
|
||||
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 e Antigravity sono verificati con prove reali (Linux/WSL, ottobre 2026).
|
||||
|
||||
| | Claude Code | Codex CLI | Antigravity CLI |
|
||||
|---|---|---|---|
|
||||
| Comando (GUI) | `claude -p <prompt>` | `codex exec --json --skip-git-repo-check -s read-only [opzioni] -- <prompt>` | `agy -p <prompt>` (da verificare) |
|
||||
| Continuare | `--session-id <guid>`, poi `--resume <guid>` | `codex exec resume --json --skip-git-repo-check -c 'sandbox_mode="read-only"' [opzioni] <id> -- <prompt>` | da verificare |
|
||||
| Modello | `--model` | `-m` / `--model` | da verificare |
|
||||
| Effort | `--effort` | `-c model_reasoning_effort=<livello>` | non noto |
|
||||
| Output usato | testo su stdout | JSONL su stdout (vedi sotto) | da verificare |
|
||||
| Comando (GUI) | `claude -p <prompt>` | `codex exec --json --skip-git-repo-check -s read-only [opzioni] -- <prompt>` | `agy --output-format json --print-timeout 120s [opzioni] -p <prompt>` |
|
||||
| Continuare | `--session-id <guid>`, poi `--resume <guid>` | `codex exec resume --json --skip-git-repo-check -c 'sandbox_mode="read-only"' [opzioni] <id> -- <prompt>` | `agy --output-format json --print-timeout 120s --conversation <id> [opzioni] -p <prompt>` |
|
||||
| Modello | `--model` | `-m` / `--model` | `--model` |
|
||||
| Effort | `--effort` | `-c model_reasoning_effort=<livello>` | `--effort` |
|
||||
| Output usato | testo su stdout | JSONL su stdout (vedi sotto) | JSON su stdout (`conversation_id`, `status`, `response`) |
|
||||
|
||||
**Codex: esito delle prove**
|
||||
- L'id di sessione non si sceglie: compare nel primo evento JSONL `{"type":"thread.started","thread_id":"<uuid>"}`. Nel testo normale compare solo nell'intestazione su stderr.
|
||||
@@ -46,17 +46,26 @@ Claude Code e Codex sono verificati con prove reali (Codex 0.144.3, 2026-10-15,
|
||||
- Il modello più piccolo di Codex è assunto essere GPT-5.6 Luna (nome e supporto agli effort inferiori: non dichiarato da OpenAI nelle prove); da rivedere quando cambia il catalogo.
|
||||
- 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.
|
||||
|
||||
**Antigravity (`agy`): esito delle prove**
|
||||
- Eseguibile nativo: ELF su Linux (`~/.gemini/bin/agy`), `.exe` nativo su Windows.
|
||||
- Permessi e sicurezza: per usare `agy` in modalità non interattiva (`-p`) in sola lettura, nel file `~/.gemini/antigravity-cli/settings.json` deve essere presente la regola `'read_file(*)'` sotto `'permissions.allow'` (altrimenti headless mode nega `read_file` con errore jetski: `a tool required the read_file permission that headless mode cannot prompt for`), mentre scritture e comandi restano negati di default. Non si passa `--mode plan` (che fallisce anch'esso con errore jetski) né `--mode auto` (non disponibile per l'account) né alcun flag `--mode`. Mai usare `--dangerously-skip-permissions`.
|
||||
- Posizione di `-p`: tutti i flag di configurazione vanno passati rigorosamente prima di `-p <prompt>`.
|
||||
- Formato output: per la GUI (`structured = true`) si passa `--output-format json`; per la CLI si omette per ereditare testo normale. L'output JSON contiene `conversation_id`, `status` ("SUCCESS") e `response` con il testo generato.
|
||||
- Sessione: l'id di sessione è un UUID generato da `agy` (`OwnsSessionId => true`), letto da `conversation_id`. La ripresa avviene passando `--conversation <sessionId>`.
|
||||
- Blocco sottoprocesso su output rediretto (issue #318): mitigato passando `--print-timeout 120s` (formato durata Go) e assicurando la chiusura immediata dello stdin.
|
||||
- Modelli ed effort: modelli fissi da catalogo `agy models`, modello più piccolo predefinito `gemini-3.0-flash-low`, effort supportati `low`, `medium`, `high`, `xhigh`, `max` (default `low`).
|
||||
|
||||
## 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
|
||||
- [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.
|
||||
- [x] **Antigravity**: verificato il comando non interattivo `agy [flag] -p <prompt>`, output JSON `--output-format json`, ripresa con `--conversation`, timeout con `--print-timeout 120s` e chiusura stdin.
|
||||
- [ ] **Forma dell'eseguibile**: `claude` e `agy` sono binari nativi avviabili direttamente; per installazioni tramite gestori di pacchetti verificare eventuali script wrapper su Windows.
|
||||
- [ ] Autenticazione: ogni CLI deve essere già autenticata; l'app mostra l'errore del motore senza gestire login.
|
||||
- [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.
|
||||
- [x] Formato dell'output di Codex e Antigravity: Codex JSONL, Antigravity JSON su stdout, normalizzati in testo markdown.
|
||||
- [x] Mappatura effort e modelli per Codex e Antigravity (liste fisse, da aggiornare).
|
||||
|
||||
## Riferimenti
|
||||
- Passi: [ROADMAP.md](../ROADMAP.md), Fase 4.
|
||||
|
||||
@@ -24,9 +24,10 @@ Entrambi i CLI hanno un comando di aggiornamento che controlla e, se c'è una ve
|
||||
- **Controllo prima dell'aggiornamento** (sola lettura, nessuna installazione): l'app confronta la versione installata (`<cli> --version`) con l'ultima pubblicata.
|
||||
- Claude Code: `GET https://downloads.claude.ai/claude-code-releases/latest`, testo con la versione (la stessa sorgente dell'installer nativo).
|
||||
- Codex: `GET https://api.github.com/repos/openai/codex/releases/latest`, campo `tag_name` (es. `rust-v0.162.1`, si estrae `0.162.1`).
|
||||
- Le versioni si ricavano con il primo numero `N.N.N` del testo e si confrontano come versioni. Verificate il 2026-10-15: restituivano 2.1.300 e 0.162.1.
|
||||
- Antigravity: `GET https://antigravity-cli-auto-updater-974169037036.us-central1.run.app`, testo con `Stable Version: X.Y.Z` (la sorgente dell'auto-updater ufficiale presente nel binario `agy`).
|
||||
- Le versioni si ricavano con il primo numero `N.N.N` del testo e si confrontano come versioni. Verificate il 2026-10-15 e 2026-10-18.
|
||||
- **Disponibile** (ultima > installata): la riga mostra "Aggiornamento disponibile: X", il pulsante Aggiorna è **evidenziato** (colore d'accento) e un puntino d'accento compare sul pulsante Impostazioni. **Aggiornato** (uguale o più nuova): riga neutra con "Aggiornato", pulsante normale, nessun puntino; resta cliccabile per forzare `update`. **Sconosciuto** (rete assente, risposta non valida, timeout di 10 s): "Controllo non riuscito", pulsante normale, nessun errore invasivo.
|
||||
- Quando si controlla: all'avvio, in background, e ogni volta che si apre il riquadro Impostazioni se l'ultimo controllo ha più di 5 minuti; subito dopo un aggiornamento. Mai in modo da rallentare la chat. Le richieste non inviano identificativi; l'app contatta solo i due indirizzi sopra.
|
||||
- Quando si controlla: all'avvio, in background, e ogni volta che si apre il riquadro Impostazioni se l'ultimo controllo ha più di 5 minuti; subito dopo un aggiornamento. Mai in modo da rallentare la chat. Le richieste non inviano identificativi; l'app contatta solo gli indirizzi ufficiali sopra.
|
||||
- **Flusso** di un clic: legge la versione (`<cli> --version`), lancia `<cli> update` (stdin chiuso, output catturato, tempo massimo 10 minuti), rilegge la versione e mostra l'esito sulla riga:
|
||||
- versione cambiata ⇒ "Aggiornato da X a Y";
|
||||
- exit 0 e versione invariata ⇒ "Già aggiornato (X)";
|
||||
@@ -34,12 +35,12 @@ Entrambi i CLI hanno un comando di aggiornamento che controlla e, se c'è una ve
|
||||
- Pulsanti disabilitati mentre una risposta è in corso o un altro aggiornamento è attivo; la chat non parte durante l'aggiornamento.
|
||||
- **Comando manuale** scelto dal metodo di installazione, dedotto dal percorso reale dell'eseguibile (collegamenti simbolici risolti):
|
||||
|
||||
| Percorso contiene | Metodo | Claude Code | Codex |
|
||||
|---|---|---|---|
|
||||
| `node_modules`, `.npm` | npm | `npm install -g @anthropic-ai/claude-code@latest` | `npm install -g @openai/codex@latest` |
|
||||
| `Cellar`, `homebrew`, `linuxbrew` | Homebrew | `brew upgrade claude-code` (o `claude-code@latest`) | `brew upgrade --cask codex` |
|
||||
| `WinGet`, `WindowsApps` | WinGet | `winget upgrade Anthropic.ClaudeCode` | script PowerShell di [INSTALL.md](../INSTALL.md) |
|
||||
| altro | nativo/script | `claude update` da terminale | script di installazione di [INSTALL.md](../INSTALL.md) |
|
||||
| Percorso contiene | Metodo | Claude Code | Codex | Antigravity |
|
||||
|---|---|---|---|---|
|
||||
| `node_modules`, `.npm` | npm | `npm install -g @anthropic-ai/claude-code@latest` | `npm install -g @openai/codex@latest` | `agy update` |
|
||||
| `Cellar`, `homebrew`, `linuxbrew` | Homebrew | `brew upgrade claude-code` (o `claude-code@latest`) | `brew upgrade --cask codex` | `brew upgrade agy` |
|
||||
| `WinGet`, `WindowsApps` | WinGet | `winget upgrade Anthropic.ClaudeCode` | script PowerShell di [INSTALL.md](../INSTALL.md) | `agy update` |
|
||||
| altro | nativo/script | `claude update` da terminale | script di installazione di [INSTALL.md](../INSTALL.md) | `agy update` |
|
||||
|
||||
L'app **mostra** il comando, non lo esegue: un'installazione globale con npm o un gestore di pacchetti può richiedere permessi o scelte che spettano all'utente.
|
||||
- Dopo un aggiornamento riuscito le versioni mostrate si rileggono; la chat aperta non cambia.
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
# ADR-0006 — Immagini nel markdown: solo file locali
|
||||
|
||||
- **Stato**: Proposta
|
||||
- **Data**: 2026-10-15
|
||||
- **Collegato a**: [ADR-0001](0001-gui-multipiattaforma-avalonia.md) (GUI), [ADR-0003](0003-strategia-di-test.md) (test)
|
||||
|
||||
## Contesto
|
||||
Il renderer markdown ([Gui/MarkdownView.cs](../../src/agent-wiki-ui/Gui/MarkdownView.cs)) trasforma `` nel solo testo alternativo `[alt]`: non carica nulla. Un motore (Claude Code, Codex) può però produrre o citare un'immagine su disco (un grafico, uno screenshot) e rispondere con ``. Se il percorso è noto e il file esiste, l'utente si aspetta di vederla nella chat. Oggi l'app contatta la rete solo per il controllo delle versioni ([AGENT.md](../../AGENT.md), "Vincoli non ovvi"); aprire le immagini remote cambierebbe questo vincolo.
|
||||
|
||||
## Criteri di decisione
|
||||
1. Mostrare l'immagine quando il percorso è locale e il file è leggibile.
|
||||
2. Nessun accesso alla rete oltre a quello già previsto.
|
||||
3. Nessun crash e nessun blocco della chat per file mancanti, enormi o corrotti: ripiego sul testo alternativo di oggi.
|
||||
4. Coerenza con il resto del renderer: colori dal tema, testo selezionabile, accessibilità.
|
||||
|
||||
## Opzioni considerate
|
||||
| Opzione | Esito | Motivo |
|
||||
|---|---|---|
|
||||
| Lasciare il testo `[alt]` | Scartata | Il percorso noto resta inutilizzato. |
|
||||
| **Solo percorsi locali** | **Scelta** | Soddisfa il caso d'uso, nessuna rete, sola lettura. |
|
||||
| Locali + URL `http(s)` | Rimandata | Una risposta del modello farebbe partire richieste di rete verso indirizzi scelti dal modello (tracciamento, indirizzo IP dell'utente esposto). Si può riprendere con un nuovo ADR e un consenso esplicito. |
|
||||
|
||||
## Decisione
|
||||
- `` mostra l'immagine se la destinazione è un **percorso locale**: assoluto, relativo oppure un URI `file://`.
|
||||
- **Percorsi relativi: radice del progetto.** Il motore gira nella cartella dell'exe, ma le istruzioni che segue (il `CLAUDE.md` di una wiki) stanno di solito in una cartella superiore e scrivono i percorsi relativi a *quella* radice (esempio: una wiki con l'exe in `<wiki>/query/`, accanto a un `CLAUDE.md` solo-query, e istruzioni del tipo «``, percorso relativo alla radice del repo»). Un percorso relativo si cerca quindi nella cartella dell'exe e poi in ciascuna cartella superiore, fino alla radice del filesystem; vince la prima in cui il file esiste (la più vicina all'exe).
|
||||
- **Nessuno standard imposto (prototipo).** L'app non conosce né una cartella delle immagini né un formato di catalogo: la cartella delle immagini e il catalogo sono la convenzione di ciascuna wiki, non un requisito. Cartella e catalogo possono cambiare; la wiki lo dice al motore nelle sue istruzioni, e l'app mostra qualunque file locale raggiungibile con il percorso scritto nella risposta.
|
||||
- **Cataloghi e metadati restano del motore.** Struttura tipo di una wiki con figure: un catalogo JSON con i metadati delle figure (file, didascalia stampata e generata, posizione, avvertenze) e una cartella delle immagini. Chi legge il catalogo e sceglie le figure è il motore, guidato dalle istruzioni della wiki; l'app non legge né interpreta il catalogo: mostra le immagini che la risposta cita in markdown, e la didascalia, la posizione e le avvertenze che il motore scrive sotto l'immagine sono testo markdown normale.
|
||||
- Formati: png, jpg/jpeg, gif (primo fotogramma), webp, bmp. Altre estensioni ⇒ ripiego.
|
||||
- Limiti: file fino a **20 MB**; se l'immagine è più larga di **1600 px** si decodifica a 1600 px di larghezza, per non occupare memoria inutile. In visualizzazione l'immagine non supera la larghezza della colonna dei messaggi (proporzioni mantenute) e non è mai ingrandita oltre l'originale.
|
||||
- Un paragrafo fatto solo di immagini le mostra come blocchi; un'immagine dentro una riga di testo (o dentro un link) è inline, con larghezza massima di 480 px.
|
||||
- **Ripiego** sul testo `[alt]` (come oggi) quando: la destinazione è `http(s)` o altro schema, il file manca o non è leggibile, il formato non è ammesso, il file supera il limite, la decodifica fallisce. Nessun errore a schermo, nessuna eccezione.
|
||||
- **Caricamento in background**: al rendering un controllo di controllo rapido e sincrono (percorso, estensione, esistenza, dimensione) decide se l'immagine è plausibile; se no, compare subito `[alt]`. Se sì, compare `[alt]` come segnaposto e la decodifica avviene su un altro thread; a fine decodifica il segnaposto diventa l'immagine, se la decodifica fallisce resta `[alt]`. Così la GUI non si blocca con file grandi o dischi lenti.
|
||||
- Accessibilità: l'immagine espone `alt` come nome per le tecnologie assistive e come suggerimento al passaggio del mouse.
|
||||
- Nessuna azione al clic (non si apre il file). Il pulsante **Copia** della risposta continua a copiare il markdown originale.
|
||||
|
||||
## Conseguenze
|
||||
**Positive**: le immagini prodotte dai motori si vedono nella chat; nessuna richiesta di rete in più; ripiego sicuro.
|
||||
**Negative**: una risposta può far leggere e mostrare un file immagine qualsiasi dell'utente (sola lettura, solo a schermo, nessun invio altrove); le immagini remote restano non supportate e si vedono come testo.
|
||||
|
||||
## Da verificare
|
||||
- Quali formati decodifica davvero Avalonia 12 con Skia su Windows e Linux (in particolare webp e gif) e se il limite di 20 MB è adeguato.
|
||||
- Il comportamento dei percorsi Windows (`C:\…`, UNC) e di quelli WSL (`/mnt/c/…`).
|
||||
- Se la ricerca nelle cartelle superiori debba fermarsi alla prima cartella con un marcatore di progetto (`CLAUDE.md`, `.git`) invece di salire fino alla radice del filesystem.
|
||||
- Se e quando emergerà uno standard per cataloghi e cartelle delle immagini (nomi, campi minimi come file, didascalia, posizione), valutare un nuovo ADR; per ora è fuori dall'app.
|
||||
|
||||
## Riferimenti
|
||||
- [MVP.md](../MVP.md) → "GUI (chat)"; matrice MD-14, MD-19…MD-30 in [TESTING.md](../TESTING.md).
|
||||
@@ -0,0 +1,62 @@
|
||||
# ADR-0007 — Schema di una wiki interrogabile, con immagini (prototipo)
|
||||
|
||||
- **Stato**: Proposta
|
||||
- **Data**: 2026-10-15
|
||||
- **Collegato a**: [ADR-0006](0006-immagini-nel-markdown.md) (l'app mostra le immagini citate nella risposta)
|
||||
|
||||
## Contesto
|
||||
agent-wiki-ui è il terminale di chat per interrogare una wiki mantenuta da un motore (Claude Code, Codex), per esempio una wiki di studio o di documentazione tecnica: pagine markdown, un indice, un catalogo JSON delle figure con i metadati e una cartella di immagini. Per rispondere in modo strutturato — meglio di un RAG, che recupera frammenti di testo e perde le relazioni — il motore deve sapere **dove cercare** (indice → pagina → tabella o figura) e **come citare** un'immagine perché la chat la stampi. Oggi questo sapere sta in un file scritto per una sola wiki. Non esiste uno standard: la cartella delle immagini e il formato del catalogo cambiano da wiki a wiki, ed è un prototipo.
|
||||
|
||||
## Criteri di decisione
|
||||
1. Un solo posto per i percorsi delle immagini, senza duplicarli a mano.
|
||||
2. Le mappe sono derivate dal catalogo (si rigenerano), non scritte a mano.
|
||||
3. Nessuna cartella o nome di file imposti: la wiki dichiara la sua struttura.
|
||||
4. Degrado sicuro: un percorso sbagliato dà `[alt]` nella chat (ADR-0006), mai un errore.
|
||||
5. Nessun codice nell'app: la logica sta nelle istruzioni al motore e negli script della wiki.
|
||||
6. Pochi campi minimi, perché lo standard possa nascere dall'uso.
|
||||
|
||||
## Opzioni considerate
|
||||
| Opzione | Esito | Motivo |
|
||||
|---|---|---|
|
||||
| Solo RAG sul testo (embedding di `search_text`) | Scartata come unica via | Perde la struttura che rende affidabile la risposta: dato → pagina unica, avvertenze, difetti noti, figura → file. Resta come ripiego (ricerca testuale). |
|
||||
| L'app legge il catalogo e inserisce le immagini nelle risposte | Scartata | Lega l'app a un formato non standard e duplica la scelta delle figure, che il motore fa già leggendo la domanda. |
|
||||
| **Schema in tre parti, letto dal motore; l'app stampa** | **Scelta** | Conserva la struttura, non tocca l'app, si adatta a wiki diverse. |
|
||||
|
||||
## Decisione
|
||||
Una wiki interrogabile ha tre parti. Il formato è una convenzione consigliata, non un requisito dell'app.
|
||||
|
||||
1. **Indice** (`index.md`): catalogo delle pagine, una riga per pagina, `- [[pagina]] — sintesi`. Una pagina con figure aggiunge in coda gli id del catalogo: `· figure: fig-026, fig-027`. L'indice non elenca le figure una per una e non contiene percorsi di file.
|
||||
2. **Catalogo delle figure** (JSON, nome e posizione dichiarati nello schema della wiki). Campi minimi per figura:
|
||||
- `id` stabile, `label` (ad es. «Figura 26»);
|
||||
- `caption_printed` (testo della fonte, alla lettera) e, a parte, ogni testo generato dall'immagine, contrassegnato come non verificato;
|
||||
- `location` (sezione e riga nella fonte);
|
||||
- `panels[]` con `file` e `path` (relativo alla cartella del catalogo) e una descrizione;
|
||||
- `quality`: `flags` e `review_notes`.
|
||||
Indici facoltativi per sezione, composto, tipo e concetto. L'ordine di preferenza per trovare una figura è: indice dell'indice → pagina → `figures` nel frontmatter → indici del catalogo → ricerca testuale.
|
||||
3. **Istruzioni per le query**: un file `CLAUDE.md` (per Codex `AGENTS.md`) in una cartella dedicata sotto la radice della wiki, da cui si avvia l'app. Si aggiunge a quello della radice e prevale su di esso. Impone la **sola lettura**, instrada le domande per tipo verso l'indice giusto e fissa le regole per le immagini.
|
||||
|
||||
**Regole per le immagini nella risposta:**
|
||||
- Il percorso si ricava da catalogo e `path`, si **verifica che esista** e si scrive **relativo alla radice** della wiki, senza `..` (l'app lo cerca nella sua cartella e poi nelle superiori, ADR-0006).
|
||||
- Subito sotto: didascalia stampata alla lettera, posizione nella fonte, link alla nota della figura; una riga di avvertenza se `flags` o `review_notes`.
|
||||
- Ciò che deriva dall'immagine (descrizioni, valori letti da un grafico) si dichiara non verificato. Massimo 3-4 immagini per risposta.
|
||||
|
||||
**Percorsi derivati.** Se il catalogo ha percorsi relativi alla propria cartella, la wiki genera con uno script una mappa figura → percorso dalla radice → pagine che la citano (per esempio colonne in più di `figure-indice.md`). Lo script, e un controllo in fase di lint che ogni percorso della mappa esista, sono della wiki, non di questo repo.
|
||||
|
||||
**Chi fa cosa**
|
||||
| Parte | Compito |
|
||||
|---|---|
|
||||
| Wiki | Mantiene indice, catalogo e immagini; rigenera la mappa; controlla che i percorsi esistano |
|
||||
| Motore | Instrada la domanda, legge indici e pagine, sceglie le figure, scrive il markdown con l'immagine e la didascalia |
|
||||
| App | Stampa il markdown; mostra l'immagine se il file esiste, altrimenti `[alt]` |
|
||||
|
||||
## Conseguenze
|
||||
**Positive**: una risposta ripercorre indice → pagina → figura invece di cercare frammenti; i percorsi stanno in un solo posto e si rigenerano; l'app non cambia; wiki diverse adottano lo stesso modello cambiando pochi campi.
|
||||
**Negative**: il rispetto dello schema dipende dal motore che segue le istruzioni (può sbagliare percorso: l'app lo mostra come `[alt]`); la wiki deve mantenere lo script e il controllo dei percorsi; lo standard è provvisorio.
|
||||
|
||||
## Da verificare
|
||||
- Che Claude Code, avviato dalla cartella dedicata, legga e rispetti sia il file di quella cartella sia quello della radice (prova con la chat reale su una wiki di esempio).
|
||||
- L'equivalente per Codex (`AGENTS.md`) e come si comporta con le immagini nei suoi comandi.
|
||||
- Se il campo minimo `path` debba diventare un percorso dalla radice nel catalogo stesso, eliminando la mappa derivata.
|
||||
|
||||
## Riferimenti
|
||||
- Uso con le immagini: [README.md](../../README.md), sezione «Immagini nelle risposte».
|
||||
@@ -0,0 +1,45 @@
|
||||
# ADR-0008 — Formule LaTeX nel markdown
|
||||
|
||||
- **Stato**: Proposta
|
||||
- **Data**: 2026-10-20
|
||||
- **Collegato a**: [ADR-0006](0006-immagini-nel-markdown.md) (stesso renderer, stessi vincoli di sicurezza), [ADR-0007](0007-schema-wiki-per-query-con-immagini.md) (stesso tipo di risposte)
|
||||
|
||||
## Contesto
|
||||
Le risposte dei motori possono contenere formule in LaTeX: `$ ... $` nel testo e `$$ ... $$` a blocco. Il renderer markdown ([Gui/MarkdownView.cs](../../src/agent-wiki-ui/Gui/MarkdownView.cs)) usa Markdig con `UseAdvancedExtensions()`, che comprende l'estensione matematica: il parser riconosce le formule ma il renderer non le gestisce. Quelle a blocco finiscono in un blocco di codice con il sorgente LaTeX; quelle nel testo non vengono disegnate (nodo ignorato). L'utente vede sorgente illeggibile o formule mancanti.
|
||||
|
||||
## Criteri di decisione
|
||||
1. Mostrare le formule composte (frazioni, apici e pedici, lettere greche, sommatorie, sopralineature) come in un testo tipografico.
|
||||
2. Nessuna rete e nessuna esecuzione di codice: la formula è solo dato da disegnare (come per le immagini, ADR-0006).
|
||||
3. Degrado sicuro: un LaTeX non valido mostra il sorgente come testo, mai un errore né un blocco della chat.
|
||||
4. Coerenza con il resto del renderer: colori dal tema, ricostruzione al cambio di tema, testo circostante selezionabile, accessibilità.
|
||||
5. Funziona su Windows e Linux nel publish single-file, senza runtime aggiuntivi.
|
||||
6. Dipendenza manutenuta e compatibile con la versione di Avalonia in uso.
|
||||
|
||||
## Opzioni considerate
|
||||
| Opzione | Esito | Motivo |
|
||||
|---|---|---|
|
||||
| Lasciare com'è | Scartata | Il sorgente LaTeX o l'assenza delle formule rendono illeggibili le risposte con formule. |
|
||||
| Convertire il LaTeX in testo Unicode (`x_1` → `x`+pedice) con un mini-interprete proprio | Scartata | Copre solo i casi semplici; frazioni, `\overline`, `\left…\right` e matrici richiederebbero di riscrivere un motore di composizione. |
|
||||
| Rendere con un motore web (KaTeX/MathJax in una WebView) | Scartata | Aggiunge un browser incorporato e codice JavaScript, contro il criterio 2 e il peso dell'eseguibile. |
|
||||
| **Libreria di composizione TeX per Avalonia (`Sylinko.CSharpMath.Avalonia`, licenza MIT, basata su CSharpMath)** | **Scelta** | Disegna LaTeX come controllo Avalonia, senza rete né script; l'ultima versione (12.0.0) è per Avalonia 12, come l'app. |
|
||||
|
||||
## Decisione
|
||||
- La formula tra `$ … $` è un'**immagine inline** composta dal controllo della libreria, inserita nel testo (`InlineUIContainer`) con allineamento al testo; `$$ … $$` è un **blocco** a sé, allineato a sinistra, con scorrimento orizzontale se più larga della colonna.
|
||||
- Il colore è quello del testo del tema (`Fg`, o il colore ereditato se il token manca); al cambio di tema la vista si ricostruisce (già così per il resto).
|
||||
- **Ripiego**: se il LaTeX non è valido la formula compare come testo sorgente (`$…$` con carattere monospazio), senza eccezioni.
|
||||
- Il nome accessibile del controllo è il sorgente LaTeX; la formula non è selezionabile come testo (è un disegno): il pulsante **Copia** della risposta continua a copiare il markdown originale, quindi anche il sorgente delle formule.
|
||||
- I simboli `$` isolati, ad esempio un prezzo («10 $ e 20 $»), restano testo. Markdig da solo li prenderebbe per una formula (e toglie gli spazi dal contenuto): il renderer applica la regola di Pandoc sul testo originale, cioè niente spazio subito dopo il `$` di apertura né subito prima di quello di chiusura. Per questo gli intervalli di Markdig si leggono su un testo con fine riga uniformi (`\n`).
|
||||
- Nessuna rete e nessuna lettura di file: la formula è solo dato per il disegno.
|
||||
|
||||
## Conseguenze
|
||||
**Positive**: le risposte con formule mostrano le formule composte; nessun cambiamento ai motori né ai prompt.
|
||||
**Negative**: una dipendenza in più (e le sue librerie di disegno); il pacchetto è un fork per Avalonia 12 e va tenuto d'occhio quando Avalonia aggiorna; le formule non sono selezionabili come testo; il supporto non copre tutto LaTeX (le macro non supportate cadono nel ripiego).
|
||||
|
||||
## Da verificare
|
||||
- Dimensione dell'eseguibile pubblicato (single-file) dopo l'aggiunta della libreria e dei suoi font.
|
||||
- Allineamento verticale delle formule inline rispetto al testo con font diversi, su Windows e Linux.
|
||||
- Quali comandi LaTeX comuni (matrici, ambienti `align`, estensioni a pacchetto) sono supportati.
|
||||
- Il comportamento con formule molto lunghe in una riga di testo (a capo).
|
||||
|
||||
## Riferimenti
|
||||
- [MVP.md](../MVP.md) → "GUI (chat)"; matrice MD-31…MD-38 in [TESTING.md](../TESTING.md).
|
||||
+1
-1
@@ -8,7 +8,7 @@ Set-Location (Split-Path -Parent $PSScriptRoot)
|
||||
|
||||
foreach ($rid in $Rids) {
|
||||
Write-Host "== $rid"
|
||||
dotnet publish agent-wiki-ui.csproj -c Release -r $rid --self-contained -p:PublishSingleFile=true -o "dist/$rid" --nologo -v quiet
|
||||
dotnet publish src/agent-wiki-ui/agent-wiki-ui.csproj -c Release -r $rid --self-contained -p:PublishSingleFile=true -o "dist/$rid" --nologo -v quiet
|
||||
if ($LASTEXITCODE -ne 0) { throw "publish fallito per $rid" }
|
||||
Get-ChildItem "dist/$rid" -Filter *.pdb -Recurse | Remove-Item # simboli delle librerie native: inutili a chi usa l'app
|
||||
}
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ rids=("$@")
|
||||
|
||||
for rid in "${rids[@]}"; do
|
||||
echo "== $rid"
|
||||
dotnet publish agent-wiki-ui.csproj -c Release -r "$rid" --self-contained -p:PublishSingleFile=true -o "dist/$rid" --nologo -v quiet
|
||||
dotnet publish src/agent-wiki-ui/agent-wiki-ui.csproj -c Release -r "$rid" --self-contained -p:PublishSingleFile=true -o "dist/$rid" --nologo -v quiet
|
||||
find "dist/$rid" -name '*.pdb' -delete # simboli delle librerie native: inutili a chi usa l'app
|
||||
done
|
||||
|
||||
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -1,4 +1,5 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Net.Http.Headers;
|
||||
|
||||
namespace AgentWikiUi.Core;
|
||||
@@ -159,11 +160,89 @@ sealed class CodexEngine : Engine
|
||||
}
|
||||
}
|
||||
|
||||
sealed class AgyEngine : Engine
|
||||
{
|
||||
public override string Id => "agy";
|
||||
public override string Name => "Antigravity";
|
||||
public override string Author => "Antigravity";
|
||||
public override string Executable => "agy";
|
||||
public override bool OwnsSessionId => true;
|
||||
|
||||
public override ModelOption[] Models { get; } =
|
||||
[
|
||||
new("Gemini 3.0 Flash (Low)", "gemini-3.0-flash-low"),
|
||||
new("Gemini 3.0 Flash (Medium)", "gemini-3.0-flash-medium"),
|
||||
new("Gemini 3.0 Flash (High)", "gemini-3.0-flash-high"),
|
||||
new("Claude Sonnet 4.6 (Thinking)", "claude-sonnet-4-6"),
|
||||
new("Claude Opus 4.6 (Thinking)", "claude-opus-4-6-thinking"),
|
||||
new("GPT-OSS 120B (Medium)", "gpt-oss-120b-medium"),
|
||||
];
|
||||
|
||||
public override string[] Efforts { get; } = ["low", "medium", "high", "xhigh", "max"];
|
||||
public override string DefaultModelId => "gemini-3.0-flash-low";
|
||||
|
||||
public override List<string> BuildArgs(string prompt, string? sessionId, bool resume, string model, string effort, bool structured)
|
||||
{
|
||||
var args = new List<string>();
|
||||
if (structured) args.AddRange(["--output-format", "json"]);
|
||||
if (resume && sessionId is not null) args.AddRange(["--conversation", sessionId]);
|
||||
if (model != "") args.AddRange(["--model", model]);
|
||||
if (effort != "") args.AddRange(["--effort", effort]);
|
||||
args.AddRange(["--print-timeout", "120s"]);
|
||||
args.AddRange(["-p", prompt]); // tutti i flag vanno passati PRIMA di -p
|
||||
return args;
|
||||
}
|
||||
|
||||
public override EngineReply Parse(int code, string stdout, string stderr)
|
||||
{
|
||||
string? conversationId = null;
|
||||
string? response = null;
|
||||
string? error = null;
|
||||
|
||||
var trimmed = stdout.Trim();
|
||||
if (!string.IsNullOrEmpty(trimmed))
|
||||
{
|
||||
var start = trimmed.IndexOf('{');
|
||||
var end = trimmed.LastIndexOf('}');
|
||||
if (start >= 0 && end > start)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(trimmed.Substring(start, end - start + 1));
|
||||
var root = doc.RootElement;
|
||||
if (root.TryGetProperty("conversation_id", out var convProp))
|
||||
conversationId = convProp.GetString();
|
||||
if (root.TryGetProperty("response", out var respProp))
|
||||
response = respProp.GetString();
|
||||
if (root.TryGetProperty("status", out var statusProp) && statusProp.GetString() != "SUCCESS")
|
||||
error = $"Stato {statusProp.GetString()}";
|
||||
if (root.TryGetProperty("error", out var errProp))
|
||||
error = errProp.GetString() ?? error;
|
||||
}
|
||||
catch (JsonException) { }
|
||||
}
|
||||
}
|
||||
|
||||
if (code != 0) return new("", conversationId, Failed(code, error ?? stderr.Trim()));
|
||||
if (error is not null && string.IsNullOrWhiteSpace(response)) return new("", conversationId, error);
|
||||
if (string.IsNullOrWhiteSpace(response)) return new("", conversationId, error ?? (string.IsNullOrWhiteSpace(stderr) ? "agy non ha restituito nessuna risposta." : stderr.Trim()));
|
||||
return new(response.Trim(), conversationId, null);
|
||||
}
|
||||
|
||||
public override async Task<string?> FetchLatestVersionAsync(HttpClient http, CancellationToken ct)
|
||||
{
|
||||
var text = await http.GetStringAsync("https://antigravity-cli-auto-updater-974169037036.us-central1.run.app", ct);
|
||||
var m = Regex.Match(text, @"Stable Version:\s*v?(\d+\.\d+(?:\.\d+)?)");
|
||||
return m.Success ? m.Groups[1].Value : text.Trim();
|
||||
}
|
||||
}
|
||||
|
||||
static class Engines
|
||||
{
|
||||
public static readonly Engine Claude = new ClaudeEngine();
|
||||
public static readonly Engine Codex = new CodexEngine();
|
||||
public static readonly Engine[] All = [Claude, Codex];
|
||||
public static readonly Engine Agy = new AgyEngine();
|
||||
public static readonly Engine[] All = [Claude, Codex, Agy];
|
||||
|
||||
public static Engine? Find(string? id) => All.FirstOrDefault(e => e.Id == id);
|
||||
}
|
||||
File renamed without changes.
@@ -126,6 +126,8 @@ static class EngineUpdater
|
||||
("codex", _) => OperatingSystem.IsWindows()
|
||||
? "powershell -ExecutionPolicy ByPass -c \"irm https://chatgpt.com/codex/install.ps1 | iex\""
|
||||
: "curl -fsSL https://chatgpt.com/codex/install.sh | sh",
|
||||
("agy", InstallMethod.Homebrew) => "brew upgrade agy",
|
||||
("agy", _) => "agy update",
|
||||
_ => $"{engine.Executable} update",
|
||||
};
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,124 @@
|
||||
using Avalonia.Automation;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Documents;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Media.Imaging;
|
||||
using Markdig.Syntax;
|
||||
using Markdig.Syntax.Inlines;
|
||||
|
||||
namespace AgentWikiUi.Gui;
|
||||
|
||||
/// <summary>Immagini nel markdown (ADR-0006): solo file locali, caricamento in background, ripiego sul testo alternativo.</summary>
|
||||
partial class MarkdownView
|
||||
{
|
||||
// Immagini (ADR-0006): solo file locali, entro questi limiti
|
||||
const long MaxImageBytes = 20L * 1024 * 1024;
|
||||
const int MaxDecodeWidth = 1600;
|
||||
const double MaxInlineImageWidth = 480;
|
||||
static readonly string[] ImageExtensions = [".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp"];
|
||||
|
||||
/// <summary>Immagini ancora in decodifica (ADR-0006); i test attendono che scenda a zero.</summary>
|
||||
internal int PendingImages { get; private set; }
|
||||
|
||||
/// <summary>Cartella da cui si cercano i percorsi relativi delle immagini: l'exe, poi le cartelle superiori (ADR-0006). Modificabile dai test.</summary>
|
||||
internal string BaseDirectory { get; set; } = AppContext.BaseDirectory;
|
||||
|
||||
static string AltText(LinkInline img) => string.Concat(img.OfType<LiteralInline>().Select(l => l.Content.ToString()));
|
||||
|
||||
/// <summary>Percorso locale a cui punta la destinazione di un'immagine; nullo per URL remoti o altri schemi.
|
||||
/// Un percorso relativo si cerca in <paramref name="baseDir"/> e poi nelle cartelle superiori (radice del progetto del motore): vince la prima in cui il file esiste.</summary>
|
||||
internal static string? ResolveImagePath(string? url, string baseDir)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(url)) return null;
|
||||
if (Uri.TryCreate(url, UriKind.Absolute, out var uri)) return uri.IsFile ? uri.LocalPath : null;
|
||||
var relative = Uri.UnescapeDataString(url);
|
||||
var first = Path.GetFullPath(relative, baseDir);
|
||||
for (var dir = new DirectoryInfo(baseDir); dir is not null; dir = dir.Parent)
|
||||
{
|
||||
var candidate = Path.GetFullPath(relative, dir.FullName);
|
||||
if (File.Exists(candidate)) return candidate;
|
||||
}
|
||||
return first; // non esiste da nessuna parte: il controllo successivo lo scarta
|
||||
}
|
||||
|
||||
/// <summary>Controllo rapido e sincrono: percorso locale, formato ammesso, file esistente entro il limite. Non decodifica.</summary>
|
||||
static bool IsImageCandidate(string? path)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (path is null || !ImageExtensions.Contains(Path.GetExtension(path), StringComparer.OrdinalIgnoreCase)) return false;
|
||||
var info = new FileInfo(path);
|
||||
return info.Exists && info.Length <= MaxImageBytes;
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
/// <summary>Decodifica l'immagine (chiamata fuori dal thread della GUI); nullo se illeggibile, mai eccezioni.</summary>
|
||||
static Bitmap? DecodeImage(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var fs = File.OpenRead(path);
|
||||
var bmp = new Bitmap(fs);
|
||||
if (bmp.PixelSize.Width <= MaxDecodeWidth) return bmp;
|
||||
bmp.Dispose();
|
||||
fs.Position = 0;
|
||||
return Bitmap.DecodeToWidth(fs, MaxDecodeWidth);
|
||||
}
|
||||
catch { return null; }
|
||||
}
|
||||
|
||||
// Segnaposto `[alt]` che diventa l'immagine a decodifica finita; se la decodifica fallisce resta il testo.
|
||||
Control ImageHost(string path, string alt, double maxWidth)
|
||||
{
|
||||
var host = new Decorator { HorizontalAlignment = HorizontalAlignment.Left, Child = new TextBlock { Text = "[" + alt + "]", TextWrapping = TextWrapping.Wrap } };
|
||||
PendingImages++;
|
||||
_ = LoadInto(host, path, alt, maxWidth);
|
||||
return host;
|
||||
}
|
||||
|
||||
async Task LoadInto(Decorator host, string path, string alt, double maxWidth)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (await Task.Run(() => DecodeImage(path)) is { } bmp) host.Child = ImageOf(bmp, alt, maxWidth);
|
||||
}
|
||||
finally { PendingImages--; }
|
||||
}
|
||||
|
||||
static Image ImageOf(Bitmap bmp, string alt, double maxWidth)
|
||||
{
|
||||
var img = new Image
|
||||
{
|
||||
Source = bmp, Stretch = Stretch.Uniform, StretchDirection = StretchDirection.DownOnly,
|
||||
MaxWidth = Math.Min(maxWidth, bmp.Size.Width), HorizontalAlignment = HorizontalAlignment.Left,
|
||||
};
|
||||
AutomationProperties.SetName(img, alt);
|
||||
ToolTip.SetTip(img, alt);
|
||||
return img;
|
||||
}
|
||||
|
||||
// Paragrafo fatto solo di immagini: blocchi alla larghezza della colonna. Nullo se non è così o nessuna è plausibile.
|
||||
Control? ImageParagraph(ParagraphBlock p)
|
||||
{
|
||||
if (p.Inline is null) return null;
|
||||
var images = new List<LinkInline>();
|
||||
foreach (var node in p.Inline)
|
||||
{
|
||||
if (node is LinkInline { IsImage: true } img) images.Add(img);
|
||||
else if (node is not LineBreakInline && !(node is LiteralInline lit && string.IsNullOrWhiteSpace(lit.Content.ToString()))) return null;
|
||||
}
|
||||
if (images.Count == 0) return null;
|
||||
var paths = images.Select(i => ResolveImagePath(i.Url, BaseDirectory)).ToList();
|
||||
if (!paths.Any(IsImageCandidate)) return null;
|
||||
var panel = new StackPanel { Spacing = 8 };
|
||||
for (var i = 0; i < images.Count; i++)
|
||||
{
|
||||
var image = images[i];
|
||||
panel.Children.Add(IsImageCandidate(paths[i]) ? ImageHost(paths[i]!, AltText(image), double.PositiveInfinity)
|
||||
: new SelectableTextBlock { Text = "[" + AltText(image) + "]", TextWrapping = TextWrapping.Wrap });
|
||||
}
|
||||
return panel;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
using Avalonia.Automation;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Documents;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.Media;
|
||||
using CSharpMath.Avalonia;
|
||||
using Markdig.Extensions.Mathematics;
|
||||
using Markdig.Syntax;
|
||||
|
||||
namespace AgentWikiUi.Gui;
|
||||
|
||||
/// <summary>Formule LaTeX nel markdown (ADR-0008): inline <c>$…$</c> e a blocco <c>$$…$$</c>; se il LaTeX non è valido compare il sorgente.</summary>
|
||||
partial class MarkdownView
|
||||
{
|
||||
const double InlineFormulaSize = 15;
|
||||
const double BlockFormulaSize = 18;
|
||||
|
||||
/// <summary>Sorgente LaTeX di un blocco <c>$$…$$</c> (righe tra i delimitatori).</summary>
|
||||
static string BlockSource(LeafBlock block) =>
|
||||
string.Join('\n', block.Lines.Lines.Take(block.Lines.Count).Select(l => l.ToString())).Trim();
|
||||
|
||||
Control FormulaBlock(string latex)
|
||||
{
|
||||
var formula = Formula(latex, BlockFormulaSize, "$$");
|
||||
// formula più larga della colonna: scorrimento orizzontale invece di tagliarla
|
||||
return new ScrollViewer
|
||||
{
|
||||
HorizontalScrollBarVisibility = ScrollBarVisibility.Auto, VerticalScrollBarVisibility = ScrollBarVisibility.Disabled,
|
||||
HorizontalAlignment = HorizontalAlignment.Left, Content = formula,
|
||||
};
|
||||
}
|
||||
|
||||
// colore del testo del tema (token Fg); senza il token (es. nei test) quello ereditato dal controllo
|
||||
Color TextColor() =>
|
||||
this.TryFindResource("Fg", ActualThemeVariant, out var v) && v is ISolidColorBrush s ? s.Color
|
||||
: TextElement.GetForeground(this) is ISolidColorBrush f ? f.Color : Colors.Black;
|
||||
|
||||
/// <summary>Regola di Pandoc per <c>$…$</c>: niente spazio dopo l'apertura né prima della chiusura, altrimenti i due <c>$</c> sono testo (prezzi, importi). Markdig da solo accetterebbe «10 $ e 20 $».</summary>
|
||||
static bool IsDollarText(string source, MathInline math)
|
||||
{
|
||||
// Markdig toglie gli spazi dal contenuto: si controlla il testo originale tra i due delimitatori
|
||||
if (math.DelimiterCount != 1) return false;
|
||||
var (start, end) = (math.Span.Start, math.Span.End);
|
||||
if (start < 0 || end >= source.Length || end - start < 2) return false; // intervallo non affidabile: si tratta come formula
|
||||
return char.IsWhiteSpace(source[start + 1]) || char.IsWhiteSpace(source[end - 1]);
|
||||
}
|
||||
|
||||
Control FormulaInline(string latex) => Formula(latex, InlineFormulaSize, "$");
|
||||
|
||||
/// <summary>Controllo che disegna la formula; se il LaTeX non è valido, il sorgente come testo monospazio (ripiego sicuro).</summary>
|
||||
Control Formula(string latex, double size, string delimiter)
|
||||
{
|
||||
latex = latex.Trim();
|
||||
try
|
||||
{
|
||||
var view = new MathView
|
||||
{
|
||||
LaTeX = latex, FontSize = (float)size, TextColor = TextColor(), DisplayErrorInline = false,
|
||||
VerticalAlignment = VerticalAlignment.Center, HorizontalAlignment = HorizontalAlignment.Left,
|
||||
};
|
||||
if (latex.Length > 0 && string.IsNullOrEmpty(view.ErrorMessage))
|
||||
{
|
||||
AutomationProperties.SetName(view, latex);
|
||||
ToolTip.SetTip(view, latex);
|
||||
return view;
|
||||
}
|
||||
}
|
||||
catch (Exception) { /* LaTeX o disegno non riusciti: ripiego sul sorgente */ }
|
||||
return new TextBlock { Text = delimiter + latex + delimiter, FontFamily = Mono, FontSize = 13.5, Foreground = Brush("FgMuted"), TextWrapping = TextWrapping.Wrap };
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ using Avalonia.Input.Platform;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.Media;
|
||||
using Markdig;
|
||||
using Markdig.Extensions.Mathematics;
|
||||
using Markdig.Extensions.Tables;
|
||||
using Markdig.Syntax;
|
||||
using Markdig.Syntax.Inlines;
|
||||
@@ -13,7 +14,7 @@ using Markdig.Syntax.Inlines;
|
||||
namespace AgentWikiUi.Gui;
|
||||
|
||||
/// <summary>Mostra markdown come controlli Avalonia (nessun HTML): il testo è Markdig, il rendering è qui.</summary>
|
||||
class MarkdownView : Decorator
|
||||
partial class MarkdownView : Decorator
|
||||
{
|
||||
public static readonly StyledProperty<string?> SourceProperty = AvaloniaProperty.Register<MarkdownView, string?>(nameof(Source));
|
||||
static readonly MarkdownPipeline Pipeline = new MarkdownPipelineBuilder().UseAdvancedExtensions().DisableHtml().Build();
|
||||
@@ -33,7 +34,14 @@ class MarkdownView : Decorator
|
||||
if (change.Property == SourceProperty) Rebuild();
|
||||
}
|
||||
|
||||
void Rebuild() => Child = string.IsNullOrEmpty(Source) ? null : RenderBlocks(Markdig.Markdown.Parse(Source, Pipeline), 10);
|
||||
string parsed = "";
|
||||
|
||||
void Rebuild()
|
||||
{
|
||||
// fine riga uniformi: gli intervalli di Markdig (Span) devono valere per il testo che si legge qui (ADR-0008)
|
||||
parsed = Source?.ReplaceLineEndings("\n") ?? "";
|
||||
Child = parsed.Length == 0 ? null : RenderBlocks(Markdig.Markdown.Parse(parsed, Pipeline), 10);
|
||||
}
|
||||
|
||||
IBrush Brush(string key) => this.TryFindResource(key, ActualThemeVariant, out var v) && v is IBrush b ? b : Brushes.Gray;
|
||||
|
||||
@@ -47,7 +55,8 @@ class MarkdownView : Decorator
|
||||
Control RenderBlock(Block block) => block switch
|
||||
{
|
||||
HeadingBlock h => Text(h.Inline, bold: true, size: h.Level switch { 1 => 24, 2 => 20, 3 => 18, 4 => 16, _ => 15.5 }),
|
||||
ParagraphBlock p => Text(p.Inline),
|
||||
ParagraphBlock p => ImageParagraph(p) ?? Text(p.Inline),
|
||||
MathBlock m => FormulaBlock(BlockSource(m)),
|
||||
FencedCodeBlock or CodeBlock => Code(string.Join('\n', ((LeafBlock)block).Lines.Lines.Take(((LeafBlock)block).Lines.Count).Select(l => l.ToString()))),
|
||||
QuoteBlock q => new Border { BorderBrush = Brush("Border"), BorderThickness = new(3, 0, 0, 0), Padding = new(12, 0, 0, 0), Child = RenderBlocks(q, 8) },
|
||||
ListBlock l => List(l),
|
||||
@@ -145,6 +154,16 @@ class MarkdownView : Decorator
|
||||
switch (node)
|
||||
{
|
||||
case LiteralInline lit: Add(target, lit.Content.ToString(), st, ref pos); break;
|
||||
case MathInline math:
|
||||
var latex = math.Content.ToString();
|
||||
if (IsDollarText(parsed, math)) // es. «10 $ e 20 $»: non è una formula
|
||||
Add(target, parsed.Substring(math.Span.Start, math.Span.End - math.Span.Start + 1), st, ref pos);
|
||||
else
|
||||
{
|
||||
target.Add(new InlineUIContainer(FormulaInline(latex)));
|
||||
pos += 1; // un controllo incorporato occupa una posizione nel testo
|
||||
}
|
||||
break;
|
||||
case LineBreakInline br: Add(target, br.IsHard ? "\n" : " ", st, ref pos); break;
|
||||
case CodeInline code:
|
||||
target.Add(new Run(code.Content) { FontFamily = Mono, FontSize = 13.5, Background = Brush("Muted") });
|
||||
@@ -156,7 +175,12 @@ class MarkdownView : Decorator
|
||||
AddInlines(target, em, inner, links, ref pos);
|
||||
break;
|
||||
case LinkInline { IsImage: true } img:
|
||||
Add(target, "[" + string.Concat(img.OfType<LiteralInline>().Select(l => l.Content.ToString())) + "]", st, ref pos);
|
||||
if (ResolveImagePath(img.Url, BaseDirectory) is { } imgPath && IsImageCandidate(imgPath))
|
||||
{
|
||||
target.Add(new InlineUIContainer(ImageHost(imgPath, AltText(img), MaxInlineImageWidth)));
|
||||
pos += 1; // un controllo incorporato occupa una posizione nel testo
|
||||
}
|
||||
else Add(target, "[" + AltText(img) + "]", st, ref pos);
|
||||
break;
|
||||
case LinkInline link:
|
||||
var start = pos;
|
||||
File renamed without changes.
File renamed without changes.
@@ -16,11 +16,16 @@
|
||||
<DebugType>none</DebugType>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="agent-wiki-ui.Tests" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia" Version="12.1.4" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="12.1.4" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="12.1.4" />
|
||||
<PackageReference Include="Markdig" Version="1.4.0" />
|
||||
<PackageReference Include="Sylinko.CSharpMath.Avalonia" Version="12.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<!-- Doppio di test di `claude` (vedi docs/TESTING.md → FakeClaude): l'eseguibile si chiama `claude` per poterlo mettere nel PATH -->
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<AssemblyName>claude</AssemblyName>
|
||||
<RootNamespace>FakeClaude</RootNamespace>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,15 @@
|
||||
using System.Text.Json;
|
||||
|
||||
// Configurazione e formato del registro: docs/TESTING.md → FakeClaude.
|
||||
var stdin = Console.In.ReadToEnd(); // resta in attesa finché il chiamante non chiude stdin
|
||||
|
||||
if (Environment.GetEnvironmentVariable("FAKE_CLAUDE_LOG") is { Length: > 0 } log)
|
||||
File.AppendAllText(log, JsonSerializer.Serialize(new { args, cwd = Directory.GetCurrentDirectory(), stdin }) + "\n");
|
||||
|
||||
if (int.TryParse(Environment.GetEnvironmentVariable("FAKE_CLAUDE_DELAY_MS"), out var delay) && delay > 0)
|
||||
Thread.Sleep(delay);
|
||||
|
||||
if (Environment.GetEnvironmentVariable("FAKE_CLAUDE_OUT") is { } o) Console.Out.Write(o);
|
||||
if (Environment.GetEnvironmentVariable("FAKE_CLAUDE_ERR") is { } e) Console.Error.Write(e);
|
||||
|
||||
return int.TryParse(Environment.GetEnvironmentVariable("FAKE_CLAUDE_EXIT"), out var code) ? code : 0;
|
||||
@@ -0,0 +1,142 @@
|
||||
using System.Net;
|
||||
using AgentWikiUi.Core;
|
||||
|
||||
namespace AgentWikiUi.Tests;
|
||||
|
||||
/// <summary>Motore Antigravity (docs/TESTING.md → Antigravity, AG-01…AG-10, AG-12, UP-21).</summary>
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class AgyEngineTests
|
||||
{
|
||||
static readonly Engine Agy = Engines.Agy;
|
||||
|
||||
sealed class FakeHandler(string body) : HttpMessageHandler
|
||||
{
|
||||
public Uri? Requested;
|
||||
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken ct)
|
||||
{
|
||||
Requested = request.RequestUri;
|
||||
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(body) });
|
||||
}
|
||||
}
|
||||
|
||||
[Fact] // AG-01
|
||||
public void Agy_primo_messaggio() =>
|
||||
Assert.Equal(["--output-format", "json", "--print-timeout", "120s", "-p", "ciao"],
|
||||
Agy.BuildArgs("ciao", null, resume: false, model: "", effort: "", structured: true));
|
||||
|
||||
[Fact] // AG-02
|
||||
public void Agy_ripresa() =>
|
||||
Assert.Equal(["--output-format", "json", "--conversation", "conv-1", "--print-timeout", "120s", "-p", "ancora"],
|
||||
Agy.BuildArgs("ancora", "conv-1", resume: true, model: "", effort: "", structured: true));
|
||||
|
||||
[Fact] // AG-03
|
||||
public void Agy_flag_prima_di_p()
|
||||
{
|
||||
var args = Agy.BuildArgs("ciao", "conv-1", true, "gemini-3.0-flash-low", "high", true);
|
||||
Assert.Equal(["-p", "ciao"], args[^2..]);
|
||||
Assert.Single(args, a => a == "-p");
|
||||
}
|
||||
|
||||
[Fact] // AG-04
|
||||
public void Agy_modello_ed_effort()
|
||||
{
|
||||
var con = Agy.BuildArgs("x", null, false, "claude-opus-4-6-thinking", "max", true);
|
||||
Assert.Equal("claude-opus-4-6-thinking", con[con.IndexOf("--model") + 1]);
|
||||
Assert.Equal("max", con[con.IndexOf("--effort") + 1]);
|
||||
|
||||
var senza = Agy.BuildArgs("x", null, false, "", "", true);
|
||||
Assert.DoesNotContain("--model", senza);
|
||||
Assert.DoesNotContain("--effort", senza);
|
||||
}
|
||||
|
||||
[Fact] // AG-05
|
||||
public void Agy_conversation_id()
|
||||
{
|
||||
Assert.True(Agy.OwnsSessionId);
|
||||
var r = Agy.Parse(0, """{"conversation_id":"abc-123","response":"ok","status":"SUCCESS"}""", "");
|
||||
Assert.Equal("abc-123", r.SessionId);
|
||||
}
|
||||
|
||||
[Fact] // AG-06
|
||||
public void Agy_estrazione_risposta()
|
||||
{
|
||||
var r = Agy.Parse(0, """{"conversation_id":"c","response":" Ciao **mondo**\n","status":"SUCCESS"}""", "");
|
||||
Assert.Equal("Ciao **mondo**", r.Text);
|
||||
Assert.Null(r.Error);
|
||||
}
|
||||
|
||||
[Fact] // AG-07
|
||||
public void Agy_errore_status()
|
||||
{
|
||||
var stato = Agy.Parse(0, """{"conversation_id":"c","response":"","status":"FAILED"}""", "");
|
||||
Assert.Equal("Stato FAILED", stato.Error);
|
||||
|
||||
var campo = Agy.Parse(0, """{"conversation_id":"c","status":"SUCCESS","error":"quota esaurita"}""", "");
|
||||
Assert.Equal("quota esaurita", campo.Error);
|
||||
|
||||
var uscita = Agy.Parse(2, "", "boom");
|
||||
Assert.Equal("", uscita.Text);
|
||||
Assert.Contains("agy è uscito con codice 2", uscita.Error);
|
||||
Assert.Contains("boom", uscita.Error);
|
||||
}
|
||||
|
||||
[Fact] // AG-08
|
||||
public void Agy_estrazione_json_robusta()
|
||||
{
|
||||
var out_ = "Avviso: aggiornamento disponibile\n{\"conversation_id\":\"c9\",\"response\":\"risposta\",\"status\":\"SUCCESS\"}\nfine\n";
|
||||
var r = Agy.Parse(0, out_, "");
|
||||
Assert.Equal("risposta", r.Text);
|
||||
Assert.Equal("c9", r.SessionId);
|
||||
Assert.Null(r.Error);
|
||||
}
|
||||
|
||||
[Fact] // AG-09
|
||||
public void Agy_nessuna_risposta()
|
||||
{
|
||||
Assert.Equal("agy non ha restituito nessuna risposta.", Agy.Parse(0, "", "").Error);
|
||||
Assert.Equal("agy non ha restituito nessuna risposta.", Agy.Parse(0, """{"conversation_id":"c","response":" ","status":"SUCCESS"}""", "").Error);
|
||||
Assert.Equal("solo stderr", Agy.Parse(0, "non json", "solo stderr").Error);
|
||||
}
|
||||
|
||||
[Fact] // AG-10
|
||||
public void Agy_cli_senza_json()
|
||||
{
|
||||
var args = Agy.BuildArgs("ciao", null, false, "", "", structured: false);
|
||||
Assert.DoesNotContain("--output-format", args);
|
||||
Assert.DoesNotContain("json", args);
|
||||
Assert.Equal(["-p", "ciao"], args[^2..]);
|
||||
}
|
||||
|
||||
[Fact] // AG-12
|
||||
public void Agy_catalogo_modelli()
|
||||
{
|
||||
Assert.Equal(
|
||||
["gemini-3.0-flash-low", "gemini-3.0-flash-medium", "gemini-3.0-flash-high",
|
||||
"claude-sonnet-4-6", "claude-opus-4-6-thinking", "gpt-oss-120b-medium"],
|
||||
Agy.Models.Select(m => m.Id));
|
||||
Assert.Equal(["low", "medium", "high", "xhigh", "max"], Agy.Efforts);
|
||||
Assert.Equal("gemini-3.0-flash-low", Agy.DefaultModelId);
|
||||
Assert.Equal("low", Agy.DefaultEffort);
|
||||
Assert.Contains(Agy.Models, m => m.Id == Agy.DefaultModelId);
|
||||
Assert.Same(Agy, Engines.Find("agy"));
|
||||
}
|
||||
|
||||
[Theory] // UP-21
|
||||
[InlineData("Stable Version: 1.4.2\nBuild: abc", "1.4.2")]
|
||||
[InlineData("Stable Version: v2.0", "2.0")]
|
||||
[InlineData("1.9.9", "1.9.9")]
|
||||
public async Task Ultima_versione_agy(string body, string expected)
|
||||
{
|
||||
var handler = new FakeHandler(body);
|
||||
using var http = new HttpClient(handler);
|
||||
Assert.Equal(expected, await Agy.FetchLatestVersionAsync(http, CancellationToken.None));
|
||||
Assert.NotNull(handler.Requested);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Comando_manuale_di_aggiornamento()
|
||||
{
|
||||
Assert.Equal("brew upgrade agy", EngineUpdater.ManualCommand(Agy, InstallMethod.Homebrew));
|
||||
Assert.Equal("agy update", EngineUpdater.ManualCommand(Agy, InstallMethod.Npm));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Automation;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Documents;
|
||||
using Avalonia.Headless.XUnit;
|
||||
using Avalonia.Media.Imaging;
|
||||
using Avalonia.Styling;
|
||||
using Avalonia.Threading;
|
||||
using Avalonia.VisualTree;
|
||||
using AgentWikiUi.Gui;
|
||||
|
||||
namespace AgentWikiUi.Tests;
|
||||
|
||||
/// <summary>Immagini nel markdown (ADR-0006): matrice MD-14 e MD-19…MD-26 in docs/TESTING.md.</summary>
|
||||
[Trait("Category", "Gui")]
|
||||
public sealed class MarkdownImageTests : IDisposable
|
||||
{
|
||||
readonly string dir = Directory.CreateTempSubdirectory("awui-img-").FullName;
|
||||
readonly List<string> inBase = [];
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Directory.Delete(dir, true);
|
||||
foreach (var f in inBase) File.Delete(f);
|
||||
}
|
||||
|
||||
string Png(string name, int w, int h)
|
||||
{
|
||||
var path = Path.Combine(dir, name);
|
||||
File.WriteAllBytes(path, PngFactory.Create(w, h));
|
||||
return path;
|
||||
}
|
||||
|
||||
static (Window Window, MarkdownView View) Show(string markdown, double width = 500, string? baseDir = null)
|
||||
{
|
||||
var view = new MarkdownView();
|
||||
if (baseDir is not null) view.BaseDirectory = baseDir;
|
||||
var window = new Window { Width = width, Height = 800, Content = view };
|
||||
window.Show();
|
||||
view.Source = markdown;
|
||||
WaitForImages(view);
|
||||
return (window, view);
|
||||
}
|
||||
|
||||
// il caricamento è in background (ADR-0006): si fa girare la coda della GUI finché tutte le immagini sono decodificate
|
||||
static void WaitForImages(MarkdownView view)
|
||||
{
|
||||
var deadline = DateTime.UtcNow.AddSeconds(10);
|
||||
while (view.PendingImages > 0 && DateTime.UtcNow < deadline) { Dispatcher.UIThread.RunJobs(); Thread.Sleep(5); }
|
||||
Dispatcher.UIThread.RunJobs();
|
||||
(TopLevel.GetTopLevel(view) as Window)?.UpdateLayout();
|
||||
Assert.Equal(0, view.PendingImages);
|
||||
}
|
||||
|
||||
static List<Image> Images(Visual v) => v.GetVisualDescendants().OfType<Image>().ToList();
|
||||
|
||||
static string Text(Visual v) => string.Concat(v.GetVisualDescendants().OfType<TextBlock>()
|
||||
.Select(t => t.Inlines is { Count: > 0 } ? string.Concat(t.Inlines.OfType<Run>().Select(r => r.Text)) : t.Text));
|
||||
|
||||
[AvaloniaTheory]
|
||||
[InlineData("https://example.com/a.png")]
|
||||
[InlineData("http://example.com/a.png")]
|
||||
[InlineData("ftp://example.com/a.png")]
|
||||
[InlineData("data:image/png;base64,AAAA")]
|
||||
public void Immagini_remote_come_testo(string url)
|
||||
{
|
||||
var (_, view) = Show($"Vedi  qui");
|
||||
Assert.Empty(Images(view));
|
||||
Assert.Contains("[logo]", Text(view));
|
||||
}
|
||||
|
||||
[AvaloniaFact]
|
||||
public void Immagine_percorso_assoluto()
|
||||
{
|
||||
var path = Png("a.png", 40, 20);
|
||||
var (_, view) = Show($"");
|
||||
var img = Assert.Single(Images(view));
|
||||
var bmp = Assert.IsAssignableFrom<Bitmap>(img.Source);
|
||||
Assert.Equal(new PixelSize(40, 20), bmp.PixelSize);
|
||||
Assert.Equal(40, img.Bounds.Width, 0.5);
|
||||
Assert.DoesNotContain("[griglia]", Text(view));
|
||||
}
|
||||
|
||||
[AvaloniaFact]
|
||||
public void Immagine_percorso_relativo_e_file_uri()
|
||||
{
|
||||
var name = $"awui-{Guid.NewGuid():N}.png";
|
||||
var inBasePath = Path.Combine(AppContext.BaseDirectory, name);
|
||||
File.WriteAllBytes(inBasePath, PngFactory.Create(10, 10));
|
||||
inBase.Add(inBasePath);
|
||||
var (_, relative) = Show($"");
|
||||
Assert.Single(Images(relative));
|
||||
|
||||
var uri = new Uri(Png("b.png", 12, 12)).AbsoluteUri;
|
||||
var (_, fileUri) = Show($"");
|
||||
Assert.Single(Images(fileUri));
|
||||
}
|
||||
|
||||
[AvaloniaTheory]
|
||||
[InlineData("manca")]
|
||||
[InlineData("corrotto")]
|
||||
[InlineData("estensione")]
|
||||
public void Immagine_non_valida_ripiego(string caso)
|
||||
{
|
||||
var path = caso switch
|
||||
{
|
||||
"manca" => Path.Combine(dir, "non-esiste.png"),
|
||||
"corrotto" => Write("rotto.png", "questo non è un png"u8.ToArray()),
|
||||
_ => Write("dati.txt", PngFactory.Create(8, 8)),
|
||||
};
|
||||
var (_, view) = Show($"");
|
||||
Assert.Empty(Images(view));
|
||||
Assert.Contains("[alt]", Text(view));
|
||||
}
|
||||
|
||||
string Write(string name, byte[] bytes)
|
||||
{
|
||||
var path = Path.Combine(dir, name);
|
||||
File.WriteAllBytes(path, bytes);
|
||||
return path;
|
||||
}
|
||||
|
||||
[AvaloniaFact]
|
||||
public void Immagine_oltre_limite()
|
||||
{
|
||||
var path = Path.Combine(dir, "grande.png");
|
||||
using (var fs = File.Create(path)) { fs.Write(PngFactory.Create(8, 8)); fs.SetLength(20L * 1024 * 1024 + 1); }
|
||||
var (_, view) = Show($"");
|
||||
Assert.Empty(Images(view));
|
||||
Assert.Contains("[enorme]", Text(view));
|
||||
}
|
||||
|
||||
[AvaloniaFact]
|
||||
public void Immagine_larghezza_massima()
|
||||
{
|
||||
var (_, wide) = Show($"})", width: 500);
|
||||
var img = Assert.Single(Images(wide));
|
||||
Assert.True(img.Bounds.Width <= 500, $"larghezza {img.Bounds.Width}");
|
||||
Assert.Equal(img.Bounds.Width / 20, img.Bounds.Height, 1); // proporzioni 20:1
|
||||
|
||||
var (_, small) = Show($"})", width: 500);
|
||||
Assert.Equal(50, Assert.Single(Images(small)).Bounds.Width, 0.5); // mai ingrandita
|
||||
}
|
||||
|
||||
[AvaloniaFact]
|
||||
public void Immagine_testo_alternativo_accessibile()
|
||||
{
|
||||
var (_, view) = Show($"})");
|
||||
var img = Assert.Single(Images(view));
|
||||
Assert.Equal("un grafico", AutomationProperties.GetName(img));
|
||||
Assert.Equal("un grafico", ToolTip.GetTip(img));
|
||||
}
|
||||
|
||||
[AvaloniaFact]
|
||||
public void Immagine_in_contesti_annidati()
|
||||
{
|
||||
var p = Png("n.png", 10, 10);
|
||||
var markdown = $"""
|
||||
Testo  e ancora testo con [un link](https://example.com).
|
||||
|
||||
[](https://example.com)
|
||||
|
||||
- voce 
|
||||
|
||||
| a | b |
|
||||
|---|---|
|
||||
|  | x |
|
||||
""";
|
||||
var (_, view) = Show(markdown);
|
||||
Assert.Equal(4, Images(view).Count);
|
||||
Assert.Contains("un link", Text(view));
|
||||
}
|
||||
|
||||
[AvaloniaFact]
|
||||
public void Immagine_cambio_tema()
|
||||
{
|
||||
var (window, view) = Show($"})");
|
||||
window.RequestedThemeVariant = ThemeVariant.Dark;
|
||||
WaitForImages(view);
|
||||
Assert.Single(Images(view));
|
||||
window.RequestedThemeVariant = ThemeVariant.Light;
|
||||
WaitForImages(view);
|
||||
Assert.Single(Images(view));
|
||||
}
|
||||
|
||||
[AvaloniaFact]
|
||||
public void Immagine_caricamento_in_background()
|
||||
{
|
||||
var path = Png("bg.png", 30, 30);
|
||||
var view = new MarkdownView();
|
||||
var window = new Window { Width = 500, Height = 800, Content = view };
|
||||
window.Show();
|
||||
view.Source = $"";
|
||||
// subito dopo il rendering: immagine in attesa e solo il segnaposto (il completamento passa dalla coda della GUI)
|
||||
Assert.Equal(1, view.PendingImages);
|
||||
Assert.Empty(Images(view));
|
||||
Assert.Contains("[segnaposto]", Text(view));
|
||||
WaitForImages(view);
|
||||
Assert.Single(Images(view));
|
||||
Assert.DoesNotContain("[segnaposto]", Text(view));
|
||||
|
||||
// file corrotto: la decodifica fallisce e il segnaposto resta
|
||||
var rotto = Write("rotto2.png", "no"u8.ToArray());
|
||||
view.Source = $"";
|
||||
WaitForImages(view);
|
||||
Assert.Empty(Images(view));
|
||||
Assert.Contains("[guasta]", Text(view));
|
||||
}
|
||||
|
||||
// Struttura tipo wiki: <radice>/catalogo.json, <radice>/figure/<file>, exe in <radice>/query/
|
||||
string Wiki(string name, params string[] files)
|
||||
{
|
||||
var root = Path.Combine(dir, name);
|
||||
Directory.CreateDirectory(Path.Combine(root, "figure"));
|
||||
Directory.CreateDirectory(Path.Combine(root, "query"));
|
||||
foreach (var f in files) File.WriteAllBytes(Path.Combine(root, "figure", f), PngFactory.Create(24, 12));
|
||||
File.WriteAllText(Path.Combine(root, "catalogo.json"), "{\"figures\":[]}");
|
||||
return root;
|
||||
}
|
||||
|
||||
[AvaloniaFact]
|
||||
public void Immagine_percorso_relativo_alla_radice()
|
||||
{
|
||||
var root = Wiki("wiki-a", "fig1.png");
|
||||
var (_, view) = Show("", baseDir: Path.Combine(root, "query"));
|
||||
Assert.Single(Images(view));
|
||||
Assert.DoesNotContain("[Figura 1]", Text(view));
|
||||
}
|
||||
|
||||
[AvaloniaFact]
|
||||
public void Immagine_cartella_piu_vicina()
|
||||
{
|
||||
var root = Wiki("wiki-b");
|
||||
File.WriteAllBytes(Path.Combine(root, "figure", "x.png"), PngFactory.Create(30, 30));
|
||||
var exeDir = Path.Combine(root, "query");
|
||||
Directory.CreateDirectory(Path.Combine(exeDir, "figure"));
|
||||
File.WriteAllBytes(Path.Combine(exeDir, "figure", "x.png"), PngFactory.Create(10, 10));
|
||||
|
||||
var (_, view) = Show("", baseDir: exeDir);
|
||||
var bmp = Assert.IsAssignableFrom<Bitmap>(Assert.Single(Images(view)).Source);
|
||||
Assert.Equal(new PixelSize(10, 10), bmp.PixelSize); // vince la cartella più vicina all'exe
|
||||
|
||||
var (_, nowhere) = Show("", baseDir: exeDir);
|
||||
Assert.Empty(Images(nowhere));
|
||||
Assert.Contains("[y]", Text(nowhere));
|
||||
}
|
||||
|
||||
[AvaloniaFact]
|
||||
public void Immagine_risposta_con_didascalia()
|
||||
{
|
||||
var root = Wiki("wiki-c", "schema-a.png");
|
||||
var risposta = """
|
||||
Lo schema è in Figura 1.
|
||||
|
||||

|
||||
|
||||
Figura 1 - schema generale del sistema · §2.1 r.40 · [[fig-001]]
|
||||
""";
|
||||
var (_, view) = Show(risposta, baseDir: Path.Combine(root, "query"));
|
||||
Assert.Single(Images(view));
|
||||
var text = Text(view);
|
||||
Assert.Contains("Figura 1 - schema generale", text);
|
||||
Assert.Contains("[[fig-001]]", text); // il link alla nota resta testo: l'app non legge il catalogo JSON
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Automation;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Documents;
|
||||
using Avalonia.Headless.XUnit;
|
||||
using Avalonia.Styling;
|
||||
using Avalonia.Threading;
|
||||
using Avalonia.VisualTree;
|
||||
using CSharpMath.Avalonia;
|
||||
using AgentWikiUi.Gui;
|
||||
|
||||
namespace AgentWikiUi.Tests;
|
||||
|
||||
/// <summary>Formule LaTeX nel markdown (ADR-0008): matrice MD-31…MD-38 in docs/TESTING.md.</summary>
|
||||
[Trait("Category", "Gui")]
|
||||
public sealed class MarkdownMathTests
|
||||
{
|
||||
static (Window Window, MarkdownView View) Show(string markdown, double width = 600)
|
||||
{
|
||||
var view = new MarkdownView();
|
||||
var window = new Window { Width = width, Height = 800, Content = view };
|
||||
window.Show();
|
||||
view.Source = markdown;
|
||||
Dispatcher.UIThread.RunJobs();
|
||||
window.UpdateLayout();
|
||||
return (window, view);
|
||||
}
|
||||
|
||||
static List<MathView> Formulas(Visual v) => v.GetVisualDescendants().OfType<MathView>().ToList();
|
||||
|
||||
static string Text(Visual v) => string.Concat(v.GetVisualDescendants().OfType<TextBlock>()
|
||||
.Select(t => t.Inlines is { Count: > 0 } ? string.Concat(t.Inlines.OfType<Run>().Select(r => r.Text)) : t.Text));
|
||||
|
||||
[AvaloniaFact]
|
||||
public void Formula_inline()
|
||||
{
|
||||
var (_, view) = Show("Il gap è $E_g = E_{LUMO} - E_{HOMO}$ in eV.");
|
||||
var f = Assert.Single(Formulas(view));
|
||||
Assert.Equal("E_g = E_{LUMO} - E_{HOMO}", AutomationProperties.GetName(f));
|
||||
Assert.True(string.IsNullOrEmpty(f.ErrorMessage));
|
||||
Assert.True(f.Bounds.Width > 0 && f.Bounds.Height > 0);
|
||||
var text = Text(view);
|
||||
Assert.Contains("Il gap è", text);
|
||||
Assert.Contains("in eV.", text);
|
||||
Assert.DoesNotContain("$", text);
|
||||
}
|
||||
|
||||
[AvaloniaFact]
|
||||
public void Formula_a_blocco()
|
||||
{
|
||||
var (_, view) = Show("Prima.\n\n$$\n\\frac{a}{b} = c\n$$\n\nDopo.");
|
||||
var f = Assert.Single(Formulas(view));
|
||||
Assert.Equal("\\frac{a}{b} = c", AutomationProperties.GetName(f));
|
||||
Assert.True(string.IsNullOrEmpty(f.ErrorMessage));
|
||||
Assert.True(f.Bounds.Height > 0);
|
||||
Assert.DoesNotContain("\\frac", Text(view)); // non è un blocco di codice col sorgente
|
||||
Assert.Contains("Prima.", Text(view));
|
||||
Assert.Contains("Dopo.", Text(view));
|
||||
}
|
||||
|
||||
[AvaloniaTheory]
|
||||
[InlineData("Testo $\\frac{a$ fine")]
|
||||
[InlineData("$$\n\\unknowncommand{x}\n$$")]
|
||||
[InlineData("Testo $\\begin{matrix$ fine")]
|
||||
public void Formula_non_valida_ripiego(string markdown)
|
||||
{
|
||||
var (_, view) = Show(markdown);
|
||||
Assert.Empty(Formulas(view));
|
||||
Assert.Contains("$", Text(view)); // il sorgente resta visibile come testo
|
||||
}
|
||||
|
||||
[AvaloniaTheory]
|
||||
[InlineData("\\frac{a}{b}")]
|
||||
[InlineData("x^2 + y_i")]
|
||||
[InlineData("\\mathbf{A} \\times \\mathbf{B}")]
|
||||
[InlineData("\\overline{x}")]
|
||||
[InlineData("\\left( \\frac{1}{2} \\right)")]
|
||||
[InlineData("\\lambda_{max} \\approx \\pi \\cdot \\tau")]
|
||||
[InlineData("\\sum_{i=1}^{n} x_i")]
|
||||
[InlineData("\\text{Mn} = \\chi \\varepsilon")]
|
||||
public void Costrutti_latex_comuni(string latex)
|
||||
{
|
||||
var (_, view) = Show($"Prima $${latex}$$ dopo".Replace("$$", "$"));
|
||||
var f = Assert.Single(Formulas(view));
|
||||
Assert.Equal(latex, AutomationProperties.GetName(f));
|
||||
Assert.True(string.IsNullOrEmpty(f.ErrorMessage), f.ErrorMessage);
|
||||
Assert.True(f.Bounds.Width > 0);
|
||||
}
|
||||
|
||||
[AvaloniaFact]
|
||||
public void Formula_cambio_tema()
|
||||
{
|
||||
var (window, view) = Show("Gap $E_g$ e\n\n$$\\frac{a}{b}$$");
|
||||
Assert.Equal(2, Formulas(view).Count);
|
||||
window.RequestedThemeVariant = ThemeVariant.Dark;
|
||||
Dispatcher.UIThread.RunJobs();
|
||||
Assert.Equal(2, Formulas(view).Count);
|
||||
window.RequestedThemeVariant = ThemeVariant.Light;
|
||||
Dispatcher.UIThread.RunJobs();
|
||||
Assert.Equal(2, Formulas(view).Count);
|
||||
}
|
||||
|
||||
[AvaloniaFact]
|
||||
public void Formula_in_contesti_annidati()
|
||||
{
|
||||
var markdown = """
|
||||
Testo $a_1$ e un [link](https://example.com).
|
||||
|
||||
[vai a $b^2$](https://example.com)
|
||||
|
||||
- voce $c_3$
|
||||
|
||||
| x | y |
|
||||
|---|---|
|
||||
| $d_4$ | z |
|
||||
""";
|
||||
var (_, view) = Show(markdown);
|
||||
Assert.Equal(4, Formulas(view).Count);
|
||||
Assert.Contains("link", Text(view));
|
||||
Assert.Contains("vai a", Text(view));
|
||||
}
|
||||
|
||||
[AvaloniaFact]
|
||||
public void Formula_nome_accessibile()
|
||||
{
|
||||
var (_, view) = Show("Gap $E_g$.");
|
||||
var f = Assert.Single(Formulas(view));
|
||||
Assert.Equal("E_g", AutomationProperties.GetName(f));
|
||||
}
|
||||
|
||||
[AvaloniaFact]
|
||||
public void Dollari_isolati_restano_testo()
|
||||
{
|
||||
var (_, view) = Show("Costa 10 $ e poi 20 $ in totale.");
|
||||
Assert.Empty(Formulas(view));
|
||||
Assert.Contains("10 $ e poi 20 $", Text(view));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
using System.Diagnostics;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace AgentWikiUi.Tests;
|
||||
|
||||
/// <summary>Il doppio di test di <c>claude</c> (docs/TESTING.md → FakeClaude, FK-01…FK-05).</summary>
|
||||
[Trait("Category", "Process")]
|
||||
public sealed class FakeClaudeTests : IDisposable
|
||||
{
|
||||
readonly string dir = Directory.CreateTempSubdirectory("awui-fake-").FullName;
|
||||
|
||||
public void Dispose() => Directory.Delete(dir, true);
|
||||
|
||||
string LogPath => Path.Combine(dir, "log.jsonl");
|
||||
|
||||
Process Start(IEnumerable<string> args, Dictionary<string, string>? env = null, string? cwd = null)
|
||||
{
|
||||
var psi = new ProcessStartInfo(FakeClaudePath.Exe)
|
||||
{
|
||||
UseShellExecute = false, RedirectStandardInput = true, RedirectStandardOutput = true, RedirectStandardError = true,
|
||||
WorkingDirectory = cwd ?? dir,
|
||||
};
|
||||
foreach (var a in args) psi.ArgumentList.Add(a);
|
||||
psi.Environment["FAKE_CLAUDE_LOG"] = LogPath;
|
||||
foreach (var (k, v) in env ?? []) psi.Environment[k] = v;
|
||||
return Process.Start(psi)!;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Registra_argomenti_cwd_e_stdin()
|
||||
{
|
||||
var cwd = Directory.CreateDirectory(Path.Combine(dir, "lavoro")).FullName;
|
||||
using var p = Start(["-p", "dì \"ciao\" a tutti", "--model", "m"], cwd: cwd);
|
||||
p.StandardInput.Write("dati da stdin");
|
||||
p.StandardInput.Close();
|
||||
Assert.True(p.WaitForExit(10_000));
|
||||
|
||||
using var line = JsonDocument.Parse(File.ReadAllLines(LogPath).Single());
|
||||
var r = line.RootElement;
|
||||
Assert.Equal(["-p", "dì \"ciao\" a tutti", "--model", "m"], r.GetProperty("args").EnumerateArray().Select(a => a.GetString()));
|
||||
Assert.Equal(cwd, r.GetProperty("cwd").GetString());
|
||||
Assert.Equal("dati da stdin", r.GetProperty("stdin").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Output_e_stderr_configurati()
|
||||
{
|
||||
using var p = Start([], new() { ["FAKE_CLAUDE_OUT"] = "risposta", ["FAKE_CLAUDE_ERR"] = "avviso" });
|
||||
p.StandardInput.Close();
|
||||
Assert.Equal("risposta", p.StandardOutput.ReadToEnd());
|
||||
Assert.Equal("avviso", p.StandardError.ReadToEnd());
|
||||
Assert.True(p.WaitForExit(10_000));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null, 0)]
|
||||
[InlineData("3", 3)]
|
||||
[InlineData("127", 127)]
|
||||
public void ExitCode_configurato(string? configured, int expected)
|
||||
{
|
||||
using var p = Start([], configured is null ? null : new() { ["FAKE_CLAUDE_EXIT"] = configured });
|
||||
p.StandardInput.Close();
|
||||
Assert.True(p.WaitForExit(10_000));
|
||||
Assert.Equal(expected, p.ExitCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ritardo_configurato()
|
||||
{
|
||||
using var p = Start([], new() { ["FAKE_CLAUDE_DELAY_MS"] = "600" });
|
||||
var sw = Stopwatch.StartNew();
|
||||
p.StandardInput.Close();
|
||||
Assert.True(p.WaitForExit(10_000));
|
||||
Assert.True(sw.ElapsedMilliseconds >= 500, $"terminato dopo {sw.ElapsedMilliseconds} ms");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Attende_la_chiusura_di_stdin()
|
||||
{
|
||||
using var p = Start([]);
|
||||
try
|
||||
{
|
||||
Assert.False(p.WaitForExit(700), "il processo è terminato con stdin ancora aperto");
|
||||
p.StandardInput.Close();
|
||||
Assert.True(p.WaitForExit(10_000));
|
||||
}
|
||||
finally { if (!p.HasExited) p.Kill(); }
|
||||
}
|
||||
|
||||
[Fact] // AG-11
|
||||
public void Agy_timeout_e_stdin()
|
||||
{
|
||||
var psi = AgentWikiUi.Core.EngineRunner.CreateStart(AgentWikiUi.Core.Engines.Agy, "ciao", captured: true);
|
||||
Assert.True(psi.RedirectStandardInput);
|
||||
Assert.Equal(AppContext.BaseDirectory, psi.WorkingDirectory);
|
||||
|
||||
// Gli argomenti di agy, con stdin chiuso subito, bastano a far terminare il processo finto: nessun blocco.
|
||||
using var p = Start(psi.ArgumentList);
|
||||
p.StandardInput.Close();
|
||||
Assert.True(p.WaitForExit(10_000));
|
||||
|
||||
using var line = JsonDocument.Parse(File.ReadAllLines(LogPath).Single());
|
||||
var args = line.RootElement.GetProperty("args").EnumerateArray().Select(a => a.GetString()).ToList();
|
||||
Assert.Equal("120s", args[args.IndexOf("--print-timeout") + 1]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace AgentWikiUi.Tests;
|
||||
|
||||
/// <summary>Percorso del <c>claude</c> finto compilato (tests/FakeClaude), nella stessa configurazione dei test.</summary>
|
||||
static class FakeClaudePath
|
||||
{
|
||||
public static string Exe
|
||||
{
|
||||
get
|
||||
{
|
||||
// …/tests/agent-wiki-ui.Tests/bin/<config>/net10.0/
|
||||
var tfm = new DirectoryInfo(AppContext.BaseDirectory.TrimEnd(Path.DirectorySeparatorChar));
|
||||
var config = tfm.Parent!;
|
||||
var testsRoot = config.Parent!.Parent!.Parent!;
|
||||
return Path.Combine(testsRoot.FullName, "FakeClaude", "bin", config.Name, tfm.Name, OperatingSystem.IsWindows() ? "claude.exe" : "claude");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using System.IO.Compression;
|
||||
|
||||
namespace AgentWikiUi.Tests;
|
||||
|
||||
/// <summary>Scrive PNG minimali (colore pieno) senza dipendere da Avalonia.</summary>
|
||||
static class PngFactory
|
||||
{
|
||||
public static byte[] Create(int width, int height)
|
||||
{
|
||||
var raw = new byte[height * (1 + width * 3)];
|
||||
for (var y = 0; y < height; y++)
|
||||
for (var x = 0; x < width; x++) raw[y * (1 + width * 3) + 1 + x * 3] = 200; // filtro 0 per riga, canale rosso
|
||||
using var ms = new MemoryStream();
|
||||
ms.Write([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]);
|
||||
var ihdr = new byte[13];
|
||||
WriteInt(ihdr, 0, width); WriteInt(ihdr, 4, height);
|
||||
ihdr[8] = 8; ihdr[9] = 2; // 8 bit, RGB
|
||||
Chunk(ms, "IHDR", ihdr);
|
||||
using (var z = new MemoryStream())
|
||||
{
|
||||
using (var zs = new ZLibStream(z, CompressionLevel.Fastest, leaveOpen: true)) zs.Write(raw);
|
||||
Chunk(ms, "IDAT", z.ToArray());
|
||||
}
|
||||
Chunk(ms, "IEND", []);
|
||||
return ms.ToArray();
|
||||
}
|
||||
|
||||
static void WriteInt(byte[] b, int at, int v) { b[at] = (byte)(v >> 24); b[at + 1] = (byte)(v >> 16); b[at + 2] = (byte)(v >> 8); b[at + 3] = (byte)v; }
|
||||
|
||||
static void Chunk(Stream s, string type, byte[] data)
|
||||
{
|
||||
var len = new byte[4]; WriteInt(len, 0, data.Length);
|
||||
s.Write(len);
|
||||
var body = new byte[4 + data.Length];
|
||||
for (var i = 0; i < 4; i++) body[i] = (byte)type[i];
|
||||
data.CopyTo(body, 4);
|
||||
s.Write(body);
|
||||
var crc = new byte[4]; WriteInt(crc, 0, (int)Crc32(body));
|
||||
s.Write(crc);
|
||||
}
|
||||
|
||||
static uint Crc32(byte[] data)
|
||||
{
|
||||
var crc = 0xFFFFFFFFu;
|
||||
foreach (var b in data)
|
||||
{
|
||||
crc ^= b;
|
||||
for (var k = 0; k < 8; k++) crc = (crc & 1) != 0 ? (crc >> 1) ^ 0xEDB88320u : crc >> 1;
|
||||
}
|
||||
return ~crc;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Headless;
|
||||
using Avalonia.Themes.Fluent;
|
||||
using AgentWikiUi.Tests;
|
||||
|
||||
[assembly: AvaloniaTestApplication(typeof(TestAppBuilder))]
|
||||
|
||||
namespace AgentWikiUi.Tests;
|
||||
|
||||
public class TestApp : Application
|
||||
{
|
||||
public override void Initialize() => Styles.Add(new FluentTheme());
|
||||
}
|
||||
|
||||
public static class TestAppBuilder
|
||||
{
|
||||
// Skia reale (non il disegno finto): serve per decodificare davvero le immagini
|
||||
public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure<TestApp>()
|
||||
.UseSkia()
|
||||
.UseHeadless(new AvaloniaHeadlessPlatformOptions { UseHeadlessDrawing = false });
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<RootNamespace>AgentWikiUi.Tests</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageReference Include="xunit.v3" Version="3.2.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.0" />
|
||||
<PackageReference Include="Avalonia.Headless.XUnit" Version="12.1.4" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../src/agent-wiki-ui/agent-wiki-ui.csproj" />
|
||||
<!-- solo compilato (non referenziato): i test avviano l'eseguibile -->
|
||||
<ProjectReference Include="../FakeClaude/FakeClaude.csproj" ReferenceOutputAssembly="false" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in new issue
Block a user