diff --git a/apps/desktop/src/lib/trpc/routers/settings/index.ts b/apps/desktop/src/lib/trpc/routers/settings/index.ts index 9fae5170f8f..fc4b04c2940 100644 --- a/apps/desktop/src/lib/trpc/routers/settings/index.ts +++ b/apps/desktop/src/lib/trpc/routers/settings/index.ts @@ -9,6 +9,7 @@ import { app } from "electron"; import { quitWithoutConfirmation } from "main/index"; import { localDb } from "main/lib/local-db"; import { + DEFAULT_AUTO_APPLY_DEFAULT_PRESET, DEFAULT_CONFIRM_ON_QUIT, DEFAULT_TERMINAL_LINK_BEHAVIOR, DEFAULT_TERMINAL_PERSISTENCE, @@ -314,6 +315,26 @@ export const createSettingsRouter = () => { return { success: true }; }), + getAutoApplyDefaultPreset: publicProcedure.query(() => { + const row = getSettings(); + return row.autoApplyDefaultPreset ?? DEFAULT_AUTO_APPLY_DEFAULT_PRESET; + }), + + setAutoApplyDefaultPreset: publicProcedure + .input(z.object({ enabled: z.boolean() })) + .mutation(({ input }) => { + localDb + .insert(settings) + .values({ id: 1, autoApplyDefaultPreset: input.enabled }) + .onConflictDoUpdate({ + target: settings.id, + set: { autoApplyDefaultPreset: input.enabled }, + }) + .run(); + + return { success: true }; + }), + restartApp: publicProcedure.mutation(() => { app.relaunch(); quitWithoutConfirmation(); diff --git a/apps/desktop/src/lib/trpc/routers/workspaces/procedures/init.ts b/apps/desktop/src/lib/trpc/routers/workspaces/procedures/init.ts index 17526759e7b..2846c552804 100644 --- a/apps/desktop/src/lib/trpc/routers/workspaces/procedures/init.ts +++ b/apps/desktop/src/lib/trpc/routers/workspaces/procedures/init.ts @@ -1,4 +1,6 @@ +import { settings } from "@superset/local-db"; import { observable } from "@trpc/server/observable"; +import { localDb } from "main/lib/local-db"; import { workspaceInitManager } from "main/lib/workspace-init-manager"; import type { WorkspaceInitProgress } from "shared/types/workspace-init"; import { z } from "zod"; @@ -7,12 +9,15 @@ import { getProject, getWorkspaceWithRelations } from "../utils/db-helpers"; import { loadSetupConfig } from "../utils/setup"; import { initializeWorkspaceWorktree } from "../utils/workspace-init"; +function getDefaultPreset() { + const row = localDb.select().from(settings).get(); + if (!row) return null; + const presets = row.terminalPresets ?? []; + return presets.find((p) => p.isDefault) ?? null; +} + export const createInitProcedures = () => { return router({ - /** - * Subscribe to workspace initialization progress events. - * Streams progress updates for workspaces that are currently initializing. - */ onInitProgress: publicProcedure .input( z.object({ workspaceIds: z.array(z.string()).optional() }).optional(), @@ -29,7 +34,6 @@ export const createInitProcedures = () => { emit.next(progress); }; - // Send current state for initializing/failed workspaces for (const progress of workspaceInitManager.getAllProgress()) { if ( !input?.workspaceIds || @@ -47,10 +51,6 @@ export const createInitProcedures = () => { }); }), - /** - * Retry initialization for a failed workspace. - * Clears the failed state and restarts the initialization process. - */ retryInit: publicProcedure .input(z.object({ workspaceId: z.string() })) .mutation(async ({ input }) => { @@ -79,9 +79,7 @@ export const createInitProcedures = () => { workspaceInitManager.clearJob(input.workspaceId); workspaceInitManager.startJob(input.workspaceId, workspace.projectId); - // Run initialization in background (DO NOT await) - // On retry, the worktree.baseBranch is already correct (either originally explicit - // or auto-corrected by P1 fix), so we treat it as explicit to prevent further updates + // baseBranch is treated as explicit on retry to prevent further auto-correction initializeWorkspaceWorktree({ workspaceId: input.workspaceId, projectId: workspace.projectId, @@ -96,21 +94,12 @@ export const createInitProcedures = () => { return { success: true }; }), - /** - * Get current initialization progress for a workspace. - * Returns null if the workspace is not initializing. - */ getInitProgress: publicProcedure .input(z.object({ workspaceId: z.string() })) .query(({ input }) => { return workspaceInitManager.getProgress(input.workspaceId) ?? null; }), - /** - * Get setup commands for a workspace. - * Used as a fallback when pending terminal setup data is lost (e.g., after retry or app restart). - * Re-reads the project config to get fresh commands. - */ getSetupCommands: publicProcedure .input(z.object({ workspaceId: z.string() })) .query(({ input }) => { @@ -127,10 +116,12 @@ export const createInitProcedures = () => { } const setupConfig = loadSetupConfig(project.mainRepoPath); + const defaultPreset = getDefaultPreset(); return { projectId: project.id, initialCommands: setupConfig?.setup ?? null, + defaultPreset, }; }), }); diff --git a/apps/desktop/src/renderer/react-query/workspaces/useCreateBranchWorkspace.ts b/apps/desktop/src/renderer/react-query/workspaces/useCreateBranchWorkspace.ts index 18575c3c5e1..468761474a1 100644 --- a/apps/desktop/src/renderer/react-query/workspaces/useCreateBranchWorkspace.ts +++ b/apps/desktop/src/renderer/react-query/workspaces/useCreateBranchWorkspace.ts @@ -1,13 +1,9 @@ import { useNavigate } from "@tanstack/react-router"; import { electronTrpc } from "renderer/lib/electron-trpc"; import { navigateToWorkspace } from "renderer/routes/_authenticated/_dashboard/utils/workspace-navigation"; -import { useTabsStore } from "renderer/stores/tabs/store"; +import { useWorkspaceInitStore } from "renderer/stores/workspace-init"; +import type { WorkspaceInitProgress } from "shared/types/workspace-init"; -/** - * Mutation hook for creating a new branch workspace - * Automatically invalidates all workspace queries on success - * Adds a tab for newly created workspaces (not existing ones) - */ export function useCreateBranchWorkspace( options?: Parameters< typeof electronTrpc.workspaces.createBranchWorkspace.useMutation @@ -15,24 +11,47 @@ export function useCreateBranchWorkspace( ) { const navigate = useNavigate(); const utils = electronTrpc.useUtils(); + const addPendingTerminalSetup = useWorkspaceInitStore( + (s) => s.addPendingTerminalSetup, + ); + const updateProgress = useWorkspaceInitStore((s) => s.updateProgress); return electronTrpc.workspaces.createBranchWorkspace.useMutation({ ...options, onSuccess: async (data, ...rest) => { - // Auto-invalidate all workspace queries await utils.workspaces.invalidate(); - // Only add a tab for newly created workspaces (not existing ones being activated) - // The store's addTab is idempotent, so duplicate calls are safe if (!data.wasExisting) { - useTabsStore.getState().addTab(data.workspace.id); + let setupData = null; + try { + setupData = await utils.workspaces.getSetupCommands.fetch({ + workspaceId: data.workspace.id, + }); + } catch (error) { + console.error( + "[useCreateBranchWorkspace] Failed to fetch setup commands:", + error, + ); + } + + addPendingTerminalSetup({ + workspaceId: data.workspace.id, + projectId: data.projectId, + initialCommands: setupData?.initialCommands ?? null, + defaultPreset: setupData?.defaultPreset ?? null, + }); + + // Branch workspaces skip git init, so mark ready immediately to trigger terminal setup + const readyProgress: WorkspaceInitProgress = { + workspaceId: data.workspace.id, + projectId: data.projectId, + step: "ready", + message: "Ready", + }; + updateProgress(readyProgress); } - // Navigate to the workspace - // Branch workspaces don't need async initialization, so always navigate navigateToWorkspace(data.workspace.id, navigate); - - // Call user's onSuccess if provided await options?.onSuccess?.(data, ...rest); }, }); diff --git a/apps/desktop/src/renderer/react-query/workspaces/useCreateWorkspace.ts b/apps/desktop/src/renderer/react-query/workspaces/useCreateWorkspace.ts index 92cf57031e2..0ce161cce51 100644 --- a/apps/desktop/src/renderer/react-query/workspaces/useCreateWorkspace.ts +++ b/apps/desktop/src/renderer/react-query/workspaces/useCreateWorkspace.ts @@ -9,24 +9,9 @@ type MutationOptions = Parameters< >[0]; interface UseCreateWorkspaceOptions extends NonNullable { - /** Skip auto-navigation to new workspace (useful for MCP/agent commands) */ skipNavigation?: boolean; } -/** - * Mutation hook for creating a new workspace - * Automatically invalidates all workspace queries on success - * - * For worktree workspaces with async initialization: - * - Returns immediately after workspace record is created - * - Terminal tab is created by WorkspaceInitEffects when initialization completes - * - * For branch workspaces (no async init): - * - Terminal setup is triggered immediately via WorkspaceInitEffects - * - * Note: Terminal creation is handled by WorkspaceInitEffects (always mounted in MainScreen) - * to survive dialog unmounts. This hook just adds to the global pending store. - */ export function useCreateWorkspace(options?: UseCreateWorkspaceOptions) { const navigate = useNavigate(); const utils = electronTrpc.useUtils(); @@ -38,9 +23,7 @@ export function useCreateWorkspace(options?: UseCreateWorkspaceOptions) { return electronTrpc.workspaces.create.useMutation({ ...options, onSuccess: async (data, ...rest) => { - // CRITICAL: Set optimistic progress BEFORE invalidation AND navigation - // to ensure isInitializing is true when workspace page first renders, - // preventing the "Setup incomplete" flash. + // Set optimistic progress before navigation to prevent "Setup incomplete" flash if (data.isInitializing) { const optimisticProgress: WorkspaceInitProgress = { workspaceId: data.workspace.id, @@ -51,29 +34,18 @@ export function useCreateWorkspace(options?: UseCreateWorkspaceOptions) { updateProgress(optimisticProgress); } - // Add to global pending store (WorkspaceInitEffects will handle terminal creation) - // This survives dialog unmounts since it's stored in Zustand, not a hook-local ref addPendingTerminalSetup({ workspaceId: data.workspace.id, projectId: data.projectId, initialCommands: data.initialCommands, }); - // Auto-invalidate all workspace queries await utils.workspaces.invalidate(); - // Handle race condition: if init already completed before we added to pending, - // WorkspaceInitEffects will process it on next render when it sees the progress - // is already "ready" and there's a matching pending setup. - - // Navigate to the new workspace immediately (unless skipNavigation is set) - // The workspace exists in DB, so it's safe to navigate - // Git operations happen in background with progress shown via toast if (!options?.skipNavigation) { navigateToWorkspace(data.workspace.id, navigate); } - // Call user's onSuccess if provided await options?.onSuccess?.(data, ...rest); }, }); diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/TerminalSettings.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/TerminalSettings.tsx index b7ef342bb51..76c10962987 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/TerminalSettings.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/terminal/components/TerminalSettings/TerminalSettings.tsx @@ -40,7 +40,10 @@ import { PRESET_COLUMNS, type PresetColumnKey, } from "renderer/routes/_authenticated/settings/presets/types"; -import { DEFAULT_TERMINAL_PERSISTENCE } from "shared/constants"; +import { + DEFAULT_AUTO_APPLY_DEFAULT_PRESET, + DEFAULT_TERMINAL_PERSISTENCE, +} from "shared/constants"; import { isItemVisible, SETTING_ITEM_ID, @@ -121,6 +124,10 @@ export function TerminalSettings({ visibleItems }: TerminalSettingsProps) { SETTING_ITEM_ID.TERMINAL_QUICK_ADD, visibleItems, ); + const showAutoApplyPreset = isItemVisible( + SETTING_ITEM_ID.TERMINAL_AUTO_APPLY_PRESET, + visibleItems, + ); const showPersistence = isItemVisible( SETTING_ITEM_ID.TERMINAL_PERSISTENCE, visibleItems, @@ -421,6 +428,35 @@ export function TerminalSettings({ visibleItems }: TerminalSettingsProps) { }); }; + // Auto-apply default preset setting + const { data: autoApplyDefaultPreset, isLoading: isLoadingAutoApply } = + electronTrpc.settings.getAutoApplyDefaultPreset.useQuery(); + + const setAutoApplyDefaultPreset = + electronTrpc.settings.setAutoApplyDefaultPreset.useMutation({ + onMutate: async ({ enabled }) => { + await utils.settings.getAutoApplyDefaultPreset.cancel(); + const previous = utils.settings.getAutoApplyDefaultPreset.getData(); + utils.settings.getAutoApplyDefaultPreset.setData(undefined, enabled); + return { previous }; + }, + onError: (_err, _vars, context) => { + if (context?.previous !== undefined) { + utils.settings.getAutoApplyDefaultPreset.setData( + undefined, + context.previous, + ); + } + }, + onSettled: () => { + utils.settings.getAutoApplyDefaultPreset.invalidate(); + }, + }); + + const handleAutoApplyToggle = (enabled: boolean) => { + setAutoApplyDefaultPreset.mutate({ enabled }); + }; + const killAllDaemonSessions = electronTrpc.terminal.killAllDaemonSessions.useMutation({ onMutate: async () => { @@ -661,13 +697,46 @@ export function TerminalSettings({ visibleItems }: TerminalSettingsProps) { )} - {showPersistence && ( + {showAutoApplyPreset && (
+
+ +

+ Automatically apply your default preset when creating new + workspaces +

+
+ +
+ )} + + {showPersistence && ( +