Skip to content
21 changes: 21 additions & 0 deletions apps/desktop/src/lib/trpc/routers/settings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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();
Expand Down
33 changes: 12 additions & 21 deletions apps/desktop/src/lib/trpc/routers/workspaces/procedures/init.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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(),
Expand All @@ -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 ||
Expand All @@ -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 }) => {
Expand Down Expand Up @@ -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,
Expand All @@ -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 }) => {
Expand All @@ -127,10 +116,12 @@ export const createInitProcedures = () => {
}

const setupConfig = loadSetupConfig(project.mainRepoPath);
const defaultPreset = getDefaultPreset();

return {
projectId: project.id,
initialCommands: setupConfig?.setup ?? null,
defaultPreset,
};
}),
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,38 +1,57 @@
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
>[0],
) {
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);
},
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,9 @@ type MutationOptions = Parameters<
>[0];

interface UseCreateWorkspaceOptions extends NonNullable<MutationOptions> {
/** 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();
Expand All @@ -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,
Expand All @@ -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);
},
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -661,13 +697,46 @@ export function TerminalSettings({ visibleItems }: TerminalSettingsProps) {
</div>
)}

{showPersistence && (
{showAutoApplyPreset && (
<div
className={
showPresets || showQuickAdd
? "flex items-center justify-between pt-6 border-t"
: "flex items-center justify-between"
}
>
<div className="space-y-0.5">
<Label
htmlFor="auto-apply-preset"
className="text-sm font-medium"
>
Auto-apply default preset
</Label>
<p className="text-xs text-muted-foreground">
Automatically apply your default preset when creating new
workspaces
</p>
</div>
<Switch
id="auto-apply-preset"
checked={
autoApplyDefaultPreset ?? DEFAULT_AUTO_APPLY_DEFAULT_PRESET
}
onCheckedChange={handleAutoApplyToggle}
disabled={
isLoadingAutoApply || setAutoApplyDefaultPreset.isPending
}
/>
</div>
)}

{showPersistence && (
<div
className={
showPresets || showQuickAdd || showAutoApplyPreset
? "flex items-center justify-between pt-6 border-t"
: "flex items-center justify-between"
}
>
<div className="space-y-0.5">
<Label
Expand Down Expand Up @@ -701,7 +770,10 @@ export function TerminalSettings({ visibleItems }: TerminalSettingsProps) {
{showLinkBehavior && (
<div
className={
showPersistence || showPresets || showQuickAdd
showPersistence ||
showPresets ||
showQuickAdd ||
showAutoApplyPreset
? "flex items-center justify-between pt-6 border-t"
: "flex items-center justify-between"
}
Expand Down
Loading