From 240b96a142d25846be27ab6621a967c992889e18 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Sun, 25 Jan 2026 16:59:02 -0800 Subject: [PATCH 1/8] feat(desktop): apply default terminal preset when creating workspaces When creating a new workspace, the default terminal preset is now applied alongside any project setup scripts. This handles four cases: - Both setup script and default preset: creates separate terminal tabs - Only setup script: creates terminal with setup commands - Only default preset: creates terminal from preset (with parallel support) - Neither: shows config toast as before --- .../routers/workspaces/procedures/init.ts | 13 +- .../workspaces/useCreateBranchWorkspace.ts | 41 +++++- .../workspaces/useCreateWorkspace.ts | 5 + .../main/components/WorkspaceInitEffects.tsx | 123 ++++++++++++++---- .../src/renderer/stores/workspace-init.ts | 2 + 5 files changed, 155 insertions(+), 29 deletions(-) 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..06221dfacdf 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,6 +9,13 @@ 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({ /** @@ -109,7 +118,7 @@ export const createInitProcedures = () => { /** * 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. + * Re-reads the project config to get fresh commands and includes the default preset. */ getSetupCommands: publicProcedure .input(z.object({ workspaceId: z.string() })) @@ -127,10 +136,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..a9ab4cbcca2 100644 --- a/apps/desktop/src/renderer/react-query/workspaces/useCreateBranchWorkspace.ts +++ b/apps/desktop/src/renderer/react-query/workspaces/useCreateBranchWorkspace.ts @@ -1,12 +1,13 @@ 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) + * Routes through WorkspaceInitEffects for terminal setup (including default preset) */ export function useCreateBranchWorkspace( options?: Parameters< @@ -15,6 +16,14 @@ export function useCreateBranchWorkspace( ) { const navigate = useNavigate(); const utils = electronTrpc.useUtils(); + const addPendingTerminalSetup = useWorkspaceInitStore( + (s) => s.addPendingTerminalSetup, + ); + const updateProgress = useWorkspaceInitStore((s) => s.updateProgress); + + // Query default preset to include in terminal setup + const { data: defaultPreset } = + electronTrpc.settings.getDefaultPreset.useQuery(); return electronTrpc.workspaces.createBranchWorkspace.useMutation({ ...options, @@ -22,14 +31,34 @@ export function useCreateBranchWorkspace( // 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 + // For newly created branch workspaces, route through WorkspaceInitEffects + // for terminal setup (including setup script and default preset) if (!data.wasExisting) { - useTabsStore.getState().addTab(data.workspace.id); + // Fetch setup commands from backend + const setupData = await utils.workspaces.getSetupCommands.fetch({ + workspaceId: data.workspace.id, + }); + + // Add to pending terminal setups + addPendingTerminalSetup({ + workspaceId: data.workspace.id, + projectId: data.projectId, + initialCommands: setupData?.initialCommands ?? null, + defaultPreset: defaultPreset ?? setupData?.defaultPreset ?? null, + }); + + // Set synthetic "ready" progress immediately (branch workspaces don't need git init) + // This triggers WorkspaceInitEffects to process the 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 diff --git a/apps/desktop/src/renderer/react-query/workspaces/useCreateWorkspace.ts b/apps/desktop/src/renderer/react-query/workspaces/useCreateWorkspace.ts index 92cf57031e2..1559b0f0276 100644 --- a/apps/desktop/src/renderer/react-query/workspaces/useCreateWorkspace.ts +++ b/apps/desktop/src/renderer/react-query/workspaces/useCreateWorkspace.ts @@ -35,6 +35,10 @@ export function useCreateWorkspace(options?: UseCreateWorkspaceOptions) { ); const updateProgress = useWorkspaceInitStore((s) => s.updateProgress); + // Query default preset to include in terminal setup + const { data: defaultPreset } = + electronTrpc.settings.getDefaultPreset.useQuery(); + return electronTrpc.workspaces.create.useMutation({ ...options, onSuccess: async (data, ...rest) => { @@ -57,6 +61,7 @@ export function useCreateWorkspace(options?: UseCreateWorkspaceOptions) { workspaceId: data.workspace.id, projectId: data.projectId, initialCommands: data.initialCommands, + defaultPreset: defaultPreset ?? null, }); // Auto-invalidate all workspace queries diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceInitEffects.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceInitEffects.tsx index 4480d91e609..656e167c6ca 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceInitEffects.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceInitEffects.tsx @@ -3,6 +3,7 @@ import { useCallback, useEffect, useRef } from "react"; import { electronTrpc } from "renderer/lib/electron-trpc"; import { useOpenConfigModal } from "renderer/stores/config-modal"; import { useTabsStore } from "renderer/stores/tabs/store"; +import type { AddTabWithMultiplePanesOptions } from "renderer/stores/tabs/types"; import { type PendingTerminalSetup, useWorkspaceInitStore, @@ -34,20 +35,92 @@ export function WorkspaceInitEffects() { const processingRef = useRef>(new Set()); const addTab = useTabsStore((state) => state.addTab); + const addTabWithMultiplePanes = useTabsStore( + (state) => state.addTabWithMultiplePanes, + ); const setTabAutoTitle = useTabsStore((state) => state.setTabAutoTitle); + const renameTab = useTabsStore((state) => state.renameTab); const createOrAttach = electronTrpc.terminal.createOrAttach.useMutation(); const openConfigModal = useOpenConfigModal(); const dismissConfigToast = electronTrpc.config.dismissConfigToast.useMutation(); const utils = electronTrpc.useUtils(); - // Helper to create terminal with setup commands + // Helper to create terminal tab for a preset + const createPresetTerminal = useCallback( + ( + workspaceId: string, + preset: NonNullable, + ) => { + const isParallel = + preset.executionMode === "parallel" && preset.commands.length > 1; + + if (isParallel) { + const options: AddTabWithMultiplePanesOptions = { + commands: preset.commands, + initialCwd: preset.cwd || undefined, + }; + const { tabId } = addTabWithMultiplePanes(workspaceId, options); + renameTab(tabId, preset.name); + } else { + const { tabId } = addTab(workspaceId, { + initialCommands: preset.commands, + initialCwd: preset.cwd || undefined, + }); + renameTab(tabId, preset.name); + } + }, + [addTab, addTabWithMultiplePanes, renameTab], + ); + + // Helper to create terminal with setup commands and/or default preset const handleTerminalSetup = useCallback( (setup: PendingTerminalSetup, onComplete: () => void) => { - if ( + const hasSetupScript = Array.isArray(setup.initialCommands) && - setup.initialCommands.length > 0 - ) { + setup.initialCommands.length > 0; + const hasDefaultPreset = + setup.defaultPreset != null && setup.defaultPreset.commands.length > 0; + + // CASE 1: Both setup script AND default preset - create separate terminals + if (hasSetupScript && hasDefaultPreset && setup.defaultPreset) { + // Create setup script terminal + const { tabId: setupTabId, paneId: setupPaneId } = addTab( + setup.workspaceId, + ); + setTabAutoTitle(setupTabId, "Workspace Setup"); + + // Create preset terminal + createPresetTerminal(setup.workspaceId, setup.defaultPreset); + + // Start the setup script terminal + createOrAttach.mutate( + { + paneId: setupPaneId, + tabId: setupTabId, + workspaceId: setup.workspaceId, + initialCommands: setup.initialCommands ?? undefined, + }, + { + onSuccess: () => onComplete(), + onError: (error) => { + console.error( + "[WorkspaceInitEffects] Failed to create terminal:", + error, + ); + toast.error("Failed to create terminal", { + description: + error.message || "Terminal setup failed. Please try again.", + }); + onComplete(); + }, + }, + ); + return; + } + + // CASE 2: Only setup script (no default preset) + if (hasSetupScript) { const { tabId, paneId } = addTab(setup.workspaceId); setTabAutoTitle(tabId, "Workspace Setup"); createOrAttach.mutate( @@ -55,12 +128,10 @@ export function WorkspaceInitEffects() { paneId, tabId, workspaceId: setup.workspaceId, - initialCommands: setup.initialCommands, + initialCommands: setup.initialCommands ?? undefined, }, { - onSuccess: () => { - onComplete(); - }, + onSuccess: () => onComplete(), onError: (error) => { console.error( "[WorkspaceInitEffects] Failed to create terminal:", @@ -72,7 +143,6 @@ export function WorkspaceInitEffects() { action: { label: "Open Terminal", onClick: () => { - // Allow user to manually trigger terminal creation const { tabId: newTabId, paneId: newPaneId } = addTab( setup.workspaceId, ); @@ -85,25 +155,32 @@ export function WorkspaceInitEffects() { }, }, }); - // Still complete to prevent infinite retries onComplete(); }, }, ); - } else { - // Show config toast if no setup commands - toast.info("No setup script configured", { - description: "Automate workspace setup with a config.json file", - action: { - label: "Configure", - onClick: () => openConfigModal(setup.projectId), - }, - onDismiss: () => { - dismissConfigToast.mutate({ projectId: setup.projectId }); - }, - }); + return; + } + + // CASE 3: Only default preset (no setup script) + if (setup.defaultPreset && setup.defaultPreset.commands.length > 0) { + createPresetTerminal(setup.workspaceId, setup.defaultPreset); onComplete(); + return; } + + // CASE 4: Neither setup script nor default preset - show config toast + toast.info("No setup script configured", { + description: "Automate workspace setup with a config.json file", + action: { + label: "Configure", + onClick: () => openConfigModal(setup.projectId), + }, + onDismiss: () => { + dismissConfigToast.mutate({ projectId: setup.projectId }); + }, + }); + onComplete(); }, [ addTab, @@ -111,6 +188,7 @@ export function WorkspaceInitEffects() { createOrAttach, openConfigModal, dismissConfigToast, + createPresetTerminal, ], ); @@ -176,6 +254,7 @@ export function WorkspaceInitEffects() { workspaceId, projectId: setupData.projectId, initialCommands: setupData.initialCommands, + defaultPreset: setupData.defaultPreset, }; handleTerminalSetup(fetchedSetup, () => { diff --git a/apps/desktop/src/renderer/stores/workspace-init.ts b/apps/desktop/src/renderer/stores/workspace-init.ts index b3f4d30f5c6..8ec1b800360 100644 --- a/apps/desktop/src/renderer/stores/workspace-init.ts +++ b/apps/desktop/src/renderer/stores/workspace-init.ts @@ -1,3 +1,4 @@ +import type { TerminalPreset } from "@superset/local-db"; import type { WorkspaceInitProgress } from "shared/types/workspace-init"; import { create } from "zustand"; import { devtools } from "zustand/middleware"; @@ -10,6 +11,7 @@ export interface PendingTerminalSetup { workspaceId: string; projectId: string; initialCommands: string[] | null; + defaultPreset?: TerminalPreset | null; } interface WorkspaceInitState { From 5b2ee6724fbfacaaa38ef03e4bf6476e6d75cda1 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Sun, 25 Jan 2026 17:02:35 -0800 Subject: [PATCH 2/8] chore: remove redundant comments and simplify code --- .../routers/workspaces/procedures/init.ts | 22 +--------- .../workspaces/useCreateBranchWorkspace.ts | 18 +------- .../workspaces/useCreateWorkspace.ts | 32 +-------------- .../main/components/WorkspaceInitEffects.tsx | 41 ++----------------- .../src/renderer/stores/workspace-init.ts | 11 ----- 5 files changed, 7 insertions(+), 117 deletions(-) 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 06221dfacdf..2846c552804 100644 --- a/apps/desktop/src/lib/trpc/routers/workspaces/procedures/init.ts +++ b/apps/desktop/src/lib/trpc/routers/workspaces/procedures/init.ts @@ -18,10 +18,6 @@ function getDefaultPreset() { 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(), @@ -38,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 || @@ -56,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 }) => { @@ -88,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, @@ -105,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 and includes the default preset. - */ getSetupCommands: publicProcedure .input(z.object({ workspaceId: z.string() })) .query(({ input }) => { diff --git a/apps/desktop/src/renderer/react-query/workspaces/useCreateBranchWorkspace.ts b/apps/desktop/src/renderer/react-query/workspaces/useCreateBranchWorkspace.ts index a9ab4cbcca2..d89e56c1d73 100644 --- a/apps/desktop/src/renderer/react-query/workspaces/useCreateBranchWorkspace.ts +++ b/apps/desktop/src/renderer/react-query/workspaces/useCreateBranchWorkspace.ts @@ -4,11 +4,6 @@ import { navigateToWorkspace } from "renderer/routes/_authenticated/_dashboard/u 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 - * Routes through WorkspaceInitEffects for terminal setup (including default preset) - */ export function useCreateBranchWorkspace( options?: Parameters< typeof electronTrpc.workspaces.createBranchWorkspace.useMutation @@ -20,26 +15,19 @@ export function useCreateBranchWorkspace( (s) => s.addPendingTerminalSetup, ); const updateProgress = useWorkspaceInitStore((s) => s.updateProgress); - - // Query default preset to include in terminal setup const { data: defaultPreset } = electronTrpc.settings.getDefaultPreset.useQuery(); return electronTrpc.workspaces.createBranchWorkspace.useMutation({ ...options, onSuccess: async (data, ...rest) => { - // Auto-invalidate all workspace queries await utils.workspaces.invalidate(); - // For newly created branch workspaces, route through WorkspaceInitEffects - // for terminal setup (including setup script and default preset) if (!data.wasExisting) { - // Fetch setup commands from backend const setupData = await utils.workspaces.getSetupCommands.fetch({ workspaceId: data.workspace.id, }); - // Add to pending terminal setups addPendingTerminalSetup({ workspaceId: data.workspace.id, projectId: data.projectId, @@ -47,8 +35,7 @@ export function useCreateBranchWorkspace( defaultPreset: defaultPreset ?? setupData?.defaultPreset ?? null, }); - // Set synthetic "ready" progress immediately (branch workspaces don't need git init) - // This triggers WorkspaceInitEffects to process the terminal setup + // Branch workspaces skip git init, so mark ready immediately to trigger terminal setup const readyProgress: WorkspaceInitProgress = { workspaceId: data.workspace.id, projectId: data.projectId, @@ -58,10 +45,7 @@ export function useCreateBranchWorkspace( updateProgress(readyProgress); } - // Navigate to the workspace 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 1559b0f0276..0b4d4f5967e 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(); @@ -34,17 +19,13 @@ export function useCreateWorkspace(options?: UseCreateWorkspaceOptions) { (s) => s.addPendingTerminalSetup, ); const updateProgress = useWorkspaceInitStore((s) => s.updateProgress); - - // Query default preset to include in terminal setup const { data: defaultPreset } = electronTrpc.settings.getDefaultPreset.useQuery(); 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, @@ -55,8 +36,6 @@ 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, @@ -64,21 +43,12 @@ export function useCreateWorkspace(options?: UseCreateWorkspaceOptions) { defaultPreset: defaultPreset ?? null, }); - // 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/screens/main/components/WorkspaceInitEffects.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceInitEffects.tsx index 656e167c6ca..6a7a3f7da13 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceInitEffects.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceInitEffects.tsx @@ -10,16 +10,8 @@ import { } from "renderer/stores/workspace-init"; /** - * Renderless component that handles terminal setup when workspaces become ready. - * - * This is mounted at the app root (MainScreen) so it survives dialog unmounts. - * When a workspace creation is initiated from a dialog (e.g., InitGitDialog, - * CloneRepoDialog), the dialog may close before initialization completes. - * This component ensures the terminal is still created when the workspace - * becomes ready. - * - * Also handles the case where pending setup data is lost (e.g., after retry - * or app restart) by fetching setup commands from the backend on demand. + * Handles terminal setup when workspaces become ready. + * Mounted at app root to survive dialog unmounts. */ export function WorkspaceInitEffects() { const initProgress = useWorkspaceInitStore((s) => s.initProgress); @@ -31,7 +23,6 @@ export function WorkspaceInitEffects() { ); const clearProgress = useWorkspaceInitStore((s) => s.clearProgress); - // Track which setups are currently being processed to prevent duplicate handling const processingRef = useRef>(new Set()); const addTab = useTabsStore((state) => state.addTab); @@ -46,7 +37,6 @@ export function WorkspaceInitEffects() { electronTrpc.config.dismissConfigToast.useMutation(); const utils = electronTrpc.useUtils(); - // Helper to create terminal tab for a preset const createPresetTerminal = useCallback( ( workspaceId: string, @@ -73,7 +63,6 @@ export function WorkspaceInitEffects() { [addTab, addTabWithMultiplePanes, renameTab], ); - // Helper to create terminal with setup commands and/or default preset const handleTerminalSetup = useCallback( (setup: PendingTerminalSetup, onComplete: () => void) => { const hasSetupScript = @@ -82,18 +71,13 @@ export function WorkspaceInitEffects() { const hasDefaultPreset = setup.defaultPreset != null && setup.defaultPreset.commands.length > 0; - // CASE 1: Both setup script AND default preset - create separate terminals if (hasSetupScript && hasDefaultPreset && setup.defaultPreset) { - // Create setup script terminal const { tabId: setupTabId, paneId: setupPaneId } = addTab( setup.workspaceId, ); setTabAutoTitle(setupTabId, "Workspace Setup"); - - // Create preset terminal createPresetTerminal(setup.workspaceId, setup.defaultPreset); - // Start the setup script terminal createOrAttach.mutate( { paneId: setupPaneId, @@ -119,7 +103,6 @@ export function WorkspaceInitEffects() { return; } - // CASE 2: Only setup script (no default preset) if (hasSetupScript) { const { tabId, paneId } = addTab(setup.workspaceId); setTabAutoTitle(tabId, "Workspace Setup"); @@ -162,14 +145,12 @@ export function WorkspaceInitEffects() { return; } - // CASE 3: Only default preset (no setup script) if (setup.defaultPreset && setup.defaultPreset.commands.length > 0) { createPresetTerminal(setup.workspaceId, setup.defaultPreset); onComplete(); return; } - // CASE 4: Neither setup script nor default preset - show config toast toast.info("No setup script configured", { description: "Automate workspace setup with a config.json file", action: { @@ -193,63 +174,51 @@ export function WorkspaceInitEffects() { ); useEffect(() => { - // Process pending setups that have reached ready state for (const [workspaceId, setup] of Object.entries(pendingTerminalSetups)) { const progress = initProgress[workspaceId]; - // Skip if already being processed if (processingRef.current.has(workspaceId)) { continue; } - // Create terminal when workspace becomes ready if (progress?.step === "ready") { - // Mark as processing to prevent duplicate handling processingRef.current.add(workspaceId); handleTerminalSetup(setup, () => { - // Only remove from pending after successful handling removePendingTerminalSetup(workspaceId); clearProgress(workspaceId); processingRef.current.delete(workspaceId); }); } - // Clean up pending if failed (user will use retry or delete) - // Note: losing pending data is OK now - we fetch on demand when ready if (progress?.step === "failed") { removePendingTerminalSetup(workspaceId); } } - // Handle workspaces that became ready without pending setup data - // (e.g., after retry or app restart during init) + // Handle workspaces that became ready without pending setup data (after retry or app restart) for (const [workspaceId, progress] of Object.entries(initProgress)) { - // Only process ready workspaces that don't have pending setup if (progress.step !== "ready") { continue; } if (pendingTerminalSetups[workspaceId]) { - continue; // Already handled above + continue; } if (processingRef.current.has(workspaceId)) { continue; } - // Mark as processing and fetch setup commands from backend processingRef.current.add(workspaceId); utils.workspaces.getSetupCommands .fetch({ workspaceId }) .then((setupData) => { if (!setupData) { - // Workspace not found or no project - just clear progress clearProgress(workspaceId); processingRef.current.delete(workspaceId); return; } - // Create a pending setup from fetched data and handle it const fetchedSetup: PendingTerminalSetup = { workspaceId, projectId: setupData.projectId, @@ -267,7 +236,6 @@ export function WorkspaceInitEffects() { "[WorkspaceInitEffects] Failed to fetch setup commands:", error, ); - // Still clear progress to avoid being stuck clearProgress(workspaceId); processingRef.current.delete(workspaceId); }); @@ -281,6 +249,5 @@ export function WorkspaceInitEffects() { utils.workspaces.getSetupCommands, ]); - // Renderless component return null; } diff --git a/apps/desktop/src/renderer/stores/workspace-init.ts b/apps/desktop/src/renderer/stores/workspace-init.ts index 8ec1b800360..25fb88337e4 100644 --- a/apps/desktop/src/renderer/stores/workspace-init.ts +++ b/apps/desktop/src/renderer/stores/workspace-init.ts @@ -3,10 +3,6 @@ import type { WorkspaceInitProgress } from "shared/types/workspace-init"; import { create } from "zustand"; import { devtools } from "zustand/middleware"; -/** - * Data needed to create a terminal when workspace becomes ready. - * Stored globally so it survives dialog/hook unmounts. - */ export interface PendingTerminalSetup { workspaceId: string; projectId: string; @@ -15,13 +11,8 @@ export interface PendingTerminalSetup { } interface WorkspaceInitState { - // Map of workspaceId -> progress initProgress: Record; - - // Map of workspaceId -> pending terminal setup (survives dialog unmount) pendingTerminalSetups: Record; - - // Actions updateProgress: (progress: WorkspaceInitProgress) => void; clearProgress: (workspaceId: string) => void; addPendingTerminalSetup: (setup: PendingTerminalSetup) => void; @@ -42,8 +33,6 @@ export const useWorkspaceInitStore = create()( }, })); - // For memory hygiene, clear "ready" progress after 5 minutes - // (long enough that WorkspaceInitEffects will have processed it) if (progress.step === "ready") { setTimeout( () => { From cf3cf2903b68af18692545fa3b733dc26a7eb5c4 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Sun, 25 Jan 2026 17:41:24 -0800 Subject: [PATCH 3/8] fix(desktop): fetch default preset from backend to avoid race condition When creating workspaces, the client-side useQuery for default preset may not have resolved yet, causing the preset to be stored as null and never applied. Now we always fetch from getSetupCommands when the preset is undefined in pending setup, ensuring it's applied even on first-run or slow settings load. --- .../workspaces/useCreateBranchWorkspace.ts | 4 +- .../workspaces/useCreateWorkspace.ts | 3 -- .../main/components/WorkspaceInitEffects.tsx | 39 ++++++++++++++++--- 3 files changed, 35 insertions(+), 11 deletions(-) diff --git a/apps/desktop/src/renderer/react-query/workspaces/useCreateBranchWorkspace.ts b/apps/desktop/src/renderer/react-query/workspaces/useCreateBranchWorkspace.ts index d89e56c1d73..d88f7b5232a 100644 --- a/apps/desktop/src/renderer/react-query/workspaces/useCreateBranchWorkspace.ts +++ b/apps/desktop/src/renderer/react-query/workspaces/useCreateBranchWorkspace.ts @@ -15,8 +15,6 @@ export function useCreateBranchWorkspace( (s) => s.addPendingTerminalSetup, ); const updateProgress = useWorkspaceInitStore((s) => s.updateProgress); - const { data: defaultPreset } = - electronTrpc.settings.getDefaultPreset.useQuery(); return electronTrpc.workspaces.createBranchWorkspace.useMutation({ ...options, @@ -32,7 +30,7 @@ export function useCreateBranchWorkspace( workspaceId: data.workspace.id, projectId: data.projectId, initialCommands: setupData?.initialCommands ?? null, - defaultPreset: defaultPreset ?? setupData?.defaultPreset ?? null, + defaultPreset: setupData?.defaultPreset ?? null, }); // Branch workspaces skip git init, so mark ready immediately to trigger terminal setup diff --git a/apps/desktop/src/renderer/react-query/workspaces/useCreateWorkspace.ts b/apps/desktop/src/renderer/react-query/workspaces/useCreateWorkspace.ts index 0b4d4f5967e..0ce161cce51 100644 --- a/apps/desktop/src/renderer/react-query/workspaces/useCreateWorkspace.ts +++ b/apps/desktop/src/renderer/react-query/workspaces/useCreateWorkspace.ts @@ -19,8 +19,6 @@ export function useCreateWorkspace(options?: UseCreateWorkspaceOptions) { (s) => s.addPendingTerminalSetup, ); const updateProgress = useWorkspaceInitStore((s) => s.updateProgress); - const { data: defaultPreset } = - electronTrpc.settings.getDefaultPreset.useQuery(); return electronTrpc.workspaces.create.useMutation({ ...options, @@ -40,7 +38,6 @@ export function useCreateWorkspace(options?: UseCreateWorkspaceOptions) { workspaceId: data.workspace.id, projectId: data.projectId, initialCommands: data.initialCommands, - defaultPreset: defaultPreset ?? null, }); await utils.workspaces.invalidate(); diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceInitEffects.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceInitEffects.tsx index 6a7a3f7da13..a7377e013c1 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceInitEffects.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceInitEffects.tsx @@ -184,11 +184,40 @@ export function WorkspaceInitEffects() { if (progress?.step === "ready") { processingRef.current.add(workspaceId); - handleTerminalSetup(setup, () => { - removePendingTerminalSetup(workspaceId); - clearProgress(workspaceId); - processingRef.current.delete(workspaceId); - }); + // Always fetch from backend to ensure we have the latest preset + // (client-side preset query may not have resolved when pending setup was created) + if (setup.defaultPreset === undefined) { + utils.workspaces.getSetupCommands + .fetch({ workspaceId }) + .then((setupData) => { + const completeSetup: PendingTerminalSetup = { + ...setup, + defaultPreset: setupData?.defaultPreset ?? null, + }; + handleTerminalSetup(completeSetup, () => { + removePendingTerminalSetup(workspaceId); + clearProgress(workspaceId); + processingRef.current.delete(workspaceId); + }); + }) + .catch((error) => { + console.error( + "[WorkspaceInitEffects] Failed to fetch setup commands:", + error, + ); + handleTerminalSetup(setup, () => { + removePendingTerminalSetup(workspaceId); + clearProgress(workspaceId); + processingRef.current.delete(workspaceId); + }); + }); + } else { + handleTerminalSetup(setup, () => { + removePendingTerminalSetup(workspaceId); + clearProgress(workspaceId); + processingRef.current.delete(workspaceId); + }); + } } if (progress?.step === "failed") { From ea10ddb9bcd334f95e4a828dd8a7e2ee22ed725c Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Sun, 25 Jan 2026 17:45:04 -0800 Subject: [PATCH 4/8] feat(desktop): add setting to disable auto-applying default preset Adds a new "Auto-apply default preset" toggle in Terminal settings that controls whether the default preset is automatically applied when creating new workspaces. Enabled by default to maintain current behavior. - Add autoApplyDefaultPreset column to settings table - Add tRPC procedures for get/set - Add UI toggle in Terminal settings - Check setting in WorkspaceInitEffects before applying preset --- .../src/lib/trpc/routers/settings/index.ts | 21 + .../TerminalSettings/TerminalSettings.tsx | 78 +- .../utils/settings-search/settings-search.ts | 19 + .../main/components/WorkspaceInitEffects.tsx | 17 +- apps/desktop/src/shared/constants.ts | 1 + .../0013_add_auto_apply_default_preset.sql | 1 + .../local-db/drizzle/meta/0013_snapshot.json | 1014 +++++++++++++++++ packages/local-db/drizzle/meta/_journal.json | 7 + packages/local-db/src/schema/schema.ts | 3 + 9 files changed, 1156 insertions(+), 5 deletions(-) create mode 100644 packages/local-db/drizzle/0013_add_auto_apply_default_preset.sql create mode 100644 packages/local-db/drizzle/meta/0013_snapshot.json 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/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 && ( +