Skip to content
Merged
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
31 changes: 20 additions & 11 deletions apps/desktop/src/lib/trpc/routers/external/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { EXTERNAL_APPS, settings } from "@superset/local-db";
import { EXTERNAL_APPS, projects } from "@superset/local-db";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import { clipboard, shell } from "electron";
import { localDb } from "main/lib/local-db";
import { z } from "zod";
Expand Down Expand Up @@ -75,17 +76,17 @@ export const createExternalRouter = () => {
z.object({
path: z.string(),
app: ExternalAppSchema,
projectId: z.string().optional(),
}),
)
.mutation(async ({ input }) => {
localDb
.insert(settings)
.values({ id: 1, lastUsedApp: input.app })
.onConflictDoUpdate({
target: settings.id,
set: { lastUsedApp: input.app },
})
.run();
if (input.projectId) {
localDb
.update(projects)
.set({ defaultApp: input.app })
.where(eq(projects.id, input.projectId))
.run();
}
await openPathInApp(input.path, input.app);
}),

Expand All @@ -100,12 +101,20 @@ export const createExternalRouter = () => {
line: z.number().optional(),
column: z.number().optional(),
cwd: z.string().optional(),
projectId: z.string().optional(),
}),
)
.mutation(async ({ input }) => {
const filePath = resolvePath(input.path, input.cwd);
const settingsRow = localDb.select().from(settings).get();
const app = settingsRow?.lastUsedApp ?? "cursor";
let app: ExternalApp = "cursor";
if (input.projectId) {
const project = localDb
.select()
.from(projects)
.where(eq(projects.id, input.projectId))
.get();
app = project?.defaultApp ?? "cursor";
}
await openPathInApp(filePath, app);
}),
});
Expand Down
17 changes: 17 additions & 0 deletions apps/desktop/src/lib/trpc/routers/projects/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { access } from "node:fs/promises";
import { basename, join } from "node:path";
import {
BRANCH_PREFIX_MODES,
EXTERNAL_APPS,
projects,
type SelectProject,
settings,
Expand Down Expand Up @@ -313,6 +314,18 @@ export const createProjectsRouter = (getWindow: () => BrowserWindow | null) => {
return project;
}),

getDefaultApp: publicProcedure
.input(z.object({ projectId: z.string() }))
.query(({ input }) => {
const project = localDb
.select()
.from(projects)
.where(eq(projects.id, input.projectId))
.get();

return project?.defaultApp ?? "cursor";
}),

getRecents: publicProcedure.query((): Project[] => {
return localDb
.select()
Expand Down Expand Up @@ -800,6 +813,7 @@ export const createProjectsRouter = (getWindow: () => BrowserWindow | null) => {
branchPrefixMode: z.enum(BRANCH_PREFIX_MODES).nullable().optional(),
branchPrefixCustom: z.string().nullable().optional(),
hideImage: z.boolean().optional(),
defaultApp: z.enum(EXTERNAL_APPS).nullable().optional(),
}),
}),
)
Expand Down Expand Up @@ -829,6 +843,9 @@ export const createProjectsRouter = (getWindow: () => BrowserWindow | null) => {
...(input.patch.hideImage !== undefined && {
hideImage: input.patch.hideImage,
}),
...(input.patch.defaultApp !== undefined && {
defaultApp: input.patch.defaultApp,
}),
lastOpenedAt: Date.now(),
})
.where(eq(projects.id, input.id))
Expand Down
4 changes: 0 additions & 4 deletions apps/desktop/src/lib/trpc/routers/settings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,10 +100,6 @@ export function getPresetsForTrigger(

export const createSettingsRouter = () => {
return router({
getLastUsedApp: publicProcedure.query(() => {
const row = getSettings();
return row.lastUsedApp ?? "cursor";
}),
getTerminalPresets: publicProcedure.query(() => {
const row = getSettings();
if (!row.terminalPresetsInitialized) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,11 @@ export function ConfigFilePreview({
<span className="text-sm text-muted-foreground font-mono truncate">
{projectName}/{PROJECT_SUPERSET_DIR_NAME}/{CONFIG_FILE_NAME}
</span>
<OpenInButton path={configFilePath} label={CONFIG_FILE_NAME} />
<OpenInButton
path={configFilePath}
label={CONFIG_FILE_NAME}
projectId={projectId}
/>
</div>

<div className="p-4 bg-background/50">
Expand Down
28 changes: 19 additions & 9 deletions apps/desktop/src/renderer/components/OpenInButton/OpenInButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,12 +108,15 @@ export interface OpenInButtonProps {
label?: string;
/** Show keyboard shortcut hints */
showShortcuts?: boolean;
/** Project ID for per-project default app */
projectId?: string;
}

export function OpenInButton({
path,
label,
showShortcuts = false,
projectId,
}: OpenInButtonProps) {
const [isOpen, setIsOpen] = useState(false);
const utils = electronTrpc.useUtils();
Expand All @@ -123,19 +126,26 @@ export function OpenInButton({
const showCopyPathShortcut =
showShortcuts && copyPathShortcut !== "Unassigned";

const { data: lastUsedApp = "cursor" } =
electronTrpc.settings.getLastUsedApp.useQuery();
const { data: defaultApp = "cursor" } =
electronTrpc.projects.getDefaultApp.useQuery(
{ projectId: projectId as string },
{ enabled: !!projectId },
);

const openInApp = electronTrpc.external.openInApp.useMutation({
onSuccess: () => utils.settings.getLastUsedApp.invalidate(),
onSuccess: () => {
if (projectId) {
utils.projects.getDefaultApp.invalidate({ projectId });
}
},
});
const copyPath = electronTrpc.external.copyPath.useMutation();

const currentApp = getAppOption(lastUsedApp);
const currentApp = getAppOption(defaultApp);

const handleOpenIn = (app: ExternalApp) => {
if (!path) return;
openInApp.mutate({ path, app });
openInApp.mutate({ path, app, projectId });
setIsOpen(false);
};

Expand All @@ -147,7 +157,7 @@ export function OpenInButton({

const handleOpenLastUsed = () => {
if (!path) return;
openInApp.mutate({ path, app: lastUsedApp });
openInApp.mutate({ path, app: defaultApp, projectId });
};

return (
Expand Down Expand Up @@ -204,7 +214,7 @@ export function OpenInButton({
/>
<span>{app.label}</span>
</div>
{showOpenInShortcut && app.id === lastUsedApp && (
{showOpenInShortcut && app.id === defaultApp && (
<span className="text-xs text-muted-foreground">
{openInShortcut}
</span>
Expand Down Expand Up @@ -235,7 +245,7 @@ export function OpenInButton({
/>
<span>{app.label}</span>
</div>
{showShortcuts && app.id === lastUsedApp && (
{showShortcuts && app.id === defaultApp && (
<span className="text-xs text-muted-foreground">⌘O</span>
)}
Comment on lines +248 to 250

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.

⚠️ Potential issue | 🟡 Minor

Hardcoded ⌘O shortcut instead of using openInShortcut variable.

Line 249 uses ⌘O while lines 219 and 281 correctly use {openInShortcut}. Additionally, the condition uses showShortcuts instead of the derived showOpenInShortcut (which also checks openInShortcut !== "Unassigned"). This means the VS Code sub-menu will display a shortcut even when unassigned.

Proposed fix
-								{showShortcuts && app.id === defaultApp && (
-									<span className="text-xs text-muted-foreground">⌘O</span>
+								{showOpenInShortcut && app.id === defaultApp && (
+									<span className="text-xs text-muted-foreground">
+										{openInShortcut}
+									</span>
 								)}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/components/OpenInButton/OpenInButton.tsx` around
lines 248 - 250, Replace the hardcoded "⌘O" and the incorrect condition: use the
derived boolean showOpenInShortcut (which already checks openInShortcut !==
"Unassigned") instead of showShortcuts, and render the actual openInShortcut
variable rather than the literal "⌘O"; update the JSX around the defaultApp
check (where app.id === defaultApp) to show <span> containing {openInShortcut}
only when showOpenInShortcut is true.

</DropdownMenuItem>
Expand Down Expand Up @@ -266,7 +276,7 @@ export function OpenInButton({
/>
<span>{app.label}</span>
</div>
{showOpenInShortcut && app.id === lastUsedApp && (
{showOpenInShortcut && app.id === defaultApp && (
<span className="text-xs text-muted-foreground">
{openInShortcut}
</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export function TopBar() {
<OpenInMenuButton
worktreePath={workspace.worktreePath}
branch={workspace.worktree?.branch}
projectId={workspace.project?.id}
/>
)}
<OrganizationDropdown />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,27 +30,34 @@ import { useHotkeyText } from "renderer/stores/hotkeys";
interface OpenInMenuButtonProps {
worktreePath: string;
branch?: string;
projectId?: string;
}

export const OpenInMenuButton = memo(function OpenInMenuButton({
worktreePath,
branch,
projectId,
}: OpenInMenuButtonProps) {
const utils = electronTrpc.useUtils();
const { data: lastUsedApp = "cursor" } =
electronTrpc.settings.getLastUsedApp.useQuery(undefined, {
staleTime: 30000,
});
const { data: defaultApp = "cursor" } =
electronTrpc.projects.getDefaultApp.useQuery(
{ projectId: projectId as string },
{ enabled: !!projectId, staleTime: 30000 },
);
const openInApp = electronTrpc.external.openInApp.useMutation({
onSuccess: () => utils.settings.getLastUsedApp.invalidate(),
onSuccess: () => {
if (projectId) {
utils.projects.getDefaultApp.invalidate({ projectId });
}
},
onError: (error) => toast.error(`Failed to open: ${error.message}`),
});
const copyPath = electronTrpc.external.copyPath.useMutation({
onSuccess: () => toast.success("Path copied to clipboard"),
onError: (error) => toast.error(`Failed to copy path: ${error.message}`),
});

const currentApp = useMemo(() => getAppOption(lastUsedApp), [lastUsedApp]);
const currentApp = useMemo(() => getAppOption(defaultApp), [defaultApp]);
const openInShortcut = useHotkeyText("OPEN_IN_APP");
const copyPathShortcut = useHotkeyText("COPY_PATH");
const showOpenInShortcut = openInShortcut !== "Unassigned";
Expand All @@ -59,15 +66,15 @@ export const OpenInMenuButton = memo(function OpenInMenuButton({

const handleOpenInEditor = useCallback(() => {
if (openInApp.isPending || copyPath.isPending) return;
openInApp.mutate({ path: worktreePath, app: lastUsedApp });
}, [worktreePath, lastUsedApp, openInApp, copyPath.isPending]);
openInApp.mutate({ path: worktreePath, app: defaultApp, projectId });
}, [worktreePath, defaultApp, projectId, openInApp, copyPath.isPending]);

const handleOpenInOtherApp = useCallback(
(appId: ExternalApp) => {
if (openInApp.isPending || copyPath.isPending) return;
openInApp.mutate({ path: worktreePath, app: appId });
openInApp.mutate({ path: worktreePath, app: appId, projectId });
},
[worktreePath, openInApp, copyPath.isPending],
[worktreePath, projectId, openInApp, copyPath.isPending],
);

const handleCopyPath = useCallback(() => {
Expand Down Expand Up @@ -159,7 +166,7 @@ export const OpenInMenuButton = memo(function OpenInMenuButton({
className="size-4 object-contain mr-2"
/>
{app.label}
{app.id === lastUsedApp && showOpenInShortcut && (
{app.id === defaultApp && showOpenInShortcut && (
<DropdownMenuShortcut>{openInShortcut}</DropdownMenuShortcut>
)}
</DropdownMenuItem>
Expand All @@ -185,7 +192,7 @@ export const OpenInMenuButton = memo(function OpenInMenuButton({
className="size-4 object-contain mr-2"
/>
{app.label}
{app.id === lastUsedApp && showOpenInShortcut && (
{app.id === defaultApp && showOpenInShortcut && (
<DropdownMenuShortcut>
{openInShortcut}
</DropdownMenuShortcut>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -299,21 +299,33 @@ function WorkspacePage() {
);

// Open in last used app shortcut
const { data: lastUsedApp = "cursor" } =
electronTrpc.settings.getLastUsedApp.useQuery();
const openInApp = electronTrpc.external.openInApp.useMutation();
const projectId = workspace?.projectId;
const { data: defaultApp = "cursor" } =
electronTrpc.projects.getDefaultApp.useQuery(
{ projectId: projectId as string },
{ enabled: !!projectId },
);
const utils = electronTrpc.useUtils();
const openInApp = electronTrpc.external.openInApp.useMutation({
onSuccess: () => {
if (projectId) {
utils.projects.getDefaultApp.invalidate({ projectId });
}
},
});
useAppHotkey(
"OPEN_IN_APP",
() => {
if (workspace?.worktreePath) {
openInApp.mutate({
path: workspace.worktreePath,
app: lastUsedApp,
app: defaultApp,
projectId,
});
}
},
undefined,
[workspace?.worktreePath, lastUsedApp],
[workspace?.worktreePath, defaultApp, projectId],
);

// Copy path shortcut
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,12 @@ interface ClickablePathProps {
className?: string;
}

const defaultApp: ExternalApp = "cursor";

export function ClickablePath({ path, className }: ClickablePathProps) {
const [isOpen, setIsOpen] = useState(false);
const utils = electronTrpc.useUtils();

const { data: lastUsedApp = "cursor" } =
electronTrpc.settings.getLastUsedApp.useQuery(undefined, {
staleTime: 30000,
});

const openInApp = electronTrpc.external.openInApp.useMutation({
onSuccess: () => utils.settings.getLastUsedApp.invalidate(),
onError: (error) => toast.error(`Failed to open: ${error.message}`),
});

Expand Down Expand Up @@ -81,7 +76,7 @@ export function ClickablePath({ path, className }: ClickablePathProps) {
>
<img src={app.icon} alt="" className="size-4 object-contain" />
<span>{app.label}</span>
{app.id === lastUsedApp && (
{app.id === defaultApp && (
<span className="ml-auto text-xs text-muted-foreground">
Default
</span>
Expand All @@ -106,7 +101,7 @@ export function ClickablePath({ path, className }: ClickablePathProps) {
>
<img src={app.icon} alt="" className="size-4 object-contain" />
<span>{app.label}</span>
{app.id === lastUsedApp && (
{app.id === defaultApp && (
<span className="ml-auto text-xs text-muted-foreground">
Default
</span>
Expand All @@ -133,7 +128,7 @@ export function ClickablePath({ path, className }: ClickablePathProps) {
>
<img src={app.icon} alt="" className="size-4 object-contain" />
<span>{app.label}</span>
{app.id === lastUsedApp && (
{app.id === defaultApp && (
<span className="ml-auto text-xs text-muted-foreground">
Default
</span>
Expand Down
Loading
Loading