diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 26f24ed6..b61f9125 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -72,9 +72,6 @@ jobs: - name: Install dependencies run: npm ci - - name: Verify Raycast CLI - run: node scripts/verify-raycast-cli.js - - name: Test run: npm test @@ -104,9 +101,6 @@ jobs: - name: Install dependencies run: npm ci - - name: Verify Raycast CLI - run: node scripts/verify-raycast-cli.js - - name: Test run: npm test diff --git a/QuickShell.Raycast/.gitignore b/QuickShell.Raycast/.gitignore index 95fb3d75..141ac6aa 100644 --- a/QuickShell.Raycast/.gitignore +++ b/QuickShell.Raycast/.gitignore @@ -1,8 +1,6 @@ node_modules/ -.raycast-swift-build/ dist/ bin/ *.log -setup-release.iss -setup-x64.iss -setup-arm64.iss + +assets/QuickShell.Suggest.exe diff --git a/QuickShell.Raycast/CHANGELOG.md b/QuickShell.Raycast/CHANGELOG.md index ef6d997a..99219a5b 100644 --- a/QuickShell.Raycast/CHANGELOG.md +++ b/QuickShell.Raycast/CHANGELOG.md @@ -1,6 +1,14 @@ -# Quick Shell Changelog +# Quick Shell Workspaces Changelog -## [macOS Tier A] - {PR_MERGE_DATE} +## [Command suggestions] - 2026-08-27 + +- Rename the Store listing to Quick Shell Workspaces and the command to Open Workspace (package id remains `quickshell`) +- Package Windows `QuickShell.Suggest.exe` into Raycast assets for richer command suggestions +- Manual Add Workspace shows suggestions in the dropdown without auto-applying them +- Keep leftover suggestions selectable; fall back to local folder heuristics on macOS or when Suggest is unavailable +- Improve discovered-repo workspace seeding and suggestion reliability + +## [macOS Tier A] - 2026-07-22 - Add macOS to Raycast platforms: Terminal.app / iTerm2 launch, Mac companions, discover roots, import/export dialogs - Multi-launch tabs on Mac (Terminal.app / iTerm2) when the preference is enabled; Windows Terminal tab grouping unchanged on Windows diff --git a/QuickShell.Raycast/README.md b/QuickShell.Raycast/README.md index bdfaca76..22c9d7b1 100644 --- a/QuickShell.Raycast/README.md +++ b/QuickShell.Raycast/README.md @@ -1,16 +1,26 @@ -# Quick Shell Raycast +# Quick Shell Workspaces Raycast-native workspace launcher for Quick Shell on **Windows** and **macOS**. +## Why Quick Shell? + +Quick Shell provides a unique feature set tailored for cross-platform developers and complex project setups: + +- **Cross-Platform Support**: Quick Shell offers first-class support for both Windows and macOS, including Windows Terminal and WSL natively. +- **Advanced Git Integration**: Quick Shell goes beyond standard directory opening by offering git repository discovery and "Git Branch Gates" (blocking launches if a target branch has uncommitted changes). +- **Companion Apps**: Launch secondary servers, scripts, or apps right alongside your primary workspace terminals. +- **Workspace Trust & Portability**: Export and import workspaces as JSON with robust validation and security metadata, making it easy to share setups across machines. +- **Comprehensive Launch Management**: Manage entire multi-app project workspaces with multi-command tabs, diagnostics, and launch gates rather than just local terminal profiles or single directory paths. + ## Commands -- **Quick Shell** — search, launch, create, discover git repos, edit, favorite, duplicate, import/export, undo/redo, preferences +- **Open Workspace** — search, launch, create, discover git repos, edit, favorite, duplicate, import/export, undo/redo, preferences -**Extension preferences** (Raycast → Extensions → Quick Shell): default terminal app, default profile, show recents, multi-command tabs, block dirty branch switch. +**Extension preferences** (Raycast → Extensions → Quick Shell Workspaces): default terminal app, default profile, show recents, multi-command tabs, block dirty branch switch. -Root search: the command is titled **Quick Shell**; its subtitle shows your 3 most recent workspaces. Register it as a fallback command so root-search text seeds the list via `fallbackText`. Open the command and use Actions (Ctrl+K) for Recent / Import / Export. Use **Add to Root Search** on a workspace to create a Quicklink. +Root search: the command is titled **Open Workspace**; its subtitle shows your 3 most recent workspaces. Register it as a fallback command so root-search text seeds the list via `fallbackText`. Open the command and use Actions (Ctrl+K) for Recent / Import / Export. Use **Add to Root Search** on a workspace to create a Quicklink. -Create, Discover, and Edit are Actions / pushed views inside Quick Shell (not separate root commands). +Create, Discover, and Edit are Actions / pushed views inside Open Workspace (not separate root commands). ## Requirements @@ -62,9 +72,9 @@ 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` (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. +The Windows suggestion CLI (`QuickShell.Suggest.exe`) is not bundled in this Store build; `fetchSuggestionPills` falls back to local folder heuristics when `assets/QuickShell.Suggest.exe` is absent, which it always is here. macOS also uses local 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. +**Distribution:** publish via the [Raycast Store](https://www.raycast.com/store) only. ## Store checklist diff --git a/QuickShell.Raycast/assets/command-create.png b/QuickShell.Raycast/assets/command-create.png deleted file mode 100644 index 740b0697..00000000 Binary files a/QuickShell.Raycast/assets/command-create.png and /dev/null differ diff --git a/QuickShell.Raycast/assets/command-edit.png b/QuickShell.Raycast/assets/command-edit.png deleted file mode 100644 index 740b0697..00000000 Binary files a/QuickShell.Raycast/assets/command-edit.png and /dev/null differ diff --git a/QuickShell.Raycast/assets/command-settings.png b/QuickShell.Raycast/assets/command-settings.png deleted file mode 100644 index 740b0697..00000000 Binary files a/QuickShell.Raycast/assets/command-settings.png and /dev/null differ diff --git a/QuickShell.Raycast/package.json b/QuickShell.Raycast/package.json index d714511f..d6e56a6c 100644 --- a/QuickShell.Raycast/package.json +++ b/QuickShell.Raycast/package.json @@ -1,7 +1,7 @@ { "$schema": "https://www.raycast.com/schemas/extension.json", "name": "quickshell", - "title": "Quick Shell", + "title": "Quick Shell Workspaces", "description": "Open saved project folders in Terminal, Windows Terminal, and other shells", "icon": "extension-icon.png", "author": "tonythethompson", @@ -99,12 +99,12 @@ } ], "engines": { - "node": ">=20" + "node": ">=22.22.2" }, "commands": [ { "name": "open-workspace", - "title": "Quick Shell", + "title": "Open Workspace", "description": "Search, open, create, and manage saved workspaces (qs)", "icon": "command-open.png", "keywords": [ @@ -154,16 +154,12 @@ "vitest": "^3.0.8" }, "scripts": { - "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", "build": "ray build", "dev": "ray develop", "lint": "ray lint", "test": "vitest run", "test:watch": "vitest", - "publish": "node scripts/ensure-suggest-asset.js && npx @raycast/api@latest publish" + "publish": "npx @raycast/api@latest publish" }, "allowScripts": { "esbuild@0.28.1": true diff --git a/QuickShell.Raycast/scripts/ensure-suggest-asset.js b/QuickShell.Raycast/scripts/ensure-suggest-asset.js deleted file mode 100644 index 8cbd3a70..00000000 --- a/QuickShell.Raycast/scripts/ensure-suggest-asset.js +++ /dev/null @@ -1,49 +0,0 @@ -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/scripts/generate-icons.sh b/QuickShell.Raycast/scripts/generate-icons.sh deleted file mode 100755 index ddbf62ca..00000000 --- a/QuickShell.Raycast/scripts/generate-icons.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ROOT="$(cd "$(dirname "$0")/.." && pwd)" -REPO="$(cd "$ROOT/.." && pwd)" -SCRIPT="$REPO/QuickShell/Assets/raycast/export-raycast-icons.ps1" - -if [[ ! -f "$SCRIPT" ]]; then - echo "Missing export script: $SCRIPT" >&2 - exit 1 -fi - -if command -v pwsh >/dev/null 2>&1; then - pwsh -NoProfile -ExecutionPolicy Bypass -File "$SCRIPT" -elif command -v powershell >/dev/null 2>&1; then - powershell -NoProfile -ExecutionPolicy Bypass -File "$SCRIPT" -else - echo "PowerShell is required to regenerate Raycast icons." >&2 - exit 1 -fi - -echo "Generated Raycast icons in $ROOT/assets" diff --git a/QuickShell.Raycast/scripts/sync-workspace-trust-features.js b/QuickShell.Raycast/scripts/sync-workspace-trust-features.js deleted file mode 100644 index a7b51209..00000000 --- a/QuickShell.Raycast/scripts/sync-workspace-trust-features.js +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Copies shared/workspace-trust-features.json into the Raycast package so Core - * and Raycast share one kill-switch source. Fail if the files diverge after copy. - */ -const fs = require("node:fs"); -const path = require("node:path"); - -const repoRoot = path.resolve(__dirname, "..", ".."); -const sharedPath = path.join(repoRoot, "shared", "workspace-trust-features.json"); -const raycastPath = path.join(__dirname, "..", "src", "lib", "workspace-trust-features.json"); - -if (!fs.existsSync(sharedPath)) { - console.error(`Missing shared trust features file: ${sharedPath}`); - process.exit(1); -} - -function normalizeJsonText(text) { - const lf = text.replace(/\r\n/g, "\n").trimEnd(); - return `${lf}\n`; -} - -fs.mkdirSync(path.dirname(raycastPath), { recursive: true }); -const shared = normalizeJsonText(fs.readFileSync(sharedPath, "utf8")); -fs.writeFileSync(raycastPath, shared, "utf8"); - -const copied = normalizeJsonText(fs.readFileSync(raycastPath, "utf8")); -if (shared !== copied) { - console.error("workspace-trust-features.json copy failed to match shared source."); - process.exit(1); -} diff --git a/QuickShell.Raycast/scripts/verify-raycast-cli.js b/QuickShell.Raycast/scripts/verify-raycast-cli.js deleted file mode 100644 index afac6292..00000000 --- a/QuickShell.Raycast/scripts/verify-raycast-cli.js +++ /dev/null @@ -1,58 +0,0 @@ -const { spawnSync } = require("node:child_process"); -const fs = require("node:fs"); -const path = require("node:path"); - -const root = path.resolve(__dirname, ".."); - -function getRayCliEntrypoint() { - const runJs = path.join(root, "node_modules", "@raycast", "api", "bin", "run.js"); - if (fs.existsSync(runJs)) { - return runJs; - } - - return null; -} - -function runRayVersionCheck() { - const runJs = getRayCliEntrypoint(); - if (!runJs) { - return { status: 1, stderr: "Missing @raycast/api Raycast CLI entrypoint." }; - } - - // Node 24 on Windows rejects spawnSync on .cmd shims (EINVAL). Invoke run.js directly. - return spawnSync(process.execPath, [runJs, "--version"], { - cwd: root, - encoding: "utf8", - }); -} - -const nodeVersion = process.versions.node; -const [major, minor, patch] = nodeVersion.split(".").map(Number); -const meetsNodeRequirement = - major > 22 || (major === 22 && (minor > 14 || (minor === 14 && patch >= 0))); - -if (!meetsNodeRequirement) { - console.error(`QuickShell Raycast requires Node.js >= 22.14.0 (current: ${nodeVersion}).`); - console.error("Install Node 22.14+ from https://nodejs.org/ and rerun npm install."); - process.exit(1); -} - -const result = runRayVersionCheck(); - -if (result.status !== 0) { - console.error("Raycast CLI is unavailable or incomplete."); - if (result.stderr?.trim()) { - console.error(result.stderr.trim()); - } - if (result.stdout?.trim()) { - console.error(result.stdout.trim()); - } - console.error(""); - console.error("Repair steps (PowerShell):"); - console.error(" cd QuickShell.Raycast"); - console.error(" Remove-Item -Recurse -Force node_modules"); - console.error(" Remove-Item -Force package-lock.json"); - console.error(" npm install"); - console.error(" npm run dev"); - process.exit(1); -} diff --git a/QuickShell.Raycast/setup-template.iss b/QuickShell.Raycast/setup-template.iss deleted file mode 100644 index 9a8fe33b..00000000 --- a/QuickShell.Raycast/setup-template.iss +++ /dev/null @@ -1,39 +0,0 @@ -; Inno Setup script for Quick Shell for Raycast (sideload / GitHub / WinGet) - -#define AppVersion "0.0.0.0" -#define DisplayName "Quick Shell for Raycast" -#define InstallerBaseName "QuickShellforRaycast" -#define DeveloperName "Tony Thompson" -#define ExtensionSource "__MUST_BE_SET_BY_BUILD_SCRIPT__" -#define ExtensionDest "{userappdata}\raycast-x\extensions\quickshell" - -[Setup] -AppId={{A4E8B2D1-7C3F-4A9E-B5D1-2F8E6C0A1B3D}} -AppName={#DisplayName} -AppVersion={#AppVersion} -AppPublisher={#DeveloperName} -AppPublisherURL=https://github.com/tonythethompson/QuickShell -DefaultDirName={userappdata}\QuickShell\Raycast -DisableDirPage=yes -DisableProgramGroupPage=yes -OutputDir=bin\Release\installer -OutputBaseFilename={#InstallerBaseName}-Setup-{#AppVersion}-x64 -Compression=lzma -SolidCompression=yes -MinVersion=10.0.19041 -PrivilegesRequired=lowest -WizardStyle=modern -ArchitecturesAllowed=x64compatible -ArchitecturesInstallIn64BitMode=x64compatible - -[Languages] -Name: "english"; MessagesFile: "compiler:Default.isl" - -[Files] -Source: "{#ExtensionSource}\*"; DestDir: "{#ExtensionDest}"; Flags: ignoreversion recursesubdirs createallsubdirs - -[Messages] -FinishedLabel=Quick Shell for Raycast was copied to %n%n{#ExtensionDest}%n%nOpen Raycast, then use Developer → Import Extension if the commands do not appear automatically. You need Raycast for Windows installed. - -[UninstallDelete] -Type: filesandordirs; Name: "{#ExtensionDest}" diff --git a/QuickShell.Raycast/src/__tests__/companion-form.test.ts b/QuickShell.Raycast/src/__tests__/companion-form.test.ts index 23448bfc..716f6333 100644 --- a/QuickShell.Raycast/src/__tests__/companion-form.test.ts +++ b/QuickShell.Raycast/src/__tests__/companion-form.test.ts @@ -4,7 +4,7 @@ import { COMPANION_CHOICE_TITLE_NONE, COMPANION_PRESET_CUSTOM, COMPANION_PRESET_NONE, - COMPANION_PRESETS, + getCompanionPresets, inferCompanionPresetFromPath, listCompanionFormChoices, normalizeCompanionPresetForForm, @@ -31,10 +31,9 @@ afterEach(() => { describe("companion-catalog", () => { it("lists presets with stable ids and default arguments", () => { - expect(COMPANION_PRESETS.length).toBeGreaterThan(5); - expect(COMPANION_PRESETS.every((preset) => preset.id && preset.title && preset.candidatePaths.length > 0)).toBe( - true, - ); + const presets = getCompanionPresets(); + expect(presets.length).toBeGreaterThan(5); + expect(presets.every((preset) => preset.id && preset.title && preset.candidatePaths.length > 0)).toBe(true); expect(resolveCompanionPreset("missing-preset")).toBeNull(); }); diff --git a/QuickShell.Raycast/src/__tests__/security.test.ts b/QuickShell.Raycast/src/__tests__/security.test.ts index 3b13b094..8ea8c52d 100644 --- a/QuickShell.Raycast/src/__tests__/security.test.ts +++ b/QuickShell.Raycast/src/__tests__/security.test.ts @@ -1,4 +1,4 @@ -import { mkdtempSync, readFileSync, rmSync } from "node:fs"; +import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; @@ -441,14 +441,18 @@ describe("workspace trust kill switch (disabled)", () => { }); it("matches the shared JSON default", () => { - const shared = JSON.parse( - readFileSync(resolve(__dirname, "../../../shared/workspace-trust-features.json"), "utf8"), - ) as { enabled: boolean }; const local = JSON.parse(readFileSync(resolve(__dirname, "../lib/workspace-trust-features.json"), "utf8")) as { enabled: boolean; }; - expect(local.enabled).toBe(shared.enabled); - expect(WORKSPACE_TRUST_DEFAULT_ENABLED).toBe(shared.enabled); + const sharedPath = resolve(__dirname, "../../../shared/workspace-trust-features.json"); + if (existsSync(sharedPath)) { + const shared = JSON.parse(readFileSync(sharedPath, "utf8")) as { enabled: boolean }; + expect(local.enabled).toBe(shared.enabled); + expect(WORKSPACE_TRUST_DEFAULT_ENABLED).toBe(shared.enabled); + return; + } + // Store PR layout has no QuickShell shared/ tree; assert the committed local default. + expect(WORKSPACE_TRUST_DEFAULT_ENABLED).toBe(local.enabled); }); it("coerceTrustedWhileDisabled rewrites untrusted rows", () => { diff --git a/QuickShell.Raycast/src/__tests__/storage.test.ts b/QuickShell.Raycast/src/__tests__/storage.test.ts index f178d2fd..0a751979 100644 --- a/QuickShell.Raycast/src/__tests__/storage.test.ts +++ b/QuickShell.Raycast/src/__tests__/storage.test.ts @@ -341,6 +341,54 @@ describe("storage", () => { expect(stored).toBe(raw); }); + it("does not overwrite malformed JSON with an empty cache", async () => { + const raw = "{not-json"; + let stored = raw; + const adapter = { + getItem: async () => stored, + setItem: async (_key: string, value: string) => { + stored = value; + }, + }; + const storage = new QuickShellStorage(adapter); + const workspace = createWorkspace(createStableId(), "Keep"); + + await expect(storage.load()).rejects.toThrow(/not valid JSON/i); + expect(stored).toBe(raw); + + await expect(storage.upsertWorkspace(workspace)).rejects.toThrow(/not valid JSON/i); + expect(stored).toBe(raw); + }); + + it("does not overwrite non-object JSON with an empty cache", async () => { + const raw = "null"; + let stored = raw; + const adapter = { + getItem: async () => stored, + setItem: async (_key: string, value: string) => { + stored = value; + }, + }; + const storage = new QuickShellStorage(adapter); + + await expect(storage.load()).rejects.toThrow(/malformed/i); + expect(stored).toBe(raw); + }); + + it("does not treat an empty stored string as missing", async () => { + let setItemCalled = false; + const adapter = { + getItem: async () => "" as string | undefined, + setItem: async (_key: string, _value: string) => { + setItemCalled = true; + }, + }; + const storage = new QuickShellStorage(adapter); + + await expect(storage.load()).rejects.toThrow(/not valid JSON/i); + expect(setItemCalled).toBe(false); + }); + it("preserves lastUsedUtc when editing a workspace after markUsed", async () => { const storage = new QuickShellStorage(createMemoryStorageAdapter()); const workspace = createWorkspace(createStableId(), "Recents"); diff --git a/QuickShell.Raycast/src/__tests__/terminal-catalog.test.ts b/QuickShell.Raycast/src/__tests__/terminal-catalog.test.ts index 514557ba..f296f1ef 100644 --- a/QuickShell.Raycast/src/__tests__/terminal-catalog.test.ts +++ b/QuickShell.Raycast/src/__tests__/terminal-catalog.test.ts @@ -4,12 +4,11 @@ import { discoverWorkspaceTerminalChoices, invalidateTerminalCatalogCache, parseJsoncForTests, - resetTerminalCatalogCacheForTests, } from "../lib/terminal-catalog"; describe("terminal-catalog", () => { it("returns workspace terminal choices on non-windows platforms", () => { - resetTerminalCatalogCacheForTests(); + invalidateTerminalCatalogCache(); const choices = discoverWorkspaceTerminalChoices(); expect(choices.some((choice) => choice.id === "default")).toBe(true); expect(choices.some((choice) => choice.id === "same-as-previous")).toBe(true); diff --git a/QuickShell.Raycast/src/components/discover-git-repos-view.tsx b/QuickShell.Raycast/src/components/discover-git-repos-view.tsx index 6e44e992..b6fec846 100644 --- a/QuickShell.Raycast/src/components/discover-git-repos-view.tsx +++ b/QuickShell.Raycast/src/components/discover-git-repos-view.tsx @@ -262,10 +262,14 @@ export default function DiscoverGitReposView({ onWorkspaceAdded, popOnAdd = true actions={ handleQuickAdd(repo.directory, repo.name, repo.remoteUrl)} - disabled={pendingQuickAddKeys.has(repo.directory.toLowerCase())} + onAction={() => { + if (pendingQuickAddKeys.has(repo.directory.toLowerCase())) { + return; + } + void handleQuickAdd(repo.directory, repo.name, repo.remoteUrl); + }} /> { - const branches = await listLocalBranches(directory); - const worktreeKey = await resolveWorktreeKey(directory); - const target = worktreeKey ? await storage.getBranchTarget(worktreeKey) : null; - return { branches, target }; - }, [directory]); + } = usePromise( + async (dir: string) => { + const branches = await listLocalBranches(dir); + const worktreeKey = await resolveWorktreeKey(dir); + const target = worktreeKey ? await storage.getBranchTarget(worktreeKey) : null; + return { branches, target }; + }, + [directory], + ); const { handleSubmit, itemProps, setValue, values } = useForm<{ branch: string }>({ async onSubmit(values) { @@ -109,8 +112,8 @@ export default function SetTargetBranchForm({ directory, workspaceName, blockDir isLoading={isLoading} actions={ - - {error ? : null} + + {error ? void revalidate()} /> : null} } > diff --git a/QuickShell.Raycast/src/components/unsupported-platform-view.tsx b/QuickShell.Raycast/src/components/unsupported-platform-view.tsx index 0a720210..7f95d102 100644 --- a/QuickShell.Raycast/src/components/unsupported-platform-view.tsx +++ b/QuickShell.Raycast/src/components/unsupported-platform-view.tsx @@ -5,7 +5,7 @@ export default function UnsupportedPlatformView() { diff --git a/QuickShell.Raycast/src/lib/companion-catalog.ts b/QuickShell.Raycast/src/lib/companion-catalog.ts index 46df4789..b36380e8 100644 --- a/QuickShell.Raycast/src/lib/companion-catalog.ts +++ b/QuickShell.Raycast/src/lib/companion-catalog.ts @@ -199,9 +199,6 @@ export function getCompanionPresets(): CompanionPreset[] { return isMacPlatform() ? MAC_COMPANION_PRESETS : WINDOWS_COMPANION_PRESETS; } -/** @deprecated Prefer getCompanionPresets(); Windows catalog for tests that do not stub platform. */ -export const COMPANION_PRESETS: CompanionPreset[] = WINDOWS_COMPANION_PRESETS; - let cachedCompanionChoices: CompanionFormChoice[] | null = null; export function resolveCompanionPreset(presetId: string): { path: string; arguments: string } | null { diff --git a/QuickShell.Raycast/src/lib/git-repo-search-roots.ts b/QuickShell.Raycast/src/lib/git-repo-search-roots.ts index 84048567..5cffb9f6 100644 --- a/QuickShell.Raycast/src/lib/git-repo-search-roots.ts +++ b/QuickShell.Raycast/src/lib/git-repo-search-roots.ts @@ -48,7 +48,7 @@ export type BuildSearchRootsOptions = { pathStyle?: "win32" | "posix"; }; -function pathApi(options?: BuildSearchRootsOptions): path.PlatformPath { +function pathApi(options?: BuildSearchRootsOptions): typeof path.posix { return useWin32Style(options) ? path.win32 : path.posix; } diff --git a/QuickShell.Raycast/src/lib/storage.ts b/QuickShell.Raycast/src/lib/storage.ts index ef999b3a..ac1e1e98 100644 --- a/QuickShell.Raycast/src/lib/storage.ts +++ b/QuickShell.Raycast/src/lib/storage.ts @@ -747,22 +747,25 @@ export class QuickShellStorage { } const raw = await this.adapter.getItem(STORAGE_KEY); - if (!raw) { + if (raw === undefined) { this.cache = createEmptyStoredData(); return; } + let parsed: unknown; try { - const parsed = JSON.parse(raw) as unknown; - this.cache = migrateStoredData(parsed); - } catch (error) { - // Newer schemas must not collapse into an empty cache that later overwrites disk. - if (error instanceof Error && error.message.startsWith("Unsupported Quick Shell data version:")) { - throw error; - } - this.cache = createEmptyStoredData(); + parsed = JSON.parse(raw) as unknown; + } catch { + throw new Error("Stored workspace data is not valid JSON."); } + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { + throw new Error("Stored workspace data is malformed."); + } + + // Leave cache unset on parse/migration failure so a later save cannot overwrite disk. + this.cache = migrateStoredData(parsed); + const coerced = coerceTrustedWhileDisabled( this.cache.workspaceSecurity, this.cache.workspaces.map((workspace) => workspace.id), diff --git a/QuickShell.Raycast/src/lib/suggest-commands.ts b/QuickShell.Raycast/src/lib/suggest-commands.ts index 43a473de..c7afd11d 100644 --- a/QuickShell.Raycast/src/lib/suggest-commands.ts +++ b/QuickShell.Raycast/src/lib/suggest-commands.ts @@ -252,10 +252,11 @@ export async function fetchSuggestionPills( const executable = resolveSuggestExecutable(assetsPath); if (!executable || !existsSync(executable)) { - if (isWindowsPlatform()) { + if (fromEnv) { + console.warn(`[quickshell] Suggest CLI override not found: ${fromEnv}`); + } else if (isWindowsPlatform()) { console.warn( - "[quickshell] Suggest CLI not found (QuickShell.Suggest.exe). " + - "Run `npm run build` or `npm run publish` so assets/QuickShell.Suggest.exe is published.", + "[quickshell] Suggest CLI not found (assets/QuickShell.Suggest.exe missing from the extension package).", ); } return null; diff --git a/QuickShell.Raycast/src/lib/terminal-catalog.ts b/QuickShell.Raycast/src/lib/terminal-catalog.ts index 03b2b8c6..a0d215b4 100644 --- a/QuickShell.Raycast/src/lib/terminal-catalog.ts +++ b/QuickShell.Raycast/src/lib/terminal-catalog.ts @@ -191,11 +191,6 @@ export function invalidateTerminalCatalogCache(): void { executableExistsCache.clear(); } -/** @deprecated Prefer invalidateTerminalCatalogCache in production code. */ -export function resetTerminalCatalogCacheForTests(): void { - invalidateTerminalCatalogCache(); -} - export function discoverDefaultProfileChoices(terminalApplication: string): TerminalChoice[] { if (terminalApplication === "wt" || terminalApplication === "it" || terminalApplication === "system") { const profiles = readWindowsTerminalProfiles(); diff --git a/QuickShell.Raycast/src/lib/terminal-options.ts b/QuickShell.Raycast/src/lib/terminal-options.ts index 648ef260..6d893ee5 100644 --- a/QuickShell.Raycast/src/lib/terminal-options.ts +++ b/QuickShell.Raycast/src/lib/terminal-options.ts @@ -60,9 +60,6 @@ export function getWorkspaceTerminalChoices(): TerminalChoice[] { return isMacPlatform() ? WORKSPACE_TERMINAL_CHOICES_MAC : WORKSPACE_TERMINAL_CHOICES_WINDOWS; } -/** @deprecated Prefer getWorkspaceTerminalChoices(); kept for Windows-oriented callers/tests. */ -export const WORKSPACE_TERMINAL_CHOICES: TerminalChoice[] = WORKSPACE_TERMINAL_CHOICES_WINDOWS; - export const TERMINAL_APPLICATION_CHOICES_WINDOWS: TerminalChoice[] = [ { id: "system", title: "Let Windows choose" }, { id: "wt", title: "Windows Terminal" }, @@ -80,9 +77,6 @@ export function getTerminalApplicationChoices(): TerminalChoice[] { return isMacPlatform() ? TERMINAL_APPLICATION_CHOICES_MAC : TERMINAL_APPLICATION_CHOICES_WINDOWS; } -/** @deprecated Prefer getTerminalApplicationChoices(). */ -export const TERMINAL_APPLICATION_CHOICES: TerminalChoice[] = TERMINAL_APPLICATION_CHOICES_WINDOWS; - const CONHOST_PROFILE_CHOICES: TerminalChoice[] = [ { id: "__default__", title: "Default profile for this app" }, { id: "powershell", title: "PowerShell" },