diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 00eb857..5013e6b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -90,6 +90,7 @@ jobs: $wpfClasses = @( "ClipboardManagerWindowBehaviorTests", "CommandButtonStyleTests", + "FileSorterWindowBehaviorTests", "FormControlHeightTests", "MainWindowIconConverterOrientationTests", "ZenEditorWindowBehaviorTests", @@ -111,6 +112,7 @@ jobs: $wpfClasses = @( "ClipboardManagerWindowBehaviorTests", "CommandButtonStyleTests", + "FileSorterWindowBehaviorTests", "FormControlHeightTests", "MainWindowIconConverterOrientationTests", "ZenEditorWindowBehaviorTests", diff --git a/AiteBar.Tests/ActionServiceTests.cs b/AiteBar.Tests/ActionServiceTests.cs index 02bac30..4c370c0 100644 --- a/AiteBar.Tests/ActionServiceTests.cs +++ b/AiteBar.Tests/ActionServiceTests.cs @@ -574,6 +574,69 @@ public async Task StartDownloadsAsync_LaunchesDownloadsShell() Assert.True(runtime.StartedProcessInfos[0].UseShellExecute); } + [Theory] + [InlineData("Search")] + [InlineData("Screenshot")] + [InlineData("Record")] + [InlineData("Calculator")] + [InlineData("Explorer")] + [InlineData("Downloads")] + [InlineData("ShowDesktop")] + [InlineData("AppsFolder")] + public async Task SystemUtilityLaunch_WhenRuntimeReturnsNull_ThrowsLocalizedFailure(string utility) + { + var runtime = new FakeActionServiceRuntime(); + runtime.ProcessesToReturn.Enqueue(null); + var service = new ActionService(new AppSettingsService(), runtime); + + Task launch = utility switch + { + "Search" => service.StartSearchAsync("release review"), + "Screenshot" => service.StartScreenshotAsync(), + "Record" => service.StartRecordVideoAsync(), + "Calculator" => service.StartCalculatorAsync(), + "Explorer" => service.StartExplorerAsync(), + "Downloads" => service.StartDownloadsAsync(), + "ShowDesktop" => service.StartShowDesktopAsync(), + "AppsFolder" => service.StartAppsFolderAsync(), + _ => throw new ArgumentOutOfRangeException(nameof(utility)) + }; + + InvalidOperationException exception = await Assert.ThrowsAsync(() => launch); + Assert.False(string.IsNullOrWhiteSpace(exception.Message)); + } + + [Fact] + public async Task StartCopilotAsync_SendsWinCAndReleasesKeys() + { + var runtime = new FakeActionServiceRuntime(); + var service = new ActionService(new AppSettingsService(), runtime); + + await service.StartCopilotAsync(); + + Assert.Equal(4, runtime.SendInputCalls.Count); + Assert.Equal(NativeMethods.VK_LWIN, runtime.SendInputCalls[0][0].U.ki.wVk); + Assert.Equal(0x43, runtime.SendInputCalls[1][0].U.ki.wVk); + Assert.Equal(NativeMethods.KEYEVENTF_KEYUP, runtime.SendInputCalls[2][0].U.ki.dwFlags); + Assert.Equal(NativeMethods.VK_LWIN, runtime.SendInputCalls[3][0].U.ki.wVk); + Assert.Equal(NativeMethods.KEYEVENTF_KEYUP, runtime.SendInputCalls[3][0].U.ki.dwFlags); + } + + [Fact] + public async Task StartCopilotAsync_WhenCKeyInjectionFails_ReleasesWinKey() + { + var runtime = new FakeActionServiceRuntime(); + runtime.SendInputResults.Enqueue(1); + runtime.SendInputResults.Enqueue(0); + var service = new ActionService(new AppSettingsService(), runtime); + + await Assert.ThrowsAsync(() => service.StartCopilotAsync()); + + Assert.Equal(3, runtime.SendInputCalls.Count); + Assert.Equal(NativeMethods.VK_LWIN, runtime.SendInputCalls[2][0].U.ki.wVk); + Assert.Equal(NativeMethods.KEYEVENTF_KEYUP, runtime.SendInputCalls[2][0].U.ki.dwFlags); + } + [Fact] diff --git a/AiteBar.Tests/AiProviderTests.cs b/AiteBar.Tests/AiProviderTests.cs index ca50a5b..16fae55 100644 --- a/AiteBar.Tests/AiProviderTests.cs +++ b/AiteBar.Tests/AiProviderTests.cs @@ -531,13 +531,15 @@ public async Task Gateway_LegacyMethods_PreserveExactLegacyRouteOrder() settingsService.Settings.Ai.Connections[2].PreferredModelId = "apple"; var gateway = new AiGateway(settingsService, client, TimeProvider.System); - await Assert.ThrowsAsync(() => + NoAvailableConnectionException exception = await Assert.ThrowsAsync(() => gateway.GenerateAsync(new AiChatRequest { Messages = [new AiChatMessage("user", "hello")], RequireFreeModel = true })); + Assert.Equal(AiAvailabilityFailureReason.RateLimited, exception.Reason); + Assert.Equal( [ "alpha:zebra", "gamma:zebra", diff --git a/AiteBar.Tests/AiStreamingTests.cs b/AiteBar.Tests/AiStreamingTests.cs index db57139..e5bce65 100644 --- a/AiteBar.Tests/AiStreamingTests.cs +++ b/AiteBar.Tests/AiStreamingTests.cs @@ -147,6 +147,70 @@ public async Task Gateway_MarksConnectionUnavailableWhenStartedStreamFails() Assert.Equal(AiConnectionState.Unavailable, gateway.GetConnectionStatus("test")?.State); } + [Fact] + public async Task TextProcessingGateway_SkipsEmptyStreamAndUsesNextConnection() + { + var credentials = new MemoryCredentialStore(); + credentials.Write("AiteBar/AI/first", "key-first"); + credentials.Write("AiteBar/AI/second", "key-second"); + var generationAttempts = new List(); + var handler = new DelegateHandler(request => + { + if (request.Method == HttpMethod.Get) + { + return Task.FromResult(Json( + "{\"data\":[{\"id\":\"llama-3.3-70b-versatile\",\"name\":\"Llama 3.3 70B\"}]}")); + } + + string key = request.Headers.Authorization?.Parameter ?? string.Empty; + generationAttempts.Add(key); + return Task.FromResult(key == "key-first" + ? Sse("data: [DONE]\n\n") + : Sse("data: {\"choices\":[{\"delta\":{\"content\":\"исправлено\"}}]}\n\ndata: [DONE]\n\n")); + }); + var settings = new AppSettingsService + { + Settings = new AppSettings + { + Ai = new AiSettings + { + FreeTierOnly = true, + ProviderOrder = ["groq"], + Connections = + [ + new AiConnectionSettings + { + Id = "first", + ProviderId = "groq", + DisplayName = "First", + CredentialTarget = "AiteBar/AI/first" + }, + new AiConnectionSettings + { + Id = "second", + ProviderId = "groq", + DisplayName = "Second", + CredentialTarget = "AiteBar/AI/second" + } + ] + } + } + }; + var gateway = new AiGateway( + settings, + new AiProviderClient(new HttpClient(handler), credentials), + TimeProvider.System); + + AiGatewayStream stream = await gateway.GenerateTextProcessingStreamingAsync(Request()); + + Assert.Equal("second", stream.ConnectionId); + Assert.Equal("исправлено", await CollectAsync(stream.Chunks)); + Assert.Equal(["key-first", "key-second"], generationAttempts); + Assert.Equal( + AiConnectionState.Unavailable, + gateway.GetQuotaStatus(settings.Settings.Ai.Connections[0], "llama-3.3-70b-versatile")?.State); + } + [Fact] public async Task StreamRead_ThrowsTimeoutAfterConfiguredInactivity() { diff --git a/AiteBar.Tests/AppSettingsLayoutContractTests.cs b/AiteBar.Tests/AppSettingsLayoutContractTests.cs index 5365430..bef72ab 100644 --- a/AiteBar.Tests/AppSettingsLayoutContractTests.cs +++ b/AiteBar.Tests/AppSettingsLayoutContractTests.cs @@ -16,7 +16,7 @@ public sealed class AppSettingsLayoutContractTests "SliderActivationZone", "LblActivationZone10", "LblActivationZone30", "LblActivationZone50", "LblActivationZone100", "SliderActivationDelay", "LblActivationDelay100", "LblActivationDelay200", "LblActivationDelay300", "LblActivationDelay500", "TxtAboutVersion", "SettingsFooter", "BtnKeepOnTop", "AiConnectionsList", "TxtAiConnectionsEmpty", - "ChkShowTaskbarPositionIndicator", "ChkSecondaryMonitor", "ChkCheckForUpdatesEnabled", + "ChkShowPanelOnMouseHover", "ChkShowTaskbarPositionIndicator", "ChkSecondaryMonitor", "ChkCheckForUpdatesEnabled", "PanelContextsList", "HotkeyShowPanel", "HotkeyNextContext", "HotkeyPreviousContext", "HotkeyAddButton", "HotkeyFileSorter", "HotkeyIconConverter", "HotkeyQuickNote", "HotkeyColorPicker", "HotkeyTimerStopwatch", "HotkeyQRCodeGenerator", "HotkeyClipboardManager", @@ -61,6 +61,23 @@ public void ExistingSettingsHandlers_RemainWired() AssertHandlerCount(window, "BtnSave_Click", 1); } + [Fact] + public void MouseHoverSwitch_AppearsImmediatelyBeforePanelIndicatorSwitch() + { + XDocument window = LoadWindow(); + XElement hoverSwitch = FindNamedElement(window, "ChkShowPanelOnMouseHover"); + XElement indicatorSwitch = FindNamedElement(window, "ChkShowTaskbarPositionIndicator"); + XElement settingsCard = Assert.IsType(hoverSwitch.Parent?.Parent?.Parent); + XElement[] namedSwitches = settingsCard + .Descendants(PresentationNamespace + "CheckBox") + .Where(element => element.Attribute(XamlNamespace + "Name") != null) + .ToArray(); + + int hoverIndex = Array.IndexOf(namedSwitches, hoverSwitch); + int indicatorIndex = Array.IndexOf(namedSwitches, indicatorSwitch); + Assert.Equal(hoverIndex + 1, indicatorIndex); + } + [Fact] public void ModernControls_ReplaceLegacyEditorsAndPreserveStagedSwitches() { diff --git a/AiteBar.Tests/AppSettingsServiceTests.cs b/AiteBar.Tests/AppSettingsServiceTests.cs index 768701b..cccfef1 100644 --- a/AiteBar.Tests/AppSettingsServiceTests.cs +++ b/AiteBar.Tests/AppSettingsServiceTests.cs @@ -173,6 +173,29 @@ public async Task LoadAsync_OversizedSettingsFile_IsRejectedAndDefaultsAreUsed() } } + [Fact] + public async Task LoadAsync_LegacySettingsWithoutMouseHoverOption_PreservesHoverActivation() + { + string root = Path.Combine(Path.GetTempPath(), "AiteBarTests", Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(root); + string settingsPath = Path.Combine(root, "settings.json"); + string configPath = Path.Combine(root, "custom_buttons.json"); + + try + { + await File.WriteAllTextAsync(settingsPath, "{}"); + var service = new AppSettingsService(configPath, settingsPath); + + await service.LoadAsync(); + + Assert.True(service.Settings.ShowPanelOnMouseHover); + } + finally + { + Directory.Delete(root, recursive: true); + } + } + [Fact] public void GetBackupFilePath_ReturnsCorrectPath() { @@ -1004,6 +1027,7 @@ public void CloneAppSettings_CopiesAllProperties() UiCulture = "de", ActiveContextId = "context-3", CheckForUpdatesEnabled = false, + ShowPanelOnMouseHover = false, ShowTaskbarPositionIndicator = false, TaskbarIndicatorPositionX = 0.75, TaskbarIndicatorPositionY = 0.25, @@ -1132,6 +1156,7 @@ public void CloneAppSettings_CopiesAllProperties() Assert.Equal("de", clone.UiCulture); Assert.Equal("context-3", clone.ActiveContextId); Assert.False(clone.CheckForUpdatesEnabled); + Assert.False(clone.ShowPanelOnMouseHover); Assert.False(clone.ShowTaskbarPositionIndicator); Assert.Equal(0.75, clone.TaskbarIndicatorPositionX); Assert.Equal(0.25, clone.TaskbarIndicatorPositionY); diff --git a/AiteBar.Tests/CommandButtonStyleTests.cs b/AiteBar.Tests/CommandButtonStyleTests.cs index b5ce613..8c9139c 100644 --- a/AiteBar.Tests/CommandButtonStyleTests.cs +++ b/AiteBar.Tests/CommandButtonStyleTests.cs @@ -86,6 +86,7 @@ await RunStaAsync(() => [Theory] [InlineData("TimerStopwatchWindow.xaml", "name", "BtnStartPause", "PrimaryCommandButtonStyle")] [InlineData("TimerStopwatchWindow.xaml", "name", "BtnReset", "CommandButtonStyle")] + [InlineData("FileSorterWindow.xaml", "name", "BtnAddFolder", "CommandButtonStyle")] [InlineData("FileSorterWindow.xaml", "name", "BtnSort", "PrimaryCommandButtonStyle")] [InlineData("IconConverterWindow.xaml", "click", "BtnChoose_Click", "CommandButtonStyle")] [InlineData("IconConverterWindow.xaml", "name", "BtnSave", "PrimaryCommandButtonStyle")] @@ -158,6 +159,35 @@ public void TimerCompactButtons_ResetInheritedCommandPaddingSoGlyphsRemainVisibl } } + [Fact] + public void MainPanelButtons_HaveVisibleKeyboardFocusGeometry() + { + XDocument window = LoadXaml("MainWindow.xaml"); + XElement[] focusChromeBorders = window + .Descendants(PresentationNamespace + "Border") + .Where(element => string.Equals( + element.Attribute(XamlNamespace + "Name")?.Value, + "FocusChrome", + StringComparison.Ordinal)) + .ToArray(); + + Assert.Equal(2, focusChromeBorders.Length); + Assert.All(focusChromeBorders, border => + { + Assert.Equal("1", border.Attribute("BorderThickness")?.Value); + Assert.Equal("Transparent", border.Attribute("BorderBrush")?.Value); + }); + + Assert.Equal(2, window.Descendants(PresentationNamespace + "MultiTrigger").Count(trigger => + trigger.Descendants(PresentationNamespace + "Condition").Any(condition => + condition.Attribute("Property")?.Value == "local:KeyboardFocusVisualService.ShowKeyboardFocusCue" && + condition.Attribute("Value")?.Value == "True") && + trigger.Descendants(PresentationNamespace + "Setter").Any(setter => + setter.Attribute("TargetName")?.Value == "FocusChrome" && + setter.Attribute("Property")?.Value == "BorderBrush" && + setter.Attribute("Value")?.Value == "#3ABEFF"))); + } + [Fact] public async Task TimerCompactButtonTemplate_LeavesMeasuredSpaceForGlyph() { diff --git a/AiteBar.Tests/FileSorterServiceTests.cs b/AiteBar.Tests/FileSorterServiceTests.cs index bb633f1..5367316 100644 --- a/AiteBar.Tests/FileSorterServiceTests.cs +++ b/AiteBar.Tests/FileSorterServiceTests.cs @@ -409,6 +409,214 @@ public async Task UndoLastSort_ReturnsRemainingEntriesWhenDestinationIsMissing() } } + [Fact] + public async Task SortMultipleFoldersAsync_SortsBothFolders() + { + string root1 = CreateTempRoot(); + string root2 = CreateTempRoot(); + try + { + File.WriteAllText(Path.Combine(root1, "photo.jpg"), "1"); + File.WriteAllText(Path.Combine(root1, "doc.pdf"), "1"); + File.WriteAllText(Path.Combine(root2, "track.mp3"), "1"); + File.WriteAllText(Path.Combine(root2, "archive.zip"), "1"); + + var service = new FileSorterService(); + MultiFileSortResult result = await service.SortMultipleFoldersAsync([root1, root2]); + + Assert.Equal(2, result.PerFolder.Count); + Assert.Equal(4, result.TotalSorted); + Assert.Equal(2, result.PerFolder[0].SortedCount); + Assert.Equal(2, result.PerFolder[1].SortedCount); + + Assert.Empty(Directory.GetFiles(root1)); + Assert.Empty(Directory.GetFiles(root2)); + Assert.Equal(2, Directory.GetDirectories(root1).Length); + Assert.Equal(2, Directory.GetDirectories(root2).Length); + Assert.Equal(2, Directory.GetFiles(root1, "*", SearchOption.AllDirectories).Length); + Assert.Equal(2, Directory.GetFiles(root2, "*", SearchOption.AllDirectories).Length); + } + finally + { + Directory.Delete(root1, true); + Directory.Delete(root2, true); + } + } + + [Fact] + public async Task SortMultipleFoldersAsync_CombinedUndoStateHasPerFolder() + { + string root1 = CreateTempRoot(); + string root2 = CreateTempRoot(); + try + { + File.WriteAllText(Path.Combine(root1, "photo.jpg"), "1"); + File.WriteAllText(Path.Combine(root2, "track.mp3"), "1"); + + var service = new FileSorterService(); + MultiFileSortResult result = await service.SortMultipleFoldersAsync([root1, root2]); + + Assert.NotNull(result.CombinedUndoState); + Assert.Equal(2, result.CombinedUndoState.PerFolder.Count); + Assert.Equal(root1, result.CombinedUndoState.PerFolder[0].RootPath); + Assert.Equal(root2, result.CombinedUndoState.PerFolder[1].RootPath); + } + finally + { + Directory.Delete(root1, true); + Directory.Delete(root2, true); + } + } + + [Fact] + public async Task UndoMultipleAsync_RestoresFilesInBothFolders() + { + string root1 = CreateTempRoot(); + string root2 = CreateTempRoot(); + try + { + string f1 = Path.Combine(root1, "photo.jpg"); + string f2 = Path.Combine(root2, "track.mp3"); + File.WriteAllText(f1, "1"); + File.WriteAllText(f2, "1"); + MakeOld(f1); + MakeOld(f2); + + var service = new FileSorterService(); + MultiFileSortResult sortResult = await service.SortMultipleFoldersAsync([root1, root2]); + + MultiFileSortUndoResult undoResult = await service.UndoMultipleAsync(sortResult.CombinedUndoState!); + + Assert.Equal(2, undoResult.TotalRestored); + Assert.Equal(0, undoResult.TotalSkipped); + Assert.Null(undoResult.RemainingUndoState); + Assert.True(File.Exists(f1)); + Assert.True(File.Exists(f2)); + } + finally + { + Directory.Delete(root1, true); + Directory.Delete(root2, true); + } + } + + [Fact] + public async Task SortMultipleFoldersAsync_ProgressReportsEachFolder() + { + string root1 = CreateTempRoot(); + string root2 = CreateTempRoot(); + try + { + File.WriteAllText(Path.Combine(root1, "photo.jpg"), "1"); + File.WriteAllText(Path.Combine(root2, "track.mp3"), "1"); + + var reports = new List(); + var progress = new SynchronousProgress(reports.Add); + + var service = new FileSorterService(); + await service.SortMultipleFoldersAsync([root1, root2], progress); + + Assert.Contains(reports, report => report.RootPath == root1 && report.FolderIndex == 0 && report.FolderCount == 2); + Assert.Contains(reports, report => report.RootPath == root2 && report.FolderIndex == 1 && report.FolderCount == 2); + Assert.Equal(1, reports.Last(report => report.RootPath == root1).ProcessedFiles); + Assert.Equal(1, reports.Last(report => report.RootPath == root2).ProcessedFiles); + } + finally + { + Directory.Delete(root1, true); + Directory.Delete(root2, true); + } + } + + [Fact] + public async Task SortMultipleFoldersAsync_EmptyInput_ReturnsEmptyResult() + { + var service = new FileSorterService(); + MultiFileSortResult result = await service.SortMultipleFoldersAsync([]); + + Assert.Empty(result.PerFolder); + Assert.Equal(0, result.TotalSorted); + Assert.Null(result.CombinedUndoState); + } + + [Fact] + public async Task SortMultipleFoldersAsync_LaterFolderFails_ExposesUndoForCompletedFolders() + { + string root = CreateTempRoot(); + string missingRoot = Path.Combine(root, "missing"); + string sourcePath = Path.Combine(root, "photo.jpg"); + try + { + File.WriteAllText(sourcePath, "1"); + + var service = new FileSorterService(); + MultiFileSortException exception = await Assert.ThrowsAsync( + () => service.SortMultipleFoldersAsync([root, missingRoot])); + + Assert.Equal(missingRoot, exception.FailedRootPath); + Assert.Single(exception.PartialResult.PerFolder); + Assert.NotNull(exception.PartialResult.CombinedUndoState); + Assert.False(File.Exists(sourcePath)); + + MultiFileSortUndoResult undoResult = await service.UndoMultipleAsync( + exception.PartialResult.CombinedUndoState); + + Assert.Equal(1, undoResult.TotalRestored); + Assert.True(File.Exists(sourcePath)); + } + finally + { + Directory.Delete(root, true); + } + } + + [Fact] + public async Task SortFilesAsync_ProgressReportsProcessedAndTotalFiles() + { + string root = CreateTempRoot(); + try + { + File.WriteAllText(Path.Combine(root, "photo.jpg"), "1"); + File.WriteAllText(Path.Combine(root, "document.pdf"), "1"); + File.WriteAllText(Path.Combine(root, "archive.zip"), "1"); + var reports = new List(); + + var service = new FileSorterService(); + await service.SortFilesAsync(root, new SynchronousProgress(reports.Add)); + + Assert.Equal(4, reports.Count); + Assert.Equal(0, reports[0].ProcessedFiles); + Assert.All(reports, report => Assert.Equal(3, report.TotalFiles)); + Assert.Equal([0, 1, 2, 3], reports.Select(report => report.ProcessedFiles)); + } + finally + { + Directory.Delete(root, true); + } + } + + [Fact] + public async Task SortFilesAsync_EmptyFolder_ReportsCompletedZeroProgress() + { + string root = CreateTempRoot(); + try + { + var reports = new List(); + + var service = new FileSorterService(); + await service.SortFilesAsync(root, new SynchronousProgress(reports.Add)); + + FileSortProgress report = Assert.Single(reports); + Assert.Equal(root, report.RootPath); + Assert.Equal(0, report.ProcessedFiles); + Assert.Equal(0, report.TotalFiles); + } + finally + { + Directory.Delete(root, true); + } + } + private static string CreateTempRoot() { string root = Path.Combine(Path.GetTempPath(), "AiteBarTests", nameof(FileSorterServiceTests), Guid.NewGuid().ToString("N")); @@ -422,4 +630,9 @@ private static void MakeOld(string path) File.SetCreationTimeUtc(path, oldTime); File.SetLastWriteTimeUtc(path, oldTime); } + + private sealed class SynchronousProgress(Action callback) : IProgress + { + public void Report(T value) => callback(value); + } } diff --git a/AiteBar.Tests/FileSorterUndoStateHelperTests.cs b/AiteBar.Tests/FileSorterUndoStateHelperTests.cs new file mode 100644 index 0000000..eeb00d1 --- /dev/null +++ b/AiteBar.Tests/FileSorterUndoStateHelperTests.cs @@ -0,0 +1,69 @@ +using System; +using System.IO; + +namespace AiteBar.Tests; + +public sealed class FileSorterUndoStateHelperTests +{ + [Fact] + public void Merge_ReplacesChangedFolderAndPreservesUnrelatedUndo() + { + string desktop = Path.Combine(Path.GetTempPath(), "Desktop"); + string downloads = Path.Combine(Path.GetTempPath(), "Downloads"); + FileSortUndoState oldDesktop = CreateState(desktop, "old.jpg"); + FileSortUndoState downloadsState = CreateState(downloads, "track.mp3"); + FileSortUndoState newDesktop = CreateState(desktop + Path.DirectorySeparatorChar, "new.pdf"); + + List merged = FileSorterUndoStateHelper.Merge( + [oldDesktop, downloadsState], + [new FileSortResult { RootPath = desktop, UndoState = newDesktop }]); + + Assert.Equal(2, merged.Count); + Assert.Same(downloadsState, FileSorterUndoStateHelper.Find(merged, downloads)); + Assert.Same(newDesktop, FileSorterUndoStateHelper.Find(merged, desktop)); + } + + [Fact] + public void Merge_NoOpResultKeepsPreviousUsableUndo() + { + string root = Path.Combine(Path.GetTempPath(), "Downloads"); + FileSortUndoState previous = CreateState(root, "photo.jpg"); + + List merged = FileSorterUndoStateHelper.Merge( + [previous], + [new FileSortResult { RootPath = root, UndoState = null }]); + + Assert.Same(previous, Assert.Single(merged)); + } + + [Fact] + public void Replace_RemovesOnlyRequestedFolder() + { + string desktop = Path.Combine(Path.GetTempPath(), "Desktop"); + string downloads = Path.Combine(Path.GetTempPath(), "Downloads"); + FileSortUndoState desktopState = CreateState(desktop, "photo.jpg"); + FileSortUndoState downloadsState = CreateState(downloads, "track.mp3"); + + List updated = FileSorterUndoStateHelper.Replace( + [desktopState, downloadsState], + desktop, + replacement: null); + + Assert.Same(downloadsState, Assert.Single(updated)); + } + + private static FileSortUndoState CreateState(string rootPath, string fileName) => + new() + { + RootPath = rootPath, + CompletedAtUtc = DateTime.UtcNow, + Entries = + [ + new FileSortOperationEntry + { + SourcePath = Path.Combine(rootPath, fileName), + DestinationPath = Path.Combine(rootPath, "Category", fileName) + } + ] + }; +} diff --git a/AiteBar.Tests/FileSorterWindowBehaviorTests.cs b/AiteBar.Tests/FileSorterWindowBehaviorTests.cs new file mode 100644 index 0000000..0ea05cf --- /dev/null +++ b/AiteBar.Tests/FileSorterWindowBehaviorTests.cs @@ -0,0 +1,190 @@ +using System.IO; +using System.Reflection; +using System.Threading; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Threading; + +namespace AiteBar.Tests; + +[Collection("WpfTestCollection")] +public sealed class FileSorterWindowBehaviorTests +{ + [Fact] + public async Task Window_BuildsSingleScreenRowsWithPerFolderActions() + { + EnsureApplicationResources(); + await RunStaAsync(() => + { + string desktopPath = Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory); + var settingsService = new AppSettingsService( + Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".config.json"), + Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".settings.json")) + { + Settings = new AppSettings + { + LastMultiFileSortOperation = new MultiFileSortUndoState + { + PerFolder = [CreateUndoState(desktopPath)] + } + } + }; + + var window = new FileSorterWindow(settingsService); + try + { + Assert.Equal(520, window.Width); + StackPanel folderList = Assert.IsType(window.FindName("FolderListPanel")); + Grid desktopRow = Assert.IsType(folderList.Children[0]); + Grid downloadsRow = Assert.IsType(folderList.Children[2]); + + Assert.Equal(4, desktopRow.ColumnDefinitions.Count); + Assert.Equal(4, downloadsRow.ColumnDefinitions.Count); + + Button[] desktopActions = FindRowActions(desktopRow); + Button[] downloadsActions = FindRowActions(downloadsRow); + Assert.Equal(2, desktopActions.Length); + Assert.Equal(2, downloadsActions.Length); + Assert.True(desktopActions[0].IsEnabled); + Assert.False(downloadsActions[0].IsEnabled); + Assert.True(desktopActions[1].IsEnabled); + Assert.Equal("\uE7A7", desktopActions[0].Content); + Assert.Equal("\uE838", desktopActions[1].Content); + Assert.Equal("Segoe MDL2 Assets", desktopActions[0].FontFamily.Source); + + Assert.NotNull(window.FindName("TxtSelectionCount")); + Assert.NotNull(window.FindName("TxtOverallStatus")); + var addButton = Assert.IsType + + + + diff --git a/AiteBar/TextProcessingWindow.xaml.cs b/AiteBar/TextProcessingWindow.xaml.cs index 3702da1..5acb4da 100644 --- a/AiteBar/TextProcessingWindow.xaml.cs +++ b/AiteBar/TextProcessingWindow.xaml.cs @@ -50,6 +50,8 @@ public partial class TextProcessingWindow : DarkWindow private bool _isProcessing; private bool _hasClipboardText; private bool _hasEligibleModel; + private bool _hasAutomaticModel; + private bool _hasSelectableModel; private bool _hasSuccessfulResult; private bool _isShowingOriginal; private bool _isShowingDiff; @@ -81,7 +83,7 @@ public TextProcessingWindow( _settingsService = settingsService ?? throw new ArgumentNullException(nameof(settingsService)); _mainWindow = mainWindow; _gateway = new AiGateway(settingsService); - _currentMode = ParseSavedMode(settingsService.Settings.TextProcessingLastMode); + _currentMode = TextProcessingMode.Proofread; InitializeComponent(); _progressTimer = new DispatcherTimer(TimeSpan.FromSeconds(1), DispatcherPriority.Background, (_, _) => UpdateProcessingProgress(), Dispatcher); @@ -243,9 +245,9 @@ private void ModeTabs_SelectionChanged(object sender, SelectionChangedEventArgs "Proofread" => TextProcessingMode.Proofread, "Typography" => TextProcessingMode.Typography, "Cleanup" => TextProcessingMode.Cleanup, + "LiteraryEdit" => TextProcessingMode.LiteraryEdit, _ => _currentMode }; - SaveModeSelection(); ApplyModeToUi(); RefreshUiState(); } @@ -279,6 +281,7 @@ private void CmbModels_SelectionChanged(object sender, SelectionChangedEventArgs _isAutoModel = item.ModelId == null; _selectedProviderId = item.ProviderId; _selectedModelId = item.ModelId; + UpdateSelectedModelAvailability(); SaveModelSelection(); SetStatus(string.Empty); RefreshUiState(); @@ -550,6 +553,7 @@ private async Task ProcessAsync(bool repeatLast) { if (!_models.Any(model => model.ModelId != null && + model.Tier == TextProcessingModelTier.CertifiedAutomatic && (!model.ContextLength.HasValue || request.RequiredContextTokens <= model.ContextLength.Value))) { SetStatus(LocalizationService.Get("TextProcessing_ErrorContextOverflow")); @@ -610,6 +614,16 @@ private async Task ProcessAsync(bool repeatLast) SetStatus(LocalizationService.Get("TextProcessing_ErrorEmptyResponse")); return; } + if (TextProcessingService.ViolatesContentPreservation( + input, + cleaned, + protectedInput.Fragments.Values, + TextProcessingService.GetMinimumWordOverlap(mode))) + { + SetEditorText(textShownBeforeRequest); + SetStatus(LocalizationService.Get("TextProcessing_ErrorContentChanged")); + return; + } _originalText = input; _processedText = cleaned; _hasSuccessfulResult = true; @@ -650,13 +664,7 @@ private async Task ProcessAsync(bool repeatLast) { Logger.Log(ex); SetEditorText(textShownBeforeRequest); - SetStatus(ex.InnerException switch - { - AiProviderHttpException providerError => GetProviderError(providerError), - HttpRequestException => LocalizationService.Get("TextProcessing_ErrorNetwork"), - TimeoutException => LocalizationService.Get("TextProcessing_ErrorTimeout"), - _ => LocalizationService.Get("TextProcessing_ErrorNoModels") - }); + SetStatus(GetAvailabilityError(ex)); } catch (AiProviderHttpException ex) { @@ -720,6 +728,25 @@ _ when (int)ex.StatusCode >= 500 => LocalizationService.Get("TextProcessing_Erro _ => LocalizationService.Get("TextProcessing_ErrorGeneric") }; + internal static string GetAvailabilityError(NoAvailableConnectionException ex) => ex.Reason switch + { + AiAvailabilityFailureReason.NoConnectionsConfigured => LocalizationService.Get("TextProcessing_ErrorNoModels"), + AiAvailabilityFailureReason.RateLimited => LocalizationService.Get("TextProcessing_ErrorRateLimit"), + AiAvailabilityFailureReason.QuotaExhausted => LocalizationService.Get("TextProcessing_ErrorQuota"), + AiAvailabilityFailureReason.Unauthorized => LocalizationService.Get("TextProcessing_ErrorUnauthorized"), + AiAvailabilityFailureReason.Forbidden => LocalizationService.Get("TextProcessing_ErrorForbidden"), + AiAvailabilityFailureReason.Network => LocalizationService.Get("TextProcessing_ErrorNetwork"), + AiAvailabilityFailureReason.Timeout => LocalizationService.Get("TextProcessing_ErrorTimeout"), + AiAvailabilityFailureReason.TemporarilyUnavailable => LocalizationService.Get("TextProcessing_ErrorUnavailable"), + _ => ex.InnerException switch + { + AiProviderHttpException providerError => GetProviderError(providerError), + HttpRequestException => LocalizationService.Get("TextProcessing_ErrorNetwork"), + TimeoutException => LocalizationService.Get("TextProcessing_ErrorTimeout"), + _ => LocalizationService.Get("TextProcessing_ErrorUnavailable") + } + }; + private void CancelProcessing() => _processingCts?.Cancel(); private void StartProcessingProgress() @@ -908,7 +935,8 @@ private void RefreshUiState() ModeProofread.IsEnabled = state.CanSelectMode; ModeTypography.IsEnabled = state.CanSelectMode; ModeCleanup.IsEnabled = state.CanSelectMode; - CmbModels.IsEnabled = state.CanSelectModel; + ModeLiteraryEdit.IsEnabled = state.CanSelectMode; + CmbModels.IsEnabled = !_isProcessing && !_isLoadingModels && _hasSelectableModel; BtnRefreshModels.IsEnabled = !_isProcessing && !_isLoadingModels; BtnPaste.IsEnabled = state.CanPaste; BtnCopy.IsEnabled = state.CanCopy; @@ -1089,11 +1117,13 @@ private void ApplyModeToUi() ModeProofread.IsSelected = _currentMode == TextProcessingMode.Proofread; ModeTypography.IsSelected = _currentMode == TextProcessingMode.Typography; ModeCleanup.IsSelected = _currentMode == TextProcessingMode.Cleanup; + ModeLiteraryEdit.IsSelected = _currentMode == TextProcessingMode.LiteraryEdit; TxtModeDescription.Text = _currentMode switch { TextProcessingMode.Proofread => LocalizationService.Get("TextProcessing_ModeProofreadDesc"), TextProcessingMode.Typography => LocalizationService.Get("TextProcessing_ModeTypographyDesc"), TextProcessingMode.Cleanup => LocalizationService.Get("TextProcessing_ModeCleanupDesc"), + TextProcessingMode.LiteraryEdit => LocalizationService.Get("TextProcessing_ModeLiteraryEditDesc"), _ => string.Empty }; } @@ -1139,6 +1169,8 @@ private async Task LoadModelsAsync(CancellationToken cancellationToken) { _isLoadingModels = true; _hasEligibleModel = false; + _hasAutomaticModel = false; + _hasSelectableModel = false; _models.Clear(); string automaticLabel = LocalizationService.Get("TextProcessing_ModelAuto"); _models.Add(new ModelItem(null, null, automaticLabel, null) { FullDisplay = automaticLabel }); @@ -1182,8 +1214,11 @@ private async Task LoadModelsAsync(CancellationToken cancellationToken) { _models.Add(model); } - _hasEligibleModel = logicalModels.Count > 0; + _hasAutomaticModel = logicalModels.Any(model => + model.Tier == TextProcessingModelTier.CertifiedAutomatic); + _hasSelectableModel = logicalModels.Count > 0; RestoreModelSelection(); + UpdateSelectedModelAvailability(); _isLoadingModels = false; RefreshUiState(); } @@ -1193,7 +1228,7 @@ internal static bool IsEligibleModel(AiModelDescriptor model) => !model.IsDeprecated && (model.Capabilities & AiCapabilities.Text) == AiCapabilities.Text && (model.CostStatus is AiCostStatus.VerifiedFree or AiCostStatus.FreeTierAvailable) && - TextProcessingService.IsSuitableForWritingModel(model); + TextProcessingModelPolicy.Classify(model) != TextProcessingModelTier.Unsupported; private static bool HasVisibleModelText(string? value) => !string.IsNullOrEmpty(value) && value.Any(character => @@ -1241,7 +1276,12 @@ internal static IReadOnlyList BuildLogicalModelItems( return new { Identity = pair.Key, - Item = new ModelItem(first.ProviderId, first.ModelId, display, contextLength) + Item = new ModelItem( + first.ProviderId, + first.ModelId, + display, + contextLength, + TextProcessingModelPolicy.Classify(first)) }; }).ToList(); @@ -1293,18 +1333,17 @@ private bool TrySelectModel(string? providerId, string? modelId) CmbModels.SelectedItem = model; _selectedProviderId = model.ProviderId; _selectedModelId = model.ModelId; + UpdateSelectedModelAvailability(); return true; } - private static TextProcessingMode ParseSavedMode(int value) => - Enum.IsDefined(typeof(TextProcessingMode), value) - ? (TextProcessingMode)value - : TextProcessingMode.Proofread; - - private void SaveModeSelection() + private void UpdateSelectedModelAvailability() { - _settingsService.UpdateSettings(settings => - settings.TextProcessingLastMode = (int)_currentMode); + _hasEligibleModel = _isAutoModel + ? _hasAutomaticModel + : CmbModels.SelectedItem is ModelItem selected && + selected.ModelId != null && + selected.Tier != TextProcessingModelTier.Unsupported; } private void RestoreModelSelection() @@ -1406,4 +1445,4 @@ private void SaveWindowState() settings.TextProcessingWindowState = state; }); } -} \ No newline at end of file +} diff --git a/AiteBar/TimerStopwatchWindow.xaml.cs b/AiteBar/TimerStopwatchWindow.xaml.cs index 4d15d23..14def48 100644 --- a/AiteBar/TimerStopwatchWindow.xaml.cs +++ b/AiteBar/TimerStopwatchWindow.xaml.cs @@ -50,8 +50,16 @@ private async void TimerStopwatchWindow_Closed(object? sender, EventArgs e) if (_settingsService != null) { - SaveSettings(_settingsService); - await _settingsService.SaveAsync(); + try + { + SaveSettings(_settingsService); + await _settingsService.SaveAsync(); + } + catch (Exception ex) + { + Logger.Log(ex); + TelemetryService.CaptureException(ex, "timer_settings_save_failed"); + } } } diff --git a/CHANGELOG.md b/CHANGELOG.md index 46cb243..195f7ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,30 @@ ## [Unreleased] +## [1.13.0] - 2026-08-02 + +### 🇷🇺 Добавлено | 🇬🇧 Added +- **Литературная редакция текста**: В «Обработку текста» добавлен отдельный режим бережной литературной правки с собственным профессиональным промптом, настройками модели и контролем сохранения языка и содержания. +- **Literary text editing**: Text Processing now includes a dedicated conservative literary-editing mode with its own professional prompt, model settings, and language/content preservation checks. + +### 🇷🇺 Изменено | 🇬🇧 Changed +- **Управление появлением панели**: В общие настройки перед указателем положения добавлен переключатель показа панели при наведении мыши; hotkey, tray и указатель продолжают открывать панель независимо от него. +- **Panel hover activation control**: General settings now include a mouse-hover activation switch immediately before the position-indicator option; hotkey, tray, and indicator opening remain available independently. +- **Сортировка файлов — единый экран**: Рабочий стол, Загрузки и пользовательские папки собраны в одном списке с переключателем, прогрессом, результатом, откатом и открытием папки в каждой строке; основные кнопки используют единый стиль и размер. +- **File Sorter single-screen workflow**: Desktop, Downloads, and custom folders now share one list with per-row selection, progress, result, undo, and open-folder actions; the primary buttons use the same size and style. +- **Обработка текста — профессиональные инструкции**: Независимые промпты проверки, типографики, очистки и литературной правки приведены к краткому английскому формату; список моделей отфильтрован по пригодности для текстовой обработки. +- **Text Processing professional instructions**: Independent proofreading, typography, cleanup, and literary-editing prompts now use concise professional English instructions, and the model list is filtered for text-processing suitability. + +### 🇷🇺 Исправлено | 🇬🇧 Fixed +- **Главная панель — подсказки и клавиатура**: Номер активного контекста снова показывает подсказку с именем, а перемещение Tab и стрелками снова отображает синюю обводку сфокусированной кнопки. +- **Main panel hints and keyboard focus**: The active context number again shows a tooltip with its name, and Tab/arrow navigation again draws a blue outline around the focused button. +- **Обработка текста — предсказуемый результат**: Повтор всегда использует исходный текст и выбранный режим; добавлены защита от смены языка, пустого потокового ответа и технических фрагментов, а первой вкладкой снова открывается проверка текста. +- **Text Processing predictable output**: Retry always uses the original text and selected mode; safeguards now reject language changes, empty streaming responses, and modified technical fragments, while Proofreading is again the startup tab. +- **Системные утилиты — ошибки запуска**: Снимок экрана, запись, калькулятор, проводник, загрузки, показ рабочего стола и папка приложений больше не скрывают отказ Windows запустить действие; Copilot гарантированно освобождает Win-клавишу при сбое. +- **System utility launch failures**: Screenshot, recording, Calculator, Explorer, Downloads, Show Desktop, and Apps Folder no longer hide Windows launch failures; Copilot reliably releases the Win key after an injection error. +- **Надёжность и локализация утилит**: Color Picker использует локализованное аварийное сообщение, Timer/Stopwatch безопасно обрабатывает ошибку сохранения при закрытии, а новые WPF-тесты File Sorter изолированы в release pipeline. +- **Utility reliability and localization**: Color Picker now uses a localized failure dialog, Timer/Stopwatch safely contains close-time save failures, and the new File Sorter WPF tests run in isolated release hosts. + ## [1.12.2] - 2026-07-31 ### 🇷🇺 Добавлено | 🇬🇧 Added diff --git a/SECURITY.md b/SECURITY.md index 1123fdb..e8baf5d 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,10 +4,10 @@ | Version | Supported | | ------- | ------------------ | +| 1.13.x | :white_check_mark: | | 1.12.x | :white_check_mark: | | 1.11.x | :white_check_mark: | -| 1.10.x | :white_check_mark: | -| < 1.10 | :x: | +| < 1.11 | :x: | ## Reporting a Vulnerability diff --git a/docs/USER_MANUAL.md b/docs/USER_MANUAL.md index d6e8f1a..3deace8 100644 --- a/docs/USER_MANUAL.md +++ b/docs/USER_MANUAL.md @@ -88,7 +88,7 @@ AiteBar заменяет разрозненные ярлыки, закладки Показать панель можно одним из способов: -1. Подведите курсор к настроенному краю экрана. +1. Подведите курсор к настроенному краю экрана, если включена настройка показа панели при наведении мыши. 2. Нажмите левой кнопкой мыши на значок AiteBar в системном трее. 3. Нажмите левой кнопкой мыши на указатель положения панели. 4. Откройте tray-меню и выберите `Открыть`. @@ -333,7 +333,12 @@ AiteBar запомнит выбранный край и монитор. ## Обработка текста -`Обработка текста` — AI-утилита с тремя режимами: `Проверка текста`, `Типографика` и `Очистка текста`. Она предназначена для технических исправлений без пересказа, перевода или изменения смысла. +`Обработка текста` — AI-утилита с четырьмя независимыми режимами: `Проверка текста`, `Типографика`, `Очистка текста` и `Литературная редакция`. Первые три режима выполняют только технические преобразования. `Литературная редакция` улучшает ясность, ритм и стиль, но не должна переводить текст, добавлять факты или менять авторскую позицию. + +- `Проверка текста` исправляет только орфографию, грамматику и пунктуацию. +- `Типографика` нормализует кавычки, тире, пробелы и другие типографские знаки без изменения слов. +- `Очистка текста` удаляет только явные артефакты копирования и извлечения документов. +- `Литературная редакция` может улучшать неудачные формулировки и убирать ненамеренные повторы, сохраняя язык, смысл, факты, имена, авторский голос и структуру абзацев. 1. Добавьте и включите AI-подключение в настройках AiteBar. 2. Откройте утилиту с панели и вставьте или введите текст. Кнопка `Вставить` добавляет содержимое буфера в позицию курсора либо заменяет выделенный фрагмент. @@ -343,7 +348,7 @@ AiteBar запомнит выбранный край и монитор. 6. `Ctrl+Z` отменяет, а `Ctrl+Y` повторяет ручные правки и операции замены текста. 7. При необходимости используйте `Повторить`. Для явно выбранной модели AiteBar перебирает доступные API-ключи того же провайдера, но не подменяет выбранную модель другой. -Текст отправляется выбранному AI-сервису только после явного запуска. URL, e-mail, пути, код, теги, версии и идентификаторы защищаются локальными маркерами и восстанавливаются после ответа. Максимальный размер одного запроса — 50 000 символов; более длинный текст не обрезается, но обработка блокируется, пока вы его не сократите. Текст, исходник и результат не сохраняются между запусками AiteBar. Выбранные режим, модель и геометрия окна запоминаются. +Текст отправляется выбранному AI-сервису только после явного запуска. URL, e-mail, пути, код, теги, версии и идентификаторы защищаются локальными маркерами и восстанавливаются после ответа. Максимальный размер одного запроса — 50 000 символов; более длинный текст не обрезается, но обработка блокируется, пока вы его не сократите. Текст, исходник и результат не сохраняются между запусками AiteBar. При каждом новом открытии первой выбирается `Проверка текста`; выбранная модель и геометрия окна запоминаются. Подробнее см. в [Карте функций](functions.md#обработка-текста). @@ -468,6 +473,7 @@ Quick Note - это быстрая заметка с автосохранени - размер панели; - зону активации; - задержку появления; +- показывать панель при наведении мыши (включено по умолчанию; можно отключить, сохранив открытие через hotkey, tray и указатель); - показывать указатель положения панели (включено по умолчанию). ### Панели diff --git a/docs/architecture.md b/docs/architecture.md index eb954cb..8e13820 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -226,7 +226,7 @@ AiteBar — это скрываемая edge-панель быстрого до | [ActionExecutionResult.cs](../AiteBar/ActionExecutionResult.cs) | Result-тип возврата `ActionService.ExecuteAsync`: Success/FailedWithWarning/FailedCancelled/Failed + сообщение; используется UI feedback и unit-тесты. | | [PanelPackageManifest.cs](../AiteBar/PanelPackageManifest.cs) | JSON-схема файла `manifest.json` внутри `.aitebarpanel` ZIP (AppVersion, ExportedAt, Elements, Images). | | [QuickNoteContracts.cs](../AiteBar/QuickNoteContracts.cs) | Интерфейсы и shared POCO для QuickNote (используются окном, `QuickNoteService` и `QuickNotePersistence`). | -| [TextProcessingUiState.cs](../AiteBar/TextProcessingUiState.cs) + [TextProcessingMode.cs](../AiteBar/TextProcessingMode.cs) | Состояния UI и enum preset-режимов Обработки текста (Proofread / Typography / Clean / Translate / Summarize…). | +| [TextProcessingUiState.cs](../AiteBar/TextProcessingUiState.cs) + [TextProcessingMode.cs](../AiteBar/TextProcessingMode.cs) | Состояния UI и четыре preset-режима Обработки текста: Proofread, Typography, Cleanup и LiteraryEdit. | ### Native Integration Layer Низкоуровневая интеграция с Windows API через `NativeMethods.cs`, содержащий P/Invoke объявления для Win32 функций: diff --git a/docs/execplans/FILE_SORTER_SINGLE_SCREEN_EXECPLAN.md b/docs/execplans/FILE_SORTER_SINGLE_SCREEN_EXECPLAN.md new file mode 100644 index 0000000..6cbec4e --- /dev/null +++ b/docs/execplans/FILE_SORTER_SINGLE_SCREEN_EXECPLAN.md @@ -0,0 +1,179 @@ +# Convert File Sorter to a single operational screen + +This ExecPlan is a living document. The sections `Progress`, `Surprises & Discoveries`, `Decision Log`, and `Outcomes & Retrospective` must be kept up to date as work proceeds. + +This document must be maintained in accordance with `PLANS.md` at the repository root. + +## Purpose / Big Picture + +The File Sorter currently replaces its folder list with separate sorting and completion screens. After this change, the folder list always remains visible. Each folder row lets the user select the folder, open it, undo the last real sorting operation for that folder, and see that folder's progress or result. A single primary button at the bottom sorts all selected rows, while a compact footer reports the overall state. The behavior is visible by opening File Sorter, selecting multiple folders, starting a sort, and watching progress move from row to row without leaving the list. + +## Progress + +- [x] (2026-08-01 06:34Z) Reviewed `PLANS.md` and the current multi-folder File Sorter implementation. +- [x] (2026-08-01 06:34Z) Chose the single-screen row state model and per-folder Undo persistence strategy. +- [x] (2026-08-01 06:52Z) Added file-level progress contracts and reporting to `FileSorterService` with focused tests. +- [x] (2026-08-01 06:52Z) Replaced the three-state WPF layout with the 520-pixel-wide single-screen folder list and inline controls. +- [x] (2026-08-01 06:52Z) Implemented per-row progress, open-folder, Undo, status, localization refresh, and persistence behavior. +- [x] (2026-08-01 06:52Z) Updated all localization resources and UI/source contract tests. +- [x] (2026-08-01 07:09Z) Ran Release build, 1092-test complete suite, STA runtime verification, and rendered WPF visual verification. +- [x] (2026-08-01 07:09Z) Rebuilt the installer and recorded final evidence and retrospective. +- [x] (2026-08-01 07:39Z) Removed the row-height-changing progress bar after user validation and moved the overall result below Sort. +- [x] (2026-08-01 07:50Z) Eliminated whole-window disabled-state flashing and placed Add Folder and Sort in equal-width columns on one row. +- [x] (2026-08-01 07:50Z) Added runtime busy-state coverage, passed 1093 tests, visually rendered the final action row, and rebuilt the installer. +- [x] (2026-08-01 08:03Z) Replaced the mismatched secondary style with the shared utility `CommandButtonStyle`; both bottom actions now inherit identical geometry from `CommandButtonBaseStyle` and differ only in color. + +## Surprises & Discoveries + +- Observation: The current `SortMultipleFoldersAsync` progress callback reports only the folder index before sorting starts, so it cannot drive a truthful percentage bar. + Evidence: `AiteBar/FileSorterService.cs` reports `(rootPath, i, total)` immediately before calling `SortFilesAsync`. + +- Observation: Undo data already contains one `FileSortUndoState` per changed folder, but the window exposes it through one global completed-state button. + Evidence: `MultiFileSortUndoState.PerFolder` in `AiteBar/Models.cs` is sufficient for locating Undo by normalized root path. + +- Observation: File moves usually complete synchronously even though the service API returns `Task`, so running the service directly on the WPF dispatcher would update progress values without giving WPF a render opportunity. + Evidence: The single-screen implementation uses `Task.Run` for filesystem work and a dispatcher-backed progress adapter; focused tests still pass with 45/45. + +- Observation: Dispatching every file update would make large folders pay for one synchronous UI round trip per file. + Evidence: `FileSorterUiProgress` reports folder boundaries and completion immediately while throttling intermediate UI updates to at most once every 50 milliseconds. + +- Observation: The initially selected bundled Fluent glyph codepoints rendered as missing-glyph squares in the real WPF preview host even when the embedded font family was assigned explicitly. + Evidence: The rendered preview showed squares for both row actions; switching only those actions to Windows `Segoe MDL2 Assets` glyphs `E7A7` and `E838` produced recognizable Undo and Open Folder icons. + +- Observation: Showing and hiding a progress grid below the folder path changed the row height at operation start, and its thin blue bar visually merged with the divider as a phantom line. + Evidence: User validation of the running utility showed both the vertical jump and blue line; the row now reports `processed/total` in its existing fixed-width status column without adding controls to the visual tree. + +- Observation: `SetBusy(true)` changed `IsEnabled` on every switch and action, so WPF simultaneously applied disabled opacity across the entire window and looked like a flash. + Evidence: Busy-state interaction is now blocked with `IsHitTestVisible` and keyboard event handling while actual availability remains in `IsEnabled`; beginning a sort no longer changes row opacity or clears all existing row statuses. + +## Decision Log + +- Decision: Keep one window surface and remove `SortingStatePanel` and `CompletedStatePanel` rather than hiding the folder list during work. + Rationale: The user explicitly requested a single screen, and keeping row context visible makes multi-folder progress and per-folder actions understandable. + Date/Author: 2026-08-01 / Codex + +- Decision: Use icon-only row actions with localized tooltips and accessible names, while widening the fixed window from 360 to approximately 520 device-independent pixels. + Rationale: Full button labels do not fit reliably in Russian, Ukrainian, German, and English alongside a switch, path, and status. Icons preserve the compact utility style. + Date/Author: 2026-08-01 / Codex + +- Decision: Treat Undo as the last sorting operation that actually moved files for each folder. Sorting a folder that has no movable files does not erase its previous usable Undo state. + Rationale: A disabled Undo immediately after a no-op sort would discard a still-valid recovery action without changing the filesystem. + Date/Author: 2026-08-01 / Codex + +- Decision: Disable selection and row actions while a sort or Undo is running. + Rationale: The settings model stores one Undo state per folder and is not designed for concurrent filesystem mutations. A single global busy state prevents conflicting operations. + Date/Author: 2026-08-01 / Codex + +- Decision: Run sorting and Undo on a worker thread and marshal throttled progress synchronously to the WPF dispatcher. + Rationale: This keeps the window responsive and makes inline progress observable without overwhelming the dispatcher for folders containing many files. + Date/Author: 2026-08-01 / Codex + +- Decision: Extract per-folder Undo merging and replacement into `AiteBar/FileSorterUndoStateHelper.cs`. + Rationale: The rules for preserving unrelated rows and retaining a usable Undo after a no-op sort are pure non-UI behavior and need direct unit coverage. + Date/Author: 2026-08-01 / Codex + +- Decision: Use Segoe MDL2 Assets for the two row-action glyphs. + Rationale: A rendered WPF preview, not just source inspection, proved that this choice is legible in the actual Windows rendering path used by the application. + Date/Author: 2026-08-01 / Codex + +- Decision: Keep row geometry invariant during sorting by using muted textual progress in a 96-pixel status column, and place the overall result after the action row. + Rationale: Stable geometry removes the visible jump and phantom divider, while the result reads naturally as feedback to the action that produced it. Add Folder and Sort use equal-width columns on one row as requested. + Date/Author: 2026-08-01 / Codex + +- Decision: Block busy-state input without changing control `IsEnabled` solely because work is running. + Rationale: Hit testing and keyboard guards prevent concurrent actions without triggering WPF's disabled visual states across the whole interface. + Date/Author: 2026-08-01 / Codex + +- Decision: Use the existing utility style pair `CommandButtonStyle` and `PrimaryCommandButtonStyle` for the two equal actions. + Rationale: Both inherit `CommandButtonBaseStyle`, guaranteeing the same height, minimum width, padding, font size, and control template while retaining neutral and accent colors. + Date/Author: 2026-08-01 / Codex + +## Outcomes & Retrospective + +The File Sorter now remains on one operational screen. Desktop appears first, Downloads second, and every row has a selection switch, stable inline textual file progress, a localized result or error, an independently enabled Undo action, and Open Folder. Custom-folder removal remains in the right-click context menu. Sorting and Undo run away from the WPF dispatcher, conflicting input is blocked without changing the interface appearance, and closing is blocked until the operation safely finishes. + +Per-folder Undo survives sorts of unrelated rows and no-op sorts because `FileSorterUndoStateHelper` merges only new non-null Undo states. Partial batch failure retains completed rows' Undo and marks the failed row. Runtime localization rebuilds rows while preserving selection, progress, and status. + +Validation completed with a zero-warning Release build, 1093 of 1093 tests passing before the style-only correction, and 29 of 29 targeted command-style and File Sorter window tests passing afterward. STA tests construct the real window and verify four-column rows, per-folder action state, visually stable input blocking, and equal effective geometry for the two bottom actions. A rendered WPF preview was inspected for stable rows, equal action sizes, spacing, long paths, switches, footer order, and glyph legibility. Two later attempts to repeat the entire WPF suite stalled in `testhost` without reporting a failed test; each repository-specific process was identified before being stopped. `installer/Build-Installer.ps1` rebuilt `artifacts/installer/AiteBar-Setup.exe`; signing was skipped because no PFX certificate was supplied. No required implementation work remains. + +## Context and Orientation + +`AiteBar/FileSorterWindow.xaml` defines the WPF window. It currently contains `IdleStatePanel`, `SortingStatePanel`, and `CompletedStatePanel`, only one of which is visible at a time. `AiteBar/FileSorterWindow.xaml.cs` creates folder rows dynamically because the first two folders are Windows Downloads and Desktop and additional folders come from `AppSettings.SavedFileSortFolders`. The same code-behind starts sorting, opens folders, persists settings, and invokes Undo. + +`AiteBar/FileSorterService.cs` performs filesystem work. `SortFilesAsync` enumerates top-level files, moves eligible files into localized category subdirectories, and returns `FileSortResult` with counts and an optional `FileSortUndoState`. `SortMultipleFoldersAsync` calls that method sequentially and wraps partial success in `MultiFileSortException` if a later folder fails. Undo moves files back using `UndoLastSortAsync`. + +`AiteBar/Models.cs` contains result and Undo types. `AppSettings.LastMultiFileSortOperation` stores the current per-folder Undo states, while `LastFileSortOperation` remains a compatibility representation for a single state. `AiteBar/AppSettingsService.cs` deep-clones these settings before updates and saves them to the normal application settings file. + +The folder list uses a local `ScrollViewer`; the whole fixed-height utility window must not gain a global vertical scrollbar. A row action is an icon button on the right. A determinate progress bar is truthful only when the service reports processed and total top-level file counts. + +## Plan of Work + +First, add `FileSortProgress` and `MultiFileSortProgress` value types to `AiteBar/Models.cs`. Extend `FileSorterService.SortFilesAsync` with an optional `IProgress` parameter. Materialize the top-level file list once, report zero processed files, then report after every file whether it was moved or skipped. Extend `SortMultipleFoldersAsync` to accept `IProgress` and adapt the inner progress with the current folder index and total folder count. Preserve the partial-result exception behavior. Add tests proving monotonic file counts, the terminal count, empty-folder reporting, and multi-folder identity. + +Second, replace the three panels in `AiteBar/FileSorterWindow.xaml` with one vertical surface. It will contain a heading with the selected count, the existing locally scrollable folder card, equal-width Add Folder and Sort actions on one row, and a compact overall status line below them. Increase the width to 520 while preserving fixed size, dark colors, standard title bar, Escape behavior, and existing corner-radius conventions. Define compact icon-button styles in the window resources. + +Third, rebuild `AddFolderRow` in `AiteBar/FileSorterWindow.xaml.cs` so each row has four logical areas: selection switch, name/path/progress, result status, and icon actions. Store the generated controls in `FolderListEntry`. The Open button launches only that row's path. The Undo button finds that path's state, calls `UndoLastSortAsync`, replaces or removes the persisted state, saves settings, and updates only the row and footer. + +Sorting will keep every row visible. The window sets a global busy flag, disables mutable controls, clears statuses only for selected rows, and passes progress into `SortMultipleFoldersAsync`. Progress updates the matching row's fixed-width status text without changing row height. Completed results update their matching rows, merge new non-null Undo states with existing states for unselected or unchanged folders, persist settings, and enable Undo where appropriate. A partial exception applies completed results, retains Undo for the failed folder, marks that row as failed, persists completed Undo states, and shows the existing error dialog. + +Fourth, update all four `AiteBar/Resources/Strings*.resx` files with selected-count, ready, progress, row-result, row-error, row Undo, and tooltip strings. Runtime localization will capture selected paths and transient row state, rebuild localized rows, and reapply those states. Update `AiteBar.Tests/FileSorterWindowLayoutTests.cs` and `AiteBar.Tests/RuntimeLocalizationWindowSourceTests.cs` to assert the single-screen contract without relying on visual screenshots. + +## Concrete Steps + +Run all commands from `D:\01_Codebdbd\01_projects\aitebar`. + +After the service milestone, run: + + dotnet test .\AiteBar.Tests\AiteBar.Tests.csproj -c Release --filter "FullyQualifiedName~FileSorterServiceTests" + +Expect every FileSorter service test to pass, including new progress and partial-Undo tests. + +After the UI milestone, run: + + dotnet build .\AiteBar.sln -c Release + dotnet test .\AiteBar.Tests\AiteBar.Tests.csproj -c Release + +The build must complete with zero warnings and zero errors. The complete test count may increase as tests are added; all discovered tests must pass. + +For manual verification, start the Release application, open File Sorter from its panel or tray entry, and observe that the folder list never disappears. Select Desktop and Downloads, start sorting, observe inline progress moving through the selected rows, then use each row's Open and Undo buttons. Right-click a custom row and verify that removal remains available. Switch application language and verify that selection and row states remain visible with translated text. + +## Validation and Acceptance + +The change is accepted when File Sorter has no separate loading or completion screen; every folder row contains a selection switch and Open action; a row with persisted Undo has an enabled Undo action; and a row without persisted Undo has a disabled Undo action. Starting a multi-folder sort leaves the rows visible, disables conflicting controls, displays real processed/total file progress on the active row, and records a result on each completed row. Undo for one row does not remove Undo availability from unrelated rows. Closing and reopening the window restores per-folder Undo availability from settings. + +If the second folder fails after the first has moved files, the first row must retain an enabled Undo action and the failed row must show an error state. Runtime language changes must not alter which switches are selected. Custom-folder removal must remain available through the localized dark context menu. + +## Idempotence and Recovery + +Build and test commands are safe to repeat. Service tests create isolated temporary directories and delete them in `finally` blocks. The implementation must not delete user files; sorting only moves eligible top-level files and Undo uses the existing guarded restore logic. If a build is interrupted and `testhost.exe` holds test DLLs, identify the exact testhost whose command line points into this repository before stopping only that process, then rerun the build. + +The existing working tree already contains the multi-folder feature and belongs to the user. Do not reset or overwrite unrelated modifications. Apply changes on top of the current diff and use `git diff --check` before completion. + +## Artifacts and Notes + +Baseline validation before this plan showed a successful Release build and 1086 passing tests after the prior multi-folder fixes. Final post-refinement validation produced 1093 passing tests, followed by 29 passing targeted tests after the style-only correction, and rebuilt `artifacts/installer/AiteBar-Setup.exe` at 79,451,632 bytes with SHA-256 `B4A3049A98DF4442C5B5E623B6E1D0FB77AFB366994090B22954FD10C242ADF6`. + +## Interfaces and Dependencies + +No new NuGet dependency is required. Use WPF controls already available in the application, `AppContextMenuFactory` for custom-row removal, `LocalizationService` for all visible strings, and `AppSettingsService` for persistence. + +At the end, `AiteBar/Models.cs` must expose progress values equivalent to: + + public sealed record FileSortProgress(string RootPath, int ProcessedFiles, int TotalFiles); + public sealed record MultiFileSortProgress(string RootPath, int FolderIndex, int FolderCount, int ProcessedFiles, int TotalFiles); + +`FileSorterService.SortFilesAsync` must accept an optional `IProgress`. `FileSorterService.SortMultipleFoldersAsync` must accept an optional `IProgress`. Folder indices are zero-based in service code; visible labels add one. + +`FileSorterWindow` must maintain one `FolderListEntry` per visible row containing references to its switch, fixed-width status text, Undo button, and Open button. Undo lookup and replacement compare normalized root paths using `StringComparison.OrdinalIgnoreCase` because the application targets Windows. + +Revision note (2026-08-01 06:34Z): Created the initial self-contained plan after reviewing the current implementation and the user's approved single-screen concept. + +Revision note (2026-08-01 06:52Z): Recorded completed service, UI, localization, and focused-test milestones; documented the worker-thread rendering discovery, throttling decision, and extracted Undo helper. + +Revision note (2026-08-01 07:09Z): Completed the plan after full automated validation, real WPF rendering inspection, glyph correction, and installer rebuild; recorded final evidence and outcomes. + +Revision note (2026-08-01 07:39Z): Incorporated user validation by eliminating the height-changing row progress grid, stabilizing the status column, reordering action feedback, and compacting Add Folder. + +Revision note (2026-08-01 07:50Z): Removed busy-state opacity flashing and changed the bottom actions to equal-width buttons on one row following additional user feedback. + +Revision note (2026-08-01 08:03Z): Corrected the Add Folder button to use the repository's existing unified utility command style pair and added geometry assertions. diff --git a/docs/execplans/MAIN_PANEL_UX_HARDENING_EXECPLAN.md b/docs/execplans/MAIN_PANEL_UX_HARDENING_EXECPLAN.md new file mode 100644 index 0000000..956bd60 --- /dev/null +++ b/docs/execplans/MAIN_PANEL_UX_HARDENING_EXECPLAN.md @@ -0,0 +1,108 @@ +# Harden main-panel focus, overflow, navigation, and feedback + +This ExecPlan is a living document. The sections `Progress`, `Surprises & Discoveries`, `Decision Log`, and `Outcomes & Retrospective` must be kept up to date as work proceeds. This document must be maintained in accordance with `PLANS.md` at the repository root. + +## Purpose / Big Picture + +This work improves the everyday behavior of the main AiteBar edge panel after the 1.13.0 release candidate was preserved in commit `3aab32a` and draft PR 28. A panel revealed only by mouse hover must not steal keyboard focus from the application where the user is typing. A panel with more buttons than three layout bands can display must keep every action reachable through an explicit overflow button instead of clipping buttons outside the window. Keyboard arrows must follow the visible two-dimensional layout, and failed drop, clipboard, location, or settings actions must provide understandable feedback. + +## Progress + +- [x] (2026-08-02 04:05Z) Published the clean 1.13.0 baseline as commit `3aab32a` on `agent/aitebar-1.13.0-release` and opened draft PR 28. +- [x] (2026-08-02 04:12Z) Reviewed focus activation, the 30 ms hover timer, settings cloning, three-band clipping, keyboard navigation, context and drag affordances, reorder boundaries, and silent errors. +- [x] (2026-08-02 05:02Z) Stopped timer-driven hover activation from taking foreground focus, reduced each tick to one settings snapshot, and disabled polling outside the hidden/idle/enabled state. +- [x] (2026-08-02 05:18Z) Replaced clipped excess buttons with a localized, keyboard-accessible “More (N)” menu backed by a tested capacity helper. +- [x] (2026-08-02 05:31Z) Added tested spatial arrow navigation, Home/End navigation, a focusable context button, and localized drag-handle accessibility metadata. +- [x] (2026-08-02 05:39Z) Added owned feedback dialogs for drop, clipboard, missing-location, and settings-save failures, and constrained reorder previews to the persisted button group. +- [x] (2026-08-02 07:54Z) Passed 9 focused helper tests, warning-free Release build, 1081 non-WPF tests, 72 isolated WPF tests (1153 total), rebuilt the installer, and smoke-started the published executable for 15 seconds. + +## Surprises & Discoveries + +- Observation: Hover opening and explicit opening share the same animation completion, which always calls `ForceForegroundWindow` and `Activate`. + Evidence: The activation timer calls parameterless `ShowDock()`, and `Toggle` activates after every show animation regardless of its source. + +- Observation: One 30 ms timer pass reads `AppSettings` six times, and every read deep-clones the full settings graph. + Evidence: `MainWindow.AppSettings` delegates to `AppSettingsService.Settings`; the getter returns `CloneAppSettings(_appSettings)`. The timer separately reads monitor index twice, delay, edge, zone size, and hover enablement. + +- Observation: `OverflowWrapPanel` caps measured cross bands at three but arranges every child, while its parent clips to bounds and keyboard navigation retains all children. + Evidence: `PanelLayoutHelper.MaxUserBands` is three, `UnifiedButtonsPanel` has `ClipToBounds="True"`, and `GetAllFocusableButtons` appends the complete `_unifiedButtons` list. + +- Observation: Existing stable panel metrics can calculate the visible capacity without altering the four-edge geometry contract. + Evidence: Horizontal metrics expose user width and height; vertical metrics additionally expose the leading and overflow reserves already consumed by `OverflowWrapPanel`. + +## Decision Log + +- Decision: Preserve explicit opening behavior and suppress foreground activation only for timer-driven hover opening. + Rationale: Hotkey, tray, and position-indicator commands are deliberate. Hover is incidental and must never interrupt typing in another application. + Date/Author: 2026-08-02 / Codex + +- Decision: Use one settings snapshot per hover tick and run the timer only while the panel is hidden, animation is idle, and hover activation is enabled. + Rationale: The timer exists only to reveal a hidden panel. Running it in every other state wastes CPU and allocates cloned settings graphs without user benefit. + Date/Author: 2026-08-02 / Codex + +- Decision: Represent excess actions with one final “More (N)” button and an existing styled context menu. + Rationale: A popup preserves the panel’s compact geometry, works on all four edges, avoids wheel conflicts with context switching, and gives keyboard users a visible destination instead of focusing clipped controls. + Date/Author: 2026-08-02 / Codex + +- Decision: Keep utilities and user buttons as separate reorder groups and make that boundary explicit in drag behavior. + Rationale: `UnifiedButtonService` always renders utilities before user buttons and persists their orders separately. Pretending they can cross groups creates misleading previews and dropped changes. + Date/Author: 2026-08-02 / Codex + +## Outcomes & Retrospective + +Implementation is complete and layered after commit `3aab32a`. The release-equivalent suite passes 1153/1153 tests, including the WPF orientation coverage, and the published executable remains alive during the 15-second startup smoke test. The rebuilt unsigned installer is `artifacts/installer/AiteBar-Setup.exe`, 79,448,300 bytes, SHA-256 `023A59088D5423962F620C950BBDD24E028BF1F0F9479F566F25966802B1A067`. Interactive feel on a real desktop remains part of the human Top/Bottom/Left/Right release check; automated geometry and orientation coverage is green. + +## Context and Orientation + +`AiteBar/MainWindow.xaml` defines the compact edge panel, fixed controls, `OverflowWrapPanel`, context badge, drag handle, and application-settings button. `AiteBar/MainWindow.xaml.cs` owns activation, animation, settings snapshots, context menus, button execution, orientation, and positioning. `AiteBar/MainWindow.KeyboardNavigationHandler.cs`, `.DragAndDropHandler.cs`, `.DropHandler.cs`, and `.PanelDragHandler.cs` contain the interaction-specific partial class code. + +`AiteBar/PanelLayoutHelper.cs` calculates fixed panel dimensions for up to three user-button bands. `AiteBar/OverflowWrapPanel.cs` arranges buttons inside those dimensions. `UnifiedButtonService` creates a logical list containing utilities first on the primary context and then user buttons. Tests for pure layout live in `AiteBar.Tests/PanelLayoutHelperTests.cs`; WPF orientation tests live in `MainWindowIconConverterOrientationTests.cs`. New behavior tests must be isolated in the existing `WpfTestCollection` and added to both WPF class arrays in `.github/workflows/release.yml` if a new test class is introduced. + +## Plan of Work + +First separate hover opening from explicit opening. Track whether the current show animation may activate the window. The activation timer passes false; hotkey, tray, and position-indicator paths retain explicit activation. Replace repeated timer property reads with one local `AppSettings` snapshot. Add a method that starts or stops the timer from startup, settings changes, and animation completion according to the hidden/idle/enabled state. + +Next compute the current panel’s visible button capacity from `PanelLayoutMetrics`, including vertical leading and overflow reserves. Build only the actions that fit, reserving the last slot for a non-draggable overflow button when necessary. Store the remaining logical actions separately and build a localized menu that executes the same action methods. Keyboard enumeration must contain displayed action buttons and the overflow button only. + +Then replace linear primary-axis navigation with spatial navigation. Use each visible button’s rectangle relative to the panel and select the nearest candidate whose center lies in the pressed direction. Tab continues to follow visual enumeration, Escape hides, Enter/Space invoke, and Home/End select the first/last visible command. Convert or wrap the context badge so it can receive focus and open the context list; add tooltip and automation metadata to the drag handle. + +Finally make drop, clipboard, location, and settings failures visible through localized owned dialogs or a lightweight panel notification. During reorder preview, reject targets from the opposite group and reset transforms cleanly. Add regression tests for every corrected behavior before full validation. + +## Concrete Steps + +Run from `D:\01_Codebdbd\01_projects\aitebar`: + + dotnet test .\AiteBar.Tests\AiteBar.Tests.csproj -c Release --filter "FullyQualifiedName~MainWindow|FullyQualifiedName~PanelLayoutHelper|FullyQualifiedName~PanelPositionHelper|FullyQualifiedName~ActivationZoneHelper|FullyQualifiedName~CommandButtonStyleTests" + dotnet build .\AiteBar.sln -c Release + +For final release-equivalent validation, run the non-WPF exclusion filter from `.github/workflows/release.yml` and each WPF class in its own host. Only after all tests pass should the installer be rebuilt. + +## Validation and Acceptance + +While typing in another application, move the pointer into the panel activation zone. The panel appears without changing the foreground application. Clicking a panel button still executes it, and opening with the global hotkey focuses the panel for keyboard navigation. + +Configure enough buttons to exceed three bands on each edge. Exactly the buttons that fit remain visible, followed by “More (N)”; no button is clipped, and every hidden action is available in the menu. Tab and arrows never focus a control outside the panel. Arrow keys move geometrically between rows or columns. + +Hover disabled means no activation timer polling and no opening. Re-enabling it from settings restores activation without restart. Failed drops and missing file locations produce localized feedback. Dragging a utility over a user button does not display a false reorder destination. + +## Idempotence and Recovery + +The published baseline is recoverable from commit `3aab32a` and PR 28. All new work remains after that commit and must not amend it. Source edits use `apply_patch`; tests and builds are repeatable. Do not rewrite or force-push the published baseline. + +## Artifacts and Notes + +Published baseline: + + branch: agent/aitebar-1.13.0-release + commit: 3aab32a + draft PR: https://github.com/codebdbd/aitebar/pull/28 + +## Interfaces and Dependencies + +No new library is required. Overflow uses existing WPF `Button`, `ContextMenu`, `MenuItem`, and `AppContextMenuFactory`. The panel must retain `PanelLayoutHelper.MaxUserBands = 3`; overflow changes reachability, not the compact size contract. Any helper introduced for capacity or directional navigation should be pure and tested outside WPF where possible. + +Plan revision note (2026-08-02 04:12Z): Created after publishing the baseline and completing the full main-panel UX review. + +Plan revision note (2026-08-02 05:39Z): Recorded implemented focus, overflow, navigation, accessibility, failure-feedback, and reorder-boundary milestones before full release validation. + +Plan revision note (2026-08-02 07:54Z): Recorded the final 1153-test release matrix, installer artifact, hash, and publish startup smoke result. diff --git a/docs/execplans/PANEL_ACCESSIBILITY_AND_HOVER_SETTING_EXECPLAN.md b/docs/execplans/PANEL_ACCESSIBILITY_AND_HOVER_SETTING_EXECPLAN.md new file mode 100644 index 0000000..1a1f2d5 --- /dev/null +++ b/docs/execplans/PANEL_ACCESSIBILITY_AND_HOVER_SETTING_EXECPLAN.md @@ -0,0 +1,97 @@ +# Restore panel cues and make mouse-hover activation optional + +This ExecPlan is a living document. The sections `Progress`, `Surprises & Discoveries`, `Decision Log`, and `Outcomes & Retrospective` must be kept up to date as work proceeds. This document must be maintained in accordance with `PLANS.md` at the repository root. + +## Purpose / Big Picture + +This release fix restores information and accessibility cues on the main AiteBar panel and gives the user control over automatic mouse activation. After the change, hovering the numbered context badge shows the active context name, keyboard navigation draws a visible blue outline around the focused panel button, and a new settings switch immediately before “Show panel position indicator” can disable only edge-hover activation. The global show-panel hotkey, tray command, and position indicator must continue to open the panel when hover activation is disabled. + +## Progress + +- [x] (2026-08-02 03:15Z) Traced the context badge, button templates, keyboard input mode, activation timer, settings clone/load/save path, localization resources, and release tests. +- [x] (2026-08-02 03:28Z) Added the localized context tooltip and visible keyboard-only focus geometry with regression contracts. +- [x] (2026-08-02 03:34Z) Added and persisted the mouse-hover activation switch in all four languages and gated only the activation timer. +- [x] (2026-08-02 03:47Z) Passed 103 focused tests, a zero-warning Release build, 1071 non-WPF plus 73 isolated WPF tests, rebuilt the 1.13.0 installer, verified its checksum, and smoke-started the published executable. + +## Surprises & Discoveries + +- Observation: The keyboard focus trigger still sets a blue `BorderBrush`, but its `FocusChrome` border has no nonzero `BorderThickness`. + Evidence: Both button templates in `AiteBar/MainWindow.xaml` contain the keyboard-only multi-trigger and set `#3ABEFF`, but the focus border defaults to zero thickness, making the cue invisible. + +- Observation: Mouse-hover activation is centralized in one dispatcher timer and does not share the explicit hotkey or tray paths. + Evidence: `EnsureStartupInfrastructure` calls `ActivationDwellTracker.Update` and `ShowDock()` only for the edge activation zone. Gating that branch preserves `TogglePanelFromKeyboard`, tray `ShowDock()`, and indicator-initiated opening. + +## Decision Log + +- Decision: Store `ShowPanelOnMouseHover` as a non-nullable Boolean with default `true`. + Rationale: Existing settings files omit the property and JSON deserialization will retain the initializer, preserving current behavior. A simple Boolean also avoids the nullable compatibility handling required by the older indicator setting. + Date/Author: 2026-08-02 / Codex + +- Decision: Show the tooltip as a localized “Context {number}: {name}”. + Rationale: The number alone is already visible; the useful missing information is the active context name, while the localized prefix makes the tooltip unambiguous. + Date/Author: 2026-08-02 / Codex + +- Decision: Gate only the timer-driven activation zone and reset its dwell tracker while disabled. + Rationale: The user asked to disable appearing on mouse hover, not to disable intentional opening. Resetting prevents a partially accumulated hover delay from opening the panel immediately after re-enabling the option. + Date/Author: 2026-08-02 / Codex + +## Outcomes & Retrospective + +All three requested behaviors are implemented. The context badge has an immediate localized tooltip containing its number and name. Both panel button templates have a one-pixel transparent focus border that turns blue only when the existing keyboard cue flag is active. `ShowPanelOnMouseHover` defaults to true for old settings, appears immediately before the position-indicator switch, persists through clone/load/save, and gates only timer-driven edge activation. + +The release candidate builds with zero warnings and errors. Focused tests passed 103/103. The release-equivalent split passed 1071 non-WPF tests and 73 isolated WPF tests, 1144 total. The rebuilt unsigned installer is 79,456,114 bytes and has SHA-256 `FABECE024C254FB7F6BB9CB9BA601385C2DB7856D9FEFAD7C3767D0AC41A9CD8`, matching `SHA256SUMS.txt`. The published executable remained alive for the eight-second smoke interval. + +## Context and Orientation + +`AiteBar/MainWindow.xaml` defines both panel button templates and the numbered `ContextIndicator`. `AiteBar/MainWindow.KeyboardNavigationHandler.cs` turns on the attached `KeyboardFocusVisualService.ShowKeyboardFocusCue` flag when navigation begins. `AiteBar/MainWindow.xaml.cs` refreshes the badge and contains `EnsureStartupInfrastructure`, whose timer detects mouse dwell in the activation zone. + +Application settings are represented by `AppSettings` in `AiteBar/Models.cs`. `AiteBar/AppSettingsService.cs` deep-clones them so every new property must be copied there. `AiteBar/AppSettingsWindow.xaml` presents the switches, while `LoadSettings`, `BtnSave_Click`, and `RefreshAutomationNames` in `AiteBar/AppSettingsWindow.xaml.cs` load, save, and expose them to accessibility tools. Text resources live in `AiteBar/Resources/Strings.resx` plus `.ru`, `.uk`, and `.de` variants. + +## Plan of Work + +Give `ContextIndicator` immediate tooltip behavior and set its tooltip during `UpdateContextIndicator`, where both the enabled ordinal and active context object are already available. Apply the same edge-aware tooltip placement used by buttons. + +In each main-panel button template, give `FocusChrome` a transparent one-pixel border so the existing keyboard-only trigger can change its brush. Keep the cue conditional on keyboard mode so ordinary mouse clicks do not leave a focus ring. + +Add `ShowPanelOnMouseHover` to the settings model and clone. Insert `ChkShowPanelOnMouseHover` as the first switch in the existing general behavior card, immediately before `ChkShowTaskbarPositionIndicator`; load and save it and add its automation name. In the activation timer, return from the hidden-panel hover branch after resetting the dwell tracker whenever the option is false. + +Add source/layout/settings tests that prove the tooltip assignment, nonzero focus border, switch ordering, clone persistence, and timer gate. Update localization completeness through the existing resource tests. + +## Concrete Steps + +Run from `D:\01_Codebdbd\01_projects\aitebar`: + + dotnet test .\AiteBar.Tests\AiteBar.Tests.csproj -c Release --filter "FullyQualifiedName~AppSettingsServiceTests|FullyQualifiedName~AppSettingsLayoutContractTests|FullyQualifiedName~RuntimeLocalizationWindowSourceTests|FullyQualifiedName~CommandButtonStyleTests" + dotnet build .\AiteBar.sln -c Release + dotnet test .\AiteBar.Tests\AiteBar.Tests.csproj -c Release --no-build + .\installer\Build-Installer.ps1 -Configuration Release + +If the combined WPF host hangs, use the release workflow strategy: exclude WPF collection classes from one run and execute each excluded class in its own `dotnet test` process. + +## Validation and Acceptance + +With the panel visible, move the pointer over the numbered badge and observe “Context N: Name” in the current UI language. Open the panel by its hotkey, press Tab or an arrow key, and observe a one-pixel blue rounded outline around exactly the focused button as focus moves. + +In application settings, observe “Show panel on mouse hover” immediately before “Show panel position indicator”. Turn it off and save. Moving the pointer into the configured edge activation zone for longer than the delay must not open the panel; the global hotkey, tray Open command, and position indicator must still open it. Restarting AiteBar must preserve the choice. Turning it back on must restore the existing dwell behavior. + +## Idempotence and Recovery + +All edits are additive or narrow template corrections and can be reapplied safely. Settings compatibility is preserved by the default-true property initializer. Build, tests, and installer generation are repeatable. Do not reset or discard the existing dirty release worktree. + +## Artifacts and Notes + +The pre-fix focus template proves the regression: + + + ... + + +Because no border thickness is assigned, the brush has no visible geometry. + +## Interfaces and Dependencies + +No new dependency is required. `AppSettings` must expose `public bool ShowPanelOnMouseHover { get; set; } = true;`. The settings window must expose a named `CheckBox` called `ChkShowPanelOnMouseHover`. Localization must provide `AppSettingsWindow_ShowPanelOnMouseHover`, `AppSettingsWindow_ShowPanelOnMouseHoverHint`, and `Main_ContextIndicatorTooltipFormat` in English, Russian, Ukrainian, and German. + +Plan revision note (2026-08-02 03:15Z): Created after tracing all three regressions and resolving backward compatibility and activation-scope decisions. + +Plan revision note (2026-08-02 03:47Z): Recorded completed implementation, backward-compatibility evidence, full test counts, rebuilt artifact details, checksum, and smoke result. diff --git a/docs/execplans/TEXT_PROCESSING_LITERARY_EDIT_EXECPLAN.md b/docs/execplans/TEXT_PROCESSING_LITERARY_EDIT_EXECPLAN.md new file mode 100644 index 0000000..414094a --- /dev/null +++ b/docs/execplans/TEXT_PROCESSING_LITERARY_EDIT_EXECPLAN.md @@ -0,0 +1,99 @@ +# Add a Literary Editing mode to Text Processing + +This ExecPlan is a living document. The sections `Progress`, `Surprises & Discoveries`, `Decision Log`, and `Outcomes & Retrospective` must be kept up to date as work proceeds. This document must be maintained in accordance with `PLANS.md` at the repository root. + +## Purpose / Big Picture + +After this change, a user can open AiteBar's Text Processing utility and choose a fourth tab, `Литературная редакция`. That mode improves clarity, fluency, rhythm, and style while preserving the input language, meaning, facts, names, intended tone, and paragraph structure. It returns only the edited text and remains separate from proofreading, typography, and technical cleanup. Proofread remains the selected tab whenever a new window opens. + +## Progress + +- [x] (2026-08-02 01:00Z) Inspected the mode enum, WPF tabs, mode-selection code, prompt builder, response validator, localizations, tests, and current documentation. +- [x] (2026-08-02 01:14Z) Added the Literary Editing enum value, professional English prompt, mode-specific generation temperature, and a response-preservation threshold suitable for controlled rewriting. +- [x] (2026-08-02 01:14Z) Added the fourth localized WPF tab and synchronized runtime selection, descriptions, accessibility, and layout tests. +- [x] (2026-08-02 01:14Z) Updated current user, function, and architecture documentation to describe four independent modes. +- [x] (2026-08-02 01:14Z) Passed 143 focused and 1128 full tests, built a zero-warning Release, rebuilt the installer, and recorded artifact evidence. + +## Surprises & Discoveries + +- Observation: The current response validator uses one 35 percent distinct-word-overlap threshold for every mode. + Evidence: `TextProcessingService.ViolatesContentPreservation` has no mode input, and `TextProcessingWindow.ProcessAsync` calls it for Proofread, Typography, and Cleanup alike. A legitimate literary rewrite can preserve meaning while changing more wording than a technical correction, so the new mode needs a lower threshold while retaining the dominant-script translation guard. + +- Observation: Existing numeric enum values are implicitly persisted in settings even though a new window now always starts on Proofread. + Evidence: `AppSettings.TextProcessingLastMode` remains in the settings model and historical data may contain values 0 through 2. The new mode must therefore be appended as value 3 rather than inserted between existing values. + +## Decision Log + +- Decision: Add `TextProcessingMode.LiteraryEdit = 3` and keep Proofread as the unconditional startup selection. + Rationale: Appending preserves existing serialized numeric meanings, and the established startup contract requires error checking to remain first. + Date/Author: 2026-08-02 / Codex + +- Decision: Literary Editing may rewrite awkward wording and remove unintentional repetition, but may not translate, invent facts, change names, alter narrative perspective, or reorganize paragraphs. + Rationale: This creates a useful editorial mode without turning the utility into a generative writer or making its output unpredictable. + Date/Author: 2026-08-02 / Codex + +- Decision: Keep dominant-script rejection for every mode and use a 15 percent distinct-word-overlap floor for Literary Editing instead of the 35 percent technical-mode floor. + Rationale: Script changes reliably catch Russian-to-English and similar translations. A lower lexical floor allows controlled same-language rewriting while still rejecting unrelated or same-script translated output with almost no shared vocabulary. + Date/Author: 2026-08-02 / Codex + +## Outcomes & Retrospective + +The fourth `Литературная редакция` tab is complete and localized in English, Russian, Ukrainian, and German. It uses an independent professional English prompt, temperature 0.4, and a 15 percent lexical-overlap safety floor while retaining dominant-script translation rejection. Existing modes keep their numeric values, prompts, temperatures, and 35 percent technical-edit floor; Proofread remains the startup tab. Current documentation describes the controlled stylistic scope. Release builds without warnings or errors, all 1128 tests pass, and the installer was rebuilt. + +## Context and Orientation + +`AiteBar/TextProcessingMode.cs` defines the preset mode values. `AiteBar/TextProcessingService.cs` builds the provider-facing system prompt, sets generation parameters, protects technical fragments, cleans responses, and checks whether output improperly changes language or content. `AiteBar/TextProcessingWindow.xaml` defines the WPF tab row, while `AiteBar/TextProcessingWindow.xaml.cs` maps tab tags to enum values, enables tabs, displays localized descriptions, sends requests, and validates completed output. + +Localized user-facing strings live in `AiteBar/Resources/Strings.resx`, `Strings.ru.resx`, `Strings.uk.resx`, and `Strings.de.resx`. Current behavior is documented in `docs/USER_MANUAL.md`, `docs/functions.md`, and `docs/architecture.md`. Automated coverage lives primarily in `AiteBar.Tests/TextProcessingServiceTests.cs`, `TextProcessingWindowLayoutTests.cs`, and the source-contract tests whose names begin with `TextProcessing`. + +A system prompt is the instruction sent to the AI provider separately from the user's text. A preservation threshold is the minimum fraction of distinct words that the input and output must share after protected technical fragments are removed. It is a final safety check, not a request to the model. + +## Plan of Work + +Append `LiteraryEdit` to `TextProcessingMode`. Add a concise professional English prompt to `TextProcessingService.GetSystemPrompt`, use the existing shared language/content and protected-token contracts, and select a controlled temperature of 0.4. Expose a small mode-to-overlap-threshold helper so the window can use 0.15 only for Literary Editing and retain 0.35 for the existing technical modes. + +Add a fourth `TabItem` named `ModeLiteraryEdit` to `TextProcessingWindow.xaml`. Extend the tag switch, enabled-state updates, selection synchronization, and localized description switch in code-behind. Add all required resource keys in the four resource files. Preserve the current fixed window geometry and the existing Proofread startup selection. + +Update tests to enumerate all four modes, assert the new English prompt and temperature, assert enum value stability, verify the fourth real WPF tab loads and fits, and prove the literary threshold is lower without disabling dominant-script translation rejection. Update current documentation from three technical modes to four independent modes and explain that Literary Editing intentionally permits controlled wording improvements. + +## Concrete Steps + +Run all commands from `D:\01_Codebdbd\01_projects\aitebar`. + + dotnet build .\AiteBar.sln -c Release + dotnet test .\AiteBar.Tests\AiteBar.Tests.csproj -c Release --filter "FullyQualifiedName~TextProcessing" + dotnet test .\AiteBar.Tests\AiteBar.Tests.csproj -c Release + .\installer\Build-Installer.ps1 + +The Release build must report zero warnings and zero errors. Focused and full tests must report zero failures. If the WPF test host remains alive after test completion, identify only processes whose command line points to this repository, stop only those processes, and rerun using the documented `dotnet vstest` fallback. + +## Validation and Acceptance + +Open Text Processing and observe four tabs in this order: Proofread, Typography, Cleanup, Literary Editing. Proofread is selected initially. Select Literary Editing and verify the description explains style improvement with meaning and facts preserved. Enter awkward same-language prose and process it; the result may improve wording and rhythm but must remain in the same language and must not add facts. `Repeat` must reuse Literary Editing. + +Automated acceptance requires tests proving that values 0, 1, and 2 retain their old enum meanings and Literary Editing is 3; that its request uses its own prompt and temperature; that English provider-facing instructions contain no Cyrillic; that Russian-to-English output is rejected in the new mode; and that a legitimate same-language rewrite can pass the lower overlap threshold. + +## Idempotence and Recovery + +All source and resource edits are repeatable. No settings migration is needed because the enum value is appended. If a localized key is missing, WPF may show the resource key instead of a label; focused localization and WPF construction tests must catch that before installer creation. Existing user settings and API credentials must not be read or modified. + +## Artifacts and Notes + +Validation evidence: + + Release build: warnings 0, errors 0 + Focused Text Processing tests: passed 143, failed 0 + Full test suite: passed 1128, failed 0 + Installer: artifacts\installer\AiteBar-Setup.exe + Installer size: 79,463,329 bytes + SHA-256: 58DF1D183908B876A6491962C0E217F7864922A3980BA05A305F2ED850A2C61B + +Signing was skipped because no PFX certificate was supplied. + +## Interfaces and Dependencies + +No new package is required. `TextProcessingMode` gains `LiteraryEdit = 3`. `TextProcessingService.GetSystemPrompt(TextProcessingMode)` and `BuildRequest(TextProcessingMode, string, int?)` remain the public prompt entry points. Add an internal mode-to-threshold helper used by `TextProcessingWindow` so pure policy is independently testable. The AI gateway, provider clients, settings schema, and credential storage remain unchanged. + +Plan revision note (2026-08-02 01:00Z): Created this self-contained plan after mapping the existing three-mode implementation and identifying the need for an appended enum value and mode-specific lexical-preservation threshold. + +Plan revision note (2026-08-02 01:14Z): Completed the feature after adding the fourth localized tab, prompt and mode-aware validation policy, focused and full automated coverage, current documentation, and a rebuilt hashed installer. diff --git a/docs/execplans/TEXT_PROCESSING_STABILITY_FIX_EXECPLAN.md b/docs/execplans/TEXT_PROCESSING_STABILITY_FIX_EXECPLAN.md new file mode 100644 index 0000000..9a10a91 --- /dev/null +++ b/docs/execplans/TEXT_PROCESSING_STABILITY_FIX_EXECPLAN.md @@ -0,0 +1,162 @@ +# Restore predictable Text Processing startup, AI errors, and layout stability + +This ExecPlan is a living document. The sections `Progress`, `Surprises & Discoveries`, `Decision Log`, and `Outcomes & Retrospective` must be kept up to date as work proceeds. + +This document must be maintained in accordance with `PLANS.md` at the repository root. + +## Purpose / Big Picture + +The Text Processing utility must open on Proofread every time, must not claim that no AI connections exist when configured connections are merely cooling down or rate-limited, and must not resize the editor when an error appears. A user can verify the result by opening the utility after previously selecting another tab, repeatedly processing until a provider returns a limit, and observing both the accurate message and unchanged editor geometry. + +## Progress + +- [x] (2026-08-01 08:26Z) Inspected the active user settings, current Text Processing window, AI gateway, recent commits, and existing tests. +- [x] (2026-08-01 08:26Z) Confirmed that ten AI connections are enabled while the displayed message incorrectly says none are configured. +- [x] (2026-08-01 08:26Z) Made Proofread the unconditional startup mode and removed tab-selection persistence from the window. +- [x] (2026-08-01 08:26Z) Added typed AI availability failure reasons and mapped cooldown, quota, authentication, network, timeout, and temporary availability to distinct localized messages. +- [x] (2026-08-01 08:26Z) Moved the error banner from the DockPanel layout flow into an overlay over the work grid. +- [x] (2026-08-01 08:26Z) Passed a zero-warning Release build and 146 focused Text Processing, AI routing, and streaming tests. +- [x] (2026-08-01 08:33Z) Passed all 1098 tests, rendered and inspected the real WPF window, rebuilt the installer, and recorded its SHA-256. +- [x] (2026-08-01 10:04Z) Reproduced a repeated-processing translation from user screenshots and added a mandatory language/content contract plus fail-closed response validation. +- [x] (2026-08-01 10:04Z) Added RU/UK/EN/DE rejection messages and passed 84 focused service, window-contract, and localization tests. +- [x] (2026-08-01 10:07Z) Passed all 1106 tests and rebuilt the installer for the language-preservation revision. +- [x] (2026-08-01 11:02Z) Added a Text Processing-specific three-tier model policy, enforced it in both the UI and gateway, passed 1116 tests, and rebuilt the installer. +- [x] (2026-08-01 11:50Z) Replaced the Proofread prompt with one direct sentence, made streaming fallback skip empty routes before the first chunk, passed 1118 tests, and rebuilt the installer. +- [x] (2026-08-02 00:42Z) Rewrote all Text Processing system prompts as concise professional English instructions, passed 1121 tests, and rebuilt the installer. + +## Surprises & Discoveries + +- Observation: The active settings file stores `TextProcessingLastMode` as `2`, which is Cleanup, and the constructor explicitly restores it. + Evidence: `%APPDATA%\Codebdbd\Aite Bar\settings.json` reports `TextProcessingLastMode: 2`; `TextProcessingWindow` assigned `ParseSavedMode(...)` before `InitializeComponent`. + +- Observation: The application has ten enabled AI connections, so the screenshot message saying no neural network was added is factually false. + Evidence: The active settings contain ten enabled Cerebras, Gemini, and Groq connections. When every route is skipped by cooldown or quota state, `NoAvailableConnectionException` had no inner exception and the window mapped that case to `TextProcessing_ErrorNoModels`. + +- Observation: `StatusBorder` was a top-docked child whose visibility changed between Collapsed and Visible. + Evidence: In WPF a visible top-docked child consumes height before the remaining editor grid is arranged, so the error banner reduced the editor and moved its footer informer. + +- Observation: The prompts already prohibited translation, but a model still translated a Russian sentence to English after repeated processing. + Evidence: User screenshots show the Russian original and an accepted English result from the same Proofread workflow. Prompt-only constraints are therefore insufficient for preserving user text. + +- Observation: Automatic routing treated every non-technical free text model as equally suitable, including the Arabic-focused ALLaM-2-7b model shown in the failed run. + Evidence: `TextProcessingWindow.IsEligibleModel` and `AiGateway.GetEligibleModels` only rejected technical modalities; neither had a multilingual quality allowlist or a Text Processing-specific exclusion policy. + +- Observation: The gateway returned an `AiGatewayStream` immediately after receiving HTTP success, before verifying that the provider stream contained text. + Evidence: `GenerateStreamingCoreAsync` selected the first route after `GenerateStreamingAsync`; an empty SSE stream was marked successful by `ObserveStreamAsync`, leaving the window to report `TextProcessing_ErrorEmptyResponse` without trying another route. + +## Decision Log + +- Decision: Always initialize a newly created Text Processing window with `TextProcessingMode.Proofread` and do not persist tab changes. + Rationale: The required first workflow is error checking. Restoring an incidental previous tab makes startup unpredictable and directly contradicts the requested behavior. + Date/Author: 2026-08-01 / Codex + +- Decision: Carry a typed `AiAvailabilityFailureReason` on `NoAvailableConnectionException`. + Rationale: Parsing exception text is fragile. The AI gateway owns cooldown and quota state and is the only layer that can accurately distinguish missing configuration from temporary unavailability; the window remains responsible only for localized presentation. + Date/Author: 2026-08-01 / Codex + +- Decision: Render the error banner in the editor-and-command grid as a top overlay. + Rationale: An overlay remains visible and assertive without participating in the vertical layout calculation, so editor height and the bottom informer remain stable. + Date/Author: 2026-08-01 / Codex + +- Decision: Validate every completed response before committing it as a successful result. + Rationale: The utility is corrective, not generative. A dominant-script change or very low word overlap indicates translation or an excessive rewrite. Such output must be rejected, the pre-request editor text restored, and a localized error shown. The validator removes shared protected technical fragments before comparison and runs in linear time for the 50,000-character limit. + Date/Author: 2026-08-01 / Codex + +- Decision: Classify Text Processing models as certified for automatic routing, manual-only, or unsupported. + Rationale: Automatic mode must be conservative and predictable. Known multilingual writing families may route automatically; unknown text models remain available for an explicit user choice; technical and known narrow-language families such as ALLaM are hidden and rejected by the gateway. The policy is scoped to Text Processing so other AI utilities keep their existing model access. + Date/Author: 2026-08-01 / Codex + +- Decision: Track automatic availability separately from manual list availability. + Rationale: When only unknown but otherwise compatible models are connected, automatic processing must remain disabled while the model selector stays enabled so the user can make an explicit manual choice. + Date/Author: 2026-08-01 / Codex + +- Decision: Proofread uses exactly one system-prompt sentence, while the gateway prefetches the first text chunk before committing to a route. + Rationale: Proofreading is a narrow task and does not need a long rule sheet. Route reliability is a transport concern: a route that produces no text must be cooled down and skipped before the UI sees it, preserving streaming after the first real chunk. + Date/Author: 2026-08-01 / Codex + +- Decision: Keep all provider-facing system instructions in concise professional English while preserving user text in its original language. + Rationale: English gives a consistent instruction surface across providers and input languages. Each prompt must define one narrow transformation, explicit non-goals, protected content, and output format without conversational wording or redundant rule lists. + Date/Author: 2026-08-02 / Codex + +## Outcomes & Retrospective + +The startup, availability, overlay, language-preservation, model-routing, empty-stream, and prompt-quality repairs are complete. All provider-facing system instructions are now concise professional English. Proofread remains one direct sentence with no appended rule blocks; Typography and Cleanup use narrowly scoped transformations followed by compact shared language/content and protected-token contracts. Deterministic response validation still rejects translation or excessive rewriting after generation. Automatic routing accepts only explicitly certified multilingual model families, and empty streams are skipped before the UI commits to a route. Final validation passed 1121 of 1121 tests and rebuilt the installer. + +## Context and Orientation + +`AiteBar/TextProcessingWindow.xaml` defines the WPF window. Its three tabs select Proofread, Typography, and Cleanup. `AiteBar/TextProcessingWindow.xaml.cs` owns startup state, model loading, processing, and localized UI messages. `AiteBar/AiGateway.cs` enumerates configured connections and models, applies rate-limit or failure cooldowns, and tries fallback routes. `AiteBar/AiModels.cs` contains the exception types shared between the gateway and the window. + +`AiteBar/TextProcessingService.cs` builds the system prompt, protects technical fragments, cleans the model response, and now checks language/content preservation. The response check strips identical protected URLs, paths, code, and identifiers before comparing scripts and word overlap so technical text cannot hide a translated prose fragment. + +The active user settings are loaded by `AppSettingsService` from `%APPDATA%\Codebdbd\Aite Bar\settings.json`; API keys are not stored there and must not be read or logged. A cooldown is a temporary period during which the gateway avoids a route after a rate limit or transport failure. It is not equivalent to an absent connection. + +## Plan of Work + +In `TextProcessingWindow.xaml.cs`, initialize `_currentMode` to Proofread and remove the saved-mode parsing and saving calls. Preserve mode switching during the lifetime of the open window. + +In `AiModels.cs`, add an internal availability-reason enum and attach it to `NoAvailableConnectionException`. In `AiGateway.cs`, set `NoConnectionsConfigured` only when candidate connections are actually empty. When routes fail or are skipped, derive the reason from the last provider exception and recorded quota or connection state. In `TextProcessingWindow.xaml.cs`, translate each reason using existing localization keys instead of using `TextProcessing_ErrorNoModels` as a catch-all. + +In `TextProcessingWindow.xaml`, remove the error border from the top-docked sequence and add it as a high-z-order child of the remaining editor grid. It spans the editor and command rail but does not create a new row. Update runtime tests to compare editor height before and after showing an error. + +For repeated processing, append one language-preservation contract to every mode prompt in `TextProcessingService.BuildRequest`. After restoring protected fragments, compare the input and cleaned output before updating `_originalText`, `_processedText`, or history. Reject a dominant Latin/Cyrillic/CJK/other script change. For longer same-script text, reject output whose distinct-word overlap is below 35 percent; normal spelling, punctuation, typography, and line-cleanup edits retain substantial overlap. Restore `textShownBeforeRequest` on rejection. + +## Concrete Steps + +Run commands from `D:\01_Codebdbd\01_projects\aitebar`. + + dotnet build .\AiteBar.sln -c Release + dotnet test .\AiteBar.Tests\AiteBar.Tests.csproj -c Release --filter "FullyQualifiedName~TextProcessing|FullyQualifiedName~AiProviderTests|FullyQualifiedName~AiStreamingTests" + dotnet test .\AiteBar.Tests\AiteBar.Tests.csproj -c Release + .\installer\Build-Installer.ps1 + +The build must report zero warnings and zero errors. Focused and full test runs must have zero failed tests. If the WPF test host stalls, identify only the `testhost.exe` whose command line points to this repository before stopping it and use the documented `dotnet vstest` fallback. + +## Validation and Acceptance + +Construct the real WPF window with settings whose saved mode is Typography or Cleanup and confirm Proofread is selected. Show a status error through the window method, arrange the root at the same size, and confirm the editor's actual height is unchanged. Simulate an AI rate limit and confirm the gateway exception reason is `RateLimited` and the window text is `TextProcessing_ErrorRateLimit`, not `TextProcessing_ErrorNoModels`. + +Manual acceptance requires opening the utility with the active settings, observing Proofread selected, entering text, and processing it. If providers are cooling down, the red message must say the request limit or temporary service availability rather than saying no connections were added. Showing and clearing that message must not move the editor footer or model informer. + +Repeat Proofread several times for a Russian sentence. A Russian corrected result may replace the previous result. If a provider returns English or otherwise rewrites the content, the result must not become successful state: the text visible before that request remains in the editor and the localized `TextProcessing_ErrorContentChanged` message appears. + +## Idempotence and Recovery + +Builds and tests are repeatable. Tests use temporary settings and in-memory credential stores; they must never read or mutate the user's API keys. The diagnostic inspection reads only non-secret connection metadata from the active JSON settings. No user settings are rewritten by this fix. + +## Artifacts and Notes + +Validation after implementation: + + Сборка успешно завершена. + Предупреждений: 0 + Ошибок: 0 + + Focused: пройдено 146, не пройдено 0 + Full after availability/layout repair: пройдено 1098, не пройдено 0 + Language-preservation focused: пройдено 84, не пройдено 0 + Model-policy focused: пройдено 114, не пройдено 0 + Short-prompt and empty-stream focused: пройдено 124, не пройдено 0 + Professional-prompt focused: пройдено 98, не пройдено 0 + Final full: пройдено 1121, не пройдено 0 + +The rendered preview is stored outside the repository at `C:\Users\ostee\.codex\visualizations\2026\08\01\019fbbd6-f341-7f32-a69a-4037ee56c8dc\text-processing-wpf-preview.png`. + +`installer\Build-Installer.ps1` produced the final `artifacts\installer\AiteBar-Setup.exe` at 79,462,316 bytes with SHA-256 `C37478DF63EC3FA82AFB6BED41CA0801B30E7CB9D2F5A3C41757B1CB19FCC66F`. Signing was skipped because no PFX certificate was supplied. + +## Interfaces and Dependencies + +No new dependency is required. `NoAvailableConnectionException` exposes `AiAvailabilityFailureReason Reason`. `AiGateway` remains responsible for deriving that reason from provider responses and its in-memory cooldown state. `TextProcessingWindow.GetAvailabilityError` maps the reason to existing `TextProcessing_Error*` localization resources. The status overlay remains named `StatusBorder` so existing automation and code-behind references continue to work. + +Revision note (2026-08-01 08:26Z): Created this focused repair plan after reproducing the three regressions from source and active non-secret settings metadata, then recorded the implemented fixes and focused validation. + +Revision note (2026-08-01 08:33Z): Completed the plan after direct WPF rendering, the 1098-test full suite, installer rebuild, and final artifact hashing. + +Revision note (2026-08-01 10:04Z): Reopened the plan after a user reproduced translation during Repeat; added the universal language contract, deterministic fail-closed validation, localization, tests, and pending final validation steps. + +Revision note (2026-08-01 10:07Z): Completed the reopened milestone after 1106 passing tests, installer rebuild, and final SHA-256 capture. + +Revision note (2026-08-01 11:02Z): Reopened and completed the plan for task-specific model filtering. Added certified automatic, manual-only, and unsupported tiers; enforced them in the UI and gateway; preserved manual selection when automatic routing is unavailable; passed 1116 tests; and rebuilt and hashed the installer. + +Revision note (2026-08-01 11:50Z): Reopened after a real empty-response failure. Reduced Proofread to one system-prompt sentence, added first-chunk prefetch and automatic route fallback for empty streams, hid additional non-writing Gemini/speech families, passed 1118 tests, and rebuilt and hashed the installer. + +Revision note (2026-08-02 00:42Z): Reopened to standardize provider-facing instructions. Replaced Russian conversational rule sets with concise professional English prompts and shared contracts, added a no-Cyrillic system-instruction test, passed 1121 tests, and rebuilt and hashed the installer. diff --git a/docs/execplans/UTILITY_RELEASE_AUDIT_1_13_0_EXECPLAN.md b/docs/execplans/UTILITY_RELEASE_AUDIT_1_13_0_EXECPLAN.md new file mode 100644 index 0000000..acca540 --- /dev/null +++ b/docs/execplans/UTILITY_RELEASE_AUDIT_1_13_0_EXECPLAN.md @@ -0,0 +1,121 @@ +# Audit every built-in utility and prepare AiteBar 1.13.0 + +This ExecPlan is a living document. The sections `Progress`, `Surprises & Discoveries`, `Decision Log`, and `Outcomes & Retrospective` must be kept up to date as work proceeds. This document must be maintained in accordance with `PLANS.md` at the repository root. + +## Purpose / Big Picture + +This work prepares a release candidate by reviewing every built-in utility in the exact order exposed by `UtilityButtonCatalog.All`, fixing release-blocking defects, proving existing user data and settings remain safe, and producing synchronized version metadata, release notes, tests, publish output, checksums, and installer. A user should be able to install version 1.13.0 and launch each visible utility or system action without a crash, missing localization, broken primary workflow, or regression in the recent File Sorter and Text Processing changes. + +## Progress + +- [x] (2026-08-02 01:25Z) Captured the dirty working tree, current version 1.12.2, release pipeline, test inventory, and the authoritative 18-item utility catalog. +- [x] (2026-08-02 01:35Z) Reviewed system actions 1–6: Search, Screenshot, Record, Calculator, Explorer, and Downloads; fixed silent launch failures and added regression tests. +- [x] (2026-08-02 02:02Z) Reviewed window utilities 7–12: File Sorter, Icon Converter, Timer/Stopwatch, Color Picker, Quick Note, and QR Code Generator; fixed Color Picker localization and contained Timer close-time save failures. +- [x] (2026-08-02 02:12Z) Reviewed utilities and system actions 13–18: Clipboard Manager, Show Desktop, Apps Folder, Copilot, Text Processing, and Zen Editor; fixed silent shell launch failures and added Copilot input-release coverage. +- [x] (2026-08-02 02:12Z) Fixed every confirmed release-blocking defect and added focused regression tests where logic could be isolated. +- [x] (2026-08-02 02:19Z) Synchronized version 1.13.0 in project, assembly, installer fallback, changelog, security support table, and release workflow. +- [x] (2026-08-02 02:27Z) Ran release-equivalent isolated WPF and non-WPF tests, built and published, built the installer, verified version/checksum, and smoke-started the published executable. + +## Surprises & Discoveries + +- Observation: The working tree contains a coherent but uncommitted release candidate spanning File Sorter, AI routing, Text Processing stability, professional prompts, and Literary Editing. + Evidence: `git status --short` reports modified source, tests, resources, and documentation plus new helper, policy, and ExecPlan files. These edits belong to the user and must be preserved as one release scope rather than reset or partially discarded. + +- Observation: The built-in catalog has 18 entries, but only 9 are `IUtility` window implementations. + Evidence: `UtilityButtonCatalog.All` contains Search, Screenshot, Record, Calculator, Explorer, Downloads, FileSorter, IconConverter, TimerStopwatch, ColorPicker, QuickNote, QRCodeGenerator, ClipboardManager, ShowDesktop, AppsFolder, Copilot, TextProcessing, and ZenEditor. The system actions are executed directly by `MainWindow` or `ActionService`; the remaining entries create managed utility windows. + +- Observation: The current version is 1.12.2 and `CHANGELOG.md` has an empty Unreleased section. + Evidence: `AiteBar/AiteBar.csproj`, `AiteBar/AssemblyInfo.cs`, and `installer/AiteBar.iss` all contain 1.12.2. The accumulated work adds a fourth Text Processing mode and materially redesigns File Sorter, which qualifies as a minor release under the repository's stated semantic-versioning policy. + +- Observation: The release workflow did not isolate the new WPF `FileSorterWindowBehaviorTests`, so it could run in the shared non-WPF test host and recreate the file-lock/hang failure seen locally. + Evidence: The class declares `WpfTestCollection`, but both class arrays in `.github/workflows/release.yml` omitted it. The workflow now includes it in both arrays and all ten WPF classes pass in separate hosts. + +- Observation: Shell protocol actions consistently need a null process-handle check even when Windows does not throw. + Evidence: The fake runtime reproduced null launches for Screenshot, Record, Calculator, Explorer, Downloads, Show Desktop, and Apps Folder. All now raise the localized `Action_LaunchFailed` path and the theory covers all eight launch variants including Search. + +## Decision Log + +- Decision: Prepare version 1.13.0. + Rationale: The release adds user-visible capabilities rather than only backward-compatible bug fixes: File Sorter becomes a one-screen multi-folder workflow and Text Processing gains Literary Editing. Existing settings and public behavior remain backward compatible, so a minor rather than major version is appropriate. + Date/Author: 2026-08-02 / Codex + +- Decision: Review utilities in `UtilityButtonCatalog.All` order and record evidence per item. + Rationale: The catalog is the only authoritative UI order and includes both window utilities and direct system actions. Following it prevents visually less prominent utilities from being skipped. + Date/Author: 2026-08-02 / Codex + +- Decision: Treat crashes, data loss, destructive ambiguity, broken launch paths, missing localized UI, incorrect state persistence, unusable fixed layout, and failing primary workflows as release blockers. + Rationale: Cosmetic preferences and speculative refactors should not destabilize a release candidate. Fixes must be evidence-driven and proportional to user risk. + Date/Author: 2026-08-02 / Codex + +## Outcomes & Retrospective + +The audit is complete. All 18 catalog entries have an itemized result and no known release blocker remains. After the final panel accessibility and hover-setting fixes, AiteBar 1.13.0 builds with zero warnings and errors; 1071 non-WPF and 73 isolated WPF tests pass (1144 total). The published executable remained alive during the eight-second smoke interval. `artifacts/installer/AiteBar-Setup.exe` is 79,456,114 bytes, reports ProductVersion 1.13.0, and matches `SHA256SUMS.txt` at `FABECE024C254FB7F6BB9CB9BA601385C2DB7856D9FEFAD7C3767D0AC41A9CD8`. The local artifact is unsigned, as expected because no signing certificate was supplied. + +## Context and Orientation + +`AiteBar/UtilityButtonCatalog.cs` defines the 18 built-in buttons and their display order. `AiteBar/UtilityRegistry.cs` manages reusable window utility instances. `AiteBar/MainWindow.xaml.cs` and `AiteBar/ActionService.cs` execute direct system actions. Each full utility has a `*Utility.cs` launcher, a `*Window.xaml`/code-behind pair when it has UI, and usually a service or helper that holds testable logic. + +The nine managed window utilities are FileSorter, IconConverter, TimerStopwatch, QuickNote, QRCodeGenerator, ClipboardManager, TextProcessing, and ZenEditor; ColorPicker is a specialized `IUtility` that owns its picker lifecycle without deriving from `UtilityBase`. Search, Screenshot, Record, Calculator, Explorer, Downloads, ShowDesktop, AppsFolder, and Copilot are direct system integrations. + +Release metadata lives in `AiteBar/AiteBar.csproj`, `AiteBar/AssemblyInfo.cs`, and the fallback constant in `installer/AiteBar.iss`. `installer/Build-Installer.ps1` reads the project version and passes it to Inno Setup. `CHANGELOG.md` supplies GitHub release notes, and `.github/workflows/release.yml` requires a changelog section matching the tag or requested release version. + +## Plan of Work + +For every direct system action, inspect argument construction, executable or URI validation, exception handling, and tests. For every window utility, inspect the launcher lifecycle, close/minimize restoration, cancellation and asynchronous exception paths, persistence boundaries, destructive operations, localization keys, accessibility-critical names, fixed geometry, and focused tests. Run each utility's existing focused tests immediately after its review; add a regression test before or with any confirmed fix. + +Review in catalog order. Record each item as Passed, Fixed, or Blocked in `Artifacts and Notes`, with the source files and test evidence. Do not rewrite stable utilities merely for stylistic consistency. Preserve user files, settings, clipboard history privacy behavior, API credentials, and the dirty working tree. + +After all items pass, change the version to 1.13.0 in all synchronized sources and replace the empty Unreleased section with bilingual Added, Changed, and Fixed entries covering the actual release scope. Run `ReleaseVersionTests` to prove metadata alignment. Execute the release workflow's split test strategy locally: one non-WPF batch and each WPF class in an isolated host. Then publish, build the installer, verify its ProductVersion and SHA-256, and start the published executable briefly to prove it initializes without immediate termination. + +## Concrete Steps + +Run commands from `D:\01_Codebdbd\01_projects\aitebar`. + + dotnet build .\AiteBar.sln -c Release + dotnet test .\AiteBar.Tests\AiteBar.Tests.csproj -c Release --no-build + .\installer\Build-Installer.ps1 + +For release-equivalent tests, copy the class lists from `.github/workflows/release.yml`: exclude all WPF classes in one non-WPF run, then run every excluded WPF class separately. The exact command transcript and counts must be recorded after completion. + +## Validation and Acceptance + +Each of the 18 catalog entries must have a recorded review result. Window utilities must construct under WPF tests, fit their current minimum layout, and preserve documented minimize/close behavior. Destructive actions such as File Sorter undo and Clipboard Manager deletion must remain explicit and scoped. AI Text Processing must retain Proofread startup, four independent modes, task-specific model routing, empty-stream fallback, and content-preservation checks. Quick Note and Zen Editor must retain their documented persistence and recovery guarantees. + +Release acceptance requires synchronized 1.13.0 metadata, a matching changelog section, zero build warnings/errors, zero failed tests, one non-empty installer artifact whose ProductVersion is 1.13.0, an updated `SHA256SUMS.txt`, and a published AiteBar process that remains alive during the smoke interval. + +## Idempotence and Recovery + +Reviews and tests are read-only. Source fixes use `apply_patch` and preserve unrelated changes. Build and installer commands can be rerun. The installer script safely replaces publish output and its own temporary files; it must not be run concurrently with another publish. Smoke testing may stop only the executable started from `artifacts/publish/win-x64`, never an installed user instance unless explicitly authorized. + +## Artifacts and Notes + +Audit ledger, updated sequentially: + + 1. Search — Passed: empty input is ignored, query is URI-escaped, Chrome/Edge/default-browser fallback is explicit, and null launch already raises localized Action_SearchFailed. + 2. Screenshot — Fixed: null Windows protocol launch now raises localized Action_LaunchFailed. + 3. Record — Fixed: null Windows protocol launch now raises localized Action_LaunchFailed. + 4. Calculator — Fixed: null process launch now raises localized Action_LaunchFailed. + 5. Explorer — Fixed: null shell launch now raises localized Action_LaunchFailed. + 6. Downloads — Fixed: null shell launch now raises localized Action_LaunchFailed. + 7. File Sorter — Passed: one-screen folder rows, scoped undo state, busy-state close guard, persistent custom folders, localized errors, and focused service/layout/behavior tests reviewed; its WPF behavior tests are now isolated in release CI. + 8. Icon Converter — Passed: stale preview cancellation, guarded conversion/save, overwrite confirmation, and service/integration/layout tests reviewed. + 9. Timer/Stopwatch — Fixed: close-time settings persistence can no longer escape an async-void close handler; compact/full behavior and formatting/layout tests pass. + 10. Color Picker — Fixed: crash fallback now uses Utility_Unavailable localization and owns its dialog. + 11. Quick Note — Passed: final-save close guard, pinned deactivation behavior, Markdown round-trip, links, conflicts, layout, and WPF formatting/close tests reviewed. + 12. QR Code Generator — Passed: validation, cancellation, copy/save exception paths, lifecycle cleanup, shortcuts, and PNG/SVG service tests reviewed. + 13. Clipboard Manager — Passed: runtime subscription cleanup, explicit destructive confirmation, copy suppression, 50-entry/10-KiB limits, optional persistence behavior, integration, and isolated WPF behavior test reviewed. + 14. Show Desktop — Fixed: null Windows shell launch now raises localized Action_LaunchFailed. + 15. Apps Folder — Fixed: null Windows shell launch now raises localized Action_LaunchFailed. + 16. Copilot — Passed: Win+C injection order and unconditional Win-key release now have success and failure regression tests. + 17. Text Processing — Passed: Proofread startup, four independent prompts, task-specific model eligibility, original-input retry, empty-stream fallback, language/content protection, and focused service/UI/model tests reviewed. + 18. Zen Editor — Passed: close-time save guard, autosave, snapshots/backups, recovery, undo history, export, minimize/restore lifecycle, and isolated WPF behavior tests reviewed. + +## Interfaces and Dependencies + +No new dependency is planned. Release fixes should use existing services and helper patterns. Version sources must end as `Version=1.13.0`, `AssemblyVersion=1.13.0.0`, `FileVersion=1.13.0.0`, matching assembly attributes, and Inno fallback `AppVersion "1.13.0"`. The output installer remains `artifacts/installer/AiteBar-Setup.exe`. + +Plan revision note (2026-08-02 01:25Z): Created the release-wide audit plan after inventorying the authoritative utility catalog, dirty working tree, current 1.12.2 metadata, tests, installer script, and GitHub release workflow. + +Plan revision note (2026-08-02 02:27Z): Completed the 18-item ledger, recorded confirmed fixes and the WPF workflow discovery, synchronized 1.13.0, and added final build, test, installer, checksum, and smoke evidence. + +Plan revision note (2026-08-02 03:47Z): Refreshed final release evidence after adding the context tooltip, keyboard focus outline, and optional mouse-hover activation setting. diff --git a/docs/functions.md b/docs/functions.md index c4e5d13..3669119 100644 --- a/docs/functions.md +++ b/docs/functions.md @@ -199,7 +199,7 @@ AiteBar - desktop-утилита для Windows: скрываемая edge-па - Главная панель. - Системный background-обработчик курсора. -- Настройки: сторона панели, монитор, зона активации, задержка. +- Настройки: сторона панели, монитор, зона активации, задержка и переключатель показа при наведении. ### Как использовать @@ -209,7 +209,7 @@ AiteBar - desktop-утилита для Windows: скрываемая edge-па ### Входные данные -Положение курсора; настройки `Edge`, `MonitorIndex`, `ActivationZoneSizePercent`, `ActivationDelayMs`. +Положение курсора; настройки `Edge`, `MonitorIndex`, `ActivationZoneSizePercent`, `ActivationDelayMs`, `ShowPanelOnMouseHover`. ### Результат @@ -217,7 +217,7 @@ AiteBar - desktop-утилита для Windows: скрываемая edge-па ### Ограничения -Панель реагирует на выбранный монитор и выбранную сторону. Если курсор вне зоны или приложение занято анимацией/перетаскиванием панели, показ не выполняется. +Панель реагирует на выбранный монитор и выбранную сторону только при включённом `ShowPanelOnMouseHover`. Если настройка отключена, курсор вне зоны или приложение занято анимацией/перетаскиванием панели, показ не выполняется. Hotkey, tray и указатель положения продолжают открывать панель независимо от этой настройки. ### Связанные функции @@ -1614,7 +1614,7 @@ HEX-цвет формата `#RRGGBB` копируется в clipboard. ### Назначение -Выполняет техническую AI-обработку текста в одном из трёх режимов: проверка орфографии и пунктуации, типографическое оформление или очистка артефактов копирования. Режимы не предназначены для перевода, пересказа или изменения смысла. +Выполняет AI-обработку текста в одном из четырёх независимых режимов: проверка орфографии, грамматики и пунктуации; типографическое оформление; очистка артефактов копирования; литературная редакция. Первые три режима не меняют формулировки. Литературная редакция улучшает ясность, ритм и стиль, но сохраняет язык, смысл, факты, имена, авторскую позицию и структуру абзацев. ### Где находится @@ -1624,7 +1624,7 @@ HEX-цвет формата `#RRGGBB` копируется в clipboard. 1. Настроить хотя бы одно включённое AI-подключение. 2. Ввести текст либо вставить содержимое буфера в позицию курсора; выделенный фрагмент при вставке заменяется. -3. Выбрать режим и автоматический выбор либо конкретную доступную бесплатную текстовую модель. Каждая модель показана один раз независимо от количества API-ключей провайдера. Платные модели, модели с неизвестной стоимостью и генераторы изображений/видео утилита не использует. +3. Выбрать один из четырёх режимов и автоматический выбор либо конкретную доступную бесплатную текстовую модель. Каждая модель показана один раз независимо от количества API-ключей провайдера. Платные модели, модели с неизвестной стоимостью и генераторы изображений/видео утилита не использует. 4. Запустить обработку кнопкой или сочетанием `Ctrl+Enter`. 5. Наблюдать появление результата по мере генерации и время выполнения; при необходимости отменить запрос кнопкой `Отменить` или `Esc`. 6. После успеха переключаться между исходником, результатом и подсвеченным списком изменений. Удалённое показывается красным зачёркнутым, добавленное — зелёным подчёркнутым; открытая команда `Показать изменения` меняется на `Скрыть изменения`. @@ -2318,6 +2318,7 @@ Tray-меню. | `ActivationZoneSizePercent` | Размер зоны активации | Slider UI; layout helper нормализует связанные проценты | `30` | | `PanelSizePercent` | Размер панели вдоль активной оси | Slider UI; расчет layout ограничивает 20-100% | `80` | | `ActivationDelayMs` | Задержка появления панели | Slider UI в миллисекундах | `150` | +| `ShowPanelOnMouseHover` | Показывать панель при задержке указателя в зоне активации | true/false | `true` | | `GlobalHotkeyCtrl` | Ctrl для hotkey показа панели | true/false | `false` | | `GlobalHotkeyAlt` | Alt для hotkey показа панели | true/false | `true` | | `GlobalHotkeyShift` | Shift для hotkey показа панели | true/false | `false` | diff --git a/installer/AiteBar.iss b/installer/AiteBar.iss index 9d991ca..6691915 100644 --- a/installer/AiteBar.iss +++ b/installer/AiteBar.iss @@ -3,7 +3,7 @@ #define AppPublisher "Codebdbd" #define AppExeName "AiteBar.exe" #ifndef AppVersion - #define AppVersion "1.12.2" + #define AppVersion "1.13.0" #endif #define PublishDir "..\artifacts\publish\win-x64"