Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
4120407
Add initial Avalonia UI project for migration
taooceros Jan 15, 2026
e961dc5
Wire up Avalonia UI to actual plugin system
taooceros Jan 15, 2026
2079531
Fix MainWindow to use shared ViewModel from DI and add console logging
taooceros Jan 15, 2026
065dc19
Add global hotkey support for Avalonia UI
taooceros Jan 15, 2026
f3d3f80
Add DynamicData sorting, result persistence, and Windows Shell icon l…
taooceros Jan 15, 2026
2466b90
Fix UI blocking by running plugin queries on thread pool
taooceros Jan 15, 2026
192eb4d
Add context menu support for results in Avalonia UI
taooceros Jan 15, 2026
36e3530
Add internationalization support for Avalonia UI
taooceros Jan 15, 2026
5d80816
Refactor i18n to be self-initializing via DI
taooceros Jan 15, 2026
c68a03e
Delay window show until plugins are initialized
taooceros Jan 16, 2026
ed668f3
Add glyph icon support for results
taooceros Jan 16, 2026
29b2664
Improve glyph support and embed Segoe Fluent Icons
taooceros Jan 17, 2026
0f9b932
Add hotkey recorder with manual modifier tracking
taooceros Jan 18, 2026
8a99881
Add detailed Avalonia migration checklist
taooceros Jan 18, 2026
1d94565
Expand GeneralSettingsPage with ~25 settings using FluentAvalonia Set…
taooceros Jan 18, 2026
1209ca5
Fix flickering & add result highlighting
taooceros Jan 18, 2026
0e1af22
Implement WPF plugin settings hosting in Avalonia
taooceros Jan 19, 2026
fb9721c
refactor: move history storage to infrastructure and implement WPF se…
taooceros Jan 19, 2026
dda5874
Migrate Program plugin settings to Avalonia and fix scroll issues
taooceros Jan 19, 2026
19913fa
Fix OpenSettings command: ensure Main Window hides before showing Set…
taooceros Jan 19, 2026
2b328e5
Fix OpenSettings command: ensure Main Window hides before showing Set…
taooceros Jan 19, 2026
32aa3ff
Fix MainWindow visibility logic:
taooceros Jan 19, 2026
d9bfe49
Fix MainWindow startup visibility and OpenSettings logic
taooceros Jan 19, 2026
0c2f026
Implement comprehensive Plugins Settings page in Avalonia
taooceros Jan 26, 2026
c0d1767
feat(avalonia): add Avalonia settings views for 8 plugins
taooceros Jan 26, 2026
5e1411f
feat(avalonia): add Plugin Store settings page with virtualized grid
taooceros Jan 27, 2026
ae0bdbc
ci: trigger builds on draft PRs and add Avalonia artifact
taooceros Jan 27, 2026
82390b5
ci: add Avalonia build processing to post_build script
taooceros Jan 27, 2026
e726c44
ci: add self-contained publish for Avalonia build
taooceros Jan 27, 2026
e337f9e
feat(explorer): add Avalonia ActionKeywordSetting dialog
taooceros Jan 27, 2026
0681332
fix(explorer): complete Avalonia migration and fix action keyword cra…
shengkai16 Jan 31, 2026
8ee32df
fix(avalonia): fix localization keys in settings pages
shengkai16 Feb 1, 2026
8300235
fix(avalonia): fix enum translation in settings pages
shengkai16 Feb 1, 2026
9c99c7c
feat(avalonia): implement HighlightTextConverter with theme-aware sty…
shengkai16 Feb 1, 2026
c507bb9
chore(git): remove codemap files from tracking and add to gitignore
shengkai16 Feb 1, 2026
fbcd700
ci(github): add avalonia_migration branch to build triggers
shengkai16 Feb 1, 2026
c51c029
Merge branch 'dev' into avalonia_migration
shengkai16 Feb 1, 2026
5e1ec2b
fix(avalonia): update PluginManager API calls for Release build
shengkai16 Feb 1, 2026
54827a1
sk
shengkai16 Feb 4, 2026
0e90bc1
chore(avalonia): remove build cleanup targets and update lock file
Feb 4, 2026
b0dfd0b
feat(avalonia): implement preview hotkey setting and update migration…
Feb 4, 2026
45e2ce4
docs(avalonia): mark text highlighting as done in checklist
Feb 4, 2026
11eefa4
chore: upgrade from .NET 9 to .NET 10
Feb 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions Flow.Launcher.Avalonia/App.axaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<Application xmlns="https://github.com/avaloniaui"

Check failure on line 1 in Flow.Launcher.Avalonia/App.axaml

View workflow job for this annotation

GitHub Actions / Check Spelling

`axaml` is not a recognized word. (check-file-path)

Check failure on line 1 in Flow.Launcher.Avalonia/App.axaml

View workflow job for this annotation

GitHub Actions / Check Spelling

`Avalonia` is not a recognized word. (check-file-path)
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Flow.Launcher.Avalonia"
x:Class="Flow.Launcher.Avalonia.App"
RequestedThemeVariant="Default">
<!-- "Default" ThemeVariant follows system theme variant.
"Dark" or "Light" are other available options. -->

<Application.Styles>
<FluentTheme />
<StyleInclude Source="avares://Flow.Launcher.Avalonia/Themes/Base.axaml"/>
</Application.Styles>

<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<!-- Additional resources can be added here -->
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>
100 changes: 100 additions & 0 deletions Flow.Launcher.Avalonia/App.axaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
using Avalonia;

Check failure on line 1 in Flow.Launcher.Avalonia/App.axaml.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`axaml` is not a recognized word. (check-file-path)

Check failure on line 1 in Flow.Launcher.Avalonia/App.axaml.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`Avalonia` is not a recognized word. (check-file-path)
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.DependencyInjection;
using Flow.Launcher.Avalonia.Helper;
using Flow.Launcher.Avalonia.ViewModel;
using Flow.Launcher.Core.Plugin;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.Storage;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Threading.Tasks;

namespace Flow.Launcher.Avalonia;

public partial class App : Application
{
private static readonly string ClassName = nameof(App);
private Settings? _settings;
private MainViewModel? _mainVM;

public static IPublicAPI? API { get; private set; }

public override void Initialize() => AvaloniaXamlLoader.Load(this);

public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
LoadSettings();
ConfigureDI();

API = Ioc.Default.GetRequiredService<IPublicAPI>();
_mainVM = Ioc.Default.GetRequiredService<MainViewModel>();

desktop.MainWindow = new MainWindow();

// Initialize hotkeys after window is created
HotKeyMapper.Initialize();

Dispatcher.UIThread.Post(async () => await InitializePluginsAsync(), DispatcherPriority.Background);

// Cleanup on exit
desktop.Exit += (_, _) => HotKeyMapper.Shutdown();
}
base.OnFrameworkInitializationCompleted();
}

private void LoadSettings()
{
try
{
var storage = new FlowLauncherJsonStorage<Settings>();
_settings = storage.Load();
_settings.SetStorage(storage);
}
catch (Exception e)
{
Log.Exception(ClassName, "Settings load failed", e);
_settings = new Settings
{
WindowSize = 580, WindowHeightSize = 42, QueryBoxFontSize = 24,
ItemHeightSize = 50, ResultItemFontSize = 14, ResultSubItemFontSize = 12, MaxResultsToShow = 6
};
}
Comment on lines +91 to +107
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Ensure fallback settings still have storage attached.

Lines 95–106 create a new Settings instance on load failure but never call SetStorage, which can prevent saves or trigger null usage later. Consider attaching a fresh storage in the catch block.

🛠️ Suggested fix
        catch (Exception e)
        {
            Log.Exception(ClassName, "Settings load failed", e);
-           _settings = new Settings
+           var storage = new FlowLauncherJsonStorage<Settings>();
+           _settings = new Settings
            {
                WindowSize = 580, WindowHeightSize = 42, QueryBoxFontSize = 24,
                ItemHeightSize = 50, ResultItemFontSize = 14, ResultSubItemFontSize = 12, MaxResultsToShow = 6
            };
+           _settings.SetStorage(storage);
        }
🤖 Prompt for AI Agents
In `@Flow.Launcher.Avalonia/App.axaml.cs` around lines 91 - 107, In
LoadSettings(), the catch creates a fallback Settings instance but doesn't
attach storage; allocate a new FlowLauncherJsonStorage<Settings> (same type used
in the try) and call _settings.SetStorage(storage) after constructing the
fallback so the _settings instance has its storage set and subsequent saves/uses
won't hit null; reference LoadSettings, FlowLauncherJsonStorage<Settings>,
_settings and SetStorage when making the change.

}

private void ConfigureDI()
{
var services = new ServiceCollection();
services.AddSingleton(_settings!);
services.AddSingleton<IAlphabet, PinyinAlphabet>();
services.AddSingleton<StringMatcher>();
services.AddSingleton<MainViewModel>();
services.AddSingleton<IPublicAPI>(sp => new AvaloniaPublicAPI(
sp.GetRequiredService<Settings>(),
() => sp.GetRequiredService<MainViewModel>()));
Ioc.Default.ConfigureServices(services.BuildServiceProvider());
}

private async Task InitializePluginsAsync()
{
try
{
Log.Info(ClassName, "Loading plugins...");
PluginManager.LoadPlugins(_settings!.PluginSettings);
Log.Info(ClassName, $"Loaded {PluginManager.AllPlugins.Count} plugins");

await PluginManager.InitializePluginsAsync();
Log.Info(ClassName, "Plugins initialized");

_mainVM?.OnPluginsReady();
}
catch (Exception e) { Log.Exception(ClassName, "Plugin init failed", e); }
}
}
141 changes: 141 additions & 0 deletions Flow.Launcher.Avalonia/AvaloniaPublicAPI.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
using System;

Check failure on line 1 in Flow.Launcher.Avalonia/AvaloniaPublicAPI.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`Avalonia` is not a recognized word. (check-file-path)

Check failure on line 1 in Flow.Launcher.Avalonia/AvaloniaPublicAPI.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`Avalonia` is not a recognized word. (check-file-path)
using System.Collections.Generic;
using System.IO;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedModels;
using Flow.Launcher.Core.Plugin;
using Flow.Launcher.Avalonia.ViewModel;
using CommunityToolkit.Mvvm.DependencyInjection;

namespace Flow.Launcher.Avalonia;

/// <summary>
/// Minimal IPublicAPI for Avalonia - just enough for plugin queries to work.
/// </summary>
public class AvaloniaPublicAPI : IPublicAPI

Check failure on line 23 in Flow.Launcher.Avalonia/AvaloniaPublicAPI.cs

View workflow job for this annotation

GitHub Actions / build

'AvaloniaPublicAPI' does not implement interface member 'IPublicAPI.GetAllInitializedPlugins(bool)'

Check failure on line 23 in Flow.Launcher.Avalonia/AvaloniaPublicAPI.cs

View workflow job for this annotation

GitHub Actions / build

'AvaloniaPublicAPI' does not implement interface member 'IPublicAPI.GetAllInitializedPlugins(bool)'
{
private readonly Settings _settings;
private readonly Func<MainViewModel> _getMainViewModel;

public AvaloniaPublicAPI(Settings settings, Func<MainViewModel> getMainViewModel)
{
_settings = settings;
_getMainViewModel = getMainViewModel;
}

#pragma warning disable CS0067
public event VisibilityChangedEventHandler? VisibilityChanged;
public event ActualApplicationThemeChangedEventHandler? ActualApplicationThemeChanged;
#pragma warning restore CS0067

// Essential for plugins
public void ChangeQuery(string query, bool requery = false) => _getMainViewModel().QueryText = query;
public string GetTranslation(string key) => key;
public List<PluginPair> GetAllPlugins() => PluginManager.AllPlugins;
public MatchResult FuzzySearch(string query, string stringToCompare) =>
Ioc.Default.GetRequiredService<StringMatcher>().FuzzyMatch(query, stringToCompare);

Comment on lines +27 to +52
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

rg -n "ChangeQuery\s*\(|ReQuery\s*\(|Requery" -g '*.cs' -A 3

Repository: Flow-Launcher/Flow.Launcher

Length of output: 14843


🏁 Script executed:

# Also search for QueryText property and how it's used
rg -n "QueryText\s*=" -g '*.cs' -A 2 -B 2

Repository: Flow-Launcher/Flow.Launcher

Length of output: 6533


🏁 Script executed:

# Look for any methods that might indicate requery intent
rg -n "class MainViewModel|QueryText|ExecuteQuery" -g '*.cs' -A 2

Repository: Flow-Launcher/Flow.Launcher

Length of output: 30993


Respect the requery flag in ChangeQuery.

The requery parameter is currently ignored, causing plugins that pass requery=true to fail to trigger re-execution when the query text remains unchanged. The WPF implementation correctly delegates this to ChangeQueryText, which checks isReQuery and calls Query() accordingly. Avalonia should implement similar logic or delegate to a VM method that respects the flag. Consider making ChangeQuery conditionally call a method that re-executes the query when requery=true and the text is unchanged.

🤖 Prompt for AI Agents
In `@Flow.Launcher.Avalonia/AvaloniaPublicAPI.cs` around lines 27 - 52,
ChangeQuery currently sets _getMainViewModel().QueryText unconditionally and
ignores the requery flag; modify ChangeQuery to retrieve the MainViewModel via
_getMainViewModel(), compare the incoming query to vm.QueryText, set
vm.QueryText if different, and if requery is true and the text is unchanged call
the ViewModel method that re-executes the search (e.g., vm.Query() or the
existing vm.ChangeQueryText/ChangeQuery method that honors isReQuery) so plugins
passing requery=true will force a re-execution; update references to ChangeQuery
in AvaloniaPublicAPI to use vm methods rather than directly assigning QueryText
when requery behavior is required.

// Logging
public void LogDebug(string className, string message, [CallerMemberName] string methodName = "") => Log.Debug(className, message, methodName);
public void LogInfo(string className, string message, [CallerMemberName] string methodName = "") => Log.Info(className, message, methodName);
public void LogWarn(string className, string message, [CallerMemberName] string methodName = "") => Log.Warn(className, message, methodName);
public void LogError(string className, string message, [CallerMemberName] string methodName = "") => Log.Error(className, message, methodName);
public void LogException(string className, string message, Exception e, [CallerMemberName] string methodName = "") => Log.Exception(className, message, e, methodName);

// Shell/URL operations
public void ShellRun(string cmd, string filename = "cmd.exe") =>
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo { FileName = filename, Arguments = $"/c {cmd}", UseShellExecute = true });
Comment on lines +61 to +62
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Potential command injection vulnerability in ShellRun.

The cmd parameter is passed directly to cmd.exe /c without escaping. Malicious input could execute arbitrary commands.

Proposed safer implementation
 public void ShellRun(string cmd, string filename = "cmd.exe") => 
-    System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo { FileName = filename, Arguments = $"/c {cmd}", UseShellExecute = true });
+    System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo 
+    { 
+        FileName = filename, 
+        Arguments = filename == "cmd.exe" ? $"/c \"{cmd.Replace("\"", "\\\"")}\"" : cmd, 
+        UseShellExecute = true 
+    });

Note: The WPF implementation may have similar logic - consider aligning the behavior or documenting the security implications.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public void ShellRun(string cmd, string filename = "cmd.exe") =>
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo { FileName = filename, Arguments = $"/c {cmd}", UseShellExecute = true });
public void ShellRun(string cmd, string filename = "cmd.exe") =>
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
{
FileName = filename,
Arguments = filename == "cmd.exe" ? $"/c \"{cmd.Replace("\"", "\\\"")}\"" : cmd,
UseShellExecute = true
});
🤖 Prompt for AI Agents
In `@Flow.Launcher.Avalonia/AvaloniaPublicAPI.cs` around lines 60 - 61, ShellRun
currently passes the user-supplied cmd directly into "cmd.exe /c" which allows
command injection; update the ShellRun implementation in
AvaloniaPublicAPI.ShellRun to avoid invoking a shell with unescaped input:
either require callers to pass a specific executable and separate arguments
(split/validate the input) and call Process.Start with UseShellExecute = false
and FileName set to the executable and Arguments to a validated args string, or
implement strict validation/whitelisting of allowed commands when invoking
cmd.exe; ensure you do not concatenate raw cmd into "/c {cmd}" and mirror the
safer behavior in the WPF implementation or document the security tradeoffs.

public void OpenUrl(string url, bool? inPrivate = null) =>
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo { FileName = url, UseShellExecute = true });
public void OpenUrl(Uri url, bool? inPrivate = null) => OpenUrl(url.ToString(), inPrivate);
public void OpenWebUrl(string url, bool? inPrivate = null) => OpenUrl(url, inPrivate);
public void OpenWebUrl(Uri url, bool? inPrivate = null) => OpenUrl(url.ToString(), inPrivate);
public void OpenAppUri(Uri appUri) => OpenUrl(appUri);
public void OpenAppUri(string appUri) => OpenUrl(appUri);
public void OpenDirectory(string DirectoryPath, string? FileNameOrFilePath = null) =>
Copy link

@cubic-dev-ai cubic-dev-ai bot Jan 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The FileNameOrFilePath parameter is ignored. When provided, the API should select/highlight that file in the opened directory. Consider using /select, argument with explorer.exe to implement proper file selection.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Flow.Launcher.Avalonia/AvaloniaPublicAPI.cs, line 69:

<comment>The `FileNameOrFilePath` parameter is ignored. When provided, the API should select/highlight that file in the opened directory. Consider using `/select,` argument with `explorer.exe` to implement proper file selection.</comment>

<file context>
@@ -0,0 +1,148 @@
+    public void OpenWebUrl(Uri url, bool? inPrivate = null) => OpenUrl(url.ToString(), inPrivate);
+    public void OpenAppUri(Uri appUri) => OpenUrl(appUri);
+    public void OpenAppUri(string appUri) => OpenUrl(appUri);
+    public void OpenDirectory(string DirectoryPath, string? FileNameOrFilePath = null) => 
+        System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo { FileName = DirectoryPath, UseShellExecute = true });
+
</file context>
Fix with Cubic

System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo { FileName = DirectoryPath, UseShellExecute = true });

// Clipboard
public void CopyToClipboard(string text, bool directCopy = false, bool showDefaultNotification = true)
{
if (global::Avalonia.Application.Current?.ApplicationLifetime is global::Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime desktop)
desktop.MainWindow?.Clipboard?.SetTextAsync(text);
}

// HTTP (delegate to Infrastructure)
public Task<string> HttpGetStringAsync(string url, CancellationToken token = default) => Infrastructure.Http.Http.GetAsync(url, token);
public Task<Stream> HttpGetStreamAsync(string url, CancellationToken token = default) => Infrastructure.Http.Http.GetStreamAsync(url, token);
public Task HttpDownloadAsync(string url, string filePath, Action<double>? reportProgress = null, CancellationToken token = default) =>
Infrastructure.Http.Http.DownloadAsync(url, filePath, reportProgress, token);

// Plugin management
public void AddActionKeyword(string pluginId, string newActionKeyword) => PluginManager.AddActionKeyword(pluginId, newActionKeyword);
public void RemoveActionKeyword(string pluginId, string oldActionKeyword) => PluginManager.RemoveActionKeyword(pluginId, oldActionKeyword);
public bool ActionKeywordAssigned(string actionKeyword) => PluginManager.ActionKeywordRegistered(actionKeyword);
public bool PluginModified(string id) => PluginManager.PluginModified(id);

// Paths
public string GetDataDirectory() => DataLocation.DataDirectory();
public string GetLogDirectory() => Log.CurrentLogDirectory;

// Stubs - not critical for basic queries
public void RestartApp() { }
public void SaveAppAllSettings() { }
public void SavePluginSettings() { }
public Task ReloadAllPluginData() => Task.CompletedTask;
public void CheckForNewUpdate() { }
public void ShowMsgError(string title, string subTitle = "") => LogError("API", $"{title}: {subTitle}");
public void ShowMsgErrorWithButton(string title, string buttonText, Action buttonAction, string subTitle = "") { }
public void ShowMainWindow() { }
public void FocusQueryTextBox() { }
public void HideMainWindow() => _getMainViewModel()?.RequestHide();
public bool IsMainWindowVisible() => true;
public void ShowMsg(string title, string subTitle = "", string iconPath = "") { }
public void ShowMsg(string title, string subTitle, string iconPath, bool useMainWindowAsOwner = true) { }
public void ShowMsgWithButton(string title, string buttonText, Action buttonAction, string subTitle = "", string iconPath = "") { }
public void ShowMsgWithButton(string title, string buttonText, Action buttonAction, string subTitle, string iconPath, bool useMainWindowAsOwner = true) { }
public void OpenSettingDialog() { }
public void RegisterGlobalKeyboardCallback(Func<int, int, SpecialKeyState, bool> callback) { }
public void RemoveGlobalKeyboardCallback(Func<int, int, SpecialKeyState, bool> callback) { }
public T LoadSettingJsonStorage<T>() where T : new() => new T();
public void SaveSettingJsonStorage<T>() where T : new() { }
Comment on lines +115 to +116
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

LoadSettingJsonStorage stub returns empty object, breaking plugin settings persistence.

Returning new T() means plugins won't load their persisted settings. This could cause significant issues for plugins that rely on saved configuration.

Consider delegating to the Infrastructure layer's JSON storage implementation or clearly documenting that settings persistence is not yet supported in Avalonia mode.

🤖 Prompt for AI Agents
In `@Flow.Launcher.Avalonia/AvaloniaPublicAPI.cs` around lines 114 - 115, The
current LoadSettingJsonStorage<T>() and SaveSettingJsonStorage<T>() stubs
return/perform nothing and break plugin settings persistence; update these
methods to delegate to the real JSON storage implementation in the
Infrastructure layer (call the Infrastructure JSON storage/load and save APIs
for type T) instead of returning new T(), and ensure SaveSettingJsonStorage<T>()
forwards the instance to be persisted (or, if no Infrastructure API exists yet,
throw a clear NotImplementedException and document that Avalonia mode does not
support settings persistence); use the method names LoadSettingJsonStorage<T>()
and SaveSettingJsonStorage<T>() as the integration points to wire to the
Infrastructure JSON storage.

public void ToggleGameMode() { }
public void SetGameMode(bool value) { }
public bool IsGameModeOn() => false;
public void ReQuery(bool reselect = true) { var q = _getMainViewModel().QueryText; _getMainViewModel().QueryText = ""; _getMainViewModel().QueryText = q; }
public void BackToQueryResults() { }
public MessageBoxResult ShowMsgBox(string messageBoxText, string caption = "", MessageBoxButton button = MessageBoxButton.OK, MessageBoxImage icon = MessageBoxImage.None, MessageBoxResult defaultResult = MessageBoxResult.OK) => defaultResult;
public Task ShowProgressBoxAsync(string caption, Func<Action<double>, Task> reportProgressAsync, Action? cancelProgress = null) => reportProgressAsync(_ => { });
public void StartLoadingBar() => _getMainViewModel().IsQueryRunning = true;
Copy link

@cubic-dev-ai cubic-dev-ai bot Jan 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Inconsistent null handling - _getMainViewModel() is accessed with . here but with ?. in other methods like HideMainWindow. This could cause NullReferenceException if the ViewModel is not yet initialized.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Flow.Launcher.Avalonia/AvaloniaPublicAPI.cs, line 123:

<comment>Inconsistent null handling - `_getMainViewModel()` is accessed with `.` here but with `?.` in other methods like `HideMainWindow`. This could cause NullReferenceException if the ViewModel is not yet initialized.</comment>

<file context>
@@ -0,0 +1,148 @@
+    public void BackToQueryResults() { }
+    public MessageBoxResult ShowMsgBox(string messageBoxText, string caption = "", MessageBoxButton button = MessageBoxButton.OK, MessageBoxImage icon = MessageBoxImage.None, MessageBoxResult defaultResult = MessageBoxResult.OK) => defaultResult;
+    public Task ShowProgressBoxAsync(string caption, Func<Action<double>, Task> reportProgressAsync, Action? cancelProgress = null) => reportProgressAsync(_ => { });
+    public void StartLoadingBar() => _getMainViewModel().IsQueryRunning = true;
+    public void StopLoadingBar() => _getMainViewModel().IsQueryRunning = false;
+    public List<ThemeData> GetAvailableThemes() => new();
</file context>
Fix with Cubic

public void StopLoadingBar() => _getMainViewModel().IsQueryRunning = false;
public List<ThemeData> GetAvailableThemes() => new();
public ThemeData? GetCurrentTheme() => null;
public bool SetCurrentTheme(ThemeData theme) => false;
public void SavePluginCaches() { }
public Task<T> LoadCacheBinaryStorageAsync<T>(string cacheName, string cacheDirectory, T defaultData) where T : new() => Task.FromResult(defaultData);
public Task SaveCacheBinaryStorageAsync<T>(string cacheName, string cacheDirectory) where T : new() => Task.CompletedTask;
public ValueTask<ImageSource> LoadImageAsync(string path, bool loadFullImage = false, bool cacheImage = true) => new((ImageSource)null!);
public Task<bool> UpdatePluginManifestAsync(bool usePrimaryUrlOnly = false, CancellationToken token = default) => Task.FromResult(true);
public IReadOnlyList<UserPlugin> GetPluginManifest() => new List<UserPlugin>();
public Task<bool> UpdatePluginAsync(PluginMetadata pluginMetadata, UserPlugin plugin, string zipFilePath) => Task.FromResult(false);
public bool InstallPlugin(UserPlugin plugin, string zipFilePath) => false;
public Task<bool> UninstallPluginAsync(PluginMetadata pluginMetadata, bool removePluginSettings = false) => Task.FromResult(false);
public bool IsApplicationDarkTheme() => true;

public long StopwatchLogDebug(string className, string message, Action action, [CallerMemberName] string methodName = "")
{ var sw = System.Diagnostics.Stopwatch.StartNew(); action(); sw.Stop(); LogDebug(className, $"{message}: {sw.ElapsedMilliseconds}ms", methodName); return sw.ElapsedMilliseconds; }
public async Task<long> StopwatchLogDebugAsync(string className, string message, Func<Task> action, [CallerMemberName] string methodName = "")
{ var sw = System.Diagnostics.Stopwatch.StartNew(); await action(); sw.Stop(); LogDebug(className, $"{message}: {sw.ElapsedMilliseconds}ms", methodName); return sw.ElapsedMilliseconds; }
public long StopwatchLogInfo(string className, string message, Action action, [CallerMemberName] string methodName = "")
{ var sw = System.Diagnostics.Stopwatch.StartNew(); action(); sw.Stop(); LogInfo(className, $"{message}: {sw.ElapsedMilliseconds}ms", methodName); return sw.ElapsedMilliseconds; }
public async Task<long> StopwatchLogInfoAsync(string className, string message, Func<Task> action, [CallerMemberName] string methodName = "")
{ var sw = System.Diagnostics.Stopwatch.StartNew(); await action(); sw.Stop(); LogInfo(className, $"{message}: {sw.ElapsedMilliseconds}ms", methodName); return sw.ElapsedMilliseconds; }
}
34 changes: 34 additions & 0 deletions Flow.Launcher.Avalonia/Converters/BoolToIsVisibleConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using System;

Check failure on line 1 in Flow.Launcher.Avalonia/Converters/BoolToIsVisibleConverter.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`Avalonia` is not a recognized word. (check-file-path)
using System.Globalization;
using Avalonia.Data.Converters;

namespace Flow.Launcher.Avalonia.Converters;

/// <summary>
/// Converts a boolean value to IsVisible (Avalonia uses bool for visibility, not Visibility enum)
/// </summary>
public class BoolToIsVisibleConverter : IValueConverter
{
/// <summary>
/// If true, inverts the boolean value (true becomes false, false becomes true)
/// </summary>
public bool Invert { get; set; }

public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is bool boolValue)
{
return Invert ? !boolValue : boolValue;
}
return false;
}

public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is bool boolValue)
{
return Invert ? !boolValue : boolValue;
}
return false;
}
}
Loading
Loading