Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
tonythethompson marked this conversation as resolved.
debug-a49e01.log
.vscode/settings.json
/.workflow
Expand Down
13 changes: 13 additions & 0 deletions QuickShell.Core.Tests/SuggestCommandLineArgsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
5 changes: 3 additions & 2 deletions QuickShell.Core/Services/SuggestCommandLineArgs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ public static bool TryParse(
string[] args,
out string? directory,
out IReadOnlyList<string> usedCommands,
out int generation)
out long generation)
{
directory = null;
usedCommands = [];
Expand Down Expand Up @@ -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);
}
}

Expand Down
4 changes: 3 additions & 1 deletion QuickShell.Raycast/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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, `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.

## Store checklist
Expand Down
6 changes: 3 additions & 3 deletions QuickShell.Raycast/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -154,16 +154,16 @@
"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",
"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
Expand Down
49 changes: 49 additions & 0 deletions QuickShell.Raycast/scripts/ensure-suggest-asset.js
Original file line number Diff line number Diff line change
@@ -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}`);
}
79 changes: 79 additions & 0 deletions QuickShell.Raycast/src/__tests__/suggest-commands.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import path from "node:path";
import { describe, expect, it } from "vitest";
import {
buildSuggestCommandArgs,
combineSuggestionTasksAndPills,
LOCAL_SETUP_SEED_TASKS,
parseSuggestionResponse,
pillsToSetupTasks,
resolveSuggestExecutable,
splitPillsIntoSeedAndLeftover,
type SuggestionPill,
} from "../lib/suggest-commands";
Expand All @@ -16,6 +21,12 @@ function pill(partial: Partial<SuggestionPill> & Pick<SuggestionPill, "command"
}

describe("suggest-commands", () => {
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",
Expand Down Expand Up @@ -68,6 +79,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" }),
Expand All @@ -89,6 +113,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" }),
Expand All @@ -100,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();
});
});
4 changes: 2 additions & 2 deletions QuickShell.Raycast/src/components/discover-git-repos-view.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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<Workspace> {
const resolved = await resolveWorkspaceSetupSuggestions(directory);
const resolved = await resolveWorkspaceSetupSuggestions(directory, [], Date.now(), environment.assetsPath);
return createWorkspaceFromDiscoveredGitRepo({
directory,
name,
Expand Down
33 changes: 27 additions & 6 deletions QuickShell.Raycast/src/components/workspace-form.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
Action,
ActionPanel,
environment,
Form,
Icon,
launchCommand,
Expand Down Expand Up @@ -32,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";
Expand Down Expand Up @@ -172,7 +177,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,
generation,
environment.assetsPath,
);
if (cancelled || generation !== suggestionGenerationRef.current) {
return;
}
Expand Down Expand Up @@ -285,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,
generation,
environment.assetsPath,
);
if (generation !== suggestionGenerationRef.current) {
return;
}
setSuggestionSource(resolved.source);
setSuggestionPills(combineSuggestionTasksAndPills(resolved.tasks, resolved.pills));
return;
}

Expand All @@ -307,7 +328,7 @@ export default function WorkspaceForm({
tasks: [] as Array<{ label: string; command: string }>,
pills: [] as SuggestionPill[],
}
: await resolveWorkspaceSetupSuggestions(nextDirectory, usedCommands);
: await resolveWorkspaceSetupSuggestions(nextDirectory, usedCommands, generation, environment.assetsPath);
if (generation !== suggestionGenerationRef.current) {
return;
}
Expand Down
Loading
Loading