diff --git a/apps/desktop/src/lib/trpc/routers/external/index.ts b/apps/desktop/src/lib/trpc/routers/external/index.ts index 7ea6cb9f5d..d5e6d621c8 100644 --- a/apps/desktop/src/lib/trpc/routers/external/index.ts +++ b/apps/desktop/src/lib/trpc/routers/external/index.ts @@ -1,4 +1,9 @@ -import { EXTERNAL_APPS, projects } from "@superset/local-db"; +import { + EXTERNAL_APPS, + NON_EDITOR_APPS, + projects, + settings, +} from "@superset/local-db"; import { TRPCError } from "@trpc/server"; import { eq } from "drizzle-orm"; import { clipboard, shell } from "electron"; @@ -14,6 +19,39 @@ import { const ExternalAppSchema = z.enum(EXTERNAL_APPS); +const nonEditorSet = new Set(NON_EDITOR_APPS); + +/** Sets the global default editor if one hasn't been set yet. Skips non-editor apps. */ +function ensureGlobalDefaultEditor(app: ExternalApp) { + if (nonEditorSet.has(app)) return; + + const row = localDb.select().from(settings).get(); + if (!row?.defaultEditor) { + localDb + .insert(settings) + .values({ id: 1, defaultEditor: app }) + .onConflictDoUpdate({ + target: settings.id, + set: { defaultEditor: app }, + }) + .run(); + } +} + +/** Resolves the default editor from project setting, then global setting. */ +export function resolveDefaultEditor(projectId?: string): ExternalApp | null { + if (projectId) { + const project = localDb + .select() + .from(projects) + .where(eq(projects.id, projectId)) + .get(); + if (project?.defaultApp) return project.defaultApp; + } + const row = localDb.select().from(settings).get(); + return row?.defaultEditor ?? null; +} + async function openPathInApp( filePath: string, app: ExternalApp, @@ -80,6 +118,9 @@ export const createExternalRouter = () => { }), ) .mutation(async ({ input }) => { + await openPathInApp(input.path, input.app); + + // Persist defaults only after successful launch if (input.projectId) { localDb .update(projects) @@ -87,7 +128,16 @@ export const createExternalRouter = () => { .where(eq(projects.id, input.projectId)) .run(); } - await openPathInApp(input.path, input.app); + + // Auto-set global default editor on first successful use (best-effort) + try { + ensureGlobalDefaultEditor(input.app); + } catch (err) { + console.warn( + "[external/openInApp] Failed to persist global default editor:", + err, + ); + } }), copyPath: publicProcedure.input(z.string()).mutation(async ({ input }) => { @@ -106,15 +156,10 @@ export const createExternalRouter = () => { ) .mutation(async ({ input }) => { const filePath = resolvePath(input.path, input.cwd); - let app: ExternalApp = "cursor"; - if (input.projectId) { - const project = localDb - .select() - .from(projects) - .where(eq(projects.id, input.projectId)) - .get(); - app = project?.defaultApp ?? "cursor"; - } + // Preserve first-run behavior for terminal/file-link flows. + // If no project/global default editor is configured yet, fall back to Cursor. + const app = resolveDefaultEditor(input.projectId) ?? "cursor"; + await openPathInApp(filePath, app); }), }); diff --git a/apps/desktop/src/lib/trpc/routers/projects/projects.ts b/apps/desktop/src/lib/trpc/routers/projects/projects.ts index 979229f2fb..41abb7f740 100644 --- a/apps/desktop/src/lib/trpc/routers/projects/projects.ts +++ b/apps/desktop/src/lib/trpc/routers/projects/projects.ts @@ -24,6 +24,7 @@ import { PROJECT_COLOR_VALUES } from "shared/constants/project-colors"; import simpleGit from "simple-git"; import { z } from "zod"; import { publicProcedure, router } from "../.."; +import { resolveDefaultEditor } from "../external"; import { activateProject, getBranchWorkspace, @@ -279,13 +280,7 @@ export const createProjectsRouter = (getWindow: () => BrowserWindow | null) => { 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"; + return resolveDefaultEditor(input.projectId); }), getRecents: publicProcedure.query((): Project[] => { diff --git a/apps/desktop/src/lib/trpc/routers/settings/index.ts b/apps/desktop/src/lib/trpc/routers/settings/index.ts index 8754053c64..ec333891bc 100644 --- a/apps/desktop/src/lib/trpc/routers/settings/index.ts +++ b/apps/desktop/src/lib/trpc/routers/settings/index.ts @@ -1,7 +1,9 @@ import { BRANCH_PREFIX_MODES, EXECUTION_MODES, + EXTERNAL_APPS, FILE_OPEN_MODES, + NON_EDITOR_APPS, settings, TERMINAL_LINK_BEHAVIORS, type TerminalPreset, @@ -661,6 +663,35 @@ export const createSettingsRouter = () => { return { success: true }; }), + getDefaultEditor: publicProcedure.query(() => { + const row = getSettings(); + return row.defaultEditor ?? null; + }), + + setDefaultEditor: publicProcedure + .input( + z.object({ + editor: z + .enum(EXTERNAL_APPS) + .nullable() + .refine((val) => val === null || !NON_EDITOR_APPS.includes(val), { + message: "Non-editor apps cannot be set as the global default", + }), + }), + ) + .mutation(({ input }) => { + localDb + .insert(settings) + .values({ id: 1, defaultEditor: input.editor }) + .onConflictDoUpdate({ + target: settings.id, + set: { defaultEditor: input.editor }, + }) + .run(); + + return { success: true }; + }), + // TODO: remove telemetry procedures once telemetry_enabled column is dropped getTelemetryEnabled: publicProcedure.query(() => { return true; diff --git a/apps/desktop/src/renderer/components/OpenInButton/OpenInButton.tsx b/apps/desktop/src/renderer/components/OpenInButton/OpenInButton.tsx index be12e96dbd..b05de5f1e2 100644 --- a/apps/desktop/src/renderer/components/OpenInButton/OpenInButton.tsx +++ b/apps/desktop/src/renderer/components/OpenInButton/OpenInButton.tsx @@ -100,7 +100,7 @@ const ALL_APP_OPTIONS = [ ]; export const getAppOption = (id: ExternalApp) => - ALL_APP_OPTIONS.find((app) => app.id === id) ?? APP_OPTIONS[1]; + ALL_APP_OPTIONS.find((app) => app.id === id); export interface OpenInButtonProps { path: string | undefined; @@ -126,11 +126,10 @@ export function OpenInButton({ const showCopyPathShortcut = showShortcuts && copyPathShortcut !== "Unassigned"; - const { data: defaultApp = "cursor" } = - electronTrpc.projects.getDefaultApp.useQuery( - { projectId: projectId as string }, - { enabled: !!projectId }, - ); + const { data: defaultApp } = electronTrpc.projects.getDefaultApp.useQuery( + { projectId: projectId as string }, + { enabled: !!projectId }, + ); const openInApp = electronTrpc.external.openInApp.useMutation({ onSuccess: () => { @@ -141,7 +140,7 @@ export function OpenInButton({ }); const copyPath = electronTrpc.external.copyPath.useMutation(); - const currentApp = getAppOption(defaultApp); + const currentApp = defaultApp ? (getAppOption(defaultApp) ?? null) : null; const handleOpenIn = (app: ExternalApp) => { if (!path) return; @@ -156,13 +155,13 @@ export function OpenInButton({ }; const handleOpenLastUsed = () => { - if (!path) return; + if (!path || !defaultApp) return; openInApp.mutate({ path, app: defaultApp, projectId }); }; return ( - {label && ( + {label && currentApp && (