RFC: SharpConsoleUI command center for the interactive shell#735
RFC: SharpConsoleUI command center for the interactive shell#735nickprotop wants to merge 6 commits into
Conversation
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.
|
I love SharpConsoleUI cant wait for your PR |
There was a problem hiding this comment.
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.cswith navigation pages, modals, status bar shortcuts, and direct installer-driven mutation flows. - Converts
InteractiveConsoleApptopartialand renames the previous prompt loop toRunClassicShellAsync. - 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.NETcollections the same way the classic prompt path guards against withBuildPromptDisplayLabel.
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 usesforce: false.SkillInstaller.Installskips existing directories unless forced, so recommended skills marked asupdatewill 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();
| /// 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(); |
| 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); |
| 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), |
| public async Task<int> RunAsync() | ||
| { | ||
| if (Console.IsInputRedirected || Console.IsOutputRedirected) |
| // This is the default surface for the bare `dotnet skills` (and `agents`) | ||
| // invocation. It replaces the prompt-first Spectre loop in |
| 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.[/]")))); |
| 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); |
| 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); |
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>
What this is
A full SharpConsoleUI command center replacing the prompt-first interactive shell (bare
dotnet skills) — the direction yourdocs/cli-rewrite-plan.mdflagged 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-skillsto take this surface, before any polish round.The Spectre prompt loop is preserved as
RunClassicShellAsyncand used automatically when stdin/stdout is redirected (CI, pipes, dumb terminals).Screens
/filters in place, search chip at top when filter is activeDropdownControlfor Platform/Scope (change-on-pick, no modal) + Refresh buttonCtrl+P)Architecture
InteractiveConsoleApp→partial. The original prompt loop is preserved asRunClassicShellAsyncand is used automatically whenConsole.IsInputRedirected || Console.IsOutputRedirected.InteractiveConsoleApp.Shell.csis the new shell —ConsoleWindowSystem+NavigationViewdriven by the existingNavigationSurfaceManifest, with one page builder method perHomeAction.PanelControl,HorizontalGrid,MarkupControl,TableControl,BarGraphControl,DropdownControl,PromptControl). The SpectrePanel/Gridhelpers (BuildRich*) are gone from the shell path.Runtime/*installers (SkillInstaller,AgentInstaller,ProjectSkillRecommender) and refresh the active page in place.InteractiveSessionStateraisesAgentChanged/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.Toast()routes towindowSystem.NotificationStateService.ShowNotification(...). Severity-aware:Info/Successrender as a sliding card and vanish;Warning/Dangeralso leave a sticky bottom-bar marker until the next page change.StatusBarControl. Top bar carries session identity (project, scope, platform, catalog version + skill count). Bottom bar carries shortcuts + the sticky-status slot + clock.Maximized().EscandOnClosedboth callws.Shutdown(0). Backdrop is a cxpost-style vertical gradient(25,32,52)→(7,7,13).HighlightBackgroundColor,ListHoverBackgroundColor, andListUnfocusedHighlightBackgroundColorpaint three different colors).SharpConsoleUIand added it toManagedCode.DotnetSkills.csproj— the main tool had no SharpConsoleUI reference yet; the dep was only declared in theagents/dotnet-agentswrappers which compile the same sources.Key bindings
↑ ↓Enter/Escclears)Ctrl+PCtrl+RCtrl+UCtrl+DelCtrl+IEscCommit ordering
Branch is structured for review as 6 self-contained commits:
b9a9dbdPhase 1 — scaffold:partialsplit, NavigationView + per-page builders, classic shell preserved behindRunClassicShellAsync.42cc836Phase 3 · live state + dual status bars + frame rules:InteractiveSessionStateevents; top/bottom status bars; rule separators.cd73b3aPhase 3 · severity-routed toast notifications:Toast()→NotificationStateService.ShowNotification.9685029Phase 3 · clickable Home metrics + inline Settings form:BuildMetricCardmouse-click handlers; Settings replaces modal-launcher list withDropdownControls.8d0fc95Phase 3 · master-detail Collections + search + command palette: Collections left-list + right-detail with two-stage install;/filter overlay;Ctrl+Ppalette.83d41ddPhase 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
Update allbutton on Installed;Ctrl+Ushortcut; outdated recommendations on Project page install withforce: true(was a Copilot-reviewer finding earlier in this PR — fixed).[stack / lane]text now go throughMarkup.Escapebefore display — earlier Copilot finding fixed.RunClassicShellAsync. No prompt-degraded mode in the rich path; if SharpConsoleUI can't run, the tool fails clearly.Status
ManagedCode.DotnetSkills,ManagedCode.Agents,ManagedCode.DotnetAgents,ManagedCode.DotnetSkills.Tests) — 0 warnings.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 inInteractiveConsoleApp.csfor the session-state events. Easy to review per commit.Questions for the maintainers
dotnet-skillsto take, given thecli-rewrite-plan.mdframing? Direction first; nits later.--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.agents/dotnet-agentswrappers still dispatch bare invocation toRunAgentListAsyncinProgram.cs— rerouting them through the command center is left as an explicit follow-up. Want it folded into this PR or kept separate?/filter chip sits.)