diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f42915..3931812 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,24 @@ recorded by the `MandoCode` submodule. ## [Unreleased] +### Added +- **First-run guided setup.** A fresh install now walks through everything in the chat itself: + reach Ollama (with a one-click winget install when it's missing), start the daemon, and pick a + starter model from a curated list — cloud recommended, or local tiers with size and hardware + hints. Setup stays discoverable afterward via `/setup` and a **Run guided setup** button in + Settings → Connection. Previously a fresh install landed on the raw Settings page. +- **Open-in-Explorer buttons in the file explorer.** Every row gains an open icon next to the `@` + tag: folders open in Windows File Explorer, files in their default app. (Double-click on + folders couldn't do this — it fights the expand/collapse toggle.) + +### Fixed +- **The app no longer freezes while the notes assistant streams a reply.** Fast models (small or + thinking models especially) could emit tokens quicker than the reply strip repainted, starving + the UI thread for the whole response. Streaming now runs off the UI thread and repaints are + batched on a 100 ms clock, so generation speed no longer affects app responsiveness. Closing + or switching notes also cancels the in-flight request instead of leaving it generating + invisibly. + Multiple agents, one window. Each tab is an independent agent with its own conversation, project folder, model, and settings — and the config file stops being "the current settings" and becomes "the defaults a new agent starts on." diff --git a/README.md b/README.md index aa6a301..7c75e0d 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,22 @@ repo is pinned here as a **git submodule** (`/MandoCode`) and this app project-r task planner, plugins, MCP, skills, config, approvals, token tracking). Only the user interface is different: WinUI 3 instead of RazorConsole. +## Install (no build needed) + +Download the latest `MandoCode.Desktop-*-win-x64.zip` from +[Releases](https://github.com/DevMando/MandoCode.Desktop/releases), extract it anywhere, and run +`MandoCode.Desktop.exe`. The zip is fully self-contained — **no .NET install required**. + +On first launch the app runs a **guided setup right in the chat**: it finds Ollama (offering to +install it via winget if it's missing), starts the daemon, and helps you pick a first model — a +cloud model (best quality, no GPU needed, free ollama.com sign-in) or a local one from a short +list with size and hardware hints. Re-run the wizard any time by typing `/setup` or with the +**Run guided setup** button in Settings. + +Requirements: Windows 10 (1809+) or Windows 11, with the WebView2 runtime — preinstalled on +Windows 11 and kept current by Edge on Windows 10. Models are served by +[Ollama](https://ollama.com); you don't need it installed beforehand, the wizard handles it. + ## Clone & build ``` @@ -62,7 +78,7 @@ for that changing. | Approvals | `DiffApprovalHandler` (Spectre panels) | `Services/WinUiApprovalService.cs` + each agent's own XAML overlay (same labels, bypass state, `DiffApprovalResult` contract) | | Transcript | ANSI scrollback + Spectre renderables | WebView2 + `TranscriptHtmlBuilder` (Markdig HTML, themed) | | Busy/spinner | `SpinnerService` (ANSI) | `BusyStateService` → ProgressRing | -| Onboarding | `OnboardingFlow` terminal prompts | `/setup` wizard + Settings page | +| Onboarding | `OnboardingFlow` terminal prompts | In-chat guided wizard (auto on first launch, `/setup` after) + Settings page | | Everything else | `Services/`, `Plugins/`, `Models/` | **reused verbatim via project reference** | Key seams the harness already provided (unchanged): `AIService.ChatStreamAsync`, @@ -330,8 +346,11 @@ within 24 hours. from a folder or zip, and an editor that can generate or refine a skill body with a model you pick (`SkillAuthor`); `SkillCoordinator` fans changes to every open agent - Guided wizards, built on the approval-overlay select + text primitives: - - `/setup` — probe/start Ollama, change endpoint, pull a starter model with live - progress, model picker, cloud-auth check + sign-in walkthrough + - First-run setup — fires automatically in the chat on a fresh install: probe/start + Ollama (offering a winget install when the CLI is missing), change endpoint, pick a + starter model from a curated list (cloud recommended, or local tiers with size and + hardware hints), pull it with live progress, cloud-auth check + sign-in walkthrough. + Re-run any time via `/setup` or Settings → Run guided setup - `/model`, `/force-skill`, `/music-playlist` — pickers - 401 auto-recovery — a cloud 401 offers the `ollama signin` walkthrough inline - Branded application icon across the exe, taskbar, and window title bar diff --git a/src/MandoCode.Desktop/Controls/ChatTabView.Explorer.cs b/src/MandoCode.Desktop/Controls/ChatTabView.Explorer.cs index fa21b85..77cf18d 100644 --- a/src/MandoCode.Desktop/Controls/ChatTabView.Explorer.cs +++ b/src/MandoCode.Desktop/Controls/ChatTabView.Explorer.cs @@ -763,7 +763,9 @@ private void ExplorerTree_ItemInvoked(TreeView sender, TreeViewItemInvokedEventA private void ExplorerTree_DoubleTapped(object sender, DoubleTappedRoutedEventArgs e) { - // The template's elements inherit the row's TreeViewNode as DataContext. + // The template's elements inherit the row's TreeViewNode as DataContext. Files only: + // for folders double-click fights the expand/collapse toggle — they open externally + // via the row's open button (ExplorerOpen_Click) instead. if ((e.OriginalSource as FrameworkElement)?.DataContext is not TreeViewNode node || node.Content is not ExplorerItem { IsDirectory: false } item) return; @@ -771,6 +773,17 @@ private void ExplorerTree_DoubleTapped(object sender, DoubleTappedRoutedEventArg _transcript.Append(_html.Warn($"Couldn't open file: {ex.Message}")); } + /// The row's open button: folders in Windows File Explorer, files in their + /// default app — ShellExecute either way. + private void ExplorerOpen_Click(object sender, RoutedEventArgs e) + { + if ((sender as FrameworkElement)?.DataContext is not TreeViewNode + { Content: ExplorerItem item }) return; + if (ShellOpen.Try(item.FullPath) is { } ex) + _transcript.Append(_html.Warn( + $"Couldn't open {(item.IsDirectory ? "folder" : "file")}: {ex.Message}")); + } + // ============================================================ // Drag & drop @-references // ============================================================ diff --git a/src/MandoCode.Desktop/Controls/ChatTabView.ViewModels.cs b/src/MandoCode.Desktop/Controls/ChatTabView.ViewModels.cs index 10363d1..603da26 100644 --- a/src/MandoCode.Desktop/Controls/ChatTabView.ViewModels.cs +++ b/src/MandoCode.Desktop/Controls/ChatTabView.ViewModels.cs @@ -50,6 +50,8 @@ public sealed class ExplorerItem : System.ComponentModel.INotifyPropertyChanged public string TagTooltip => $"Tag in prompt \u2014 inserts {Token}"; + public string OpenTooltip => IsDirectory ? "Open in File Explorer" : "Open in its default app"; + /// Files: this file has uncommitted changes. Folders: something inside does. /// Mutable + observable so rows already realized in the tree light up in place when a /// git refresh lands (rebuilding the tree would lose expansion state). diff --git a/src/MandoCode.Desktop/Controls/ChatTabView.xaml b/src/MandoCode.Desktop/Controls/ChatTabView.xaml index 95970ec..d49bd4e 100644 --- a/src/MandoCode.Desktop/Controls/ChatTabView.xaml +++ b/src/MandoCode.Desktop/Controls/ChatTabView.xaml @@ -212,6 +212,7 @@ + + + - + + (); if (cliInstalled) options.Add(new("Start Ollama now", ApprovalOptionKind.Proceed)); options.Add(new("Change endpoint URL", ApprovalOptionKind.Proceed)); - if (!cliInstalled) options.Add(new("Open the Ollama download page", ApprovalOptionKind.Proceed)); + if (!cliInstalled) + { + if (OllamaSetupHelper.GetOsInstallCommand() != null) + options.Add(new("Install Ollama for me", ApprovalOptionKind.Proceed)); + options.Add(new("Open the Ollama download page", ApprovalOptionKind.Proceed)); + } options.Add(new("Retry", ApprovalOptionKind.Proceed)); options.Add(new("Cancel setup", ApprovalOptionKind.Redirect)); @@ -202,6 +207,32 @@ private async Task RunSetupWizardAsync() continue; } + if (choice == "Install Ollama for me") + { + // InstallOllamaAsync runs `winget install Ollama.Ollama` as a child process with + // its own console window — the user sees winget's progress and any UAC prompt. + _transcript.Append(_html.Info($"Running: {OllamaSetupHelper.GetOsInstallCommand()} — a console window will show the installer's progress.")); + _busy.Start("Installing Ollama..."); + int exit; + try { exit = await OllamaSetupHelper.InstallOllamaAsync(); } + catch (Exception ex) { exit = -1; _transcript.Append(_html.Warn(ex.Message)); } + finally { _busy.Reset(); } + + if (exit == 0) + { + _transcript.Append(_html.Success("✓ Installer finished.")); + } + else + { + _transcript.Append(_html.Warn(exit == -1 + ? "Couldn't launch the installer — winget may not be available on this machine." + : $"Installer exited with code {exit} — the install may not have completed.")); + _transcript.Append(_html.Dim("Opening the download page as a fallback — finish the install there, then pick Retry.")); + OllamaSetupHelper.OpenInBrowser("https://ollama.com/download"); + } + continue; + } + if (choice == "Open the Ollama download page") { OllamaSetupHelper.OpenInBrowser("https://ollama.com/download"); @@ -227,15 +258,51 @@ private async Task RunSetupWizardAsync() var models = fetched.Ok ? fetched.Models : new List(); if (models.Count == 0) { - _transcript.Append(_html.Warn("No models pulled yet.")); - if (await WizardConfirmAsync("Pull a model now?", defaultYes: true)) + // Curated starter picker (mirrors the CLI's PickWhenEmptyAsync): a newcomer with an + // empty daemon shouldn't need to know model tags. Sizes and hardware hints let them + // self-select; free-text entry remains for people who know what they want. + _transcript.Append(_html.Warn("No models are pulled yet — let's get you one.")); + _transcript.Append(_html.Dim("Cloud models run on ollama.com's servers: more capable, no GPU needed, free with a sign-in. " + + "Local models run privately on your own hardware — bigger is smarter but needs more memory.")); + + var starter = await WizardSelectAsync("Pick a starter model to install:", new List + { + new($"Cloud — {MandoCodeConfig.DefaultCloudModel} (best quality, no GPU needed)", ApprovalOptionKind.Proceed), + new("Local — qwen2.5:1.5b (~1 GB · fast on any laptop)", ApprovalOptionKind.Proceed), + new("Local — qwen3:4b (~2.6 GB · balanced day-to-day)", ApprovalOptionKind.Proceed), + new("Local — qwen2.5-coder:7b (~4.7 GB · code-focused, 6 GB+ VRAM)", ApprovalOptionKind.Proceed), + new("Local — qwen3:8b (~5.2 GB · best local quality, 8 GB+ VRAM)", ApprovalOptionKind.Proceed), + new("Type a different model tag", ApprovalOptionKind.Proceed), + new("Skip — I'll pull one myself", ApprovalOptionKind.Redirect), + }); + + string? tag = null; + if (starter.StartsWith("Cloud")) + { + tag = MandoCodeConfig.DefaultCloudModel; + var auth = await OllamaSetupHelper.CheckCloudSignInAsync(_config.OllamaEndpoint); + if (auth != OllamaSetupHelper.CloudAuthState.SignedIn) + { + _transcript.Append(_html.Info("Cloud models need a free ollama.com account.")); + if (await WizardConfirmAsync("Sign in to Ollama cloud now?", defaultYes: true)) + await RunCloudSigninWalkthroughAsync(); + } + } + else if (starter.StartsWith("Local — ")) + { + tag = starter["Local — ".Length..].Split(' ')[0]; + } + else if (starter == "Type a different model tag") { - var tag = await WizardTextAsync( + var typed = await WizardTextAsync( "Model tag to pull:", "e.g. qwen2.5-coder:7b", t => string.IsNullOrWhiteSpace(t) ? "Enter a model tag" : null); - if (tag != null && await PullModelWithProgressAsync(tag.Trim())) - models = new List { tag.Trim() }; + tag = typed?.Trim(); } + + if (tag != null && await PullModelWithProgressAsync(tag)) + models = new List { tag }; + if (models.Count == 0) { _transcript.Append(_html.Dim("Setup incomplete — pull a model (ollama pull ) and run /setup again.")); diff --git a/src/MandoCode.Desktop/ViewModels/ChatController.cs b/src/MandoCode.Desktop/ViewModels/ChatController.cs index 4f149fd..1405fce 100644 --- a/src/MandoCode.Desktop/ViewModels/ChatController.cs +++ b/src/MandoCode.Desktop/ViewModels/ChatController.cs @@ -272,12 +272,25 @@ public async Task InitializeAsync() IsConnected = probe.Ok; var shouldSetup = MandoCodeConfig.IsFirstRun() || (!probe.Ok && !_config.HasCompletedOnboarding); - if (!probe.Ok) + if (shouldSetup) + { + // First launch (or setup never finished and no daemon): run the guided wizard right + // here in the chat, like the CLI's onboarding. A fresh user shouldn't have to + // discover /setup or hand-edit an endpoint on the Settings page for a first reply. + _transcript.Append(_html.Info("Welcome to MandoCode! Let's get you set up — takes about a minute.")); + _isProcessing = true; // input waits until the wizard is done, same as /setup mid-chat + StateChanged?.Invoke(); + try { await RunSetupWizardAsync(); } + finally { _isProcessing = false; } + + // Wizard cancelled or didn't get connected — land on Settings as the manual fallback. + if (!IsConnected) SetupNeeded?.Invoke(); + } + else if (!probe.Ok) { _transcript.Append(_html.Warn($"Can't reach Ollama at {_config.OllamaEndpoint}.")); _transcript.Append(_html.Dim("Open Settings (gear icon) to set the endpoint and model, make sure 'ollama serve' is running, then hit Reconnect — or type /retry.")); } - if (shouldSetup) SetupNeeded?.Invoke(); if (IsConnected) {