From fd9ba08dc17acb7228bc83954c9ee8845e74b14d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 19 Jan 2026 01:02:25 +0000 Subject: [PATCH 1/5] Initial plan From 8d4ee68556bc9aad25b939db316186c03d583b8d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 19 Jan 2026 01:06:56 +0000 Subject: [PATCH 2/5] Make windows resizable and add sentence-by-sentence speech option Co-authored-by: ExtCan <60326708+ExtCan@users.noreply.github.com> --- src/Agent/AgentManager.cs | 24 ++++++++++++++++++++++++ src/Config/AppSettings.cs | 31 +++++++++++++++++++++++++++++++ src/UI/ChatForm.cs | 12 ++++++++++-- src/UI/MainForm.cs | 12 ++++++++++-- src/UI/SettingsForm.cs | 39 +++++++++++++++++++++++++++++++-------- 5 files changed, 106 insertions(+), 12 deletions(-) diff --git a/src/Agent/AgentManager.cs b/src/Agent/AgentManager.cs index fe04c00..9f5b68d 100644 --- a/src/Agent/AgentManager.cs +++ b/src/Agent/AgentManager.cs @@ -597,6 +597,30 @@ public void Speak(string text) } } + /// + /// Makes the character speak text sentence by sentence with delays + /// + public void SpeakSentences(List sentences, int delayMs = 500) + { + EnsureLoaded(); + if (sentences == null || sentences.Count == 0) + return; + + // Speak each sentence with a small delay between them + foreach (var sentence in sentences) + { + if (!string.IsNullOrEmpty(sentence)) + { + _character.Speak(sentence, null); + // Add a small pause between sentences + if (delayMs > 0) + { + System.Threading.Thread.Sleep(delayMs); + } + } + } + } + /// /// Makes the character think the specified text (shows in thought balloon) /// diff --git a/src/Config/AppSettings.cs b/src/Config/AppSettings.cs index cfbab83..eab2eae 100644 --- a/src/Config/AppSettings.cs +++ b/src/Config/AppSettings.cs @@ -153,6 +153,9 @@ public class AppSettings // Agent size (100 = normal, 50 = half, 200 = double) public int AgentSize { get; set; } = 100; + // Speech truncation (sentence by sentence) + public bool TruncateSpeech { get; set; } = false; + // Idle animation spacing (in idle timer ticks - higher = less frequent) public int IdleAnimationSpacing { get; set; } = 5; @@ -297,6 +300,34 @@ public static (string text, List animations) ExtractAnimationTriggers(st return (text, animations); } + /// + /// Splits text into sentences for sentence-by-sentence speech + /// + public static List SplitIntoSentences(string text) + { + var sentences = new List(); + if (string.IsNullOrEmpty(text)) + return sentences; + + // Split by common sentence endings: period, exclamation, question mark + // Also handle ellipsis (...) as a sentence boundary + var parts = System.Text.RegularExpressions.Regex.Split( + text, + @"(?<=[.!?])\s+|(?<=\.\.\.)\s+" + ); + + foreach (var part in parts) + { + var trimmed = part.Trim(); + if (!string.IsNullOrEmpty(trimmed)) + { + sentences.Add(trimmed); + } + } + + return sentences; + } + /// /// Gets a random line with text processing applied /// diff --git a/src/UI/ChatForm.cs b/src/UI/ChatForm.cs index ce833b8..1a7ea73 100644 --- a/src/UI/ChatForm.cs +++ b/src/UI/ChatForm.cs @@ -288,8 +288,16 @@ private void SpeakWithAnimations(string text, string defaultAnimation = null) _agentManager.PlayAnimation(defaultAnimation); } - // Speak the processed text - _agentManager.Speak(cleanText); + // Speak the processed text - check if truncation is enabled + if (_settings.TruncateSpeech) + { + var sentences = AppSettings.SplitIntoSentences(cleanText); + _agentManager.SpeakSentences(sentences, 500); + } + else + { + _agentManager.Speak(cleanText); + } } private void AppendToHistory(string speaker, string message, Color color) diff --git a/src/UI/MainForm.cs b/src/UI/MainForm.cs index b36803d..f345da3 100644 --- a/src/UI/MainForm.cs +++ b/src/UI/MainForm.cs @@ -381,8 +381,16 @@ private void SpeakWithAnimations(string text, string defaultAnimation = null) _agentManager.PlayAnimation(defaultAnimation); } - // Speak the processed text - _agentManager.Speak(cleanText); + // Speak the processed text - check if truncation is enabled + if (_settings.TruncateSpeech) + { + var sentences = AppSettings.SplitIntoSentences(cleanText); + _agentManager.SpeakSentences(sentences, 500); + } + else + { + _agentManager.Speak(cleanText); + } } /// diff --git a/src/UI/SettingsForm.cs b/src/UI/SettingsForm.cs index 9cf3a6c..6fbd612 100644 --- a/src/UI/SettingsForm.cs +++ b/src/UI/SettingsForm.cs @@ -178,15 +178,15 @@ private void InitializeComponent() this.Text = "MSAgent AI Settings"; this.Size = new Size(650, 550); this.StartPosition = FormStartPosition.CenterScreen; - this.FormBorderStyle = FormBorderStyle.FixedDialog; - this.MaximizeBox = false; - this.MinimizeBox = false; + this.FormBorderStyle = FormBorderStyle.Sizable; + this.MinimumSize = new Size(650, 550); // Create main tab control _tabControl = new TabControl { Location = new Point(10, 10), - Size = new Size(615, 450) + Size = new Size(615, 450), + Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right }; // Create tabs @@ -205,7 +205,8 @@ private void InitializeComponent() Text = "OK", Location = new Point(365, 470), Size = new Size(80, 30), - DialogResult = DialogResult.OK + DialogResult = DialogResult.OK, + Anchor = AnchorStyles.Bottom | AnchorStyles.Right }; _okButton.Click += OnOkClick; @@ -214,14 +215,16 @@ private void InitializeComponent() Text = "Cancel", Location = new Point(455, 470), Size = new Size(80, 30), - DialogResult = DialogResult.Cancel + DialogResult = DialogResult.Cancel, + Anchor = AnchorStyles.Bottom | AnchorStyles.Right }; _applyButton = new Button { Text = "Apply", Location = new Point(545, 470), - Size = new Size(80, 30) + Size = new Size(80, 30), + Anchor = AnchorStyles.Bottom | AnchorStyles.Right }; _applyButton.Click += OnApplyClick; @@ -645,6 +648,15 @@ private void CreateVoiceTab() ForeColor = Color.Gray, Font = new Font(this.Font.FontFamily, 7.5f) }; + + // Truncate speech checkbox + CheckBox _truncateSpeechCheckBox = new CheckBox + { + Text = "Sentence-by-sentence speech (truncate long speeches into separate bubbles)", + Location = new Point(15, 370), + Size = new Size(550, 25), + Name = "truncateSpeechCheckBox" + }; _voiceTab.Controls.AddRange(new Control[] { @@ -656,7 +668,8 @@ private void CreateVoiceTab() callModeLabel, micLabel, _microphoneComboBox, confidenceLabel, _confidenceTrackBar, _confidenceValueLabel, confidenceHint, - silenceLabel, _silenceTrackBar, _silenceValueLabel, silenceHint + silenceLabel, _silenceTrackBar, _silenceValueLabel, silenceHint, + _truncateSpeechCheckBox }); } @@ -1334,6 +1347,11 @@ private void LoadSettings() // Agent size _agentSizeTrackBar.Value = Math.Max(_agentSizeTrackBar.Minimum, Math.Min(_agentSizeTrackBar.Maximum, _settings.AgentSize)); _agentSizeValueLabel.Text = _agentSizeTrackBar.Value.ToString() + "%"; + + // Truncate speech + var truncateSpeechCheckBox = _voiceTab.Controls.Find("truncateSpeechCheckBox", false).FirstOrDefault() as CheckBox; + if (truncateSpeechCheckBox != null) + truncateSpeechCheckBox.Checked = _settings.TruncateSpeech; // Ollama settings _ollamaUrlTextBox.Text = _settings.OllamaUrl; @@ -1431,6 +1449,11 @@ private void SaveSettings() // Agent size _settings.AgentSize = _agentSizeTrackBar.Value; + + // Truncate speech + var truncateSpeechCheckBox = _voiceTab.Controls.Find("truncateSpeechCheckBox", false).FirstOrDefault() as CheckBox; + if (truncateSpeechCheckBox != null) + _settings.TruncateSpeech = truncateSpeechCheckBox.Checked; // Ollama settings _settings.OllamaUrl = _ollamaUrlTextBox.Text; From 9fed1285988aeadbf0d4b1880d0ff20a852c73b4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 19 Jan 2026 01:09:03 +0000 Subject: [PATCH 3/5] Address code review feedback: fix UI thread blocking and magic strings Co-authored-by: ExtCan <60326708+ExtCan@users.noreply.github.com> --- src/Agent/AgentManager.cs | 9 +++------ src/UI/SettingsForm.cs | 14 +++++--------- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/src/Agent/AgentManager.cs b/src/Agent/AgentManager.cs index 9f5b68d..cee0601 100644 --- a/src/Agent/AgentManager.cs +++ b/src/Agent/AgentManager.cs @@ -606,17 +606,14 @@ public void SpeakSentences(List sentences, int delayMs = 500) if (sentences == null || sentences.Count == 0) return; - // Speak each sentence with a small delay between them + // MS Agent Speak queues the text, so we can just call Speak for each sentence + // and it will display them sequentially. The delay parameter is kept for future + // enhancements but not used to avoid blocking the UI thread. foreach (var sentence in sentences) { if (!string.IsNullOrEmpty(sentence)) { _character.Speak(sentence, null); - // Add a small pause between sentences - if (delayMs > 0) - { - System.Threading.Thread.Sleep(delayMs); - } } } } diff --git a/src/UI/SettingsForm.cs b/src/UI/SettingsForm.cs index 6fbd612..b55d060 100644 --- a/src/UI/SettingsForm.cs +++ b/src/UI/SettingsForm.cs @@ -66,6 +66,7 @@ public class SettingsForm : Form private Label _confidenceValueLabel; private TrackBar _silenceTrackBar; private Label _silenceValueLabel; + private CheckBox _truncateSpeechCheckBox; // Ollama controls private TextBox _ollamaUrlTextBox; @@ -650,12 +651,11 @@ private void CreateVoiceTab() }; // Truncate speech checkbox - CheckBox _truncateSpeechCheckBox = new CheckBox + _truncateSpeechCheckBox = new CheckBox { Text = "Sentence-by-sentence speech (truncate long speeches into separate bubbles)", Location = new Point(15, 370), - Size = new Size(550, 25), - Name = "truncateSpeechCheckBox" + Size = new Size(550, 25) }; _voiceTab.Controls.AddRange(new Control[] @@ -1349,9 +1349,7 @@ private void LoadSettings() _agentSizeValueLabel.Text = _agentSizeTrackBar.Value.ToString() + "%"; // Truncate speech - var truncateSpeechCheckBox = _voiceTab.Controls.Find("truncateSpeechCheckBox", false).FirstOrDefault() as CheckBox; - if (truncateSpeechCheckBox != null) - truncateSpeechCheckBox.Checked = _settings.TruncateSpeech; + _truncateSpeechCheckBox.Checked = _settings.TruncateSpeech; // Ollama settings _ollamaUrlTextBox.Text = _settings.OllamaUrl; @@ -1451,9 +1449,7 @@ private void SaveSettings() _settings.AgentSize = _agentSizeTrackBar.Value; // Truncate speech - var truncateSpeechCheckBox = _voiceTab.Controls.Find("truncateSpeechCheckBox", false).FirstOrDefault() as CheckBox; - if (truncateSpeechCheckBox != null) - _settings.TruncateSpeech = truncateSpeechCheckBox.Checked; + _settings.TruncateSpeech = _truncateSpeechCheckBox.Checked; // Ollama settings _settings.OllamaUrl = _ollamaUrlTextBox.Text; From 2c36ac6a2d132df9ab91b8b28bf1fc84a5fe7444 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 19 Jan 2026 01:11:25 +0000 Subject: [PATCH 4/5] Clean up code: use Regex shorthand and remove unused delayMs parameter Co-authored-by: ExtCan <60326708+ExtCan@users.noreply.github.com> --- src/Agent/AgentManager.cs | 7 +++---- src/Config/AppSettings.cs | 18 ++++++++---------- src/UI/ChatForm.cs | 2 +- src/UI/MainForm.cs | 2 +- 4 files changed, 13 insertions(+), 16 deletions(-) diff --git a/src/Agent/AgentManager.cs b/src/Agent/AgentManager.cs index cee0601..469e4a7 100644 --- a/src/Agent/AgentManager.cs +++ b/src/Agent/AgentManager.cs @@ -598,17 +598,16 @@ public void Speak(string text) } /// - /// Makes the character speak text sentence by sentence with delays + /// Makes the character speak text sentence by sentence /// - public void SpeakSentences(List sentences, int delayMs = 500) + public void SpeakSentences(List sentences) { EnsureLoaded(); if (sentences == null || sentences.Count == 0) return; // MS Agent Speak queues the text, so we can just call Speak for each sentence - // and it will display them sequentially. The delay parameter is kept for future - // enhancements but not used to avoid blocking the UI thread. + // and it will display them sequentially in separate speech bubbles. foreach (var sentence in sentences) { if (!string.IsNullOrEmpty(sentence)) diff --git a/src/Config/AppSettings.cs b/src/Config/AppSettings.cs index eab2eae..eb09285 100644 --- a/src/Config/AppSettings.cs +++ b/src/Config/AppSettings.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Drawing; using System.IO; +using System.Text.RegularExpressions; using Newtonsoft.Json; namespace MSAgentAI.Config @@ -252,15 +253,15 @@ public string ProcessText(string text) { // Use word boundaries (\b) to match WHOLE words only, not substrings // This prevents "AI" from matching inside "Entertaining" - string pattern = @"\b" + System.Text.RegularExpressions.Regex.Escape(entry.Key) + @"\b"; + string pattern = @"\b" + Regex.Escape(entry.Key) + @"\b"; // The \map\ command REPLACES the word with the pronunciation // Format: \map="anim-ay"="anime"\ (replaces the word entirely) - text = System.Text.RegularExpressions.Regex.Replace( + text = Regex.Replace( text, pattern, match => $"\\map=\"{entry.Value}\"=\"{match.Value}\"\\", - System.Text.RegularExpressions.RegexOptions.IgnoreCase); + RegexOptions.IgnoreCase); } } } @@ -288,14 +289,14 @@ public static (string text, List animations) ExtractAnimationTriggers(st if (string.IsNullOrEmpty(text)) return (text, animations); - var matches = System.Text.RegularExpressions.Regex.Matches(text, @"&&(\w+)"); - foreach (System.Text.RegularExpressions.Match match in matches) + var matches = Regex.Matches(text, @"&&(\w+)"); + foreach (Match match in matches) { animations.Add(match.Groups[1].Value); } // Remove animation triggers from text - text = System.Text.RegularExpressions.Regex.Replace(text, @"&&\w+\s*", "").Trim(); + text = Regex.Replace(text, @"&&\w+\s*", "").Trim(); return (text, animations); } @@ -311,10 +312,7 @@ public static List SplitIntoSentences(string text) // Split by common sentence endings: period, exclamation, question mark // Also handle ellipsis (...) as a sentence boundary - var parts = System.Text.RegularExpressions.Regex.Split( - text, - @"(?<=[.!?])\s+|(?<=\.\.\.)\s+" - ); + var parts = Regex.Split(text, @"(?<=[.!?])\s+|(?<=\.\.\.)\s+"); foreach (var part in parts) { diff --git a/src/UI/ChatForm.cs b/src/UI/ChatForm.cs index 1a7ea73..a33fd9a 100644 --- a/src/UI/ChatForm.cs +++ b/src/UI/ChatForm.cs @@ -292,7 +292,7 @@ private void SpeakWithAnimations(string text, string defaultAnimation = null) if (_settings.TruncateSpeech) { var sentences = AppSettings.SplitIntoSentences(cleanText); - _agentManager.SpeakSentences(sentences, 500); + _agentManager.SpeakSentences(sentences); } else { diff --git a/src/UI/MainForm.cs b/src/UI/MainForm.cs index f345da3..c51433f 100644 --- a/src/UI/MainForm.cs +++ b/src/UI/MainForm.cs @@ -385,7 +385,7 @@ private void SpeakWithAnimations(string text, string defaultAnimation = null) if (_settings.TruncateSpeech) { var sentences = AppSettings.SplitIntoSentences(cleanText); - _agentManager.SpeakSentences(sentences, 500); + _agentManager.SpeakSentences(sentences); } else { From e64862047f3e3af4c8f7403c2e43153462521b0e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 19 Jan 2026 01:12:53 +0000 Subject: [PATCH 5/5] Improve sentence splitting to handle ellipsis at end of text Co-authored-by: ExtCan <60326708+ExtCan@users.noreply.github.com> --- src/Config/AppSettings.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Config/AppSettings.cs b/src/Config/AppSettings.cs index eb09285..d7d3795 100644 --- a/src/Config/AppSettings.cs +++ b/src/Config/AppSettings.cs @@ -312,7 +312,9 @@ public static List SplitIntoSentences(string text) // Split by common sentence endings: period, exclamation, question mark // Also handle ellipsis (...) as a sentence boundary - var parts = Regex.Split(text, @"(?<=[.!?])\s+|(?<=\.\.\.)\s+"); + // Note: This is a simple implementation that may not handle all edge cases + // (e.g., abbreviations like "Dr." or decimal numbers like "3.14") + var parts = Regex.Split(text, @"(?<=[.!?])\s+|(?<=\.\.\.)\s*"); foreach (var part in parts) {