Skip to content
Closed
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
10 changes: 6 additions & 4 deletions QuickShell.Raycast/src/components/discover-git-repos-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,22 +101,23 @@ export default function DiscoverGitReposView({ onWorkspaceAdded, popOnAdd = true
}

let cancelled = false;
const controller = new AbortController();
const timer = setTimeout(() => {
setTargetedLoadingQuery(query);
void (async () => {
try {
const existing = await storage.getWorkspaces();
const existingDirs = new Set(existing.map((workspace) => workspace.directory.toLowerCase()));
const extraRoots = searchRootsFromWorkspaces(existing.map((workspace) => workspace.directory));
const repos = (await discoverGitReposForQueryAsync(query, extraRoots)).filter(
const repos = (await discoverGitReposForQueryAsync(query, extraRoots, { signal: controller.signal })).filter(
(repo) => !existingDirs.has(repo.directory.toLowerCase()),
);
if (!cancelled) {
setTargetedSearch({ query, repos });
}
} catch {
if (!cancelled) {
setTargetedSearch({ query, repos: [] });
} catch (error) {
if (!cancelled && !controller.signal.aborted) {
throw error;
Comment on lines +118 to +120

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Re-throwing inside an un-awaited async function will likely cause unhandled promise rejections.

Because this async IIFE is invoked with void, its promise is never awaited. Re-throwing in the catch will therefore become an unhandled rejection, bypassing React error boundaries and any upstream error handling. If you want the error to be handled, move the async/await to a caller that can catch it or route it through centralized error reporting instead of throwing here. If you only want to ignore abort-related errors, consider restoring the previous setTargetedSearch({ query, repos: [] }) behavior or logging the error without throwing.

Fix in Cursor

}
} finally {
if (!cancelled) {
Expand All @@ -128,6 +129,7 @@ export default function DiscoverGitReposView({ onWorkspaceAdded, popOnAdd = true

return () => {
cancelled = true;
controller.abort();
clearTimeout(timer);
};
}, [data, searchText]);
Expand Down
6 changes: 4 additions & 2 deletions QuickShell.Raycast/src/components/set-target-branch-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export default function SetTargetBranchForm({ directory, workspaceName, blockDir
const { pop } = useNavigation();
const storage = getQuickShellStorage();

const { data: branchChoices, isLoading } = usePromise(async () => {
const { data: branchChoices, isLoading, error, revalidate } = usePromise(async () => {
const branches = await listLocalBranches(directory);
const worktreeKey = await resolveWorktreeKey(directory);
const target = worktreeKey ? await storage.getBranchTarget(worktreeKey) : null;
Expand Down Expand Up @@ -104,15 +104,17 @@ export default function SetTargetBranchForm({ directory, workspaceName, blockDir
isLoading={isLoading}
actions={
<ActionPanel>
{error ? <Action title="Retry Loading Branches" onAction={revalidate} /> : null}
<Action.SubmitForm
title="Switch Branch"
onSubmit={handleSubmit}
disabled={isLoading || (branchChoices?.branches.length ?? 0) === 0}
disabled={isLoading || !!error || (branchChoices?.branches.length ?? 0) === 0}
/>
</ActionPanel>
}
>
<Form.Description text={`Choose the branch Quick Shell should switch ${workspaceName} to before launch.`} />
{error ? <Form.Description title="Branches unavailable" text="Could not load local branches. Try again." /> : null}
{branchChoices && branchChoices.branches.length === 0 ? (
<Form.Description title="Branches" text="No local branches were found for this repository." />
) : (
Expand Down
4 changes: 3 additions & 1 deletion QuickShell.Raycast/src/lib/discovered-workspace-seed.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { deriveAbbreviationFromName, deriveNameFromDirectory } from "./directory-helpers";
import { tryGetGitRemoteUrl } from "./git-repo-discovery";
import { createStableId } from "./ids";
import type { WorkspaceSetupTask } from "./project-setup-suggestion";
import type { CompanionAppEntry, Workspace } from "./schema";
Expand All @@ -21,6 +22,7 @@ type DiscoveredWorkspaceSeed = {
export function createWorkspaceFromDiscoveredGitRepo(seed: DiscoveredWorkspaceSeed): Workspace {
const directory = seed.directory.trim();
const name = seed.name.trim() || deriveNameFromDirectory(directory);
const repoUrl = seed.remoteUrl ?? tryGetGitRemoteUrl(directory) ?? null;
const launches = launchRowsFromSuggestions(seed.tasks).map((row, index) => ({
id: row.id,
label: row.label,
Expand All @@ -45,7 +47,7 @@ export function createWorkspaceFromDiscoveredGitRepo(seed: DiscoveredWorkspaceSe
wtProfile: null,
command: null,
runAsAdmin: false,
repoUrl: seed.remoteUrl ?? null,
repoUrl,
devServerUrl: seed.devServerUrl ?? null,
// normalizeWorkspace supplies a usable blank launch when discovery finds no tasks.
launches,
Expand Down
18 changes: 14 additions & 4 deletions QuickShell.Raycast/src/lib/git-repo-discovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ type DiscoveryOptions = {
maxScanned?: number;
query?: string;
rootDirectories?: string[];
signal?: AbortSignal;
};

export function discoverGitRepos(extraRoots: string[] = []): GitRepoCandidate[] {
Expand Down Expand Up @@ -88,9 +89,10 @@ export async function discoverGitReposAsync(
const maxRepos = options?.maxRepos ?? MAX_REPOS;
const maxScanned = options?.maxScanned ?? MAX_SCANNED;
const query = normalizeDiscoveryQuery(options?.query ?? "");
const signal = options?.signal;

async function worker(): Promise<void> {
while (results.length < maxRepos && scanned < maxScanned) {
while (!signal?.aborted && results.length < maxRepos && scanned < maxScanned) {
const work = queue.shift();
if (!work) {
return;
Expand All @@ -99,6 +101,7 @@ export async function discoverGitReposAsync(
continue;
}

if (signal?.aborted) return;
let isDirectory = false;
try {
isDirectory = (await fs.stat(work.directory)).isDirectory();
Expand All @@ -120,6 +123,7 @@ export async function discoverGitReposAsync(
return;
}

if (signal?.aborted) return;
let entries: Array<{ name: string; isDirectory: () => boolean }> = [];
try {
entries = await fs.readdir(work.directory, { withFileTypes: true });
Expand All @@ -128,6 +132,7 @@ export async function discoverGitReposAsync(
}

for (const entry of entries) {
if (signal?.aborted) return;
if (!entry.isDirectory() || SKIP_DIRS.has(entry.name.toLowerCase())) {
continue;
}
Expand All @@ -154,7 +159,7 @@ export async function discoverGitReposAsync(
export async function discoverGitReposForQueryAsync(
searchText: string,
extraRoots: string[] = [],
options?: { rootDirectories?: string[]; concurrency?: number; maxScanned?: number },
options?: { rootDirectories?: string[]; concurrency?: number; maxScanned?: number; signal?: AbortSignal },
): Promise<GitRepoCandidate[]> {
const query = normalizeDiscoveryQuery(searchText);
if (!query) {
Expand All @@ -172,6 +177,7 @@ export async function discoverGitReposForQueryAsync(
maxScanned: options?.maxScanned ?? MAX_TARGETED_SCANNED,
query,
rootDirectories: options?.rootDirectories,
signal: options?.signal,
});
}

Expand Down Expand Up @@ -280,8 +286,12 @@ function repoCandidateFromPathQuery(value: string): GitRepoCandidate | null {
if (!statSync(candidate).isDirectory()) {
candidate = path.dirname(candidate);
}
} catch {
return null;
} catch (error) {
const code = (error as NodeJS.ErrnoException).code;
if (code === "ENOENT" || code === "ENOTDIR") {
return null;
}
throw error;
}

while (true) {
Expand Down
Loading