GUI a chat con rendering markdown, sessioni e correzione del thread STA
Sostituisce la GUI a singola richiesta con una chat in stile claude.ai: conversazione in markdown (Markdig + WebView2) in alto, input in basso, contesto mantenuto con --session-id / --resume. La GUI ora gira su un thread STA, necessario a WebView2. Documentazione aggiornata. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e4f75f5aa4
commit
5a18b9c23d
7 files changed
+190
-85
No files matched your search
@@ -20,17 +20,18 @@ LlmRun: eseguibile C# (.NET 10, Windows) che invoca Claude Code (`claude -p`) da
|
|||||||
|
|
||||||
## Architettura
|
## Architettura
|
||||||
|
|
||||||
Tutto in [Program.cs](Program.cs): top-level statements per la CLI e la classe `Gui` (Form) per la GUI. Entrambe costruiscono un `ProcessStartInfo` per `claude -p <prompt>` con `ArgumentList` (niente quoting manuale). Nessuna dipendenza esterna.
|
- [Program.cs](Program.cs): top-level statements. Senza argomenti avvia `Gui`; con argomenti è la CLI (`claude -p <prompt>` con `ArgumentList`, stdout/stderr ereditati, exit code propagato, 127 se `claude` non è nel PATH).
|
||||||
|
- [Gui.cs](Gui.cs): chat WinForms. WebView2 (conversazione, in alto) + pannello in basso (menu modello/effort, "Nuova chat", input, Invia). Ogni messaggio lancia `claude -p` con stdout/stderr rediretti; la risposta è convertita da markdown a HTML con Markdig (`DisableHtml`) e la pagina viene rigenerata per intero con `NavigateToString`. Contesto tra messaggi: `--session-id <guid>` al primo, `--resume <guid>` poi. `--model`/`--effort` omessi se "predefinito". Lista modelli hardcoded in `Gui.Models`, da aggiornare quando escono nuovi modelli.
|
||||||
- CLI: stdout/stderr ereditati (streaming), exit code propagato, 127 se `claude` non è nel PATH.
|
- Dipendenze NuGet (solo GUI): Markdig, Microsoft.Web.WebView2.
|
||||||
- GUI: output rediretto e mostrato riga per riga. Barra impostazioni con ComboBox modello (editabile, voci "Nome versione (id)"; lista hardcoded in `Gui.Models`, da aggiornare quando escono nuovi modelli) ed effort → `--model` / `--effort`, omessi se "predefinito".
|
|
||||||
|
|
||||||
## Vincoli non ovvi
|
## 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).
|
- La working directory di `claude` deve essere la cartella dell'exe: usare `AppContext.BaseDirectory`, mai `Environment.CurrentDirectory` né `Assembly.Location` (vuoto in single-file).
|
||||||
- Lo stdin di `claude` va redirezionato e chiuso subito, altrimenti aspetta 3s.
|
- Lo stdin di `claude` va redirezionato e chiuso subito, altrimenti aspetta 3s.
|
||||||
- `claude` è un .exe nativo (`~/.local/bin`), avviabile direttamente. Se fosse `claude.cmd` serve `cmd.exe /c`.
|
- `claude` è un .exe nativo (`~/.local/bin`), avviabile direttamente. Se fosse `claude.cmd` serve `cmd.exe /c`.
|
||||||
- La logica di avvio processo è duplicata tra CLI e GUI (volutamente, per restare in un file solo): se cambia, modificarla in entrambi i punti.
|
- La logica di avvio processo è duplicata tra CLI (`Program.cs`) e GUI (`Gui.cs`): se cambia (cartella di lavoro, stdin, flag), modificarla in entrambi i punti.
|
||||||
|
- La GUI non mostra link né HTML della risposta nella finestra: i link `http(s)` vanno aperti nel browser di sistema.
|
||||||
|
- La GUI deve girare su un thread STA (creato a mano in `Program.cs`, perché i top-level statements non hanno `[STAThread]`): senza, WebView2 fallisce con `RPC_E_CHANGED_MODE`.
|
||||||
|
|
||||||
## Skill del repo
|
## Skill del repo
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,154 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using System.Net;
|
||||||
|
using System.Windows.Forms;
|
||||||
|
using Markdig;
|
||||||
|
using Microsoft.Web.WebView2.WinForms;
|
||||||
|
|
||||||
|
class Gui : Form
|
||||||
|
{
|
||||||
|
record Model(string Label, string Id) { public override string ToString() => Label; }
|
||||||
|
static readonly Model[] Models =
|
||||||
|
[
|
||||||
|
new("Fable 5.1 (claude-fable-5-1)", "claude-fable-5-1"),
|
||||||
|
new("Opus 5.5 (claude-opus-5-5)", "claude-opus-5-5"),
|
||||||
|
new("Sonnet 5.5 (claude-sonnet-5-5)", "claude-sonnet-5-5"),
|
||||||
|
new("Haiku 5.5 (claude-haiku-5-5)", "claude-haiku-5-5"),
|
||||||
|
];
|
||||||
|
const string Default = "predefinito";
|
||||||
|
|
||||||
|
static readonly MarkdownPipeline Md = new MarkdownPipelineBuilder().UseAdvancedExtensions().DisableHtml().Build();
|
||||||
|
|
||||||
|
const string Css = """
|
||||||
|
:root { color-scheme: light dark; }
|
||||||
|
body { font: 15px/1.6 'Segoe UI', sans-serif; margin: 0; padding: 16px 0 32px; background: Canvas; color: CanvasText; }
|
||||||
|
.msg { max-width: 800px; margin: 0 auto 18px; padding: 0 16px; }
|
||||||
|
.user { text-align: right; }
|
||||||
|
.user div { display: inline-block; text-align: left; white-space: pre-wrap; background: color-mix(in srgb, CanvasText 10%, Canvas); padding: 10px 14px; border-radius: 18px; max-width: 85%; }
|
||||||
|
.err div { color: #c0392b; white-space: pre-wrap; }
|
||||||
|
.wait { opacity: .6; font-style: italic; }
|
||||||
|
pre { background: color-mix(in srgb, CanvasText 8%, Canvas); padding: 12px; border-radius: 8px; overflow-x: auto; }
|
||||||
|
code { font-family: Consolas, monospace; font-size: 13.5px; }
|
||||||
|
table { border-collapse: collapse; } th, td { border: 1px solid color-mix(in srgb, CanvasText 25%, Canvas); padding: 4px 10px; }
|
||||||
|
blockquote { margin-left: 0; padding-left: 12px; border-left: 3px solid color-mix(in srgb, CanvasText 30%, Canvas); }
|
||||||
|
""";
|
||||||
|
|
||||||
|
// (ruolo, testo): "user" | "ai" | "err" | "wait"
|
||||||
|
readonly List<(string Role, string Text)> chat = [];
|
||||||
|
readonly WebView2 view = new() { Dock = DockStyle.Fill };
|
||||||
|
readonly TextBox input = new() { Dock = DockStyle.Fill, Multiline = true, AcceptsReturn = true, ScrollBars = ScrollBars.Vertical, Font = new Font("Segoe UI", 11) };
|
||||||
|
readonly Button send = new() { Text = "Invia", Dock = DockStyle.Fill };
|
||||||
|
readonly Button reset = new() { Text = "Nuova chat", AutoSize = true };
|
||||||
|
readonly ComboBox model = new() { DropDownStyle = ComboBoxStyle.DropDown, Width = 300 }; // editabile: ID completo
|
||||||
|
readonly ComboBox effort = new() { DropDownStyle = ComboBoxStyle.DropDownList, Width = 100 };
|
||||||
|
|
||||||
|
string sessionId = Guid.NewGuid().ToString();
|
||||||
|
bool started; // true dopo il primo messaggio riuscito: da lì in poi --resume
|
||||||
|
|
||||||
|
public Gui()
|
||||||
|
{
|
||||||
|
Text = "LlmRun";
|
||||||
|
Width = 1000; Height = 700;
|
||||||
|
|
||||||
|
model.Items.Add(Default); model.Items.AddRange(Models); model.SelectedIndex = 0;
|
||||||
|
effort.Items.AddRange([Default, "low", "medium", "high", "xhigh", "max"]); effort.SelectedIndex = 0;
|
||||||
|
|
||||||
|
var settings = new FlowLayoutPanel { Dock = DockStyle.Fill, WrapContents = false };
|
||||||
|
settings.Controls.AddRange([
|
||||||
|
new Label { Text = "Modello", AutoSize = true, Margin = new Padding(0, 8, 0, 0) }, model,
|
||||||
|
new Label { Text = "Effort", AutoSize = true, Margin = new Padding(8, 8, 0, 0) }, effort, reset]);
|
||||||
|
|
||||||
|
var bottom = new TableLayoutPanel { Dock = DockStyle.Bottom, Height = 140, Padding = new Padding(8), ColumnCount = 2, RowCount = 2 };
|
||||||
|
bottom.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
|
||||||
|
bottom.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 90));
|
||||||
|
bottom.RowStyles.Add(new RowStyle(SizeType.Absolute, 34));
|
||||||
|
bottom.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
|
||||||
|
bottom.Controls.Add(settings, 0, 0); bottom.SetColumnSpan(settings, 2);
|
||||||
|
bottom.Controls.Add(input, 0, 1);
|
||||||
|
bottom.Controls.Add(send, 1, 1);
|
||||||
|
|
||||||
|
Controls.Add(view); Controls.Add(bottom); // Fill per primo: l'ordine inverso dei Dock
|
||||||
|
|
||||||
|
send.Click += async (_, _) => await Send();
|
||||||
|
reset.Click += (_, _) => { chat.Clear(); sessionId = Guid.NewGuid().ToString(); started = false; Render(); };
|
||||||
|
input.KeyDown += async (_, e) =>
|
||||||
|
{
|
||||||
|
if (e.KeyCode == Keys.Enter && !e.Shift) { e.SuppressKeyPress = true; await Send(); }
|
||||||
|
};
|
||||||
|
Load += async (_, _) =>
|
||||||
|
{
|
||||||
|
var dir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "LlmRun");
|
||||||
|
await view.EnsureCoreWebView2Async(await Microsoft.Web.WebView2.Core.CoreWebView2Environment.CreateAsync(null, dir));
|
||||||
|
view.CoreWebView2.NavigationStarting += (_, e) =>
|
||||||
|
{
|
||||||
|
if (e.Uri.StartsWith("http", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
e.Cancel = true;
|
||||||
|
Process.Start(new ProcessStartInfo(e.Uri) { UseShellExecute = true });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
view.CoreWebView2.Settings.AreDefaultContextMenusEnabled = true;
|
||||||
|
Render();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
void Render()
|
||||||
|
{
|
||||||
|
if (view.CoreWebView2 == null) return;
|
||||||
|
var body = string.Concat(chat.Select(m => m.Role switch
|
||||||
|
{
|
||||||
|
"user" => $"<div class='msg user'><div>{WebUtility.HtmlEncode(m.Text)}</div></div>",
|
||||||
|
"ai" => $"<div class='msg'>{Markdown.ToHtml(m.Text, Md)}</div>",
|
||||||
|
"err" => $"<div class='msg err'><div>{WebUtility.HtmlEncode(m.Text)}</div></div>",
|
||||||
|
_ => $"<div class='msg wait'>{WebUtility.HtmlEncode(m.Text)}</div>",
|
||||||
|
}));
|
||||||
|
view.NavigateToString($"<!doctype html><meta charset='utf-8'><style>{Css}</style><body>{body}<script>scrollTo(0,document.body.scrollHeight)</script></body>");
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task Send()
|
||||||
|
{
|
||||||
|
var text = input.Text.Trim();
|
||||||
|
if (text == "" || !send.Enabled) return;
|
||||||
|
send.Enabled = false;
|
||||||
|
input.Clear();
|
||||||
|
chat.Add(("user", text));
|
||||||
|
chat.Add(("wait", "Claude sta scrivendo…"));
|
||||||
|
Render();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var (code, stdout, stderr) = await RunClaude(text);
|
||||||
|
chat.RemoveAt(chat.Count - 1);
|
||||||
|
if (code == 0) { started = true; chat.Add(("ai", stdout)); }
|
||||||
|
else chat.Add(("err", $"claude è uscito con codice {code}\n{stderr}".Trim()));
|
||||||
|
}
|
||||||
|
catch (System.ComponentModel.Win32Exception)
|
||||||
|
{
|
||||||
|
chat.RemoveAt(chat.Count - 1);
|
||||||
|
chat.Add(("err", "'claude' non trovato nel PATH."));
|
||||||
|
}
|
||||||
|
finally { send.Enabled = true; Render(); input.Focus(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task<(int Code, string Out, string Err)> RunClaude(string prompt)
|
||||||
|
{
|
||||||
|
var psi = new ProcessStartInfo("claude")
|
||||||
|
{
|
||||||
|
WorkingDirectory = AppContext.BaseDirectory,
|
||||||
|
UseShellExecute = false,
|
||||||
|
RedirectStandardInput = true, RedirectStandardOutput = true, RedirectStandardError = true,
|
||||||
|
CreateNoWindow = true,
|
||||||
|
};
|
||||||
|
psi.ArgumentList.Add("-p");
|
||||||
|
psi.ArgumentList.Add(prompt);
|
||||||
|
psi.ArgumentList.Add(started ? "--resume" : "--session-id");
|
||||||
|
psi.ArgumentList.Add(sessionId);
|
||||||
|
foreach (var (flag, value) in new[] { ("--model", model.SelectedItem is Model m ? m.Id : model.Text.Trim()), ("--effort", effort.Text) })
|
||||||
|
if (value != "" && value != Default) { psi.ArgumentList.Add(flag); psi.ArgumentList.Add(value); }
|
||||||
|
|
||||||
|
using var p = Process.Start(psi)!;
|
||||||
|
p.StandardInput.Close(); // evita l'attesa di stdin di claude
|
||||||
|
var o = p.StandardOutput.ReadToEndAsync();
|
||||||
|
var e = p.StandardError.ReadToEndAsync();
|
||||||
|
await p.WaitForExitAsync();
|
||||||
|
return (p.ExitCode, (await o).Trim(), (await e).Trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,4 +8,9 @@
|
|||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Markdig" Version="1.4.0" />
|
||||||
|
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.4258.31" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
+9
-71
@@ -3,8 +3,15 @@ using System.Windows.Forms;
|
|||||||
|
|
||||||
if (args.Length == 0)
|
if (args.Length == 0)
|
||||||
{
|
{
|
||||||
ApplicationConfiguration.Initialize();
|
// WebView2 richiede un thread STA, che i top-level statements non danno
|
||||||
Application.Run(new Gui());
|
var ui = new Thread(() =>
|
||||||
|
{
|
||||||
|
ApplicationConfiguration.Initialize();
|
||||||
|
Application.Run(new Gui());
|
||||||
|
});
|
||||||
|
ui.SetApartmentState(ApartmentState.STA);
|
||||||
|
ui.Start();
|
||||||
|
ui.Join();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,72 +41,3 @@ static ProcessStartInfo NewClaude(string prompt)
|
|||||||
psi.ArgumentList.Add(prompt);
|
psi.ArgumentList.Add(prompt);
|
||||||
return psi;
|
return psi;
|
||||||
}
|
}
|
||||||
|
|
||||||
class Gui : Form
|
|
||||||
{
|
|
||||||
readonly TextBox prompt = new() { Dock = DockStyle.Top, Multiline = true, Height = 120, AcceptsReturn = true };
|
|
||||||
readonly Button send = new() { Dock = DockStyle.Top, Text = "Invia a Claude", Height = 32 };
|
|
||||||
readonly TextBox output = new() { Dock = DockStyle.Fill, Multiline = true, ReadOnly = true, ScrollBars = ScrollBars.Vertical };
|
|
||||||
|
|
||||||
record Model(string Label, string Id) { public override string ToString() => Label; }
|
|
||||||
static readonly Model[] Models =
|
|
||||||
[
|
|
||||||
new("Fable 5.1 (claude-fable-5-1)", "claude-fable-5-1"),
|
|
||||||
new("Opus 5.5 (claude-opus-5-5)", "claude-opus-5-5"),
|
|
||||||
new("Sonnet 5.5 (claude-sonnet-5-5)", "claude-sonnet-5-5"),
|
|
||||||
new("Haiku 5.5 (claude-haiku-5-5)", "claude-haiku-5-5"),
|
|
||||||
];
|
|
||||||
const string Default = "predefinito";
|
|
||||||
readonly ComboBox model = new() { DropDownStyle = ComboBoxStyle.DropDown, Width = 300 }; // editabile: ID completo
|
|
||||||
readonly ComboBox effort = new() { DropDownStyle = ComboBoxStyle.DropDownList, Width = 100 };
|
|
||||||
|
|
||||||
public Gui()
|
|
||||||
{
|
|
||||||
Text = "LlmRun";
|
|
||||||
Width = 1000; Height = 700;
|
|
||||||
model.Items.Add(Default); model.Items.AddRange(Models); model.SelectedIndex = 0;
|
|
||||||
effort.Items.AddRange([Default, "low", "medium", "high", "xhigh", "max"]); effort.SelectedIndex = 0;
|
|
||||||
var settings = new FlowLayoutPanel { Dock = DockStyle.Top, Height = 32, Padding = new Padding(4) };
|
|
||||||
settings.Controls.AddRange([new Label { Text = "Modello", AutoSize = true, Margin = new Padding(0, 6, 0, 0) }, model,
|
|
||||||
new Label { Text = "Effort", AutoSize = true, Margin = new Padding(8, 6, 0, 0) }, effort]);
|
|
||||||
Controls.Add(output); Controls.Add(send); Controls.Add(prompt); Controls.Add(settings); // Fill per primo: l'ordine inverso dei Dock
|
|
||||||
send.Click += async (_, _) => await Run();
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task Run()
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(prompt.Text)) return;
|
|
||||||
send.Enabled = false;
|
|
||||||
output.Clear();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var psi = NewClaude(prompt.Text);
|
|
||||||
using var p = Process.Start(psi)!;
|
|
||||||
p.StandardInput.Close();
|
|
||||||
void Append(string? l) { if (l != null) BeginInvoke(() => output.AppendText(l + Environment.NewLine)); }
|
|
||||||
p.OutputDataReceived += (_, e) => Append(e.Data);
|
|
||||||
p.ErrorDataReceived += (_, e) => Append(e.Data);
|
|
||||||
p.BeginOutputReadLine(); p.BeginErrorReadLine();
|
|
||||||
await p.WaitForExitAsync();
|
|
||||||
output.AppendText($"[exit {p.ExitCode}]");
|
|
||||||
}
|
|
||||||
catch (System.ComponentModel.Win32Exception) { output.Text = "'claude' non trovato nel PATH."; }
|
|
||||||
finally { send.Enabled = true; }
|
|
||||||
}
|
|
||||||
|
|
||||||
ProcessStartInfo NewClaude(string prompt)
|
|
||||||
{
|
|
||||||
var psi = new ProcessStartInfo("claude")
|
|
||||||
{
|
|
||||||
WorkingDirectory = AppContext.BaseDirectory,
|
|
||||||
UseShellExecute = false,
|
|
||||||
RedirectStandardInput = true, RedirectStandardOutput = true, RedirectStandardError = true,
|
|
||||||
CreateNoWindow = true,
|
|
||||||
};
|
|
||||||
psi.ArgumentList.Add("-p");
|
|
||||||
psi.ArgumentList.Add(prompt);
|
|
||||||
foreach (var (flag, value) in new[] { ("--model", model.SelectedItem is Model m ? m.Id : model.Text.Trim()), ("--effort", effort.Text) })
|
|
||||||
if (value != "" && value != Default) { psi.ArgumentList.Add(flag); psi.ArgumentList.Add(value); }
|
|
||||||
return psi;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,10 +5,11 @@ Eseguibile C# (.NET 10, Windows) che lancia Claude Code (`claude -p`) nella cart
|
|||||||
## Requisiti
|
## Requisiti
|
||||||
- .NET SDK 10
|
- .NET SDK 10
|
||||||
- `claude` (Claude Code) nel PATH
|
- `claude` (Claude Code) nel PATH
|
||||||
|
- Runtime WebView2 (incluso in Windows 11) per la GUI
|
||||||
|
|
||||||
## Uso
|
## Uso
|
||||||
|
|
||||||
GUI (nessun argomento): si apre una finestra con campo prompt, pulsante e output.
|
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).
|
||||||
```
|
```
|
||||||
dotnet run
|
dotnet run
|
||||||
```
|
```
|
||||||
|
|||||||
+11
-7
@@ -16,20 +16,24 @@ Tutti gli argomenti sono uniti in un unico prompt.
|
|||||||
4. Esce con lo stesso exit code di `claude`.
|
4. Esce con lo stesso exit code di `claude`.
|
||||||
5. Se `claude` non è trovato: messaggio chiaro ed exit code 127.
|
5. Se `claude` non è trovato: messaggio chiaro ed exit code 127.
|
||||||
|
|
||||||
## GUI
|
## GUI (chat)
|
||||||
Senza argomenti si apre una finestra WinForms (prompt, pulsante, output) che usa lo stesso lancio di `claude -p`. Con argomenti resta la CLI.
|
Senza argomenti si apre una finestra WinForms in stile claude.ai/ChatGPT. Con argomenti resta la CLI.
|
||||||
La GUI ha una barra impostazioni con due menu: **Modello** (predefinito, Fable 5.1, Opus 5.5, Sonnet 5.5, Haiku 5.5: ogni voce mostra versione e ID completo, che è ciò che viene passato a `claude`; oppure ID digitato a mano) e **Effort** (predefinito, low, medium, high, xhigh, max). Passati a `claude` come `--model` / `--effort`; con "predefinito" il flag viene omesso. Solo GUI: la CLI non li espone ancora.
|
- **In alto**: la conversazione, a messaggi alternati utente/Claude. Le risposte di Claude sono renderizzate in markdown (titoli, liste, tabelle, blocchi di codice) con Markdig dentro un WebView2; i messaggi utente sono testo semplice. Il rendering segue il tema chiaro/scuro di Windows.
|
||||||
|
- **In basso**: campo di input multilinea e pulsante Invia. `Invio` invia, `Maiusc+Invio` va a capo.
|
||||||
La finestra si apre a 1000x700 e output e prompt si ridimensionano con essa.
|
- **Sopra l'input**: menu **Modello** (predefinito, Fable 5.1, Opus 5.5, Sonnet 5.5, Haiku 5.5: ogni voce mostra versione e ID completo, che è ciò che viene passato a `claude`; oppure ID digitato a mano), menu **Effort** (predefinito, low, medium, high, xhigh, max) e pulsante **Nuova chat**. Passati come `--model` / `--effort`; con "predefinito" il flag viene omesso. Solo GUI.
|
||||||
|
- **Contesto**: la conversazione prosegue con `claude -p --session-id <guid>` al primo messaggio e `--resume <guid>` ai successivi. "Nuova chat" svuota la vista e genera un nuovo guid.
|
||||||
|
- La risposta compare a fine generazione (nessuno streaming nella GUI); durante l'attesa si vede "Claude sta scrivendo…". Se `claude` esce con errore, lo stderr appare come messaggio di errore.
|
||||||
|
- I link nelle risposte si aprono nel browser di sistema, non nella finestra. L'HTML grezzo nel markdown è disabilitato.
|
||||||
|
- Finestra 1000x700.
|
||||||
|
|
||||||
## Scelte tecniche
|
## Scelte tecniche
|
||||||
- .NET 8+, un solo `Program.cs`, nessuna dipendenza esterna.
|
- .NET 10 (WinForms). `Program.cs` (CLI + avvio) e `Gui.cs` (chat). Dipendenze NuGet solo per la GUI: `Markdig` (markdown→HTML) e `Microsoft.Web.WebView2` (richiede il runtime WebView2, già presente su Windows 11).
|
||||||
- `ProcessStartInfo.ArgumentList` per passare il prompt (niente problemi di quoting).
|
- `ProcessStartInfo.ArgumentList` per passare il prompt (niente problemi di quoting).
|
||||||
- `claude` è un .exe nativo (`~/.local/bin`): si avvia direttamente, senza `cmd /c`. Lo stdin è chiuso subito per evitare l'attesa di 3s di claude.
|
- `claude` è un .exe 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.
|
- Working dir: `AppContext.BaseDirectory`, non la cwd del chiamante.
|
||||||
|
|
||||||
## Fuori scope (MVP)
|
## Fuori scope (MVP)
|
||||||
Modalità interattiva (chat), sessioni/`--resume`, config file, logging su file, test automatici, supporto Linux/macOS.
|
Streaming token-per-token nella GUI, annullamento di una risposta in corso, salvataggio/riapertura delle chat, config file, logging su file, test automatici, supporto Linux/macOS.
|
||||||
|
|
||||||
## Criteri di accettazione
|
## Criteri di accettazione
|
||||||
- [ ] Eseguito da un'altra cartella, `claude` parte nella cartella dell'exe (verificabile chiedendo "pwd").
|
- [ ] Eseguito da un'altra cartella, `claude` parte nella cartella dell'exe (verificabile chiedendo "pwd").
|
||||||
|
|||||||
+3
-1
@@ -11,6 +11,8 @@ Dettagli dell'MVP in [MVP.md](MVP.md).
|
|||||||
- [x] Lancio di `claude -p` nella cartella dell'exe, streaming, exit code
|
- [x] Lancio di `claude -p` nella cartella dell'exe, streaming, exit code
|
||||||
- [ ] Publish single-file win-x64 e verifica dei criteri di accettazione
|
- [ ] Publish single-file win-x64 e verifica dei criteri di accettazione
|
||||||
|
|
||||||
|
- [x] GUI a chat: conversazione markdown in alto, input in basso, sessione con `--session-id`/`--resume`
|
||||||
|
|
||||||
## Fase 2 — Robustezza
|
## Fase 2 — Robustezza
|
||||||
- [ ] Input del prompt da stdin e da file
|
- [ ] Input del prompt da stdin e da file
|
||||||
- [ ] Timeout e cancellazione (Ctrl+C propagato al figlio)
|
- [ ] Timeout e cancellazione (Ctrl+C propagato al figlio)
|
||||||
@@ -18,6 +20,6 @@ Dettagli dell'MVP in [MVP.md](MVP.md).
|
|||||||
- [ ] CLI: stesse opzioni; inoltre `--allowedTools`, `--output-format`
|
- [ ] CLI: stesse opzioni; inoltre `--allowedTools`, `--output-format`
|
||||||
|
|
||||||
## Fase 3 — Oltre
|
## Fase 3 — Oltre
|
||||||
- [ ] Modalità sessione (`--resume` / `--continue`)
|
- [ ] GUI: streaming della risposta (`--output-format stream-json`), stop, elenco chat salvate
|
||||||
- [ ] Config file accanto all'exe
|
- [ ] Config file accanto all'exe
|
||||||
- [ ] Test automatici e CI
|
- [ ] Test automatici e CI
|
||||||
Reference in new issue
Block a user