GUI: impostazioni di modello ed effort in un riquadro in alto a destra

Modello ed effort lasciano il composer e passano in un riquadro a comparsa
aperto dal pulsante in alto a destra, simmetrico a quello della barra
laterale. Il composer resta con campo di testo e pulsante Invia sulla
stessa riga. Eliminati i suggerimenti della schermata iniziale. MVP.md e la
matrice di TESTING.md sono aggiornati di conseguenza.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
davideandClaude Sonnet 5.5 committed 2026-10-15 11:26:06 +02:00
1 parent 84fefda219
commit fee788134d
5 files changed
+43 -67

No files matched your search

-13
View File
@@ -66,19 +66,6 @@
<Setter Property="BorderBrush" Value="{DynamicResource Border}" />
<Setter Property="BorderThickness" Value="1" />
</Style>
<Style Selector="Button.chip">
<Setter Property="Background" Value="{DynamicResource Surface}" />
<Setter Property="BorderBrush" Value="{DynamicResource Border}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="14" />
<Setter Property="Padding" Value="16,12" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="HorizontalContentAlignment" Value="Left" />
</Style>
<Style Selector="Button.chip:pointerover /template/ ContentPresenter">
<Setter Property="BorderBrush" Value="{DynamicResource Accent}" />
<Setter Property="Background" Value="{DynamicResource Muted}" />
</Style>
<Style Selector="Button.send">
<Setter Property="Width" Value="36" />
<Setter Property="Height" Value="36" />
-12
View File
@@ -21,8 +21,6 @@ class ChatItem(string id, string title, DateTime updated, bool active)
public bool IsActive { get; } = active;
}
record Suggestion(string Title, string Subtitle, string Prompt);
class MainViewModel : ObservableObject
{
static readonly ModelOption Default = new("Predefinito", "");
@@ -33,20 +31,11 @@ class MainViewModel : ObservableObject
public ModelOption[] ModelChoices { get; } = [Default, .. Catalog.Models, Custom];
public string[] EffortChoices { get; } = ["Predefinito", .. Catalog.Efforts];
public Suggestion[] Suggestions { get; } =
[
new("Spiega il progetto", "Panoramica e struttura", "Spiega cosa fa questo progetto e come è organizzato."),
new("Scrivi uno script", "PowerShell, Python, C#…", "Scrivi uno script PowerShell che "),
new("Rivedi del codice", "Bug, leggibilità, prestazioni", "Rivedi questo codice e suggerisci miglioramenti:\n\n"),
new("Riassumi un testo", "Punti chiave in breve", "Riassumi in modo chiaro questo testo:\n\n"),
];
public ICommand NewChatCommand => newChat;
public ICommand CopyCommand { get; }
public ICommand SendCommand { get; }
public ICommand OpenCommand { get; }
public ICommand DeleteCommand { get; }
public ICommand SuggestCommand { get; }
public ICommand ToggleSidebarCommand { get; }
/// <summary>Conferma di eliminazione, fornita dalla finestra.</summary>
@@ -75,7 +64,6 @@ class MainViewModel : ObservableObject
CopyCommand = new RelayCommand<string>(t => { if (t is not null) _ = CopyText(t); });
OpenCommand = new RelayCommand<ChatItem>(c => { if (c is not null) OpenChat(c.Id); });
DeleteCommand = new RelayCommand<ChatItem>(c => { if (c is not null) _ = DeleteAsync(c); });
SuggestCommand = new RelayCommand<Suggestion>(s => { if (s is not null) { Input = s.Prompt; FocusInput?.Invoke(); } });
ToggleSidebarCommand = new RelayCommand<object>(_ => SidebarOpen = !SidebarOpen);
Messages.CollectionChanged += (_, _) => { Raise(nameof(HasMessages)); Raise(nameof(ShowEmpty)); };
+35 -36
View File
@@ -77,22 +77,7 @@
</Viewbox>
</Border>
<TextBlock Text="Come posso aiutarti?" FontSize="28" FontWeight="SemiBold" HorizontalAlignment="Center" />
<TextBlock Text="Scrivi un messaggio oppure parti da uno di questi spunti." Foreground="{DynamicResource FgMuted}" HorizontalAlignment="Center" />
<ItemsControl ItemsSource="{Binding Suggestions}" Margin="0,16,0,0" Width="640">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate><UniformGrid Columns="2" /></ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="gui:Suggestion">
<Button Classes="chip" Margin="5" Command="{Binding $parent[ItemsControl].((gui:MainViewModel)DataContext).SuggestCommand}" CommandParameter="{Binding}">
<StackPanel>
<TextBlock Text="{Binding Title}" FontWeight="SemiBold" />
<TextBlock Text="{Binding Subtitle}" FontSize="12.5" Foreground="{DynamicResource FgMuted}" />
</StackPanel>
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<TextBlock Text="Scrivi un messaggio per iniziare." Foreground="{DynamicResource FgMuted}" HorizontalAlignment="Center" />
</StackPanel>
<!-- Conversazione -->
@@ -133,31 +118,45 @@
<Viewbox Width="16" Height="16"><Path Data="M4 6H20M4 12H20M4 18H20" Stroke="{DynamicResource Fg}" StrokeThickness="2" StrokeLineCap="Round" /></Viewbox>
</Button>
<!-- Composer -->
<StackPanel Grid.Row="1" MaxWidth="780" Margin="24,0,24,12">
<Border Classes="composer" Background="{DynamicResource Surface}" BorderBrush="{DynamicResource Border}" BorderThickness="1" CornerRadius="20" Padding="14,10">
<StackPanel Spacing="8">
<TextBox x:Name="InputBox" Classes="bare" Text="{Binding Input, Mode=TwoWay}" AcceptsReturn="True" TextWrapping="Wrap" MaxHeight="200"
PlaceholderText="Scrivi un messaggio a Claude…" BorderThickness="0" Background="Transparent"
AutomationProperties.Name="Messaggio" />
<Grid ColumnDefinitions="Auto,Auto,Auto,Auto,*,Auto">
<TextBlock Text="Modello" FontSize="13" Foreground="{DynamicResource FgMuted}" VerticalAlignment="Center" Margin="0,0,8,0" />
<ComboBox Grid.Column="1" ItemsSource="{Binding ModelChoices}" SelectedItem="{Binding SelectedModel}" MinWidth="150"
<!-- Impostazioni: modello ed effort -->
<Button Grid.Row="0" Classes="ghost" HorizontalAlignment="Right" VerticalAlignment="Top" Margin="12" Padding="0" Width="36"
HorizontalContentAlignment="Center" AutomationProperties.Name="Impostazioni" ToolTip.Tip="Impostazioni">
<Button.Flyout>
<Flyout Placement="BottomEdgeAlignedRight">
<StackPanel Width="300" Spacing="6" Margin="4">
<TextBlock Text="Impostazioni" FontWeight="SemiBold" Margin="0,0,0,6" />
<TextBlock Text="Modello" FontSize="13" Foreground="{DynamicResource FgMuted}" />
<ComboBox ItemsSource="{Binding ModelChoices}" SelectedItem="{Binding SelectedModel}" HorizontalAlignment="Stretch"
AutomationProperties.Name="Modello">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="core:ModelOption"><TextBlock Text="{Binding Label}" /></DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<TextBox Grid.Column="2" Text="{Binding CustomModel}" IsVisible="{Binding IsCustomModel}" PlaceholderText="es. claude-opus-5-5" Width="190"
Margin="8,0,0,0" AutomationProperties.Name="ID modello personalizzato" />
<TextBlock Grid.Column="3" Text="Effort" FontSize="13" Foreground="{DynamicResource FgMuted}" VerticalAlignment="Center" Margin="16,0,8,0" />
<ComboBox Grid.Column="3" ItemsSource="{Binding EffortChoices}" SelectedItem="{Binding SelectedEffort}" MinWidth="130"
Margin="64,0,0,0" AutomationProperties.Name="Effort di ragionamento" />
<Button Grid.Column="5" Classes="send" Command="{Binding SendCommand}" AutomationProperties.Name="Invia messaggio">
<Viewbox Width="18" Height="18"><Path Data="M12 19V5M5 12L12 5L19 12" Stroke="{Binding $parent[Button].Foreground}" StrokeThickness="2.2" StrokeLineCap="Round" StrokeJoin="Round" /></Viewbox>
</Button>
</Grid>
</StackPanel>
<TextBox Text="{Binding CustomModel}" IsVisible="{Binding IsCustomModel}" PlaceholderText="es. claude-opus-5-5"
AutomationProperties.Name="ID modello personalizzato" />
<TextBlock Text="Effort" FontSize="13" Foreground="{DynamicResource FgMuted}" Margin="0,8,0,0" />
<ComboBox ItemsSource="{Binding EffortChoices}" SelectedItem="{Binding SelectedEffort}" HorizontalAlignment="Stretch"
AutomationProperties.Name="Effort di ragionamento" />
</StackPanel>
</Flyout>
</Button.Flyout>
<Viewbox Width="16" Height="16">
<Path Data="M4 7H11M17 7H20M4 17H7M13 17H20 M11 7A3 3 0 1 0 17 7A3 3 0 1 0 11 7Z M7 17A3 3 0 1 0 13 17A3 3 0 1 0 7 17Z"
Stroke="{DynamicResource Fg}" StrokeThickness="2" StrokeLineCap="Round" />
</Viewbox>
</Button>
<!-- Composer -->
<StackPanel Grid.Row="1" MaxWidth="780" Margin="24,0,24,12">
<Border Classes="composer" Background="{DynamicResource Surface}" BorderBrush="{DynamicResource Border}" BorderThickness="1" CornerRadius="20" Padding="14,10">
<Grid ColumnDefinitions="*,Auto">
<TextBox x:Name="InputBox" Classes="bare" Text="{Binding Input, Mode=TwoWay}" AcceptsReturn="True" TextWrapping="Wrap" MaxHeight="200"
PlaceholderText="Scrivi un messaggio a Claude…" BorderThickness="0" Background="Transparent"
VerticalContentAlignment="Center" AutomationProperties.Name="Messaggio" />
<Button Grid.Column="1" Classes="send" VerticalAlignment="Bottom" Margin="8,0,0,0" Command="{Binding SendCommand}" AutomationProperties.Name="Invia messaggio">
<Viewbox Width="18" Height="18"><Path Data="M12 19V5M5 12L12 5L19 12" Stroke="{Binding $parent[Button].Foreground}" StrokeThickness="2.2" StrokeLineCap="Round" StrokeJoin="Round" /></Viewbox>
</Button>
</Grid>
</Border>
<TextBlock Text="Invio per inviare · Maiusc+Invio per andare a capo" FontSize="12" Foreground="{DynamicResource FgMuted}"
HorizontalAlignment="Center" Margin="0,8,0,0" />
+3 -3
View File
@@ -22,9 +22,9 @@ 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.
- **Schermata iniziale** (chat vuota): titolo, sottotitolo e quattro suggerimenti cliccabili che precompilano il campo.
- **Composer** (in basso, scheda arrotondata): campo di testo che cresce fino a ~8 righe, selettori **Modello** (Predefinito, Fable 5.1, Opus 5.5, Sonnet 5.5, Haiku 5.5, "Altro ID…" con campo libero) ed **Effort** (Predefinito, low, medium, high, xhigh, max), pulsante Invia circolare con icona. `Invio` invia, `Maiusc+Invio` va a capo. Hint sulla tastiera sotto la scheda.
- Passati come `--model` / `--effort`; con "Predefinito" il flag viene omesso. Solo GUI.
- **Schermata iniziale** (chat vuota): solo logo, titolo e sottotitolo (nessun suggerimento precompilato).
- **Composer** (in basso, scheda arrotondata): campo di testo che cresce fino a ~8 righe e pulsante Invia circolare con icona. `Invio` invia, `Maiusc+Invio` va a capo. Hint sulla tastiera sotto la scheda. Non contiene più i selettori di modello ed effort.
- **Impostazioni** (in alto a destra, pulsante con icona a cursori, simmetrico a quello della barra laterale): aprono un riquadro a comparsa con **Modello** (Predefinito, Fable 5.1, Opus 5.5, Sonnet 5.5, Haiku 5.5, "Altro ID…" con campo libero) ed **Effort** (Predefinito, low, medium, high, xhigh, max). Si chiude con `Esc` o con un clic fuori; le scelte restano valide per i messaggi successivi (non sono ancora salvate su disco). Passati come `--model` / `--effort`; con "Predefinito" il flag viene omesso. Il riquadro è il punto in cui in futuro compariranno le impostazioni del motore ([ADR-0002](adr/0002-motori-cli-claude-codex-antigravity.md)). Solo GUI.
- **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. 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.
+5 -3
View File
@@ -97,7 +97,6 @@ Fonte dei requisiti: [MVP.md](MVP.md) e gli ADR. Livello: vedi sopra. Il nome de
| VM-12 | Errore di I/O nel salvataggio non interrompe l'invio né cancella i messaggi | U | `Salvataggio_fallito_non_perde_chat` | [ ] |
| VM-13 | Apertura chat: carica messaggi, `started` e id; chat inesistente ⇒ elenco ricaricato | U | `Apri_chat` | [ ] |
| VM-14 | Eliminazione: con conferma rimuove; senza conferma non fa nulla; se è la chat aperta parte una nuova | U | `Elimina_con_e_senza_conferma` | [ ] |
| VM-15 | Suggerimento: precompila il campo e chiede il focus | U | `Suggerimento_precompila` | [ ] |
| VM-16 | Barra laterale: il comando commuta `SidebarOpen` | U | `Barra_laterale` | [ ] |
| VM-17 | Copia: il comando passa il testo alla funzione degli appunti | U | `Copia_negli_appunti` | [ ] |
| VM-18 | Notifiche di proprietà: `HasMessages`, `ShowEmpty`, `CanSend`, `IsCustomModel` cambiano quando devono | U | `Notifiche_proprieta` | [ ] |
@@ -128,7 +127,7 @@ Fonte dei requisiti: [MVP.md](MVP.md) e gli ADR. Livello: vedi sopra. Il nome de
### Finestra e interazione — `Gui/MainWindow*`, `Gui/App.axaml`, `Gui/ConfirmDialog.cs`
| ID | Requisito | Liv. | Test | |
|---|---|---|---|---|
| UI-01 | La finestra si apre e mostra la schermata iniziale con quattro suggerimenti | G | `Apertura_schermata_iniziale` | [ ] |
| UI-01 | La finestra si apre e mostra la schermata iniziale (logo, titolo, sottotitolo, nessun suggerimento) | G | `Apertura_schermata_iniziale` | [ ] |
| UI-02 | Proporzioni 16:10, 75% dell'area utile (max 1440), minimo 900x600; senza schermo primario 1280x800 | G | `Dimensioni_finestra` | [ ] |
| UI-03 | `Invio` invia, `Maiusc+Invio` va a capo, nessun invio a campo vuoto | G | `Tasti_invio` | [ ] |
| UI-04 | Il pulsante Invia è disabilitato con campo vuoto o in attesa | G | `Pulsante_invia` | [ ] |
@@ -136,12 +135,15 @@ Fonte dei requisiti: [MVP.md](MVP.md) e gli ADR. Livello: vedi sopra. Il nome de
| UI-06 | La conversazione scorre in fondo a ogni nuovo messaggio | G | `Scorrimento_in_fondo` | [ ] |
| UI-07 | Indicatore di attesa visibile solo durante l'elaborazione | G | `Indicatore_attesa` | [ ] |
| UI-08 | Elenco chat: voce attiva evidenziata, clic apre, ✕ elimina; "Nessuna chat salvata" se vuoto | G | `Elenco_chat` | [ ] |
| UI-09 | Il campo "Altro ID…" compare solo con quella voce del modello | G | `Campo_modello_personalizzato` | [ ] |
| UI-09 | Nel riquadro Impostazioni il campo "Altro ID…" compare solo con quella voce del modello | G | `Campo_modello_personalizzato` | [ ] |
| UI-10 | Il pulsante a tre linee mostra/nasconde la barra laterale | G | `Mostra_nascondi_barra` | [ ] |
| UI-11 | Dialogo di conferma: restituisce vero con Elimina, falso con Annulla o chiusura | G | `Dialogo_conferma` | [ ] |
| UI-12 | Accessibilità: ogni pulsante a sola icona e il campo messaggio hanno `AutomationProperties.Name` | G | `Nomi_accessibili` | [ ] |
| UI-13 | Tema: sia il chiaro sia lo scuro risolvono tutti i token usati (nessuna risorsa mancante) | G | `Token_tema_completi` | [ ] |
| UI-14 | Focus sul campo messaggio all'apertura, dopo la risposta e dopo "Nuova chat" | G | `Focus_campo` | [ ] |
| UI-16 | Il pulsante Impostazioni (in alto a destra) apre il riquadro con Modello ed Effort; `Esc` lo chiude | G | `Riquadro_impostazioni` | [ ] |
| UI-17 | Il composer non contiene selettori di modello ed effort | G | `Composer_senza_selettori` | [ ] |
| UI-18 | Le scelte fatte nel riquadro arrivano ai flag `--model`/`--effort` del messaggio successivo | G/U | `Impostazioni_applicate_all_invio` | [ ] |
| UI-15 | Il nome dell'app compare nel titolo della finestra e nella barra laterale | G | `Nome_app_visibile` | [ ] |
### Eseguibile pubblicato