using System.Collections.ObjectModel; using System.Windows.Input; using AgentWikiUi.Core; namespace AgentWikiUi.Gui; class MessageVm(string role, string text, string author) { public string Role { get; } = role; public string Text { get; } = text; public string Author { get; } = author; public bool IsUser => Role == "user"; public bool IsAi => Role == "ai"; public bool IsErr => Role == "err"; } class ChatItem(string id, string title, DateTime updated, bool active, string engineName) { public string Id { get; } = id; public string Title { get; } = title; public string When { get; } = $"{engineName} · {updated:d MMM HH:mm}"; public bool IsActive { get; } = active; } /// Voce dell'interruttore CLI: un motore non installato è mostrato ma non selezionabile. class EngineChoice(Engine engine) { public Engine Engine { get; } = engine; public bool Installed { get; } = engine.IsInstalled(); public string Label => Installed ? Engine.Name : $"{Engine.Name} (non installato)"; } class MainViewModel : ObservableObject { static readonly ModelOption Default = new("Predefinito", ""); static readonly ModelOption Custom = new("Altro ID…", "__custom"); const string DefaultEffort = "Predefinito"; public ObservableCollection Messages { get; } = []; public ObservableCollection Chats { get; } = []; public EngineChoice[] EngineChoices { get; } = Engines.All.Select(e => new EngineChoice(e)).ToArray(); public ICommand NewChatCommand => newChat; public ICommand CopyCommand { get; } public ICommand SendCommand { get; } public ICommand OpenCommand { get; } public ICommand DeleteCommand { get; } public ICommand ToggleSidebarCommand { get; } /// Conferma di eliminazione, fornita dalla finestra. public Func> ConfirmDelete { get; set; } = () => Task.FromResult(true); /// Copia negli appunti, fornita dalla finestra. public Func CopyText { get; set; } = _ => Task.CompletedTask; /// Chiesto alla finestra di rimettere il fuoco sul campo messaggio. public event Action? FocusInput; // Una chat = un motore. chatId è il nome del file; sessionId è l'id del motore: // per Claude Code lo sceglie l'app (= chatId), per Codex arriva dall'output dopo il primo messaggio. string chatId = Guid.NewGuid().ToString(); string? sessionId; bool started; // true dopo il primo messaggio riuscito: da lì in poi si riprende la sessione List history = []; EngineChoice selectedEngine; ModelOption[] modelChoices = []; string[] effortChoices = []; string input = ""; bool busy, sidebarOpen = true; ModelOption selectedModel = Default; string customModel = ""; string selectedEffort = DefaultEffort; readonly RelayCommand send, newChat; public MainViewModel() { selectedEngine = EngineChoices.FirstOrDefault(c => c.Installed) ?? EngineChoices[0]; ApplyEngine(selectedEngine); send = new(_ => _ = SendAsync(), () => CanSend); SendCommand = send; newChat = new(_ => NewChat(), () => !busy); CopyCommand = new RelayCommand(t => { if (t is not null) _ = CopyText(t); }); OpenCommand = new RelayCommand(c => { if (c is not null) OpenChat(c.Id); }); DeleteCommand = new RelayCommand(c => { if (c is not null) _ = DeleteAsync(c); }); ToggleSidebarCommand = new RelayCommand(_ => SidebarOpen = !SidebarOpen); Messages.CollectionChanged += (_, _) => { Raise(nameof(HasMessages)); Raise(nameof(ShowEmpty)); }; ResetSession(); // all'avvio riapre la chat più recente if (ChatStore.List().FirstOrDefault() is { } last) OpenChat(last.Id); else RefreshChats(); } Engine Engine => selectedEngine.Engine; public bool HasMessages => Messages.Count > 0; public bool ShowEmpty => Messages.Count == 0; public bool CanSend => !busy && !string.IsNullOrWhiteSpace(input); public string Author => Engine.Author; public string InputPlaceholder => $"Scrivi un messaggio a {Engine.Author}…"; public string BusyText => $"{Engine.Author} sta scrivendo…"; public string Input { get => input; set { if (Set(ref input, value)) { Raise(nameof(CanSend)); send.Refresh(); } } } public bool IsBusy { get => busy; private set { if (Set(ref busy, value)) { Raise(nameof(CanSend)); Raise(nameof(CanSwitchEngine)); send.Refresh(); newChat.Refresh(); } } } public bool SidebarOpen { get => sidebarOpen; set => Set(ref sidebarOpen, value); } public bool CanSwitchEngine => !busy; /// Interruttore CLI: cambiare motore apre una nuova chat (il contesto non passa da un motore all'altro). public EngineChoice SelectedEngine { get => selectedEngine; set { if (value is null || value == selectedEngine || busy || !value.Installed) return; ApplyEngine(value); NewChat(); } } public ModelOption[] ModelChoices { get => modelChoices; private set => Set(ref modelChoices, value); } public string[] EffortChoices { get => effortChoices; private set => Set(ref effortChoices, value); } public ModelOption SelectedModel { get => selectedModel; set { if (Set(ref selectedModel, value)) Raise(nameof(IsCustomModel)); } } public bool IsCustomModel => selectedModel == Custom; public string CustomModel { get => customModel; set => Set(ref customModel, value); } public string SelectedEffort { get => selectedEffort; set => Set(ref selectedEffort, value); } // Adotta un motore: liste di modelli ed effort sue, scelte riportate a "Predefinito". void ApplyEngine(EngineChoice choice) { selectedEngine = choice; ModelChoices = [Default, .. choice.Engine.Models, Custom]; EffortChoices = [DefaultEffort, .. choice.Engine.Efforts]; SelectedModel = Default; CustomModel = ""; SelectedEffort = DefaultEffort; Raise(nameof(SelectedEngine)); Raise(nameof(Author)); Raise(nameof(InputPlaceholder)); Raise(nameof(BusyText)); } void ResetSession() { chatId = Guid.NewGuid().ToString(); sessionId = Engine.OwnsSessionId ? null : chatId; started = false; history = []; } void RefreshChats() { Chats.Clear(); foreach (var c in ChatStore.List()) Chats.Add(new(c.Id, c.Title, c.Updated, c.Id == chatId, Engines.Find(c.EngineId)?.Name ?? c.EngineId)); } void Show(List msgs, string author) { Messages.Clear(); foreach (var m in msgs) Messages.Add(new(m.Role, m.Text, author)); } void NewChat() { ResetSession(); Show(history, Author); RefreshChats(); FocusInput?.Invoke(); } void OpenChat(string id) { if (busy) return; if (ChatStore.Load(id) is not { } chat) { RefreshChats(); return; } var choice = EngineChoices.FirstOrDefault(c => c.Engine.Id == chat.EngineId); if (choice is not null && choice != selectedEngine) ApplyEngine(choice); chatId = chat.Id; sessionId = chat.EffectiveSessionId; started = chat.Started; history = chat.Messages; Show(history, Author); RefreshChats(); } async Task DeleteAsync(ChatItem c) { if (busy || !await ConfirmDelete()) return; ChatStore.Delete(c.Id); if (c.Id == chatId) NewChat(); else RefreshChats(); } async Task SendAsync() { if (!CanSend) return; var text = input.Trim(); var model = IsCustomModel ? customModel.Trim() : selectedModel.Id; var effort = selectedEffort == DefaultEffort ? "" : selectedEffort; Input = ""; IsBusy = true; Add("user", text); Persist(); // il messaggio non va perso se l'app si chiude durante l'attesa RefreshChats(); try { var r = await EngineRunner.RunCapturedAsync(Engine, text, sessionId, started, model, effort); if (r.Error is null) { started = true; if (r.SessionId is not null) sessionId = r.SessionId; Add("ai", r.Text); } else Add("err", r.Error); } catch (System.ComponentModel.Win32Exception) { Add("err", $"'{Engine.Executable}' non trovato nel PATH."); } catch (Exception ex) { Add("err", ex.Message); } finally { IsBusy = false; Persist(); RefreshChats(); FocusInput?.Invoke(); } } void Add(string role, string text) { history.Add(new(role, text)); Messages.Add(new(role, text, Author)); } void Persist() { try { ChatStore.Save(chatId, Engine.Id, sessionId, started, history); } catch (IOException) { } // la chat resta a schermo anche se il salvataggio fallisce } }