From b05d749364438f0e1a05caaf196ecec1823bc696 Mon Sep 17 00:00:00 2001 From: tonythethompson Date: Wed, 29 Jul 2026 06:34:40 -0700 Subject: [PATCH 01/13] Package Raycast command suggestions --- .gitignore | 1 + QuickShell.Raycast/README.md | 4 +- .../src/__tests__/suggest-commands.test.ts | 8 +++ .../components/discover-git-repos-view.tsx | 4 +- .../src/components/workspace-form.tsx | 18 ++++-- .../src/lib/suggest-commands.ts | 10 +-- docs/architecture/intelligence.md | 2 +- scripts/RaycastLifecycle.ps1 | 11 ++++ scripts/build-raycast-extension.ps1 | 8 +++ scripts/build-raycast-suggest.ps1 | 64 +++++++++++++++++++ 10 files changed, 118 insertions(+), 12 deletions(-) create mode 100644 scripts/build-raycast-suggest.ps1 diff --git a/.gitignore b/.gitignore index 281aaa88..331480ab 100644 --- a/.gitignore +++ b/.gitignore @@ -89,6 +89,7 @@ ltm/reports/* ltm/snapshots/* # --- /ltm-power --- QuickShell.Raycast/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +/QuickShell.Raycast/assets/QuickShell.Suggest.exe debug-a49e01.log .vscode/settings.json /.workflow diff --git a/QuickShell.Raycast/README.md b/QuickShell.Raycast/README.md index 6ad83c43..1213ead8 100644 --- a/QuickShell.Raycast/README.md +++ b/QuickShell.Raycast/README.md @@ -15,7 +15,7 @@ Create, Discover, and Edit are Actions / pushed views inside Quick Shell (not se ## Requirements - **Raycast** for Windows or macOS -- **Node.js 22.14+** (development only) +- **Node.js 22.14+** and the **.NET 10 SDK** (development only) - Windows: Windows Terminal, PowerShell, or WSL - macOS: Terminal.app and/or iTerm2 (multi-launch can open as tabs in one window) @@ -62,6 +62,8 @@ npm run dev Run `npm run build` before submitting Store changes. Do not add a `version` field to `package.json`; Store versioning uses `CHANGELOG.md`. +On Windows, `scripts/deploy-all.ps1` and `scripts/build-raycast-extension.ps1` publish the Core-backed suggestion CLI into Raycast assets automatically. The packaged CLI uses the .NET 10 Desktop Runtime; macOS continues to use local folder heuristics. + **Distribution:** publish via the [Raycast Store](https://www.raycast.com/store) only. GitHub Releases and WinGet do not ship Raycast sideload packages. `scripts/build-raycast-extension.ps1` is for local/dev packaging. ## Store checklist diff --git a/QuickShell.Raycast/src/__tests__/suggest-commands.test.ts b/QuickShell.Raycast/src/__tests__/suggest-commands.test.ts index f3e30f94..0af6b0bf 100644 --- a/QuickShell.Raycast/src/__tests__/suggest-commands.test.ts +++ b/QuickShell.Raycast/src/__tests__/suggest-commands.test.ts @@ -1,7 +1,9 @@ +import path from "node:path"; import { describe, expect, it } from "vitest"; import { buildSuggestCommandArgs, pillsToSetupTasks, + resolveSuggestExecutable, splitPillsIntoSeedAndLeftover, type SuggestionPill, } from "../lib/suggest-commands"; @@ -16,6 +18,12 @@ function pill(partial: Partial & Pick { + it("resolves the packaged CLI from Raycast assets", () => { + expect(resolveSuggestExecutable("C:\\Raycast\\assets")).toBe( + path.join("C:\\Raycast\\assets", "QuickShell.Suggest.exe"), + ); + }); + it("builds suggest CLI args with used commands", () => { expect(buildSuggestCommandArgs("C:\\Projects\\app", ["npm run dev", " ", "dotnet watch"], 42)).toEqual([ "suggest", diff --git a/QuickShell.Raycast/src/components/discover-git-repos-view.tsx b/QuickShell.Raycast/src/components/discover-git-repos-view.tsx index 897e63ab..6e44e992 100644 --- a/QuickShell.Raycast/src/components/discover-git-repos-view.tsx +++ b/QuickShell.Raycast/src/components/discover-git-repos-view.tsx @@ -1,4 +1,4 @@ -import { Action, ActionPanel, Icon, List, showToast, Toast, useNavigation } from "@raycast/api"; +import { Action, ActionPanel, environment, Icon, List, showToast, Toast, useNavigation } from "@raycast/api"; import { usePromise } from "@raycast/utils"; import { useEffect, useMemo, useRef, useState } from "react"; import WorkspaceForm from "./workspace-form"; @@ -28,7 +28,7 @@ type ReviewWorkspaceFormProps = { /** Full seed for every repository selected from Discover Git Repos. */ async function buildWorkspaceFromRepo(directory: string, name: string, remoteUrl?: string | null): Promise { - const resolved = await resolveWorkspaceSetupSuggestions(directory); + const resolved = await resolveWorkspaceSetupSuggestions(directory, [], Date.now(), environment.assetsPath); return createWorkspaceFromDiscoveredGitRepo({ directory, name, diff --git a/QuickShell.Raycast/src/components/workspace-form.tsx b/QuickShell.Raycast/src/components/workspace-form.tsx index 475bbff1..fc1e4d0f 100644 --- a/QuickShell.Raycast/src/components/workspace-form.tsx +++ b/QuickShell.Raycast/src/components/workspace-form.tsx @@ -1,6 +1,7 @@ import { Action, ActionPanel, + environment, Form, Icon, launchCommand, @@ -172,7 +173,12 @@ export default function WorkspaceForm({ let cancelled = false; void (async () => { const generation = ++suggestionGenerationRef.current; - const resolved = await resolveWorkspaceSetupSuggestions(directory, seededCommands); + const resolved = await resolveWorkspaceSetupSuggestions( + directory, + seededCommands, + Date.now(), + environment.assetsPath, + ); if (cancelled || generation !== suggestionGenerationRef.current) { return; } @@ -307,7 +313,7 @@ export default function WorkspaceForm({ tasks: [] as Array<{ label: string; command: string }>, pills: [] as SuggestionPill[], } - : await resolveWorkspaceSetupSuggestions(nextDirectory, usedCommands); + : await resolveWorkspaceSetupSuggestions(nextDirectory, usedCommands, Date.now(), environment.assetsPath); if (generation !== suggestionGenerationRef.current) { return; } @@ -695,12 +701,13 @@ export default function WorkspaceForm({ placeholder={index === 0 ? "npm run dev" : "dotnet watch"} /> ))} - {unusedSuggestionPills.length > 0 ? ( + {suggestionSource ? ( { if (key === "choose-suggestion") { return; @@ -711,7 +718,10 @@ export default function WorkspaceForm({ } }} > - + 0 ? "Choose a suggestion…" : "All suggestions applied"} + /> {unusedSuggestionPills.map((pill) => ( { - const executable = resolveSuggestExecutable(); + const executable = resolveSuggestExecutable(assetsPath); if (!executable || !existsSync(executable)) { return null; } @@ -170,13 +171,14 @@ export async function resolveWorkspaceSetupSuggestions( directory: string, usedCommands: string[] = [], generation = Date.now(), + assetsPath?: string, ): Promise { const trimmed = directory.trim(); if (!trimmed) { return { source: "local", tasks: [], pills: [] }; } - const response = await fetchSuggestionPills(trimmed, usedCommands, generation); + const response = await fetchSuggestionPills(trimmed, usedCommands, generation, assetsPath); if (response && response.pills.length > 0) { const split = splitPillsIntoSeedAndLeftover(response.pills); return { diff --git a/docs/architecture/intelligence.md b/docs/architecture/intelligence.md index e853e51d..50dfee32 100644 --- a/docs/architecture/intelligence.md +++ b/docs/architecture/intelligence.md @@ -75,7 +75,7 @@ GetPills(directory, usedCommands, maxCount = MaxPills) |------|-------------| | CmdPal | Adaptive Card pill actions on form (4 per row, 3 rows collapsed; template rebuilt for visible count; same-type pills grouped by `TypeTitle`, groups ordered by best score) | | Run | `RunLaunchSuggestionPanel` | -| Raycast | `QuickShell.Suggest.exe` → JSON pills via `suggest-commands.ts` (form seeds + Actions). Falls back to `project-setup-suggestion.ts` heuristics when Suggest.exe is missing. `QUICKSHELL_SUGGEST_EXE` overrides the path in development. | +| Raycast | `QuickShell.Suggest.exe` → JSON pills via `suggest-commands.ts` (form seeds + Actions/dropdown). Windows deploy/package scripts publish the CLI into Raycast assets and runtime resolution uses `environment.assetsPath`. Falls back to `project-setup-suggestion.ts` heuristics when Suggest.exe is missing (including macOS). `QUICKSHELL_SUGGEST_EXE` overrides the path in development. | ## Related helpers diff --git a/scripts/RaycastLifecycle.ps1 b/scripts/RaycastLifecycle.ps1 index 8504e52c..a66e4b15 100644 --- a/scripts/RaycastLifecycle.ps1 +++ b/scripts/RaycastLifecycle.ps1 @@ -155,6 +155,17 @@ function Deploy-RaycastExtension { throw 'Node.js/npm is required to build QuickShell.Raycast.' } + if ($env:OS -eq 'Windows_NT') { + $suggestBuildScript = Join-Path $ProjectRoot 'scripts\build-raycast-suggest.ps1' + if (-not (Test-Path -LiteralPath $suggestBuildScript)) { + throw "QuickShell.Suggest build script not found at $suggestBuildScript" + } + & $suggestBuildScript -ProjectRoot $ProjectRoot -Configuration Release -Platform x64 + if ($LASTEXITCODE -ne 0) { + throw "QuickShell.Suggest publish failed with exit code $LASTEXITCODE" + } + } + Push-Location $raycastRoot try { if (-not (Test-Path 'node_modules/@raycast/api')) { diff --git a/scripts/build-raycast-extension.ps1 b/scripts/build-raycast-extension.ps1 index 8447e9ca..328b76d7 100644 --- a/scripts/build-raycast-extension.ps1 +++ b/scripts/build-raycast-extension.ps1 @@ -22,6 +22,14 @@ if (-not (Test-Path $raycastRoot)) { } Write-Host "Building Quick Shell for Raycast v$Version..." -ForegroundColor Cyan +& (Join-Path $PSScriptRoot 'build-raycast-suggest.ps1') ` + -ProjectRoot $repoRoot ` + -Configuration $Configuration ` + -Platform x64 +if ($LASTEXITCODE -ne 0) { + throw "QuickShell.Suggest publish failed with exit code $LASTEXITCODE" +} + Push-Location $raycastRoot try { if (Get-Command npm -ErrorAction SilentlyContinue) { diff --git a/scripts/build-raycast-suggest.ps1 b/scripts/build-raycast-suggest.ps1 new file mode 100644 index 00000000..26c02bb0 --- /dev/null +++ b/scripts/build-raycast-suggest.ps1 @@ -0,0 +1,64 @@ +param( + [string]$ProjectRoot = (Split-Path -Parent $PSScriptRoot), + + [ValidateSet('Debug', 'Release')] + [string]$Configuration = 'Release', + + [ValidateSet('x64')] + [string]$Platform = 'x64' +) + +$ErrorActionPreference = 'Stop' +$projectPath = Join-Path $ProjectRoot 'QuickShell.Suggest\QuickShell.Suggest.csproj' +$raycastRoot = Join-Path $ProjectRoot 'QuickShell.Raycast' +$publishRoot = Join-Path $raycastRoot 'bin\SuggestPublish' +$assetPath = Join-Path $raycastRoot 'assets\QuickShell.Suggest.exe' + +if (-not (Test-Path -LiteralPath $projectPath)) { + throw "QuickShell.Suggest project not found at $projectPath" +} + +$dotnetCommand = Get-Command dotnet -ErrorAction Stop +$dotnetRoot = Split-Path -Parent $dotnetCommand.Source +$sdkVersion = (& $dotnetCommand.Source --version).Trim() +if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($sdkVersion)) { + throw 'Unable to determine the installed .NET SDK version.' +} +$sdkPath = Join-Path $dotnetRoot "sdk\$sdkVersion\Sdks" +if (-not (Test-Path -LiteralPath $sdkPath)) { + throw "The .NET SDK resolver path was not found at $sdkPath" +} + +$previousDotnetRoot = $env:DOTNET_ROOT +$previousMsBuildSdksPath = $env:MSBuildSDKsPath +try { + # Use the SDK belonging to the selected dotnet host even when the parent + # shell contains stale Scoop/MSBuild environment variables. + $env:DOTNET_ROOT = $dotnetRoot + $env:MSBuildSDKsPath = $sdkPath + + & $dotnetCommand.Source publish $projectPath ` + -c $Configuration ` + -p:Platform=$Platform ` + -r win-x64 ` + --self-contained false ` + -p:PublishSingleFile=true ` + -p:DebugType=None ` + -p:DebugSymbols=false ` + -o $publishRoot + if ($LASTEXITCODE -ne 0) { + throw "dotnet publish QuickShell.Suggest failed with exit code $LASTEXITCODE" + } +} +finally { + $env:DOTNET_ROOT = $previousDotnetRoot + $env:MSBuildSDKsPath = $previousMsBuildSdksPath +} + +$publishedExecutable = Join-Path $publishRoot 'QuickShell.Suggest.exe' +if (-not (Test-Path -LiteralPath $publishedExecutable)) { + throw "Published QuickShell.Suggest executable not found at $publishedExecutable" +} + +Copy-Item -LiteralPath $publishedExecutable -Destination $assetPath -Force +Write-Host "Published Raycast suggestion CLI: $assetPath" -ForegroundColor Green From 59bb57fe7327a160a4d32f4f67e6aa67eb01a4be Mon Sep 17 00:00:00 2001 From: tonythethompson Date: Wed, 29 Jul 2026 06:41:03 -0700 Subject: [PATCH 02/13] Show suggestions in manual workspace creation --- .../src/__tests__/suggest-commands.test.ts | 14 +++++++++++ .../src/components/workspace-form.tsx | 23 +++++++++++++++---- .../src/lib/suggest-commands.ts | 20 ++++++++++++++++ 3 files changed, 53 insertions(+), 4 deletions(-) diff --git a/QuickShell.Raycast/src/__tests__/suggest-commands.test.ts b/QuickShell.Raycast/src/__tests__/suggest-commands.test.ts index 0af6b0bf..666138a2 100644 --- a/QuickShell.Raycast/src/__tests__/suggest-commands.test.ts +++ b/QuickShell.Raycast/src/__tests__/suggest-commands.test.ts @@ -2,6 +2,7 @@ import path from "node:path"; import { describe, expect, it } from "vitest"; import { buildSuggestCommandArgs, + combineSuggestionTasksAndPills, pillsToSetupTasks, resolveSuggestExecutable, splitPillsIntoSeedAndLeftover, @@ -76,6 +77,19 @@ describe("suggest-commands", () => { ]); }); + it("keeps seeded and leftover suggestions selectable for manual create", () => { + const combined = combineSuggestionTasksAndPills( + [{ label: "Dev", command: "npm run dev", taskType: "frontend" }], + [ + pill({ command: "npm run dev", taskType: "frontend" }), + pill({ command: "npm test", taskType: "test", displayTitle: "Test" }), + ], + ); + + expect(combined.map((entry) => entry.command)).toEqual(["npm run dev", "npm test"]); + expect(combined[0].displayTitle).toBe("Dev"); + }); + it("splits preferred setup pills into a short seed and leftover Actions pills", () => { const pills = [ pill({ command: "npm run build", taskType: "build", displayTitle: "Build" }), diff --git a/QuickShell.Raycast/src/components/workspace-form.tsx b/QuickShell.Raycast/src/components/workspace-form.tsx index fc1e4d0f..b0c95fe6 100644 --- a/QuickShell.Raycast/src/components/workspace-form.tsx +++ b/QuickShell.Raycast/src/components/workspace-form.tsx @@ -33,7 +33,11 @@ import { tryGetGitRemoteUrl } from "../lib/git-remote-url"; import { createStableId } from "../lib/ids"; import type { OpenWorkspaceLaunchContext } from "../lib/launch-context"; import { buildProjectSetupSuggestions } from "../lib/project-setup-suggestion"; -import { resolveWorkspaceSetupSuggestions, type SuggestionPill } from "../lib/suggest-commands"; +import { + combineSuggestionTasksAndPills, + resolveWorkspaceSetupSuggestions, + type SuggestionPill, +} from "../lib/suggest-commands"; import { getQuickShellStorage } from "../lib/raycast-storage"; import type { Workspace } from "../lib/schema"; import { suggestionPillIcon } from "../lib/task-type-accent"; @@ -291,10 +295,21 @@ export default function WorkspaceForm({ } } - // Manual Add Workspace: stop after name / repo / dev-server. + // Manual Add Workspace: offer commands without auto-applying them. if (directorySeedMode !== "full") { - setSuggestionPills([]); - setSuggestionSource(null); + const generation = ++suggestionGenerationRef.current; + const usedCommands = launches.map((launch) => launch.command.trim()).filter(Boolean); + const resolved = await resolveWorkspaceSetupSuggestions( + nextDirectory, + usedCommands, + Date.now(), + environment.assetsPath, + ); + if (generation !== suggestionGenerationRef.current) { + return; + } + setSuggestionSource(resolved.source); + setSuggestionPills(combineSuggestionTasksAndPills(resolved.tasks, resolved.pills)); return; } diff --git a/QuickShell.Raycast/src/lib/suggest-commands.ts b/QuickShell.Raycast/src/lib/suggest-commands.ts index d7a1e31b..f2e7cc4c 100644 --- a/QuickShell.Raycast/src/lib/suggest-commands.ts +++ b/QuickShell.Raycast/src/lib/suggest-commands.ts @@ -74,6 +74,26 @@ export function pillsToSetupTasks(pills: SuggestionPill[]): WorkspaceSetupTask[] return tasks; } +/** Present seeded tasks as selectable pills without auto-applying them (manual create flow). */ +export function combineSuggestionTasksAndPills(tasks: WorkspaceSetupTask[], pills: SuggestionPill[]): SuggestionPill[] { + const combined: SuggestionPill[] = tasks.map((task) => ({ + command: task.command, + taskType: task.taskType?.trim() || "none", + typeTitle: task.taskType?.trim() || "Setup", + displayTitle: task.label, + tooltip: task.command, + })); + const seen = new Set(combined.map((pill) => pill.command.trim().toLowerCase())); + for (const pill of pills) { + const key = pill.command.trim().toLowerCase(); + if (key && !seen.has(key)) { + seen.add(key); + combined.push(pill); + } + } + return combined; +} + export function isPreferredSetupSeedPill(pill: SuggestionPill): boolean { const taskType = pill.taskType?.trim().toLowerCase() ?? ""; if (PREFERRED_SEED_TASK_TYPES.has(taskType)) { From d6545c7f840b218565f807bcc2f547220880ed90 Mon Sep 17 00:00:00 2001 From: tonythethompson Date: Wed, 29 Jul 2026 07:13:01 -0700 Subject: [PATCH 03/13] Fix Raycast Suggest packaging and command dropdown UX Generate Suggest.exe on Windows prebuild/prepublish so Store packages include it, hide the empty disabled dropdown, and leave local-heuristic leftovers selectable. Co-authored-by: Cursor --- QuickShell.Raycast/README.md | 2 +- QuickShell.Raycast/package.json | 5 +- .../scripts/ensure-suggest-asset.js | 49 +++++++++++++++++++ .../src/__tests__/suggest-commands.test.ts | 14 ++++++ .../src/components/workspace-form.tsx | 8 +-- .../src/lib/suggest-commands.ts | 23 ++++++++- docs/architecture/forms.md | 2 +- docs/architecture/intelligence.md | 2 +- 8 files changed, 92 insertions(+), 13 deletions(-) create mode 100644 QuickShell.Raycast/scripts/ensure-suggest-asset.js diff --git a/QuickShell.Raycast/README.md b/QuickShell.Raycast/README.md index 1213ead8..5119cc61 100644 --- a/QuickShell.Raycast/README.md +++ b/QuickShell.Raycast/README.md @@ -62,7 +62,7 @@ npm run dev Run `npm run build` before submitting Store changes. Do not add a `version` field to `package.json`; Store versioning uses `CHANGELOG.md`. -On Windows, `scripts/deploy-all.ps1` and `scripts/build-raycast-extension.ps1` publish the Core-backed suggestion CLI into Raycast assets automatically. The packaged CLI uses the .NET 10 Desktop Runtime; macOS continues to use local folder heuristics. +On Windows, `npm run build`, `npm run publish`, `npm run dev` (if the asset is missing), `scripts/deploy-all.ps1`, and `scripts/build-raycast-extension.ps1` all publish `QuickShell.Suggest.exe` into Raycast `assets/` (gitignored; generated at build time). The CLI needs the .NET 10 Desktop Runtime. macOS continues to use local folder heuristics. **Distribution:** publish via the [Raycast Store](https://www.raycast.com/store) only. GitHub Releases and WinGet do not ship Raycast sideload packages. `scripts/build-raycast-extension.ps1` is for local/dev packaging. diff --git a/QuickShell.Raycast/package.json b/QuickShell.Raycast/package.json index a92e0956..bb4148af 100644 --- a/QuickShell.Raycast/package.json +++ b/QuickShell.Raycast/package.json @@ -154,10 +154,11 @@ "vitest": "^3.0.8" }, "scripts": { - "predev": "node scripts/sync-workspace-trust-features.js && node scripts/verify-raycast-cli.js", - "prebuild": "node scripts/sync-workspace-trust-features.js && node scripts/verify-raycast-cli.js", + "predev": "node scripts/sync-workspace-trust-features.js && node scripts/verify-raycast-cli.js && node scripts/ensure-suggest-asset.js --if-missing", + "prebuild": "node scripts/sync-workspace-trust-features.js && node scripts/verify-raycast-cli.js && node scripts/ensure-suggest-asset.js", "prelint": "node scripts/sync-workspace-trust-features.js && node scripts/verify-raycast-cli.js", "pretest": "node scripts/sync-workspace-trust-features.js", + "prepublishOnly": "node scripts/ensure-suggest-asset.js", "build": "ray build", "dev": "ray develop", "lint": "ray lint", diff --git a/QuickShell.Raycast/scripts/ensure-suggest-asset.js b/QuickShell.Raycast/scripts/ensure-suggest-asset.js new file mode 100644 index 00000000..8cbd3a70 --- /dev/null +++ b/QuickShell.Raycast/scripts/ensure-suggest-asset.js @@ -0,0 +1,49 @@ +const { spawnSync } = require("node:child_process"); +const fs = require("node:fs"); +const path = require("node:path"); + +const raycastRoot = path.resolve(__dirname, ".."); +const repoRoot = path.resolve(raycastRoot, ".."); +const assetPath = path.join(raycastRoot, "assets", "QuickShell.Suggest.exe"); +const buildScript = path.join(repoRoot, "scripts", "build-raycast-suggest.ps1"); + +const ifMissing = process.argv.includes("--if-missing"); + +function fail(message) { + console.error(message); + process.exit(1); +} + +if (process.platform !== "win32") { + process.exit(0); +} + +if (ifMissing && fs.existsSync(assetPath)) { + process.exit(0); +} + +if (!fs.existsSync(buildScript)) { + fail(`QuickShell.Suggest build script not found at ${buildScript}`); +} + +const result = spawnSync( + "powershell.exe", + ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", buildScript, "-ProjectRoot", repoRoot], + { + cwd: repoRoot, + encoding: "utf8", + stdio: "inherit", + }, +); + +if (result.error) { + fail(`Failed to publish QuickShell.Suggest.exe: ${result.error.message}`); +} + +if (result.status !== 0) { + fail(`QuickShell.Suggest publish failed with exit code ${result.status ?? 1}`); +} + +if (!fs.existsSync(assetPath)) { + fail(`QuickShell.Suggest.exe was not published to ${assetPath}`); +} diff --git a/QuickShell.Raycast/src/__tests__/suggest-commands.test.ts b/QuickShell.Raycast/src/__tests__/suggest-commands.test.ts index 666138a2..b96cb9c5 100644 --- a/QuickShell.Raycast/src/__tests__/suggest-commands.test.ts +++ b/QuickShell.Raycast/src/__tests__/suggest-commands.test.ts @@ -3,6 +3,7 @@ import { describe, expect, it } from "vitest"; import { buildSuggestCommandArgs, combineSuggestionTasksAndPills, + LOCAL_SETUP_SEED_TASKS, pillsToSetupTasks, resolveSuggestExecutable, splitPillsIntoSeedAndLeftover, @@ -111,6 +112,19 @@ describe("suggest-commands", () => { expect(split.leftoverPills.map((entry) => entry.command)).toEqual(["claude", "npm run lint"]); }); + it("keeps local-heuristic leftovers selectable with the smaller local seed cap", () => { + const pills = [ + pill({ command: "dotnet build", taskType: "none", displayTitle: "Build" }), + pill({ command: "dotnet test", taskType: "none", displayTitle: "Tests" }), + pill({ command: "dotnet watch", taskType: "none", displayTitle: "Watch" }), + pill({ command: "dotnet run", taskType: "none", displayTitle: "Run" }), + ]; + + const split = splitPillsIntoSeedAndLeftover(pills, LOCAL_SETUP_SEED_TASKS); + expect(split.tasks.map((task) => task.command)).toEqual(["dotnet build", "dotnet test"]); + expect(split.leftoverPills.map((entry) => entry.command)).toEqual(["dotnet watch", "dotnet run"]); + }); + it("falls back to the first pills when none are preferred setup types", () => { const pills = [ pill({ command: "echo one", taskType: "none", displayTitle: "One" }), diff --git a/QuickShell.Raycast/src/components/workspace-form.tsx b/QuickShell.Raycast/src/components/workspace-form.tsx index b0c95fe6..7d292c6f 100644 --- a/QuickShell.Raycast/src/components/workspace-form.tsx +++ b/QuickShell.Raycast/src/components/workspace-form.tsx @@ -716,13 +716,12 @@ export default function WorkspaceForm({ placeholder={index === 0 ? "npm run dev" : "dotnet watch"} /> ))} - {suggestionSource ? ( + {unusedSuggestionPills.length > 0 ? ( { if (key === "choose-suggestion") { return; @@ -733,10 +732,7 @@ export default function WorkspaceForm({ } }} > - 0 ? "Choose a suggestion…" : "All suggestions applied"} - /> + {unusedSuggestionPills.map((pill) => ( { + const fromEnv = process.env.QUICKSHELL_SUGGEST_EXE?.trim(); + // Packaged Suggest.exe is Windows-only; allow an explicit override for local experiments. + if (!fromEnv && !isWindowsPlatform()) { + return null; + } + const executable = resolveSuggestExecutable(assetsPath); if (!executable || !existsSync(executable)) { + if (isWindowsPlatform()) { + console.warn( + `[quickshell] Suggest CLI not found at ${executable ?? "(unresolved)"}. ` + + "Run `npm run build` (or deploy-all) so assets/QuickShell.Suggest.exe is published.", + ); + } return null; } @@ -177,11 +193,14 @@ export async function fetchSuggestionPills( const { stdout } = await execFileAsync(executable, args, { windowsHide: true, maxBuffer: 1024 * 1024 }); const parsed = JSON.parse(stdout) as SuggestionResponse; if (parsed.generation !== generation) { + console.warn(`[quickshell] Suggest CLI generation mismatch (wanted ${generation}, got ${parsed.generation}).`); return null; } return parsed; - } catch { + } catch (error) { + const detail = error instanceof Error ? error.message : String(error); + console.warn(`[quickshell] Suggest CLI failed (${executable}): ${detail}`); return null; } } @@ -216,6 +235,6 @@ export async function resolveWorkspaceSetupSuggestions( displayTitle: task.label, tooltip: task.command, })); - const split = splitPillsIntoSeedAndLeftover(asPills); + const split = splitPillsIntoSeedAndLeftover(asPills, LOCAL_SETUP_SEED_TASKS); return { source: "local", tasks: split.tasks, pills: split.leftoverPills }; } diff --git a/docs/architecture/forms.md b/docs/architecture/forms.md index 909e7b0c..9ca18097 100644 --- a/docs/architecture/forms.md +++ b/docs/architecture/forms.md @@ -29,7 +29,7 @@ Browse/Paste folder on the form fills name (if unset), repo URL, and Dev Server Suggestion pills on the form call into [intelligence.md](./intelligence.md); companion fields into [companions.md](./companions.md). -In Raycast full-seed forms, unused command suggestions appear in a searchable, nonpersistent **Command suggestions** dropdown directly below the command fields. Choosing one fills the first blank command row or appends a row, then removes that choice from the dropdown. The action-panel suggestion actions remain keyboard-accessible alternatives; manual Add Workspace still uses the minimal seed and does not auto-add commands or companions. +In Raycast full-seed forms, unused command suggestions appear in a searchable, nonpersistent **Command suggestions** dropdown directly below the command fields (hidden when there are no unused leftovers). Choosing one fills the first blank command row or appends a row, then removes that choice from the dropdown. The action-panel suggestion actions remain keyboard-accessible alternatives. Manual Add Workspace offers the same suggestions in the dropdown without auto-applying them. ## Adaptive Card loop diff --git a/docs/architecture/intelligence.md b/docs/architecture/intelligence.md index 50dfee32..765ca438 100644 --- a/docs/architecture/intelligence.md +++ b/docs/architecture/intelligence.md @@ -75,7 +75,7 @@ GetPills(directory, usedCommands, maxCount = MaxPills) |------|-------------| | CmdPal | Adaptive Card pill actions on form (4 per row, 3 rows collapsed; template rebuilt for visible count; same-type pills grouped by `TypeTitle`, groups ordered by best score) | | Run | `RunLaunchSuggestionPanel` | -| Raycast | `QuickShell.Suggest.exe` → JSON pills via `suggest-commands.ts` (form seeds + Actions/dropdown). Windows deploy/package scripts publish the CLI into Raycast assets and runtime resolution uses `environment.assetsPath`. Falls back to `project-setup-suggestion.ts` heuristics when Suggest.exe is missing (including macOS). `QUICKSHELL_SUGGEST_EXE` overrides the path in development. | +| Raycast | `QuickShell.Suggest.exe` → JSON pills via `suggest-commands.ts` (form seeds + Actions/dropdown). Windows `npm` prebuild/prepublish/predev and deploy scripts publish the CLI into Raycast assets; runtime resolution uses `environment.assetsPath`. Falls back to `project-setup-suggestion.ts` heuristics when Suggest.exe is missing (including macOS); local fallback seeds at most two commands so leftover pills stay selectable. `QUICKSHELL_SUGGEST_EXE` overrides the path in development. | ## Related helpers From 44b71a65933f7dd606d0e1598f9fac23e8adbc8c Mon Sep 17 00:00:00 2001 From: tonythethompson Date: Wed, 29 Jul 2026 07:27:44 -0700 Subject: [PATCH 04/13] Fix Raycast Suggest publish path and CLI failure handling Ensure Store publish builds Suggest.exe, validate CLI payloads before use, and avoid logging local paths on Suggest failures. Co-authored-by: Cursor --- QuickShell.Raycast/README.md | 2 +- QuickShell.Raycast/package.json | 3 +- .../src/lib/suggest-commands.ts | 74 +++++++++++++++++-- 3 files changed, 70 insertions(+), 9 deletions(-) diff --git a/QuickShell.Raycast/README.md b/QuickShell.Raycast/README.md index 5119cc61..bdfaca76 100644 --- a/QuickShell.Raycast/README.md +++ b/QuickShell.Raycast/README.md @@ -62,7 +62,7 @@ npm run dev Run `npm run build` before submitting Store changes. Do not add a `version` field to `package.json`; Store versioning uses `CHANGELOG.md`. -On Windows, `npm run build`, `npm run publish`, `npm run dev` (if the asset is missing), `scripts/deploy-all.ps1`, and `scripts/build-raycast-extension.ps1` all publish `QuickShell.Suggest.exe` into Raycast `assets/` (gitignored; generated at build time). The CLI needs the .NET 10 Desktop Runtime. macOS continues to use local folder heuristics. +On Windows, `npm run build`, `npm run publish` (runs `ensure-suggest-asset.js` before the Raycast publish CLI), `npm run dev` (if the asset is missing), `scripts/deploy-all.ps1`, and `scripts/build-raycast-extension.ps1` all publish `QuickShell.Suggest.exe` into Raycast `assets/` (gitignored; generated at build/publish time). The CLI needs the .NET 10 Desktop Runtime. macOS continues to use local folder heuristics. **Distribution:** publish via the [Raycast Store](https://www.raycast.com/store) only. GitHub Releases and WinGet do not ship Raycast sideload packages. `scripts/build-raycast-extension.ps1` is for local/dev packaging. diff --git a/QuickShell.Raycast/package.json b/QuickShell.Raycast/package.json index bb4148af..d714511f 100644 --- a/QuickShell.Raycast/package.json +++ b/QuickShell.Raycast/package.json @@ -158,13 +158,12 @@ "prebuild": "node scripts/sync-workspace-trust-features.js && node scripts/verify-raycast-cli.js && node scripts/ensure-suggest-asset.js", "prelint": "node scripts/sync-workspace-trust-features.js && node scripts/verify-raycast-cli.js", "pretest": "node scripts/sync-workspace-trust-features.js", - "prepublishOnly": "node scripts/ensure-suggest-asset.js", "build": "ray build", "dev": "ray develop", "lint": "ray lint", "test": "vitest run", "test:watch": "vitest", - "publish": "npx @raycast/api@latest publish" + "publish": "node scripts/ensure-suggest-asset.js && npx @raycast/api@latest publish" }, "allowScripts": { "esbuild@0.28.1": true diff --git a/QuickShell.Raycast/src/lib/suggest-commands.ts b/QuickShell.Raycast/src/lib/suggest-commands.ts index 2fc58f68..0aae2ab8 100644 --- a/QuickShell.Raycast/src/lib/suggest-commands.ts +++ b/QuickShell.Raycast/src/lib/suggest-commands.ts @@ -164,6 +164,54 @@ export function splitPillsIntoSeedAndLeftover( }; } +function isSuggestionResponse(value: unknown): value is SuggestionResponse { + if (!value || typeof value !== "object") { + return false; + } + + const record = value as Record; + return typeof record.generation === "number" && Array.isArray(record.pills); +} + +/** Spawn/exec failures from Suggest.exe (missing binary, non-zero exit, signals). */ +function isExpectedSuggestExecFailure(error: unknown): boolean { + if (!(error instanceof Error)) { + return false; + } + + const execError = error as NodeJS.ErrnoException & { + signal?: NodeJS.Signals | null; + status?: number | null; + }; + return ( + typeof execError.code === "string" || + typeof execError.code === "number" || + execError.signal != null || + typeof execError.status === "number" + ); +} + +function formatSuggestExecFailure(error: unknown): string { + if (!(error instanceof Error)) { + return "unknown error"; + } + + const execError = error as NodeJS.ErrnoException & { + signal?: NodeJS.Signals | null; + }; + const parts: string[] = []; + if (execError.code != null) { + parts.push(`code=${execError.code}`); + } + if (execError.signal) { + parts.push(`signal=${execError.signal}`); + } + if (parts.length === 0) { + parts.push(error.name); + } + return parts.join(" "); +} + export async function fetchSuggestionPills( directory: string, usedCommands: string[], @@ -180,8 +228,8 @@ export async function fetchSuggestionPills( if (!executable || !existsSync(executable)) { if (isWindowsPlatform()) { console.warn( - `[quickshell] Suggest CLI not found at ${executable ?? "(unresolved)"}. ` + - "Run `npm run build` (or deploy-all) so assets/QuickShell.Suggest.exe is published.", + "[quickshell] Suggest CLI not found (QuickShell.Suggest.exe). " + + "Run `npm run build` or `npm run publish` so assets/QuickShell.Suggest.exe is published.", ); } return null; @@ -191,7 +239,19 @@ export async function fetchSuggestionPills( try { const { stdout } = await execFileAsync(executable, args, { windowsHide: true, maxBuffer: 1024 * 1024 }); - const parsed = JSON.parse(stdout) as SuggestionResponse; + let parsed: unknown; + try { + parsed = JSON.parse(stdout); + } catch { + console.warn("[quickshell] Suggest CLI returned invalid JSON."); + return null; + } + + if (!isSuggestionResponse(parsed)) { + console.warn("[quickshell] Suggest CLI returned an unexpected payload shape."); + return null; + } + if (parsed.generation !== generation) { console.warn(`[quickshell] Suggest CLI generation mismatch (wanted ${generation}, got ${parsed.generation}).`); return null; @@ -199,9 +259,11 @@ export async function fetchSuggestionPills( return parsed; } catch (error) { - const detail = error instanceof Error ? error.message : String(error); - console.warn(`[quickshell] Suggest CLI failed (${executable}): ${detail}`); - return null; + if (isExpectedSuggestExecFailure(error)) { + console.warn(`[quickshell] Suggest CLI failed (${formatSuggestExecFailure(error)}). Falling back to local heuristics.`); + return null; + } + throw error; } } From 2c459a5cf7eec572cfd65c50447f8f3cccbc379c Mon Sep 17 00:00:00 2001 From: tonythethompson Date: Wed, 29 Jul 2026 07:36:42 -0700 Subject: [PATCH 05/13] Fix Suggest generation overflow and add Raycast ensure script Parse --generation as long so Date.now() values echo correctly, pass the form request counter into Suggest, and add ensure-raycast-suggest.ps1 for Suggest publish plus Raycast deploy. Co-authored-by: Cursor --- .../SuggestCommandLineArgsTests.cs | 13 ++ .../Services/SuggestCommandLineArgs.cs | 5 +- .../src/components/workspace-form.tsx | 6 +- scripts/ensure-raycast-suggest.ps1 | 127 ++++++++++++++++++ 4 files changed, 146 insertions(+), 5 deletions(-) create mode 100644 scripts/ensure-raycast-suggest.ps1 diff --git a/QuickShell.Core.Tests/SuggestCommandLineArgsTests.cs b/QuickShell.Core.Tests/SuggestCommandLineArgsTests.cs index a6f487f9..356ee935 100644 --- a/QuickShell.Core.Tests/SuggestCommandLineArgsTests.cs +++ b/QuickShell.Core.Tests/SuggestCommandLineArgsTests.cs @@ -21,6 +21,19 @@ public void TryParse_RepeatedUsedFlags_PreservesCommandsWithCommas() Assert.Equal(3, generation); } + [Fact] + public void TryParse_UnixEpochGeneration_ParsesAsLong() + { + var ok = SuggestCommandLineArgs.TryParse( + ["suggest", "--dir", @"C:\Projects\demo", "--generation", "1785335300454"], + out _, + out _, + out var generation); + + Assert.True(ok); + Assert.Equal(1785335300454L, generation); + } + [Fact] public void TryParse_WithoutUsed_ReturnsEmptyList() { diff --git a/QuickShell.Core/Services/SuggestCommandLineArgs.cs b/QuickShell.Core/Services/SuggestCommandLineArgs.cs index 45319ea1..311f2776 100644 --- a/QuickShell.Core/Services/SuggestCommandLineArgs.cs +++ b/QuickShell.Core/Services/SuggestCommandLineArgs.cs @@ -6,7 +6,7 @@ public static bool TryParse( string[] args, out string? directory, out IReadOnlyList usedCommands, - out int generation) + out long generation) { directory = null; usedCommands = []; @@ -34,7 +34,8 @@ public static bool TryParse( if (string.Equals(args[i], "--generation", StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length) { - _ = int.TryParse(args[++i], out generation); + // Raycast passes Date.now() (ms since epoch), which exceeds Int32. + _ = long.TryParse(args[++i], out generation); } } diff --git a/QuickShell.Raycast/src/components/workspace-form.tsx b/QuickShell.Raycast/src/components/workspace-form.tsx index 7d292c6f..717df97d 100644 --- a/QuickShell.Raycast/src/components/workspace-form.tsx +++ b/QuickShell.Raycast/src/components/workspace-form.tsx @@ -180,7 +180,7 @@ export default function WorkspaceForm({ const resolved = await resolveWorkspaceSetupSuggestions( directory, seededCommands, - Date.now(), + generation, environment.assetsPath, ); if (cancelled || generation !== suggestionGenerationRef.current) { @@ -302,7 +302,7 @@ export default function WorkspaceForm({ const resolved = await resolveWorkspaceSetupSuggestions( nextDirectory, usedCommands, - Date.now(), + generation, environment.assetsPath, ); if (generation !== suggestionGenerationRef.current) { @@ -328,7 +328,7 @@ export default function WorkspaceForm({ tasks: [] as Array<{ label: string; command: string }>, pills: [] as SuggestionPill[], } - : await resolveWorkspaceSetupSuggestions(nextDirectory, usedCommands, Date.now(), environment.assetsPath); + : await resolveWorkspaceSetupSuggestions(nextDirectory, usedCommands, generation, environment.assetsPath); if (generation !== suggestionGenerationRef.current) { return; } diff --git a/scripts/ensure-raycast-suggest.ps1 b/scripts/ensure-raycast-suggest.ps1 new file mode 100644 index 00000000..11df2a1b --- /dev/null +++ b/scripts/ensure-raycast-suggest.ps1 @@ -0,0 +1,127 @@ +<# +.SYNOPSIS + Publish Suggest.exe into Raycast assets, then build and deploy the Raycast extension. + +.DESCRIPTION + One-shot Raycast test loop (does not touch CmdPal or PowerToys Run): + 1. Publish QuickShell.Suggest.exe into QuickShell.Raycast/assets/ + 2. Smoke-test suggest against a directory + 3. npm test/build the Raycast extension and start `npm run dev` + 4. Restart Raycast so the develop extension is live + + Requires .NET 10 SDK + Desktop Runtime, Node.js 22.14+, and Raycast for Windows. + +.EXAMPLE + .\scripts\ensure-raycast-suggest.ps1 + +.EXAMPLE + .\scripts\ensure-raycast-suggest.ps1 -Directory D:\Dev\some-project -SkipTests + +.EXAMPLE + .\scripts\ensure-raycast-suggest.ps1 -BuildOnly +#> +param( + [string]$ProjectRoot = (Split-Path -Parent $PSScriptRoot), + + [string]$Directory = $ProjectRoot, + + [ValidateSet('Debug', 'Release')] + [string]$Configuration = 'Release', + + [ValidateSet('x64')] + [string]$Platform = 'x64', + + [switch]$SkipSmokeTest, + [switch]$SkipTests, + [switch]$BuildOnly, + [switch]$NoRestart, + [switch]$SkipDeploy +) + +$ErrorActionPreference = 'Stop' + +if ($env:OS -ne 'Windows_NT') { + throw 'ensure-raycast-suggest.ps1 is Windows-only (Suggest.exe is not used on macOS).' +} + +$buildScript = Join-Path $PSScriptRoot 'build-raycast-suggest.ps1' +$lifecycleScript = Join-Path $PSScriptRoot 'RaycastLifecycle.ps1' +if (-not (Test-Path -LiteralPath $buildScript)) { + throw "Build script not found at $buildScript" +} +if (-not (Test-Path -LiteralPath $lifecycleScript)) { + throw "Raycast lifecycle helpers not found at $lifecycleScript" +} + +. $lifecycleScript + +Write-Host '1/3 Publishing QuickShell.Suggest.exe into Raycast assets...' -ForegroundColor Cyan +& $buildScript -ProjectRoot $ProjectRoot -Configuration $Configuration -Platform $Platform +if ($LASTEXITCODE -ne 0) { + throw "build-raycast-suggest.ps1 failed with exit code $LASTEXITCODE" +} + +$assetPath = Join-Path $ProjectRoot 'QuickShell.Raycast\assets\QuickShell.Suggest.exe' +if (-not (Test-Path -LiteralPath $assetPath)) { + throw "Expected asset missing after publish: $assetPath" +} + +$item = Get-Item -LiteralPath $assetPath +Write-Host ("Published: {0} ({1:N1} KB)" -f $item.FullName, ($item.Length / 1KB)) -ForegroundColor Green + +if (-not $SkipSmokeTest) { + if (-not (Test-Path -LiteralPath $Directory)) { + throw "Smoke-test directory not found: $Directory" + } + + Write-Host "Smoke-testing suggest against $Directory ..." -ForegroundColor Cyan + $stdout = & $assetPath suggest --dir $Directory --generation 1 + if ($LASTEXITCODE -ne 0) { + throw "Suggest.exe exited with code $LASTEXITCODE" + } + + $parsed = $stdout | ConvertFrom-Json + $pillCount = @($parsed.pills).Count + Write-Host "Smoke test OK: generation=$($parsed.generation), pills=$pillCount" -ForegroundColor Green +} + +if ($SkipDeploy) { + Write-Host '' + Write-Host 'Suggest ready (-SkipDeploy). Start Raycast yourself:' -ForegroundColor Yellow + Write-Host ' cd QuickShell.Raycast' + Write-Host ' npm run dev' + Write-Host (" `$env:QUICKSHELL_SUGGEST_EXE = '{0}'" -f $assetPath) -ForegroundColor DarkGray + exit 0 +} + +Write-Host '2/3 Stopping Raycast (so the extension can reload)...' -ForegroundColor Cyan +$stoppedRaycast = $false +try { + Stop-RaycastProcesses + $stoppedRaycast = $true +} +catch { + Write-Warning "Could not stop Raycast: $($_.Exception.Message)" +} + +Write-Host '3/3 Building and deploying QuickShell.Raycast...' -ForegroundColor Cyan +Deploy-RaycastExtension ` + -ProjectRoot $ProjectRoot ` + -SkipTests:$SkipTests ` + -BuildOnly:$BuildOnly ` + -StartDevServer:(-not $BuildOnly) + +if (-not $NoRestart -and $stoppedRaycast) { + Write-Host 'Restarting Raycast...' -ForegroundColor Cyan + if (-not (Start-RaycastApp)) { + Write-Warning 'Raycast deploy finished but Raycast could not be restarted.' + } +} + +Write-Host '' +Write-Host 'Raycast Suggest + deploy complete.' -ForegroundColor Green +if (-not $BuildOnly) { + Write-Host 'Use the new develop terminal (npm run dev) or search Quick Shell in Raycast.' +} +Write-Host 'In the workspace form, expect Suggest copy (not "Suggest.exe is unavailable").' -ForegroundColor DarkGray +Write-Host ("Asset: {0}" -f $assetPath) -ForegroundColor DarkGray From 2a3c5c4332e60d99c1455e13e632c623ac20ec20 Mon Sep 17 00:00:00 2001 From: tonythethompson Date: Wed, 29 Jul 2026 07:37:45 -0700 Subject: [PATCH 06/13] Fix Prettier formatting in suggest-commands.ts Co-authored-by: Cursor --- QuickShell.Raycast/src/lib/suggest-commands.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/QuickShell.Raycast/src/lib/suggest-commands.ts b/QuickShell.Raycast/src/lib/suggest-commands.ts index 0aae2ab8..cc846016 100644 --- a/QuickShell.Raycast/src/lib/suggest-commands.ts +++ b/QuickShell.Raycast/src/lib/suggest-commands.ts @@ -260,7 +260,9 @@ export async function fetchSuggestionPills( return parsed; } catch (error) { if (isExpectedSuggestExecFailure(error)) { - console.warn(`[quickshell] Suggest CLI failed (${formatSuggestExecFailure(error)}). Falling back to local heuristics.`); + console.warn( + `[quickshell] Suggest CLI failed (${formatSuggestExecFailure(error)}). Falling back to local heuristics.`, + ); return null; } throw error; From 92bfb64447504a9762c6c73ea6ab16af921e92e7 Mon Sep 17 00:00:00 2001 From: tonythethompson Date: Wed, 29 Jul 2026 09:48:08 -0700 Subject: [PATCH 07/13] Harden Suggest payload parsing and Raycast ensure script Validate each suggestion pill shape before use, and make the ensure helper safer for smoke-test JSON and SkipDeploy/stop paths. Co-authored-by: Cursor --- .../src/__tests__/suggest-commands.test.ts | 43 +++++++++++++++++++ .../src/lib/suggest-commands.ts | 39 ++++++++++++++--- scripts/ensure-raycast-suggest.ps1 | 17 +++----- 3 files changed, 83 insertions(+), 16 deletions(-) diff --git a/QuickShell.Raycast/src/__tests__/suggest-commands.test.ts b/QuickShell.Raycast/src/__tests__/suggest-commands.test.ts index b96cb9c5..8384035c 100644 --- a/QuickShell.Raycast/src/__tests__/suggest-commands.test.ts +++ b/QuickShell.Raycast/src/__tests__/suggest-commands.test.ts @@ -4,6 +4,7 @@ import { buildSuggestCommandArgs, combineSuggestionTasksAndPills, LOCAL_SETUP_SEED_TASKS, + parseSuggestionResponse, pillsToSetupTasks, resolveSuggestExecutable, splitPillsIntoSeedAndLeftover, @@ -136,4 +137,46 @@ describe("suggest-commands", () => { expect(split.tasks.map((task) => task.command)).toEqual(["echo one", "echo two"]); expect(split.leftoverPills.map((entry) => entry.command)).toEqual(["echo three"]); }); + + it("parses Suggest payloads and drops malformed pills", () => { + expect( + parseSuggestionResponse({ + generation: 7, + pills: [ + { + command: "npm run dev", + taskType: "frontend", + typeTitle: "Frontend", + displayTitle: "Dev", + tooltip: "npm run dev", + }, + { command: 42, taskType: "frontend" }, + null, + "bad", + ], + }), + ).toEqual({ + generation: 7, + pills: [ + { + command: "npm run dev", + taskType: "frontend", + typeTitle: "Frontend", + displayTitle: "Dev", + tooltip: "npm run dev", + }, + ], + }); + }); + + it("rejects Suggest payloads when every pill is malformed", () => { + expect( + parseSuggestionResponse({ + generation: 1, + pills: [{ command: 1 }, { taskType: "frontend" }], + }), + ).toBeNull(); + expect(parseSuggestionResponse({ generation: "1", pills: [] })).toBeNull(); + expect(parseSuggestionResponse({ generation: 1 })).toBeNull(); + }); }); diff --git a/QuickShell.Raycast/src/lib/suggest-commands.ts b/QuickShell.Raycast/src/lib/suggest-commands.ts index cc846016..6fdd42f5 100644 --- a/QuickShell.Raycast/src/lib/suggest-commands.ts +++ b/QuickShell.Raycast/src/lib/suggest-commands.ts @@ -164,13 +164,39 @@ export function splitPillsIntoSeedAndLeftover( }; } -function isSuggestionResponse(value: unknown): value is SuggestionResponse { +function isSuggestionPill(value: unknown): value is SuggestionPill { if (!value || typeof value !== "object") { return false; } + const pill = value as Record; + return ( + typeof pill.command === "string" && + typeof pill.taskType === "string" && + typeof pill.typeTitle === "string" && + typeof pill.displayTitle === "string" && + typeof pill.tooltip === "string" + ); +} + +/** Parse Suggest CLI JSON; drop malformed pills. Returns null when the payload is unusable. */ +export function parseSuggestionResponse(value: unknown): SuggestionResponse | null { + if (!value || typeof value !== "object") { + return null; + } + const record = value as Record; - return typeof record.generation === "number" && Array.isArray(record.pills); + if (typeof record.generation !== "number" || !Number.isFinite(record.generation) || !Array.isArray(record.pills)) { + return null; + } + + const pills = record.pills.filter(isSuggestionPill); + // Non-empty array with zero valid pills is a corrupt payload, not "no suggestions". + if (record.pills.length > 0 && pills.length === 0) { + return null; + } + + return { generation: record.generation, pills }; } /** Spawn/exec failures from Suggest.exe (missing binary, non-zero exit, signals). */ @@ -247,17 +273,18 @@ export async function fetchSuggestionPills( return null; } - if (!isSuggestionResponse(parsed)) { + const response = parseSuggestionResponse(parsed); + if (!response) { console.warn("[quickshell] Suggest CLI returned an unexpected payload shape."); return null; } - if (parsed.generation !== generation) { - console.warn(`[quickshell] Suggest CLI generation mismatch (wanted ${generation}, got ${parsed.generation}).`); + if (response.generation !== generation) { + console.warn(`[quickshell] Suggest CLI generation mismatch (wanted ${generation}, got ${response.generation}).`); return null; } - return parsed; + return response; } catch (error) { if (isExpectedSuggestExecFailure(error)) { console.warn( diff --git a/scripts/ensure-raycast-suggest.ps1 b/scripts/ensure-raycast-suggest.ps1 index 11df2a1b..2ccda64e 100644 --- a/scripts/ensure-raycast-suggest.ps1 +++ b/scripts/ensure-raycast-suggest.ps1 @@ -80,7 +80,9 @@ if (-not $SkipSmokeTest) { throw "Suggest.exe exited with code $LASTEXITCODE" } - $parsed = $stdout | ConvertFrom-Json + # Native stdout may be a string or a line array; always parse as one JSON document. + $json = if ($null -eq $stdout) { '' } elseif ($stdout -is [string]) { $stdout } else { $stdout -join "`n" } + $parsed = $json | ConvertFrom-Json $pillCount = @($parsed.pills).Count Write-Host "Smoke test OK: generation=$($parsed.generation), pills=$pillCount" -ForegroundColor Green } @@ -91,18 +93,13 @@ if ($SkipDeploy) { Write-Host ' cd QuickShell.Raycast' Write-Host ' npm run dev' Write-Host (" `$env:QUICKSHELL_SUGGEST_EXE = '{0}'" -f $assetPath) -ForegroundColor DarkGray - exit 0 + return } Write-Host '2/3 Stopping Raycast (so the extension can reload)...' -ForegroundColor Cyan -$stoppedRaycast = $false -try { - Stop-RaycastProcesses - $stoppedRaycast = $true -} -catch { - Write-Warning "Could not stop Raycast: $($_.Exception.Message)" -} +# Stop-RaycastProcesses already uses -ErrorAction SilentlyContinue; let unexpected errors surface. +Stop-RaycastProcesses +$stoppedRaycast = $true Write-Host '3/3 Building and deploying QuickShell.Raycast...' -ForegroundColor Cyan Deploy-RaycastExtension ` From 804232ef0a4f47cff59e8c9dd41176aeeb5770a6 Mon Sep 17 00:00:00 2001 From: tonythethompson Date: Thu, 30 Jul 2026 04:37:12 -0700 Subject: [PATCH 08/13] Harden Suggest fallback and Raycast ensure lifecycle Catch unexpected Suggest failures so form flows fall back locally, and tighten ensure-raycast-suggest smoke-test and BuildOnly/SkipDeploy behavior. Co-authored-by: Cursor --- .../src/lib/suggest-commands.ts | 22 +++++++++----- scripts/ensure-raycast-suggest.ps1 | 29 ++++++++++++++----- 2 files changed, 36 insertions(+), 15 deletions(-) diff --git a/QuickShell.Raycast/src/lib/suggest-commands.ts b/QuickShell.Raycast/src/lib/suggest-commands.ts index 6fdd42f5..43a473de 100644 --- a/QuickShell.Raycast/src/lib/suggest-commands.ts +++ b/QuickShell.Raycast/src/lib/suggest-commands.ts @@ -308,14 +308,20 @@ export async function resolveWorkspaceSetupSuggestions( return { source: "local", tasks: [], pills: [] }; } - const response = await fetchSuggestionPills(trimmed, usedCommands, generation, assetsPath); - if (response && response.pills.length > 0) { - const split = splitPillsIntoSeedAndLeftover(response.pills); - return { - source: "suggest", - tasks: split.tasks, - pills: split.leftoverPills, - }; + // Fire-and-forget form callers must not see unhandled rejections from unexpected Suggest failures. + try { + const response = await fetchSuggestionPills(trimmed, usedCommands, generation, assetsPath); + if (response && response.pills.length > 0) { + const split = splitPillsIntoSeedAndLeftover(response.pills); + return { + source: "suggest", + tasks: split.tasks, + pills: split.leftoverPills, + }; + } + } catch (error) { + const kind = error instanceof Error ? error.name : "unknown"; + console.warn(`[quickshell] Suggest resolution failed unexpectedly (${kind}); using local heuristics.`); } const tasks = buildProjectSetupSuggestions(trimmed); diff --git a/scripts/ensure-raycast-suggest.ps1 b/scripts/ensure-raycast-suggest.ps1 index 2ccda64e..0effe909 100644 --- a/scripts/ensure-raycast-suggest.ps1 +++ b/scripts/ensure-raycast-suggest.ps1 @@ -83,23 +83,38 @@ if (-not $SkipSmokeTest) { # Native stdout may be a string or a line array; always parse as one JSON document. $json = if ($null -eq $stdout) { '' } elseif ($stdout -is [string]) { $stdout } else { $stdout -join "`n" } $parsed = $json | ConvertFrom-Json + if ($null -eq $parsed -or $parsed -is [System.Array] -or $parsed -is [string] -or $parsed -is [ValueType]) { + throw 'Suggest.exe returned a non-object JSON payload.' + } + if ($null -eq $parsed.PSObject.Properties['generation'] -or $null -eq $parsed.PSObject.Properties['pills']) { + throw 'Suggest.exe response missing generation or pills.' + } + if ([long]$parsed.generation -ne 1) { + throw "Suggest.exe generation mismatch (wanted 1, got $($parsed.generation))." + } + if ($null -eq $parsed.pills -or $parsed.pills -isnot [System.Array]) { + throw 'Suggest.exe response pills must be an array.' + } $pillCount = @($parsed.pills).Count Write-Host "Smoke test OK: generation=$($parsed.generation), pills=$pillCount" -ForegroundColor Green } if ($SkipDeploy) { + $raycastRoot = Join-Path $ProjectRoot 'QuickShell.Raycast' Write-Host '' Write-Host 'Suggest ready (-SkipDeploy). Start Raycast yourself:' -ForegroundColor Yellow - Write-Host ' cd QuickShell.Raycast' - Write-Host ' npm run dev' Write-Host (" `$env:QUICKSHELL_SUGGEST_EXE = '{0}'" -f $assetPath) -ForegroundColor DarkGray + Write-Host (" Set-Location -LiteralPath '{0}'" -f $raycastRoot) + Write-Host ' npm run dev' return } -Write-Host '2/3 Stopping Raycast (so the extension can reload)...' -ForegroundColor Cyan -# Stop-RaycastProcesses already uses -ErrorAction SilentlyContinue; let unexpected errors surface. -Stop-RaycastProcesses -$stoppedRaycast = $true +# -BuildOnly should not stop or launch Raycast; only package/build. +if (-not $BuildOnly) { + Write-Host '2/3 Stopping Raycast (so the extension can reload)...' -ForegroundColor Cyan + # Stop-RaycastProcesses already uses -ErrorAction SilentlyContinue; let unexpected errors surface. + Stop-RaycastProcesses +} Write-Host '3/3 Building and deploying QuickShell.Raycast...' -ForegroundColor Cyan Deploy-RaycastExtension ` @@ -108,7 +123,7 @@ Deploy-RaycastExtension ` -BuildOnly:$BuildOnly ` -StartDevServer:(-not $BuildOnly) -if (-not $NoRestart -and $stoppedRaycast) { +if (-not $BuildOnly -and -not $NoRestart) { Write-Host 'Restarting Raycast...' -ForegroundColor Cyan if (-not (Start-RaycastApp)) { Write-Warning 'Raycast deploy finished but Raycast could not be restarted.' From 8ba68c082794b0f7850d4bd5ec214a94142551d5 Mon Sep 17 00:00:00 2001 From: tonythethompson Date: Thu, 30 Jul 2026 04:48:59 -0700 Subject: [PATCH 09/13] Tighten Raycast ensure smoke checks and path handling Require absolute SkipDeploy paths, numeric generation without string coercion, valid pill shapes, and BuildOnly-specific completion output. Co-authored-by: Cursor --- scripts/ensure-raycast-suggest.ps1 | 66 +++++++++++++++++++++++++++--- 1 file changed, 60 insertions(+), 6 deletions(-) diff --git a/scripts/ensure-raycast-suggest.ps1 b/scripts/ensure-raycast-suggest.ps1 index 0effe909..4a74a9c3 100644 --- a/scripts/ensure-raycast-suggest.ps1 +++ b/scripts/ensure-raycast-suggest.ps1 @@ -44,6 +44,12 @@ if ($env:OS -ne 'Windows_NT') { throw 'ensure-raycast-suggest.ps1 is Windows-only (Suggest.exe is not used on macOS).' } +if (-not (Test-Path -LiteralPath $ProjectRoot)) { + throw "ProjectRoot not found: $ProjectRoot" +} +# Absolute paths so printed SkipDeploy commands stay valid after Set-Location. +$ProjectRoot = (Resolve-Path -LiteralPath $ProjectRoot).ProviderPath + $buildScript = Join-Path $PSScriptRoot 'build-raycast-suggest.ps1' $lifecycleScript = Join-Path $PSScriptRoot 'RaycastLifecycle.ps1' if (-not (Test-Path -LiteralPath $buildScript)) { @@ -65,10 +71,49 @@ $assetPath = Join-Path $ProjectRoot 'QuickShell.Raycast\assets\QuickShell.Sugges if (-not (Test-Path -LiteralPath $assetPath)) { throw "Expected asset missing after publish: $assetPath" } +$assetPath = (Resolve-Path -LiteralPath $assetPath).ProviderPath $item = Get-Item -LiteralPath $assetPath Write-Host ("Published: {0} ({1:N1} KB)" -f $item.FullName, ($item.Length / 1KB)) -ForegroundColor Green +function Test-SuggestGenerationEqualsOne { + param([object]$Generation) + + # Reject strings/bools so "1" is not coerced to 1. + if ($null -eq $Generation -or $Generation -is [string] -or $Generation -is [bool] -or $Generation -is [char]) { + return $false + } + if (-not ( + $Generation -is [byte] -or $Generation -is [sbyte] -or + $Generation -is [int16] -or $Generation -is [uint16] -or + $Generation -is [int] -or $Generation -is [uint32] -or + $Generation -is [long] -or $Generation -is [uint64] -or + $Generation -is [float] -or $Generation -is [double] -or + $Generation -is [decimal] + )) { + return $false + } + if (($Generation -is [float] -or $Generation -is [double]) -and -not [double]::IsFinite([double]$Generation)) { + return $false + } + return $Generation -eq 1 +} + +function Test-SuggestPillShape { + param([object]$Pill) + + if ($null -eq $Pill -or $Pill -is [string] -or $Pill -is [ValueType] -or $Pill -is [System.Array]) { + return $false + } + foreach ($field in @('command', 'taskType', 'typeTitle', 'displayTitle', 'tooltip')) { + $prop = $Pill.PSObject.Properties[$field] + if ($null -eq $prop -or $prop.Value -isnot [string]) { + return $false + } + } + return $true +} + if (-not $SkipSmokeTest) { if (-not (Test-Path -LiteralPath $Directory)) { throw "Smoke-test directory not found: $Directory" @@ -89,18 +134,23 @@ if (-not $SkipSmokeTest) { if ($null -eq $parsed.PSObject.Properties['generation'] -or $null -eq $parsed.PSObject.Properties['pills']) { throw 'Suggest.exe response missing generation or pills.' } - if ([long]$parsed.generation -ne 1) { + if (-not (Test-SuggestGenerationEqualsOne -Generation $parsed.generation)) { throw "Suggest.exe generation mismatch (wanted 1, got $($parsed.generation))." } if ($null -eq $parsed.pills -or $parsed.pills -isnot [System.Array]) { throw 'Suggest.exe response pills must be an array.' } + foreach ($pill in @($parsed.pills)) { + if (-not (Test-SuggestPillShape -Pill $pill)) { + throw 'Suggest.exe returned a malformed pill.' + } + } $pillCount = @($parsed.pills).Count Write-Host "Smoke test OK: generation=$($parsed.generation), pills=$pillCount" -ForegroundColor Green } if ($SkipDeploy) { - $raycastRoot = Join-Path $ProjectRoot 'QuickShell.Raycast' + $raycastRoot = (Resolve-Path -LiteralPath (Join-Path $ProjectRoot 'QuickShell.Raycast')).ProviderPath Write-Host '' Write-Host 'Suggest ready (-SkipDeploy). Start Raycast yourself:' -ForegroundColor Yellow Write-Host (" `$env:QUICKSHELL_SUGGEST_EXE = '{0}'" -f $assetPath) -ForegroundColor DarkGray @@ -131,9 +181,13 @@ if (-not $BuildOnly -and -not $NoRestart) { } Write-Host '' -Write-Host 'Raycast Suggest + deploy complete.' -ForegroundColor Green -if (-not $BuildOnly) { +if ($BuildOnly) { + Write-Host 'Raycast Suggest build complete (-BuildOnly).' -ForegroundColor Green + Write-Host ("Asset: {0}" -f $assetPath) -ForegroundColor DarkGray +} +else { + Write-Host 'Raycast Suggest + deploy complete.' -ForegroundColor Green Write-Host 'Use the new develop terminal (npm run dev) or search Quick Shell in Raycast.' + Write-Host 'In the workspace form, expect Suggest copy (not "Suggest.exe is unavailable").' -ForegroundColor DarkGray + Write-Host ("Asset: {0}" -f $assetPath) -ForegroundColor DarkGray } -Write-Host 'In the workspace form, expect Suggest copy (not "Suggest.exe is unavailable").' -ForegroundColor DarkGray -Write-Host ("Asset: {0}" -f $assetPath) -ForegroundColor DarkGray From bcf862b72a94c9190965adaf702263d6df60c24a Mon Sep 17 00:00:00 2001 From: tonythethompson Date: Thu, 30 Jul 2026 05:03:17 -0700 Subject: [PATCH 10/13] Align Suggest deploy config and enforce JSON key casing Pass Configuration through Deploy-RaycastExtension and require exact lowercase smoke-test JSON property names. Co-authored-by: Cursor --- scripts/RaycastLifecycle.ps1 | 4 +++- scripts/ensure-raycast-suggest.ps1 | 36 +++++++++++++++++++++++------- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/scripts/RaycastLifecycle.ps1 b/scripts/RaycastLifecycle.ps1 index a66e4b15..9597999a 100644 --- a/scripts/RaycastLifecycle.ps1 +++ b/scripts/RaycastLifecycle.ps1 @@ -137,6 +137,8 @@ function Invoke-NpmCommand { function Deploy-RaycastExtension { param( [string]$ProjectRoot, + [ValidateSet('Debug', 'Release')] + [string]$Configuration = 'Release', [switch]$SkipTests, [switch]$BuildOnly, [switch]$StartDevServer @@ -160,7 +162,7 @@ function Deploy-RaycastExtension { if (-not (Test-Path -LiteralPath $suggestBuildScript)) { throw "QuickShell.Suggest build script not found at $suggestBuildScript" } - & $suggestBuildScript -ProjectRoot $ProjectRoot -Configuration Release -Platform x64 + & $suggestBuildScript -ProjectRoot $ProjectRoot -Configuration $Configuration -Platform x64 if ($LASTEXITCODE -ne 0) { throw "QuickShell.Suggest publish failed with exit code $LASTEXITCODE" } diff --git a/scripts/ensure-raycast-suggest.ps1 b/scripts/ensure-raycast-suggest.ps1 index 4a74a9c3..fc440ace 100644 --- a/scripts/ensure-raycast-suggest.ps1 +++ b/scripts/ensure-raycast-suggest.ps1 @@ -99,6 +99,23 @@ function Test-SuggestGenerationEqualsOne { return $Generation -eq 1 } +function Get-ExactJsonProperty { + param( + [Parameter(Mandatory)] + [object]$Object, + [Parameter(Mandatory)] + [string]$Name + ) + + # PSObject.Properties[$name] is case-insensitive; match JSON key casing exactly. + foreach ($prop in $Object.PSObject.Properties) { + if ($prop.Name -ceq $Name) { + return $prop + } + } + return $null +} + function Test-SuggestPillShape { param([object]$Pill) @@ -106,7 +123,7 @@ function Test-SuggestPillShape { return $false } foreach ($field in @('command', 'taskType', 'typeTitle', 'displayTitle', 'tooltip')) { - $prop = $Pill.PSObject.Properties[$field] + $prop = Get-ExactJsonProperty -Object $Pill -Name $field if ($null -eq $prop -or $prop.Value -isnot [string]) { return $false } @@ -131,22 +148,24 @@ if (-not $SkipSmokeTest) { if ($null -eq $parsed -or $parsed -is [System.Array] -or $parsed -is [string] -or $parsed -is [ValueType]) { throw 'Suggest.exe returned a non-object JSON payload.' } - if ($null -eq $parsed.PSObject.Properties['generation'] -or $null -eq $parsed.PSObject.Properties['pills']) { + $generationProp = Get-ExactJsonProperty -Object $parsed -Name 'generation' + $pillsProp = Get-ExactJsonProperty -Object $parsed -Name 'pills' + if ($null -eq $generationProp -or $null -eq $pillsProp) { throw 'Suggest.exe response missing generation or pills.' } - if (-not (Test-SuggestGenerationEqualsOne -Generation $parsed.generation)) { - throw "Suggest.exe generation mismatch (wanted 1, got $($parsed.generation))." + if (-not (Test-SuggestGenerationEqualsOne -Generation $generationProp.Value)) { + throw "Suggest.exe generation mismatch (wanted 1, got $($generationProp.Value))." } - if ($null -eq $parsed.pills -or $parsed.pills -isnot [System.Array]) { + if ($null -eq $pillsProp.Value -or $pillsProp.Value -isnot [System.Array]) { throw 'Suggest.exe response pills must be an array.' } - foreach ($pill in @($parsed.pills)) { + foreach ($pill in @($pillsProp.Value)) { if (-not (Test-SuggestPillShape -Pill $pill)) { throw 'Suggest.exe returned a malformed pill.' } } - $pillCount = @($parsed.pills).Count - Write-Host "Smoke test OK: generation=$($parsed.generation), pills=$pillCount" -ForegroundColor Green + $pillCount = @($pillsProp.Value).Count + Write-Host "Smoke test OK: generation=$($generationProp.Value), pills=$pillCount" -ForegroundColor Green } if ($SkipDeploy) { @@ -169,6 +188,7 @@ if (-not $BuildOnly) { Write-Host '3/3 Building and deploying QuickShell.Raycast...' -ForegroundColor Cyan Deploy-RaycastExtension ` -ProjectRoot $ProjectRoot ` + -Configuration $Configuration ` -SkipTests:$SkipTests ` -BuildOnly:$BuildOnly ` -StartDevServer:(-not $BuildOnly) From 6d1534d7b5c0c6a4bdfbca87f72045e01972fe5f Mon Sep 17 00:00:00 2001 From: tonythethompson Date: Thu, 30 Jul 2026 16:02:10 -0700 Subject: [PATCH 11/13] Docs update --- .gitignore | 1 + README.md | 4 +- cmdpal-gallery/README.md | 7 ++ .../tonythethompson/quickshell/extension.json | 2 +- docs/assets/css/main.scss | 15 ++++ docs/index.md | 8 ++ docs/install.md | 9 ++- docs/release-packages.md | 2 +- scripts/submit-cmdpal-gallery.ps1 | 73 ++++++++++++------- winget/README.md | 14 ++-- 10 files changed, 96 insertions(+), 39 deletions(-) diff --git a/.gitignore b/.gitignore index 331480ab..a6b62f75 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ _nupkg/ _winget-pkgs/ _winget-pr/ _winget-test/ +winget-out/ # Shared Claude hooks/settings; ignore the rest of the tree. .claude/* diff --git a/README.md b/README.md index 149cb5c0..102c3aa1 100644 --- a/README.md +++ b/README.md @@ -281,7 +281,7 @@ The Store package is **Command Palette only**. For PowerToys Run, see [PowerToys ### Option 2: WinGet -Two packages, same extension, different extras: +Same extension, choose bundled (CmdPal + Run) or CmdPal only: | Package | What you get | | --- | --- | @@ -292,7 +292,7 @@ Two packages, same extension, different extras: # Bundled (CmdPal + Run) winget install tonythethompson.QuickShell -# CmdPal only +# CmdPal only (Store-equivalent) winget install tonythethompson.QuickShellforCmdPal ``` diff --git a/cmdpal-gallery/README.md b/cmdpal-gallery/README.md index a452c4e5..9cf1fc38 100644 --- a/cmdpal-gallery/README.md +++ b/cmdpal-gallery/README.md @@ -11,6 +11,11 @@ Ready-to-submit package for [microsoft/CmdPal-Extensions](https://github.com/mic Publishing to the Microsoft Store registers the extension with Command Palette, but **does not** add it to the in-app Extension Gallery. That requires a separate PR to `microsoft/CmdPal-Extensions`. +## Install sources + +- Store: `9PC8S6LNRT3R` +- WinGet: `tonythethompson.QuickShellforCmdPal` (CmdPal only; not the bundled Run package) + ## Submit / update Listing already lives at `extensions/tonythethompson/quickshell/` upstream. To refresh logo, screenshots, or metadata: @@ -21,6 +26,8 @@ Listing already lives at `extensions/tonythethompson/quickshell/` upstream. To r Manual path: fork https://github.com/microsoft/CmdPal-Extensions, replace `extensions/tonythethompson/quickshell/`, open a PR titled `Update tonythethompson.quickshell gallery listing`. +WinGet version bumps do not need a gallery PR; the listing only stores the package ID. + ## Gallery title note The gallery entry uses **Quick Shell** in Command Palette. The Store listing title **Quick Shell for CmdPal** is set in Partner Center, not in the MSIX manifest (CmdPal reads package display name from the manifest). diff --git a/cmdpal-gallery/extensions/tonythethompson/quickshell/extension.json b/cmdpal-gallery/extensions/tonythethompson/quickshell/extension.json index 826fe30a..ed59a47a 100644 --- a/cmdpal-gallery/extensions/tonythethompson/quickshell/extension.json +++ b/cmdpal-gallery/extensions/tonythethompson/quickshell/extension.json @@ -29,7 +29,7 @@ }, { "type": "winget", - "id": "tonythethompson.QuickShell" + "id": "tonythethompson.QuickShellforCmdPal" } ] } diff --git a/docs/assets/css/main.scss b/docs/assets/css/main.scss index 1fd4a4f4..546fb224 100644 --- a/docs/assets/css/main.scss +++ b/docs/assets/css/main.scss @@ -321,6 +321,21 @@ kbd { } } +.install-snippet { + margin: 0 0 0.5rem; + max-width: 36rem; + text-align: left; + font-size: 0.82rem; + padding: 0.9rem 1.1rem; +} + +.install-snippet-note { + margin: 0 0 1.5rem; + color: $text-muted; + font-size: 0.85rem; + max-width: 36rem; +} + // ── Sponsor ───────────────────────────────────────────── .sponsor-row { diff --git a/docs/index.md b/docs/index.md index d1a4d5c9..ec94d5aa 100644 --- a/docs/index.md +++ b/docs/index.md @@ -14,11 +14,19 @@ description: Quick Shell opens saved project folders from PowerToys Command Pale +
winget install tonythethompson.QuickShell
+winget install tonythethompson.QuickShellforCmdPal
+

+ Bundled (Command Palette + PowerToys Run), or CmdPal only (Store-equivalent). + More install options +

+ ### WinGet (Command Line) +{: #winget-command-line} + +Both Command Palette packages are live on WinGet. Prefer the bundled ID if you also want PowerToys Run (`qs`). ```powershell -# Command Palette + PowerToys Run +# Command Palette + PowerToys Run (recommended for most users) winget install tonythethompson.QuickShell -# Command Palette only (Store-equivalent) +# Command Palette only (same scope as Microsoft Store) winget install tonythethompson.QuickShellforCmdPal -# PowerToys Run only +# PowerToys Run only (when that package is listed) winget install tonythethompson.QuickShellforRun ``` diff --git a/docs/release-packages.md b/docs/release-packages.md index f6135cf0..119d95c9 100644 --- a/docs/release-packages.md +++ b/docs/release-packages.md @@ -56,7 +56,7 @@ winget install tonythethompson.QuickShellforCmdPal winget install tonythethompson.QuickShellforRun ``` -Initial manifests for `QuickShellforCmdPal` and `QuickShellforRun` must be created in [microsoft/winget-pkgs](https://github.com/microsoft/winget-pkgs) before CI can auto-submit updates for those IDs. Until then, release CI updates **`tonythethompson.QuickShell`** (bundled) and soft-skips the missing packages. The release workflow submits updates when `WINGET_PAT` is configured. +`tonythethompson.QuickShell` (bundled) and `tonythethompson.QuickShellforCmdPal` are published in [microsoft/winget-pkgs](https://github.com/microsoft/winget-pkgs); release CI submits version bumps for both when `WINGET_PAT` is configured. `tonythethompson.QuickShellforRun` still needs its initial package merge before CI can auto-update that ID (soft-skip until then). The former `tonythethompson.QuickShellforRaycast` WinGet package is retired (no further CI updates). Prefer the Raycast Store. diff --git a/scripts/submit-cmdpal-gallery.ps1 b/scripts/submit-cmdpal-gallery.ps1 index 171cf914..7cdf5da8 100644 --- a/scripts/submit-cmdpal-gallery.ps1 +++ b/scripts/submit-cmdpal-gallery.ps1 @@ -1,8 +1,12 @@ # Submit or update Quick Shell in the Command Palette Extension Gallery. # Requires: gh auth login, fork of microsoft/CmdPal-Extensions +# +# Always creates a fresh branch from upstream main and a normal push (no force). param( [switch]$DryRun, - [string]$Branch = 'update-tonythethompson-quickshell' + [string]$Branch = '', + [string]$Title = 'Update tonythethompson.quickshell gallery listing', + [string]$Body = '' ) $ErrorActionPreference = 'Stop' @@ -15,8 +19,12 @@ if (-not (Test-Path $source)) { throw "Missing gallery source at $source" } +if ([string]::IsNullOrWhiteSpace($Branch)) { + $Branch = 'update-tonythethompson-quickshell-' + (Get-Date -Format 'yyyyMMdd-HHmmss') +} + if ($DryRun) { - Write-Host "Would sync fork $upstream, copy $source, and open PR on branch $Branch" + Write-Host "Would sync fork $upstream, copy $source, push branch $Branch (no force), and open a PR" exit 0 } @@ -41,36 +49,51 @@ if (-not $forkExists) { } Write-Host "Using fork: $forkRepo" +# Sync the fork's default branch from upstream (not a branch force-push). gh repo sync $forkRepo --source $upstream --force 2>$null gh repo clone $forkRepo $workDir -- --depth=1 Push-Location $workDir -git checkout -b $Branch -$dest = Join-Path $workDir 'extensions\tonythethompson\quickshell' -New-Item -ItemType Directory -Force -Path $dest | Out-Null -# Replace listing contents so renamed/removed screenshots do not linger. -Get-ChildItem -Force $dest | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue -Copy-Item -Recurse -Force (Join-Path $source '*') $dest -git add -A extensions/tonythethompson/quickshell -git status --short -git commit -m "Update tonythethompson.quickshell gallery listing" -git push -u origin $Branch --force-with-lease -$bodyFile = Join-Path $env:TEMP 'cmdpal-gallery-pr-body.md' -@' +try { + git checkout -b $Branch + $dest = Join-Path $workDir 'extensions\tonythethompson\quickshell' + New-Item -ItemType Directory -Force -Path $dest | Out-Null + # Replace listing contents so renamed/removed screenshots do not linger. + Get-ChildItem -Force $dest | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue + Copy-Item -Recurse -Force (Join-Path $source '*') $dest + git add -A extensions/tonythethompson/quickshell + git status --short + + if (-not (git status --porcelain)) { + throw 'No gallery listing changes to commit.' + } + + git commit -m $Title + git push -u origin $Branch + if ($LASTEXITCODE -ne 0) { + throw "git push failed for branch $Branch (refusing to force-push)." + } + + if ([string]::IsNullOrWhiteSpace($Body)) { + $Body = @" ## Summary Updates the **Quick Shell** Command Palette Extension Gallery listing. -- New product logo (`icon.png`, Store AppTile) -- Screenshots refreshed for current workspace UI (list, create, settings, detail) -- Description/tags aligned with current product language -- Install sources unchanged: Microsoft Store `9PC8S6LNRT3R`, WinGet `tonythethompson.QuickShell` +Describe only what this PR changes (for example logo, screenshots, copy, or install sources). Do not reuse stale bullets from a prior submission. ## Test plan - [ ] CI schema validation passes -- [ ] Store product ID resolves -- [ ] Icon under 100 KB; screenshots under 1 MB each +- [ ] Store / WinGet install source IDs resolve +- [ ] Icon under 100 KB; screenshots under 1 MB each (when media changed) - [ ] Tags ≤ 5 -'@ | Set-Content -Path $bodyFile -Encoding utf8 -$prUrl = gh pr create --repo $upstream --head "${login}:$Branch" --title 'Update tonythethompson.quickshell gallery listing' --body-file $bodyFile -Remove-Item $bodyFile -Force -ErrorAction SilentlyContinue -Pop-Location -Write-Host "PR opened: $prUrl" +"@ + } + + $bodyFile = Join-Path $env:TEMP 'cmdpal-gallery-pr-body.md' + Set-Content -Path $bodyFile -Value $Body -Encoding utf8 + $prUrl = gh pr create --repo $upstream --head "${login}:$Branch" --title $Title --body-file $bodyFile + Remove-Item $bodyFile -Force -ErrorAction SilentlyContinue + Write-Host "PR opened: $prUrl" +} +finally { + Pop-Location +} diff --git a/winget/README.md b/winget/README.md index 47ea4e2d..e5e18338 100644 --- a/winget/README.md +++ b/winget/README.md @@ -13,12 +13,12 @@ Both installers register the same Command Palette extension (shared Inno `AppId` Template manifests for the repo copy. Release CI runs `wingetcreate update` against both IDs after each GitHub Release. -## First-time setup for `tonythethompson.QuickShellforCmdPal` +## Package status -`wingetcreate update` only works after the package exists in [microsoft/winget-pkgs](https://github.com/microsoft/winget-pkgs). Before the first release that ships `QuickShellforCmdPal-Setup-*.exe`: +| Package ID | Status | +| --- | --- | +| `tonythethompson.QuickShell` | Published; CI submits version bumps | +| `tonythethompson.QuickShellforCmdPal` | Published (initial `0.2.3.0`); CI submits version bumps | +| `tonythethompson.QuickShellforRun` | Initial package PR must merge once before CI can `wingetcreate update` | -1. Cut a GitHub Release that includes the CmdPal-only installers. -2. Update SHA256 values in `tonythethompson.QuickShellforCmdPal.installer.yaml`. -3. Open a PR to winget-pkgs with the three manifest files (or run `wingetcreate new` / `wingetcreate submit`). - -Subsequent releases are updated automatically by `.github/workflows/release-extension.yml`. +`wingetcreate update` only works after the package exists in [microsoft/winget-pkgs](https://github.com/microsoft/winget-pkgs). Subsequent releases for published IDs are submitted by `.github/workflows/release-extension.yml`. From c0baa8b595912439b6f57be60f102d02b42621fe Mon Sep 17 00:00:00 2001 From: Anthony Thompson Date: Fri, 31 Jul 2026 05:37:23 -0700 Subject: [PATCH 12/13] Update scripts/submit-cmdpal-gallery.ps1 Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Signed-off-by: Anthony Thompson --- scripts/submit-cmdpal-gallery.ps1 | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/submit-cmdpal-gallery.ps1 b/scripts/submit-cmdpal-gallery.ps1 index 7cdf5da8..73f29c9b 100644 --- a/scripts/submit-cmdpal-gallery.ps1 +++ b/scripts/submit-cmdpal-gallery.ps1 @@ -91,6 +91,10 @@ Describe only what this PR changes (for example logo, screenshots, copy, or inst $bodyFile = Join-Path $env:TEMP 'cmdpal-gallery-pr-body.md' Set-Content -Path $bodyFile -Value $Body -Encoding utf8 $prUrl = gh pr create --repo $upstream --head "${login}:$Branch" --title $Title --body-file $bodyFile + if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($prUrl)) { + Remove-Item $bodyFile -Force -ErrorAction SilentlyContinue + throw 'gh pr create failed; no PR was opened.' + } Remove-Item $bodyFile -Force -ErrorAction SilentlyContinue Write-Host "PR opened: $prUrl" } From 62ff52f4c2d3337ce9fcc006f418cc502caba832 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Fri, 31 Jul 2026 12:41:58 +0000 Subject: [PATCH 13/13] fix: apply CodeRabbit auto-fixes Fixed 4 file(s) based on 3 unresolved review comments. Co-authored-by: CodeRabbit --- docs/install.md | 2 +- docs/release-packages.md | 2 +- scripts/submit-cmdpal-gallery.ps1 | 4 ++-- winget/README.md | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/install.md b/docs/install.md index 4715b9eb..cc9be59d 100644 --- a/docs/install.md +++ b/docs/install.md @@ -43,7 +43,7 @@ winget install tonythethompson.QuickShell # Command Palette only (same scope as Microsoft Store) winget install tonythethompson.QuickShellforCmdPal -# PowerToys Run only (when that package is listed) +# PowerToys Run only (awaits initial package listing) winget install tonythethompson.QuickShellforRun ``` diff --git a/docs/release-packages.md b/docs/release-packages.md index 119d95c9..bc50e09c 100644 --- a/docs/release-packages.md +++ b/docs/release-packages.md @@ -56,7 +56,7 @@ winget install tonythethompson.QuickShellforCmdPal winget install tonythethompson.QuickShellforRun ``` -`tonythethompson.QuickShell` (bundled) and `tonythethompson.QuickShellforCmdPal` are published in [microsoft/winget-pkgs](https://github.com/microsoft/winget-pkgs); release CI submits version bumps for both when `WINGET_PAT` is configured. `tonythethompson.QuickShellforRun` still needs its initial package merge before CI can auto-update that ID (soft-skip until then). +`tonythethompson.QuickShell` (bundled) and `tonythethompson.QuickShellforCmdPal` are published in [microsoft/winget-pkgs](https://github.com/microsoft/winget-pkgs); release CI submits version bumps for both when `WINGET_PAT` is configured. `tonythethompson.QuickShellforRun` awaits its initial package merge before CI can auto-update that ID. The former `tonythethompson.QuickShellforRaycast` WinGet package is retired (no further CI updates). Prefer the Raycast Store. diff --git a/scripts/submit-cmdpal-gallery.ps1 b/scripts/submit-cmdpal-gallery.ps1 index 73f29c9b..d804c69e 100644 --- a/scripts/submit-cmdpal-gallery.ps1 +++ b/scripts/submit-cmdpal-gallery.ps1 @@ -58,7 +58,7 @@ try { $dest = Join-Path $workDir 'extensions\tonythethompson\quickshell' New-Item -ItemType Directory -Force -Path $dest | Out-Null # Replace listing contents so renamed/removed screenshots do not linger. - Get-ChildItem -Force $dest | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue + Get-ChildItem -Force $dest | Remove-Item -Recurse -Force Copy-Item -Recurse -Force (Join-Path $source '*') $dest git add -A extensions/tonythethompson/quickshell git status --short @@ -84,7 +84,7 @@ Describe only what this PR changes (for example logo, screenshots, copy, or inst - [ ] CI schema validation passes - [ ] Store / WinGet install source IDs resolve - [ ] Icon under 100 KB; screenshots under 1 MB each (when media changed) -- [ ] Tags ≤ 5 +- [ ] Tags: at most 5 "@ } diff --git a/winget/README.md b/winget/README.md index e5e18338..1202997b 100644 --- a/winget/README.md +++ b/winget/README.md @@ -19,6 +19,6 @@ Template manifests for the repo copy. Release CI runs `wingetcreate update` agai | --- | --- | | `tonythethompson.QuickShell` | Published; CI submits version bumps | | `tonythethompson.QuickShellforCmdPal` | Published (initial `0.2.3.0`); CI submits version bumps | -| `tonythethompson.QuickShellforRun` | Initial package PR must merge once before CI can `wingetcreate update` | +| `tonythethompson.QuickShellforRun` | Awaits initial package merge before CI can `wingetcreate update` | `wingetcreate update` only works after the package exists in [microsoft/winget-pkgs](https://github.com/microsoft/winget-pkgs). Subsequent releases for published IDs are submitted by `.github/workflows/release-extension.yml`.