Guidance for coding agents working in QuickShell. Architecture tours live in docs/architecture/; numbered 0001-0005 files there are proposals that may lag landed code, so prefer the as-built tours when changing behavior and update the matching tour when you change a spine.
Quick Shell's .NET desktop hosts are Windows-only; QuickShell.Raycast is a separate TypeScript host with macOS support. A workspace is a saved folder plus metadata (terminal launches, companion app, git target, dev server). The primary surface is a PowerToys Command Palette extension (QuickShell), packaged out-of-process as a signed MSIX COM server. Two sibling hosts reuse the same on-disk model: a PowerToys Run plugin (QuickShell.Run, qs keyword) and a TypeScript Raycast extension (QuickShell.Raycast, parallel storage). QuickShell.Core owns all domain logic (no CmdPal SDK dependency) so the hosts are swappable UI shells. QuickShell.Suggest is a console CLI that emits JSON suggestion pills for Raycast.
The UI term is workspace; the on-disk file is still %LOCALAPPDATA%\QuickShell\shortcuts.json. Keep the product term and the storage term separate in code/comments.
Hosting (out-of-process COM). QuickShell/Program.cs is the entry point: [MTAThread] Main launches the COM server via Shmuelie.WinRTServer.ComServer, registering QuickShellExtension (QuickShell/QuickShell.cs, [Guid], : IExtension) as IExtension. The process blocks on a ManualResetEvent until IExtension.Dispose() fires it. The [Guid] CLSID must match Package.appxmanifest; do not modify the COM-hosting pattern.
CmdPal entry point. QuickShellCommandsProvider : CommandProvider, IDisposable (QuickShell/QuickShellCommandsProvider.cs) is returned by QuickShellExtension.GetProvider(ProviderType.Commands). Its constructor builds the DI container (new ServiceCollection().AddQuickShellHost(...)) and BuildServiceProvider(). It exposes TopLevelCommands() and FallbackCommands() and delegates GetCommandItem(id) to ICommandRouter.TryHandle.
Pages-as-ICommands. Pages (QuickShell/Pages/*, e.g. QuickShellPage : DynamicListPage) and Commands (QuickShell/Commands/*) are ICommand implementations. The same object can be a top-level command, a search result, or a MoreCommands item.
Composition root / DI. QuickShell.Core/Composition/QuickShellServiceCollectionExtensions.cs (AddQuickShellCore(configDirectory?)) registers all core services as interfaces. QuickShell/Services/CommandRouting/QuickShellCommandRoutingServiceCollectionExtensions.cs adds AddQuickShellCommandRouting and AddQuickShellHost (core + routing), the entry actually used by the provider. It prefers explicit factory lambdas over reflection for AOT/trim friendliness. Interfaces live in QuickShell.Core/Abstractions/ and Abstractions/Classification/.
Typed command routing. CommandRouter (CommandRouting/CommandRouter.cs) uses ICommandIdParser to parse a deep-link string into a CommandDescriptor record (Id, Kind, WorkspaceId, LaunchId, Directory, Branch), then dispatches by CommandKind to a registered ICommandItemHandler (CommandRouting/ICommandItemHandler.cs + CommandItemHandlers.cs). Handlers receive a QuickShellPageContext (Shortcuts, Settings, CreateShortcut, ReloadPages) and return an ICommandItem (often a Page). To add a new deep link: add a CommandKind + an ICommandItemHandler.
User command to launch flow.
- CmdPal invokes
GetCommandItem(id)(deep link) or showsTopLevelCommands(). ICommandRouter->ICommandIdParser.TryParse->CommandDescriptor.- Matching
ICommandItemHandler.Createbuilds aCommandItem/Page. - On invoke, the page/command builds a
TerminalShortcut(possibly viaWorkspaceSeedFactory) and callsShortcutLaunchExecutor.Launch. ShortcutLaunchExecutorrunsWorkspaceHealthCheck(Error blocks, Warning allows) ->WorkspaceGitLaunchGate(target branch fromworktree-branch-targets.json) -> optionalCompanionAppLauncher->TerminalLauncher.Resolve->TerminalLauncher.Open->Process.Start(or testStartProcessOverride). Result isShortcutLaunchResult(Dismiss/StayOpen +LaunchDiagnosticsReport). Quick Shell does not wait for command exit (handoff only).
Workspace intelligence. Folders are understood by IProjectAnalysisService (Classification/ProjectAnalysisService.cs) orchestrating IEnumerable<IProjectClassifier> (Node, DotNet, DockerCompose, TaskRunner, Rust, Python, Editor, Go, Java, Deno, Procfile, Ruby, Elixir) plus IDevServerDetector/ICompanionAppDetector. WorkspaceSeedFactory and CommandSuggestionService consume this to seed launches and suggestion pills.
Multi-launch (tabs vs windows). ShortcutLaunchExecutor.LaunchAll groups compatible entries via GroupPlans / TerminalLauncher.OpenGroup (; new-tab). GroupPlans key = (tabHostExecutable, elevation); build wt.exe <tab0> ; new-tab <tab1> and do not add -w on tab segments. Console Host and mixed elevation fall back to separate windows. Controlled by multiLaunchPresentation (singleWindowTabs default | separateWindows) in settings.json.
Persistence. ShortcutRepository (IShortcutRepository) is the owner of %LOCALAPPDATA%\QuickShell\shortcuts.json. On-disk is a layout (Shortcuts + Separators) written as envelope {"version":1,"entries":[...]} (PersistenceVersion.Current = 1); v0 root array is still readable (dual-read). AtomicFileWriter writes path.tmp then File.Replace(path.tmp, path, path.bak), guarded by a process-wide named Mutex Global\QuickShell_shortcuts_json plus a SemaphoreSlim; a Timer flushes pending writes. Undo/redo stacks are <=25. Debounced MarkUsed flushes LastUsedUtc after 2s. WorkspacesChanged event lets UI react without polling. Never write shortcuts.json outside ShortcutRepository; persist via IAtomicFileWriter.
| Path | Purpose |
|---|---|
QuickShell.Core/ |
Domain: models, persistence, launch, health, git, terminals, classification, suggestions, companions. No CmdPal SDK dependency. Services/, Models/, Composition/, Classification/, Abstractions/. |
QuickShell/ |
CmdPal extension: MSIX, Adaptive Card pages, command routing. Pages/, Commands/, Services/CommandRouting/, Program.cs, QuickShell.cs, QuickShellCommandsProvider.cs. |
QuickShell.Run/ |
PowerToys Run plugin (IPlugin, qs keyword); consumes Core. |
QuickShell.Core.Tests/ |
xUnit unit tests for Core (Windows-only TFM). |
QuickShell.Raycast/ |
Separate npm/TS extension; not in the .sln; mirrors product rules, shells out to QuickShell.Suggest. |
QuickShell.Suggest/ |
Console CLI emitting JSON suggestion pills for Raycast. |
scripts/ |
deploy.ps1, run-cmdpal-dev.ps1, deploy-all.ps1/ddeploy.ps1, generate-assets.ps1, RaycastLifecycle.ps1, build-exe.ps1, setup-template.iss, LogoAssetGenerator/. |
docs/architecture/ |
As-built tours (overview, launch, persistence, cmdpal-surface, hosts, settings, forms, intelligence, companions, git-and-discover) + ADRs 0001-0005. |
.github/workflows/ |
ci.yml (build/test), release-extension.yml (tag-triggered release + WinGet). |
Windows (primary, authoritative). Platform flag is required on the CLI (Directory.Build.props sets <Platforms>x64;ARM64</Platforms> with no default); omitting -p:Platform=x64 fails.
# Build the whole solution (Release, x64).
dotnet build QuickShell.sln -c Release -p:Platform=x64
# Test only the Core test project (the only runnable test project).
dotnet test QuickShell.Core.Tests/QuickShell.Core.Tests.csproj -c Release -p:Platform=x64
# Default CmdPal dev loop: stop CmdPal -> regen assets -> build/sign/install MSIX -> restart.
.\scripts\deploy.ps1
# -SkipElevation trust cert in CurrentUser\TrustedPeople (no UAC)
# -RecreateCertificate force new dev signing cert
# -UseLocalCmdPalSdk build against a sibling PowerToys CmdPal SDK
# -NoRestartCmdPal build/install only
# Daily wrapper over deploy.ps1; prints the Reload steps. Most common command.
.\scripts\run-cmdpal-dev.ps1 -UseLocalSdk
# Deploy all three surfaces: CmdPal MSIX + Run plugin + Raycast.
.\scripts\deploy-all.ps1 # shorthand: .\scripts\ddeploy.ps1
# -SkipCmdPal/-SkipRun/-SkipRaycast/-SkipTests/-NoRestart/-Configuration ReleaseIn Visual Studio: Build > Deploy (not just Build), then run Reload Command Palette Extension in CmdPal. After any deploy: open CmdPal (Win+Alt+Space), run Reload Command Palette Extension, search Quick Shell.
Raycast extension (Node). QuickShell.Raycast/ requires Node.js >= 20 (engines in package.json; .nvmrc pins 22.22.2). It is not part of the .NET solution; CI runs it under the raycast-check (windows-latest) and raycast-check-macos (macos-latest) jobs, matching platforms: ["Windows", "macOS"].
cd QuickShell.Raycast
npm ci
npm test # vitest run
npm run lint # ray lint
npm run build # ray build
npm run dev # ray developray (Raycast CLI) is a precondition: scripts/verify-raycast-cli.js runs as a pre-hook on predev/prebuild/prelint and fails clearly if ray is missing.
- Namespaces mirror folders.
QuickShell,QuickShell.Services,QuickShell.Services.CommandRouting,QuickShell.Pages,QuickShell.Commands,QuickShell.Core,QuickShell.Core.Services|Models|Composition|Abstractions,QuickShell.Core.Classification[.Classifiers|.Detectors]. All Core projects shareRootNamespace=QuickShell.NullableandImplicitUsingsare enabled project-wide. - One type per file; most types are
internal(small public surface). Stateless helpers areinternal static class(TerminalLauncher,WorkspaceSeedFactory,CommandSuggestionService,ShortcutLaunchExecutor). Stateful singletons areinternal sealed class. - Records vs classes. Value/result DTOs are
readonly record struct(ResolvedLaunch,CommandDescriptor,ShortcutExportResult); richer results arerecord(TerminalLaunchAttempt). Entities are mutableclass(TerminalShortcut,WorkspaceEntry).init-only andrequiredproperties are used (QuickShellPageContext). - DI style.
Microsoft.Extensions.DependencyInjection; composition-root extension methodsAddQuickShellCore/AddQuickShellHost. Most servicesAddSingleton;IWorkspaceHealthCheckerandIWorkspaceGitOperationsareAddTransient. The classifier registry usesIEnumerable<IProjectClassifier>(auto-injected, priority-ordered). Add new services viaAddQuickShellCore; expose via an interface inAbstractions/orQuickShell.Services. - Static vs DI split. Pure logic =
internal statichelper; swappable dependency = interface + DI registration. Prefer the established split. - Error handling. Mixed. The launch path throws (
InvalidOperationException,DirectoryNotFoundException,Win32Exception) caught inLaunchSingle->ShortcutLaunchResult.StayOpen. Import/export/transfer use result types (ShortcutTransferResult,ShortcutExportResult,ShortcutImportReadResult) withSuccess/Error. No globalResultmonad. - async/await.
*Asyncmethods takeCancellationToken cancellationToken = default; sync wrappers call.GetAwaiter().GetResult(). Fire-and-forgetTask.Run(git prewarm) is best-efforttry/catch. - Dispose / cancellation.
IDisposableon provider, extension, repository, pages,SearchDebouncer. Extension shutdown viaManualResetEvent.ShortcutRepositoryowns aMutex+SemaphoreSlim+ persistTimer. No rootCancellationTokenSourceyet (ADR 0005, partial). - Instrumentation. Pervasive
#region agent logblocks callingAgentDebugLog.Write/WriteException(... hypothesisId)for traceability; harmless to behavior, leave them. - Localization. User strings go through
QuickShell/Resources/Strings.cs. - Command/pill model.
CommandSuggestionServiceproducesCommandSuggestionPillobjects (TaskTypeCatalogids likeapi,frontend,agent).QuickShell.Suggestserializes these to JSON stdout for Raycast;QUICKSHELL_SUGGEST_EXEoverrides the executable path in development. - Settings vs workspaces. Preferences live in
%LOCALAPPDATA%\QuickShell\settings.json(QuickShellSettingsManager/QuickShellJsonSettingsStorefor CmdPal,QuickShellSettingsReaderfor Run), not inshortcuts.json. Keys:terminalApplication(system/wt/it/conhost),defaultProfile,multiLaunchPresentation,blockDirtyBranchSwitch(default true),recentWorkspaceCount. Launch always reads the live manager/reader values.
- Entry / COM:
QuickShell/Program.cs,QuickShell/QuickShell.cs. - CmdPal provider:
QuickShell/QuickShellCommandsProvider.cs. - DI:
QuickShell.Core/Composition/QuickShellServiceCollectionExtensions.cs,QuickShell/Services/CommandRouting/QuickShellCommandRoutingServiceCollectionExtensions.cs. - Routing:
QuickShell/Services/CommandRouting/{CommandRouter,CommandItemHandlers,ICommandItemHandler,CommandItemFactoryContext}.cs;QuickShell.Core/Services/{CommandDescriptor,CommandKind,CommandIdParser}.cs. - State / persistence:
QuickShell.Core/Services/{ShortcutRepository,IShortcutRepository,ShortcutDraftStore,AtomicFileWriter,PersistenceVersion}.cs. - Launch:
QuickShell.Core/Services/{TerminalLauncher,ShortcutLaunchExecutor,WorkspaceSeedFactory,WorkspaceHealthCheck,WorkspaceGitLaunchGate,CompanionAppLauncher}.cs. - Intelligence:
QuickShell.Core/Classification/{ProjectAnalysisService,ProjectLayoutAnalyzer,ProjectClassificationPipeline}.cs,QuickShell.Core/Classification/Classifiers/*,QuickShell.Core/Services/CommandSuggestionService.cs. - UI:
QuickShell/Pages/QuickShellPage.cs,QuickShell/Services/{ShortcutListItems,ShortcutTaskActionListItems}.cs,QuickShell/Commands/*. - Config:
QuickShell/QuickShellSettingsManager.cs,QuickShell/Services/QuickShellJsonSettingsStore.cs. - Models:
QuickShell.Core/Models/{TerminalShortcut,WorkspaceEntry,Workspace,ShortcutLayoutEntry}.cs. - Build config (protected):
Directory.Build.props(pinsAppVersion,<Platforms>, analyzers; aPreToolUsehook blocks edits),Directory.Packages.props(Central Package Management),QuickShell/QuickShell.csproj(MSIX identitytonythethompson.536944BA0D095, build variants Debug/Release/Store/WinGet),QuickShell/setup-template.iss,QuickShell/build-exe.ps1.
- .NET 10 SDK (
mise.tomlpins 10.0.302; noglobal.json). Target frameworks differ by project: theQuickShellCmdPal host andQuickShell.Runtargetnet10.0-windows10.0.26100.0;QuickShell.Core.Teststargetsnet10.0-windows10.0.26100.0too (it referencesQuickShell.csproj);QuickShell.CoreandQuickShell.Suggesttargetnet10.0-windows7.0. All are Windows-only (CsWinRT/CsWin32, Windows App SDK, WinUI, MSIX tooling). QuickShell.Coreis WinForms-free despite owning the clipboard/path pickers: WinForms was removed to unblock trimming in the packaged host.ShellFileDialog+Win32Clipboarduse source-generated COM /LibraryImport(AllowUnsafeBlocks=true), and the one GDI+ need (TerminalListIconCache) comes from the lighterSystem.Drawing.Commonpackage. Core is only compilable off-Windows (EnableWindowsTargeting=trueis already set inDirectory.Build.props); it cannot execute on Linux. Keep Windows-only APIs in Core minimal so the swappable-host story holds.- Package manager: NuGet with Central Package Management (
Directory.Packages.props,ManagePackageVersionsCentrally=true). CmdPal SDK isMicrosoft.CommandPalette.Extensions(NuGet) or a sibling local PowerToys SDK via-p:UseLocalCmdPalSdk=true(definesCMDPAL_HOVER_ACTIONS; don't assume those APIs exist otherwise). - No
.editorconfigorglobal.json. Analyzers are on:EnableNETAnalyzers=true,AnalysisMode=Recommended, plus StyleCop. Treat analyzer warnings seriously; they can break the Windows build. - Node >= 20 (
.nvmrcpins 22.22.2) for the Raycast surface;npm/rayCLI for its build/lint/test. - PowerShell drives build/deploy (
scripts/*.ps1). Platform flag (x64/ARM64) is required on CLIdotnet build/test. Directory.Build.propsis protected by aPreToolUsehook (.claude/hooks/run-guard-directory-build-props.sh); do not edit it unless explicitly asked.- Cross-platform (Linux cloud VM): only
QuickShell.Core(andQuickShell.Suggest) build with-p:EnableWindowsTargeting=true;net10.0-windows*assemblies cannot execute there (see the WinForms-free Core note above). The full solution does not build on Linux becauseQuickShell.Core.TestsreferencesQuickShell.csproj(Win10.26100). Validate shared-logic changes by building Core alone on Linux; anything touching the extension, Run plugin, tests, or packaging must be verified on Windows.
- Framework: xUnit (
global using Xunit;inQuickShell.Core.Tests/GlobalUsings.cs). Method names use underscores;CA1707is suppressed in the test csproj (intentional). No Moq / FluentAssertions. - Seams, not mocks. Tests use real services plus process-wide static override seams:
LaunchExecutorTestEnvironment.Apply()/Reset()(stubs terminal discovery + health),FakeShortcutRepository(in-memoryIShortcutRepository), andAgentCliCatalog.IsCommandOnPathOverride. Shared seams are grouped with[Collection]. - InternalsVisibleTo:
QuickShell.Coreexposes internals toQuickShell,QuickShell.Run,QuickShell.Core.Tests, andQuickShell.Suggest(seeQuickShell.Core.csproj). - Raycast: Vitest (
vitest run) underQuickShell.Raycast/src/__tests__/windows-launch.test.ts(arg escaping, target resolution,wtlaunch plan), kept in parity with Core behavior. - What is covered:
AgentCliSuggestionTests,TaskTypeCatalogTests,LaunchRowListEditorTests,TerminalProfileIconResolverTests,RunQueryScoringTests,ShortcutDisplayTests,ShortcutFormSaveRunEditorTests,ShortcutLaunchFormJsonTests,WorkspaceUtilityTests,ShortcutLaunchExecutorTests,TerminalLauncherArgsTests. - CI gates:
.github/workflows/ci.yml->build-test(windows-latest: solution build,dotnet test --no-build, Pester overscripts/tests), plusraycast-check(windows-latest) andraycast-check-macos(macos-latest) jobs that each runnpm test/lint/build, plus an informationalperf-harnessjob (Category=PerformanceMeasurement; never gates PRs)..github/workflows/release-extension.yml(onv*tag or dispatch) builds CmdPal/Run EXE installers, creates a GitHub Release, and opens WinGet manifest PRs. Raycast ships via the Raycast Store only (not GitHub/WinGet). No coverage threshold; CI gates on pass/fail only.
- Always Deploy (not just Build) to register the MSIX; after deploying, use the Reload command in Command Palette to refresh.
- Don't modify
Program.csCOM hosting or the[Guid]inQuickShell.cs(must matchPackage.appxmanifest). .gitignorenote: the Copilot instructions say to remove**/Properties/launchSettings.jsonand*.pubxmlfor git deployment, but those lines are not present in the current.gitignore(dev certsQuickShell_Dev.cer/.pfxanddev-shortcuts.jsonare intentionally ignored). Do not reintroduce them.- Raycast
rayCLI is a precondition forbuild/lint/dev; a missing binary fails fast viaverify-raycast-cli.js. - Honor existing rule files if present:
.github/copilot-instructions.md,.github/instructions/cmdpal-extension.instructions.md(**/*.cs),.claude/settings.json+ hooks,.cursor/*, plus Copilot skills under.github/skills/(add-adaptive-card-form,add-extension-settings,add-dock-band,add-fallback-commands,publish-extension).