Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand Down
25 changes: 22 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

```
Expand Down Expand Up @@ -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`,
Expand Down Expand Up @@ -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
Expand Down
15 changes: 14 additions & 1 deletion src/MandoCode.Desktop/Controls/ChatTabView.Explorer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -763,14 +763,27 @@ 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;
if (ShellOpen.Try(item.FullPath) is { } ex)
_transcript.Append(_html.Warn($"Couldn't open file: {ex.Message}"));
}

/// <summary>The row's open button: folders in Windows File Explorer, files in their
/// default app — ShellExecute either way.</summary>
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
// ============================================================
Expand Down
2 changes: 2 additions & 0 deletions src/MandoCode.Desktop/Controls/ChatTabView.ViewModels.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";

/// <summary>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).</summary>
Expand Down
14 changes: 13 additions & 1 deletion src/MandoCode.Desktop/Controls/ChatTabView.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<FontIcon Glyph="{x:Bind ((local:ExplorerItem)Content).Glyph}"
FontSize="13" VerticalAlignment="Center"
Expand All @@ -226,10 +227,21 @@
Fill="{StaticResource MandoGoldBrush}"
Visibility="{x:Bind ((local:ExplorerItem)Content).DirtyVisibility, Mode=OneWay}"
ToolTipService.ToolTip="Uncommitted changes"/>
<!-- Open button: folders open in Windows File Explorer, files in
their default app. Double-click can't do this for folders — it
fights the expand/collapse toggle. -->
<Button Grid.Column="3" Click="ExplorerOpen_Click"
PointerEntered="ExplorerTag_PointerEntered"
PointerExited="ExplorerTag_PointerExited"
Padding="6,1" Background="Transparent" BorderThickness="0"
Opacity="0.45" VerticalAlignment="Center"
ToolTipService.ToolTip="{x:Bind ((local:ExplorerItem)Content).OpenTooltip}">
<FontIcon Glyph="&#xE8A7;" FontSize="12"/>
</Button>
<!-- Tag button: inserts this row's @token into the prompt, same as
dragging it there. Dim at rest, brightens on hover (code-behind —
WinUI styles have no triggers). -->
<Button Grid.Column="3" Click="ExplorerTag_Click"
<Button Grid.Column="4" Click="ExplorerTag_Click"
PointerEntered="ExplorerTag_PointerEntered"
PointerExited="ExplorerTag_PointerExited"
Padding="6,1" Background="Transparent" BorderThickness="0"
Expand Down
54 changes: 53 additions & 1 deletion src/MandoCode.Desktop/MainWindow.Notes.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Text;
using MandoCode.Desktop.Services;
using MandoCode.Services; // OllamaSetupHelper — lists models without needing an agent
using Microsoft.UI.Xaml;
Expand Down Expand Up @@ -322,6 +323,7 @@ private void OpenNote(NoteEntry note)
// previous note would be answering about text that's no longer on screen.
if (!string.Equals(_noteThreadFor, note.Path, StringComparison.OrdinalIgnoreCase))
{
_noteAskCts?.Cancel();
_noteThread.Clear();
_noteThreadFor = note.Path;
NoteAsk.ClearReply();
Expand All @@ -336,6 +338,7 @@ private void OpenNote(NoteEntry note)

private void ShowNoteBrowse()
{
_noteAskCts?.Cancel(); // the reply strip is about to be cleared; stop streaming into it
NoteEditor.Close(); // saves and stops watching
_lastNotePath = null;
_noteThread.Clear();
Expand Down Expand Up @@ -446,7 +449,53 @@ private async Task AskNotesAsync(string question)
_noteAskCts = new CancellationTokenSource();
var ct = _noteAskCts.Token;

void OnDelta(string delta) => OnUi(() => NoteAsk.AppendDelta(delta));
// Deltas arrive on a background thread, possibly hundreds per second from a small model, and
// every repaint re-lays-out the entire reply. Posting per token starves the UI thread, and
// even coalesced back-to-back flushes stutter once the reply grows — so batch on a clock:
// deltas pile up off-thread, and a timer paints at most ten times a second. 100ms batching is
// invisible when reading streaming text; unbounded repainting is a frozen app.
var pendingDelta = new StringBuilder();
var pumping = false; // guarded by deltaGate — the producers' view of "timer is running"
var deltaGate = new object();

var flushTimer = _dispatcher.CreateTimer();
flushTimer.Interval = TimeSpan.FromMilliseconds(100);
flushTimer.Tick += (_, _) =>
{
string chunk;
lock (deltaGate)
{
chunk = pendingDelta.ToString();
pendingDelta.Clear();
if (chunk.Length == 0) pumping = false;
}
if (chunk.Length == 0) { flushTimer.Stop(); return; } // idle — a new delta restarts it
NoteAsk.AppendDelta(chunk);
};

void OnDelta(string delta)
{
lock (deltaGate)
{
pendingDelta.Append(delta);
if (pumping) return;
pumping = true;
}
OnUi(flushTimer.Start);
}

void FlushDelta() // final drain on the UI thread, once the stream is over
{
string chunk;
lock (deltaGate)
{
chunk = pendingDelta.ToString();
pendingDelta.Clear();
pumping = false;
}
flushTimer.Stop();
if (chunk.Length > 0) NoteAsk.AppendDelta(chunk);
}

try
{
Expand All @@ -463,6 +512,7 @@ await NoteAssistant.AskAboutPadAsync(
thread, question, OnDelta, ct);
}

FlushDelta(); // drain the tail before reading NoteAsk.Reply below
NoteAsk.EndReply();

thread.Add(new NoteAssistant.Turn(true, question));
Expand All @@ -471,10 +521,12 @@ await NoteAssistant.AskAboutPadAsync(
}
catch (OperationCanceledException)
{
FlushDelta(); // show what had streamed before the stop
NoteAsk.EndReply(label: "stopped");
}
catch (Exception ex)
{
FlushDelta(); // stop the timer — a late tick would paint reply text over the error
// Ollama down, model pulled away, endpoint wrong: say which, in the strip.
NoteAsk.EndReply(error: $"{ex.Message}\n\nEndpoint: {endpoint} · model: {model}");
}
Expand Down
9 changes: 9 additions & 0 deletions src/MandoCode.Desktop/MainWindow.Settings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,15 @@ private void LoadSettings()
}
}

/// <summary>Runs the guided /setup wizard in the active agent's chat — the same flow that
/// fires on first launch. Routed through SubmitAsync so it gets the standard command echo
/// and the is-processing guard.</summary>
private void RunSetupWizard_Click(object sender, RoutedEventArgs e)
{
SwitchPage("chat");
_ = Task.Run(() => _controller.SubmitAsync("/setup"));
}

private void SettingsTabs_SelectionChanged(SelectorBar sender, SelectorBarSelectionChangedEventArgs args)
{
var s = sender.SelectedItem;
Expand Down
11 changes: 11 additions & 0 deletions src/MandoCode.Desktop/MainWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -863,6 +863,17 @@
HorizontalAlignment="Left" Padding="0,4,0,8">
<TextBlock Text="Connection" FontSize="13" FontWeight="SemiBold"
Foreground="{StaticResource MandoAccentBrush}"/>
<StackPanel Orientation="Horizontal" Spacing="8">
<Button Click="RunSetupWizard_Click"
ToolTipService.ToolTip="Guided setup: reach Ollama, install or start it if needed, and pick a model — the same wizard that runs on first launch.">
<StackPanel Orientation="Horizontal" Spacing="6">
<FontIcon Glyph="&#xE945;" FontSize="13"/>
<TextBlock Text="Run guided setup"/>
</StackPanel>
</Button>
<TextBlock Text="walks through connection + model, like first launch"
VerticalAlignment="Center" FontSize="12" Opacity="0.6"/>
</StackPanel>
<TextBlock Text="Ollama endpoint" FontSize="13" Opacity="0.8"/>
<TextBox x:Name="EndpointBox" PlaceholderText="http://localhost:11434" MinWidth="560"
HorizontalAlignment="Left"
Expand Down
5 changes: 4 additions & 1 deletion src/MandoCode.Desktop/Services/NoteAssistant.cs
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,10 @@ private static async Task StreamAsync(
// Low temperature: this rewrites the user's own words, so faithful beats inventive.
var settings = new OllamaPromptExecutionSettings { Temperature = 0.3f };

await foreach (var chunk in chat.GetStreamingChatMessageContentsAsync(history, settings, kernel, ct))
// ConfigureAwait(false): per-chunk continuations must not hop through the caller's UI
// dispatcher — a small model can stream hundreds of chunks a second.
await foreach (var chunk in chat.GetStreamingChatMessageContentsAsync(history, settings, kernel, ct)
.ConfigureAwait(false))
{
if (ct.IsCancellationRequested) return;
if (!string.IsNullOrEmpty(chunk.Content)) onDelta(chunk.Content);
Expand Down
Loading
Loading