diff --git a/src/OneWare.Chat/ChatHeightEstimation.cs b/src/OneWare.Chat/ChatHeightEstimation.cs
new file mode 100644
index 00000000..50c6ff00
--- /dev/null
+++ b/src/OneWare.Chat/ChatHeightEstimation.cs
@@ -0,0 +1,34 @@
+using System;
+
+namespace OneWare.Chat;
+
+///
+/// Heuristics for estimating a chat message's rendered pixel height from its markdown content,
+/// before it has been realized and measured. Used to give the virtualizing message list a stable,
+/// content-accurate scroll extent.
+///
+public static class ChatHeightEstimation
+{
+ private const double CharWidth = 7.5;
+ private const double LineHeight = 18;
+ private const double VerticalPadding = 16;
+
+ ///
+ /// Estimates the height of a block of markdown/plain text for the given available width by
+ /// approximating wrapped line counts. The real measured height replaces this once realized,
+ /// so the estimate only needs to be in the right ballpark.
+ ///
+ public static double EstimateMarkdown(string? text, double width, double extra = 0)
+ {
+ if (string.IsNullOrEmpty(text))
+ return VerticalPadding + LineHeight;
+
+ var charsPerLine = Math.Max(1, width / CharWidth);
+ double lines = 0;
+
+ foreach (var hardLine in text.Split('\n'))
+ lines += Math.Max(1, Math.Ceiling(hardLine.Length / charsPerLine));
+
+ return VerticalPadding + extra + lines * LineHeight;
+ }
+}
diff --git a/src/OneWare.Chat/ViewModels/ChatMessages/ChatMessageAssistantViewModel.cs b/src/OneWare.Chat/ViewModels/ChatMessages/ChatMessageAssistantViewModel.cs
index ed165d0b..e67e8839 100644
--- a/src/OneWare.Chat/ViewModels/ChatMessages/ChatMessageAssistantViewModel.cs
+++ b/src/OneWare.Chat/ViewModels/ChatMessages/ChatMessageAssistantViewModel.cs
@@ -1,9 +1,10 @@
using System.Runtime.Serialization;
using CommunityToolkit.Mvvm.ComponentModel;
+using OneWare.Essentials.Controls;
namespace OneWare.Chat.ViewModels.ChatMessages;
-public class ChatMessageAssistantViewModel : ObservableObject, IChatMessage
+public class ChatMessageAssistantViewModel : ObservableObject, IChatMessage, IEstimatedHeightItem
{
public ChatMessageAssistantViewModel(string? messageId = null)
{
@@ -27,4 +28,6 @@ public bool IsStreaming
get;
set => SetProperty(ref field, value);
}
+
+ public double EstimateHeight(double width) => ChatHeightEstimation.EstimateMarkdown(Content, width);
}
diff --git a/src/OneWare.Chat/ViewModels/ChatMessages/ChatMessageReasoningViewModel.cs b/src/OneWare.Chat/ViewModels/ChatMessages/ChatMessageReasoningViewModel.cs
index 5249f1cb..6e4f56a9 100644
--- a/src/OneWare.Chat/ViewModels/ChatMessages/ChatMessageReasoningViewModel.cs
+++ b/src/OneWare.Chat/ViewModels/ChatMessages/ChatMessageReasoningViewModel.cs
@@ -1,9 +1,10 @@
using System.Runtime.Serialization;
using CommunityToolkit.Mvvm.ComponentModel;
+using OneWare.Essentials.Controls;
namespace OneWare.Chat.ViewModels.ChatMessages;
-public class ChatMessageReasoningViewModel : ObservableObject, IChatMessage
+public class ChatMessageReasoningViewModel : ObservableObject, IChatMessage, IEstimatedHeightItem
{
public ChatMessageReasoningViewModel(string? reasoningId = null)
{
@@ -27,4 +28,12 @@ public bool IsStreaming
get;
set => SetProperty(ref field, value);
}
+
+ public double EstimateHeight(double width)
+ {
+ const double header = 36;
+ if (!IsStreaming)
+ return header;
+ return header + System.Math.Min(200, ChatHeightEstimation.EstimateMarkdown(Content, width)) + 8;
+ }
}
diff --git a/src/OneWare.Chat/ViewModels/ChatMessages/ChatMessageToolViewModel.cs b/src/OneWare.Chat/ViewModels/ChatMessages/ChatMessageToolViewModel.cs
index b52ff621..7fa55b71 100644
--- a/src/OneWare.Chat/ViewModels/ChatMessages/ChatMessageToolViewModel.cs
+++ b/src/OneWare.Chat/ViewModels/ChatMessages/ChatMessageToolViewModel.cs
@@ -1,9 +1,10 @@
using System.Runtime.Serialization;
using CommunityToolkit.Mvvm.ComponentModel;
+using OneWare.Essentials.Controls;
namespace OneWare.Chat.ViewModels.ChatMessages;
-public class ChatMessageToolViewModel : ObservableObject, IChatMessage
+public class ChatMessageToolViewModel : ObservableObject, IChatMessage, IEstimatedHeightItem
{
public ChatMessageToolViewModel(string id, string toolName)
{
@@ -34,4 +35,12 @@ public bool IsSuccessful
get;
set => SetProperty(ref field, value);
}
+
+ public double EstimateHeight(double width)
+ {
+ const double header = 36;
+ if (!IsToolRunning)
+ return header;
+ return header + System.Math.Min(200, ChatHeightEstimation.EstimateMarkdown(ToolOutput, width)) + 8;
+ }
}
diff --git a/src/OneWare.Chat/ViewModels/ChatMessages/ChatMessageUserViewModel.cs b/src/OneWare.Chat/ViewModels/ChatMessages/ChatMessageUserViewModel.cs
index 98b817c0..9687baf8 100644
--- a/src/OneWare.Chat/ViewModels/ChatMessages/ChatMessageUserViewModel.cs
+++ b/src/OneWare.Chat/ViewModels/ChatMessages/ChatMessageUserViewModel.cs
@@ -1,9 +1,10 @@
using System.Runtime.Serialization;
using CommunityToolkit.Mvvm.ComponentModel;
+using OneWare.Essentials.Controls;
namespace OneWare.Chat.ViewModels.ChatMessages;
-public class ChatMessageUserViewModel : ObservableObject, IChatMessage
+public class ChatMessageUserViewModel : ObservableObject, IChatMessage, IEstimatedHeightItem
{
public ChatMessageUserViewModel(string message)
{
@@ -15,4 +16,6 @@ public ChatMessageUserViewModel(string message)
public string Message { get; }
public DateTimeOffset Timestamp { get; }
+
+ public double EstimateHeight(double width) => ChatHeightEstimation.EstimateMarkdown(Message, width);
}
diff --git a/src/OneWare.Chat/Views/ChatView.axaml b/src/OneWare.Chat/Views/ChatView.axaml
index aba6e18d..035163fb 100644
--- a/src/OneWare.Chat/Views/ChatView.axaml
+++ b/src/OneWare.Chat/Views/ChatView.axaml
@@ -75,65 +75,68 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
-
-
+
+
-
-
-
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+