diff --git a/PolyPilot.IntegrationTests/AdvancedCliConfigTests.cs b/PolyPilot.IntegrationTests/AdvancedCliConfigTests.cs new file mode 100644 index 0000000000..4a38a56692 --- /dev/null +++ b/PolyPilot.IntegrationTests/AdvancedCliConfigTests.cs @@ -0,0 +1,59 @@ +using PolyPilot.IntegrationTests.Fixtures; + +namespace PolyPilot.IntegrationTests; + +/// +/// Integration tests for the Advanced CLI config settings in the Settings page. +/// Verifies that the Advanced section renders with the expected toggles +/// (CompactPaste, RespectGitignore, DisableAllHooks) end-to-end through +/// the live Blazor UI via DevFlow CDP. +/// Related to issue #698 (Expose additional CLI config options in Settings UI). +/// +[Collection("PolyPilot")] +[Trait("Category", "AdvancedCliConfig")] +public class AdvancedCliConfigTests : IntegrationTestBase +{ + public AdvancedCliConfigTests(AppFixture app, ITestOutputHelper output) + : base(app, output) { } + + [Fact] + public async Task SettingsPage_ShowsAdvancedSection() + { + await WaitForCdpReadyAsync(); + + // Navigate to settings + await ClickAsync("[href='/settings'], .settings-link, a[title='Settings']"); + await WaitForAsync("#settings-page, .settings-container", TimeSpan.FromSeconds(10)); + + // The Advanced section should be present in the page + var hasAdvanced = await ExistsAsync("#settings-advanced"); + Output.WriteLine($"Advanced section visible: {hasAdvanced}"); + Assert.True(hasAdvanced, "Advanced section (#settings-advanced) should be visible on the Settings page"); + + await ScreenshotAsync("settings-advanced-section"); + } + + [Fact] + public async Task AdvancedSection_HasCliConfigToggles() + { + await WaitForCdpReadyAsync(); + + // Navigate to settings + await ClickAsync("[href='/settings'], .settings-link, a[title='Settings']"); + await WaitForAsync("#settings-page, .settings-container", TimeSpan.FromSeconds(10)); + + // Scroll to and check for the Advanced navigation item + var navVisible = await ExistsAsync(".settings-nav-item"); + Output.WriteLine($"Nav items visible: {navVisible}"); + Assert.True(navVisible, "Settings nav items should be visible"); + + // Check the page text contains our setting labels + var pageText = await GetTextAsync("#settings-advanced") ?? ""; + Output.WriteLine($"Advanced section text length: {pageText.Length}"); + Assert.Contains("Compact Paste", pageText); + Assert.Contains("Respect .gitignore", pageText); + Assert.Contains("Disable All Hooks", pageText); + + await ScreenshotAsync("settings-advanced-toggles"); + } +} diff --git a/PolyPilot.Tests/ConnectionSettingsTests.cs b/PolyPilot.Tests/ConnectionSettingsTests.cs index b50b19c765..1626e96cec 100644 --- a/PolyPilot.Tests/ConnectionSettingsTests.cs +++ b/PolyPilot.Tests/ConnectionSettingsTests.cs @@ -686,4 +686,231 @@ private void Dispose() { try { Directory.Delete(_testDir, true); } catch { } } + + // ── Advanced CLI config tests ─────────────────────────────────── + + [Fact] + public void DefaultValues_AdvancedCliConfig_AreFalse() + { + var settings = new ConnectionSettings(); + Assert.False(settings.CompactPaste); + Assert.False(settings.RespectGitignore); + Assert.False(settings.DisableAllHooks); + } + + [Fact] + public void RoundTrip_AdvancedCliConfig() + { + var original = new ConnectionSettings + { + CompactPaste = true, + RespectGitignore = true, + DisableAllHooks = true + }; + + var json = JsonSerializer.Serialize(original); + var loaded = JsonSerializer.Deserialize(json); + + Assert.NotNull(loaded); + Assert.True(loaded!.CompactPaste); + Assert.True(loaded.RespectGitignore); + Assert.True(loaded.DisableAllHooks); + } + + [Fact] + public void BackwardCompatibility_OldJson_AdvancedCliConfigDefaultsFalse() + { + var json = """{"Mode":0,"Host":"localhost","Port":4321}"""; + var loaded = JsonSerializer.Deserialize(json); + + Assert.NotNull(loaded); + Assert.False(loaded!.CompactPaste); + Assert.False(loaded.RespectGitignore); + Assert.False(loaded.DisableAllHooks); + } + + [Fact] + public void SyncCliConfig_WritesConfigFile() + { + var tempDir = Path.Combine(Path.GetTempPath(), $"copilot-test-{Guid.NewGuid():N}"); + try + { + var settings = new ConnectionSettings + { + CompactPaste = true, + RespectGitignore = false, + DisableAllHooks = true + }; + settings.SyncCliConfig(tempDir); + + var configPath = Path.Combine(tempDir, "config.json"); + Assert.True(File.Exists(configPath), "config.json should be created"); + + using var doc = JsonDocument.Parse(File.ReadAllText(configPath)); + Assert.True(doc.RootElement.GetProperty("compactPaste").GetBoolean()); + Assert.False(doc.RootElement.GetProperty("respectGitignore").GetBoolean()); + Assert.True(doc.RootElement.GetProperty("disableAllHooks").GetBoolean()); + } + finally + { + try { Directory.Delete(tempDir, true); } catch { } + } + } + + [Fact] + public void SyncCliConfig_PreservesExistingKeys() + { + var tempDir = Path.Combine(Path.GetTempPath(), $"copilot-test-{Guid.NewGuid():N}"); + try + { + // Pre-populate config with an existing key + Directory.CreateDirectory(tempDir); + File.WriteAllText( + Path.Combine(tempDir, "config.json"), + """{"existingKey": "existingValue", "compactPaste": false}"""); + + var settings = new ConnectionSettings { CompactPaste = true }; + settings.SyncCliConfig(tempDir); + + var configPath = Path.Combine(tempDir, "config.json"); + using var doc = JsonDocument.Parse(File.ReadAllText(configPath)); + Assert.Equal("existingValue", doc.RootElement.GetProperty("existingKey").GetString()); + Assert.True(doc.RootElement.GetProperty("compactPaste").GetBoolean()); + } + finally + { + try { Directory.Delete(tempDir, true); } catch { } + } + } + + [Fact] + public void SyncCliConfig_CorruptFile_AbortsWithoutOverwriting() + { + var tempDir = Path.Combine(Path.GetTempPath(), $"copilot-test-{Guid.NewGuid():N}"); + try + { + Directory.CreateDirectory(tempDir); + var configPath = Path.Combine(tempDir, "config.json"); + var corruptContent = "{ not valid json !!!"; + File.WriteAllText(configPath, corruptContent); + + var settings = new ConnectionSettings { CompactPaste = true }; + settings.SyncCliConfig(tempDir); + + // The corrupt file should NOT be overwritten + var afterContent = File.ReadAllText(configPath); + Assert.Equal(corruptContent, afterContent); + } + finally + { + try { Directory.Delete(tempDir, true); } catch { } + } + } + + [Fact] + public void SyncCliConfig_AtomicWrite_UsesTempFile() + { + var tempDir = Path.Combine(Path.GetTempPath(), $"copilot-test-{Guid.NewGuid():N}"); + try + { + var settings = new ConnectionSettings { CompactPaste = true }; + settings.SyncCliConfig(tempDir); + + var configPath = Path.Combine(tempDir, "config.json"); + Assert.True(File.Exists(configPath), "config.json should exist after sync"); + + // Verify the temp file was cleaned up (rename happened) + Assert.False(File.Exists(configPath + ".tmp"), "Temp file should not remain after atomic write"); + } + finally + { + try { Directory.Delete(tempDir, true); } catch { } + } + } + + [Fact] + public void ImportCliConfigValues_ImportsFromConfigJson() + { + var tempDir = Path.Combine(Path.GetTempPath(), $"copilot-test-{Guid.NewGuid():N}"); + try + { + Directory.CreateDirectory(tempDir); + File.WriteAllText( + Path.Combine(tempDir, "config.json"), + """{"compactPaste": true, "respectGitignore": true, "disableAllHooks": false}"""); + + var settings = new ConnectionSettings(); + Assert.False(settings.CompactPaste); + Assert.False(settings.RespectGitignore); + + bool changed = settings.ImportCliConfigValues(tempDir); + + Assert.True(changed); + Assert.True(settings.CompactPaste); + Assert.True(settings.RespectGitignore); + Assert.False(settings.DisableAllHooks); + } + finally + { + try { Directory.Delete(tempDir, true); } catch { } + } + } + + [Fact] + public void ImportCliConfigValues_NoFile_ReturnsFalse() + { + var tempDir = Path.Combine(Path.GetTempPath(), $"copilot-test-{Guid.NewGuid():N}"); + try + { + Directory.CreateDirectory(tempDir); + var settings = new ConnectionSettings(); + bool changed = settings.ImportCliConfigValues(tempDir); + Assert.False(changed); + } + finally + { + try { Directory.Delete(tempDir, true); } catch { } + } + } + + [Fact] + public void ImportCliConfigValues_CorruptFile_ReturnsFalse() + { + var tempDir = Path.Combine(Path.GetTempPath(), $"copilot-test-{Guid.NewGuid():N}"); + try + { + Directory.CreateDirectory(tempDir); + File.WriteAllText(Path.Combine(tempDir, "config.json"), "not json!"); + + var settings = new ConnectionSettings(); + bool changed = settings.ImportCliConfigValues(tempDir); + Assert.False(changed); + Assert.False(settings.CompactPaste); + } + finally + { + try { Directory.Delete(tempDir, true); } catch { } + } + } + + [Fact] + public void ImportCliConfigValues_NoChange_ReturnsFalse() + { + var tempDir = Path.Combine(Path.GetTempPath(), $"copilot-test-{Guid.NewGuid():N}"); + try + { + Directory.CreateDirectory(tempDir); + File.WriteAllText( + Path.Combine(tempDir, "config.json"), + """{"compactPaste": false, "respectGitignore": false, "disableAllHooks": false}"""); + + var settings = new ConnectionSettings(); + bool changed = settings.ImportCliConfigValues(tempDir); + Assert.False(changed); // defaults match file + } + finally + { + try { Directory.Delete(tempDir, true); } catch { } + } + } } diff --git a/PolyPilot.Tests/SettingsRegistryTests.cs b/PolyPilot.Tests/SettingsRegistryTests.cs index 769a5d57a7..c00658b927 100644 --- a/PolyPilot.Tests/SettingsRegistryTests.cs +++ b/PolyPilot.Tests/SettingsRegistryTests.cs @@ -358,4 +358,112 @@ public void Editor_VisibleOnDesktopOnly() ctx.IsDesktop = false; Assert.False(desc.IsVisible!(ctx)); } + + // ── Advanced CLI config tests ─────────────────────────────────── + + [Fact] + public void Categories_ContainsAdvanced() + { + Assert.Contains("Advanced", SettingsRegistry.Categories); + } + + [Fact] + public void Advanced_CompactPaste_DefaultFalse() + { + var ctx = CreateContext(); + var desc = SettingsRegistry.All.First(s => s.Id == "advanced.compactPaste"); + Assert.False((bool)desc.GetValue!(ctx)!); + } + + [Fact] + public void Advanced_CompactPaste_ToggleValue() + { + var settings = new ConnectionSettings { CompactPaste = false }; + var ctx = CreateContext(settings); + var desc = SettingsRegistry.All.First(s => s.Id == "advanced.compactPaste"); + desc.SetValue!(ctx, true); + Assert.True(settings.CompactPaste); + } + + [Fact] + public void Advanced_RespectGitignore_ToggleValue() + { + var settings = new ConnectionSettings(); + var ctx = CreateContext(settings); + var desc = SettingsRegistry.All.First(s => s.Id == "advanced.respectGitignore"); + Assert.False((bool)desc.GetValue!(ctx)!); + desc.SetValue!(ctx, true); + Assert.True(settings.RespectGitignore); + } + + [Fact] + public void Advanced_DisableAllHooks_ToggleValue() + { + var settings = new ConnectionSettings(); + var ctx = CreateContext(settings); + var desc = SettingsRegistry.All.First(s => s.Id == "advanced.disableAllHooks"); + Assert.False((bool)desc.GetValue!(ctx)!); + desc.SetValue!(ctx, true); + Assert.True(settings.DisableAllHooks); + } + + [Fact] + public void Advanced_HiddenInRemoteMode() + { + var settings = new ConnectionSettings { Mode = ConnectionMode.Remote }; + var ctx = CreateContext(settings); + var desc = SettingsRegistry.All.First(s => s.Id == "advanced.compactPaste"); + Assert.False(desc.IsVisible!(ctx)); + } + + [Fact] + public void Advanced_HiddenInDemoMode() + { + var settings = new ConnectionSettings { Mode = ConnectionMode.Demo }; + var ctx = CreateContext(settings); + var desc = SettingsRegistry.All.First(s => s.Id == "advanced.respectGitignore"); + Assert.False(desc.IsVisible!(ctx)); + } + + [Fact] + public void Advanced_VisibleInPersistentMode() + { + var settings = new ConnectionSettings { Mode = ConnectionMode.Persistent }; + var ctx = CreateContext(settings); + var desc = SettingsRegistry.All.First(s => s.Id == "advanced.disableAllHooks"); + Assert.True(desc.IsVisible!(ctx)); + } + + [Fact] + public void Advanced_VisibleInEmbeddedMode() + { + var settings = new ConnectionSettings { Mode = ConnectionMode.Embedded }; + var ctx = CreateContext(settings); + var compactPaste = SettingsRegistry.All.First(s => s.Id == "advanced.compactPaste"); + Assert.True(compactPaste.IsVisible!(ctx)); + } + + [Fact] + public void Search_FindsAdvancedByKeyword() + { + var ctx = CreateContext(); + var results = SettingsRegistry.Search("compact paste", ctx).ToList(); + Assert.Contains(results, s => s.Id == "advanced.compactPaste"); + } + + [Fact] + public void Search_FindsAdvancedByGitignore() + { + var ctx = CreateContext(); + var results = SettingsRegistry.Search("gitignore", ctx).ToList(); + Assert.Contains(results, s => s.Id == "advanced.respectGitignore"); + } + + [Fact] + public void Search_FindsAdvancedByHooks() + { + var ctx = CreateContext(); + var results = SettingsRegistry.Search("hooks", ctx).ToList(); + Assert.Contains(results, s => s.Id == "advanced.disableAllHooks"); + } } diff --git a/PolyPilot/Components/Pages/Settings.razor b/PolyPilot/Components/Pages/Settings.razor index e6e0851895..2b8d3bbfac 100644 --- a/PolyPilot/Components/Pages/Settings.razor +++ b/PolyPilot/Components/Pages/Settings.razor @@ -67,6 +67,13 @@ Diagnostics + @if (settings.Mode != ConnectionMode.Remote && settings.Mode != ConnectionMode.Demo) + { + + } } } @@ -739,6 +746,24 @@ } + @{ var advancedSettings = SettingsRegistry.ForCategory("Advanced", settingsCtx).ToList(); } + @if (advancedSettings.Any()) + { +
+

Advanced

+
+

Copilot CLI configuration options. These are written to ~/.copilot/config.json and take effect on the next session.

+ @foreach (var desc in advancedSettings) + { + @if (SettingMatchesSearch(desc)) + { + + } + } +
+
+ } + @if (!string.IsNullOrEmpty(statusMessage)) {
@(statusClass == "success" ? "✓ " : statusClass == "error" ? "✗ " : "")@statusMessage
@@ -834,6 +859,7 @@ "developer" => SectionVisible("auto update main git watch relaunch rebuild cli source built-in system repository repo clone worktree storage root directory dev drive"), "plugins" => SectionVisible("plugins provider extension dll assembly trust enable disable"), "diagnostics" => SectionVisible("logs diagnostics troubleshoot crash event console"), + "advanced" => SectionVisible("compact paste gitignore hooks disable cli config advanced"), _ => true }; } @@ -894,6 +920,11 @@ CopilotService.CodespacesEnabled = settings.CodespacesEnabled && settings.Mode == ConnectionMode.Embedded; CopilotService.NotifyStateChanged(); break; + case "advanced.compactPaste": + case "advanced.respectGitignore": + case "advanced.disableAllHooks": + settings.SyncCliConfig(); + break; } SaveSettingsQuietly(); StateHasChanged(); @@ -927,6 +958,15 @@ { settings = ConnectionSettings.Load(); settings.RepositoryStorageRoot = ConnectionSettings.NormalizeRepositoryStorageRoot(settings.RepositoryStorageRoot); + + // Import CLI config values from ~/.copilot/config.json so manual edits are respected + if (settings.Mode != ConnectionMode.Remote && settings.Mode != ConnectionMode.Demo) + { + if (settings.ImportCliConfigValues()) + { + try { settings.Save(); } catch { /* best-effort */ } + } + } _initialCliSource = settings.CliSource; _initialMode = settings.Mode; _initialRepositoryStorageRoot = NormalizePathForCompare(settings.RepositoryStorageRoot); diff --git a/PolyPilot/Models/ConnectionSettings.cs b/PolyPilot/Models/ConnectionSettings.cs index 0c84061f13..0fc8de1673 100644 --- a/PolyPilot/Models/ConnectionSettings.cs +++ b/PolyPilot/Models/ConnectionSettings.cs @@ -131,6 +131,28 @@ public string? ServerPassword /// public bool EnableVerboseEventTracing { get; set; } = false; + // ── Advanced CLI config ───────────────────────────────────────── + // These map to Copilot CLI configuration options and are synced + // to ~/.copilot/config.json so the CLI process picks them up. + + /// + /// When true, the CLI collapses large pasted content into a compact + /// representation to save context-window tokens. + /// + public bool CompactPaste { get; set; } = false; + + /// + /// When true, the CLI excludes files matched by .gitignore from + /// the working-tree context it sends to the model. + /// + public bool RespectGitignore { get; set; } = false; + + /// + /// When true, all Copilot CLI hooks (pre-tool-use, post-tool-use, etc.) + /// are globally disabled for every session. + /// + public bool DisableAllHooks { get; set; } = false; + /// /// Normalizes a remote URL by ensuring it has an http(s):// scheme. /// Plain IPs/hostnames get http://, devtunnels/known TLS hosts get https://. @@ -345,6 +367,121 @@ public bool Save() catch { return false; } } + /// + /// Writes the advanced CLI config values (CompactPaste, RespectGitignore, + /// DisableAllHooks) to ~/.copilot/config.json so the Copilot CLI + /// process picks them up. Merges with any existing keys in the file. + /// + /// Override for testing — pass the directory to + /// write config.json into. When null, defaults to ~/.copilot/. + public void SyncCliConfig(string? copilotConfigDir = null) + { + try + { + if (copilotConfigDir == null) + { + var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + if (string.IsNullOrEmpty(home)) return; + copilotConfigDir = Path.Combine(home, ".copilot"); + } + Directory.CreateDirectory(copilotConfigDir); + var configPath = Path.Combine(copilotConfigDir, "config.json"); + + // Read existing config to preserve unrelated keys + var existing = new Dictionary(); + if (File.Exists(configPath)) + { + try + { + using var doc = JsonDocument.Parse(File.ReadAllText(configPath)); + foreach (var prop in doc.RootElement.EnumerateObject()) + existing[prop.Name] = prop.Value.Clone(); + } + catch { return; /* abort sync — don't overwrite a corrupt file and lose unrelated keys */ } + } + + // Merge our values + using var ms = new System.IO.MemoryStream(); + using (var writer = new Utf8JsonWriter(ms, new JsonWriterOptions { Indented = true })) + { + writer.WriteStartObject(); + // Write all existing keys first (excluding ours to avoid duplicates) + var ourKeys = new HashSet(StringComparer.OrdinalIgnoreCase) + { "compactPaste", "respectGitignore", "disableAllHooks" }; + foreach (var kvp in existing) + { + if (!ourKeys.Contains(kvp.Key)) + { + writer.WritePropertyName(kvp.Key); + kvp.Value.WriteTo(writer); + } + } + // Write our values + writer.WriteBoolean("compactPaste", CompactPaste); + writer.WriteBoolean("respectGitignore", RespectGitignore); + writer.WriteBoolean("disableAllHooks", DisableAllHooks); + writer.WriteEndObject(); + } + // Atomic write: write to temp file then rename + var tempPath = configPath + ".tmp"; + File.WriteAllBytes(tempPath, ms.ToArray()); + File.Move(tempPath, configPath, overwrite: true); + } + catch + { + // Best-effort — don't crash the app if the config write fails + } + } + + /// + /// Reads ~/.copilot/config.json and imports the CLI config values + /// (compactPaste, respectGitignore, disableAllHooks) into this settings instance. + /// Call this at startup / Settings page load so manual edits to config.json are respected + /// and the UI stays in sync with the CLI's actual configuration. + /// + /// Override for testing — pass the directory to + /// read config.json from. When null, defaults to ~/.copilot/. + /// True if any value was imported (caller should persist the change). + public bool ImportCliConfigValues(string? copilotConfigDir = null) + { + try + { + if (copilotConfigDir == null) + { + var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + if (string.IsNullOrEmpty(home)) return false; + copilotConfigDir = Path.Combine(home, ".copilot"); + } + var configPath = Path.Combine(copilotConfigDir, "config.json"); + if (!File.Exists(configPath)) return false; + + using var doc = JsonDocument.Parse(File.ReadAllText(configPath)); + var root = doc.RootElement; + bool changed = false; + + if (root.TryGetProperty("compactPaste", out var cp) && (cp.ValueKind == JsonValueKind.True || cp.ValueKind == JsonValueKind.False)) + { + var val = cp.GetBoolean(); + if (CompactPaste != val) { CompactPaste = val; changed = true; } + } + if (root.TryGetProperty("respectGitignore", out var rg) && (rg.ValueKind == JsonValueKind.True || rg.ValueKind == JsonValueKind.False)) + { + var val = rg.GetBoolean(); + if (RespectGitignore != val) { RespectGitignore = val; changed = true; } + } + if (root.TryGetProperty("disableAllHooks", out var dh) && (dh.ValueKind == JsonValueKind.True || dh.ValueKind == JsonValueKind.False)) + { + var val = dh.GetBoolean(); + if (DisableAllHooks != val) { DisableAllHooks = val; changed = true; } + } + return changed; + } + catch + { + return false; // Best-effort — don't crash on read failure + } + } + #if MACCATALYST /// /// One-time reverse migration: PR 341 moved ServerPassword/RemoteToken/LanToken to diff --git a/PolyPilot/Services/SettingsRegistry.cs b/PolyPilot/Services/SettingsRegistry.cs index 64e02fd54e..a67ca56ca6 100644 --- a/PolyPilot/Services/SettingsRegistry.cs +++ b/PolyPilot/Services/SettingsRegistry.cs @@ -389,6 +389,68 @@ private static List Build() SearchKeywords = "auto update main git watch relaunch rebuild developer", }); + // ── Advanced ──────────────────────────────────────────────── + + list.Add(new SettingDescriptor + { + Id = "advanced.compactPaste", + Label = "Compact Paste", + Description = "Collapse large pasted content into a compact representation to save context-window tokens.", + Category = "Advanced", + Section = "CLI Config", + Type = SettingType.Bool, + Order = 10, + SearchKeywords = "compact paste collapse token context cli config advanced", + GetValue = ctx => ctx.Settings.CompactPaste, + SetValue = (ctx, v) => + { + if (v is bool b) + ctx.Settings.CompactPaste = b; + }, + IsVisible = ctx => ctx.Settings.Mode != ConnectionMode.Remote + && ctx.Settings.Mode != ConnectionMode.Demo + }); + + list.Add(new SettingDescriptor + { + Id = "advanced.respectGitignore", + Label = "Respect .gitignore", + Description = "Exclude files matched by .gitignore from the working-tree context sent to the model.", + Category = "Advanced", + Section = "CLI Config", + Type = SettingType.Bool, + Order = 20, + SearchKeywords = "gitignore ignore files context exclude respect cli config advanced", + GetValue = ctx => ctx.Settings.RespectGitignore, + SetValue = (ctx, v) => + { + if (v is bool b) + ctx.Settings.RespectGitignore = b; + }, + IsVisible = ctx => ctx.Settings.Mode != ConnectionMode.Remote + && ctx.Settings.Mode != ConnectionMode.Demo + }); + + list.Add(new SettingDescriptor + { + Id = "advanced.disableAllHooks", + Label = "Disable All Hooks", + Description = "Globally disable all Copilot CLI hooks (pre-tool-use, post-tool-use, etc.) for every session.", + Category = "Advanced", + Section = "CLI Config", + Type = SettingType.Bool, + Order = 30, + SearchKeywords = "hooks disable all global toggle cli config advanced", + GetValue = ctx => ctx.Settings.DisableAllHooks, + SetValue = (ctx, v) => + { + if (v is bool b) + ctx.Settings.DisableAllHooks = b; + }, + IsVisible = ctx => ctx.Settings.Mode != ConnectionMode.Remote + && ctx.Settings.Mode != ConnectionMode.Demo + }); + return list; }