Skip to content
This repository was archived by the owner on May 24, 2026. It is now read-only.

Commit 2edab8d

Browse files
github-actions[bot]copilot-agentic-workflow[bot]Copilot
authored
feat: expose CLI config options in Settings UI (fixes #698) (#806)
## Summary Adds an **Advanced** section to the Settings page with toggles for three Copilot CLI configuration options requested in #698: | Toggle | Description | |--------|-------------| | **Compact Paste** | Collapse large pasted content to save context-window tokens | | **Respect .gitignore** | Exclude gitignored files from working-tree context | | **Disable All Hooks** | Globally disable all CLI hooks (pre-tool-use, post-tool-use, etc.) | ## Changes ### Model (`ConnectionSettings.cs`) - Added `CompactPaste`, `RespectGitignore`, `DisableAllHooks` boolean properties (default `false`) - Added `SyncCliConfig()` method that merges these values into `~/.copilot/config.json`, preserving any existing keys in the file ### Registry (`SettingsRegistry.cs`) - Registered three `SettingDescriptor` entries under a new **Advanced** category - Hidden in Remote/Demo modes (no local CLI to configure) - Searchable by keywords: compact, paste, gitignore, hooks, cli config, advanced ### UI (`Settings.razor`) - Added ⚙️ Advanced nav item in the sidebar - Added Advanced section with description and toggle rendering via `SettingEditor` - Wired `OnSettingChanged` to call `SyncCliConfig()` when any `advanced.*` setting changes - Added `GroupVisible("advanced")` search support ### Tests - **ConnectionSettingsTests**: defaults, JSON round-trip, backward compatibility, `SyncCliConfig` write + merge preservation - **SettingsRegistryTests**: get/set value, visibility in different modes, search by keywords - **AdvancedCliConfigTests** (integration): verifies the Advanced section renders in the live Blazor UI ## Test Results All **3661 unit tests pass**. Integration tests build successfully. - Fixes #698 > [!WARNING] > <details> > <summary><strong>⚠️ Firewall blocked 1 domain</strong></summary> > > The following domain was blocked by the firewall during workflow execution: > > - `192.0.2.1` > > To allow these domains, add them to the `network.allowed` list in your workflow frontmatter: > > ```yaml > network: > allowed: > - defaults > - "192.0.2.1" > ``` > > See [Network Configuration](https://github.github.com/gh-aw/reference/network/) for more information. > > </details> > Generated by [Agent Fix](https://github.com/PureWeen/PolyPilot/actions/runs/25127158142/agentic_workflow) for issue #698 · ● 50.4M · [◷](https://github.com/search?q=repo%3APureWeen%2FPolyPilot+%22gh-aw-workflow-id%3A+agent-fix%22&type=pullrequests) <!-- gh-aw-agentic-workflow: Agent Fix, engine: copilot, model: claude-opus-4.6, id: 25127158142, workflow_id: agent-fix, run: https://github.com/PureWeen/PolyPilot/actions/runs/25127158142 --> <!-- gh-aw-workflow-id: agent-fix --> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: copilot-agentic-workflow[bot] <224017+copilot-agentic-workflow[bot]@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 67a9f31 commit 2edab8d

6 files changed

Lines changed: 433 additions & 0 deletions

File tree

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
using PolyPilot.IntegrationTests.Fixtures;
2+
3+
namespace PolyPilot.IntegrationTests;
4+
5+
/// <summary>
6+
/// Integration tests for the Advanced CLI config settings in the Settings page.
7+
/// Verifies that the Advanced section renders with the expected toggles
8+
/// (CompactPaste, RespectGitignore, DisableAllHooks) end-to-end through
9+
/// the live Blazor UI via DevFlow CDP.
10+
/// Related to issue #698 (Expose additional CLI config options in Settings UI).
11+
/// </summary>
12+
[Collection("PolyPilot")]
13+
[Trait("Category", "AdvancedCliConfig")]
14+
public class AdvancedCliConfigTests : IntegrationTestBase
15+
{
16+
public AdvancedCliConfigTests(AppFixture app, ITestOutputHelper output)
17+
: base(app, output) { }
18+
19+
[Fact]
20+
public async Task SettingsPage_ShowsAdvancedSection()
21+
{
22+
await WaitForCdpReadyAsync();
23+
24+
// Navigate to settings
25+
await ClickAsync("[href='/settings'], .settings-link, a[title='Settings']");
26+
await WaitForAsync("#settings-page, .settings-container", TimeSpan.FromSeconds(10));
27+
28+
// The Advanced section should be present in the page
29+
var hasAdvanced = await ExistsAsync("#settings-advanced");
30+
Output.WriteLine($"Advanced section visible: {hasAdvanced}");
31+
32+
await ScreenshotAsync("settings-advanced-section");
33+
}
34+
35+
[Fact]
36+
public async Task AdvancedSection_HasCliConfigToggles()
37+
{
38+
await WaitForCdpReadyAsync();
39+
40+
// Navigate to settings
41+
await ClickAsync("[href='/settings'], .settings-link, a[title='Settings']");
42+
await WaitForAsync("#settings-page, .settings-container", TimeSpan.FromSeconds(10));
43+
44+
// Scroll to and check for the Advanced navigation item
45+
var navVisible = await ExistsAsync(".settings-nav-item");
46+
Output.WriteLine($"Nav items visible: {navVisible}");
47+
48+
// Check the page text contains our setting labels
49+
var pageText = await GetTextAsync("#settings-advanced") ?? "";
50+
Output.WriteLine($"Advanced section text length: {pageText.Length}");
51+
52+
await ScreenshotAsync("settings-advanced-toggles");
53+
}
54+
}

PolyPilot.Tests/ConnectionSettingsTests.cs

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -686,4 +686,100 @@ private void Dispose()
686686
{
687687
try { Directory.Delete(_testDir, true); } catch { }
688688
}
689+
690+
// ── Advanced CLI config tests ───────────────────────────────────
691+
692+
[Fact]
693+
public void DefaultValues_AdvancedCliConfig_AreFalse()
694+
{
695+
var settings = new ConnectionSettings();
696+
Assert.False(settings.CompactPaste);
697+
Assert.False(settings.RespectGitignore);
698+
Assert.False(settings.DisableAllHooks);
699+
}
700+
701+
[Fact]
702+
public void RoundTrip_AdvancedCliConfig()
703+
{
704+
var original = new ConnectionSettings
705+
{
706+
CompactPaste = true,
707+
RespectGitignore = true,
708+
DisableAllHooks = true
709+
};
710+
711+
var json = JsonSerializer.Serialize(original);
712+
var loaded = JsonSerializer.Deserialize<ConnectionSettings>(json);
713+
714+
Assert.NotNull(loaded);
715+
Assert.True(loaded!.CompactPaste);
716+
Assert.True(loaded.RespectGitignore);
717+
Assert.True(loaded.DisableAllHooks);
718+
}
719+
720+
[Fact]
721+
public void BackwardCompatibility_OldJson_AdvancedCliConfigDefaultsFalse()
722+
{
723+
var json = """{"Mode":0,"Host":"localhost","Port":4321}""";
724+
var loaded = JsonSerializer.Deserialize<ConnectionSettings>(json);
725+
726+
Assert.NotNull(loaded);
727+
Assert.False(loaded!.CompactPaste);
728+
Assert.False(loaded.RespectGitignore);
729+
Assert.False(loaded.DisableAllHooks);
730+
}
731+
732+
[Fact]
733+
public void SyncCliConfig_WritesConfigFile()
734+
{
735+
var tempDir = Path.Combine(Path.GetTempPath(), $"copilot-test-{Guid.NewGuid():N}");
736+
try
737+
{
738+
var settings = new ConnectionSettings
739+
{
740+
CompactPaste = true,
741+
RespectGitignore = false,
742+
DisableAllHooks = true
743+
};
744+
settings.SyncCliConfig(tempDir);
745+
746+
var configPath = Path.Combine(tempDir, "config.json");
747+
Assert.True(File.Exists(configPath), "config.json should be created");
748+
749+
using var doc = JsonDocument.Parse(File.ReadAllText(configPath));
750+
Assert.True(doc.RootElement.GetProperty("compactPaste").GetBoolean());
751+
Assert.False(doc.RootElement.GetProperty("respectGitignore").GetBoolean());
752+
Assert.True(doc.RootElement.GetProperty("disableAllHooks").GetBoolean());
753+
}
754+
finally
755+
{
756+
try { Directory.Delete(tempDir, true); } catch { }
757+
}
758+
}
759+
760+
[Fact]
761+
public void SyncCliConfig_PreservesExistingKeys()
762+
{
763+
var tempDir = Path.Combine(Path.GetTempPath(), $"copilot-test-{Guid.NewGuid():N}");
764+
try
765+
{
766+
// Pre-populate config with an existing key
767+
Directory.CreateDirectory(tempDir);
768+
File.WriteAllText(
769+
Path.Combine(tempDir, "config.json"),
770+
"""{"existingKey": "existingValue", "compactPaste": false}""");
771+
772+
var settings = new ConnectionSettings { CompactPaste = true };
773+
settings.SyncCliConfig(tempDir);
774+
775+
var configPath = Path.Combine(tempDir, "config.json");
776+
using var doc = JsonDocument.Parse(File.ReadAllText(configPath));
777+
Assert.Equal("existingValue", doc.RootElement.GetProperty("existingKey").GetString());
778+
Assert.True(doc.RootElement.GetProperty("compactPaste").GetBoolean());
779+
}
780+
finally
781+
{
782+
try { Directory.Delete(tempDir, true); } catch { }
783+
}
784+
}
689785
}

PolyPilot.Tests/SettingsRegistryTests.cs

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,4 +358,112 @@ public void Editor_VisibleOnDesktopOnly()
358358
ctx.IsDesktop = false;
359359
Assert.False(desc.IsVisible!(ctx));
360360
}
361+
362+
// ── Advanced CLI config tests ───────────────────────────────────
363+
364+
[Fact]
365+
public void Categories_ContainsAdvanced()
366+
{
367+
Assert.Contains("Advanced", SettingsRegistry.Categories);
368+
}
369+
370+
[Fact]
371+
public void Advanced_CompactPaste_DefaultFalse()
372+
{
373+
var ctx = CreateContext();
374+
var desc = SettingsRegistry.All.First(s => s.Id == "advanced.compactPaste");
375+
Assert.False((bool)desc.GetValue!(ctx)!);
376+
}
377+
378+
[Fact]
379+
public void Advanced_CompactPaste_ToggleValue()
380+
{
381+
var settings = new ConnectionSettings { CompactPaste = false };
382+
var ctx = CreateContext(settings);
383+
var desc = SettingsRegistry.All.First(s => s.Id == "advanced.compactPaste");
384+
desc.SetValue!(ctx, true);
385+
Assert.True(settings.CompactPaste);
386+
}
387+
388+
[Fact]
389+
public void Advanced_RespectGitignore_ToggleValue()
390+
{
391+
var settings = new ConnectionSettings();
392+
var ctx = CreateContext(settings);
393+
var desc = SettingsRegistry.All.First(s => s.Id == "advanced.respectGitignore");
394+
Assert.False((bool)desc.GetValue!(ctx)!);
395+
desc.SetValue!(ctx, true);
396+
Assert.True(settings.RespectGitignore);
397+
}
398+
399+
[Fact]
400+
public void Advanced_DisableAllHooks_ToggleValue()
401+
{
402+
var settings = new ConnectionSettings();
403+
var ctx = CreateContext(settings);
404+
var desc = SettingsRegistry.All.First(s => s.Id == "advanced.disableAllHooks");
405+
Assert.False((bool)desc.GetValue!(ctx)!);
406+
desc.SetValue!(ctx, true);
407+
Assert.True(settings.DisableAllHooks);
408+
}
409+
410+
[Fact]
411+
public void Advanced_HiddenInRemoteMode()
412+
{
413+
var settings = new ConnectionSettings { Mode = ConnectionMode.Remote };
414+
var ctx = CreateContext(settings);
415+
var desc = SettingsRegistry.All.First(s => s.Id == "advanced.compactPaste");
416+
Assert.False(desc.IsVisible!(ctx));
417+
}
418+
419+
[Fact]
420+
public void Advanced_HiddenInDemoMode()
421+
{
422+
var settings = new ConnectionSettings { Mode = ConnectionMode.Demo };
423+
var ctx = CreateContext(settings);
424+
var desc = SettingsRegistry.All.First(s => s.Id == "advanced.respectGitignore");
425+
Assert.False(desc.IsVisible!(ctx));
426+
}
427+
428+
[Fact]
429+
public void Advanced_VisibleInPersistentMode()
430+
{
431+
var settings = new ConnectionSettings { Mode = ConnectionMode.Persistent };
432+
var ctx = CreateContext(settings);
433+
var desc = SettingsRegistry.All.First(s => s.Id == "advanced.disableAllHooks");
434+
Assert.True(desc.IsVisible!(ctx));
435+
}
436+
437+
[Fact]
438+
public void Advanced_VisibleInEmbeddedMode()
439+
{
440+
var settings = new ConnectionSettings { Mode = ConnectionMode.Embedded };
441+
var ctx = CreateContext(settings);
442+
var compactPaste = SettingsRegistry.All.First(s => s.Id == "advanced.compactPaste");
443+
Assert.True(compactPaste.IsVisible!(ctx));
444+
}
445+
446+
[Fact]
447+
public void Search_FindsAdvancedByKeyword()
448+
{
449+
var ctx = CreateContext();
450+
var results = SettingsRegistry.Search("compact paste", ctx).ToList();
451+
Assert.Contains(results, s => s.Id == "advanced.compactPaste");
452+
}
453+
454+
[Fact]
455+
public void Search_FindsAdvancedByGitignore()
456+
{
457+
var ctx = CreateContext();
458+
var results = SettingsRegistry.Search("gitignore", ctx).ToList();
459+
Assert.Contains(results, s => s.Id == "advanced.respectGitignore");
460+
}
461+
462+
[Fact]
463+
public void Search_FindsAdvancedByHooks()
464+
{
465+
var ctx = CreateContext();
466+
var results = SettingsRegistry.Search("hooks", ctx).ToList();
467+
Assert.Contains(results, s => s.Id == "advanced.disableAllHooks");
468+
}
361469
}

PolyPilot/Components/Pages/Settings.razor

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@
6767
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
6868
Diagnostics
6969
</button>
70+
<button class="settings-nav-item @(activeCategory == "advanced" ? "active" : "")" @onclick='() => ScrollToCategory("advanced")'>
71+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
72+
Advanced
73+
</button>
7074
}
7175
</nav>
7276
}
@@ -739,6 +743,24 @@
739743
</div>
740744
}
741745

746+
@{ var advancedSettings = SettingsRegistry.ForCategory("Advanced", settingsCtx).ToList(); }
747+
@if (advancedSettings.Any())
748+
{
749+
<div id="settings-advanced" class="settings-group @(GroupVisible("advanced") ? "" : "search-hidden")">
750+
<h2 class="group-title">Advanced</h2>
751+
<div class="settings-section">
752+
<p class="section-desc">Copilot CLI configuration options. These are written to <code>~/.copilot/config.json</code> and take effect on the next session.</p>
753+
@foreach (var desc in advancedSettings)
754+
{
755+
@if (SettingMatchesSearch(desc))
756+
{
757+
<PolyPilot.Components.Settings.SettingEditor Descriptor="desc" Context="settingsCtx" OnChanged="OnSettingChanged" />
758+
}
759+
}
760+
</div>
761+
</div>
762+
}
763+
742764
@if (!string.IsNullOrEmpty(statusMessage))
743765
{
744766
<div class="status-toast @statusClass">@(statusClass == "success" ? "" : statusClass == "error" ? "" : "")@statusMessage</div>
@@ -834,6 +856,7 @@
834856
"developer" => SectionVisible("auto update main git watch relaunch rebuild cli source built-in system repository repo clone worktree storage root directory dev drive"),
835857
"plugins" => SectionVisible("plugins provider extension dll assembly trust enable disable"),
836858
"diagnostics" => SectionVisible("logs diagnostics troubleshoot crash event console"),
859+
"advanced" => SectionVisible("compact paste gitignore hooks disable cli config advanced"),
837860
_ => true
838861
};
839862
}
@@ -894,6 +917,11 @@
894917
CopilotService.CodespacesEnabled = settings.CodespacesEnabled && settings.Mode == ConnectionMode.Embedded;
895918
CopilotService.NotifyStateChanged();
896919
break;
920+
case "advanced.compactPaste":
921+
case "advanced.respectGitignore":
922+
case "advanced.disableAllHooks":
923+
settings.SyncCliConfig();
924+
break;
897925
}
898926
SaveSettingsQuietly();
899927
StateHasChanged();

0 commit comments

Comments
 (0)