Skip to content

RFC: SharpConsoleUI command center for the interactive shell#735

Open
nickprotop wants to merge 6 commits into
managedcode:mainfrom
nickprotop:feat/consoleex-command-center
Open

RFC: SharpConsoleUI command center for the interactive shell#735
nickprotop wants to merge 6 commits into
managedcode:mainfrom
nickprotop:feat/consoleex-command-center

Conversation

@nickprotop
Copy link
Copy Markdown

@nickprotop nickprotop commented May 11, 2026

What this is

A full SharpConsoleUI command center replacing the prompt-first interactive shell (bare dotnet skills) — the direction your docs/cli-rewrite-plan.md flagged as a possible later phase, taken end-to-end.

Not a prototype. The shell renders entirely through native ConsoleUI controls (zero Spectre renderables on screen), has live session state, sortable tables, a global command palette, severity-routed toast notifications, and inline form-style settings. Visual identity, IA, and key bindings are open for direction — this PR is being opened to confirm you want dotnet-skills to take this surface, before any polish round.

The Spectre prompt loop is preserved as RunClassicShellAsync and used automatically when stdin/stdout is redirected (CI, pipes, dumb terminals).

Screens

Home session card · 5 clickable metric cards (each navigates) · quick-start hints
Skills / Installed / Bundles / Packages / Agents dense list/table pages with mouse + keyboard activation, / filters in place, search chip at top when filter is active
Collections master-detail (left list + right detail), inline two-stage install button — no modal
Project scan summary · sortable recommendation list · "install all recommended" with proper force-flag handling per row
Analysis sortable heaviest-skills table + two native bar charts (tokens-by-skill with standard threshold gradient, skills-per-collection)
Settings inline form — DropdownControl for Platform/Scope (change-on-pick, no modal) + Refresh button
Command palette (Ctrl+P) fuzzy search across every skill / bundle / agent / settings action / page jump

Architecture

  • InteractiveConsoleApppartial. The original prompt loop is preserved as RunClassicShellAsync and is used automatically when Console.IsInputRedirected || Console.IsOutputRedirected.
  • InteractiveConsoleApp.Shell.cs is the new shell — ConsoleWindowSystem + NavigationView driven by the existing NavigationSurfaceManifest, with one page builder method per HomeAction.
  • Zero Spectre renderables on screen. Every page renders through native ConsoleUI controls (PanelControl, HorizontalGrid, MarkupControl, TableControl, BarGraphControl, DropdownControl, PromptControl). The Spectre Panel/Grid helpers (BuildRich*) are gone from the shell path.
  • Mutations call the existing Runtime/* installers (SkillInstaller, AgentInstaller, ProjectSkillRecommender) and refresh the active page in place.
  • Live state: InteractiveSessionState raises AgentChanged / ScopeChanged / ProjectChanged / SnapshotChanged. Each page subscribes on entry and detaches on the next page switch, so flipping scope or platform from Settings (or from the command palette) refreshes the open page without re-navigating.
  • Notifications: Toast() routes to windowSystem.NotificationStateService.ShowNotification(...). Severity-aware: Info/Success render as a sliding card and vanish; Warning/Danger also leave a sticky bottom-bar marker until the next page change.
  • Status bars: dual StatusBarControl. Top bar carries session identity (project, scope, platform, catalog version + skill count). Bottom bar carries shortcuts + the sticky-status slot + clock.
  • Frame: rounded subtle border, hidden title, Maximized(). Esc and OnClosed both call ws.Shutdown(0). Backdrop is a cxpost-style vertical gradient (25,32,52)(7,7,13).
  • List selection theme is pinned so keyboard / mouse hover / mouse click all paint the same solid selection bar (otherwise HighlightBackgroundColor, ListHoverBackgroundColor, and ListUnfocusedHighlightBackgroundColor paint three different colors).
  • Bumped SharpConsoleUI and added it to ManagedCode.DotnetSkills.csproj — the main tool had no SharpConsoleUI reference yet; the dep was only declared in the agents / dotnet-agents wrappers which compile the same sources.

Key bindings

↑ ↓ move within a list/table
Enter open detail / activate row / install (per page)
/ open search overlay (filters the active list page; Esc clears)
Ctrl+P open the global command palette
Ctrl+R refresh catalog
Ctrl+U update all outdated (Installed page)
Ctrl+Del remove all installed (Installed page)
Ctrl+I install all recommended (Project page)
Esc clear active filter, otherwise quit

Commit ordering

Branch is structured for review as 6 self-contained commits:

  1. b9a9dbd Phase 1 — scaffold: partial split, NavigationView + per-page builders, classic shell preserved behind RunClassicShellAsync.
  2. 42cc836 Phase 3 · live state + dual status bars + frame rules: InteractiveSessionState events; top/bottom status bars; rule separators.
  3. cd73b3a Phase 3 · severity-routed toast notifications: Toast()NotificationStateService.ShowNotification.
  4. 9685029 Phase 3 · clickable Home metrics + inline Settings form: BuildMetricCard mouse-click handlers; Settings replaces modal-launcher list with DropdownControls.
  5. 8d0fc95 Phase 3 · master-detail Collections + search + command palette: Collections left-list + right-detail with two-stage install; / filter overlay; Ctrl+P palette.
  6. 83d41dd Phase 3 · TableControl + BarGraph charts: Installed and Analysis pages use sortable tables; Analysis grows two native bar charts.

(Phase 2 — every Spectre renderable swept to native — landed during the conversion and is squashed into the scaffold commit. Happy to split it out if it helps review.)

Compliance with AGENTS.md

  • Install overview before confirmation: Collections has a two-stage inline button (first click arms with a warning toast, second commits). Skill/bundle/agent detail modals show the install plan before any installer is called.
  • Outdated-skill refresh is first-class: Update all button on Installed; Ctrl+U shortcut; outdated recommendations on Project page install with force: true (was a Copilot-reviewer finding earlier in this PR — fixed).
  • Direct Skills browse: Skills page lists individual catalog skills with detail/install; not buried behind Collections or Bundles.
  • Bracketed markup escaping: list/table cells with [stack / lane] text now go through Markup.Escape before display — earlier Copilot finding fixed.
  • Real terminal only: redirected stdin/stdout falls through to RunClassicShellAsync. No prompt-degraded mode in the rich path; if SharpConsoleUI can't run, the tool fails clearly.

Status

  • All four projects build clean (ManagedCode.DotnetSkills, ManagedCode.Agents, ManagedCode.DotnetAgents, ManagedCode.DotnetSkills.Tests) — 0 warnings.
  • All 613 tests pass.
  • dotnet skills <subcommand> (list / install / recommend / bundle list / agent install / …) is unchanged — only the bare interactive invocation is affected.

Diff size

5 files changed, ~2250 insertions / ~10 deletions. Almost all of it in cli/ManagedCode.DotnetSkills/InteractiveConsoleApp.Shell.cs (the partial-class shell file) plus ~70 lines in InteractiveConsoleApp.cs for the session-state events. Easy to review per commit.

Questions for the maintainers

  1. Is this a direction you want dotnet-skills to take, given the cli-rewrite-plan.md framing? Direction first; nits later.
  2. Full-screen alt-buffer TUI as the default for bare invocation vs. an opt-in (--ui / --shell)? Today CI and pipes already fall through to the classic shell automatically; the question is what a real-terminal user sees by default.
  3. The agents / dotnet-agents wrappers still dispatch bare invocation to RunAgentListAsync in Program.cs — rerouting them through the command center is left as an explicit follow-up. Want it folded into this PR or kept separate?
  4. Anything in the IA you'd want different before this lands? (e.g. NavigationView rail sections, top-bar identity layout, where the / filter chip sits.)

Replace the prompt-first Spectre.Console SelectionPrompt loop (bare
`dotnet skills` / `agents`) with a retained-mode SharpConsoleUI shell
built on ConsoleWindowSystem + NavigationView. The classic prompt
loop is preserved as RunClassicShellAsync and used automatically when
stdin/stdout is redirected (CI, pipes, dumb terminals).

InteractiveConsoleApp.Shell.cs (new) adds the command center:

- NavigationView with pages for every HomeAction surface (Home,
  Skills, Installed, Collections, Bundles, Packages, Agents, Project,
  Analysis, Remove-all, Update-all, Settings, About) driven by the
  existing NavigationSurfaceManifest.
- Page content reuses the existing BuildRich* Spectre renderables
  verbatim via SpectreRenderableControl - no rendering rewrite.
- Selection flows are ListControl activation -> modal Windows with a
  ToolbarControl row of ButtonControls. Mutations call the Runtime
  installers (SkillInstaller / AgentInstaller / ProjectSkillRecommender)
  directly and re-render the affected page in place.
- Interactive bottom status bar (StatusBarControl): per-page dynamic
  hints, clickable items, shortcut highlighting, ticking clock, live
  catalog summary, toast slot for action results. Ctrl+R / Ctrl+U /
  Ctrl+I shortcuts are also wired in the window key handler.
- Main window: rounded subtle border, hidden title, Maximized; Esc and
  OnClosed both call ws.Shutdown(0). Modals: centered, non-minimizable
  / non-maximizable, Esc dismisses.
- List selection theme is pinned so keyboard, mouse hover, and click
  all paint the same solid selection bar (otherwise the three list
  states render in three different colors).

SharpConsoleUI 2.4.55 -> 2.4.61, and added to ManagedCode.DotnetSkills
(the main tool had no SharpConsoleUI reference yet; the dep was only
declared in the agents / dotnet-agents wrappers, which compile the
same sources).

All four projects build clean. All 613 tests pass.
`dotnet skills <subcommand>` (list / install / recommend / ...) is
unchanged - only the bare interactive invocation is affected.
@KSemenenko KSemenenko marked this pull request as ready for review May 13, 2026 18:39
Copilot AI review requested due to automatic review settings May 13, 2026 18:39
@KSemenenko
Copy link
Copy Markdown
Member

I love SharpConsoleUI cant wait for your PR

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR prototypes a SharpConsoleUI retained-mode command center for the bare dotnet skills interactive experience while preserving the classic Spectre prompt shell as a fallback path.

Changes:

  • Adds InteractiveConsoleApp.Shell.cs with navigation pages, modals, status bar shortcuts, and direct installer-driven mutation flows.
  • Converts InteractiveConsoleApp to partial and renames the previous prompt loop to RunClassicShellAsync.
  • Adds/updates SharpConsoleUI package references across the CLI projects.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
cli/ManagedCode.DotnetSkills/ManagedCode.DotnetSkills.csproj Adds SharpConsoleUI to the main dotnet-skills tool.
cli/ManagedCode.DotnetSkills/InteractiveConsoleApp.Shell.cs Adds the new SharpConsoleUI command center implementation.
cli/ManagedCode.DotnetSkills/InteractiveConsoleApp.cs Makes the app partial and preserves the classic shell as RunClassicShellAsync.
cli/ManagedCode.DotnetAgents/ManagedCode.DotnetAgents.csproj Updates SharpConsoleUI version for the dotnet-agents wrapper.
cli/ManagedCode.Agents/ManagedCode.Agents.csproj Updates SharpConsoleUI version for the agents wrapper.
Comments suppressed due to low confidence (8)

cli/ManagedCode.DotnetSkills/InteractiveConsoleApp.Shell.cs:404

  • Installed skill labels also include unescaped [collection / lane] text while the list control is used with markup elsewhere. This can misparse entries for .NET collections the same way the classic prompt path guards against with BuildPromptDisplayLabel.
        foreach (var record in installed)
        {
            list.AddItem((record.IsCurrent ? "✓ " : "↻ ") + BuildInstalledSkillChoiceLabel(record), record);
        }

cli/ManagedCode.DotnetSkills/InteractiveConsoleApp.Shell.cs:771

  • Outdated recommendations are included in installable, but the later bulk install uses force: false. SkillInstaller.Install skips existing directories unless forced, so recommended skills marked as update will remain outdated after the action.
        var installable = scan.Recommendations
            .Where(r => !installedByName.TryGetValue(r.Skill.Name, out var rec) || !rec.IsCurrent)
            .Select(r => r.Skill)
            .GroupBy(s => s.Name, StringComparer.OrdinalIgnoreCase).Select(g => g.First())
            .ToArray();

cli/ManagedCode.DotnetSkills/InteractiveConsoleApp.Shell.cs:1290

  • The global install-recommended shortcut has the same update issue: this set includes outdated installed skills, but the install call below uses force: false, which skips existing directories instead of updating them.
        var installable = scan.Recommendations
            .Where(r => !installedByName.TryGetValue(r.Skill.Name, out var rec) || !rec.IsCurrent)
            .Select(r => r.Skill)
            .GroupBy(s => s.Name, StringComparer.OrdinalIgnoreCase).Select(g => g.First())
            .ToArray();

cli/ManagedCode.DotnetSkills/InteractiveConsoleApp.Shell.cs:601

  • Bundle installation also skips the established preview/confirmation step. Before installing multiple skills from a bundle, the UI should show the install plan and ask for confirmation as the classic bundle flow does.
            ("Install bundle into current target", () =>
            {
                var skills = SafeGet(() => new SkillInstaller(skillCatalog).SelectSkillsFromPackages(new[] { package.Name }), Array.Empty<SkillEntry>());
                var summary = skills.Count == 0 ? null : SafeGet(() => new SkillInstaller(skillCatalog).Install(skills, ResolveSkillLayout(), force: false), default(SkillInstallSummary));
                Toast(summary is null ? $"Could not install bundle {package.Name}" : $"{package.Name}: {summary.InstalledCount} written, {summary.SkippedExisting.Count} skipped");

cli/ManagedCode.DotnetSkills/InteractiveConsoleApp.Shell.cs:762

  • The per-recommendation activation mutates the target without an install preview or confirmation. This diverges from the repo's interactive install convention that users see what will be written before the install proceeds.
            if (item.Tag is ProjectSkillRecommendation recommendation)
            {
                var summary2 = SafeGet(() => new SkillInstaller(skillCatalog).Install(new[] { recommendation.Skill }, ResolveSkillLayout(), force: false), default(SkillInstallSummary));
                Toast(summary2 is null ? $"Install failed for {ToAlias(recommendation.Skill.Name)}" : $"{ToAlias(recommendation.Skill.Name)}: {summary2.InstalledCount} written, {summary2.SkippedExisting.Count} skipped");
                BuildProjectPage(ws, panel);

cli/ManagedCode.DotnetSkills/InteractiveConsoleApp.Shell.cs:531

  • Collection installation confirms only the count and target, not the install plan. The repo's interactive install convention requires an overview of the concrete skills/files to be written before confirmation, especially for whole-collection installs.
                ConfirmModal(ws, $"Install collection {view.Collection}?",
                    $"Installs all {view.SkillCount} skill(s) from this collection into {ResolveSkillLayout().PrimaryRoot.FullName}.",
                    () =>
                    {
                        var skills = SafeGet(() => new SkillInstaller(skillCatalog).SelectSkillsFromCollections(new[] { view.Collection }), Array.Empty<SkillEntry>());

cli/ManagedCode.DotnetSkills/InteractiveConsoleApp.Shell.cs:687

  • Agent installation also bypasses the established confirmation step. The existing agent detail flow asks the user to confirm the target path before writing the agent file.
            buttons.Add(("Install into current target", () =>
            {
                var summary = SafeGet(() => new AgentInstaller(agentCatalog).Install(new[] { agent }, layout, force: false), default(AgentInstallSummary));
                Toast(summary is null ? "Install failed" : $"{ToAlias(agent.Name)}: {summary.InstalledCount} written, {summary.SkippedExisting.Count} skipped");

cli/ManagedCode.DotnetSkills/InteractiveConsoleApp.Shell.cs:1299

  • The status-bar shortcut path also installs project recommendations without persisting the auto-managed state. This bypasses the existing project sync service state file, so later auto-prune/reconcile behavior will not account for skills installed from this shortcut.
        var summary = SafeGet(() => new SkillInstaller(skillCatalog).Install(installable, layout, force: false), default(SkillInstallSummary));
        Toast(summary is null ? "Install failed" : $"Installed {summary.InstalledCount}, skipped {summary.SkippedExisting.Count}");
        RebuildActivePage();

Comment on lines +64 to +70
/// command center; falls back to the classic prompt loop when there is no real terminal.
/// </summary>
public async Task<int> RunAsync()
{
if (Console.IsInputRedirected || Console.IsOutputRedirected)
{
return await RunClassicShellAsync();
Comment on lines +545 to +583
private void BuildBundlesPage(ConsoleWindowSystem ws, ScrollablePanelControl panel, bool primaryOnly)
{
panel.ClearContents();

var packages = (primaryOnly
? GetPrimaryBundles()
: skillCatalog.Packages.OrderBy(p => p.Name, StringComparer.Ordinal).ToArray())
.ToArray();
var title = primaryOnly ? "focused bundles" : "catalog packages";
var skillTokens = skillCatalog.Skills.ToDictionary(skill => skill.Name, skill => skill.TokenCount, StringComparer.OrdinalIgnoreCase);

var summary = BuildRichPropertyGrid(
("catalog", $"{Escape(skillCatalog.SourceLabel)} [dim]({Escape(skillCatalog.CatalogVersion)})[/]"),
(primaryOnly ? "bundles" : "packages", packages.Length.ToString()),
("skills covered", skillCatalog.Skills.Count.ToString()));
panel.AddControl(new SpectreRenderableControl(BuildRichShellPanel(title, summary)));

if (packages.Length == 0)
{
panel.AddControl(new SpectreRenderableControl(BuildRichShellPanel(title, new Spectre.Console.Markup("[dim]Nothing available in this catalog version.[/]"))));
return;
}

var list = StyledList($"{(primaryOnly ? "Bundles" : "Packages")} (Enter for details)")
.MaxVisibleItems(16)
.WithScrollbarVisibility(ScrollbarVisibility.Auto);
foreach (var package in packages)
{
var tokenCount = package.Skills.Sum(name => skillTokens.TryGetValue(name, out var value) ? value : 0);
list.AddItem($"{package.Name} [dim]({package.Skills.Count} skills, {FormatTokenCount(tokenCount)} tokens)[/]", package);
}
list.OnItemActivated((_, item) =>
{
if (item.Tag is SkillPackageEntry package)
{
ShowBundleModal(ws, panel, package, primaryOnly);
}
});
panel.AddControl(list.Build());
.WithScrollbarVisibility(ScrollbarVisibility.Auto);
foreach (var skill in available)
{
list.AddItem(BuildSkillChoiceLabel(skill, installed), skill);
Comment on lines +758 to +762
if (item.Tag is ProjectSkillRecommendation recommendation)
{
var summary2 = SafeGet(() => new SkillInstaller(skillCatalog).Install(new[] { recommendation.Skill }, ResolveSkillLayout(), force: false), default(SkillInstallSummary));
Toast(summary2 is null ? $"Install failed for {ToAlias(recommendation.Skill.Name)}" : $"{ToAlias(recommendation.Skill.Name)}: {summary2.InstalledCount} written, {summary2.SkippedExisting.Count} skipped");
BuildProjectPage(ws, panel);
HomeAction.ManageInstalled => new (string, string, Action)[]
{
("Ctrl+U", "Update outdated", UpdateAllOutdatedFromUi),
("Ctrl+Del", "Remove all", RemoveAllFromUi),
Comment on lines +66 to +68
public async Task<int> RunAsync()
{
if (Console.IsInputRedirected || Console.IsOutputRedirected)
Comment on lines +4 to +5
// This is the default surface for the bare `dotnet skills` (and `agents`)
// invocation. It replaces the prompt-first Spectre loop in
Comment on lines +710 to +713
var scan = SafeGet(() => new ProjectSkillRecommender(skillCatalog).Analyze(Session.ProjectDirectory), null);
if (scan is null)
{
panel.AddControl(new SpectreRenderableControl(BuildRichShellPanel("project scan", new Spectre.Console.Markup("[red]Could not scan the project directory.[/]"))));
Comment on lines +692 to +694
var summary = SafeGet(() => new AgentInstaller(agentCatalog).Remove(new[] { agent }, layout), default(AgentRemoveSummary));
Toast(summary is null ? "Remove failed" : $"Removed {ToAlias(agent.Name)} ({summary.RemovedCount} file(s))");
BuildAgentsPage(ws, owner);
Comment on lines +777 to +779
var summary2 = SafeGet(() => new SkillInstaller(skillCatalog).Install(installable, ResolveSkillLayout(), force: false), default(SkillInstallSummary));
Toast(summary2 is null ? "Install failed" : $"Installed {summary2.InstalledCount}, skipped {summary2.SkippedExisting.Count}");
BuildProjectPage(ws, panel);
nickprotop and others added 5 commits May 14, 2026 00:01
Replaces the system-level top/bottom panels with two interactive StatusBarControls
plus rule separators. Top bar carries session identity (project, scope, platform,
catalog version); bottom bar keeps shortcuts + toast slot. Both stretch and use
transparent backgrounds so the cxpost/cxfiles gradient shows through.

InteractiveSessionState gains AgentChanged/ScopeChanged/ProjectChanged events and
a SnapshotChanged signal. Each page builder calls AttachSessionEvents() to bind a
fresh handler to the open page and detach the previous one — flipping scope or
platform anywhere now refreshes the active page in place without re-navigating.
RaiseSnapshotChanged() after a catalog refresh updates the top bar's version line
through the same path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Toast() now calls ConsoleWindowSystem.NotificationStateService.ShowNotification —
install/refresh/remove results render as sliding cards on the right. Severity
routing: Info/Success stay transient (card only); Warning/Danger also leave a
sticky marker in the bottom status bar until the next page change so the user
has time to read it. ClearStickyStatus() fires at every page entry.

Adds ToastResult(result, fail, success) for the common SkillInstaller pattern
where a null summary means failure. Every Toast call site is now severity-tagged.
Settings refresh path raises Session.RaiseSnapshotChanged so the live event chain
from Commit 1 reflows the open page.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Home metric cards (skills/bundles/installed/outdated/agents) are now native
PanelControl mouse-click targets. Clicking installed/outdated lands on the
Installed page; clicking skills/bundles/agents lands on the matching browse
surface. BuildMetricCard takes an optional onClick that hooks PanelControl's
MouseClick event.

Settings replaces the 3-row prompt list and its ChooseEnumModal popups with
native DropdownControls for Platform and Scope plus a Button for catalog
refresh. SelectionChanged updates Session.Agent/Scope directly; the live event
chain from Commit 1 redraws the page and top status bar without a modal.

NavigateTo(HomeAction) is the shared entry point — also used by the command
palette in Commit 4.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Collections is now a HorizontalGrid: left pane is the collection list, right
pane is a ScrollablePanel that re-renders on selection change — collection
stats, lanes, and an inline two-stage install button (first click arms, second
commits) replace the modal-and-back-out flow. Satisfies AGENTS.md's "install
overview before confirmation" rule without a popup.

`/` opens a small search overlay (PromptControl, Enter applies) that filters
the active list page (Skills/Installed/Collections/Bundles/Packages/Agents).
Esc clears an active filter; page-switch clears it automatically. A small
yellow chip at the top of filtered pages shows the active query.

Ctrl+P opens a centered command palette modal — fuzzy haystack search across
every catalog skill, bundle, agent, plus settings actions and page jumps.
Activating an entry routes to the matching detail modal or page.

Bottom status bar advertises `/` (when on a list page) and Ctrl+P (everywhere).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Installed and Analysis pages drop the markup-formatted ListControl rows in
favor of native sortable TableControls. Installed columns: status, skill,
collection, lane, installed version, latest, tokens. Analysis "heaviest
skills" columns: skill, collection, lane, tokens. Click a header to sort;
Enter or double-click activates the row's detail modal. Outdated rows render
in yellow via TableRow.ForegroundColor — no per-cell markup needed.

Analysis grows two BarGraphControl sections below the table: tokens by skill
(top 12, standard threshold gradient — green/yellow/red) and skills per
collection (top 8, turquoise). Bars are static visualization; the table below
covers drill-down.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants