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
66 changes: 60 additions & 6 deletions src/MandoCode.Desktop/Controls/ChatTabView.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>

<!-- ======= Per-tab header =======
Connection, model, tokens, project folder, and the two folder/save actions all
Connection, model, tokens, project folder, and the folder action all
describe THIS agent. Each tab has its own, because each tab has its own
AIService, TokenTrackingService, and ProjectRootAccessor. -->
<Grid Grid.Row="0" Padding="16,8,16,8" Background="{StaticResource MandoPanelBrush}"
Expand Down Expand Up @@ -104,10 +105,6 @@
ToolTipService.ToolTip="This tab's project folder"/>

<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="6">
<Button x:Name="SaveTranscriptButton" Click="SaveTranscript_Click" Padding="8,5"
ToolTipService.ToolTip="Save this tab's transcript as a standalone HTML page">
<FontIcon Glyph="&#xE74E;" FontSize="15"/>
</Button>
<Button x:Name="OpenFolderButton" Click="OpenFolderButton_Click" Padding="8,5"
ToolTipService.ToolTip="Change this tab's project folder">
<FontIcon Glyph="&#xE8B7;" FontSize="15"/>
Expand All @@ -132,7 +129,64 @@
<TextBlock Text="(Esc to cancel)" VerticalAlignment="Center" Opacity="0.4" FontSize="12"/>
</StackPanel>

<Grid Grid.Row="4" Padding="16,10,16,14" ColumnSpacing="8">
<!-- ======= Create-snapshot offer (opt-in) =======
Appears when a model switch (or "Take snapshot") buffers a conversation. Snapshots are
born summarized: pick the model to summarize with — the origin model by default, or any
installed model (local = free, cloud = may spend tokens). Ignore it and the conversation
is discarded (pure opt-in, no auto-save). -->
<Border x:Name="SnapshotOfferCard" Grid.Row="4" Visibility="Collapsed"
Margin="16,8,16,0" Padding="14,12" CornerRadius="10"
Background="{StaticResource MandoPanelBrush}"
BorderBrush="{StaticResource MandoBorderBrush}" BorderThickness="1">
<StackPanel Spacing="10">
<Grid>
<StackPanel Spacing="2">
<StackPanel Orientation="Horizontal" Spacing="8">
<FontIcon Glyph="&#xE81C;" FontSize="14" VerticalAlignment="Center"
Foreground="{StaticResource MandoAccentBrush}"/>
<TextBlock Text="Create a context snapshot?" FontWeight="SemiBold" FontSize="13"/>
</StackPanel>
<TextBlock x:Name="SnapshotOfferSubtitle" Opacity="0.6" FontSize="12"
TextWrapping="Wrap"/>
</StackPanel>
<Button Click="SnapshotOfferDismiss_Click" Padding="6" Background="Transparent"
BorderThickness="0" HorizontalAlignment="Right" VerticalAlignment="Top"
ToolTipService.ToolTip="Dismiss — the conversation won't be saved">
<FontIcon Glyph="&#xE711;" FontSize="12"/>
</Button>
</Grid>
<!-- Name (optional) and the summarizer model share one row: [name] [model] [Create]. -->
<Grid ColumnSpacing="8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBox x:Name="SnapshotNameBox" Grid.Column="0" PlaceholderText="Name (optional)"
MaxLength="80" VerticalAlignment="Center"/>
<ComboBox x:Name="SnapshotModelCombo" Grid.Column="1" HorizontalAlignment="Stretch"
VerticalAlignment="Center"
ToolTipService.ToolTip="Model that will write the recap">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="local:ModelChoice">
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock Text="{x:Bind Name}" VerticalAlignment="Center"/>
<Border CornerRadius="5" Padding="6,1" VerticalAlignment="Center"
Background="{StaticResource MandoBackgroundBrush}">
<TextBlock Text="{x:Bind Tag}" FontSize="10" Opacity="0.75"/>
</Border>
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<Button x:Name="SnapshotCreateButton" Grid.Column="2" Content="Create"
Click="SnapshotCreate_Click" VerticalAlignment="Center"
Style="{StaticResource AccentButtonStyle}"/>
</Grid>
</StackPanel>
</Border>

<Grid Grid.Row="5" Padding="16,10,16,14" ColumnSpacing="8">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
Expand Down
92 changes: 87 additions & 5 deletions src/MandoCode.Desktop/Controls/ChatTabView.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ public ChatTabView(Window owner, AgentSession session, TranscriptHtmlBuilder htm
_controller.McpEditorRequested += OnMcpEditorRequested;
_controller.ClipboardCopyRequested += OnClipboardCopy;
_controller.ExitRequested += OnExitRequested;
_controller.SnapshotOfferChanged += OnSnapshotOfferChanged;

UpdateHeader();
}
Expand All @@ -126,6 +127,7 @@ public ChatTabView(Window owner, AgentSession session, TranscriptHtmlBuilder htm
private void OnMcpEditorRequested(string? name) => OnUi(() => McpEditorRequested?.Invoke(name));
private void OnClipboardCopy(string text) => OnUi(() => ClipboardCopyRequested?.Invoke(text));
private void OnExitRequested() => OnUi(() => ExitRequested?.Invoke());
private void OnSnapshotOfferChanged() => OnUi(RefreshSnapshotOffer);

private void OnUi(Action action)
{
Expand Down Expand Up @@ -278,6 +280,7 @@ public void Shutdown()
_controller.McpEditorRequested -= OnMcpEditorRequested;
_controller.ClipboardCopyRequested -= OnClipboardCopy;
_controller.ExitRequested -= OnExitRequested;
_controller.SnapshotOfferChanged -= OnSnapshotOfferChanged;

_controller.CancelActiveRequest();

Expand Down Expand Up @@ -327,12 +330,91 @@ private async void ClearTranscript()
catch { }
}

/// <summary>Snapshots the transcript document to a standalone .html file. The highlight
/// classes and CSS are already baked into the DOM, so the saved page keeps its colors.</summary>
private void SaveTranscript_Click(object sender, RoutedEventArgs e) => _ = ExportTranscriptAsync();
/// <summary>Offer to snapshot this tab's conversation (the "Take snapshot" tab action) — pops the
/// opt-in create card so the user can pick a summarizer model.</summary>
public void TakeSnapshotManually() => _ = _controller.OfferManualSnapshotAsync();

/// <summary>Manually snapshot this tab's conversation (the "Take snapshot" tab action).</summary>
public void TakeSnapshotManually() => _ = _controller.CaptureManualSnapshotAsync();
// ============================================================
// Create-snapshot offer card — shown when the controller buffers a conversation (on a model
// switch or "Take snapshot"). The user picks a summarizer model and creates, or dismisses to
// discard. Snapshots are born summarized; there is no light/un-enhanced state.
// ============================================================

/// <summary>Shows or hides the offer card to match the controller's pending buffer, and (when
/// shown) loads the model picker.</summary>
private void RefreshSnapshotOffer()
{
var offer = _controller.PendingOffer;
if (offer == null)
{
SnapshotOfferCard.Visibility = Visibility.Collapsed;
return;
}

SnapshotOfferSubtitle.Text =
$"{offer.MessageCount} message{(offer.MessageCount == 1 ? "" : "s")} from {offer.OriginModel} — "
+ "pick a model to write the recap, or dismiss to discard.";
SnapshotCreateButton.Content = "Create";
SnapshotNameBox.Text = ""; // a fresh offer starts unnamed
SnapshotOfferCard.Visibility = Visibility.Visible;
_ = LoadSnapshotModelsAsync(offer.OriginModel);
}

/// <summary>Populates the model picker without making the card wait on a network round-trip: the
/// model that had the conversation is shown selected instantly, then the full installed-model list
/// (an Ollama /api/tags fetch, slow on cloud setups) streams in behind it for "pick another."</summary>
private async Task LoadSnapshotModelsAsync(string originModel)
{
// Instant: seed with just the current model so the card is usable with zero lag.
var current = new ModelChoice(originModel, MandoCodeConfig.IsCloudModel(originModel));
SnapshotModelCombo.ItemsSource = new List<ModelChoice> { current };
SnapshotModelCombo.SelectedIndex = 0;
SnapshotModelCombo.IsEnabled = true;
SnapshotCreateButton.IsEnabled = true;

// Background: fetch the rest so the dropdown fills in for choosing another model.
var result = await _controller.LoadAvailableModelsAsync();
if (!result.Ok || result.Models.Count == 0) return; // keep the single current entry

// Guard against a race: if a newer offer/switch swapped models while we were fetching, don't
// clobber its selection with this stale list.
if ((SnapshotModelCombo.SelectedItem as ModelChoice)?.Name != originModel) return;

var choices = result.Models
.Select(m => new ModelChoice(m, MandoCodeConfig.IsCloudModel(m)))
.ToList();
if (!choices.Any(c => string.Equals(c.Name, originModel, StringComparison.OrdinalIgnoreCase)))
choices.Insert(0, current); // keep the current model even if the list omits it

SnapshotModelCombo.ItemsSource = choices;
SnapshotModelCombo.SelectedItem =
choices.First(c => string.Equals(c.Name, originModel, StringComparison.OrdinalIgnoreCase));
}

private async void SnapshotCreate_Click(object sender, RoutedEventArgs e)
{
if (SnapshotModelCombo.SelectedItem is not ModelChoice choice)
{
_transcript.Append(_html.Warn("Pick a model to summarize with first."));
return;
}

SnapshotCreateButton.IsEnabled = false;
SnapshotCreateButton.Content = "Creating Snapshot...";

var error = await _controller.CreateSnapshotAsync(choice.Name, SnapshotNameBox.Text);
if (error != null)
{
SnapshotCreateButton.IsEnabled = true;
SnapshotCreateButton.Content = "Create";
_transcript.Append(_html.Warn(error));
}
// On success the controller clears the offer → SnapshotOfferChanged → RefreshSnapshotOffer
// hides the card, and a "Snapshot saved" chip lands in the transcript.
}

private void SnapshotOfferDismiss_Click(object sender, RoutedEventArgs e)
=> _controller.DismissSnapshotOffer();

/// <summary>Saves this tab's transcript as a standalone HTML page. Shared by the header save
/// button and the tab's options menu.</summary>
Expand Down
55 changes: 42 additions & 13 deletions src/MandoCode.Desktop/MainWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@
</Grid.ColumnDefinitions>
<StackPanel Spacing="3">
<TextBlock Text="Context snapshots" FontSize="16" FontWeight="SemiBold"/>
<TextBlock Text="Saved when a tab switches models, or on demand. Import carries a saved context into the active agent's next message."
<TextBlock Text="Created on demand when you switch a tab's model or Take snapshot — summarized by a model you choose. Import carries a recap into the active agent's next message."
Opacity="0.6" FontSize="12" TextWrapping="Wrap"/>
</StackPanel>
<Button Grid.Column="1" Click="CloseSnapshots_Click" Padding="6" Background="Transparent"
Expand All @@ -81,7 +81,7 @@
</Grid>

<TextBlock x:Name="SnapshotsEmpty" Grid.Row="1" Visibility="Collapsed"
Text="No snapshots yet. Switch a tab's model, or use a tab's ⋯ menu → Take snapshot."
Text="No snapshots yet. Switch a tab's model, or use a tab's ⋯ menu → Take snapshot — then choose a model to summarize it."
Opacity="0.6" FontSize="12" TextWrapping="Wrap" Padding="18,8,18,18"/>

<ScrollViewer x:Name="SnapshotsScroller" Grid.Row="1">
Expand All @@ -100,21 +100,44 @@
Padding="12,10">
<StackPanel Spacing="6">
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock Text="{x:Bind OriginModel}" FontWeight="SemiBold" FontSize="13"
TextTrimming="CharacterEllipsis"/>
<FontIcon Glyph="&#xE81C;" FontSize="13" VerticalAlignment="Center"
Foreground="{StaticResource MandoAccentBrush}"/>
<TextBlock Text="{x:Bind DisplayTitle}" FontWeight="SemiBold" FontSize="13"
VerticalAlignment="Center" TextTrimming="CharacterEllipsis"
ToolTipService.ToolTip="{x:Bind OriginModel}"/>
<Border CornerRadius="5" Padding="6,0" VerticalAlignment="Center"
Background="{StaticResource MandoPanelBrush}">
<TextBlock Text="{x:Bind Tag}" FontSize="10" Opacity="0.8"/>
Background="{StaticResource MandoPanelBrush}"
ToolTipService.ToolTip="Model that wrote this recap">
<TextBlock Text="{x:Bind SummarizerModel}" FontSize="10" Opacity="0.8"/>
</Border>
</StackPanel>
<TextBlock Text="{x:Bind TimeLabel}" Opacity="0.55" FontSize="11"/>
<TextBlock Text="{x:Bind LightRecap}" Opacity="0.75" FontSize="12"
<StackPanel Orientation="Horizontal" Spacing="6">
<FontIcon Glyph="&#xE823;" FontSize="11" Opacity="0.55" VerticalAlignment="Center"/>
<TextBlock Text="{x:Bind TimeLabel}" Opacity="0.55" FontSize="11"
VerticalAlignment="Center"/>
<TextBlock Text="·" Opacity="0.4" FontSize="11" VerticalAlignment="Center"/>
<FontIcon Glyph="&#xE8BD;" FontSize="11" Opacity="0.55" VerticalAlignment="Center"/>
<TextBlock Text="{x:Bind MessageCount}" Opacity="0.55" FontSize="11"
VerticalAlignment="Center"/>
</StackPanel>
<TextBlock Text="{x:Bind Recap}" Opacity="0.75" FontSize="12"
MaxLines="4" TextTrimming="CharacterEllipsis" TextWrapping="Wrap"/>
<StackPanel Orientation="Horizontal" Spacing="6">
<Button Content="Import" FontSize="12" Padding="10,4"
Click="SnapshotImport_Click" Tag="{x:Bind}"/>
<Button Content="Delete" FontSize="12" Padding="10,4"
Click="SnapshotDelete_Click" Tag="{x:Bind}"/>
<Button FontSize="12" Padding="10,4"
Click="SnapshotImport_Click" Tag="{x:Bind}"
ToolTipService.ToolTip="Carry this recap into the active agent's next message">
<StackPanel Orientation="Horizontal" Spacing="6">
<FontIcon Glyph="&#xE896;" FontSize="12"/>
<TextBlock Text="Import"/>
</StackPanel>
</Button>
<Button FontSize="12" Padding="10,4"
Click="SnapshotDelete_Click" Tag="{x:Bind}">
<StackPanel Orientation="Horizontal" Spacing="6">
<FontIcon Glyph="&#xE74D;" FontSize="12"/>
<TextBlock Text="Delete"/>
</StackPanel>
</Button>
</StackPanel>
</StackPanel>
</Border>
Expand Down Expand Up @@ -168,6 +191,9 @@

<!-- ============ SETTINGS PAGE ============ -->
<Grid x:Name="SettingsPage" Visibility="Collapsed" Padding="24,16,24,16" RowSpacing="12">
<Grid.RenderTransform>
<TranslateTransform x:Name="SettingsPageTransform"/>
</Grid.RenderTransform>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
Expand All @@ -191,8 +217,8 @@
</StackPanel>

<SelectorBar Grid.Row="1" x:Name="SettingsTabs" SelectionChanged="SettingsTabs_SelectionChanged">
<SelectorBarItem x:Name="Tab_Appearance" Text="Appearance"/>
<SelectorBarItem x:Name="Tab_Connection" Text="Connection"/>
<SelectorBarItem x:Name="Tab_Appearance" Text="Appearance"/>
<SelectorBarItem x:Name="Tab_Generation" Text="Generation"/>
<SelectorBarItem x:Name="Tab_Behavior" Text="Behavior"/>
<SelectorBarItem x:Name="Tab_Limits" Text="Limits"/>
Expand Down Expand Up @@ -392,6 +418,9 @@

<!-- ============ MCP PAGE ============ -->
<Grid x:Name="McpPage" Visibility="Collapsed" Padding="24,16,24,24" RowSpacing="12">
<Grid.RenderTransform>
<TranslateTransform x:Name="McpPageTransform"/>
</Grid.RenderTransform>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
Expand Down
Loading
Loading